YiBi Dev Docs
架構決策 (ADR)

Spec 一致性 CI 把關用開源 @fission-ai/openspec CLI(ubuntu),而非 macOS-only 的 Spectra GUI binary

Context

repo 目前有 13 個 active spectra change、60 個 canonical spec,但 spec ↔ code 的一致性 完全沒有自動把關grep spectra .github/workflows/ 零命中)。後果已發生:change 0036 帶 invalid delta-spec、多個 change 進度長期卡在 0/N(0019 0/31、0031 0/36、0024 0/15、 0033 0/10)卻無人提醒。

目標是建兩道 gate:每次 PR 的快速確定性檢查 + 定期 full-scope 深度掃描。實作前 先釐清「用什麼工具、在哪個環境跑」,因為這直接受制於 Spectra 的散佈形態。

關鍵約束與發現(皆實測,非推測):

  1. Spectra CLI 是 macOS-only 桌面 App。本機 spectra/Applications/Spectra.app 內 binary 的 symlink,透過 brew install --cask spectra-app 安裝,cask 標明 Required: macOS,無 Linux build。scripts/setup-cloud.sh 寫的 npm install -g @heyu-ai/spectra 在公開 npm 回 404(該 scope 未公開發布,且該行本身帶 warn fallback)。 → 真 Spectra binary 無法在便宜的 ubuntu-latest CI runner 跑

  2. 但底層是開源的 @fission-ai/openspec v1.5.0(Node 20.19+,npm 安裝,ubuntu 相容)。 實測:從 docs/ 目錄跑 openspec validate --changes --json 能解析本 repo 的 docs/openspec/ 並驗證 change/spec;validate 的 flag(--all/--changes/--specs/ --strict/--json)與 Spectra 完全相同 → Spectra 包的就是這套 OSS,只是再疊一層 GUI 與專屬加值。

  3. validate/drift/analyze 是編譯進 binary 的 subcommand(確定性、不吃 LLM), 不是 skill/spectra-* Claude Code skill 只是薄包裝、shell out 去呼叫 binary subcommand 再排版(例如 /spectra-drift 實際跑 spectra drift --json);drift 的 anchor 上限寫在 Rust 模組 spectra_core::drift,是編譯碼而非 skill 邏輯。純終端機 spectra drift <change> 不需任何 skill 或 LLM。其中 drift/analyze 是 Spectra 專屬——OSS OpenSpec binary 沒有此二 subcommand(openspec --help 只有 validatelistarchivestatusdoctor 等)。唯一例外是 verify(code↔spec 比對): Spectra 與 OSS 都沒有對應 binary subcommand,只有 /spectra-verify / /opsx:verify 這種純 LLM agentic skill。 → 推論:drift/analyze 因為是確定性 binary、理論上能 headless 跑 CI,唯一阻擋是 binary 只有 macOS 版。長期緩解路徑:https://github.com/howie/openspectra(開發中,目標提供 Linux-native CLI 以移除此限制);verify 則不論平台都必須靠 LLM。 這三者現階段只能在 Mac(Spectra binary)或 headless Claude(verify)跑。

  4. OpenSpec OSS 的 root 解析預設找 openspec/ 目錄,本 repo 在 docs/openspec/,故 CI 須 以 working-directory: docs 執行才解析得到 root(從 repo 根跑會得到 0 items)。

Decision

結論:per-PR 的確定性 gate 用開源 @fission-ai/openspecvalidate 在 ubuntu CI 跑 (pin 版本、零 macOS 成本);drift / analyze / verify 等 Spectra 專屬或 LLM 深度檢查, 留在 Mac 上的手動 make spectra-scan

  • 每次 PR(ubuntu CI)npm install -g @fission-ai/openspec@<pinned>working-directory: docsopenspec validate <diff-scoped change/spec> --strict --json → PR comment + 折進 ci-status rollup(兩階段 rollout:先 report-only,baseline 清乾淨後轉硬 gate)。
  • 定期 full-scope(Mac 手動)make spectra-scan loop spectra list --json 跑真 spectra drift + analyze,並對 in-progress change 提示 /spectra-verify,聚合報告 → GitHub Issue + Slack。

工具邊界一句話:確定性、便宜、跨平台的 validate 上 ubuntu;專屬、LLM、macOS-bound 的 drift/analyze/verify 留 Mac。

