diff --git a/scripts/gen-plugin-files.mjs b/scripts/gen-plugin-files.mjs index d6356c5..f44724d 100644 --- a/scripts/gen-plugin-files.mjs +++ b/scripts/gen-plugin-files.mjs @@ -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) { const tmpl = fs.readFileSync(path.join(TEMPLATES_DIR, 'AGENTS.md.tmpl'), 'utf8'); @@ -170,6 +170,34 @@ function getHeadings(meta) { 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(' → ')}。` + + `本工具只報錯、不自動搬移章節位置,請人工調整後重跑。` + ); + } + } +} + /** * 找出「## 」章節在整份文件中的字元範圍:[sectionStart, sectionEnd)。 * sectionStart 指向標題行開頭;sectionEnd 指向下一個獨立一行 `---` 或下一個 `## ` 標題(不含),或檔尾。 @@ -217,13 +245,21 @@ function replaceSection(text, heading, bodyLines) { } /** - * 章節文字的「可比較表示」:既有章節(sec.sectionText)永遠包含結尾那一行空行(緊接在 `---` 之前), - * 所以這裡也補上同一個結尾空行,才能跟 oldSectionText 公平比較,避免只因為少了那一行空行就誤報「有變動」。 + * 章節文字的「可比較表示」:一般章節後面接 `---`,`sec.sectionText` 會包含結尾那一行空行, + * 所以這裡補上同一個結尾空行才能公平比較。但**檔案最後一節**(後面沒有 `---`/`## `)的 + * `sec.sectionText` 直接切到檔尾,結尾空行數等於檔案實際的結尾換行數(可能是 1 行、2 行或 + * 更多),不是固定 1 行——若仍然無條件補一行,會跟只有單一結尾換行的檔案產生假性「有變動」 + * (已於 L1-6 驗證時發現:`replaceSection()` 實際寫出的內容其實逐字元相同,只有這裡的比較 + * 基準算錯)。比較前一律把兩邊的結尾空行正規化掉,只在意內容本身是否改變。 */ function renderSectionForCompare(heading, bodyLines) { return [`## ${heading}`, '', ...bodyLines, ''].join('\n'); } +function normalizeForCompare(sectionText) { + return sectionText.replace(/\n+$/, ''); +} + // ---- 章節 1/5:前綴與呼叫方式(完全模板化) ---- function buildPrefixTableBodyLines(meta) { @@ -297,6 +333,7 @@ function buildPassthroughBodyLines(currentReadmeText, heading) { */ function buildReadme(meta, currentReadmeText) { const headings = getHeadings(meta); + checkHeadingOrder(meta.shortName, currentReadmeText, headings); const changes = []; let text = currentReadmeText; @@ -308,7 +345,7 @@ function buildReadme(meta, currentReadmeText) { changes.push({ key: 'prefixTable', heading: headings.prefixTable, - changed: oldSectionText !== newSectionText, + changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText), oldText: oldSectionText, newText: newSectionText, }); @@ -326,7 +363,7 @@ function buildReadme(meta, currentReadmeText) { changes.push({ key: 'dirTree', heading: headings.dirTree, - changed: oldSectionText !== newSectionText, + changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText), oldText: oldSectionText, newText: newSectionText, }); @@ -342,7 +379,7 @@ function buildReadme(meta, currentReadmeText) { changes.push({ key: 'install', heading: headings.install, - changed: oldSectionText !== newSectionText, + changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText), oldText: oldSectionText, newText: newSectionText, }); @@ -360,7 +397,7 @@ function buildReadme(meta, currentReadmeText) { changes.push({ key: 'headless', heading: headings.headless, - changed: oldSectionText !== newSectionText, + changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText), oldText: oldSectionText, newText: newSectionText, }); @@ -379,7 +416,7 @@ function buildReadme(meta, currentReadmeText) { changes.push({ key: 'addSkill', heading: headings.addSkill, - changed: oldSectionText !== newSectionText, + changed: normalizeForCompare(oldSectionText) !== normalizeForCompare(newSectionText), oldText: oldSectionText, newText: newSectionText, });