refactor(collector): 剪掉臍帶——collector/ 自己就算得出自己的版本號(D95 第一輪②)

leo 2026-08-17:「身為管理者,你要從頭到尾**不要有很多扭曲**,因為你根本不記得
你做的這些扭曲,**每次都要查**,很直接,源碼、產出物,從 stage 到 prod。」

`collector/` 原本要伸手到 repo 根才算得出自己的版本,共四條:

  ① `daemon-version.py` 讀 `docs-site/src/content/docs/help/changelog.md`
  ② `daemon-version.py` 讀 `ROOT/DAEMON_LINE`
  ③ `daemon-version.py` 的原始碼指紋從 repo 根 `git ls-files collector`
  ④ `changelog-section.sh` 讀 `$REPO_ROOT/docs-site/...` 並 exec
     `$REPO_ROOT/installer/scripts/daemon-notes.mjs`
  ⑤(複驗才發現的第五條)`build-msix.sh` 用 `git rev-parse --show-toplevel`/../../.env
     推 InkStoneCo 頂層拿 MS Store Identity

現在全部落在 `collector/` 內部:

  · `collector/CHANGELOG.md`  ← 新家。**原檔同時裝著兩條版本線**
    (桌面版 `v0.18.x` + 雲端引擎 `1.4.x`),已按版號格式拆開;
    雲端那 17 段原地不動留在 docs-site,桌面版這 30 段搬過來。
  · `collector/DAEMON_LINE`   ← 從 repo 根搬進來
  · 指紋改以 `collector/` 為根算(`cwd=COLLECTOR`、pathspec `.`)
    ⇒ 相對路徑前綴變了、而路徑有進雜湊 ⇒ `FINGERPRINT_ALGO` 3→4,
      照既有設計讓帳本自動整本作廢重記(不要手改 JSON)
  · `daemon-notes.mjs` 實作搬進 `collector/cmd/arcrun-app/`,
    `installer/scripts/daemon-notes.mjs` 變薄殼轉呼叫
    ⇒ **根可以往內伸手,collector 不可以往外伸手**,方向單向
  · `build-msix.sh` 改成往上找「帶著那把鍵的 .env」,
    leo 08-06「腳本自己去讀不要再問人」原樣保留,但不再綁目錄結構

驗收閘:`collector/check-standalone.sh`——不是 grep,是**行為證明**:
把 collector/ 的檔案單獨複製到 repo 之外的臨時目錄、在那裡 git init,
再跑版本計算與打包前置閘。跑得起來=真的自足。