Alternatives Considered

  • 方案 A:macos-latest CI runner brew 裝真 Spectra — 考慮過,因為 macOS 分鐘數計費 10x、 95MB App 每次 brew 裝 2-3 分鐘、cask auto_updates=true 導致版本不可 pin(TestFlight 式的 版本漂移風險搬進 CI) 成本與穩定性代價過高沒有選。
  • 方案 B:ubuntu 用 Python 重寫 validate 格式檢查 — 考慮過,因為 要自己維護一套 delta-spec 格式驗證、且會跟 Spectra/OpenSpec 的真實規則靜默漂移(reimplementation drift) 沒有選。
  • 方案 C(採用):ubuntu 跑開源 OpenSpec CLI 的真 validate — 便宜 + 用真工具不重造輪子, 代價是 OSS 1.5.0 vs Spectra 2.3.1 的格式規則可能緩慢分歧。緩解:pin 版本 + 納入 ADR-0006 的 vendor-drift 監控 + 上線前驗 OSS↔Spectra parity(確認兩者對 0036 等案例判定一致)。

Consequences

  • per-PR 確定性 gate 零 macOS CI 成本,跑在既有的 ubuntu CI 慣例上(dorny/paths-filterchanges job、ci-status rollup、actions/github-script PR comment 全可復用)。
  • 真工具驗證(OpenSpec 是 Spectra 的上游),不需自行 reimplement 格式規則。
  • 工具邊界清楚:誰能上 Linux CI、誰留 Mac,一目了然,未來新增檢查有依據可循。

壞 / 代償

  • OSS ↔ Spectra 版本分歧風險:1.5.0(OSS)與 2.3.1(Spectra)共用格式但 Spectra 有專屬擴充 (spec-driven-yibi schema、.openspec.yaml per-change metadata)。validate 目前相容,但需 持續監控;若日後分歧過大,per-PR gate 退回方案 A(macos runner 跑真 Spectra)。
  • drift 現階段訊號弱:多數 change 的 .openspec.yamlcreated 日期(實測 0040 有、 0039/0027 為 null),Time 與 Task-collision 維度失效,只剩 Structure(broken anchor)有用。 需先回填 created 才能讓 drift 在 Mac scan 發揮價值。
  • per-PR gate 與深度 scan 分屬兩個環境/工具,心智負擔比「單一工具一條龍」高。

