Commit Graph

4 Commits

Author SHA1 Message Date
Leo 9299ec8960 feat(88): 版本線判別器從「有沒有 v」換成 DAEMON_LINE,產生端才吐得出裸號
leo 2026-08-17:「對外號就是三個數字,不要 v」。

08-18 第一次改失敗的理由要先講清楚:那個 `v` **兼任版本線判別器**
(daemon `v0.18.x` vs 雲端引擎 `1.4.x`),有四處在讀它。天真拿掉之後那些閘
**不報錯**,而是往下比對到更舊的 `## v0.18.29(…)` ⇒ 打包出 0.18.30 的執行檔、
manifest 卻宣稱是 v0.18.29。**靜默,21 站全綠。**

── 四處讀取點,各自靠什麼分辨(換掉之後)──────────────────────────
① `daemon-notes.DAEMON_RELEASED_RE`(daemon-in-bundle-gate 與 ship.mjs daemon-sync
   共用的那一份,第一輪已從四份拷貝收攏成一份)
   舊:`^## (v\d+\.\d+\.\d+)(` ——「有 v = daemon」
   新:`daemonReleasedReFor(repoRoot)` 由 `collector/DAEMON_LINE` 產生
       `^## (v?0\.18\.\d+)(` ——「在宣告的那條線上 = daemon」
② `daemon-in-bundle-gate.collectFacts/judge`(同上那份的使用端)
   新增一項 `daemon-line-declared` 排在**所有檢查最前面**:
   讀不出 DAEMON_LINE = 沒有判別器 ⇒ 當場斷,不給預設值。
   另記一個**只給錯誤訊息用**的 `changelogTopAny`,讓「沒中」時說得出
   「你指到的是 1.4.47,那是雲端那條線」——判斷與診斷分開。
③ `ship.mjs` daemon-sync 站:同 ①,並在丟例外時附上量到的事實。
④ `daemon-notes.changelogRelFor()`
   舊:`/^v/.test(version)` ⇒ 裸號時代會把 daemon 的 0.18.31 指去雲端的
   CHANGELOG.md(叫人去錯的檔補說明)。新:比對版本線。

⇒ 判別依據換成**宣告出來的事實**,不是外觀 ⇒ 產生端這才拿得掉那個 `v`:
   `daemon-version.py` 升版路徑 `"v%d.%d.%d"` → `"%d.%d.%d"`。
   「重打同一版」路徑刻意**不動**(沿用 prev_prefix):selfupdate.go 的
   newerThanCurrent() 用字串不等於判斷更新,重打時換寫法會讓每一台已安裝的機器
   看到一個內容一模一樣、卻宣稱是新版的假更新。

── 測試(+7 支,每支都附反向對照)──────────────────────────────────
故意製造「版本線判別會出錯」的情境,並證明**新的會擋、舊的會靜默放行**:
  · 產生端吐裸號 → 閘讀到最新那一版(反向對照:舊判別式讀到更舊的一版)
  · 源碼樹 0.18.31/bundle 停在 v0.18.30 → 擋(舊判別式在此判「相符」⇒ 放行)
  · DAEMON_LINE 換 0.19 而 changelog 還是 0.18.x → 擋(舊做法看不出線換了)
  · DAEMON_LINE 缺席/寫壞 → 擋,且擋在第一項
只有「新的怎麼跑」而沒有「舊的怎麼跑」的話,這些測試就只是回音,不是閘
(wiki/mistakes.md「測試複製了實作邏輯就不再是閘」)。

installer 262/262(+5)、collector daemon-version 11/11(+2)。

