feat(gen-plugin-files): 新增 README 五個共通章節的順序漂移檢查,並把 persona 納入 AGENTS.md 產生範圍

- checkHeadingOrder():驗證五個共通章節的相對順序是否與 DEFAULT_HEADINGS 一致,不一致時報錯並指出違規的 repo 與章節,只報錯不自動搬移。
- AGENTS_APPLIES_TO 加入 persona,改用 agentsExtraBullets 機制產生其 AGENTS.md,不再是唯一排除在產生範圍外的 repo。
- 順帶修正 renderSectionForCompare() 對「檔案最後一節」結尾空行數的錯誤假設,避免內容其實相同卻被誤報「會變動」。
This commit is contained in:
2026-08-12 05:52:21 +00:00
parent 9580bb0c68
commit fc27067ddd
+46 -9
View File
@@ -139,10 +139,10 @@ function buildCodexMarketplaceJson(meta) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// AGENTS.md(只給 shared / doc / code;persona 不套用) // AGENTS.md(四個 repo 皆套用;persona 特有內容走 agentsExtraBullets,見 L1-5)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const AGENTS_APPLIES_TO = new Set(['shared', 'doc', 'code']); const AGENTS_APPLIES_TO = new Set(['shared', 'doc', 'code', 'persona']);
function buildAgentsMd(meta) { function buildAgentsMd(meta) {
const tmpl = fs.readFileSync(path.join(TEMPLATES_DIR, 'AGENTS.md.tmpl'), 'utf8'); const tmpl = fs.readFileSync(path.join(TEMPLATES_DIR, 'AGENTS.md.tmpl'), 'utf8');
@@ -170,6 +170,34 @@ function getHeadings(meta) {
return { ...DEFAULT_HEADINGS, ...(meta.readmeHeadings || {}) }; return { ...DEFAULT_HEADINGS, ...(meta.readmeHeadings || {}) };
} }
/**
* 檢查五個共通章節在該份 README 裡的**相對順序**是否與 DEFAULT_HEADINGS 的宣告順序一致
* (L1-4):只驗證順序,不驗證內容——內容正確性屬於 buildReadme() 各章節自己的職責。
* 找不到的章節(例如該 repo 沒有這節)直接跳過,不算順序錯誤。
* 順序不一致時丟出 Error,訊息帶 repo 短名與違規的章節名稱,呼叫端不吞掉這個例外,
* 讓產生器對這個 repo 的處理直接中止(依 L1-4「只報錯、不自動搬移」)。
*/
function checkHeadingOrder(shortName, currentReadmeText, headings) {
const order = Object.keys(DEFAULT_HEADINGS); // prefixTable, dirTree, install, headless, addSkill
const positions = [];
for (const key of order) {
const heading = headings[key];
const sec = findSection(currentReadmeText, heading);
if (sec === null) continue; // 該 repo 沒有這節,不納入順序比較
positions.push({ key, heading, line: sec.startLine });
}
for (let i = 1; i < positions.length; i++) {
if (positions[i].line < positions[i - 1].line) {
throw new Error(
`README 章節順序漂移(repo: ${shortName}):「## ${positions[i].heading}」(第 ${positions[i].line + 1} 行)` +
`出現在「## ${positions[i - 1].heading}」(第 ${positions[i - 1].line + 1} 行)之前,` +
`五個共通章節的相對順序須為:${order.map((k) => headings[k]).join(' → ')}。` +
`本工具只報錯、不自動搬移章節位置,請人工調整後重跑。`
);
}
}
}
/** /**
* 找出「## <heading>」章節在整份文件中的字元範圍:[sectionStart, sectionEnd)。 * 找出「## <heading>」章節在整份文件中的字元範圍:[sectionStart, sectionEnd)。
* sectionStart 指向標題行開頭;sectionEnd 指向下一個獨立一行 `---` 或下一個 `## ` 標題(不含),或檔尾。 * sectionStart 指向標題行開頭;sectionEnd 指向下一個獨立一行 `---` 或下一個 `## ` 標題(不含),或檔尾。
@@ -217,13 +245,21 @@ function replaceSection(text, heading, bodyLines) {
} }
/** /**
* 章節文字的「可比較表示」:既有章節(sec.sectionText)永遠包含結尾那一行空行(緊接在 `---` 之前), * 章節文字的「可比較表示」:一般章節後面接 `---`,`sec.sectionText` 會包含結尾那一行空行,
* 所以這裡也補上同一個結尾空行,才能跟 oldSectionText 公平比較,避免只因為少了那一行空行就誤報「有變動」。 * 所以這裡補上同一個結尾空行才能公平比較。但**檔案最後一節**(後面沒有 `---`/`## `)的
* `sec.sectionText` 直接切到檔尾,結尾空行數等於檔案實際的結尾換行數(可能是 1 行、2 行或
* 更多),不是固定 1 行——若仍然無條件補一行,會跟只有單一結尾換行的檔案產生假性「有變動」
* (已於 L1-6 驗證時發現:`replaceSection()` 實際寫出的內容其實逐字元相同,只有這裡的比較
* 基準算錯)。比較前一律把兩邊的結尾空行正規化掉,只在意內容本身是否改變。
*/ */
function renderSectionForCompare(heading, bodyLines) { function renderSectionForCompare(heading, bodyLines) {
return [`## ${heading}`, '', ...bodyLines, ''].join('\n'); return [`## ${heading}`, '', ...bodyLines, ''].join('\n');
} }
function normalizeForCompare(sectionText) {
return sectionText.replace(/\n+$/, '');
}
// ---- 章節 1/5:前綴與呼叫方式(完全模板化) ---- // ---- 章節 1/5:前綴與呼叫方式(完全模板化) ----
function buildPrefixTableBodyLines(meta) { function buildPrefixTableBodyLines(meta) {
@@ -297,6 +333,7 @@ function buildPassthroughBodyLines(currentReadmeText, heading) {
*/ */
function buildReadme(meta, currentReadmeText) { function buildReadme(meta, currentReadmeText) {
const headings = getHeadings(meta); const headings = getHeadings(meta);
checkHeadingOrder(meta.shortName, currentReadmeText, headings);
const changes = []; const changes = [];
let text = currentReadmeText; let text = currentReadmeText;
@@ -308,7 +345,7 @@ function buildReadme(meta, currentReadmeText) {
changes.push({ changes.push({
key: 'prefixTable', key: 'prefixTable',
heading: headings.prefixTable, heading: headings.prefixTable,
changed: oldSectionText !== newSectionText, changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText),
oldText: oldSectionText, oldText: oldSectionText,
newText: newSectionText, newText: newSectionText,
}); });
@@ -326,7 +363,7 @@ function buildReadme(meta, currentReadmeText) {
changes.push({ changes.push({
key: 'dirTree', key: 'dirTree',
heading: headings.dirTree, heading: headings.dirTree,
changed: oldSectionText !== newSectionText, changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText),
oldText: oldSectionText, oldText: oldSectionText,
newText: newSectionText, newText: newSectionText,
}); });
@@ -342,7 +379,7 @@ function buildReadme(meta, currentReadmeText) {
changes.push({ changes.push({
key: 'install', key: 'install',
heading: headings.install, heading: headings.install,
changed: oldSectionText !== newSectionText, changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText),
oldText: oldSectionText, oldText: oldSectionText,
newText: newSectionText, newText: newSectionText,
}); });
@@ -360,7 +397,7 @@ function buildReadme(meta, currentReadmeText) {
changes.push({ changes.push({
key: 'headless', key: 'headless',
heading: headings.headless, heading: headings.headless,
changed: oldSectionText !== newSectionText, changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText),
oldText: oldSectionText, oldText: oldSectionText,
newText: newSectionText, newText: newSectionText,
}); });
@@ -379,7 +416,7 @@ function buildReadme(meta, currentReadmeText) {
changes.push({ changes.push({
key: 'addSkill', key: 'addSkill',
heading: headings.addSkill, heading: headings.addSkill,
changed: oldSectionText !== newSectionText, changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText),
oldText: oldSectionText, oldText: oldSectionText,
newText: newSectionText, newText: newSectionText,
}); });