fix(docs): 桌面版新版本重新長回文件站——投影不複製(D95 第三輪)

D95 第一輪把 daemon 的 changelog 搬進 collector/ 讓 collector/ 自足,
但文件站的建置沒有任何一步會讀那個新位置 ⇒ 從那一刻起,桌面版的新版本
再也上不了 rag.arcrun.dev/docs/help/changelog/(匿名實測:線上 v0.18.28
有 4 次、v0.18.29 零次;v0.18.29 本來就只到 stage,所以這是未來式的退化,
不是已經在傷害用戶)。

修法:建置時投影,不複製、不改成只給連結。
  collector/CHANGELOG.md
    → collector/cmd/arcrun-app/daemon-notes.mjs 的 releasedSections()(切出已發佈段落)
    → docs-site/remark-daemon-changelog.mjs(remark 階段換掉佔位符)
    → /docs/help/changelog/(網址不變、頁面不變、docs-site 裡零副本)

「哪些段落算已發佈」住在 collector/ 那棵樹——那是 changelog 自己的格式知識,
不是文件站的。方向仍單向:根往內伸手拿,collector 不往外伸手。

順手修掉施工中發現的兩個沉默失敗:
  · Astro 的內容快取只認那一頁自己的檔案有沒有變 ⇒ 只改 collector/CHANGELOG.md
    會建出「成功但內容是舊的」頁面(實測:不加 --force 新段落 0 次命中,加了 4 次)。
    docs-site 的 build 因此帶 --force,理由寫在 package.json 的 //build 與 plugin 檔頭。
  · 佔位符被刪掉時原本會安靜地少掉整條版本歷史 ⇒ 改成當場 throw 並指名修法。

