文件解析與索引規格
狀態:P2-1 設計已通過審閱(20261016-docs-resolve-p2-1,第 4 輪,受審 revision r4-cfb48816b403fb1f;8 條意見均處置);P2-1 實作已通過審閱(20261016-docs-resolve-p2-1-impl,第 3 輪,受審 revision r3-1b702f80d5efef53;4 條意見均處置),實作中確認的修訂見 §10 依據:ADR-023 的 D3、D4 與實作分期 P2;文檔 Registry 規格;交換文件規格;追蹤項目 FU-142 本頁定義:skill 如何向 registry 取得文件路徑(
docs.py resolve),以及站台索引llms.txt如何由 registry 與頁面規則產生(docs.py register)。內容照常由 skill 以 Read/Edit 撰寫。
1. P2 的分期與本頁範圍
P2 分六個階段,各自設計、實作與審閱;全部通過後才一次 /methodology publish,中間階段只在框架提交,下游不會看到「部分 skill 已遷移」的狀態。
| 階段 | 內容 |
|---|---|
| P2-1 工具(本頁) | registry 的產生範本與參數;resolve;register;補齊掃描範圍內的 registry 缺口 |
| P2-2 規格類 | /roadmap、/feature-list、/epic、/us、/sbe、/checklist、/assess、/analyze 改用 resolve/register,並為其類型加上範本 |
| P2-3 設計類 | /domain-model、/api-spec、/system-context、/persona |
| P2-4 驗證類 | /uat-script、/uat(含 evidence.py)、/report、/sim-plan、/impact |
| P2-5 其他 | /br、/transcribe、/rfc、/rfc-intake、/progress、/sprint、/follow-up、/presentation-design、/design-language;/tmp 中給人看的檔案移出;另盤點設計類讀取方 /assess、/analyze、/impl、/next、/impact、/us 的位置字樣,更新為已遷移的 resolve 契約(包含 /assess 的「Domain Model 沿用現行規則」),不改其文件內容與業務流程 |
| P2-6 保留 | /uat 快照與 /report run 的 retention;ADR-020 原樣張對齊 |
P2-1 不改任何產出文件的 skill;只提供工具與 registry 欄位。各類型的範本與 skill 的改寫,隨對應批次一起送審,讓審閱者能把範本與實際使用它的 skill 對照。
| 做 | 不做 |
|---|---|
類型欄位 template/locate/params/key/example,lint 新增 R12 | 各類型的範本與查詢樣式(隨 P2-2~P2-5) |
docs.py resolve(唯讀) | 改寫任何產出文件的 skill |
docs.py register(只寫 llms.txt 的受管區塊)與 --check | 側欄、_category_.json、README 目錄等內容性導覽(見 §5.1) |
permalink 取自站台的路由紀錄(docs-site-routes 外掛),以內容雜湊核對新鮮度 | create 動詞(併入 resolve,見 §4.5);目錄範本與交換類型的範本(§4.4) |
| 補齊掃描範圍內、已有產出者的缺口類型(§6) | 掃描範圍外的位置(workspace presentations/、mockup 的 design-language 產物:P2-5 決定) |
audit 新增索引同步檢查 | retention 自動判定(P2-6) |
2. 為什麼需要範本
registry 的 paths 是比對用的樣式(例如 US 是 {docsRoot}/docs/specs/user-stories/**/US-*.md),可以判斷一個既有檔案是什麼類型,卻無法從「US-201」產生它應該在哪裡。要讓 skill 不再自己寫死路徑,每個由 skill 產出的類型都需要一個產生範本,說明「給哪些參數、得到哪個路徑」。反過來,查詢既有文件時要從路徑擷取編號等參數,並涵蓋舊佈局,因此另需查詢樣式(§3 的 locate)。
3. Registry 欄位(schema 1 擴充)
新增五個選用的類型欄位,舊投影不含這些欄位也仍有效。產生新路徑與查詢既有文件分開描述(R1-02):
| 欄位 | 用途 | 說明 |
|---|---|---|
template | 產生 | 新文件的路徑範本,語法同 paths(錨點 {docsRoot}/{workspace}),另含具名參數 {name};不得含 *、**。只描述檔案,不描述目錄(R1-03) |
params | 兩者 | 參數名 → 值的 regex(完整比對)。值一律不得含 /、不得為 . 或 ..(工具另行強制,不靠 regex) |
key | 查詢 | 識別一份文件的參數子集(例如 US 只有 us)。key 以外的參數(epic、slug)是屬性:產生新路徑時需要,查詢時只用來比較(R1-01)。單一文件(roadmap)的 key 為空 |
locate | 查詢 | 一組含具名參數的完整路徑樣式,涵蓋這個類型所有合法佈局,包含 legacy 位置;用來從既有檔案擷取參數。template 必須也列在其中 |
example | lint | 每個參數的範例值(R12) |
範例(實際欄位隨 P2-2 送審,此處僅示意;第二條 locate 是假設的舊佈局):
{
"id": "us",
"paths": ["{docsRoot}/docs/specs/user-stories/**/US-*.md"],
"template": "{docsRoot}/docs/specs/user-stories/epic-{epic}/US-{us}-{slug}.md",
"locate": [
"{docsRoot}/docs/specs/user-stories/epic-{epic}/US-{us}-{slug}.md",
"{docsRoot}/docs/specs/user-stories/US-{us}-{slug}.md"
],
"params": {"epic": "^[0-9]+$", "us": "^[0-9]{3}$", "slug": "^[a-z0-9]+(-[a-z0-9]+)*$"},
"key": ["us"],
"example": {"epic": "2", "us": "201", "slug": "create-order"}
}
lint R12(範本一致性):
template與每條locate的具名參數都在params宣告;params沒有多餘參數;key是每條locate參數的子集;template列在locate中。example涵蓋所有參數且符合 regex;以它填入template的路徑經classify命中同一類型、matched、非legacy。- 以
example填入每條locate的路徑都必須命中同一類型(legacy佈局可為legacy: true)。
R12 只證明範例成立,不保證所有參數值都成立;每次產生新路徑時另做執行期驗證(§4.2)。
4. docs.py resolve(唯讀)
docs.py resolve --workspace {ws} --type {typeId} [--param name=value ...] [--format json]
4.1 查詢既有文件
- 類型沒有
template/locate→ exit 2(RS-NO-TEMPLATE),提示該類型尚未遷移。給定參數未宣告或不符 regex → exit 2(RS-PARAM)。 - 列出這個類型的所有既有檔案:依
paths與legacy掃描(不跟隨符號連結),經classify命中該類型者。 - 擷取參數:每個檔案依序比對
locate,取第一條比對成功者的參數。任何檔案無法以locate擷取時,resolve拒絕(exit 1,RS-UNPARSED),列出這些檔案;不在無法確認的情況下回報「不存在」(R1-02)。docs.py audit另以DR-UNPARSED-LOCATION(warning,可豁免)列出這些檔案,讓既有專案在 skill 改用resolve前,先改名或在locate補上舊佈局。 - 候選=
key參數與給定值完全相同的檔案(key參數必須全部給定,否則 exit 2,RS-KEY)。跨目錄也算同一份:例如 US-201 同時出現在epic-2/與epic-3/即為兩個候選。 - 結果:
| 候選 | 結果 |
|---|---|
| 1 個 | exists: true、path 為該檔案、legacy 依 classify;回傳擷取的全部參數。給定的屬性參數與擷取值不同時列在 conflicts(例如新的 slug),不會因此改回報不存在或產生同編號的新路徑(R1-01) |
| 0 個,產生用參數齊全 | exists: false、path 為新路徑(須通過 §4.2) |
| 0 個,產生用參數不齊全 | exists: false、path: null,列出缺少的參數 |
| 2 個以上 | exit 1(RS-AMBIGUOUS),列出全部候選與擷取的參數,不猜 |
4.2 新路徑的執行期驗證(R1-04)
回傳任何新路徑前逐項檢查,任一失敗即 exit 1(RS-UNSAFE),不回傳路徑:
- 參數值不含
/、不是.或..;填入後的路徑正規化後不變,且位於 workspace 內。 - 路徑上已存在的每一層都不是符號連結。
classify命中同一類型、matched、非legacy。專案層加入更精確的類型時,結果以此為準,不以 lint 時的範例為準。
4.3 輸出
沿用 P0 的 JSON 外層(readOnly: true、writes: 0),本體:
{
"type": "us",
"exists": true,
"path": "app-docs/docs/specs/user-stories/epic-2/US-201-create-order.md",
"legacy": false,
"params": {"epic": "2", "us": "201", "slug": "create-order"},
"missing": [],
"conflicts": [{"param": "slug", "given": "new-order", "existing": "create-order"}],
"effective": {"lifecycle": "persistent", "git": "tracked", "distribution": "site", "...": "..."}
}
effective 取自 classify。
4.4 目錄與交換類型
template/locate 只描述檔案(R1-03)。交換目錄(審閱與交付)的位置仍由 exchange.py 從容器類型推導,P2-1 不為交換類型加範本;需要目錄的 skill(例如寫入證據)使用 docs.py evidence/publish 回報的目錄,或對目錄內的具體檔案類型使用 resolve。
4.5 不另設 create
ADR-023 D4 列了 create。內容由 skill 以 Write/Edit 撰寫,Write 會建立上層目錄;create 剩下的只有「確認不存在、算出安全路徑」,與 resolve 回傳新路徑時相同。P2-1 因此不實作 create,ADR 隨本設計通過修訂 D4。需要目錄索引檔(例如 SBE 的 _category_.json)的 skill,照常自己寫入;那是內容,不是位置。
5. docs.py register(站台索引)
docs.py register --workspace {ws} [--check] [--format json]
5.1 範圍
站台的側欄由 docs-site bundle 依目錄自動產生,新增頁面不需要登錄;_category_.json 的標籤、epics/README.md 等目錄頁是內容,由 skill 維護。剩下需要「登錄」的只有 static/llms.txt,因此 register 只負責它。
5.2 受管頁面
由 registry 推導,不另列清單:檔案在 {docsRoot}/docs/ 下、副檔名為 .md/.mdx、classify 命中的類型 presentation: page、生效 distribution: site、且不屬交換(無 exchangeKind)。_ 開頭的檔案與標準建置排除的路徑不納入。register 只管理這些頁面的條目;其他條目(例如 /project-docs、分類首頁、靜態資產連結)不屬受管範圍(R1-05)。
5.3 permalink 只取自站台的路由紀錄(R1-06、R2-01)
permalink 不自行計算。Docusaurus 的分類首頁判定使用未去前綴的檔名(01-README.md 對應 /…/README,不是目錄首頁),並受 frontmatter parse_number_prefixes、全站 numberPrefixParser、baseUrl 等設定影響;只採站台實際算出的結果,才能與路由一致。
.docusaurus/ 下的 metadata 檔不能單獨使用:修改 baseUrl 等設定而未重新載入時,來源檔與 metadata 都沒變;反之,Docusaurus 開發模式在 metadata 內容不變時保留原檔與修改時間,只改正文時 metadata 仍會早於來源檔。修改時間因此既會放行錯誤網址,也會誤擋正確網址。
路由紀錄:docs-site bundle 新增外掛 docs-site-routes,在 allContentLoaded 寫入 node_modules/.cache/docs-site/routes.json(一律不進 git)。這是 Docusaurus 算出全部路由的時點;開發服務重新載入任何外掛或設定時都會重跑,build 也會經過。紀錄內容:
| 欄位 | 內容 |
|---|---|
loadedAt、command | 寫入時間;start 或 build |
docusaurusVersion | 實際載入的 @docusaurus/core 版本 |
inputs | 影響路由的設定檔 SHA-256:docusaurus.config.ts、site.project.ts 與 registry 兩層 |
effective | 實際生效的 url、baseUrl、trailingSlash,及各 docs 外掛的 path、routeBasePath |
docs[] | 每份文件的 source、permalink、title、sourceSha256;雜湊以外掛讀到的同一份 bytes 計算。若重新解析的 frontmatter 與 Docusaurus 載入結果不同(載入後檔案又被改),該文件不記雜湊 |
register 寫入前逐項核對,任一不成立即 exit 1(RG-ROUTES),不寫入,並提示啟動或等待審閱預覽重新載入、或執行 npm run build:docs-only:
- 路由紀錄存在且可解析。
docusaurusVersion等於目前安裝的版本;inputs的每個檔案 SHA-256 都等於目前內容(設定改了、站台還沒重新載入就拒絕)。- 每個受管頁面都有紀錄,且
sourceSha256等於目前來源的 SHA-256(只比內容,不比修改時間;正文改過、站台已重新載入就接受)。
--check 遇到同樣情況時回報路由紀錄不可信,exit 1。輸出列出所採用的 effective 設定。
不在支援範圍:專案設定若依環境變數決定 baseUrl 等路由設定,紀錄反映的是寫入那次執行的設定;register 不判斷它是否等於正式部署的設定,只在輸出列出 effective 供人核對。專案設定若再引用其他檔案,這些檔案不在 inputs 中,修改後須重新載入站台。
5.4 寫入方式
llms.txt以標記區塊管理:<!-- >>> docs-site: 頁面索引(由 registry 產生,勿手改) -->到<!-- <<< docs-site -->。只有區塊內由register管理;區塊外的標題、說明、專案條目與段落一律不動。- 區塊內依路由第一段分組(
## {段}),組內依 permalink 排序,每行- {permalink} - {描述}。 - 描述:區塊內既有條目的描述保留;新頁面取 frontmatter
sidebar_label,沒有時取路由紀錄的title,超過 12 字截斷並加「…」。 - 首次導入(檔案還沒有標記區塊):
- 只把「permalink 屬於受管頁面」的
- {permalink} - {描述}行移入區塊,描述沿用;其他行留在原處。 - 移走條目後只剩標題、沒有任何內容的
## {段}標題一併移除;仍有非受管條目的段落與其標題保留。 - 區塊放在原檔第一個被移走條目的位置。
- 只把「permalink 屬於受管頁面」的
- 冪等:重建內容與現況相同時不寫入;第二次執行不產生差異。
--check:只比對、不寫入;有新增、移除的受管條目或區塊外被誤收的條目時 exit 1,列出各類差異。
5.5 與 audit 的整合
docs.py audit 新增發現代碼 DR-INDEX-STALE(warning,可豁免):register --check 會產生差異時回報,列出缺漏與多餘的 permalink;路由紀錄不可信時改報 DR-INDEX-UNVERIFIED(info),不判定同步與否。
6. 補齊的 registry 類型
只補掃描範圍內、已有產出者或已有檔案的缺口;範圍外的位置留給對應批次:
| 類型 | 路徑 | 產出者 | 備註 |
|---|---|---|---|
impact-report | {docsRoot}/docs/testing/impact-reports/*.md | /impact | persistent、page |
rfc-submission | {docsRoot}/docs/rfcs/submissions/*.md | /rfc --keep-submission | persistent、page;預設寫 /tmp 的行為在 P2-5 處理 |
uat-origin | {docsRoot}/docs/testing/uat-results/origins/** | /uat 歷史匯入 | persistent、hidden、evidence;retention 於 P2-6 定 |
requirements-agreement | {docsRoot}/docs/requirements/agreements/** | 人工(/roadmap、/domain-model 讀取) | persistent、page、downstream: exclude(需求來源,R6) |
參考實作 app-docs 的 static/design-language(64 MB)、static/reference-audits、static/reference-demo、static/testing/evidence 是這個 workspace 特有的內容,沒有方法論產出者;P2-1 只在 audit 報告中列出,是否建立參考實作的專案層屬待決 7,不在本階段決定。
7. 測試
| 類別 | 測試 |
|---|---|
| R12 | 缺參數宣告、多餘參數、key 不是 locate 參數的子集、template 未列在 locate、example 不符 regex、example 路徑命中其他類型或 legacy 時 lint 報錯;正確範本通過 |
| resolve 查詢 | 候選 1 個/0 個且參數齊全/0 個且不齊全/2 個以上;給不同 slug 時回傳既有檔案並列出 conflicts,不產生同編號的新路徑;同編號出現在兩個 epic 目錄時回報 RS-AMBIGUOUS;只存在於 legacy 佈局時回報 legacy: true;同類型有任一檔案無法以 locate 擷取時回報 RS-UNPARSED;未給 key 參數時回報 RS-KEY;參數 regex 錯誤;類型無範本;不跟隨符號連結 |
| resolve 新路徑 | 參數值為 ..、含 /、正規化後改變時拒絕;路徑上有符號連結時拒絕;專案層新增較精確類型、使新路徑命中其他類型時回報 RS-UNSAFE(R12 範例仍通過的反例) |
| permalink 來源 | 路由紀錄缺失時拒絕(RG-ROUTES);改了 baseUrl 等設定、來源未變且站台未重新載入時拒絕舊紀錄;只改正文、.docusaurus metadata JSON 未變(修改時間早於來源)但站台已重新載入時接受;正文改過、尚未重新載入時拒絕;Docusaurus 版本改變時拒絕;載入後 frontmatter 又被改的文件不記雜湊並拒絕;--check 回報不可信;以 01-README.md(對應 /…/README)、parse_number_prefixes: false、日期與版號名稱、frontmatter slug/id 的 fixture 確認只採用紀錄中的值。外掛以實際啟動 start:docs-only 與 build:docs-only 的整合測試驗證紀錄內容 |
| register | 首次導入只移入受管頁面的條目並保留描述;/project-docs 等非受管條目與段落留在原處;只剩標題的段落移除;第二次執行無差異;新增頁取 sidebar_label/title 並截斷;刪除頁移除;交換、preview-only、_ 檔不納入;--check 只比對 |
| audit | DR-INDEX-STALE 的回報與豁免;路由紀錄不可信時改報 DR-INDEX-UNVERIFIED;有 locate 的類型中無法擷取參數的檔案回報 DR-UNPARSED-LOCATION |
| 交換 | P1 的 39 個交換測試全部通過;route_id/stripNumberPrefix 補上日期與版號例外(2026-10-16-x 不去前綴) |
8. 會修改的檔案
| 檔案 | 變更 |
|---|---|
/docs-site 的 docs.py、新增 resolve.py(或併入既有模組)、docs-registry.json、測試 | 欄位與 R12、resolve、register、路由紀錄核對、DR-INDEX-STALE/DR-INDEX-UNVERIFIED/DR-UNPARSED-LOCATION、缺口類型 |
/docs-site bundle 新增 scripts/routes.cjs(docs-site-routes 外掛)並在 docusaurus.config.ts 註冊 | 寫入路由紀錄(§5.3) |
/docs-site bundle 的 exchanges.cjs、exchange.py 的 route_id | 修正 P1 的遺漏:去數字前綴的規則補上 Docusaurus 的日期與版號例外,讓洩漏掃描的路由形式與審閱 id 驗證和實際路由一致。交換目錄的位置仍由 exchange.py 推導(§4.4) |
/docs-site SKILL.md、m-docs-site.md | 動詞說明;llms.txt 受管區塊;register 前須有最新的路由紀錄 |
app-docs 11-llms-txt-sync.md(docs practices) | 改為「新增、刪除、搬移頁面並重新載入站台後執行 docs.py register」 |
| 參考實作 app-docs | 投影更新;llms.txt 首次導入受管區塊 |
| ADR-023 | D4 修訂(不設 create;resolve 查詢含舊位置,以 key 判定同一份文件);變更歷史 |
m-methodology.md | 索引 |
9. 待決與審閱重點
已決(第 1 輪審閱後):
- permalink 只採站台實際算出的結果,不自行計算(R1-06);新鮮度以 bundle 外掛寫的路由紀錄與內容雜湊核對,不比修改時間(R2-01)。代價是
register前須有審閱預覽在跑,或執行 docs-only 建置;換來與實際路由一致,且紀錄缺失或過期時不會寫入錯誤網址。 resolve的查詢與產生分開:以key判定同一份文件,以locate涵蓋所有佈局(R1-01、R1-02)。- 範本只描述檔案,交換目錄仍由
exchange.py推導(R1-03)。
待決:
- 新頁面描述的截斷長度(12 字)與來源優先序(
sidebar_label先於title)。 - 框架文檔站 appfuse-docs 不是 docs-site 站台,
llms.txt有自己的 lint;register不處理它。
請特別看:
- 路由紀錄以
allContentLoaded的內容載入為準,不以前端打包是否成功為準(§5.3):路由在內容載入時就已決定,打包失敗不改變網址;頁面能否開啟由 docs-site 的建置與實際瀏覽驗證,不由register判斷(audit是靜態盤點,不驗證編譯結果或 HTTP 可用性)。 key/locate的分工能否涵蓋 US、SBE、Epic 等主要類型的實際佈局(各類型的值隨 P2-2 送審)。RS-UNPARSED讓整個類型 fail-closed:一個命名不合規的舊檔會擋住該類型的所有查詢。這是刻意的(不在無法確認時回報不存在),但也可能在既有專案造成阻擋,需由audit的DR-UNPARSED-LOCATION先行揭露。- 首次導入只接管受管頁面的條目,是否足以保留專案手寫的
llms.txt內容。 - 補齊的四個類型(§6)的特性是否合適。
10. 實作修訂
實作與參考實作的實際站台驗證中,確認下列設計需要補正,隨 P2-1 實作一併送審:
| 項目 | 原設計 | 修訂 | 理由 |
|---|---|---|---|
| 首次導入的區塊位置(§5.4) | 放在第一個被移走條目的位置 | 該條目所屬的 ## 段落因此變空時,區塊取代該段落;仍有其他內容時,區塊放在該段落之後 | 照原設計會把仍有非受管條目的段落切成兩半,剩下的條目看起來屬於區塊的最後一段 |
| 專案層補舊佈局(§4.1) | 「在 locate 補上舊佈局」未定義落點 | 專案層 extendCommon 可附加 locate(與 legacy 並列);共用類型須已有 locate | 專案層不能改寫共用類型,舊佈局的查詢樣式需要一個明確的附加入口 |
| 參數值的結構限制(§4.1、§4.2) | 未分 | 含 /、\、NUL,或為空、.、.. 一律 RS-UNSAFE(exit 1);不符 regex 為 RS-PARAM(exit 2);未知類型為 RS-TYPE(exit 2) | 結構限制屬安全檢查,由工具強制,不依賴各類型的 regex |
| 站台刻意未載入的頁面(§5.3) | 每個受管頁面都須有紀錄 | 路由紀錄另記 unloaded(docs 目錄中站台未載入的頁面及其雜湊,例如被專案排除設定或草稿排除),以及每份文件的 draft/unlisted;這些頁面不列入索引,內容與紀錄相同時不阻擋,改過則拒絕 | 否則被專案設定排除的頁面會讓 register 永遠拒絕 |
| 設定檔的重新載入(§5.3) | 「重新載入站台」 | Docusaurus 開發服務只監看 docusaurus.config.ts 本身,site.project.ts 與 registry 改了不會自動重載;拒絕訊息改為提示重啟預覽或執行 build:docs-only | 參考實作實測:改 site.project.ts 後 90 秒內沒有重載,register 持續拒絕(行為正確,只是提示要準確) |
| 區塊標記損壞 | 未定義 | 開始或結束標記缺失、重複或順序顛倒時拒絕(RG-BLOCK),不猜測區塊範圍 | 避免把區塊外的手寫內容當成受管內容重寫 |
| 根層頁面的分組(§5.4) | 依路由第一段 | 只有一段的路由(/、/intro)歸入區塊開頭、沒有標題的首組 | 這些頁面沒有可作為段落名稱的上層 |
| 首次導入的回報 | 未定義 | added 只列原本不在 llms.txt 的頁面;從區塊外移入的列在 stray | 讓首次導入的回報能看出哪些是真正新增的頁面 |
| 設定檔雜湊的取得時點(§5.3,實作審閱 R1-01) | 紀錄中的 inputs 未定義何時計算 | 在設定被求值時(createConfig)計算並交給外掛;內容重載不重新求值設定,紀錄沿用載入時的雜湊 | 若在 allContentLoaded 才讀檔,改了設定、再由文件觸發內容重載時,紀錄會含新設定的雜湊與舊的 permalink,register 誤判為最新 |
| 含空白的 permalink(§5.4,實作審閱 R1-02) | 條目格式 - {permalink} - {描述} | 解析時先以路由紀錄中已知的 permalink 比對(取最長者),再退回不含空白的樣式 | Docusaurus 依檔名產生的 permalink 可含空白(例如 US-06 組織.md);以空白切分會漏認既有條目、產生重複列並覆寫手寫描述 |
| 參數值的比對方式(§3,實作審閱 R1-03) | 「值的 regex(完整比對)」 | 擷取時不把參數 regex 嵌入整條路徑,而是在字面片段之間回溯切分,每個值各自以宣告的 regex 做 fullmatch | 去掉錨點後嵌入會改變交替式(^foo$|^bar$)與 inline flags((?i)…)的語意,造成 lint 通過、有效文件卻被報 RS-UNPARSED |