Archive Spec (封存規格)
透過在已解析的封存根目錄下寫入 <slug>.md,並移除 <resolved-spec-root>/<slug>/ 來封存已完成的規格;該資料夾會保留在 Git 中記錄的 source_revision。來源是已配置的 Spec Root;目的地是其已解析的封存根目錄 — 對於內建的 docs/specs 根目錄為 docs/history/specs/,對於外部或非預設根目錄則為 <spec-root>/_archived/,其位置與 roundfix archive 的目標一致。正常的封存意味著已實作、已驗證且自我包含 — 每個任務都已完成,當 Task Graph 設定了 QA 關卡時 QA 已通過,且每個索引參考都歸屬於該規格。QA 封存覆寫則記錄明確的例外情況,而不聲稱已驗證。在這兩種處置之後,執行一次 ls <spec-root>/ 即可區分進行中的工作與歷史記錄,而封存記錄作為已建置內容及其原因的記錄,仍可透過 grep 搜尋。
觸發條件是規格完成,而非發布:在 Roundfix Spec Run 結束、QA 關卡通過後自動執行此動作,或在使用者要求時執行。合併和發布是分開的、由使用者驅動的步驟 — 封存提交僅隨分支移動,並在功能本身的 PR 內提交。
Archive Relink (封存重新連結)
在寫入封存記錄之前,封存會執行 Archive Relink。它會自動將即時文件 (Live Document) 引用的每個非核心檔案,從擁有者規格提升到 docs/references/<basename>,然後將每個即時文件的引用重寫為指向提升後的副本;對規格資料夾或核心工件的引用則指向封存記錄。當存在任何重寫的檔案時,記錄會在 relinked 下列出它們。來源保留一個擁有者,因此次要規格不需要副本 (ADR-0260、ADR-0083)。
規格資料夾會透過刪除而退役。在複製的結果和每個提升的副本中,已移除資料夾內的目標會命名一個提升的副本,或成為一個 Git 定位器 `<path>@<commit>`;它永遠不會指向記錄本身。該提交包含路徑,並且可以從預設分支或標籤到達。當不存在這樣的提交時,定位器會命名該路徑和記錄的 pull request。其他目標則保留其儲存庫目標 (ADR-0278)。
當提升目標已存在、兩個提升共用一個基礎名稱、或要重寫的即時文件在 HEAD 上有未提交的變更時,封存會拒絕寫入。非即時文件中的引用,或重寫後仍存在的引用,會產生警告,指出檔案、行號和目標;封存會保持這些檔案不變。
Squash merges (壓縮合併)
壓縮合併僅在 pull request 的頭部引用中保留 source_revision;記錄會在 pull_request 中命名該 pull request。
Declared QA decline (聲明式 QA 拒絕)
聲明 qa: declined 且包含非空 qa_reason 的 Task Graph,會在每個任務都完成後封存一次,無需 QA 報告也無需覆寫,其封存記錄的處置為 no-qa。對於此類規格,會拒絕 --qa-override。所有其他封存前提條件仍然適用。
QA settlement (QA 結算)
相同的結果會結算被撰寫的 qa 任務,並決定封存留下的內容:
| 結果 | 結算方式 | 封存方式 |
|---|---|---|
pass | 將 QA 任務結算為 completed,並在報告沒有不被允許的封鎖行時使規格符合封存資格。 | 封存記錄,其中包含 QA 報告和裁決;規格、其 QA 報告和證據會保留在 Git 中記錄的 source_revision。 |
符合條件的聲明 partial | 在沒有行失敗、被跳過或因發現而受阻、每個因聲明而受阻的行都有一個匹配的 ## Unreachable Acceptance (無法達到的驗收) 聲明、且每個因環境而受阻的行都是 PR 前的 Pull Request 行(記錄為 blocked (environment: no open Pull Request) 並在其來源中命名該 Pull Request 行),或是執行沙盒無法觸及的外部證據行(記錄為 blocked (environment: network denied: <host>) 並在其來源中命名該外部證據行)時,將 QA 任務結算為 completed。這兩個行都不需要 Unreachable Acceptance 聲明,且若唯一的未滿足行是這類行,則 partial 符合資格。 | 封存記錄,其中包含 QA 報告和裁決,並將聲明的 satisfied-by 記錄攜帶為 unproven;規格、其 QA 報告和證據會保留在 Git 中記錄的 source_revision。 |
environment-blocked | 將行保留為受阻;當等效證據滿足環境政策時,報告仍可結算為 pass。 | 本身不會封存任何內容;符合條件的報告可以封存規格。 |
failed | 將 QA 任務保留為未解決,除非應用了授權的覆寫,否則拒絕封存。 | 不會封存任何內容。 |
missing | 將 QA 任務保留為未解決,除非應用了授權的覆寫,否則拒絕封存。 | 不會封存任何內容。 |
override | 不會改變 QA 任務狀態或報告裁決;將封存結算為在 QA 失敗或缺失的情況下明確授權。 | 封存記錄,包含 qa_override、qa_override_approval、qa_override_reason、qa_override_qa_outcome、在 QA 任務未完成時的 qa_override_qa_task_status 和 qa_override_revision;QA 任務和報告在 Git 中記錄的 source_revision 保持不變。 |
Rows the Spec's authorization keeps offline (規格授權保留的離線行)
上述符合條件的聲明 partial 也接受一個外部證據行,該行因為規格自身的授權禁止查找而未被關卡查閱,記錄為 blocked (environment: <subject> prohibited by Spec authorization) 並在其來源中命名該外部證據行。subject 名稱說明了授權禁止的內容,使用其原始措辭:network 代表網路查閱,或 another repository;長度為 1 到 80 個字元,且不含括號。與網路拒絕的行一樣,它永遠不會決定符合條件的 partial,也不需要 Unreachable Acceptance 聲明,一個行最多計入一種豁免類型,而結算、roundfix qa-report accept 和 roundfix archive 透過相同的政策來讀取它 (ADR-0276、ADR-0335)。
Cancelling a Spec (取消規格)
不會交付的規格使用 roundfix cancel <slug> --reason <text> 來取消,絕不手動刪除。該命令會寫入一個處置為 cancelled 的封存記錄,將每個已採用的待辦事項 (Backlog Entry) 以 status: open 返回到 docs/backlog/,將每個已採用的發現 (Finding) 以 status: pending 返回到 docs/findings/,然後移除規格資料夾,而其內容會保留在 Git 的 source_revision 中。被取消的記錄永遠不會滿足前提條件,永遠不會取代另一個規格,並且會被 Delivery Retry 拒絕。
Ignored files and a failed removal (被忽略的檔案與失敗的刪除)
當 Git 忽略規格資料夾下的任何檔案時,封存命令和取消命令會拒絕執行。每個命令都會列出每個被忽略的檔案,並且不會寫入任何內容。操作員必須移除每個檔案,或使用 git add -f 提交它,然後重試。
如果規格資料夾的最終移除失敗,兩個命令都會保留所有寫入,以 1 退出,並列印完成移除的命令:chmod -R u+w '<folder>' && rm -rf '<folder>'。
Preconditions — verify, don't trust (前提條件 — 驗證,不信任)
在觸碰任何事物之前,使用最新的命令證據檢查所有三項:
-
必需的任務已完成。 對於正常封存,讀取
_tasks.md中列出的每個task_NN.md;每個status必須是completed。對於 QA 封存覆寫,每個非 QA 任務必須是completed;QA 任務可以保持pending或failed。如果 QA 任務也是completed,則最新的報告必須仍然不符合資格,否則覆寫是不必要的且會被拒絕。命令: 執行
grep -n '^status:' <each-task-file-listed-in-_tasks.md>並保留其輸出。任何不是status: completed的值都會阻止正常的封存。在覆寫期間,只有另一個狀態的非 QA 任務會阻止封存;命名該任務檔案。 -
QA 已通過。
qa/中最新的報告必須通過儲存庫的 QA 驗證器。不要使用行 grep 來替代結構化驗證。在 Roundfix 中,使用與 Archive Command 相同的internal/spec.QAVerdict合約:透過qa-report-YYYY-MM-DD[-NN].md檔案名稱合約選擇最新的報告,解析其 YAML 前端資料,要求支援的verdict,在存在時要求 blocked-row 欄位必須是非負整數,並在rows_blocked_finding非零時拒絕verdict: pass。保留驗證器的報告路徑和結果作為證據。缺失的qa/目錄、格式錯誤的最新報告或未通過的裁決都會阻止正常封存。僅在透過roundfix archive <slug> --qa-override --approval <source> --reason <text>執行明確的 QA 封存覆寫時才繼續。切勿透過編輯封存記錄來記錄覆寫。符合條件的最新報告不會使失敗或待處理的 QA 任務完成;僅在報告符合條件且每個任務都完成時,覆寫才會被拒絕。 -
規格是自我包含的。 當
docs/specs/<slug>/references/_index.md存在或是符號連結時,應用此前提條件;符號連結(包括中斷的連結)是無效的索引狀態,而非舊版規格。只有當該路徑不存在也不是符號連結的規格,才早於此合約,且無需改造歷史工件即可通過。對於索引規格,每個索引的path必須相對於_index.md存在,每個從未更新的source路徑必須不存在,且規格內部的 Markdown 連結目標不得指向docs/_inbox/或docs/findings/。命令: 首先執行此連結目標檢查;其語法特意匹配內聯或參考風格的 Markdown 連結,而不是僅命名任一棵樹的散文:
spec_dir=docs/specs/<slug> link_hits=$(grep --include='*.md' -RInE '(\([^)]*(docs/)?(_inbox|findings)/[^)]*\)|^[[:space:]]*\[[^]]+\]:[[:space:]]*<?[^[:space:]>]*(docs/)?(_inbox|findings)/)' "$spec_dir") link_status=$? if test "$link_status" -eq 0; then printf '%s\n' "$link_hits" echo "self-containment failed: rewrite each listed link at adoption step 8" exit 1 fi test "$link_status" -eq 1 || exit "$link_status"然後解析並驗證每個資料行。索引屬於目前的規格:
owner必須等於其四位數字前綴,type必須是inbox、finding或backlog,且每個source和path必須只出現一次。path必須是相對於_index.md的一個基礎名稱;拒絕絕對路徑、.、..、路徑分隔符號和符號連結,而不是允許遍歷或指向references/外部的連結。從儲存庫根目錄執行以下命令,並將規範化的行以及任何診斷資訊保留為證據:slug=<slug> index="docs/specs/$slug/references/_index.md" expected_owner=${slug%%-*} parsed_index=$(mktemp) || exit 1 trap 'rm -f "$parsed_index"' EXIT HUP INT TERM if test -L "$(dirname "$index")"; then printf 'self-containment failed: references/ must not be a symbolic link\n' >&2 exit 1 fi if test -L "$index" || test ! -f "$index"; then printf 'self-containment failed: references/_index.md must be a regular, non-symbolic-link file\n' >&2 exit 1 fi awk -F '|' -v expected_owner="$expected_owner" ' function trim(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) return value } function reject(message) { print "self-containment failed: " message > "/dev/stderr" invalid = 1 } function separator(value) { value = trim(value) return value ~ /^:?-{3,}:?$/ } /^[[:space:]]*$/ || /^[[:space:]]*#/ { next } !header { if (NF != 7 || trim($2) != "source" || trim($3) != "type" || trim($4) != "owner" || trim($5) != "adopted date" || trim($6) != "path") { reject("_index.md must use the fixed source | type | owner | adopted date | path header") } else { header = 1 } next } !divider { if (NF != 7 || !separator($2) || !separator($3) || !separator($4) || !separator($5) || !separator($6)) { reject("_index.md has an invalid table separator") } else { divider = 1 } next } { source = trim($2) type = trim($3) owner = trim($4) adopted = trim($5) path = trim($6) if (NF != 7 || source == "" || type == "" || owner == "" || adopted == "" || path == "") { reject("invalid index row at line " NR ": " $0) next } if (type != "inbox" && type != "finding" && type != "backlog") { reject("type must be `inbox`, `finding`, or `backlog` at line " NR ": " type) } if (owner != expected_owner) { reject("owner must be " expected_owner " at line " NR ": " owner) } if (seen_source[source]++) { reject("duplicate source at line " NR ": " source) } if (seen_path[path]++) { reject("duplicate path at line " NR ": " path) } if (adopted !~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) { reject("adopted date must be YYYY-MM-DD at line " NR ": " adopted) } if ((type == "inbox" && source !~ /^docs\/_inbox\/[^\/]+\.md$/) || (type == "finding" && source !~ /^docs\/findings\/[^\/]+\.md$/) || (type == "backlog" && source !~ /^docs\/backlog\/[^\/]+\.md$/)) { reject("source does not match type at line " NR ": " source) } print source "|" type "|" owner "|" adopted "|" path } END { if (!header || !divider) { reject("_index.md is missing its fixed table header") } if (invalid) { exit 1 } } ' "$index" > "$parsed_index" || exit $? while IFS='|' read -r source type owner adopted path; do case "$path" in ""|.|..|/*|*/*|*\\*) printf 'self-containment failed: path must be one basename relative to `_index.md`: %s\n' "$path" >&2 exit 1 ;; esac source_basename=${source##*/} if test "$path" != "$source_basename"
…
[截斷以進行翻譯]