另:第一輪誤把一顆 __pycache__/*.pyc 帶進版控,移出追蹤並補 .gitignore。
(同目錄那些 20MB/11MB 的執行檔與 dmg 是 6b44784/e09f866/c6d7b2c 的舊帳,這輪不動。)

實測
  · 本機建置:v0.18.29 出現 6 次(本輪前為 0);daemon 30 段+雲端 17 段,
    與 D95 之前那一頁的 30/17 完全一致
  · 來源證明:docs-site/src 裡 `^## v0.` 零筆;在 collector/CHANGELOG.md 頂端塞
    v9.9.9 重建 → 頁面命中 4 次,移除後回復
  · 網址:dist/help/changelog/index.html → deploy/docs/…(rsync 鏡射)路徑與本輪前相同;
    線上 /docs/help/changelog 307 → /docs/help/changelog/ 200(平台行為,本輪未動路由)
  · 建置輸出會印「[daemon-changelog] 投影 30 段(v0.18.29 … v0.15.4)|來源 …」
    ——這道閘說得出它實際投影了什麼
  · node --test:installer/scripts/*.test.mjs 與新增的 daemon-notes.test.mjs 共 238 支全綠

Refs: inkstone/InkStoneCo#40

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:00:32 +08:00
parent bd25efaae5
commit 00ccec53f2
4 changed files with 148 additions and 0 deletions
+4
View File
@@ -12,3 +12,7 @@ dist-msix/root/
# 進版控只會讓 repo 膨脹,且每次重編都變動、每次都來吵未推警察。
dist/
dist-msix/
# Python 匯入快取(daemon-version.py 被 import 時產生)。可重現的產物,且綁 Python 版本
# (檔名帶 cpython-314)⇒ 不進版控。2026-08-18 D95 第三輪:第一輪搬檔時誤把一顆帶了進來。
__pycache__/
+64
View File
@@ -49,6 +49,70 @@ export function stripMarkdown(s) {
.trim();
}
/**
* 「已發佈」的版本段落長這樣:`## v0.18.292026-08-16`。
* 待發佈的那一段標題是「下一版(未發佈)」,**不符合這個形狀** ⇒ 天然不會被投影出去。
* 這是刻意的:投影器不必另外認一次「未發佈」,少一條要記得的規則。
*/
const RELEASED_HEADING = /^##\s+(v\d+\.\d+\.\d+)(?=\D|$)/;
/**
* 把 changelog 切成「只剩已發佈段落」的 markdown。**給文件站投影用。**
*
* ── 為什麼這件事住在 collector/2026-08-18D95 第三輪)─────────────────
* 文件站要在建置時把桌面版的更新內容接回 `/docs/help/changelog/` 那一頁
* (網址不變、使用者看到的東西不變)。**接的方式必須是「讀本檔」而不是「複製一份」**
* ——第一輪把這條線搬進 `collector/CHANGELOG.md` 就是為了拔掉「同一份 changelog 存兩地」。
*
* 而「哪些段落算已發佈、preamble 要丟掉」是 **changelog 自己的格式知識**
* 不是文件站的知識 ⇒ 解析住在 daemon 這棵樹裡,文件站只負責把結果貼上去。
* 方向仍然單向:**根(docs-site)往內伸手拿,collector 不往外伸手。**
*
* 丟掉的:檔頭那段給維護者看的說明(含「怎麼出新版」與那個 `<二級標題>` 範例)、
* 以及任何還沒戳版號的段落。留下的:每一個 `## vX.Y.Z(日期)` 段的原文,一字不改。
*
* @param {string} changelogText changelog 全文
* @returns {{markdown: string, versions: string[]}} markdown=只剩已發佈段落;versions=由新到舊
*/
export function releasedSections(changelogText) {
const lines = String(changelogText).split('\n');
const kept = [];
const versions = [];
let inside = false;
for (const l of lines) {
// 只有二級標題會切換「現在在不在一個已發佈段落裡」。
// `### 小標` 不會(`^##\s` 要求第三個字是空白),所以段落內的結構完整保留。
if (/^##\s/.test(l)) {
const m = l.match(RELEASED_HEADING);
inside = Boolean(m);
if (m) versions.push(m[1]);
}
if (inside) kept.push(l);
}
return { markdown: kept.join('\n').trim(), versions };
}
/**
* 同 `releasedSections()`,但直接讀檔,且**問不出東西就 throw**。
*
* 🔴 為什麼是 throw 不是回空字串:這支的呼叫端是文件站的建置。
* 回空字串=建置成功、頁面少了整條桌面版版本歷史、**沒有任何人會發現**——
* 那正是 D95 第二輪抓到的那個形狀(出貨線問錯檔案就整站安靜跳過)。
* 壞掉要當場停,不要安靜地產出一個少一半的頁面。
*/
export function releasedSectionsFromFile(changelogPath = CHANGELOG_PATH) {
if (!existsSync(changelogPath)) {
throw new Error(`找不到 daemon 的版本說明檔:${changelogPath}(單一真相源=collector/CHANGELOG.md`);
}
const r = releasedSections(readFileSync(changelogPath, 'utf8'));
if (!r.versions.length) {
throw new Error(
`${changelogPath} 裡沒有任何**已發佈**的版本段(\`## vX.Y.Z(日期)\`)。\n` +
' → 若只有「下一版(未發佈)」,先跑 `daemon-version.py --stamp` 戳成正式版號。');
}
return { ...r, path: changelogPath };
}
/**
* 從 changelog 取某一版的「一句話摘要」清單。
* 規則:只認**頂層條目**(行首 `- `)的第一個粗體片段——那就是該條的標題。
+80
View File
@@ -0,0 +1,80 @@
/**
* daemon-notes.test.mjs — `releasedSections()` 的演練(D95 第三輪)
*
* 這支存在的理由:`releasedSections()` 的產出**會直接變成使用者讀的那一頁**
* `rag.arcrun.dev/docs/help/changelog/`)。它多切一段、少切一段、或把給維護者看的
* preamble 漏出去,都是使用者當場看得到的錯,而建置不會報任何錯。
*
* 跑法:node --test collector/cmd/arcrun-app/daemon-notes.test.mjs
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { releasedSections, releasedSectionsFromFile, CHANGELOG_PATH } from './daemon-notes.mjs';
const FIXTURE = [
'# Arcrun 桌面版(daemon)版本說明',
'',
'> 給維護者看的說明,不該出現在網站上。',
'',
'## 怎麼出新版(不要手打版本號)',
'',
'在本檔最上面加一個二級標題:',
'',
' <二級標題> 下一版(未發佈)',
'',
'---',
'',
'## 下一版(未發佈)',
'',
'- 🔑 **還沒戳版號的東西**:不該出現在網站上。',
'',
'## v0.18.292026-08-16',
'',
'**建議更新**',
'',
'- 🔑 **甲**:內文甲。',
'',
'### 小標題也要留著',
'',
'- 乙',
'',
'## v0.18.282026-08-16',
'',
'- 丙',
'',
].join('\n');
test('只留已發佈段落:preamble 與「下一版(未發佈)」都不會被投影出去', () => {
const { markdown, versions } = releasedSections(FIXTURE);
assert.deepEqual(versions, ['v0.18.29', 'v0.18.28']);
for (const forbidden of ['給維護者看的說明', '怎麼出新版', '<二級標題>', '下一版(未發佈)', '還沒戳版號']) {
assert.ok(!markdown.includes(forbidden), `不該出現在使用者眼前:${forbidden}`);
}
assert.ok(markdown.startsWith('## v0.18.292026-08-16'), '第一行就該是最新的已發佈版本');
});
test('段落內文原樣保留,`###` 小標題不會被當成段落結束', () => {
const { markdown } = releasedSections(FIXTURE);
assert.ok(markdown.includes('### 小標題也要留著'));
assert.ok(markdown.includes('- 乙'), '`###` 之後的內容仍屬於同一個版本段');
assert.ok(markdown.includes('**建議更新**') && markdown.includes('- 丙'));
});
test('一段都沒有就 throw——不准安靜回空字串(那會建出一頁少一半的東西)', () => {
assert.throws(
() => releasedSectionsFromFile('/nonexistent/CHANGELOG.md'),
/找不到 daemon 的版本說明檔/);
// 只有未發佈段落時同樣要停
const onlyUnreleased = '# 標題\n\n## 下一版(未發佈)\n\n- 甲\n';
assert.deepEqual(releasedSections(onlyUnreleased).versions, []);
});
test('對真的 collector/CHANGELOG.md 跑一次:段數=檔案裡 `## vX.Y.Z` 的行數', () => {
const raw = readFileSync(CHANGELOG_PATH, 'utf8');
const expected = raw.split('\n').filter((l) => /^##\s+v\d+\.\d+\.\d+/.test(l)).length;
const { versions, markdown } = releasedSectionsFromFile();
assert.equal(versions.length, expected);
assert.ok(expected > 0, '真檔裡至少要有一個已發佈版本');
assert.ok(!markdown.includes('單一真相源'), '檔頭那段維護者說明不該被投影出去');
});