feat(W2 Phase 0-1): 雙 profile 地基+JDD 兩軸身分+兩支防炸閘

SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。

■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
  基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
  拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
  舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)

■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
  修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
  subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
  ⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
  身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)

■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
  CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
  settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)

■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
  (SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)

■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
  repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
  解掉後歸零;連跑三輪冪等

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 23:56:48 +08:00
parent 2a8c259d08
commit 6a49f25aef
23 changed files with 1259 additions and 132 deletions
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# check-no-instance-names.sh — 機械閘 #2:框架範本不得混入實例專名
#
# 出處:《分離導入規格》第六節第 2 條、第七節第四題 Gherkin。
# Scenario: 框架混入實例名被 CI 擋
# Given 範本檔新增一行含 "arcrun"
# When CI 執行
# Then 檢查 fail 並指出檔案與行號
#
# 為什麼需要它(不是潔癖,是分層會自己塌):
# L1 框架的判準是「換一家公司照樣成立」。一旦範本裡寫著某個實例的專案名,
# 下一個裝這套 template 的人就會讀到一段對他不成立的規則——而他無從分辨
# 「這條是普世方法論」還是「這是別人家的家規」。分層一旦糊掉就回不去了。
#
# ── 豁免機制(為什麼要有)─────────────────────────────
# 本閘上線時,範本區已經有 22 行命中(8 個檔)。若不給豁免就硬上線,唯一的
# 通關方式會是「把 arcrun 改寫成『某工作流引擎』」——**資訊消失了,問題還在**
# (假性清理)。所以:真正該搬走的東西標豁免、記在帳上,隨 policy pack 一起搬;
# 純粹是註解舉例的,當場改寫成通用敘述。
# 豁免有兩級:
# · 行級 `sdt-instance-name-ok` → 該行豁免。用在「檔案本身是框架的,只有這行欠著」。
# · 檔級 `sdt-instance-name-ok-file` → 整檔豁免(寫在檔首 30 行內,需附理由)。
# 用在「整支本來就是 L2 政策包產物、只是還沒搬走」——逐行標 13 次是雜訊不是紀律。
#
# 誠實限制:這支只認「字面出現」。它擋不了「把實例假設寫成通用語氣」
# (例如寫「你的工作流引擎會在遠端執行」其實只有某一家成立)。
# 它是底線不是萬能——真正的分層判斷仍要人/CC 動腦。
#
# 用法:
# bash scripts/check-no-instance-names.sh # CI 模式,有未豁免命中 → exit 1
# bash scripts/check-no-instance-names.sh --baseline # 只列清單(含已豁免),永遠 exit 0
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
NAMES_FILE="scripts/instance-names.txt"
SCAN_DIR="template"
EXEMPT_MARK="sdt-instance-name-ok"
EXEMPT_FILE_MARK="sdt-instance-name-ok-file"
EXEMPT_HEAD_LINES=30 # 檔級豁免標記必須寫在檔首這麼多行內(強迫它顯眼、不許藏在檔尾)
MODE="ci"
[ "${1:-}" = "--baseline" ] && MODE="baseline"
if [ ! -f "$NAMES_FILE" ]; then
echo "❌ 找不到黑名單設定檔:$NAMES_FILE" >&2
exit 1
fi
if [ ! -d "$SCAN_DIR" ]; then
echo "❌ 找不到範本區:$SCAN_DIR/" >&2
exit 1
fi
# ── 讀黑名單 → 組成 grep 的 alternation pattern ───────────────
PATTERN=""
while IFS= read -r line; do
case "$line" in ''|'#'*) continue ;; esac
term="$(printf '%s' "$line" | tr -d '[:space:]')"
[ -z "$term" ] && continue
if [ -z "$PATTERN" ]; then PATTERN="$term"; else PATTERN="$PATTERN|$term"; fi
done < "$NAMES_FILE"
if [ -z "$PATTERN" ]; then
echo "⚠️ 黑名單是空的,本閘等同關閉($NAMES_FILE"
exit 0
fi
# ── 掃描 ────────────────────────────────────────────
# policy pack 不受此限(L2 本來就是一家之言)。目前尚無 policy pack 目錄,
# 先把慣例路徑寫在這裡,W3 建立時自動生效。
HITS="$(grep -rniE "$PATTERN" "$SCAN_DIR" 2>/dev/null \
| grep -v "^$SCAN_DIR/policy-packs/" || true)"
VIOLATIONS=0
EXEMPTED=0
VIOL_LINES=""
EXEMPT_LINES=""
if [ -n "$HITS" ]; then
while IFS= read -r hit; do
[ -z "$hit" ] && continue
hit_file="${hit%%:*}"
# 檔級豁免:整支本來就是 L2 產物,檔首宣告過就整檔記帳
if [ -f "$hit_file" ] && head -n "$EXEMPT_HEAD_LINES" "$hit_file" 2>/dev/null | grep -q "$EXEMPT_FILE_MARK"; then
EXEMPTED=$((EXEMPTED + 1))
EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n'
continue
fi
# 行級豁免:檔案是框架的,只有這行欠著
if printf '%s' "$hit" | grep -q "$EXEMPT_MARK"; then
EXEMPTED=$((EXEMPTED + 1))
EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n'
else
VIOLATIONS=$((VIOLATIONS + 1))
VIOL_LINES="${VIOL_LINES}${hit}"$'\n'
fi
done <<< "$HITS"
fi
# ── 輸出 ────────────────────────────────────────────
if [ "$MODE" = "baseline" ]; then
echo "════════════════════════════════════════════════"
echo "📊 實例專名基線報表($SCAN_DIR/"
echo "════════════════════════════════════════════════"
echo "未豁免(違規):$VIOLATIONS"
[ -n "$VIOL_LINES" ] && printf '%s' "$VIOL_LINES" | cut -c1-160 | sed 's/^/ ❌ /'
echo "已豁免(記在帳上,待搬遷):$EXEMPTED"
[ -n "$EXEMPT_LINES" ] && printf '%s' "$EXEMPT_LINES" | cut -c1-160 | sed 's/^/ ⏳ /'
exit 0
fi
if [ "$VIOLATIONS" -gt 0 ]; then
echo "════════════════════════════════════════════════" >&2
echo "❌ 框架範本混入實例專名($VIOLATIONS 行)" >&2
echo "════════════════════════════════════════════════" >&2
printf '%s' "$VIOL_LINES" | cut -c1-200 | sed 's/^/ /' >&2
cat >&2 <<EOF
鐵律:框架不含實例資料(《分離導入規格》第一節鐵律 2)。
L1 範本要「換一家公司照樣成立」;上面這些行只對某一家成立。
怎麼修(擇一,別選第四條「把詞換掉了事」):
1. 只是註解/舉例 → 改寫成通用敘述(例:「某個 repo」而非具體專案名)
2. 其實是政策包的內容 → 搬進 policy pack(L2),不要留在框架
3. 真的暫時搬不走 → 行尾加註記 $EXEMPT_MARK + 一句「為什麼還不能拿掉」
⚠️ 不准做的:把專名換成模糊詞讓檢查過關——資訊消失了,分層問題還在。
已豁免(不計違規):$EXEMPTED 行
EOF
exit 1
fi
echo "✅ 範本區無未豁免的實例專名(已豁免 $EXEMPTED 行,記在帳上待搬遷)"
exit 0