自己發現並修掉的三件:
  · `arcrun-tray/assets/store/` 與 `--setup` 其實還活著(見上一顆 commit)
  · 兩支同名的 `daemon-notes.mjs` CLI 守衛比的是**檔名尾綴**
    ⇒ 兩支一起開火,問雲端版號時 collector 那支先 exit(1),
      薄殼根本沒機會查 docs-site。改成比絕對路徑。
  · `collector/CHANGELOG.md` 的「怎麼出新版」說明若照抄那行標題,
    會被 `daemon-version.py` 的純字串比對當成「有待發佈內容」而誤升版
    ⇒ 說明裡刻意不寫成真的標題,並把這個邊角寫在檔案裡

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:05:33 +08:00
parent cef157c584
commit bd25efaae5
10 changed files with 725 additions and 22 deletions
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# check-standalone.sh — 證明 `collector/` 沒有任何「往上伸」的相依(D95 第一輪②的驗收閘)
#
# 🔴 leo 2026-08-17:「身為管理者,你要從頭到尾**不要有很多扭曲**,因為你根本不記得
# 你做的這些扭曲,**每次都要查**,很直接,源碼、產出物,從 stage 到 prod。」
#
# ── 為什麼主證明不是 grep ──────────────────────────────────────────────────
# grep 只證明「我想得到的那幾種寫法沒出現」,證明不了「真的自足」。
# 少想到一種寫法(`$(dirname $0)/../../..`、環境變數繞路、執行期才拼出來的路徑…)
# 就會拿到一個**假綠**——而假綠比紅還糟,因為它讓人停止檢查。
#
# ⇒ **② 才是判準**:把 collector/ 的檔案單獨複製到一個臨時目錄
# (那裡**沒有** docs-siteinstallerlandingrepo 根的任何東西,
# 也不是原 repo 的子目錄,`git rev-parse --show-toplevel` 只會指到它自己),
# 在那裡跑真正的版本計算與打包前置閘。
# **跑得起來=真的自足**;有任何一條偷偷伸手到 repo 根,在那裡就會當場斷掉。
#
# ① 只是**快篩**,抓兩個「出現在可執行位置就一定是錯」的字樣,讓回歸早一步紅。
#
# ⚠️ 快篩必須避開一個假警報:collector 是**處理使用者知識庫**的程式,
# 它的原始碼與測試裡到處是 `system-dev/wiki/...` 這種字串——那是**使用者 vault 裡的路徑**,
# 不是本 repo 的目錄。拿目錄名當樣式掃會掃出上百筆雜訊,然後沒有人再看這份輸出。
# ⇒ 快篩只認語法構造(`git rev-parse --show-toplevel``REPO_ROOT`),且**先剝掉註解與說明字串**。
#
# 用法:
# bash collector/check-standalone.sh # 全部檢查
# KEEP=1 bash collector/check-standalone.sh # 保留臨時目錄以便查看
set -uo pipefail
cd "$(dirname "$0")"
COLLECTOR="$(pwd)"
FAIL=0
ok(){ printf " ✅ %s\n" "$1"; }
ng(){ printf " ❌ %s\n" "$1"; FAIL=1; }
# 受控檔案清單(含未提交、排除 gitignore)——與 daemon-version.py 算指紋的口徑一致。
files_list() { git -C "$COLLECTOR" ls-files -co --exclude-standard . ; }
echo "━━━ ① 快篩:可執行位置不得出現往 repo 根定位的構造 ━━━"
HITS="$(files_list | python3 -c '
import io, sys, tokenize, re
from pathlib import Path
BAD = [
(re.compile(r"rev-parse\b[^|;)]*--show-toplevel"), "git rev-parse --show-toplevel"),
(re.compile(r"\bREPO_ROOT\b"), "REPO_ROOT"),
]
SELF = {"check-standalone.sh", "CHANGELOG.md"}
def strip_py(src):
"""用 tokenize 剝掉 Python 的註解與獨立字串(docstring)——說明文字裡提到不算違規。"""
out = {}
try:
toks = list(tokenize.generate_tokens(io.StringIO(src).readline))
except Exception:
return {i + 1: l for i, l in enumerate(src.split("\n"))}
for t in toks:
if t.type in (tokenize.COMMENT, tokenize.STRING, tokenize.NL, tokenize.NEWLINE):
continue
out.setdefault(t.start[0], "")
out[t.start[0]] += t.string + " "
return out
def strip_hash(src):
"""`#` / `//` / ` * ` 行註解剝掉(sh、js、mjs、go)。"""
out = {}
for i, l in enumerate(src.split("\n"), 1):
s = l.split("#", 1)[0] if l.lstrip().startswith("#") else l
if s.lstrip().startswith(("//", "*", "/*")):
s = ""
s = re.sub(r"#.*$", "", s) if l.lstrip().startswith("#") else s
out[i] = s
return out
for rel in (x.strip() for x in sys.stdin):
if not rel or Path(rel).name in SELF:
continue
p = Path(rel)
if not p.is_file() or p.suffix in (".png", ".ico", ".sum", ".md", ".json"):
continue
try:
src = p.read_text(errors="ignore")
except Exception:
continue
lines = strip_py(src) if p.suffix == ".py" else strip_hash(src)
for n, code in lines.items():
for rx, why in BAD:
if rx.search(code):
print(f"{rel}:{n}: [{why}] {code.strip()[:100]}")
')"
if [ -n "$HITS" ]; then
ng "有往 repo 根定位的構造(下列都在可執行位置,不是註解)"
printf '%s\n' "$HITS" | sed 's/^/ /'
else
ok "沒有 git rev-parse --show-toplevel、沒有 \$REPO_ROOT"
fi
echo
echo "━━━ ② 行為證明:把 collector/ 單獨搬出去,看它還算不算得出自己的版本 ━━━"
TMP="$(mktemp -d)"
trap '[ "${KEEP:-}" = "1" ] || rm -rf "$TMP"' EXIT
DEST="$TMP/collector-standalone"
mkdir -p "$DEST"
# 只複製受控檔案——build 產物、node_modules、dist 一律不帶(它們本來就不該影響自足性)
N=0
while IFS= read -r f; do
[ -f "$COLLECTOR/$f" ] || continue
mkdir -p "$DEST/$(dirname "$f")"
cp "$COLLECTOR/$f" "$DEST/$f"
N=$((N+1))
done < <(files_list)
echo " ️ 臨時樹:$DEST$N 個檔,不在原 repo 底下)"
# 這裡是全新的 git repo——`git rev-parse --show-toplevel` 只會指到 $DEST 自己,
# 任何原本靠 repo 根才拿得到的東西都會在這裡消失。
git -C "$DEST" init -q
git -C "$DEST" add -A
git -C "$DEST" -c user.email=x@y -c user.name=z commit -qm standalone
APP="$DEST/cmd/arcrun-app"
VER="$("$APP/daemon-version.py" 2>/dev/null)"
if [ -n "$VER" ]; then ok "daemon-version.py 在獨立樹裡算得出版本:$VER"
else ng "daemon-version.py 在獨立樹裡算不出版本(還有東西往上伸):$("$APP/daemon-version.py" 2>&1 | head -2)"; fi
FP="$(cd "$APP" && python3 -c "
import importlib.util
s=importlib.util.spec_from_file_location('dv','daemon-version.py')
m=importlib.util.module_from_spec(s); s.loader.exec_module(m)
print(m.source_fingerprint())" 2>/dev/null)"
[ -n "$FP" ] && ok "原始碼指紋算得出來:$FP" || ng "原始碼指紋算不出來"
if (cd "$APP" && ./changelog-section.sh "$VER" --check >/dev/null 2>&1); then
ok "changelog-section.sh --check 通過(三支 build 腳本靠它擋『忘了寫更新內容』)"
else
ng "changelog-section.sh --check 失敗:$(cd "$APP" && ./changelog-section.sh "$VER" --check 2>&1 | head -2)"
fi
NOTES="$(cd "$APP" && ./changelog-section.sh "$VER" 2>/dev/null | head -1)"
[ -n "$NOTES" ] && ok "更新說明投影得出來:${NOTES:0:36}" || ng "更新說明投影不出來(daemon-notes.mjs 還在往上找)"
echo
[ "$FAIL" = 0 ] && echo "✅ collector/ 自足:搬出去照樣算得出自己的版本號" \
|| echo "❌ collector/ 還有往上伸的相依(見上)"
exit "$FAIL"