文檔 Registry 規格
狀態:P0 設計已通過審閱(20261015-docs-registry-p0,第 3 輪,受審 revision r3-11470bae46a0d5bd;4 條 major 均處置)。P0 實作(
docs.py、registry 投影、三專案基線)已完成並通過審閱(20261015-docs-registry-p0-impl,第 3 輪,受審 revision r3-4d58c60ef77aecc0;5 條意見均處置);基線結果見 ADR-023 附錄 B 依據:ADR-023 文檔位置與特性 Registry 的 D3、D4 與實作分期 P0;追蹤項目 FU-142 本頁定義:registry 檔案放在哪、格式與驗證、檔案如何對應類型,以及入口程式docs.py的介面。ADR 保存政策與取捨,本頁是操作規範,不重複 ADR 的理由。
1. P0 範圍
| 做 | 不做(後續分期) |
|---|---|
共用層 registry 進 docs-site bundle,投影到各 docs 模組 | 交換文件搬家、publish/close、revision(P1) |
| 專案層 registry 的位置、合併規則與豁免(waiver) | D2 審閱提交守衛、D7 編譯前排除(P1) |
docs.py 的 lint、classify、audit(全部唯讀) | resolve/create/register、retention 自動判定(P2) |
/docs-site audit 同時執行 registry 對照 | gitignore、host 排除、scaffold 排除由 registry 產生(P1/P2) |
| 三個專案跑 audit 定基線 | PreToolUse hook(P3) |
P0 只新增唯讀能力,不改變任何既有 skill 的寫入位置;現行規則在後續分期通過前維持原狀。
2. 檔案位置
| 檔案 | 位置 | 管理方式 |
|---|---|---|
| 共用層 registry(canonical) | .claude/skills/docs-site/assets/site/docs-registry.json | 方法論檔,/methodology publish 下行 |
| 共用層 registry(投影) | {docsRoot}/docs-registry.json | /docs-site 受管檔,.docs-site.json 記錄雜湊,不可手改 |
| 專案層 registry | {docsRoot}/docs-registry.project.json | 專案自有,比照 site.project.ts;不存在代表沒有專案擴充 |
| 入口程式 | .claude/skills/docs-site/scripts/docs.py | 方法論檔;其他 skill 以「共用 docs.py」引用,比照 /br 的 br_trace.py |
| 測試 | .claude/skills/docs-site/scripts/test_docs.py | 方法論檔 |
為什麼投影到 docs 模組,而不是直接讀方法論目錄:
- docs 模組的建置(P1 的 D7 編譯前排除、導覽分類)需要在 build 時讀 registry;投影讓 docs 模組自足,host 建置不依賴
.claude/。 - 每個站台讀的是自己已套用的版本,與其他受管檔同一個版本單位;canonical 更新後由
/docs-site audit報出投影落後,再經 plan/apply 套用。 docs.py一律讀投影(站台實際使用的版本);/docs-site負責投影與 canonical 是否一致。
bundle 的受管檔由 assets/site/ 下的實際檔案推導,新增 docs-registry.json 不需要改 update.py;各站台在方法論發布後執行一次 /docs-site plan/apply 即取得投影。
3. Registry 檔案格式(schema 1)
兩層都是嚴格 JSON(不支援註解與尾逗號,與 package.json/tsconfig.json 的處理一致)。未知欄位一律拒絕,以抓出拼字錯誤。
3.1 頂層欄位
| 欄位 | 共用層 | 專案層 | 說明 |
|---|---|---|---|
schema | 必填 1 | 必填 1 | 不認得的 schema 版本直接拒絕 |
layer | "common" | "project" | — |
defaults | 選填 | 選填 | 只套用到同一層宣告的類型(ADR D3) |
scan | 必填 | 不允許 | 掃描範圍,見 §5 |
types | 必填 | 選填 | 類型清單,見 §3.2 |
extendCommon | 不允許 | 選填 | [{id, legacy}],只能對共用類型追加舊位置 |
waivers | 不允許 | 選填 | 已知欠帳的豁免,見 §6 |
3.2 類型欄位
欄位與值域沿用 ADR-023 Schema,另補三項 P0 實作需要的欄位:
| 欄位 | 說明 |
|---|---|
id | 小寫 kebab-case,全部層級唯一 |
paths、legacy | 路徑樣式,語法見 §4 |
exchange | 交換容器類型專用:{kind, anchors}。kind 是交換種類(例如 peer-review),anchors 是錨點檔的完整路徑樣式(與 paths 同語法、必含 {id}),用來判定孤兒,見 §4.3 |
exchangeKind | 路徑含 {id} 的類型必填,宣告其檔案屬於哪一種交換;必須對應某個容器類型的 exchange.kind |
inherit | "container";索引/分類類型專用 |
priority | 整數,預設 0 |
其餘欄位(category、role、lifecycle、close、derivedFrom、retention、authority、writer、change、scope、presentation、distribution、downstream、git、gitReason、belongsTo、base、purpose、status、note、title)的值域見 ADR。
3.3 驗證順序
- 結構:嚴格 JSON、必填欄位、值域、未知欄位、id 格式與唯一性。
- 合併:共用層先載入;專案層同 id 拒絕;
extendCommon只能有id、legacy,目標必須是共用類型。 - 推導:git 由
lifecycle推導,有git覆寫時必須有gitReason。 - lint:ADR 的 R1–R11。
前三步失敗屬「registry 無效」,docs.py 回 exit 2 且不做任何對照;lint 問題回報在結果中。
4. 路徑語法與比對
4.1 語法
- 相對於 workspace(含
project.json的modules/目錄),以/分隔,區分大小寫,不得含..或絕對路徑。 - 錨點:
{docsRoot}=docs 模組目錄名;{workspace}=workspace 根(只用於.claude/、output/等 workspace 層位置);{module}=project.json中 docs 模組以外的每個模組。 - 萬用字元:
*比對一個路徑段內任意字元;**比對零或多個路徑段;{id}比對一個路徑段中的一部分(可帶前後綴,例如{id}-evidence*),並把值擷取為交換 id。一個樣式最多一個{id}。 {id}採最長比對:例如x-evidence-review-evidence對{id}-evidence*擷取為x-evidence-review,不會因 id 本身含-evidence而截短。{id}不得為空、不得含/。
4.2 比對規則
- 對每個檔案找出所有命中的樣式(含
legacy)。 - 分數=樣式的字面字元數+
priority×1000;取最高分者。 - 最高分有兩個以上不同類型時,結果為
ambiguous,不猜。 - 命中類型有
inherit: "container"時,從其餘命中中取最高分、且不是繼承類型的那一個當容器;保存與發布限制(lifecycle、retention、distribution、close、scope、downstream、git)取容器的值,role與presentation保留索引類型自己的值。沒有容器時用索引類型自己的值。 - 命中
legacy時標記legacy: true,類型與特性照常判定。
4.3 交換 id 與孤兒
分組鍵是 (交換種類, id):檔案所屬類型的 exchangeKind 加上從路徑擷取的 {id}。不同種類的交換使用相同 id 時互不影響(例如審閱 x 與 work x 是兩組)。
錨點解析:容器類型的 exchange.anchors 是一組完整路徑樣式,以 workspace 為根解析,不相對於任何容器目錄。判定某組 (kind, id) 時,把 {id} 代入該種類的每個錨點樣式(樣式中其他萬用字元照常比對),任一錨點存在即為有效交換;全部不存在、但這組有任何成員檔案時,判為孤兒。錨點只代入同一組的 id,不會借用其他交換的錨點。
新舊位置寫在同一個容器的 anchors 裡,因此不需要「容器相對」的關係也能涵蓋同層的舊簡報。審閱交換的例子:
{
"id": "peer-review-exchange",
"paths": ["{docsRoot}/docs/exchanges/reviews/{id}/**"],
"exchangeKind": "peer-review",
"exchange": {
"kind": "peer-review",
"anchors": [
"{docsRoot}/docs/exchanges/reviews/{id}/brief.md",
"{workspace}/.claude/peer-review/{id}.md"
]
}
}
搭配 peer-review-evidence 的舊位置 {workspace}/.claude/peer-review/{id}-evidence*/**、{workspace}/.claude/peer-review/{id}-reviewer*/**(exchangeKind: "peer-review"):證據目錄 20261005-report-section-summary-evidence-r2/ 擷取 id 20261005-report-section-summary,若 .claude/peer-review/20261005-report-section-summary.md 與新位置的 brief.md 都不存在,即為孤兒。
lint 另檢查:exchangeKind 必須有對應的容器;每個容器的 anchors 至少一條、每條含 {id};同一 kind 只能有一個容器。
5. 掃描範圍
掃描範圍由共用層 scan 宣告,不寫死在程式:
{
"scan": {
"roots": [
"{docsRoot}",
"{workspace}/.claude/peer-review",
"{workspace}/.claude/work",
"{workspace}/.claude/docs",
"{workspace}/.claude/handoff.md",
"{workspace}/output",
"{module}/docs",
"{module}/FEATURE_CHANGELOG.md"
],
"excludeDirs": ["node_modules", "build", ".docusaurus", ".cache-loader", ".gradle", ".git"],
"excludeFiles": [".DS_Store", ".gitkeep"]
}
}
- 不存在的根略過;符號連結不跟隨,遇到時列為
DR-SYMLINK並略過。 .codex/與/tmp不在掃描範圍:前者是 agent 設定,後者不在工作樹內,audit 不宣稱能看見。D1 的文件用途義務仍由 rules/skills 與人審執行;不能只憑路徑判定這些位置寫入的是人類文件或合法設定/暫存。- P3 覆蓋澄清(2026-10-20 設計修訂):預定寫前 hook 限 Claude Code 的 Write/Edit 與 registry 掃描根;NotebookEdit、shell/MCP/外部編輯器及根外的語意文件不宣稱已攔截。詳見文件寫入守衛 §2/§4/§6。P3 設計已通過第 2 輪審閱(受審 revision
r2-a2b1504b61f97ef6),結論見該規格 §13;P3 實作已通過第 2 輪審閱(受審 revisionr2-38282cca1103fcfa,結論見該規格 §16),參考工作區尚未安裝/啟用;原生接線修訂及已採納裁決見該規格 §14~§15,不是全檔案系統存取控制。
6. 豁免(waiver)
已知欠帳在處理前會一直被報出;用專案層 waivers 明確記下決定,避免基線被淹沒:
{
"waivers": [
{
"code": "DR-EVIDENCE-IN-PAGE-TREE",
"paths": ["{docsRoot}/docs/testing/reports/**"],
"reason": "ADR-023 待決 3:證據是否移出 docs/ 尚未決定",
"owner": "FU-142",
"review": "2026-12-31"
}
]
}
code、paths、reason、owner必填;review為下次檢視日期,過期時 audit 以 warning 提醒。- 只能豁免 §7 標為「可豁免」的代碼;registry 無效、多重比對不可豁免。
- 被豁免的發現仍列在結果中(
waived欄寫原因),不計入--fail-on的判定。 - 判定順序:先逐檔產生發現 → 逐檔比對豁免(
code相同且路徑命中paths)→ 已豁免與未豁免分開彙整(見 §8.5)。同一目錄只有部分檔案被豁免時,其餘檔案的發現照常計入。 - 沒有命中任何發現的豁免列為
DR-STALE-WAIVER,比照 doc-lint 的「未生效 ignore 視為錯誤」,避免豁免殘留吃掉日後的真問題。 - 共用層不能宣告豁免。
- 寫入與安裝不沿用 audit waiver:waiver 保留本節既有彙整/
--fail-on語意,不將未登記位置變成已登記。P3 設計要求既有/新建未登記檔同樣拒絕,安裝前完整列出所有受影響檔(含 waived/ignored);清單非空時不寫入 hook 設定,先依用途補類型或修正位置,不能只加 waiver/ignore 或刪除原件過關。詳見文件寫入守衛 §4.3/§7;這是設計與實作均已通過審閱的 P3 安裝契約,結論見該規格 §16;不改現行 audit,也不新增 UAT/retention/交付閘門。
7. 發現代碼
| 代碼 | 嚴重度 | 可豁免 | 判定 |
|---|---|---|---|
DR-REGISTRY-INVALID | error | 否 | 結構、合併或推導失敗(exit 2) |
DR-LINT | error | 否 | R1–R11 任一條違反 |
DR-AMBIGUOUS | error | 否 | 同一檔案同分命中兩個以上類型 |
DR-UNREGISTERED | warning | 是 | 掃描範圍內的檔案沒有對應類型 |
DR-GIT-TRACKED | error | 是 | 應為 ignored 卻已追蹤 |
DR-GIT-UNPROTECTED | warning | 是 | 應為 ignored 卻既未追蹤也未被 gitignore |
DR-GIT-IGNORED | error | 是 | 應追蹤卻被 gitignore |
DR-ORPHAN | error | 是 | (kind, id) 組有成員檔案,但所有錨點都不存在(§4.3) |
DR-LEGACY | info | — | 檔案在舊位置 |
DR-OUTSIDE-DOCS | warning | 是 | 要呈現(presentation 不是 hidden)的檔案不在 docs 模組內(D1) |
DR-UNREACHABLE | warning | 是 | 要呈現的非 md 檔不在站台索引範圍(相依資源不計) |
DR-UNRENDERABLE | warning | 是 | page 類型中混有非 md 檔 |
DR-EVIDENCE-IN-PAGE-TREE | info | 是 | 證據放在 {docsRoot}/docs/ 下(ADR 待決 3 決定前只提示) |
DR-SYMLINK | warning | 是 | 掃描時遇到符號連結 |
DR-STALE-WAIVER | warning | 否 | 豁免沒有命中任何發現,或已過 review 日期 |
「尚未提交」(應追蹤、目前未追蹤也未忽略)是正常的工作中狀態,不列為發現,只在統計中呈現。
8. docs.py 介面
8.1 共通約定
python3 {docsSiteSkillRoot}/scripts/docs.py <verb> --workspace {workspaceRoot} [--docs-module {name}] [verb 參數]
{docsSiteSkillRoot}:/docs-siteskill 目錄;其他 skill 以「共用docs.py」引用。- docs 模組:從
project.json找type: docs的模組;只有一個時自動採用,多個時必須給--docs-module,不猜。 - 只使用 Python 標準函式庫與
gitCLI;不需要安裝套件。 - 輸出一律為一個 JSON 物件(
--format md例外,見audit),欄位依字母排序,比照/progressnative 腳本:
{
"schemaVersion": 1,
"tool": "docs.py",
"verb": "audit",
"status": "ok",
"readOnly": true,
"writes": 0,
"workspace": "/abs/path/modules",
"docsModule": "app-docs",
"registry": {"schema": 1, "commonSha256": "…", "projectSha256": null},
"errors": [],
"warnings": []
}
| exit | 意義 |
|---|---|
| 0 | 執行完成,且沒有達到 --fail-on 門檻的發現 |
| 1 | 執行完成,但有達到 --fail-on 門檻的發現 |
| 2 | 參數、registry 或 I/O 錯誤;status: "error",不輸出部分結果 |
P0 的動詞全部唯讀(readOnly: true、writes: 0)。
8.2 動詞與分期
| 動詞 | 分期 | 寫入 | 用途 |
|---|---|---|---|
lint | P0 | 否 | 驗證兩層 registry |
classify | P0 | 否 | 查詢路徑的類型與生效特性 |
audit | P0 | 否 | registry × 實際檔案 × git × 站台可達性 |
publish、close | P1 | 是 | 交換文件的呈現與收尾(D6、D2) |
resolve、create、register | P2 | create/register 是 | 類型+id 解析路徑、建立、登記導覽 |
P1 以後的動詞在各自分期另訂細節;P0 先保留名稱,避免日後改名。
8.3 lint
docs.py lint --workspace {ws}
結果欄位:status(ok/issues)、issues: [{rule, type, message}]、typeCount: {common, project}。
8.4 classify
docs.py classify --workspace {ws} --path {相對路徑} [--path …]
路徑不需存在,可在寫入前查詢(P3 的 hook 會用到)。每筆結果:
{
"path": "app-docs/docs/exchanges/reviews/demo/README.md",
"match": "matched",
"type": "section-index",
"layer": "common",
"legacy": false,
"exchangeKind": "peer-review",
"exchangeId": "demo",
"container": "peer-review-exchange",
"role": "document",
"presentation": "page",
"effective": {"lifecycle": "temporary", "retention": "remove-at-close", "distribution": "preview-only", "git": "tracked", "scope": "team", "downstream": null, "close": {"trigger": "/peer-review close", "owner": "作者"}},
"candidates": ["section-index", "peer-review-exchange"]
}
match 為 matched/unregistered/ambiguous;ambiguous 時 type 為 null,candidates 列出同分類型。
8.5 audit
docs.py audit --workspace {ws} [--format json|md] [--fail-on none|error|warning] [--include-waived]
[--registry-common {path}] [--registry-project {path}] # 只供基線盤點(§10.1)
--fail-on預設none(人工檢視);CI 與/docs-site audit用error。--format md輸出給人讀的報告,內容與 JSON 相同;仍不寫檔。
JSON 結果主要欄位:
| 欄位 | 內容 |
|---|---|
summary | 掃描檔數、命中類型數、各嚴重度發現數、被豁免數 |
types | 每個命中類型:檔數、位元組、git 狀態分布、應有 git 狀態、繼承來源 |
findings | [{code, severity, type, group, paths, pathsTruncated, count, bytes, exchangeId, message, waived}],彙整方式見下 |
lint | 同 lint 的 issues |
exchanges | 交換清單:(kind, id)、成員檔數、找到的錨點路徑 |
registry | 實際採用的 registry 來源:每層的路徑、SHA-256,以及來自投影或命令列覆寫(§10) |
彙整規則(避免部分豁免遮蔽整組):
- 每個發現先以單一檔案為單位產生,並逐檔決定是否被豁免。
- 彙整鍵為
(code, type, group, waived);group是所在目錄。已豁免與未豁免永遠不會併在同一筆。 count、bytes是該筆的完整數量;paths只是展示用,最多 20 個,超過時pathsTruncated: true。--fail-on以第 1 步的逐檔未豁免發現判定,不看展示用的截斷清單;summary的各項計數也由逐檔結果計算。
9. 與 /docs-site 的整合
/docs-site audit在原有 bundle 對照後執行docs.py audit --fail-on error,兩者結果分開報告;bundle 落後時 registry 對照讀的是舊投影,報告須註明。- plan/apply 不需新增邏輯:
docs-registry.json是一般受管檔。docs-registry.project.json不由 bundle 建立,專案需要時自行新增。 m-docs-site.md補「registry 投影與專案層」段落;/docs-siteSKILL.md 補docs.py用法與工具回歸測試。
10. 三專案基線
三個專案都還沒有專案層 registry(已核對三個 docs 模組都沒有 docs-registry*.json)。基線一律用同一套流程,區分「輸入差異」與「工具判定差異」。
10.1 輸入
| 專案 | 共用層 | 專案層 |
|---|---|---|
| 參考實作(app-docs) | 方法論下行後經 /docs-site plan/apply 取得的投影 | 唯讀覆寫 --registry-project(草稿) |
| florae-g1、nbsos | 唯讀覆寫 --registry-common(框架 canonical;下游尚未投影) | 唯讀覆寫 --registry-project(草稿) |
- 專案層草稿:把 ADR 附錄 B 三份專案擴充依 schema 1 重寫(
base: other、必填purpose),放在目標 repo 以外的本次 P0 實作審閱交換證據中。參考實作也一樣:它的擴充(設計語言對齊證據、參考實作稽核證據、UAT 證據來源對應、測試靜態證據)同樣只作基線輸入。 - 參考實作暫不建立正式專案層:app-docs 是 scaffold 來源,正式的
docs-registry.project.json會被/scaffold-module複製到新專案,把參考實作專屬的擴充帶到下游。要不要建立、以及 scaffold 時如何排除或清空,留給參考實作決定採用時一併處理(見 §13 註)。 - 覆寫參數的適用範圍:
--registry-common {path}:只在目標站台尚未投影時使用。--registry-project {path}:未投影或已投影站台都可使用,只供基線盤點;目標站台已有正式專案層時拒絕覆寫,避免兩份專案層並存。- 有覆寫時,結果的
registry欄標明每層的source(projection/override)、路徑與 SHA-256。/docs-site audit不使用覆寫參數。
10.2 每個專案都跑兩組
- common-only:只用共用層,呈現專案尚未宣告任何擴充時的實際狀態。
- common+專案層草稿:與 ADR 附錄 B 的輸入等價。
10.3 歸因規則
- 兩組之間的差異(主要是未登記數量)歸因於輸入差異:缺少專案擴充。
- 與 ADR 附錄 B 對照時只用第 2 組;剩餘差異逐項歸因為工具判定差異(原型與正式工具的規則不同)或期間新增的檔案,不能把輸入差異算成工具差異。
- 對照表記錄兩邊實際採用的 registry 來源與 SHA-256(附錄 B 的原型輸入以 ADR 附件的
registry.common.json與當時的專案擴充為準)。
10.4 結果處置
各專案的已知欠帳先列出建議的豁免清單與專案層草稿,由各專案決定是否採用;P0 不替任何專案(含參考實作)寫入豁免或正式專案層。
11. 測試
test_docs.py 以暫存 workspace+git init 建立 fixture,至少涵蓋:
| 類別 | 測試 |
|---|---|
| 格式 | 嚴格 JSON、未知欄位、缺必填、值域錯誤、schema 版本不符都回 exit 2 |
| 合併 | 專案 defaults 不影響共用類型;同 id 拒絕;extendCommon 只能追加 legacy |
| 比對 | 分數與 priority;同分不同類型為 ambiguous;{id} 擷取(含前後綴、最長比對、id 本身含 -evidence) |
| 繼承 | 審閱證據與審閱根目錄的 README/_category_.json;報表 run README;無容器時用自身值 |
| lint | R1–R11 各至少一個違反案例 |
| git | tracked/ignored/unprotected/尚未提交 四種狀態的判定 |
| 孤兒 | 新位置錨點;舊位置證據對同層舊簡報;新舊並存;同一 kind 中有錨點與無錨點的兩組只判後者;不同 kind 使用相同 id 不互相借用錨點;-reviewer* 舊目錄 |
| 豁免 | 生效、不可豁免代碼被拒、未命中的豁免、過期提醒;同目錄只有部分檔案被豁免時其餘照常計入;超過 20 檔且含未豁免檔時 --fail-on 仍觸發、count 完整 |
| 基線輸入 | 覆寫時結果記錄每層來源(projection/override)與雜湊;已投影站台拒絕 --registry-common;已有正式專案層時拒絕 --registry-project;已投影站台可用 --registry-project;不寫入目標 workspace |
| 介面 | JSON envelope 欄位、exit code、readOnly/writes、classify 對不存在路徑可用 |
| 唯讀 | 執行前後 workspace 檔案與 git index 完全相同 |
12. 原型處置
ADR 附件中的原型(/methodology/docs-registry/ 下的 registry.common.json、audit.py、test_audit.py)是審閱證據。P0 正式工具通過後保留原檔,ADR 附錄改註「已由 docs.py 取代」並連到本頁,不再更新原型。
13. P0 會修改的檔案
| 檔案 | 變更 |
|---|---|
.claude/skills/docs-site/assets/site/docs-registry.json | 新增:共用層 registry(由原型轉為 schema 1,補 scan、exchange/exchangeKind、{id}) |
.claude/skills/docs-site/scripts/docs.py、test_docs.py | 新增 |
.claude/skills/docs-site/SKILL.md | audit 模式加 registry 對照、docs.py 用法與測試 |
.claude/rules/m-docs-site.md | registry 投影與專案層段落 |
.claude/rules/m-methodology.md | 索引登錄 registry 與 docs.py |
appfuse-docs/docs-methodology/decision-records/023-docs-registry.md | 附錄 A 指向本頁;變更歷史 |
app-docs(參考實作) | /docs-site apply 後新增投影 docs-registry.json 並更新 .docs-site.json |
註:P0 不建立任何正式專案層。日後參考實作若決定採用 docs-registry.project.json,須同時讓 /scaffold-module 排除或清空該檔,避免參考實作專屬擴充下行;此修改屬採用當時的變更範圍。
14. 審閱時請特別看
- registry 投影到 docs 模組(而非直接讀方法論目錄)的取捨。
(kind, id)分組與exchange.anchors是否足以判定新舊位置的孤兒。- 發現代碼的嚴重度與可否豁免,特別是
DR-EVIDENCE-IN-PAGE-TREE先列 info。 - 豁免機制是否會被濫用,
DR-STALE-WAIVER與review日期是否足夠。 - 下游基線只做唯讀盤點、不寫入下游 repo 是否合適;
--registry-common/--registry-project是否只該在 P0 盤點時使用。