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:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user