inkstone/arcrun-rag#88
2026-08-18 16:33:27 +08:00
Leo 03a2c3d5f8 fix(ship): 解開出貨線的兩個自鎖——閘改成量指紋、戳版順序倒過來(arcrun-rag#88)
出貨線從第 5 站 daemon-source-check 就把自己鎖死,push/deploy/verify/
release-record 四站一次都沒執行過。實查真兇是**兩個**,不是一個,而且同源:
D95 第一輪把 `CHANGELOG.md` 搬進 `collector/`(為了讓 collector/ 自足、
有資格獨立成 repo)之後,**「宣告新版本」這個動作本身就是在改 collector/**。

── 自鎖① 出貨線那道閘:用 git 歷史當代理 ────────────────────────────
舊判法=`git log <宣告那顆>..HEAD -- collector` 非空就擋。
⇒ 一顆**只改宣告、一行程式都沒動**的 commit(dcd0132 就是)也被算成
  「宣告之後源碼又動過」⇒ 閘擋自己。
⇒ 反過來也不準:collector/ 底下有些檔案(打包腳本旁的 .mjs 工具、README)
  根本不會進到執行檔,動了它們一樣被判「要重打包」。

🔴 兩件事都對,疊起來變成死結:**衝突在範圍重疊,不在任何一方做錯。**

修法不是把 changelog 搬回去(那會推翻 D95 第一輪),也不是加路徑白名單
(那就是被禁的字串比對)。改成問**同一件事實**,而那份事實這個 repo 早就在算:
`daemon-version.py` 每次戳版都把「當下的原始碼指紋」記進
`.version-source.json`(2026-08-06 起就存在)。
**版本 X 的指紋 == 現在算出來的 ⇒ X 的成品確實是照這份源碼打的。**
宣告那一步改到的檔案,在戳版當下就已經算進指紋 ⇒ 結構上不可能擋自己;
而「改了 code 卻沒重打包」照樣指紋不同 ⇒ 原本要擋的一個都沒放過。

指紋怎麼算是 daemon 自己的知識,出貨線**不重寫一份**(兩套並存必然漂移):
新增唯讀的 `daemon-version.py --source-state`,`daemon-freshness.mjs` 只負責問與判。
這就是遷移計畫寫的「daemon-freshness 該**搬家**不是加固」。

── 自鎖② 戳版的順序:帳本記的是「還沒戳版」那一刻的樹 ──────────────
`main()` 原本先 `check_or_record()` 再把宣告寫進 changelog。而 changelog 就住在
指紋涵蓋的樹底下 ⇒ 帳本記下來的那棵樹,從寫完宣告那一刻起就不存在了。
實撞(本輪 A/B 重現,見 daemon-version.test.mjs ⑦):
  build-mac.sh 戳版打完 dmg → 同一輪 build-win.sh 走「重打同一版」路徑
  → 現在的指紋(含已戳版的 changelog)≠ 帳本裡那個 → dist/ 已有 dmg
  → 判「版號已對應另一份原始碼」→ **第二個平台永遠打不出來**
而上游 `daemon-sync` 兩個平台都要,缺一不准出貨 ⇒ 出貨線在這裡也走不完。
⇒ 先寫宣告、再記指紋。(這條路是升版,帳本不可能已有該版號 ⇒ 不會中途 die。)
🔴 這個病是 D95 第一輪帶進來的:v0.18.29 打包時 changelog 還在 docs-site/,
  不在指紋範圍內,所以當時不會發作。

── 為什麼要多一本逐檔帳(.version-source-files.json)────────────────
總指紋只有 16 個字,對不上時只講得出「不一樣」。而紅線要求
「回報通過時要說得出它實際比對了什麼」⇒ 逐檔雜湊讓閘講得出**哪幾個檔**變了。
只留最新一版(閘只問最上面那一版),與總指紋帳本一樣排除在指紋之外(自我參照)。

── 閘要留痕(InkStoneCo#48)────────────────────────────────────────
`installer/daemon-freshness-gate-log.md`:擋下、放行、明知故犯放行,三種都記。

── 順手收進來的一筆(同一輪的複驗項)───────────────────────────────
cherry-pick 89dd323:`checkDocsLive()` 的假綠。實測線上 stage 文件站——
那一頁是舊的(135KB 逐版列表、沒有 dcd0132 的 meta-refresh),卻因為內文
剛好含一次 releases 網址而被 `body.includes()` 判過 ⇒ fails=0。修後 fails=1。

實測
  · daemon 0.18.30:Mac dmg 8.3M + Windows exe 22M **同一輪打出來**(修前不可能)
  · 閘判決:status=ok,帳本 ad1106716ae426d2 == 現在 ad1106716ae426d2(涵蓋 228 個檔)
  · installer 測試 257 支全綠;collector 版本產生器 9 支全綠(新增 ⑦⑧⑨)
  · daemon-freshness 自己的演練 15 支:兩個方向各有案例
    (不擋自己/仍擋得住沒重打包/問不出來一律停/留痕)

紅線遵守:只碰 stage;沒推 main、沒併分支、沒碰 prod/GitHub。

Refs: inkstone/arcrun-rag#88

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:39:52 +08:00
Leo aeb44c712b fix(88): 撤回「產生端吐裸號」的宣稱——那個 v 是版本線判別器,不能單獨拿掉
總管複驗退回一格(`inkstone/arcrun-rag#88#issuecomment-3082`),退得對。
我照他要的兩條路去查,結論是**選 B,而且原本那個改動本身有 bug**。

## 兩件事要分開講

**① 總管的事實宣稱有一處錯,但他看到的現象是真的**
「這條分支一行都沒動 daemon-version.py」— 不對,上一個 commit 動了 37 行,
含 `version = "v%d.%d.%d"` → `"%d.%d.%d"`。他跑 `daemon-version.py` 得到 `v0.18.29`
也是真的:當時的過渡規則讓「重打已發佈的版本」沿用原本寫法。
⇒ 他跑對了指令、看到真東西,只是把原因歸成「沒改」。

**② 但我的改動確實有 bug,而且比他指出的更嚴重**
`^## v` 這條判斷式有**四份各自為政的拷貝**(daemon-freshness/daemon-in-bundle-gate/
ship.mjs daemon-sync/daemon-version.py),三份只認帶 `v` 的。
產生端一旦戳出 `## 0.18.30(…)`,那三份**不會報錯**,而是往下比對到更舊的
`## v0.18.29(…)` ⇒ **打包出 0.18.30 的執行檔,manifest 卻宣稱是 v0.18.29。**
版本號說謊,靜默,21 站全綠。上一個 commit 就埋著這顆。

## 為什麼不是把三份放寬成 `v?`(我試了,當場被測試擋下)

🔴 **那個 `v` 目前身兼「哪一條版本線」的判別器。**
放寬後 daemon 的閘開始把雲端那條 `## 1.4.47(…)` 當成 daemon 版本撈走
⇒ `daemon-in-bundle-gate.test.mjs` ①⑮⑯ 轉紅,而它們釘的正是 D95 第二輪剛修好的
「出貨線指到錯的 changelog 卻安靜放行」。`daemon-notes.changelogRelFor()` 也用 `/^v/`。

⇒ 「不要 v」要落到產生端,得**先給那些閘一個替代判別器**
  (建議拿 `DAEMON_LINE` 比對,例 `^## v?0\.18\.\d+(`,同時擋「錯的檔」與「錯的線」)。
  那要動 `topReleasedVersion()` 等對外簽章、跨 installer/ 與 collector/,
  **與正在進行的 collector 拆 repo 相衝** ⇒ 不在這條分支做。

## 這個 commit 做了什麼

- 產生端**恢復原行為**(一律吐 `v`):不留「產生端裸號+閘只認 v」那顆靜默炸彈
- 四份拷貝**收成一份**(`daemon-notes.DAEMON_RELEASED_RE`,維持嚴格版)
  ⇒ **零行為變更**,但哪天要改格式只需改一個地方,而不是記得改四個
- 把「為什麼還不能改裸號」釘成一支會說話的測試(`daemon-version.test.mjs` ⑤⑥)
  ——不是註解,是紅燈
- 兩處註解寫清楚接手要先做什麼

**上一個 commit 的另外兩格(release 掛成品、內外對應)未受影響,總管已複驗通過。**

測試:installer 252/252、collector 6/6。
2026-08-18 15:25:22 +08:00
Leo 2e26528bc9 feat(version): 對外號真的不帶 v + release 掛得動成品 + 內外對應由機器產生(arcrun-rag#88)
leo 2026-08-18 當場抓到「剛剛不是說不要 v?」——實查確認不是措辭問題:
規約寫在 wiki,而**產生號碼的那條路照舊帶 v**。這輪修的是產地與載體。

① 對外號從**產生端**就是裸的
   `daemon-version.py` 是 daemon 那條線唯一的版本產生器,一路長到
   產物檔名(Arcrun-<版本>.dmg)、manifest.daemon.version、/api/latest。
   - 新戳的版本一律裸號;`RELEASED_RE` 加 `v?` 讀得懂歷史
   - 🔴 重打同一版**沿用 changelog 上原本的寫法**:selfupdate.go 的
     newerThanCurrent() 是字串不等於就提示更新 ⇒ 把 v0.18.28 改吐成 0.18.28
     會讓每台已安裝的機器收到一個內容相同卻宣稱是新版的**假更新**。
     規約會自己過期:頂上那段變裸號之後,重打路徑吐的也是裸號。
   - 實測:真實 changelog +一段未發佈 → 產生器吐 `0.18.30`(不是 v0.18.30)

② release 掛得動真的成品(本輪的硬前置)
   leo:「assets 都寫 source code,這兩個附檔實際是什麼?是 dmg 還是 go?」
   ——那是 Gitea/GitHub 自動產生的整包 repo 快照。實測既有 release 的
   attachment 數是 **0**,坐實那兩個附檔沒有一個是成品。
   - gitea/github-release.mjs 各補 uploadReleaseAsset/listReleaseAssets
     (兩邊 API 形狀不同是 API 本身的差別,不硬套成同一種)
   - release-lines.mjs 讓**每條版本線各自宣告**自己的成品(assetKeys);
     daemon 線明列 mac/win/msix,**排除釘在 0.15.7 的兩筆歷史遺留**
     ——照「有 file 欄就掛」會把舊檔掛上新版頁面,正是本輪要治的病
   - ship.mjs 建完 release 後掛檔,並**回頭查證**附件真的在頁面上;
     缺任何一個就擋(不留「點下去沒東西」的版本頁)

③ 內外對應由機器產生,不再是總管手寫
   leo:「對應關係記在該次發佈的 release note,不靠號碼本身編碼」
   - 新增 internal-version.mjs:`RAG-20260818-001-<commit7>`
   - 🔴 `formatInternalVersion` 收到空的 artifacts 直接丟例外
     ⇒ **算不出一個沒有成品的內部號**(leo:「內部每次就要出貨 bundle 的版本」)。
     ops-facts 原本記著「這件事目前靠自律,沒有機械閘」——這就是那道閘
   - 序號從**既有 release 的事實**算,不養會漂的帳;tag 與內文都掃
     (08-17 那兩筆內部號寫在 tag_name 上,只掃內文當天會從 1 重編、無聲撞號)

紅線遵守:既有 tag/檔名一個都沒回頭改;沒寫 GitHub(②③ 實測都在 Gitea);
沒部署、沒推 main、沒自己併分支。演練建的 release 與 tag 都已刪除複驗。

測試:installer 252 支全綠(原 233 +新 19);collector 產生器新增 6 支全綠。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:13:00 +08:00