中性

  • OpenSpec OSS root 解析需 working-directory: docs,是設定細節而非阻礙。
  • validate --all 在本機會誤掃 .claude/worktrees/* 副本;CI 乾淨 checkout 無此問題,反而比 本機更準。
  • 本 ADR 只定「工具與環境邊界」;兩道 gate 的完整實作步驟見方案計畫 ~/.claude/plans/gate-ci-merry-dewdrop.md,後續以實作 change 落地並回填 related.change

Postscript — 2026-07-03 實測 parity(issue #665 Part A Phase 1 落地)

實作前依「驗證步驟 1」實測 @fission-ai/openspec@1.5.0 vs spectra 2.3.1validate --changes [--strict]--strict 與否結果相同)。結論:parity 不成立—— OSS 為 stricter superset。 本 ADR「確認兩者對 0036 等案例判定一致」的假設需修正如下。

OSS 1.5.0 額外以 ERROR(Spectra 2.3.1 不擋)強制:

  1. 每個 ### Requirement body 必含 SHALL / MUST(RFC 2119);
  2. 每個 requirement 至少一個 #### Scenario: block;
  3. 更嚴的 delta-section 結構(例:change 0041 被判 "No deltas found")。

實測分歧範圍:Spectra 14/14 valid(0036 修 header 後);OSS 8/14 valid,額外判 invalid 的 是 0019 0020 0031 0036 0039 0041(皆 pre-existing、他人進行中的 change)。

對 Decision 的影響(不推翻,收斂實作)

  • Decision 核心(OSS validate 上 ubuntu、drift/analyze 留 Mac)維持
  • 分歧由既有兩項緩解吸收:(a) diff-scoped——gate 只驗每個 PR 動到的 change,pre-existing OSS-only 債不擋無關 PR;(b) Phase 1 report-only——job 不進 ci-statusexit 0 恆真, 純 PR comment。故 Phase 1 安全落地。
  • Phase 2(硬 gate)新增前置條件:上述 6 個 baseline change 需先清到 OSS-valid(各 change owner 協調),才可加 --fail-on-diff-invalid 並接進 ci-status。追蹤見 issue #703。
  • 版本治理:CI pin 記於 docs/openspec/spectra-vendor/CI_OPENSPEC_VERSION,分歧量測納入 ADR-0006 Layer 2 vendor-drift 審查(見該 ADR 2026-07-03 Postscript)。

實作 PR:#702(issue #665 Part C 前置清理 + Part A Phase 1 report-only)。Phase 2 follow-up: issue #703。related.change 仍為 N/A(純 CI/toolchain,無對應 OpenSpec capability delta)。 Phase 2 硬 gate 上線後,本 ADR 由 proposedaccepted

Decision 內描述的 Mac 端 make spectra-scan(定期 full-scope)屬 issue #665 Part B,尚未 實作;本 PR 僅落地 ubuntu 端的 per-PR validate gate。在 Part B 落地前,make spectra-scan 是規劃中的目標,非可執行指令。

Postscript — 2026-07-03 Phase 2 準備:OSS 不支援巢狀 capability(決策:gate 跳過)

推進 Phase 2 硬 gate 時,實測發現 OSS 1.5.0 無法解析本專案的巢狀 capability 布局specs/ENN-epic/FMM-feature/spec.md,openspec/CLAUDE.md「巢狀 spec」+issue #492 明訂的 支援慣例)。實例:change 0041 的 spec 結構完整(## MODIFIED Requirements### Requirement#### Scenario+SHALL/MUST 齊備),但 OSS 對整個 change 回報 No deltas found——因為它只掃 specs/<cap>/spec.md 一層,看不到 specs/E02-child-profile/F028-.../spec.md。macOS Spectra binary 則能正確解析。

這比「6 個 baseline 債」更根本:硬 gate 一旦上線,會擋下所有未來的巢狀-cap change PR, 而巢狀-cap 是專案支援的合法結構。

決策(deciders: howie,2026-07-03):gate 跳過巢狀-cap change(不硬擋),改由 Mac 端 spectra validate 覆蓋。實作於 scripts/openspec_validate_report.pychange_has_nested_caps 結構偵測 → report 顯示 SKIPPED--fail-on-diff-invalid 不計入)。 單層-cap change 仍走硬 gate。長期若 openspectra(Linux-native CLI)支援巢狀 cap,可再納入硬 gate。

Phase 2 硬 gate 上線的剩餘前置(本 PR 未 flip,維持 report-only):

  1. 5 個單層-cap baseline change(0019/0020/0031/0036/0039)清到 OSS-valid——各 change owner 執行,清單見 issue #703。(0041 因巢狀已由 gate 跳過,不在此列。)
  2. baseline 清乾淨後:ci.yml 的 validate step 加 --fail-on-diff-invalid+把 job 接進 ci-status needs: 與 failure rollup。
  3. 屆時本 ADR 由 proposedaccepted

本 PR(Phase 2 準備):實作巢狀-cap 跳過邏輯+測試,維持 report-only。related.change 續為 N/A。

Postscript — 2026-07-03 Accepted:Phase 2 dry-run 開關已開啟(issue #720 P1-1 / Q1 裁決)

本 ADR 依 2026-07-03 owner 裁決(deciders: howie,issue #720 Q1)由 proposedaccepted:Phase 2 開關(--fail-on-diff-invalid)啟用,required-check 轉正在一週 dry-run 後執行。裁決內容:

  • scripts/openspec_validate_report.py--fail-on-diff-invalid.github/workflows/ci.ymlopenspec-validate job 開啟。
  • 開關本身即是 dry-run:job 仍ci-statusneeds: 與 failure rollup,branch-protection 層級也維持非 required check(該項屬 GitHub repo settings 變更,由 owner 之後另行執行)。因此 job 現在可以變紅(診斷任何 touched change 為 diff-invalid),但紅燈不會擋 merge——這正是 dry-run 要觀察的訊號。
  • dry-run 週期一週,預定 2026-07-10 檢視期間是否有 false positive(例如 OSS 1.5.0 比 Spectra 2.3.1 更嚴格判到的假警報,或巢狀 cap 跳過邏輯的邊界案例)。 一週後若無 false positive,才將此 job 加入 ci-status needs: 陣列並在 GitHub repo settings 設為 required check(Phase 2 正式轉硬 gate)。
  • 前一版本 Postscript 列出的「5 個 baseline change 需先清到 OSS-valid」前置 條件(issue #703)不再是本次開關的阻擋條件——因為 gate 是 diff-scoped, 只在 PR 實際觸碰到該 change 的 delta-spec 時才會判定,且 dry-run 期間紅燈 不擋 merge。清理 baseline debt 仍應在 issue #703 追蹤,作為降低 dry-run 期間雜訊、及未來轉正式硬 gate 前的收斂項目,但不再是「開啟 dry-run 開關」 本身的前置。
  • 本 postscript 明確取代(supersede)前兩則 postscript 中「Phase 2 硬 gate 上線後,本 ADR 才由 proposedaccepted」的時序條件:依 Q1 裁決, accepted 與 dry-run 開關同步生效,不等 required-check 轉正。前兩則 postscript 的該句自此僅具歷史紀錄性質。

On this page