diff --git a/.claude/rules/mcp-cli-parity.md b/.claude/rules/mcp-cli-parity.md index 429e1755..fbd2e60d 100644 --- a/.claude/rules/mcp-cli-parity.md +++ b/.claude/rules/mcp-cli-parity.md @@ -35,159 +35,9 @@ CLI-only 能力已於同日一次性補裁(見 CLI-only 表)——此前的 (#206 對匯入面的原話:「能不能無損匯入,不該取決於使用者會不會寫 script。」) -## 裁決史(封閉列舉——現有 35 工具,一格不多一格不少) +## 裁決史(MCP 工具表,已搬出) -| MCP 工具 | CLI 對應 | 裁決 | -|---|---|---| -| `akashic_search` | `query`(filter flags)| ✅ 功能重疊 | -| `akashic_relations` | `query`(relation flags)| ✅ 功能重疊 | -| `akashic_graph` | `graph` | ✅ | -| `akashic_export` | `export-bib`/`export-tables` | ✅ 功能重疊(tables 面與 #274 的 `--view` 為 **CLI-only**——正式裁決見 CLI-only 表的 `export-tables --view` 列,#259)| -| `akashic_doctor` | `doctor` | ✅ **#581 重新確認,裁決不變、契約有改**:MCP 多一個 `owner` 參數(`:`,kind 是 work/person/organization/venue/divergence 之一、必填),帶它時回**那一筆記錄的完整 per-record 明細**——不套組合式六族每筆 20 則的列出上限,近重複的組內 5,000 對與整筆 100,000 對求值上限照舊(那是 CPU 的界限,觸頂時訊息自己會說);輸出受 `candidateByteBudget`(48 KiB)約束、每則截 1,000 字元,以 `total`/`truncated` 揭露;唯讀,不重建 index、不含跨記錄檢查與 quarantine。**CLI 面是 `validate --owner`,不是 `doctor --owner`**:CLI 的 `doctor` 本來就不印 per-record 家族(它們住在 `validate`),出口放在印那些家族的命令上,使用者在看到概括句的同一份輸出旁邊就找得到它。兩面走同一個 `perRecordIssues(from:owner:)`、同一套定址(`RecordAddress.parse`)與拒絕:kind 缺漏或不在值域、key 不合 StoreKey(divergence 要 UUID)、找不到、同 kind 同 key 兩筆以上——都拒絕、不猜(kind 必填的理由是跨 kind 同 key 真的存在:2026-09-28 實測 live store organization 與 venue 共用 2 個 key)。**有記錄的差異**:CLI 逐行不截、沒有位元組預算;MCP 受位元組預算——理由同 CLI-only 表 `validate` 列的面級截斷分工(輸出進 LLM context,呼叫端無法在收到後丟棄已付的代價) **#609 重新確認,裁決不變、契約有改**:兩面多一項「附加來源已刪除、主連結仍在」的 entry(MCP `orphanedAdditionalSources`、CLI `orphaned additional sources:` 一行),與 `orphaned` 不相交;`orphaned` 的判準改為 `Entry.zoteroLinkState`——沒有主來源而附加來源全部已刪除也算(先前兩面都看不見)。CLI `doctor` 的 orphaned 一行改讀 `health`,不再自己推導。處置在 App 裁決台(CLI/MCP 沒有 orphan 裁決面,與 #605 之前相同)。 **#609 R1 verify 補缺席理由**:這一格是**候補缺席**——處置是判定型動作(Zotero 端的刪除可能是暫時的),要人看過清單、逐筆確認「這一組」之後才拿掉,並要求記錄檔已 commit;目前沒有 LLM 消費面的流程需要它(doctor 只列出),需求出現即重啟裁決。 **#703 R1 重新確認,裁決不變、契約有改**:`sources` 一致性多一類——殘留的暫存檔(`.<62 hex>.incoming-`,存檔在複製途中被中斷留下的)。CLI 逐行 ⚠ 路徑與大小(至多 20 行,其餘一句概括),MCP `sources.strayTemporaryFiles`(至多 20 筆,每筆路徑與 bytes)加 `strayTemporaryFilesTotal`;兩面與 App 讀同一個 `auditSourceIndex`。只報不刪——正在進行的存檔也長這樣。 **#703 R2 重新確認,裁決不變、契約有改**:`sources` 再多一類——位址問題(位址上是目錄、symlink、特殊檔案,或普通檔的大小與 index 那一列的 `bytes` 不同):CLI 逐行 ⚠(至多 20 行),MCP `sources.occupantProblems`(每筆 `path`、`kind`,至多 20 筆)加 `occupantProblemsTotal`;殘留暫存檔每筆多最後修改的時間(CLI「最後修改於 N 秒前」、MCP `ageSeconds` 與 `possiblyInProgress`),一小時內還在動的不計入 `hasFindings`(App 的健康區塊不因它亮起)。三面同一個 `auditSourceIndex`、同一個 `sourceOccupant` 分類。 **b26 F6 重新確認,裁決不變、契約有改**:殘留暫存檔的修改時間在未來超過 300 秒(時鐘被往回撥、備份還原、跨時區的 FAT 卷)時不信——MCP `ageSeconds` 是 null(與「讀不到」同一個值)、~~`possiblyInProgress` 是 false~~、算舊;CLI 說「修改時間在未來」而不是「0 秒前…不要刪」。 **b29 V5 重新確認,裁決不變、契約有改**:時間不可信(`ageSeconds` 是 null)時 MCP `possiblyInProgress` 是 **true**——「算舊」只管 `hasFindings` 與 App 計數(要人看一眼),「可不可以刪」的疑問往不刪的方向;CLI 對同一個檔不再說「中斷的存檔留下的——…可以刪掉」,改說判不出是中斷留下的還是正在進行。兩面同一個 `StrayTemporaryFile.possiblyInProgress`。說明補 `orphanBlobs`/`danglingIndexEntries`/`malformedIndexLines`/`unreadableShards`(b26 F6 以位元組預算為由沒寫)與「`ageSeconds` null=判不出」,見 `changelog/2026-10-02-b29-h5-fixes-703-700.md`。位址問題的訊息兩面都說兩邊(存檔被截短、接長或換掉:移走後重存;index 那一列的 `bytes` 記錯:重存不會改既有的列,要手改 `index.jsonl`)。`akashic_doctor` 的說明把 R2 新增的兩個陣列(`occupantProblems[]`、`strayTemporaryFiles[]`)與它們的元素鍵寫齊(兩個 `*Total` 以速記涵蓋;`tools/list` 的位元組預算要求這一輪淨增 ≤ 0),由 `SourceAddressServiceTests.testTheDoctorDescriptionNamesEveryKeyOfTheSourcesPayload` 釘住——`sources{}` 在 `ToolPayloadNestedPaths` 之外(使用者裁決 (a)),守衛看不到這一層。 **#700 b31 W5 重新確認,裁決不變**:CLI `doctor` 經 `ensureLayout` 建佈局,`.gitignore` 沒有 sources 標記區塊時只在尾端附加位元組;讀不懂的(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)不改寫,改印一則 warning(附要自己加的那段)、結束碼不變——先前整份換成只含區塊。MCP `akashic_doctor` 不建佈局、不碰 `.gitignore`,這一格沒有對應。 **#700 b33 verify 重新確認,裁決不變、契約有改**:同時跑的另一個 akashic 剛加好區塊時不再報假 warning(開檔後取 `flock`、鎖內重讀內容);不改寫的再加四類(symlink 打不開、有其他硬連結、大到 git 不讀、有標記沒有排除規則);寫到一半截不回來時 warning 說「寫到一半、沒能收回」。CLI `doctor --help` 自此寫出 `.gitignore` 的行為。 **#700 b36 verify 重新確認,裁決不變、契約有改**:「區塊已在」改成與 git 的判讀一致——規則行照 git 的切法比(行首帶空白或 tab、行尾帶 tab 的不算,先前剝掉後判成已在、不出聲);`.gitignore` 是 symlink 時不論指向的內容有沒有區塊都報(git 2.32 起不讀 symlink 的 `.gitignore`,先前指向的內容有區塊就判成已在);等鎖有上限(10 秒,逾時報 warning、不讀不寫);warning 首句改成「沒有生效的 sources 排除區塊」,處置不再對已有區塊的檔叫人再加一份。 | -| `akashic_files` | `file` | ✅ | -| `akashic_libraries` | `library` | ✅(#455 起 add/remove 兩面同走 `setMembership`:CLI 收多 citekey 是批次形、MCP 維持單 citekey——批次面的裁決見 CLI-only 表) **#628 重新確認,裁決不變、契約有改**:add/remove 指名的 citekey 重複或與另一筆 work 共用 id 時兩面都整批拒絕。 **#641 重新確認,裁決不變、契約有改**:「無法唯一定位」自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),併進同一個 `unlocatableCitekeys`,兩面照舊整批拒絕;訊息共用 `UnlocatableReason` 一句話。 **#642 重新確認,裁決不變、契約有改**(使用者 2026-09-28 照提案定案):library 多一個成員性質(`topic`/`rule`/`document`,`Library.membership`),兩面同時新增兩個 action——`set-kind`(CLI `library set-kind`,標性質與規則)與 `check`(CLI `library check`,列出不符規則的成員,唯讀);`create` 自此**要求性質**(CLI `--kind`、MCP `kind`,規則指涉的 venue/文件要在庫);`list` 帶性質、規則與不符數。**add 對規則型/文件型逐筆比對、不符的不寫並具名**(不是拒絕整批、也不是同意後照寫——Clarity row 1:改成正確的再寫),**未標性質的 library 拒絕 add、零寫入**(查不到依據就不寫),remove 不查依據。兩面同一條 service 路徑(`parseLibraryMembership`/`setMembership`/`libraryViolations`),判定只有 `LibraryMembershipCheck` 一份,App 的加入動作(第三面)也問它。**有記錄的差異**:CLI `library add` 全部不符時非零結束,MCP 照常回成功、由呼叫端讀 `written`/`skipped`(#624 全數排除的同形);CLI `library check` 不截,MCP `check` 受 `candidateByteBudget`、以 `total`/`truncated` 揭露(#388 的分工);CLI `set-kind` 全列現有不符的成員,MCP `set-kind` 列前 20 筆加計數。 **#642 R1 verify 重新確認,裁決不變、契約有改**:(a) `set-kind` 是整值替換,兩面的回應都回顯**先前**的成員性質(CLI 印完整的舊規則含每一個排除的 citekey;MCP 回 `previous`/`previousBasis`,`excluded` 至多 50 筆加 `excludedTotal`——同 `membershipPayload` 的既有上限);替換一條**既有**的性質時要求 registry 檔 tracked 且 clean(同 #573 一族的閘,`LibraryStore.recoverabilityRefusal` 涵蓋 `libraries/.yaml`),從未標性質標成任何一種不需要;重送相同的值不需要。**「放寬規則要明確確認」不做**(列為待使用者決定)。(b) 規則指涉的東西要在庫**含排除清單的 citekey**(create 與 set-kind 兩面同一個 `requireMembershipReferents`)。(c) 規則的 venue key 有不只一筆記錄時 add 三面都不寫(`LibraryMembershipViolation.ruleVenueAmbiguous`,判定只有 `LibraryMembershipCheck` 一份,`violation(of:)` 對未標性質也 fail-closed 回 `.libraryUnmarked`);`check` 兩面都揭露依據問題(CLI 印 ⚠、MCP 回 `basisProblem`),零成員的 library 也看得到。 **有記錄的差異**:CLI `library check` 印**完整**規則(含全部 excluded),MCP 的 `membership` 載荷仍截 50 筆——輸出進 LLM context 的既有分工(#388)。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_import_zotero` | `import-zotero` | ✅(#658 起 CLI 面過 #298 的目標確認閘——沒有乾跑,所以無條件閘、拒絕在建佈局之前;MCP 面不閘,理由見橫切選項表的 `--yes` 列:active store 是 session 狀態) **#607/#610 重新確認,裁決不變、契約有改**:比對順序改成主來源 → 附加來源 → legacy 裸 key,裸 key 的「已被其他 library 持有」看附加來源;同一個 `(library_id, zotero_key)` 被兩筆以上 entry 宣稱(兩筆以上 legacy 同裸 key 亦同)時不更新任何一筆、不新建,報告多一個 `ambiguousSourceClaims`(MCP 只在非空時出現,CLI 逐行印)。兩面同一個 `ZoteroImporter`。 **#607/#610 R1 verify 再確認,裁決不變、契約有改**:(a) 兩筆以上 legacy 同裸 key 的判斷先於「其他 library 已持有」,別的 library 也持有同一個裸 key 時歧義照樣被報、條目照樣不新建;(b) 宣稱者的定義只有 `ZoteroSourceClaims` 一份(含 `?:<裸 key>` 桶),所以兩種鍵在載入時都由 `validate`/`akashic_doctor` 的跨記錄檢查報 warning,CLI 標題行指向的警告真的存在;(c) MCP 工具描述點名 `ambiguousSourceClaims`(只在非空時出現,不讀描述的呼叫端沒有理由去檢查它);index rebuild 失敗時錯誤訊息帶成功時同一份 payload(先前只帶四個計數,歧義被吞)。 ~~有記錄的差異:`ambiguousSourceClaims` 兩面都沒有筆數上限(鍵數受本機 Zotero 條目數限制、值是宣稱者的 citekeys)——與相鄰的 `authorsPreserved`、`quarantineConflicts` 同慣例,MCP 面沒有 `enrich` 那種 `total`/`truncated` 揭露;一個大量 entry 宣稱同一來源的 store 會讓單次回應膨脹,出現時比照 D30 重開。~~ → **#684(2026-09-29)重開並裁決:MCP 面加上限、CLI 維持全列**(見本列末段;`authorsPreserved`、`quarantineConflicts` 與其他清單維持原慣例,不在這次範圍——#696 補上,見本列末段)。 **#682 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):被多筆 entry 宣稱的來源,本趟仍**不更新書目欄位、不動 version/hash、不新建**,但 orphan 標記照常處理——Zotero 端復原時各宣稱者身上這個來源的標記都清(主來源的進 `orphanCleared`、附加來源的進 `secondarySourceRestored`,只寫標記),刪除時各宣稱者本來就都被標(偵測迴圈逐筆看每一筆自己的來源,pin 在 `ZoteroSourceRoutingTests`);`ambiguousSourceClaims` 照舊列出。先前連標記的清除一起略過,App 的 Orphans 頁會把復原的 item 列成「已刪除」,而那一頁的動作是破壞性的(移到垃圾桶、脫鉤)。舊檔那一桶(`?:`)只在歸屬沒有爭議(沒有別的 library 持有同一個裸 key)時才清,與單一舊檔的認領同一條件。 **#684 重新確認,裁決不變、契約有改**:MCP 的 `ambiguousSourceClaims` 至多 20 個來源、每個來源至多 20 個宣稱者(依來源鍵、citekey 排序留前面的),並多兩個鍵 `ambiguousSourceClaimsTotal`(完整的來源數)與 `ambiguousSourceClaimsTruncated`(來源被截或任一來源的宣稱者被截即 true),三個鍵同進同出——沒有歧義時都不出現;index rebuild 失敗時錯誤訊息裡的那份報告同一個上限。**CLI 逐行全列**(有記錄的兩面差異,理由同 `akashic_enrich` 的 items 上限與 #388 的 `--rows`:MCP 的輸出進 LLM context、呼叫端無法在收到後丟棄已付的代價,CLI 的輸出進人的終端機、可捲可 pipe)。上限是 service 的 `claimLimit` 參數、由 server 傳入(`AkashicMCPServer.ambiguousClaimsLimit`),沒給=全列;`StdioE2ETests.testImportZoteroCapsTheAmbiguousClaimsAtTheServer` 釘住 server 有傳。 **#608 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):報告多一格 `secondarySourceHashOnly`(MCP payload 與 CLI 一行,與 `secondarySourceChanged` 同形)——附加來源的 hash 與存下的不同、**而 Zotero 的 `version` 沒動**~~(沒有人改這個條目;變的是 hash 的算法或子項附件集合,報告不宣稱原因)~~ → (見本列末段 #608 verify R1:判準改成 Zotero 那一列已同步、`version` > 0 且沒動,報告不宣稱原因);這種情形先前與「內容真的變了」報在同一格(`secondarySourceChanged`),mapping 定義一演進每個附加來源都被列成「有人改了」。`version` 前進或倒退而 hash 不同仍是 `secondarySourceChanged`(倒退時不對沒把握的事下結論)。不改 store 存的任何東西。 **#608 verify R1/#694/#696 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) #608 verify R1:「只有 hash 不同」的判準改成**有舊 hash 且不同、而 Zotero 那一列已同步(`items.synced` = 1)、`version` > 0 且與存下的相同**(`ZoteroImporter.isHashOnlyDifference`,一份判準)。初版只看 `version` 沒動,並把那一格說成「沒有人改這個條目」——但 `version` 只在同步時才變:還沒同步的本機修改(`synced` = 0)與從未同步的 library(`version` = 0)是內容變動,初版把它們報成只有 hash 不同。現在這兩種與「資料庫沒有 `synced` 欄」都回到 `secondarySourceChanged`;兩面的說明只說觀察到的事實、~~不宣稱原因(多半是 mapping 定義演進,或子項附件增減)~~ → 可能的原因(mapping 定義改了、子項附件增減)並列、不排序(R1 verify:「不宣稱原因」接著「多半是」自相矛盾)。(b) #694:主來源多一格 `updatedHashOnly`(同一份判準;MCP payload 永遠在,與 `updated` 同形;CLI 非空時另印一行),不列在 `updated`。**寫入不變**:兩格走同一段改寫、同一次寫入,分格只改報告(`ZoteroImportTests.testWriteIsIdenticalWhicheverBucketTheUpdateLandsIn` 比對兩格寫出的檔逐位元組相同)。(c) #696:MCP 其餘 citekey 清單——`created`、`updated`、`updatedHashOnly`、`orphaned`、`orphanCleared`、四個 `secondarySource*`、`unnormalizedDates`、`authorsPreserved`、~~`quarantineConflicts`、`writeFailed`(截的是條目數)~~(R1 verify 起不截,見本列末段)——各至多 20 筆(依 citekey 排序留前面的);揭露是**一對鍵**:`listTotals`(清單名 → 完整筆數,十三個名字都在、空的是 0)與 `truncatedLists`(被截的清單名;沒有截就是空陣列),兩個鍵永遠在。~~形照 `akashic_enrich` 的 `counts`(封閉列舉的名字 → 完整筆數)加截斷揭露~~(R1 verify:這個先例只截明細、不截失敗清單,對 `writeFailed`/`quarantineConflicts` 是過度宣稱);**不**逐清單加 `…Total`/`…Truncated`——十三對鍵名要寫進工具描述,會吃掉 tools/list 的位元組預算(#578)。`ambiguousSourceClaims` 維持 #684 自己的鍵。計數(`unchanged`、`skippedLinkedAttachments`)照舊完整;index rebuild 失敗時錯誤訊息裡的報告同一個上限;上限只截報告、不截寫入。上限是 service 的 `listLimit` 參數、由 server 傳入(`AkashicMCPServer.importListLimit`),沒給=全列;`StdioE2ETests.testImportZoteroCapsEveryListAtTheServer` 釘住 server 有傳,`ImportZoteroReportSurfaceTests.testEveryReportCollectionIsCappedOrNamed` 讓 `ImportReport` 新長出的集合欄位不是有上限就是具名寫理由。`residualFields` 不截(鍵是 Zotero 的欄位名,筆數受 schema 的欄位表限制、不隨一次匯入的筆數成長)。**有記錄的兩面差異**:MCP 截、CLI 不受此上限(理由同 #684:MCP 的輸出進 LLM context)。但 CLI 本來就不是每一格都全列——`created` 只印筆數、`unnormalizedDates` 只印前 8 筆,這是 #696 之前就有的差異,本次未改。 **#608/#694/#696 R1 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) MCP payload 多兩格 `authorsOverwritten`(pull 覆寫的未歸戶作者,citekeys;有上限、進 `listTotals`)與 `fieldsRemovedByPull`(被整份替換拿掉的欄位名 → 次數;不截,同 `residualFields`),都只在非空時出現——先前只有 CLI 印,而 `updatedHashOnly` 那一格與 `updated` 同一段整份替換,讀起來無害的分類底下照樣拿掉手加的欄位、覆寫 literal 作者(`lossless-intake`:丟棄必須可見,兩面都要看得到);(b) **失敗清單不截**:`writeFailed`、`quarantineConflicts` 全列、只在非空時出現、不在 `listTotals`——沒寫進去的記錄在 store 裡沒有痕跡、原因只在這份報告,重跑是再寫一次、不是重播,截掉的就拿不回來;(c) 仍截的清單描述的都是**寫進去了**的記錄:結果在 store 裡,這一趟改了哪些檔在 store 的 git diff 裡看得到——截掉的成員從那裡找得回來,只有落在哪一格(例如 `updated` 還是 `updatedHashOnly`)找不回來(不加分頁、不存完整報告)。~~`authorsPreserved`/`authorsOverwritten`/`fieldsRemovedByPull` 在寫入之前就記下,沒寫進去的那一筆也在不截的 `writeFailed` 或 `quarantineConflicts` 裡~~(#702 起改為寫入成功後才記,見本列末段);(d) index rebuild 失敗時錯誤訊息改說「報告如下——清單有上限」(先前說「完整報告」,而清單已截);(e) CLI 的只有 hash 不同那兩行列出可能的原因、不排序,主來源那一行指向 `authors overwritten`/`fields removed by pull`。 **#702 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):主來源更新分支的 `authorsPreserved`、`authorsOverwritten`、`fieldsRemovedByPull` 改在寫入成功之後才記。先前在寫入之前就記,目的檔被隔離或寫入擲錯的那一筆同時出現在失敗清單與「已覆寫/已拿掉」清單,而那一筆沒有寫。現在它只在 `quarantineConflicts` 或 `writeFailed` 裡。附加來源、新建、orphan 各分支本來就在寫入之後才記,不變。R1 verify 補一個例外:#631 的搬移寫完之後刪 legacy 檔失敗(`StoreIOError.legacyCopyNotRemoved`)時那一筆**寫了**,照常記在各清單,`writeFailed` 的訊息以「已寫入」開頭、說明留下兩份;兩面同一個 `ZoteroImporter`,CLI 那一行的標題不再說「已略過」。 **#705 重新確認,裁決不變、契約有改**:上一句的例外改掉——那一筆不再進 `writeFailed`,改記在成功那一側的 `writtenWithLegacyCopy`(`ZoteroImporter.run` 自己的收集範圍收下,報告欄位 `ImportReport.writtenWithLegacyCopy`;MCP payload 只在非空時出現、不截),照常記在各清單;CLI 那一行的標題回到「已略過續跑」,這幾筆另印一段、排在 index rebuild 之前。同一趟稍早一步已寫入、之後的步驟被 #631 拒絕的那一筆(#702 R2 verify),`writeFailed` 那一則說出前一步寫了;同一筆多則以「;」串接、不覆寫。兩面同一個 `ZoteroImporter`。 **#611 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決「照建,並自動記一筆歧異提名」;兩面同一個 `ZoteroImporter`):這一趟新建的 work 與另一筆 work(既有的,或同一趟也新建的)共用 DOI(`canonicalDOIs` 的正規形)時照建,並寫一筆 work 形狀、沒有判斷的歧異記錄——候選是那一對,一對一筆——交給 `resolve-divergence` 判定;DOI 相等只是提名。只在新建時觸發:再匯入不新建就不提名,被 `dismiss-divergence` 放棄的一對不會被重記。已有涵蓋這一對的 work 歧異記錄(同一組或更大的一組)不重寫;無法唯一定位的候選(`unlocatableCitekeys`,加上這一趟寫入後留下 legacy 拷貝的)不點名。報告多一格 `doiNominations`,每列 `created`/`other`/`dois`/`status`/`divergence`/`error`,status 封閉~~四值 recorded/alreadyRecorded/unlocatable/failed~~ → 五值(R1 verify 加 groupTooLarge,見下)。MCP 只在非空時出現;~~recorded/alreadyRecorded 兩種列受 `listLimit`(歧異記錄在 store 裡、`akashic_divergences` 列得出來),unlocatable/failed 不截(同失敗清單)~~ → 兩種列各自受 `listLimit`(見下);`listTotals` 永遠有這一格;index rebuild 失敗時錯誤訊息裡的報告同一份。CLI 逐行全列。~~寫不進去的(legacy 佈局、store format < 5)報 failed、不改變結束狀態(兩筆都已照建,`akashic validate` 的跨記錄 DOI 警告照樣報這一對)~~ → 結束狀態見下。工具描述加一句,`tools/list` 51,997 → 52,376 bytes(預算 54,000——當時的預算;現行預算見〈工具描述有位元組預算〉)。 **#611 R1 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) status 多第五值 `groupTooLarge`:一個 DOI 被超過 10 筆 work 共用(這一趟新建的加上其餘的)時**一對都不記**,改報一列(一個 DOI 一列、`dois` 是那一個 DOI、帶 `groupSize`、不帶 `other`、`created` 是這一趟新建的 citekey 最小的一筆);一對一筆在 k 筆共用時寫 C(k,2) 筆記錄(DA 實測 k=80 → 3,160 筆、10.2 秒;修後 0 筆、0.25 秒),一個 DOI 被這麼多筆共用幾乎必是書或資料集的概念 DOI。沒有新建的 work 的組不觸發;一對同時共用另一個沒超過門檻的 DOI 時仍由那個 DOI 提名。(b) MCP 的兩種列**各自**受 `listLimit`:歧異記錄在 store 裡的(recorded/alreadyRecorded)與沒記下來的(unlocatable/failed/groupTooLarge)各留前 N 列(先前後三種不截,legacy 佈局上一個大群組會回出數十萬列);`listTotals["doiNominations"]` 仍是全部列數。(c) **沒記下來的提名重新匯入不會再提名**(只在新建時觸發):MCP payload 多一個鍵 `doiNominationsUnrecorded`(沒記下來的列數,永遠完整、非零才出現),CLI 印摘要行並**以非零結束**(與 `writeFailed` 同一條慣例;先前寫「不改變結束狀態」——那讓重新匯入救不回的一對除了 `validate` 的 DOI 警告之外沒有任何出聲);處置是先處理各列說的原因、再用 `record-divergence`/`akashic_record_divergence` 手記。**有記錄的兩面差異**:MCP 不以錯誤結束、呼叫端讀 `doiNominationsUnrecorded`(同 #624 全數排除的同形差異)。(d) 記錄路徑(`recordDivergence`)的兩個改動影響 `record-divergence`/`akashic_record_divergence` 兩面:同一個 id 的既有記錄若候選(形狀與 key)不同——歧異記錄的 id 只由候選 key 決定、不含形狀,所以 person 的 `{a, b}` 與 work 的 `{a, b}` 是同一個檔——**拒絕覆寫**(~~`destinationHoldsAnotherRecord`~~ → R2 起是專用的 `divergenceIdHeldByOtherCandidates`,見 R2 段;先前互相覆寫、不出聲);既有判斷與 prefers 的保護改為**寫入當下重新讀磁碟上那一個檔**,不看呼叫端的快照(匯入可能跑數分鐘)。`recordDivergence(…against:)` 收的是建一次的 `DivergenceCandidatePool`(候選存在的 key 集合),不再是整份 `LibraryLoad`。本輪 `tools/list` 53,403 → 53,772 bytes(真 binary,預算 54,000——當時的預算,現行 60,000)。 **#611 R2 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`;`record-divergence`/`akashic_record_divergence` 同走記錄路徑,見那一列):(e) 同一個 id 被另一組候選占著的拒絕改用專用的 `StoreIOError.divergenceIdHeldByOtherCandidates`:訊息點名現有那一筆的候選、說原因是 key 不同(`rename` 就地改寫候選而不重算 id)還是只有形狀不同、出路是 `akashic divergences`/`resolve-divergence`/`dismiss-divergence`——先前借用 #631 的泛用拒絕,把原因一律說成形狀、出路叫人「看那個檔、修好、移走」(那是給被 quarantine 的記錄的),而 `record-divergence` 撞同一個拒絕、指它手記是循環。(f) 提名寫入被拒時**再讀一次磁碟**:那一對若已被別的程序記下(可能帶判斷),報 `alreadyRecorded`、不報 `failed`——匯入要確認它在、不是重錄它,記錄路徑對「無判斷的重錄」的拒絕(#133/#159)對匯入沒有意義,報 failed 會讓 CLI 以 1 結束、叫人手記已存在的記錄;涵蓋判斷也改成**每一個**候選都是 work(先前只看第一個候選的形狀)。(g) CLI 摘要行的 N 只數真的記下的對,沒記下的另標「(另有 M 對沒記下)」,不再把它們數進「照建,並記一筆…」。(h) MCP `doiNominations` 的排序鍵含 DOI(`groupTooLarge` 的 `other` 恆為空,兩個過大群組 `created` 相同時順序不再取決於排序的穩定性)。(i) **`akashic_import_zotero` 的說明還原**:f96e922a 為了擠進當時的 54,000 預算修剪的承重語意(`groupSize` 是共用的 work 數、`groupTooLarge` 列不帶 `other`、一對都不記、DOI 相等只是提名、recorded/alreadyRecorded 與沒記下的各是哪幾種、`updatedHashOnly` 的「Zotero 已同步」與「不宣稱原因」)全部補回——預算隨後調到 60,000,修剪卻留著;`StdioE2ETests.testImportZoteroDescriptionStatesTheDOINominationAndHashOnlyContracts` 釘住這八句。**沒有改的、要使用者確認的取捨**:`groupTooLarge`(刻意的不提名,不是寫入失敗)仍讓 CLI 以非零結束,已有 >10 筆共用 DOI 的 store 之後每新建一個該群組的成員那一次匯入就以 1 結束——見 `zero-instance-guards` 第 80 列;另一個同形的角落:這一趟寫入後留下 legacy 拷貝的 work(#705 的成功那一側)若與新建的 work 共用 DOI,提名報 `unlocatable`、同樣讓 CLI 以 1 結束,兩者都等使用者確認。 **#700 b31 W5 重新確認,裁決不變、契約有改**:兩面都經 `ensureLayout`,`.gitignore` 讀不懂時(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)在建立任何東西之前具名拒絕、`.gitignore` 不動,訊息附上要自己加的那段——先前整份換成只含區塊、使用者原有的規則全部消失;有區塊或可附加時照常。 **#700 b33 verify 重新確認,裁決不變、契約有改**:同時跑的另一個程序剛加好區塊時不再假拒絕;拒絕的原因多四類(同 `akashic_doctor` 那一格);開檔、取鎖、寫入時才發現的原因在佈局建好之後擲出,訊息說「佈局已建好」。~~**有記錄的缺席**:MCP 說明不寫這些拒絕(位元組預算;錯誤訊息自己說出原因並附要加的那段),CLI `import-zotero --help` 寫出。~~ **#700 b35 重新確認(使用者 2026-10-05 裁決第 1 項),裁決不變、契約有改**:讀不懂的 `.gitignore` 不再擋匯入——兩面都經 `ensureLayout(sourcesIgnore: .report)`,`.gitignore` 不動、照常匯入;CLI 在讀 zotero.sqlite 之前把 warning 印到 stderr(stdout 是匯入報告、結束碼不因此改變),MCP 回應帶 `gitignoreWarning`(只在有問題時出現;成功回應與 index rebuild 失敗時錯誤訊息裡的報告都帶)。兩面與 `doctor` 同一份文字(`SourcesIgnoreProblem.warningLines`,多一句匯入照常完成、防線在 store-source)。匯入本身不寫 `sources/`,擋存檔進版控的防線在 `store-source`(store 在 git 裡時以 git 驗排除、未生效拒寫)。上面那些拒絕自此只剩 `file add`。**有記錄的缺席**:MCP 說明不寫 `gitignoreWarning`(位元組預算;值自己說出原因並附要加的那段),payload 鍵守衛以 advisory 具名豁免;CLI `import-zotero --help` 寫出。**誠實邊界**:MCP 的匯入在 `.gitignore` 那一步之後自己擲錯(zotero.sqlite 讀不出來之類)時,回應只有那個錯誤、沒有這則 warning——佈局已建好、`.gitignore` 沒動,下一次匯入會再說。 **#700 b36 verify 重新確認,裁決不變、契約有改**:判讀同 `akashic_doctor` 那一格的 b36 補記(規則行照 git 的切法、symlink 一律報、等鎖有上限);warning 那一句改成「匯入不因此中止」(先前「匯入照常完成」——CLI 在讀 zotero.sqlite 之前就印它,之後失敗時兩句矛盾)、處置的「之後怎麼再跑」改成「跑 akashic doctor(或重跑匯入)」,兩面同一份文字(`SourcesIgnoreProblem.importRerun`)。 | -| `akashic_import_wos` | `import-wos` | ✅(#290;#259 CLI-only 盤點唯一「需要」格的補齊——#206 鏡像判準) **#641 重新確認,裁決不變、契約有改**:回填(只多不少)遇到無法唯一定位的既有記錄(`unlocatableCitekeys` 三類)時,該列具名進 `skippedRows`、零寫入、其餘照跑——先前寫入當下被拒時整趟匯入擲出、前面幾列已落盤而報告丟掉;兩面同一個 `WoSImport.run`。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#700 b31 W5 重新確認,裁決不變、契約有改**:MCP 面經 `ensureLayout`(含 `dry_run`),`.gitignore` 讀不懂時在建立任何東西之前具名拒絕、不改寫(同 `akashic_import_zotero` 那一格)。**有記錄的差異**:CLI `import-wos` 開既有 store(`openStore`)、不建佈局、不碰 `.gitignore`——這個差異早於 #700,本輪只記下。 **#700 b33 verify**:MCP 面同 `akashic_import_zotero` 那一格。說明裡的「建議先 dry_run」不改:`dry_run` 也經 `ensureLayout`(既有行為:乾跑也建佈局、也會附加區塊),讀不懂時同樣拒絕、錯誤訊息自己說明——說明不加(位元組預算),預覽面要不要改成只看不寫沒有裁決。 **#700 b35 重新確認(使用者 2026-10-05 裁決第 1 項),裁決不變、契約有改**:MCP 面(含 `dry_run`)同 `akashic_import_zotero` 那一格——照常匯入、回應帶 `gitignoreWarning`;index rebuild 失敗的錯誤訊息末尾也附上那則 warning。上一句的「讀不懂時同樣拒絕」自此不成立。CLI `import-wos` 照舊開既有 store、不碰 `.gitignore`(有記錄的差異不變)。 **#700 b36 verify 重新確認,裁決不變、契約有改**:MCP `dry_run` 的參數說明改成照實說「不寫記錄(佈局與 .gitignore 區塊照建)」——先前寫「不寫檔」,而它建佈局、會附加區塊(使用者 2026-10-05 的裁決把乾跑納入「警告後繼續」,行為不改,說明改;tools/list 淨 −1 位元組)。warning 的文字同 `akashic_import_zotero` 那一格的 b36 補記。 | -| `akashic_resolve_people` | `resolve-people` | ✅(#272 起兩面契約有記錄的差異:MCP 允許 apply+reject 組合(兩段式、按腿回報);CLI 維持分兩次呼叫——互動面天然序列,組合是 LLM 批次 triage 的需求。#303 起兩面同步帶 `tier`(封閉四值 exact/confirmed-elsewhere/reorder/initials,信心降冪):MCP 每列 `tier` 欄、CLI 按 tier 分組標頭與 `--tier` 篩選(裸 `--apply` 對寬鬆 tier 拒絕)、App 候選列標示;apply id 升三段形 `citekey:authorIndex:personKey`(釘 person,兩段 legacy 收);否決抑制改與提名同一套正規化、淘汰而得的唯一命中在 reason 揭露——R1 verify 後 apply 語意有這些**有記錄的變更**,非純 additive。R3 裁決的**面不對稱**:tier 閘只在 CLI 篩選式批次(MCP per-id 顯式+tier 可見,刻意不閘;tier-acknowledgment 參數列 follow-up);rejected/applied 回音均三段 pinned 形。**#624 起另一個有記錄的差異**:淘汰而得的唯一候選(此位置其他人選已被否決,`eliminatedPairings > 0`)——CLI 的篩選式 `--apply` 排除它、逐筆列出並指路 `--judge`,全被排除時零寫入、非零結束;MCP 的逐 id apply **照寫**(顯式指名,理由同 tier 閘的面不對稱)。MCP 列表帶結構化的 `eliminatedPairings`;CLI 沒有 `--json`,同一件事以文字揭露——列表每列標「淘汰而得」、reason 帶「已被否決」、`--apply` 時另列排除清單。兩段 legacy id 若指到淘汰所得會被拒(只指到位置、沒點名人),適用於 MCP 的 apply/reject 與 CLI 的 `--reject`(CLI 的 `--apply` 只送三段 id),要用三段形;**#627 起 citekey 重複時一律不猜**:兩面的 apply/reject(兩段與三段 id)與 split/un-split/drop/attribute-org 整批拒絕,judge/refute 該筆具名略過,CLI 篩選式 `--apply` 排除並另列;「無法唯一定位」自 R2 起含與另一筆共用 id 的 work(`unlocatableCitekeys` 一個定義,各腿共用——寫入以 id 定檔,改它等於改兄弟的檔);`PersonResolver.apply` 本身也不套用這些位置、以陣列位置就地改寫(最後一道防線);App 裁決台的 accept 具名拒絕(App 列表沒有標記,第三面的已知差異);MCP 列表以 `unlocatableCitekey` 先標出,CLI 列表以 ⟨citekey 重複或共用 id⟩ 標出——這兩面同一件事;confirm 的 verdict 只寫給真的改到的作者位,apply 以 `notApplied` 回報;judge 同一次呼叫把同一作者位判給兩個人整批拒絕。**有記錄的差異**:judge/refute 全部略過時 CLI 非零結束、MCP 照常回成功由呼叫端讀 `skipped`(同 #624 全數排除的同形差異);作者位早已歸給同一個人時,兩面同契約:已有同一句理由的逐篇判定=no-op 成功(`alreadyJudged`),理由不同則具名略過;由其他規則歸戶的具名略過(升級面缺口 #636——**自 change `resolution-verdict-states` 起不成立**,見本列 (c))。**#635 起 judge/refute 各自單獨呼叫**:兩面都顯式拒絕它們與彼此或 apply/reject 的組合(先前靠提早返回隱含達成,其餘腿被靜默丟掉、仍回成功)——同結構腿(#443)的既有契約。造出這些狀態的儲存層路徑見 #631。同型但不屬 resolve-people 族的寫入面見 #628。tier 閘看排除後的套用集。刻意不給 CLI 關掉排除的旗標——那會把排除變回一鍵可關的預設;查過未決的配對兩面都看不到,繫於 #619)。**change `resolution-verdict-states`(#619/#636,2026-09-25)重新確認,裁決不變、契約有改**:(a) 兩面同時新增 **undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedAuthorships`),記下「查過、判不出來」與查了什麼——兩面同契約:整批拒絕六類、逐筆略過四類、完全相同=`alreadyRecorded`、單獨呼叫;上一句「兩面都看不到」自此不成立。(b) 查過未決的配對在列表揭露(MCP `undecidedChecks`/`counts.undecided`/`undecidedTotal`,CLI「查過未決 N 次」與四態計數行);**CLI 的篩選式 `--apply` 排除它、另列並指路 `--judge`,MCP 逐 id apply 照寫**——與 #624 淘汰所得同形的有記錄差異,理由相同;App 裁決台(第三面)同樣標「查過未決 N 次」,Accept 是逐筆顯式、照常可用(R1 verify:初稿漏了這一面)。(c) `judge`/`refute` 對「已由提名層同向判過」的配對改為寫入並存的逐篇判定(回應帶 `coexistsWith`,需 store format ≥ 19),兩面同契約。(d) R2/R3 verify:judge 與 attribute-org 對 literal 作者位照常歸戶、理由存不進去時該列帶 `verdictNotRecorded`(CLI 印 ⚠),兩面同契約;同批 refute 碰撞具名略過;undecided 腿的整批拒絕另含三個上限(200 個 id、20 個 digest、4,096 位元組)。**#597 起多一個有記錄的差異**:CLI 列表模式(不帶 `--apply`)的 `--citekey`/`--person`/`--tier` 收窄候選列與歧義條目,並印出收窄後/全部的計數;MCP 的列表面沒有對應參數。理由同 `--rows` 那格:CLI 的輸出進人的終端機,逐篇查證要的是「這一篇還有誰被提名」;MCP 呼叫端拿得到 `candidateTotal` 與逐列 id,可以自己篩,而為它加參數會讓一個 LLM 呼叫端以為沒列出的候選不存在——那正是 CLI 用「全部 M 個」那一行防的誤會,MCP 面沒有對應的一行。MCP 呼叫端若需要逐篇收窄,要重開這一格並一起裁位元組預算) **#641 重新確認,裁決不變、契約有改**:(a)「無法唯一定位」的 work 多一類——load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),併進 `unlocatableCitekeys`,各腿既有的處置不變;(b) person 側新增 `unlocatablePersonKeys`(person key 重複、或 load 判定它的檔案寫入時會被拒——作品側的「共用 id」不收,理由在型別 doc):apply/reject 整批拒絕、judge/refute/undecided 該筆略過並具名、CLI 篩選式 `--apply` 排除並另列、MCP 列表以 `unlocatablePersonKey` 標出、App 裁決台的 accept 具名拒絕(`AdjudicationError.unlocatablePerson`)——三面同一個定義。在此之前 judge 與 apply 先寫 work 再寫 person,person 那一格在寫入當下被拒時作者位已升格、verdict 沒寫(#631 R2 verify 真 binary 重現)。拒絕與略過訊息共用 `UnlocatableReason`。 。**#648 重新確認,裁決不變、契約有改**:judge/refute 的理由上限 4,096 位元組(與 undecided 的說明同一個常數 `maxStatementBytes`),超過整批拒絕、零寫入、不截斷,在任何 store 狀態分支之前擋——兩面同契約(CLI `--judge`/`--refute` 的 help 與 MCP `judge`/`refute` 的描述都寫明上限);judge 的寫入集合(entry 與 person)與 undecided 的 person 在任何檔落盤之前逐筆 preflight,寫出後超過讀取上限(8 MiB)即整批拒絕、錯誤具名記錄與兩個位元組數(在此之前 judge 先寫 entry、person 在寫入當下才被拒——作者位歸戶而 verdict 沒寫);attribute_org/split_author/un_split/drop_author 的「整批驗證通過才寫」自此也含 encode 與讀取上限。apply/reject 維持既有的逐筆收容(寫入失敗逐筆回報),拒絕自此具名**#663 重新確認,裁決不變、契約有改**:歧義條目對隸屬的說法自此由 `TimelineOf.standing`(AkashicCore;與匯出端 `researcher.status` 同一個推導,CLI、MCP、App 三個面與匯出各自推導的四份併成一份)決定。`formerAffiliation`/`formerAffiliationEnd` 只給**宣稱已結束**的段(有 end 或 ended-unknown);只被觀測到的段(attested、沒有 end)改送新鍵 `observedAffiliation`/`observedAffiliationAt`(最近的一個觀測點)——被看到過不等於離開了,匯出端對這種人說 `undetermined`(#661)。混合情形(有 end 的段加上觀測段)兩組都送。**舊鍵 `formerAffiliationAttested` 退場、不沿用**:讀它的呼叫端得到缺席,不是一個換了意思的值;**`formerAffiliation` 的意思收窄為「宣稱已結束」**——先前它也被送給只被觀測到的人,那是同一個鍵名在改動前後指不同的事,所以在這裡寫明。CLI 措辭同步:`曾隸屬:X(…)` 只給已結束的段,只被觀測到的是 `觀測到隸屬:X(觀測點)`,混合情形兩者都印;App 裁決台同。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | - -**#457 起另有一個「把作者位移除」的面**(`--drop-author` / `drop_author`,兩面同走 -`dropAuthors`):`Author` 的三態(#323)都假設那一格背後有一個作者,而實測(2026-09-09, -全庫 3,885 個 distinct literal 逐一掃過)有一個不是——PsycInfo 的 `No authorship indicated`, -21 筆。在此之前**沒有任何面到得了 0 個作者位**:`apply` 升格、`attribute_org` 改歸屬、 -`split_author` 增加數量、`un_split` 減到 1。唯一的路是手改 YAML。 - -它今天的代價不是「還沒歸戶」,是出貨的 `.bib` 裡有一個**被捏造**出來的人 -(`AUTHOR = {indicated, No authorship}`),citekey 也照它生。APA7 §9.12 對無署名作品 -以標題起首,那要求作者位是**空的**。 - -**以值定位不用索引**(同 `un_split`):索引在同一批的前一次移除之後會位移,而「這個字串 -不是人」本來就是關於字串的宣稱。同一筆 work 的作者位裡出現多次 → **拒絕不判定**。 -**兩面同契約**:per-id 顯式、理由必填、整批驗證通過才寫、單獨呼叫不與其餘腿組合。 -**#658 起 CLI 另過 #298 的目標確認閘**(使用者 2026-09-28 裁決:它沒有具名逆操作、被移除的作者位不留位置,寫錯 store 要靠 git 收拾);閘在參數組合檢查之後、開 store 之前。MCP 面不閘,理由同橫切選項表的 `--yes` 列。 - -**同輪動到 export 面**:`apa7Report` 對帶移除記錄的 work 抑制 `AUTHOR` 的 required 檢查 -——形狀取自 #406 的 `paginated`,且**空的 `authors` 本身不足以抑制**(那是「還沒記」, -正是該檢查要抓的東西;同 `paginated` 的 `nil` 照報)。少了這一半,21 個被捏造的 byline -會變成 21 個 error。 - -**#443 起另有一個「把作者位拆開」的面**(`--split-author` / `split_author`,兩面同走 -`splitAuthors`):一個 literal 裝了兩個人時,在此之前**沒有任何面拆得開**——`apply` -只能把它整個升格成一個 person,而那會建出一個不存在的人。實測 4 筆「某人與雷庚玲」。 - -**收分隔符而不是拆好的名字**:後者等於讓呼叫端**編造**,打錯一個字就寫進 store 而沒有 -任何東西擋得住;收分隔符則拆出的每一段必然是原文的子字串。切出空段即拒絕(分隔符選錯); -換行等空白段同拒;段數有上界(32,且在 materialization 之前生效——literal 是未信任 -輸入,高頻分隔符不得先被切成無界陣列再拒);分隔符**在語法上無法**含 `=`(第一個 `=` -之後一律是理由——誤解析由報告的 separator/judgement 欄揭露、有測試釘住,根治需 -結構化參數:MCP 收 object 陣列、CLI 形式另議。**觸發條件(#451 (b)):出現一個真實 literal -其正確分隔符含 `=` 時重開;在此之前不做**——實務上含 `=` 的人名分隔符零實例,先造介面等於 -替一個還不存在的輸入設計形狀)。分隔符、**原始 literal 與理由**都被丟棄,而報告逐筆印出 -「原文、用什麼切、切成什麼、為什麼」(`lossless-intake` 的丟棄必須可見;揭露的是 -消毒顯示形——`displaySafe` 200/300 上限,#165 的既有取捨)——~~**store 不留原文與 -理由是本面的誠實邊界**(R1 verify):拆分沒有「被判定的另一方」可落 verdict,work 側 references -值域目前只收識別碼;un-split 所需資訊在 store 內不可回復(只在 store 的 git 歷史),持久化需要 -值域的顯式裁決(follow-up)。~~ → **自 #450 起不成立**(Spectra change -`split-verdict-historical-reference`,2026-09-07):拆分的判定持久化到 work 側 `references` -(`{field: authors, value: 原 literal 逐字, judgement: 拆為 ⟦a⟧ ⟦b⟧:理由, rests-on: []}`——第 15 -條邊值域的顯式擴充、statement 走 `SplitRecordValue` 單一解析器、需要 store format 16),與作者位 -改寫在**同一次**寫入,所以拆分永遠不會沒有記錄、也不會記兩次;段含文法保留字元 `⟦⟧` 拒絕。 -報告的 `original`/`judgement` 仍是消毒顯示形,`recorded: true` 說記錄寫了。un-split 所需資訊 -自此在 store 內;**un-split 操作面另開 issue**,兩面都還沒有。 - -**拆出來的仍是 `.literal`**——拆是**形狀**修正不是身分判定,每一段各自走既有消歧路徑。 -**兩面同契約**:per-id 顯式、理由必填、**同一個作者位一次只能拆一次**(去重以解析後的 -(citekey, index) 為鍵——字面去重會被同 slot 異拼法繞過,R1 verify HIGH)、**單獨呼叫 -不與其餘腿組合且組合被顯式拒絕**(#418 樣式;R1 verify 前靠分支順序隱含達成、其餘腿被 -靜默忽略——那不是「不可組合」而是隱藏優先序。空陣列與 JSON null 同拒)。它改的是 -作者位的**數量**,混在一批裡會讓其他腿的 index 意義改變。同一筆 work 的多個位置一起拆時 -**index 位移由實作處理**(由大到小),呼叫端給的是原始索引(報告的 `authorIndex` 也是 -原始索引,非寫入後位置)。 - -**#513 起多一個把上面那個拆分合回去的面**(`--un-split` / `un_split`,兩面同走 `unsplitAuthors`):#450 讓拆分的判定持久化到 work 側,於是 **un-split 所需的全部資訊自此在 store 內**——但沒有面把它合回去,還原只能手改 YAML(三個動作要一致,而那是 `replace-endnote-and-zotero` 第 4 條要防的安靜失敗)。 - -**以值定位**(`citekey:原literal`,`ProvenanceReference` 的既有立場 D2),**同 value 多筆記錄拒絕不判定**——那時兩筆記錄的 `parts` 可能不同,而「哪幾個作者位屬於哪一筆」store 裡沒有東西說得出來(形狀取自 `akashic_enrich` 對 DOI 命中 ≥2 筆的 `ambiguous`)。**還原後刪掉那筆記錄**:它的存在理由是「`authors` 已經沒有原 literal 了」,還原之後那句話為假——留著會讓 store 斷言一件假的事,並點亮 `staleSplitRecords`(一次合法的 un-split 製造一條永久 warning)。歷史留在 git,與 #443 那 4 筆的既有取捨相同。**#659 起兩面同時要求那個 git 真的在**:刪記錄之前比照 #573 檢查那些 work 檔已 commit、乾淨,否則整批拒絕、零寫入(使用者 2026-09-27 對移除面一族的裁決——要求已 commit、不改 store format——套在這裡,所以不改成留一筆「已還原」記錄)。 - -**兩面同契約**:per-id 顯式、整批拒絕零寫入、**與 `split_author`/`attribute_org` 三者互斥且都不與其餘腿組合**(它同樣改作者位的數量,N → 1)。段已升格為 `.key`/`.organization` 時拒絕並**指向 `demote`**——把已歸戶的身分塞回黏著的 literal 是判定的逆轉,不屬本面。 - -**它不是新的一列,是既有 `akashic_resolve_people` 的一個參數**——加列會讓上表的封閉列舉與機械稽核(`grep -oE 'Tool\(name: "akashic_[a-z0-9_]+"'`)對不上。#513 的 Expected 寫「兩張表各加一列」,那個寫法會壞掉本檔自己的稽核程序。 - -**#443 起多一個團體作者的升格面**(`--attribute-org` / `attribute_org`,兩面同走 -`attributeToOrganizations`):`.literal` → **`.organization`**。`Author` 的三態 -(#323)在此之前只有兩態接得起來——`apply` 升格成 `.key`,而團體作者**只能在建檔時 -指定**,既有記錄改不了(實測 #443:兩筆機構被記成 `.literal` 作者,唯一出路是手改 -YAML)。**放在 `resolve-people` 是因為作用對象相同**(work 的一個作者位),不是因為 -它是消歧——org key 由呼叫端顯式給,不經提名,所以沒有 tier 也沒有候選清單。 -**兩面同契約**:per-id 顯式、judgement 必填、不提供批次形式、**不與 apply/reject 組合** -(升格目標是另一個值域,混在一批裡會讓「哪些寫了」難以判讀——同 #418 對 `repoint`/ -`demote` 的既有裁決)。失敗語意:**整批驗證通過才寫**,任一筆前提不符即零寫入。**#669 起**:目標 organization 的 key 有不只一筆記錄時同樣整批拒絕(寫進哪一筆是猜;在此之前那裡以 `uniqueKeysWithValues` 建表,重複直接讓 process trap),兩面同一條 service 路徑。 - -**#386 起多一個 per-id 判定面**(`judge` / `--judge`,兩面同走 `judgeAuthorships`): -收 `citekey:authorIndex:personKey=理由`,**歧義列也適用**——歧義的意思是提名器分不出來, -不是人/AI 分不出來(`identity-is-judged-not-matched`)。**契約有記錄的差異**:CLI **不提供** -篩選式批次形式(judgement 必填形成摩擦,批次會讓它退化成罐頭);MCP 面沿用該面既有的 -per-id 顯式契約。兩面的失敗語意相同且刻意分兩類:輸入語法錯整批拒絕零寫入、store 狀態不符 -該筆略過並具名。 - -**#388 起 CLI 多一個 `--rows`(歧義段列數上限的旋鈕),MCP 面刻意不加**——這是本列 -第三個有記錄的差異。理由不是「還沒做」: - -- **消費端不同,而上限保護的是消費端。** CLI 的輸出進人的終端機(可捲、可 pipe、 - 可 `grep`);MCP 的輸出進 LLM context(更貴、且呼叫端無法在收到後丟棄已付的代價)。 - 兩面的位元組預算本來就不同值且刻意如此——CLI 128 KB、MCP 48 KB。給 MCP 一個放寬 - 列數的旋鈕,等於讓呼叫端有辦法撐爆自己的 context。 -- **MCP 已有對等的揭露**:`candidateTotal` 送分母讓程式判斷「我看到的是不是全部」, - 而 CLI 那面是印差額讓人一眼看到。缺的從來不是資訊,是**人**這一側取回全部的手段。 -- **不對稱的方向與既有兩條一致**:本列的另外兩個差異(tier 閘、批次形式)也都是 - 「CLI 給人多一個把手/多一道摩擦」。三者同向,不是各自為政。 - -日後若 MCP 面真的需要(例如出現一個要一次讀完全部歧義的 LLM 流程),要重新裁決的是 -**該面的位元組預算**而不是列數——列數上限追不上內容,這是 #236 R4 已經量過的 -(列數與 ref 上限都在時仍產出 3,844,596 bytes)。 -| `akashic_record_divergence` | `record-divergence` | ✅(**#611 重新確認,裁決不變、契約有改**,兩面同走 `recordDivergence`:(a) 同一個 id 的既有記錄若候選 (shape, key) 與這次不同——歧異記錄的 id 只由候選 key 決定、不含形狀,`rename` 就地改寫候選而不重算 id——**整個拒絕、不覆寫**(`StoreIOError.divergenceIdHeldByOtherCandidates`,訊息點名現有候選、說原因是 key 還是形狀、出路是 `resolve-divergence`/`dismiss-divergence`;先前互相覆寫、不出聲);(b) 既有判斷與 prefers 的保護改為寫入當下重新讀磁碟上那一個檔,不看呼叫端的快照。兩面的說明(MCP 描述、CLI abstract)各加一句;`docs/store-format.md` §5.8 第 7 條是 normative 的版本)。**#611 R3**:訊息改成逐行、出路在第二行——兩面的錯誤出口都逐行截 400,R2 的單行版在兩面都被截在出路之前;出路同時寫出 CLI 的 `dismiss-divergence` 與 MCP 的 `akashic_dismiss_divergence`,並註明 `resolve-divergence` 只有 CLI。MCP 描述的出路改成 `akashic_dismiss_divergence 或合併`(-5 bytes) | -| `akashic_dismiss_divergence` | `dismiss-divergence` | ✅(#586;Spectra change `divergence-dismissal`)。歧異記錄在此之前記得下去、解不掉(org shape)、也刪不掉,唯一出路是手改 YAML。**它與 `resolve-divergence` 的 CLI-only 裁決不矛盾**:那一格缺席的理由是操作的性質(合併+全庫改寫+刪檔,不可逆、要逐筆過目);本面只刪一筆記錄、候選實體與參照都不動、刪除前要求記錄檔已 commit——被刪的東西在 git 裡有副本,LLM 消費面記錯的一筆可以由同一面收回(`akashic_record_divergence` 是 MCP 面,只有記錄面沒有移除面會讓記錯的代價落在手改 YAML)。兩面同契約:以 UUID 定位、理由必填且只進報告(使用者 2026-09-27 對移除面一族的裁決)、不寫進 store、不改 store format、乾跑只回報(CLI `--dry-run`、MCP `dry_run`,兩面預設都是實跑)。~~**CLI 不過目標 store 確認閘**:以 UUID 定位,指錯 store 只會找不到那筆記錄——閘防的「在錯的 store 上照樣對得上」在這裡不成立~~ → **#586 R1 verify 起 CLI 實跑過目標 store 確認閘**:那個前提是假的——歧異記錄的 UUID 由候選 key 決定(`DeterministicUUID.forDivergence`),clone、備份、或兩個各自記下同一組候選的 store 裡,同一筆記錄的 UUID 相同,正是 #580 裁決的形狀(錯的 store 上照樣對得上)。乾跑不閘;MCP 面不閘,理由同橫切選項表的 `--yes` 列 | -| `akashic_update_person` | `update-person` | ✅(#68)。**#554 R26 D73 重新確認(R27 補記),裁決不變、契約有改**:references 的 append-only 去重自此比位元組(`byteExactKey`)——canonical 相等而位元組不同的兩筆都會進 store(先前是靜默 no-op);重複的偵測面缺口記 #582(R26 verify requirements 第 10 列、regression 第 54 列:R26 改了語意而這一列沉默)。**#674 重新確認,裁決不變、契約有改(收緊)**:`references`(MCP 在 `fields` 裡、CLI 在 `--fields` 的 JSON 內)與 `akashic_update_venue` 的 `references`(CLI `--references`)改呼叫**同一個解析函式**(`parseReferenceObjects`;解析與驗證都在讀 store 之前,CLI 的 `validate()` 經 `checkUpdatePersonFields` 呼叫同一個函式,違反是用法錯誤 64)。往 venue 那一邊收,被拒的輸入類別(先前寫得進去、現在整個呼叫拒絕):(1) retrieval 沒給 `status`(先前預設 200,會把離線掃描檔記成 HTTP 200);(2) `status` 不是整數(boolean、小數、字串——先前 boolean 被當 1、字串被當 200)或不在 100–599;(3) 不認得的鍵(先前靜默忽略,打錯的欄位名不出聲);(4) `value`/`media_type` 不是字串、`rests_on` 有非字串元素(先前靜默變成 nil/略過那些元素);(5) `statement` 超過 4,096 位元組、`rests_on` 超過 20 個、一次超過 200 筆(先前無上限);(6) `url` 不是 http/https(`file:`、`ftp:`、無 scheme)或主機為空或含帳密(userinfo);(7) `retrieved` 不是 ISO 8601 日期/日期時間(裸日期 `YYYY-MM-DD` 照收);(8) `field` 只有空白(先前只擋空字串)。~~**有記錄的差異**:空陣列(`"references": []`)在 person 仍是 no-op(`fields` 裡被提及的一格,先前就是),在 venue 是錯(獨立參數);~~ → **#695 起不成立**(見本列末);兩面各自收哪些 field(person 收 verdict 以外、venue 只收 issn/names)是 holder 的政策不是契約。~~`enrich` 的 `sourceURL`/`sourceRetrieved`/`sourceStatus` 仍是各自的契約(沒有 url/retrieved/status 範圍的檢查)——不在本次範圍,追蹤於 #695(待使用者裁決)~~ → **#695 起不成立**(見 `akashic_enrich` 那一列) **#695 重新確認,裁決不變、契約有改**(使用者 2026-09-30 裁決「兩面都拒絕」):`"references": []` 在 person 改為拒絕(先前 no-op),與 venue 同一句話——拒絕住在共用的 `parseReferenceObjects`,兩面自然同句;CLI 的 `validate()` 經 `checkUpdatePersonFields` 呼叫同一個函式,違反是用法錯誤 64。status/url/retrieved 的形狀搬到 Core 的 `RetrievalWriteShape`(`enrich` 的來源欄位呼叫同一個函式),本面的訊息不變。R1 verify 起同一個函式多拒四種(兩面、三個入口一起):`@` 後接組合符號/ZWJ/VS16 的帳密(定界符改在 scalar 上找)、url 含控制/格式/零寬字元或空白、url 的主機含反斜線、media_type 含這類字元或前後空白。R2 verify(2026-10-01)起再多拒三種:url 主機部分的全形/相容形定界符(`@`、`:`、`℀` 這類,相容分解含 `@ : / ? # \`;IDN 主機照收)、不是 ASCII 數字的 port、空的或只有空白的 media_type;`https://` 前綴改在 scalar 上比;retrieved 的原值排在理由之後、以逃脫後的長度截;控制/格式字元的補救改說百分比編碼或 punycode(拒絕範圍不變,仍涵蓋路徑與 query)——兩面、三個入口一起。enrich 面另兩處:`sourceDigest` 以送來的原值驗(前後空白拒絕),空的 `sourceMediaType` 不寫進 reference。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#564 修正輪重新確認(使用者 2026-10-02 裁決第 1、2 點),裁決不變、契約有改(BREAKING)**:兩面同時新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`)與 `remove_names`(CLI `--remove-name`)。`fields.names` 讓名字進或出 authorized 時理由必填(缺了整批拒絕、零寫入——先前不附理由也寫得進),寫 `field: authorized` 的~~指定/確認/撤回~~ → 指定/撤回(b34 起留下的不寫確認)記錄(需要 store format ≥ 22,回報 `judgementsRecorded`;乾跑回 `judgementsToRecord`);只動 variant 的不必。替換拿掉有記錄的名字、或讓對外形整個離開 names,具名拒絕並指出口。理由與證據只伴隨 `fields.names`。`remove_names`:`<名字>=理由`,只收最後一筆名字分類記錄是撤回的 variant 名字,連同記錄一起刪,理由只進報告(`namesRemoved`),刪前要求 person 檔已 commit(乾跑不查);單獨呼叫(MCP 不帶 `fields`、CLI 不帶 `--fields`)。一次至多 200 個名字(`fields.names` 的 authorized——R2 起 variant 不計、`remove_names`;b33 X1 第 15 列:這一句曾留著「兩個分割合計」)。兩面同一條 `updatePerson`;CLI 的 `validate()` 呼叫同一個 `checkUpdatePersonArguments`(`--fields` 從 stdin 來時組合的檢查在執行期)。沒有有記錄的差異 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:(a) ~~讓**沒有記錄**的對外形整個離開 names 改為放行(附 `judgement`;先前一律拒絕,整份替換改正對外形拼寫的既有用法被改成三步——第 4 列),回應多 `authorizedRemovedWithoutRecord`(dry_run 同;它沒有地方錨定記錄)~~ → b34 退場(見下方 b34 的 (a));(b) 沒附理由的拒絕在 store format < 22 時說出寫入閘與升級路徑(第 5 列);(c) 一次上限只數 authorized(variant 不寫記錄、不計——第 9 列;讀 store 之後另以這次要寫的記錄數再擋);(d) `remove_names` 刪完沒有名字整批拒絕(第 18 列,與 organization、venue 同),沒有記錄的名字被拒時指向 `fields.names` 整份替換、不在 authorized 而最後一筆是指定的指向「先指定回去再撤回」(第 6/23 列)。MCP 描述那一句改短(「拿掉有記錄的名字整批拒絕」),細節在 CLI help 與 store-format §3.5。兩面同一條 `updatePerson`。~~**交給使用者**:「只動 variant 的不必附理由」是實作對裁決第 1 點的讀法(第 24 列);附理由時仍在 authorized 的每個名字都寫確認(第 10 列)~~ → 使用者 2026-10-05 裁決:前者確認,後者改為只替進出的名字寫(見下方 b34 的 (b)) **#564 b34 重新確認(使用者 2026-10-05 裁決),裁決不變、契約有改(BREAKING)**:(a) 上面 R2 的 (a) 退場:對外形在替換裡整個離開 names 一律整批拒絕(有沒有記錄都一樣——移出要寫「撤回:理由」、記錄錨定 names,出口是放在 variant),`authorizedRemovedWithoutRecord` 不再出現(b33 X1:那條路的理由不入庫、format < 22 的寫入閘沒觸發);(b) 只替進出的名字寫記錄(指定/撤回),留下的不寫確認;附理由而沒有名字進出 authorized 整批拒絕;只動 variant 不必理由(使用者確認了上一格的讀法);(c) 替換不把 person 刪到沒有名字;(d) `remove_names` 的拒絕在 format < 22 時說出寫入閘、刪到沒有名字與 canonical 孿生拼法說出工具面的出口;(e) **有記錄的差異**:CLI `--remove-name` 過 #298 的目標 store 確認閘(裁決第 4 點;`--dry-run` 不擋),MCP `remove_names` 不閘(橫切表 `--yes` 列:active store 是 session 狀態)。MCP 描述那一句改寫(「只替進出的名字寫指定/撤回」「對外形整個離開 names、拿掉有記錄的名字都整批拒絕」),淨減 4 位元組。兩面同一條 `updatePerson` **#564 b37 重新確認(b36 Y1 第 2/3/14 列),裁決不變、契約有改**:(a) `fields` 路徑(一般欄位、`names` 附 `judgement`,含 dry_run)對兩筆**不同**的 person(id 不同)共用同一個 key 整批拒絕、零寫入——先前寫進先載入的那一筆,名字分類記錄也一樣。不問涵蓋更廣的 `unlocatablePersonKeys`:同一筆記錄的 legacy 拷貝與其他「load 判定那筆的檔寫入時會被拒」的是給多檔寫入者擋撕裂的,這裡只寫一個檔,由寫入當下的 #631 前置擋下、訊息說出同一個操作稍早寫過(#705);(b) MCP `judgement` 屬性說明改成實話(「名字進出 authorized 時必填,否則拒收」——先前仍寫「仍是對外形的寫確認」,只讀屬性說明的呼叫端會以為附理由能替沒動的名字留確認),淨減 9 位元組,真 binary 的 `tools/list` 由 `NameClassificationSchemaTextTests` 釘住。兩面同一條 `updatePerson` | -| `akashic_update_entry` | `update-entry` | ✅(#544;work 的部分更新面,兩面同走 `AkashicService.updateEntry`)。**預設乾跑**:CLI `--apply`、MCP `dry_run`(預設 true),比照 `enrich`;CLI 的 `--apply` 過 #298 的目標確認閘(`WriteGateRulings` 的 `update-entry` 格),MCP 不閘(橫切選項表 `--yes` 列的理由)。**`remove_fields`/`--remove-field`(#544)**:`<鍵>=理由` 移除一個 `fields` 的值——在此之前沒有任何面刪得掉它(`enrich` 只補不存在的鍵、`import-zotero` 只對 Zotero 來源的記錄整份替換),唯一的路是手改 YAML(#544:21 筆 `abstract` 是 Crossref 的錯誤頁,兩筆的 `journaltitle`/`publisher` 是補助計畫名稱與總統令字號)。它是判定(`two-kinds-of-edits` 的 AI 欄),套使用者 2026-09-27 對移除面一族的裁決:理由必填、只進報告(`fieldRemovals`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`)。#544 的 body 寫「留記錄、形狀取自 #450」——**這一條沒有照做,而且那個決定不是使用者下的**:09-27 的裁決是為 #588/#572/#586(邊、識別碼、divergence 記錄)下的,把它**延伸**到 `fields` 的值是 Claude 代裁(b11c 實作,2026-09-29),**待使用者確認**——#544 自己沒有裁決紀錄,issue 要的 #450 形記錄(work 側 `references` 的退役記錄)沒有做;要補的話是第 15 條邊的值域擴充與 store format 的變更,需要顯式裁決,不是本列的旗標。指向被移除鍵的 `fields.<鍵>` reference 一併刪除(#588 `remove_issn` 的同一處置:留著它,那筆的語意會從「值出自這份來源」安靜地翻成 #517 的負結果「查過了、沒有」)。**為什麼不違反 `lossless-intake`**:那條規則管**進來的那一刻**,禁的是匯入時靜默丟掉來源給的東西;本面是事後的更正——人讀過值、判定它不是來源給這個欄位的資料(錯誤頁被當成內容收下,本身是那條規則的反面),而且不靜默(理由必填、逐欄回報、值的前 300 字印在報告)、不讓資訊不可回復(移除前的檔在 git)。兩面同契約:鍵不存在、同鍵兩次、理由空白或過長、一次超過 200 個——整批拒絕、零寫入;work 無法唯一定位(`unlocatableCitekeys`)時拒絕。由被移除值推導的 literal venue 邊不動、具名在 `venueEdgesFromRemovedValues`(刪它是 `resolve-venues --drop-venue` 的判定,#572);**已歸戶的 key 邊連同 venue 上的 confirmed verdict 也不動、具名在 `venueKeyEdgesFromRemovedValues`**(處置是先 `--demote` 把邊退回 literal、再 `--drop-venue`;b11c R1:先前只列 literal 邊,而「補助計畫名稱被記成期刊」若已被歸戶,錯的歸屬會繼續有效而報告沉默)。**會把值補回去的路徑有三條,都在 `reintroductionNote` 說出來**:`import-wos` 回填(只多不少,缺席的鍵會被補進去)、`enrich`(add-only)、Zotero pull(主來源是 Zotero 的記錄另附 `zoteroNote`)——store 不記得這個值被判定過不屬於這裡,重跑同一份來源它會回來(報告先前只點名 Zotero)。移除 APA7 必要欄位時附 `apa7RequiredNowMissing`(用既有的必要欄位表 `BibExport.apa7Report`、只報這次新增的缺漏;`validate` 不報、`export-bib` 才印 ERROR)。`remove_fields` 兩面同一個 payload、沒有有記錄的差異,CLI 不設 `--json`(寫入面封閉例外形);MCP 的 `citekey` 缺少或空白是缺參數錯誤(該工具自己的必填檢查,不是「找不到:citekey「」」)。**#614 起兩面同時新增 `add_sources`/`--add-source`**:把已存進 `sources/` 的內容宣告為這篇的副本(`akashic.sources`,store-format §2.4.1)。在此之前沒有任何入口寫得進它——`store-source` 只存位元組與取得記錄、`enrich` 的 `sourceDigest` 寫的是欄位層級的 reference(值 ← 證據,不是作品 ← 副本)、`link` 只管 cites/related。它是落地不是判定(`two-kinds-of-edits` 的程式欄):「這份 PDF 是這篇」在上游判定(`akashic-fetch-fulltext` 的驗證)。兩面同契約:digest 必須合法(空內容的 digest 以 #654 的原句拒絕)、新加的 digest 已在本機 `sources/` 且 index 有取得記錄(`LibraryStore.sourcePresence`;已連過的是 no-op、不檢查——全是已連過的就不讀 index;blob 的位置必須是普通檔,目錄與 symlink 具名拒絕)、同一次不重複、至多 200 個,否則整批拒絕零寫入;add-only 冪等(`sourcesAlreadyPresent`,沒有新東西就不寫);報告逐個帶 index 的取得記錄——**有記錄的差異(b11c R1)**:MCP 面的 `sourcesAdded` 截 20 筆(`sourcesAddedTotal`/`truncated` 揭露)、CLI 全列,理由同 `--rows` 那格(每筆帶最多約 2 KB 的第三方字串,輸出進 LLM context、呼叫端無法丟棄已付的代價)。**讀取面**:`get-entry`/`akashic_get_entry` 自 b11c R1 起列出 `akashic.sources`(同一條 `entryDict` 路徑;在此之前能寫不能讀,#218/#219 的形狀)。**四條腿兩兩各自單獨呼叫**(`remove_fields`/`add_sources`/`remove_zotero_sources`/`remove_sources`,共 6 對,任兩條組合都整批拒絕、零寫入——移除是判定、加入是落地,被移除的東西也各不相同,混在一次呼叫裡報告與 git 閘的語意也混在一起;`ServiceArgvExitCodeTests.testUpdateEntryLegsAreMutuallyExclusive` 與 `StdioE2ETests.testUpdateEntryDefaultsToDryRun` 逐對釘住)。**#680 起兩面同時新增 `remove_zotero_sources`/`--remove-zotero-source`**:移除 work 記下的 Zotero 來源(主來源 `provenance` 或附加來源 `provenance_additional`)。在此之前活著的 Zotero 來源沒有任何移除面——#610 對「其中一筆記錯了」的處置寫的是手改 YAML,#679 對「附加來源沒記 `library_id`」的出路也只能手改(`replace-endnote-and-zotero` 第 4 條的形狀);App 的裁決台只處理**已在 Zotero 端刪除**的來源。**這一條腿不是 `add_sources` 的逆操作**(那條腿寫的是 `akashic.sources` 的副本 digest,逆操作是 #677 的 `remove_sources`);名字刻意帶 `zotero`。它是判定(`two-kinds-of-edits` 的 AI 欄):套使用者 2026-09-27 對移除面一族(#572/#588/#586)的裁決——`<來源鍵>=理由`、理由必填且只進報告(`zoteroSourceRemovals`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`);預設乾跑,乾跑不需要 git。**來源鍵**與跨記錄警告、`ambiguousSourceClaims` 同一種(`ZoteroSourceClaims.key`):`:`,沒記 `library_id` 的來源是 `?:`(#679 的出路);`05:K` 與 `5:K` 同一個來源。每個來源鍵都要在這筆 work 命中,否則整批拒絕、零寫入;同一來源兩次、形狀錯、理由空白或過長、一次超過 200 個同樣整批拒絕。同一筆 work 的主來源與附加來源恰好是同一個來源時兩處都拿掉。**主來源被移除而附加來源仍在時,附加來源不升格為主來源**(升格會把書目欄位的改寫權交給另一個 library,與 App 的「與 Zotero 脫鉤」同一條裁決;`primaryRemovedNote`);連結狀態的前後照 `Entry.zoteroLinkState` 的既有定義具名(`zoteroLinkState.before/after`,本面不改那個定義);書目欄位不動。**移除對下一次 `import-zotero` 的意義依「移除之後還有沒有別的 entry 宣稱這個來源」而定**(b13f R1 verify:首版無條件說「另建一筆新 entry(已沒有 entry 宣稱它了)」,在 #610 的主場景與 #679 的出路兩個形狀都是假的)——`reimportNote` 逐來源說、每個被移除的來源另帶 `reimportEffect`(封閉四值:`newEntry` 沒有任何 entry 宣稱它、Zotero 端仍有那個條目時另建一筆;`routesToOther` 仍由另一筆宣稱、匯入照常路由到那一筆;`stillAmbiguous` 仍有兩筆以上、匯入仍略過;`notAClaim` 沒記 `library_id` 的附加來源本來就不算宣稱者、移除它不改變匯入行為),宣稱者用 `ZoteroSourceClaims` 那一份定義算,行為由 `ZoteroSourceRemovalReimportTests` 與 `ZoteroSourceRemovalReimportBehaviourTests` 釘住;已在 Zotero 端刪除的來源不說。移除讓這筆變成 orphaned 時另附 `orphanedNote`(App 的 Orphans 頁會列它並提供破壞性動作,而這筆的 Zotero 條目並沒有被刪)。**#677 起兩面同時新增 `remove_sources`/`--remove-source`**:`=理由` 收回一條 `akashic.sources` 上的宣告——在此之前 `add_sources` 只能加,連錯了(連到別篇的 PDF)唯一的路是手改 YAML 或以 git 還原那個 work 檔。它是判定(`two-kinds-of-edits` 的 AI 欄:「這份內容不是這篇的副本」要讀內容才知道),套使用者 2026-09-27 對移除面一族的裁決:理由必填(≤ 4,096 位元組)、只進報告(`sourcesRemoved`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`,與 `remove_fields`、`remove_zotero_sources` 同一支閘)。兩面同契約:digest 形狀不對、不在該 work 的 `akashic.sources` 上(具名列出)、理由空白或過長、同一個 digest 兩次、一次超過 200 個——整批拒絕、零寫入;work 無法唯一定位時拒絕。**只移除宣告**:`sources/` 的 blob 與 `index.jsonl` 的取得記錄不動(同一份內容可能被別筆 work 宣告或被欄位層級的 reference 引用——刪存檔是另一件事,`#544` 同族的移除面,另案),`Entry.references` 也不動(§2.4.1:兩種關係不得合併)。**不要求本機有位元組**:別台 clone 上 `sources/` 本來就可能不在,連錯的宣告在那裡照樣要收得回來;報告帶 index 的取得記錄(origin、mediaType、note…)只是讓乾跑的人認得出是哪份內容,本機沒有存檔或 index 讀不到時省略(`contentInfoUnreadable`)。**有記錄的兩面差異(b13f R1 verify,比照 `add_sources`)**:MCP 面的 `sourcesRemoved` 每一筆都列(`digest`+`reason`——理由只在報告裡有一份,不截),只有前 20 筆多帶 index 的取得記錄(每筆至多約 2 KB 的第三方字串),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列。與 `add_sources` 不同的是**不截整筆**:截掉第 21 筆以後的理由就無處可記。**寫檔之後 index 重建失敗(b13f R1 verify)**:三條移除腿與 `remove_fields`(以及 `update-venue --remove-reference`)呼叫仍回成功、報告多 `indexRebuilt: false`/`indexRebuildError`/`indexNote`——不擲錯,因為錯誤出口逐行截 400 字元、一段長理由會被截掉,而移除已落盤、理由只在報告裡、重試會被拒;`import-zotero` 的先例(b11b R1)擲錯並把整份報告放進錯誤訊息,那份報告是計數與 citekey 清單,短。呼叫端要讀 `indexRebuilt`;CLI 的 `query` 不看 mtime,要跑 `akashic doctor`(MCP 讀取面依 mtime 自動重建)。 **WriteGate**:`update-entry` 整個命令是一格(過閘,`--apply` 才閘),第三、第四條腿都不新增格;一格就夠的理由(b13f R1 verify 第 14/43 列):裁決表的粒度是命令,只有 `resolve-*` 與三個逐腿命令才分腿(它們的腿各有不同的閘決定);`update-entry` 的四條腿決定相同——閘在命令入口、看 `--apply`,每條腿都預設乾跑、實跑才改寫既有記錄檔——所以新增第五條腿只要也預設乾跑就自動在同一格之內,不預設乾跑的腿要重新裁決(那時才需要逐腿);兩面同一個 payload、沒有有記錄的差異(`zoteroSourcesRemaining` 兩面都截 20 筆並附 `zoteroSourcesRemainingTotal`——一筆 work 的附加來源數不受本工具限制)。同輪 `Entry.validate()` 對沒記 `library_id` 的附加來源報 warning(#679,per-record 的 warning,走既有的 `validate`/`akashic_doctor` 面,沒有新工具面),跨記錄的多筆宣稱 warning(#610)的出路改指這一條腿。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_create_entry` | `create-entry` | ✅(#206;#455 起兩面同走 `createEntries`——MCP 單筆是薄包裝,CLI 的 JSON 陣列是批次形,批次面的裁決見 CLI-only 表)。**#637 重新確認,裁決不變、契約有改**:DOI 已在庫(或同一批稍早的一筆已用過)時照常建,兩面同一份 `doiHits` 具名回報命中的 citekey(MCP 回應鍵、CLI 印 ⚠)——只回報不拒絕:DOI 相等依 `identity-is-judged-not-matched` 足以判定同一篇,但 erratum 會與原文共用 DOI,拒絕會擋掉這類合法記錄;CLI 的 `--dry-run` 也回報(寫入前看得到),MCP 面寫入前的查法是 `akashic_enrich` 的乾跑| -| `akashic_person` | `person` | ✅(#218)| -| `akashic_people` | `people` | ✅(#219)| -| `akashic_get_entry` | `get-entry` | ✅(#219)。**b11c R1(#614)重新確認,裁決不變、契約有改**:回應多 `akashic.sources`(連好的副本 digest;CLI 人可讀面多一行 `sources`)——`update-entry --add-source` 讓 store 第一次出現帶它的記錄,讀取面先前對它靜默 | -| `akashic_link` | `link` | ✅(#219) **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_tag` | `tag` | ✅(#219;契約已收斂——零參數兩面皆拒,守衛下沉 service 單一路徑,#258)。**#628 起 tag/link/set-status 在 citekey 重複或與另一筆 work 共用 id 時兩面都拒絕**(同走 `requireEntry`) **#641 起「無法唯一定位」含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)**。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_set_status` | `set-status` | ✅(#219;契約已收斂——省略拒絕、清除須顯式(CLI `--clear`/MCP `clear:true`),守衛下沉 service,#258) **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_add_person` | `add-person` | ✅(#250;寫入面封閉例外形。實證需求:#238 對 unkeyable 作者的處置就是單筆指定 key)| -| `akashic_divergences` | `divergences` | ✅(#250;讀取面 `--json`+人可讀同源。#218 同形:能寫不能讀的格)| -| `akashic_venue` | `venue` | ✅(#304 venue change;讀取面 `--json`+人可讀同源;編年 list 由反向邊現算)。**#673 重新確認,裁決不變、契約有改**:多一個 `references`——通用 references(不是 verdict、不是 `paginated` 判定的那些:`issn`/`names` 的來源記錄,手改或舊資料另可能有 `authorized`/`note`),鍵名同 `akashic_update_venue.references` 的輸入形(`media_type`/`statement`/`rests_on`),所以讀取面看到的就是移除面收的形狀;有界(`AkashicService.venueReferencesCap`=25 筆,`referencesTotal`/`referencesTruncated` 揭露)——**25 是選的數字、沒有量測依據**(live 為 0),**限的是筆數、不保證輸出位元組**(CJK 與逃脫會膨脹;`akashic_venue` 沒有單一輸出的位元組預算,`works` 編年清單本來就無界,b13f R1 verify);沒有就不輸出這些鍵。在此之前 reference 寫得進去(#587)、讀取面看不到(`verdicts` 與 `paginatedJudgements` 各有自己的鍵,其餘沒有),定位一筆要移除的得先開 YAML。CLI 人可讀面逐筆印(judgement 的證據份數讀 `rests_on_total`、被截時標出,b13f R1 verify);兩面同一條 `venue()` 路徑,沒有有記錄的差異。字串逐一消毒且有長度上限(`value` 200 字元、`url`/`statement` 300;`rests_on` 只列前 5 個,`rests_on_total` 揭露),超過上限的形只用來認得,`--remove-reference` 的定位仍以逐字的值為準。**#675 重新確認,裁決不變、契約有改**:`names[]` 自此多帶 `source`/`note`(各截 300 字元;沒有就不輸出)——`update-venue --edit-name-segment` 的 `match`/`set` 收這兩個鍵,而讀取面在此之前看不到它們,同名的段只差 `source` 或 `note` 時(#565 起合併會拒絕的那種)呼叫端認不出要改哪一段(能寫不能讀,#218/#219 的形狀);鍵名同 `match`/`set`(`ended` 不是 `ended_unknown`);CLI 人可讀面在該段後印 `source 「…」`/`note 「…」`,兩面同一條 `venue()` 路徑、沒有有記錄的差異;讀取面截在 300 字元,超過的段要對照 YAML **#564 重新確認,裁決不變**:`references` 自此常態帶名字分類的判定記錄(`field: authorized`/`variant` 的「指定/確認/撤回:理由」),與 `issn`/`names` 的來源記錄共用 25 筆的上限——`referencesTruncated` 為 true 時沒列完,要看全部讀 YAML(#564 R1 verify F2 第 15 列) | -| `akashic_venues` | `venues` | ✅(#304 venue change;讀取面同源)| -| `akashic_add_venue` | `add-venue` | ✅(#304 venue change;寫入面封閉例外形,同 `add-person`)。**#554 R5 重新確認,裁決不變、契約有改**:`names` 走與 `update_venue.add_names` 同一個入口(`vetVenueNames`——canonical 入庫、空白項略過、近重複留一筆、含不合法字元或無字母無數字整個呼叫拒絕、全空白拒絕),兩面描述於 R6 同步(R5 verify 第 14 列抓到只改了 update 面的描述) **#587 重新確認,裁決不變、契約有改**:`issn` 收角色(與 `update_venue.add_issn` 同一個解析函式),報告多 `issnMediumRecorded`/`issnDropped`;兩面同契約。 **#564 修正輪重新確認(R2 verify 補記,b29 V1 第 21 列),裁決不變、契約有改**:`names` 經 `vetVenueNames` 之後單一名字至多 65,536 位元組(`AddOnlyEnrichment.maxValueBytes`,在 canonical 之前擋),超過整個呼叫拒絕——修正輪為名字分類面加的上限,同一個入口也套在 `add_names`/`add-venue --names` 上;修正輪之前的 binary 收 70,000 位元組的名字。兩面同一個入口。 | -| `akashic_update_venue` | `update-venue` | ✅(#306;寫入面封閉例外形;append 語意——整組替換刻意不提供,R3F-2 教訓)。**#471 起兩面同時新增 variant 寫入**(MCP `add_variant`/CLI `--add-variant`,append 語意同 `add_names`):在此之前 variant **兩面都沒有寫入面**,唯一的寫入者是 `migrate-venue-variants`(#567 退場刪除)——而它用的是「`authorized` 的補集」。**一個不做判定的操作成了唯一的判定寫入者**,正面撞上 `identity-is-judged-not-matched`:「這個名字是那個名字的異寫」是判定,不是「不在對外清單裡」的推論(`two-kinds-of-edits` 同向:判定型寫入要有自己的面,不能是決定論式遷移的副產品)。**不在 `names` 的一併 append 進 `names`**——兩個分割都是對 names 的標記,標一個 names 沒有的字串會造出孤兒,而孤兒 variant 自 #473 起是 error。分割互斥與孤兒由 `Venue.validate()` 擋,寫入面不重造。**#554 起兩面同時新增 authorized 的判定寫入**(MCP `authorize`/CLI `--authorize`)——**它不是 append,這是端到端測出來的**:`AuthorizedNames.validate` 對 authorized 有「每書寫系統至多一個」的內容約束,而 2026-09-11 實測 470 筆 venue 已有一個 latin authorized(`VenueBootstrap` 取第一個名字的機械值;`addVenue` 則**不寫**,走 #227 的「建檔不機械偽造」語意),append 第二個 latin 名必被擋——第一版就是照 `add_variant` 做 append,對那 470 筆全部無用。所以語意是**同書寫系統原子替換**:X 成為對外形、原本的 Y **移出 authorized、留在 names、不標 variant**(R1 verify D1,使用者 2026-09-12 裁決:呼叫端只說了「X 是對外形」,程式不替它多說「Y 是異寫」——`venue-entity` spec 的未標是「不作任何宣稱」的誠實狀態;而 Y 若是沿革前身(names 帶時間欄位),標成 variant 會撞 variant 的時間不變式、整個呼叫被拒,DA 席實測)、報告 `authorizedRemoved` 逐筆印出;X 若原本在 variant(被 #553 降過去的)從那個分割移出、報告 `liftedFromVariant`;已是 authorized 的報告 `alreadyAuthorized`(no-op 但不沉默)。**「書寫系統」是 `WritingSystem` 的三個值 han/latn/other**——不同值之間才是 append,`.other` 是一個桶(西里爾與假名互相替換,logic 席實測)。這是 #553 攣生合併「authorized → variant」降級**在該名字上**的逆操作(#565 起合併不再標 variant——被併者的 authorized 併入後未標,`authorize` 對未標的名字同樣收)——不是「精確」逆操作(#553 改一個名字的分類,`authorize A` 改兩個:A 升、同書寫系統的 S 移出),命名回到 `authorize` 而非 `add_*`,因為叫 add 會說謊。不在 `names` 的一併 append 進 `names`、分割互斥仍由 `Venue.validate()` 擋、不重造——與 variant 那段相同。**入口的輸入驗證**(都是**輸入**驗證,不是分割互斥的第二份副本——它們擋的是呼叫端的參數矛盾,不是 store 狀態;有幾道不寫數字,R3 verify 抓到「兩道」在同一個 diff 裡就過期):同一次呼叫把同一個字串既送 `add_variant` 又送 `authorize` 是兩句矛盾的話;同一次呼叫**兩個同 `WritingSystem` 的名字**也是(R1 verify 第 1 列、四席各自重現:迴圈逐一處理時第 N+1 輪會把第 N 輪剛升的移出,陣列順序決勝)——兩者都整批拒絕零寫入。兩個檢查都在空白過濾**之後**算(空白是「沒說話」,不是矛盾)。**相等走 `NameIdentity.canonical`,與 store 守衛同一條**(R2 verify 第 1 列,五路獨立命中):R2 之前入口是精確 `String ==`,而 R1 report 寫的「近重複 fail-closed」**只對撞到 variant 成立**——D1 讓被換下來的舊名留在未標,`Venue.validate()` 對 names 沒有 near-duplicate 檢查,於是 `--authorize "X "`(尾隨空白)對既有 authorized `[X]` **寫入成功**:帶空白的字串進 names、真名被移出、帶空白版成為 displayName。那句「fail-closed」是 D1 之前的量測被抄進 D1 之後的規則,沒有重量(`assertions-must-be-measured` §2 的形狀)。**R4(D6)→ R5(D8,使用者 2026-09-12):名字內容的不變式住在 store 邊界,不在任何一個寫入面**。R2→R4 三輪把閘裝在 `updateVenue` 的三個迴圈裡(精確相等 → 只 authorize 走 canonical → 三個迴圈一致),每一輪都修在看見的那一圈,R4 verify 指出同一個 `names` 欄位還有 `addVenue`(原樣存入、連空字串都收)與 `VenueBootstrap`(只 trim)兩個寫入者——`add-venue --names "X "` 種下的髒條目讓乾淨拼法永遠進不了。`Venue.swift` 的 dated-variant 守衛 doc 早就寫著答案:「守衛住在 validate → writeVenue 的交會處才擋得住所有路徑」。現在:**`Venue.validate()` 對 names/authorized/variant 逐條驗(error 級)**——canonical 形(前後/連續空白、tab、NFD 都不是)、不含控制/格式字元(與輸出閘 `UnsafeToEmitScalar` **同一份**定義再加整個 Cf/Cc/Zl/Zp 類別——R4 曾是 19 個例子的列舉、170 個 Cf 漏 149:ALM、TAG 字元;**不可見**用 Unicode 的 `Default_Ignorable_Code_Point`(R6——R5 verify 第 2 列:VS16/CGJ 是 Mn、Hangul filler 是 Lo,四類 generalCategory 都放行,`心\u{FE0F}理學報` 存成第二筆「不同」名字、`add-venue --names $'\u{3164}'` 建出 displayName 空白的 venue);ZWJ/ZWNJ 只在兩個脈絡合法——(a) 掛在同一文字字母上的 virama 之後(右鄰居若在,要是同一文字的字母/數字)、(b) 左鄰居是 join-control 文字的字母/標記/數字、右鄰居是同一文字的字母/數字(R6——R5 verify 第 1 列五路命中:「兩側是字母」對拉丁字母 fail-open、對 Malayalam chillu 與波斯數字 fail-closed;R7 再收:區塊裡的標點不算鄰居、連續 joiner 不算——R6 verify 第 1/19 列;R8 再收:virama 的基底要是**字母**、不是數字或標記(Codex,R7 verify 第 1 列)、兩側要同一文字、joiner 之後不能直接接標記或 virama(R7 verify 第 26/33 列);`NameIdentity.joinerIsLegal` 是封閉的兩個脈絡,區塊表補進 Mandaic/Syriac Supplement/Adlam 等草書文字——第 14/22 列);「至少一個字母或數字」的字母是 generalCategory 的 L 類,不是 `isAlphabetic`(R7 verify 第 15 列:一個孤立的 Arabic fatha 曾寫得進去);U+2800 BRAILLE PATTERN BLANK 顯式列入不可見(第 20 列))、至少一個字母**或數字**(純數字刊名 *1843*/*2600* 是真的,R4 曾寫「至少一個字母」)、三張清單各無 canonical-相等對(names 的例外:兩段都帶不相交時間的沿革改回舊名——R6,R5 verify 第 4 列:Sankhyā 1933–1960 → 2002–2007 合回同名那筆記錄在 R5 寫不進去;R7 起「不相交」用 `Venue.segmentsAreDisjoint`——一段有 end、另一段有 start、**兩端都是 ISO 8601 前綴**(R8,R7 verify 第 2/3/7 列三席:手改的 `2003-1`/`民國49` 曾解鎖豁免,`ISO8601Prefix.isValid` 早就在)、較粗粒度截斷後嚴格早於;端點相等與 attested-only 都不豁免——R6 verify 第 9/35/47 列指出 `DateRange.overlaps` 對混合粒度 fail-open,而這裡是放行條件;掃描先以 canonical 鍵分組,O(n) 不是 O(n²)——第 21 列);謂詞一份在 `NameIdentity.wellFormednessIssue`,訊息對操作者說改什麼(R6)。**D8 不進 `venue-entity` spec 是一個要記下來的裁決**(R6 verify 第 3 列:R6 說記了、實際沒記):#554 的裁決是「既有 tool 的新參數,不走 Spectra」,D8 把它擴成 store 不變式時沒有重開;規範文字住 `docs/store-format.md` §5.7(store 契約,與 §3.4 同級),spec 的 Requirement 是 #570。`canonical` 只丟 `White_Space` scalar、不刪任何其他 scalar(R6——R5 在 Character 上切,「空白+combining mark」整個 cluster 被刪,`add-venue --names $'Jour ́nal'` 存成 `Jour nal` 零回報)。**所有寫入者存 canonical**:`updateVenue`(`vetVenueNames`:canonical → 逐項驗 → 去重,順序不能反——R4 曾先去重再驗,`["New Journal", "New\tJournal"]` 兩種順序得到不同結果)、`addVenue`(同一個入口)、`VenueBootstrap`。相等只有一條:canonical 查找、回傳 **store 條目**——「先精確」在 Swift 裡不存在(`String ==` 是 canonical equivalence,R4 曾讓 NFD 輸入把 NFD 位元組寫進 authorized、非 Swift 讀者看到 `authorized ⊄ names`)。手改出來的違反(近重複對、非 canonical)在下一次寫入被 validate 具名擋下、零寫入——不猜、不靜默修(**含 `fmt`**:R6 起 `.venue` 分支與 `.person` 同樣跑 validate——R5 的裸 `encode(decode)` 讓 `fmt --check` 對一個 validate 報 error 的 store 印「✓ 全部已是 canonical form」;**含合併的 dry-run**:預測的 keeper 過同一道 `assertVenueWritable`;**`resolve-venues` apply/repoint/demote 改成 venue 先過閘再寫 entry**——R5 是 entry 先落盤、venue 才 throw,手改一筆尾隨空白的 venue 後 apply 會留下升格的 entry 與沒落的 verdict,Claude 代裁 D11;R7 補 `repoint` 對懸空 from-key 的具名拒絕——base 既有的 `venuesByKey[k]!` 在 D11 之前是「entry 已落盤再 crash」、之後是「零寫入再 crash」,對 MCP 面都是以合法參數殺死 server 的路徑,R6 verify 第 4/27 列(R8 把訊息的指路從 `--demote` 改成「救回檔案或手改 YAML」——`--demote` 對同一狀態也是 notFound,R7 verify 第 4 列);**R8 起 `repoint` 的 verdict literal 從 from-venue 的 confirmed verdict 逐字取回**,取不到就拒絕——#418 用 work 的 `title` 當 literal,之後 `--demote` 會把 venue 邊改寫成論文標題(DA,R7 verify 第 6 列;既有缺陷,兩輪在同一函式上動刀而沒看到);**work/person 合併對持有被併鍵 verdict 的 venue 在 commit 之前過寫入閘**(R8 當時的 `assertVenueHoldersWritable`——R9 起是涵蓋三種 holder 的 `assertHoldersWritable`,見後——preview 共用——post-commit 的 holder 遷移曾是第七個沒過閘的 venue 寫入者,dry-run 說 OK、apply 刪了檔、venue 留死 verdict,DA,R7 verify 第 5 列);**合併的被併記錄自己違反不變式時,訊息指向被併者**(`doomedRecordInvalid`,第 28 列:那個字串只在 doomed 的 YAML);拒絕訊息的參數名兩面各自正確(`add_names(--add-name)`,第 45 列)、原字串截 120 讓理由不被截掉(第 7/12 列));唯一的自我修復路是 authorized 裡同名不同位元組(NFD)的舊指定在 `--authorize` 同名時被換成 canonical——R5 至 R9 報在 `authorizedRemoved`,R10 起報在自己的桶 `authorizedRewritten`(見下方 R10 段;R10 verify 第 11 列抓到這一句仍以現在式說 `authorizedRemoved`,同一列內兩份描述)。**誠實邊界**:NFC 對 CJK 相容表意文字有損(U+FA10 塚 → U+585A;位元組層有損、Swift 層無損);~~`displaySafe`/`UnsafeToEmitScalar` 對 Cf 字元的**輸出**逃脫仍是列舉(TAG 字元在錯誤訊息裡原樣印出)——輸出閘另裁(#569)。~~ → **#569 已裁(2026-09-27)**:輸出閘改成性質,與名字閘、`escapingInvisibleScalars` 同一份定義;人可讀輸出保留 ZWJ/ZWNJ,JSON 出口逃脫它們。**#560 的程式缺口由本輪關掉**(`zero-instance-guards` 第 25 列)。**R9(R8 verify 39 列,DA 席缺席)**:joiner 的 (a) 支要求 virama 與跳過的標記都是基底的文字、詞尾 chillu 後的空白視同沒有(D22);(b) 支放行同一 Indic 文字的 virama(Bengali ya-phalaa ``,Microsoft Devanagari/Bengali 音節文法 `+H` 實取確認——R8 的「沒有正字法意義」對 Bengali 為假,D21);區塊表補 Devanagari Extended/Extended-A、Myanmar Extended-A/B;`repoint`/`demote` 寫 verdict 時退役同 holder 上同一配對的相反判定(D20——R8 逐字帶原 literal 之後,每一次合法的 repoint 都在 from-venue 留下 #486 的矛盾對,處置「刪掉另一個」沒有工具面),報告 `verdictsRetired`;from-venue 上同一 work 有 ≥2 個正規化後不同的 confirmed literal 時具名拒絕、零寫入(D23,verdict 不帶 index;live store 2026-09-12 實測 0);「找不到 confirmed verdict」的拒絕補出路(改回 `- literal:` 再 `--apply`);work/person 合併的commit 前閘擴到三種 holder(`assertHoldersWritable`,D24——R8 的 doc 說 person/organization 沒有內容不變式,假的:兩者的寫入閘都跑 `validate()`)。R8 對 `zero-instance-guards.md` 的 splice 錨到 row-18 的 heredoc、把第 17/18/20/21/22 列的量測段整段蓋掉——R9 從 a03933d 逐字復原,第 25 列的Python 鏡射自此只有一份(ISO 鏡射 `ISO8601Prefix.isValid`、`dated` 鏡射 `makesTemporalClaim`)。**R10(R9 verify 29 列,6 席齊)**:D20 的退役只在**配對由一條邊實例化**時做——verdict 不帶 venue index,同一 work 兩條邊指同一 venue 時只有一筆 confirmed,退役它讓另一條邊在任何工具面上都救不回來且 #486 的 warning 一起消失(六路命中、DA 真 binary 重現),`repoint`/`demote` 對這種 work 具名拒絕零寫入(D25;live store 2026-09-14:2,411 筆 work、同 venue 兩條 key 邊 0、兩條 literal 邊同配對 0);`verdictsRetired` 從整數改成逐筆具名(venue、欄位、value、statement——#553 合併會把被併 venue 的顯式 `--reject` 搬進 keeper,日後一次 repoint 退役它時被併檔已不在);`confirmedLiteral` 的去重改位元組相等(`matchingKey` 去重會把另一條邊的字還回去——DA 第 9 列);(b) 支收 virama 只限 Devanagari/Bengali 且 virama 之後要接同文字的字母(D26——R9 對十個 Indic 文字一起放行且不看右脈絡,`क\u{200D}\u{094D}` 與 `क्` 渲染相同);`add_venue` 回報存入的名字與 `namesDropped`(R4 起走 `vetVenueNames` 但 payload 仍回原輸入);`--authorize` 的 NFD 自我修復報在 `authorizedRewritten`(R9 讓同一字串同時落在 `alreadyAuthorized` 與 `authorizedRemoved`);`Venue.validate()` 的近重複訊息每組最多列 3 對(O(k²) 則訊息由讀取路徑對未信任 store 觸發);holder 閘移到 `fieldsLostByMerging` 之後。**R11(R10 verify 29 列,5 席——DA 席 errored)**:D25 只驗原始 entry 裡的 from 配對,`newKey` 不看——`repoint` 自己就能造出 `[key V, key V]`(logic 席真 binary 重現:alpha 上兩筆 confirmed、beta 的 confirmed 被退役、validate 全綠、之後兩條邊都動不了),而 `apply` 對兩條同刊名的 literal 邊各 apply 一次也造得出(requirements/regression 席:R10 自己的測試就是證據)。**D27**:配對的唯一性對改指**之後**的邊集合驗,同一批同一 work 的兩個 move 不得帶同一個 literal(verdict 以 (work, literal) 為鍵,逐 move 的退役會互相覆蓋、留下哪一側取決於輸入順序;literal 不同時交換是對的);**D28**:`apply`/`repoint` 在會造出同一 work 兩條 key 邊指同一 venue 時具名拒絕零寫入(生產端 fail-closed),`Entry.validate()` 對既有的這種 work 報 warning(`zero-instance-guards` 第 26 列),移除面另案 #572(作者位有 `--drop-author`/`--un-split`,venue 邊沒有——`replace-endnote-and-zotero` 第 4 條);D25 的 `.literal` 支只對 repoint 算(demote 沒有 to-venue,literal 邊在該 venue 上不可能持有 confirmed,R10 對 demote 也拒且出路等於叫人手做那次 demote);**D30**:`verdictsRetired` 截 20 筆、`verdictsRetiredTotal`/`truncated` 揭露(`akashic_enrich` 的形——它是 payload 裡唯一由 store 內容決定體積的欄位;它迴送 verdict 的 value 與 statement,#569 的迴送點 4 → 6);`Venue.validate()` 的近重複掃描求值也設上限(5,000 對 ≈ 100 筆同名段,超過即 error——R10 的上限只綁訊息,全豁免的沿革組仍 O(k²),真 binary 4,500 段 20 秒全綠),「另至多 M 對」改說「未評估」(已評估而豁免的對同樣沒被列出);`addVenue`/`update_venue.add_names` 的報告拆成 `namesRewritten`(canonical 形在 store、位元組不同)與 `namesDropped`(真的沒進——R10 用位元組相等,NFD 輸入同時落在 `names` 與 `namesDropped`);**兩面描述補 D20(會刪掉一筆人的判定記錄)、D23、D25、D27、D28、`verdictsRetired`,MCP `authorize` 補 `authorizedRewritten`、`add_venue` 補兩個桶**(R10 verify 第 6/12 列:R6 抓過只改一面,這次兩面都沒改);joiner 的 help 改寫成左右不對稱(右鄰不收標記);venue 合併的 `doomedRecordInvalid` 移到 `fieldsLostByMerging` 之後(與 R9 對 person/work 的先後原則對齊)。**R12(R11 verify 32 列,6 席齊——DA 回來了)**:兩個 HIGH 都在合併路徑,四席都沒碰到、DA 真 binary 重現——`mergedVenueKeeper`(與 person 側逐字同型)的 verdict 遷移用位元組相等去重而 `supersede`/#486 用正規化鍵,一次合法合併就造出永久的矛盾對;合併把同一 work 的兩條 key 邊塌成一條而兩筆正規化後不同的 confirmed literal 都留下,D23 之後那條邊永久不可 demote/repoint 且 validate 零診斷。**D31**:遷移以 `verdictEqualityKey` 去重,倖存者與被併者對同一配對持相反判定時合併具名拒絕(person 同);**D32**:塌邊會留兩筆不同 confirmed literal 時拒(出路:先 demote 一條)。**D33**:`apply` 的 D28 閘從整批拒絕改成**逐筆略過並具名**(`skippedDuplicateVenueEdge`)——一筆毒候選曾讓同批無關的候選零寫入、每次重列都再提(campaign 是照 listing 全量 apply),既有的重複邊(手改/舊 binary)曾把同一 work 上不相干的歸戶鎖死、訊息還把因果歸給這次 apply;store 狀態不符是「該筆略過並具名」那一類(`judge` 的先例)。D27 收窄:唯一性只看**被動到的邊**,同 literal 的兩個 move 只在 venue 集合相交時才拒(不相交的各在自己的檔裡退役、與順序無關——R11 的訊息在那一格為假、出路「分兩次呼叫」直接到達被拒的狀態)。`VenueResolver` 的否決抑制改比 `matchingKey`(與 `verdictEqualityKey`/#486/D25 一族同粒度,`PersonResolver` 早就這樣;R11 放寬 demote 的 literal 支後,apply → demote → apply 三步全工具面造出永久矛盾對)。沿革豁免要求兩段本身是有效區間(倒置日期曾解鎖放行)。`verdictsRetired` 以性質逃脫不可見 scalar(Cc/Cf/Zl/Zp/DI——#569 的局部圍堵);no-op 早退也帶 D30 三鍵;訊息索引改「index N」(0-based)且列舉有上限;`namesRewritten` 拆成 `namesFolded`(這次折過才存)與 `namesAlreadyPresent`(本來就在,不論位元組——冪等但要說);**R28 起同批位元組相同的重複也進 `namesFolded`**(R27 verify Codex 第 7 列:兩面描述承諾回報同批去重,而 `afterBytes` 是集合、對 `["Journal", "Journal"]` 零回報);§5.7 補第 5 條(求值上限)、配對唯一性搬到 §3.5 成 normative 段;guards 第 26 列改寫「合併不是入口」。**R13(R12 verify 43 列,6 席齊)**:七個 HIGH 全在 R12 剛加的合併閘上——D31 只比 keeper↔doomed(三方合併的 doomed↔doomed 相反判定兩筆都進 keeper,五席同指)、且只裝在 person/venue 自己的 references 上(DA 真 binary 全工具面:work 合併的 holder 遷移把 `confirmed :: work:keep` 與 `rejected :: work:doom` 都改寫成 keep,#486 矛盾對)→ 累積比對+閘裝在 `assertHoldersWritable`(三種 shape 共用);D32 的 `hits.count > 1` 守的是「塌邊」這個症狀(DA:不塌邊也到得了同一終局;logic:keeper 既有的違反擋住無關的合併、出路被 D25 堵死)→ 改守不變式本身(這次新增的 confirmed literal 讓 keeper 對某 work ≥2 個正規化後不同的 confirmed 即拒,keeper 既有的不擋);遷移去重丟掉的判定記錄零回報(四席)→ 逐筆進 `verdictsCollapsed`;repoint 同 literal 的相交檢查只比相鄰兩筆(Codex+logic:三個 move 的第 1 與第 3 個相交逃過)→ 全配對;MCP 組合腿的 `skippedBecauseRejected` 只比 id 字面而抑制已比正規形(DA:reject 已提交、apply 回一句指錯路的 notFound)→ 以正規化配對算;性質式逃脫從 `verdictsRetired` 下沉到 Core 的 `displaySafeInvisible`,名字不變式的訊息、`doomedRecordInvalid`、`confirmedLiteral` 的 literal 列、合併拒絕訊息共用,補零到四位、加 U+2800/非 ASCII 的 Zs/Co(security 四列);`skippedDuplicateVenueEdge` 的訊息分「既有的 key 邊」與「同一批稍早的候選」(先到先寫,順序由呼叫端決定——兩面描述寫明);`add_variant`/`authorize` 的全空白項回報在 `variantDropped`/`authorizeDropped`;`fmt` 的 `.organization` 分支補語意驗證(三個同形 shape 只修了兩個);apply 的重複邊預掃改 `Set`(O(entries×requested) → O(entries));`assertKeyEdgesAreUnique` 的 doc 不再說 apply 會拒;`assertPairingHasOneEdge` 的索引排序;§5.7 兩處「四條」→「五條」、§3.5 補 R13 三個收緊與「合併本身是一種移除面」;`supersede` 退役判定記錄卻沒有 trackedness 前置(security)另案 #573。**R14(R13 verify 33 列,6 席齊)**:三個 HIGH 中兩個是同一格——holder 遷移的閘只補了「相反判定」那一半,work 合併純工具面(`--apply` 兩個刊名變體 + `resolve-divergence`)造得出 D23 從此拒的邊、validate 零診斷(Codex 第 1 列、DA 第 3 列);第三個是 dry-run 對去重丟掉的判定沉默(regression 第 2 列)。收成一個 delta 謂詞(D34:合併後 vs 合併前,keeper 與 holder 兩路同一份、兩半都算;既有的違反不擋——R13 的 holder 側是絕對謂詞、keeper 側漏單一被併者內部)、訊息印出處與遷移前原值、preview 與實跑對 `verdictsCollapsed` 同源(D35)、`Venue.validate()` 對同一 work ≥2 個 confirmed literal 報 warning(D36,`zero-instance-guards` 第 27 列)、近重複與 bootstrap 的訊息以性質逃脫、三則入口拒絕訊息項數截 10、`skippedDuplicateVenueEdge` 列出既有重複邊的全部索引、repoint 相交訊息印兩個拼法、兩面描述補 `variantDropped`/`authorizeDropped`(R13 verify 第 26 列:第三次只做一半)、sink 守衛認得 `displaySafeInvisible(`。**R15(R14 verify 31 列,6 席齊)**:兩個 HIGH——D34 的差集在配對鍵層算而配對鍵含 holder,被併鍵上的既有違反被判成新(logic 第 2 列,四席同指;D37 改在索引層算、訊息分「帶進來的」與「既有的」);生產端的 fail-closed 只在合併有,apply/repoint 對「目的 venue 已對該 work 持有另一個 confirmed literal」照寫(Codex 第 1 列;D38 兩面同契約:`apply` 逐筆略過並回報 `skippedConflictingConfirmedLiteral`、`repoint` 整批拒絕);D36 只掃 `matchingKey` 不同的而 D23 比位元組(Codex 第 3/requirements 第 5 列;D39 兩類都掃、家族前綴、`StoreHealth` 計數、每筆 20 則上限);收攏丟列印遷移後的 value(DA 第 10 列;D40 印原值、逐段截、rename 的 CLI/App sink 同一種逃脫);`migrate-venue-variants --apply` 一律拒絕(regression 第 9 列;D41);MCP/CLI 描述補 D38 與否決抑制的正規化鍵(regression 第 21 列);guards 第 27 列的 Python 鏡射對齊 Swift 的空白集合與 grapheme 層連字號(logic 第 17/DA 第 23 列);malformed 回退鍵加不可碰撞的前綴(security 第 28 列)。**R16(R15 verify:30 findings 合併成 19 列,6 席齊;列號=報告公布的合併列號——R17 verify 第 6 列:R16/R17 兩句先前用的是合併前的 finding 編號,越界且對不上)**:D42(第 1 列)、D43(第 3 列)、D44(第 2 列)、D45(第 6 列)、D46(第 5 列;#574);第 4/7/8/9/10/11/12/13/14 列逐項修;第 15/16/17/18 列記錄不動——細節在 changelog「R15 verify」節與 §3.5,本列自 R16 起只記編號(第 18 列)。**R17(R16 verify:31 findings 合併成 24 列,6 席齊)**:D47(第 1 列)、D48(第 2 列)、D50(第 7 列)、D46 更正(第 8 列;#574);第 3/4/5/6/9/10/11/12/13/14/15/17/18/19 列逐項修;第 16/20/21/22/23 列記錄不動;第 24 列開 #575。**R18(R17 verify:31 findings 合併成 20 列,6 席齊)**:D51(第 1/2 列)、D52(第 7/8 列)、D53(第 3 列)、D54(第 5 列);第 4/6/10/11/12/15/17/18 列逐項修;第 9/14/19 列記錄不動;第 13 列開 #576。**R19(R18 verify:5 席在起跑就撞上 session limit、只有 Codex 席回 3 列——不是一次完整的 ensemble,記為不完整、R19 落地後重跑)**:D55(第 1 列 HIGH)、D56(第 2 列)、D57(第 3 列);搭車:`Package.swift` 為 `AkashicKitTests` 補宣告 `AkashicMCPKit`(Xcode 27 的預設建置系統 swiftbuild 只連宣告過的依賴;三個外部探針測試又寫死 native 佈局,pre-push 暫釘 `--build-system native`——#577)。**R20(R19 verify:34 列,5 席齊、Codex 席 HTTP 429 至 09-19)**:D58(第 1 列 HIGH:rename 對早已指向新鍵的 verdict 不論 field 一律丟、收攏列印 rests-on)、D59(第 7 列:`count`/`errors`/`total` 是訊息則數、被截時同為下限);doctor 名冊補 `deadVerdicts`/`contradictoryVerdicts`、App 補後者、反射守衛改掃源碼(第 12 列——R18 的守衛只看 stored property,對 computed 的家族從沒生效);`VerdictEdgeSet` 驗 StoreKey、分隔符 U+0000(第 19 列);`Package.swift` 的閉包不變式由 `PackageManifestTests` 守(第 14 列);hook 重指失敗中止、`ci.yml` 釘 native(第 10/30/34 列);§3.5 寫明第二半的掃描面只有 venue×work(第 4 列)。**R21(R20 verify:26 列,5 席齊、Codex 席仍 429)**:D60(第 1–5 列 HIGH:D58 的丟棄只在 holder 另有被改寫 verdict 時生效,且生效時是無乾跑、無逆操作的判定刪除——改成 rename 對目的鍵上已有 verdict 的 holder 具名拒絕、零寫入,與 merge 的 D31/D34 同向);hook 的重指移進守衛階段並驗 readlink(第 6/13 列:build 後重指讓以 repo root 為 cwd 的 hook 測試改寫真工作樹;`ln -sfn` 對真目錄建巢狀連結回 0);`ci.yml` 的 release 步驟同樣釘 native(第 7 列);`PackageManifestTests` 遞迴、讀不到即紅、母體取自 `.testTarget(`、target 名 `-`→`_` 映射、掃描數與 `Tests/` 對帳(第 12/16/17/19/22 列);App 的 rename sink 上限對齊 CLI 的 1,000(第 14 列);doctor `first` 註解不再說「count 完整」(第 11 列);App help 的 20 改讀 `Entry.perRecordWarningCap`(第 20 列);第 8 列(manifest 位元組預算)開 #578、第 15 列(`venues[].key` 不驗 StoreKey)開 #579;第 9/10 列的外部寫入更正(#554 body、R19 report 第 1 列 errata、#561 的 pointer 真的貼出)。**R22(R21 verify:38 列,5 席齊、Codex 席仍 429)**:D61(第 1/8/12/16/36 列:D60 的母體補 quarantined 檔,行級比對、讀不到即拒)、D62(DA 第 14 列:折疊只折完全相同的重複);拒絕訊息每個 digest 自己一行、至多 20 筆、出路不指 `repoint`(第 3/7/9/10/11/15/17/24/25/28 列);`renameEntry` 補 `oldKey != newKey`(第 4/20 列);`census-parity.yml` 與 `ci.yml` 的 `swift run` 釘 native、兩處 ln 驗 readlink(第 5/22/26/38 列);hook 對真目錄在 ln 之前就拒(第 26 列);`PackageManifestTests` 改問目錄在不在(`enumerator` 對不存在的目錄回非 nil——第 13/32 列)、模組名碰撞即紅(第 23/29 列);§3.5 的 D55 段改成過去式(第 6/30 列);本列「兩處」→「三處」(第 35 列)、`--yes` 列的「6 個」→ 量測(第 33 列)。**R23(R22 verify:37 列,6 席齊——Codex 席回來了)**:D63(第 1/2/3/4 列四席 HIGH:D61 的行級 needle 含空白、被 YAML 折行擊穿,live store 2 筆——改位元組比對 `:`,quarantined 命中先列、上限走 `Entry.perRecordWarningCap`)、D64(第 14 列:D62 留下的同鍵異 judgement 有掃描面 `duplicateVerdictRecords`,doctor/App 各一格);CLI 兩處收攏抬頭改 D62 語意(第 7/10/15/26 列)、兩份 report doc 與 `migratedVerdicts` 不再說「同一條不變式」(第 8/11 列)、`describeDedupedVerdict` 印 rests-on(第 6 列)、D62 折疊改 `Hashable` 字典(第 5/21/24/35 列)、400 的算術改成量測且只截一次(第 9/23/37 列)、hook 訊息(第 34 列)、§3.5 R18 段標記 rename 退出(第 20 列)、`--yes` 列補 `resolve-venues` 不在表(第 31 列);第 17/19/27/30/36 列記錄。**R28/R29(R27 verify 50 列→D80;R28 verify 50 列、11 HIGH→D81)**:兩面的錯誤出口收成同一條邊界——store 字串在擲出端逃脫一次、之後每一層只截;`ServiceError` 的 140 個建構點與 CLI 的 `ValidationError` 包裝納入同一條規則,MCP 的 per-tool 錯誤出口與 `Main.swift`、App 的六個 errorMessage、CLI 的五個 `ValidationError(error…)` 包裝改走 `displaySafeErrorMultiline`/`displaySafeError`(R28 verify 第 1/7/15/17 列:MCP 面對每一則已消毒的訊息再逃一次,`\A[a-z0-9]…\z` 被改寫成不存在的正則);自帶消毒的錯誤型別以 `SanitizedErrorDescription` protocol 宣告、守衛掃全樹(第 5/13/14/19/37 列:R28 的 `is` 鏈住在 StoreIO、`ServiceError` 在鏈外)。**這一格兩面契約不變**——變的是錯誤訊息的位元組,兩面自此對同一個錯誤印出同一個字串(R28 之前 CLI 只截、MCP 再逃,同一個 store 兩面看到不同的訊息)。**R30(R29 verify 40 列、9 HIGH→D82)**:「同一個字串」在 R29 只對沒被截斷的訊息成立——MCP 先截再加 `Error: `、CLI 先加再截,差 7 個字元(第 9 列);R30 把前綴接進 `displaySafeErrorMultiline` 再截,CLI 的 `ValidationError` 包裝改送**逃一次不截**的 `displaySafeErrorText`、只在頂層 sink 截一次,兩面自此對被截斷的訊息也逐字相同(`testTwoFacesAgreeEvenWhenTruncated`)。同輪:`StoreVersionError`/`AuthorshipCompletenessValidationError`/`CorpusDiagnostic` 補 conform、`StoreIncarnationError.unreadable` 兩端各逃一次改成擲出端逃、11 個 `"\(error)"` 入口收攏、`displaySafeAssembled` 也退讓、96 KB 上限量最終輸出、`displaySafeError` 的 `max` 改成輸出上限(MCP 的 `applyDict["error"]` 真的是 512 了);守衛的型別集合與 needle 全部由原始碼現算、payload 粒度比對。**R31(R30 verify 42 列、1 CRITICAL/10 HIGH→D83)**:R30 的「先逃完再截」讓逃脫沒有上界——400,000 個 ZWSP 的單行讓 `akashic validate` 跑 20 秒(接近二次,R29 是有界的);R31 把**輸入上限**(`ErrorDisplay.inputScalarCeiling`=4,096,每行只讀這麼多)與**輸出上限**(sink 的 `max`)分開,截出來的字串與無界版本逐字相同、工作量線性;單行家族先折 LF 再截、96 KB 以位元組計、CLI 頂層對非 ArgumentParser 的原始錯誤也先逃一次(R30 只有五個 `ValidationError` 包裝走新入口,直接傳到頂層的 Yams/Foundation 錯誤 ZWSP 原樣落 stderr);守衛的豁免只看註記文字(R30 一句不相干的 `display-safe-exempt: Int` 讓整個語句免檢)、裸回傳 `return why` 判成「擲出端必逃」(R30 把 `ServiceError.invalid` 的 149 個站點判成沒用到、一個都沒檢查)、conform 的判準只看 Error → 文字那條路徑。**這一格兩面契約不變**,`sourcesAuditError` 進 MCP payload 前再截 512(R30 以 2,400 進去,是 doctor payload 唯一沒再截的欄位)。**R32(R31 verify 47 列、1 HIGH→D84)**:只截支(`displaySafeClipOnly`/`displaySafeAssembled`)的預算改以**輸出** scalar 計——R31 只把 LF 折成八個字元,TAB/CR/LS 在自帶消毒的描述裡照樣八倍膨脹;守衛的豁免只對非字面裸引數整段放行、字面逐插值(R31 一句 `label 是字面常量` 讓同一字面裡 store 名字的消毒免檢)、`"字面" + expr` 的運算元也掃、helper 查表連 owner、UUID 由描述端的 `.uuidString` 成員鏈釘型別;`updateVenue` 的同書寫系統衝突清單改性質式(rename 閘與 atomicWrite 遮罩不是本工具的能力、不在本列——R32 verify 第 29 列,記在 changelog)。**R33(R32 verify 39 列、0 HIGH——PASS,in-scope fix)**:`authorize` 對被 `field: authorized` reference 指著的舊指定具名拒絕、零寫入(Codex 第 2 列:R32 之前由寫入時的孤兒附著驗證拒、不具名也不給出路;程式不替人改判定,D60 同向——value 改成新的對外形或刪掉它都是人的事);`updateVenue`/`addVenue` 回傳的名字鍵性質式(第 9 列:同一函式的錯誤路徑 R32 已改、成功 payload 仍列舉式)。**兩面契約不變**。**`migrate-venue-variants` 在 #554 之後不得再跑**:它的補集規則會把 D1 留在未標的舊指定重新標成 variant(R2 verify 第 3 列,DA 席實測);#567 已退場刪除。~~**不留 judgement**:與 person `authorize-names`(#81)、`add_variant`(#471)同為不留,三個名字分類面一次裁(#564)——這一格因此是有記錄的裁決,不是遺漏;後果是 #553 合併端仍分不出「人確認過的 names[0]」與機械值,那格維持提醒不擋、觸發條件改綁 #564。~~ → **#564 落地(2026-10-01)重新確認,裁決不變、契約有改(BREAKING)**:`add_variant`/`authorize`/`unauthorize` 三條腿自此**必附 `judgement`**(CLI `--judgement`)、證據 `rests_on`(`--rests-on`)可省略;兩面同走 `updateVenueArguments`,缺理由整批拒絕、零寫入,排在名字 vetting 與兩句矛盾的檢查之後、讀 store 之前(#654 的形狀,CLI 的 `validate()` 呼叫同一個函式)。每次指定、確認(對已在分割內的名字再說一次)、撤回、標異寫各寫一筆 `field: authorized`/`variant` 的判定記錄,回報 `judgementsRecorded`;需要 store format ≥ 22。與 `paginated`/`clear_paginated` **不同一次呼叫**(兩個判定各要自己的理由)。**沒有有記錄的差異**:兩面同一條路徑,CLI 只回 service payload。撤回面(把名字從 authorized 移出而不放新的進去)是 #559 的 `unauthorize`;variant 沒有獨立的撤回面(只有被 `authorize` 抬出時連帶撤回)。**#394 起兩面同時新增 ISSN 寫入**(MCP `add_issn`/CLI `--add-issn`,append 語意同 `add_names`——ISSN 本來就是清單,print 與 electronic 是兩個真的號)。**這是本檔「識別碼寫入面的裁決」那一節記為「最弱的一列」的第一格補齊**:在此之前識別碼只有遷移路徑(從 `fields` 殘留搬值),查到一個**新的** ISSN 時唯一的路是手改 YAML。相等看正規形(與 `IdentifierMigration.normalizedUnique` 同一條規則——兩面若用不同的相等,對「這本刊有幾個 ISSN」會給出不同答案);任一個不合法即整批拒絕、零寫入,與同函式既有的 `type` 值域檢查同型)。**#406 起兩面同時新增 `paginated` 判定寫入**(MCP `paginated`+`judgement`+`rests_on`/CLI `--paginated --judgement --rests-on`,同走 `updateVenue`):「本刊是否使用頁碼」是**判定**不是設定——必附理由與證據 digest(缺任一整個呼叫拒絕、零寫入;rests-on 的非空與形狀由 `ProvenanceReference` 平面 init 驗,單一驗證入口);(field,value,kind) 冪等、翻轉判定留史;`nil` 是誠實的未判定狀態,floor 檢查(`apa7Report` 的 `PAGES` recommended)只對 **false** 抑制、nil 與 true 照報。**#588 起兩面同時新增 ISSN 移除**(MCP `remove_issn`/CLI `--remove-issn`,同走 `updateVenue`;`=理由`):`add_issn` 補齊之後,掛錯刊的號(合法但屬於姊妹刊)仍只能手改 YAML,而 `akashic-verify-venue`(#556)是會寫進 ISSN 的通道。兩面同契約:理由必填、只進報告(`issnRemoved`,全文不截斷;使用者 2026-09-27 裁決)——git 保存的是移除前的檔,理由要進 git 得由操作者寫進 commit message;移除前要求 venue 檔已 commit、乾淨,否則整批拒絕零寫入;指向被移除號的 `field: issn` provenance 一併移除、逐號回報筆數(R1 verify:留著會讓寫入閘以孤兒 provenance 拒絕,而 venue 的 reference 沒有移除面,#587);不合法的號、同號重複、venue 沒有這個號、同一個號同時在 `add_issn` 與 `remove_issn`,都整批拒絕零寫入。同輪 `validate` 對同一個 ISSN 掛在 2+ venue 報 warning(`zero-instance-guards` 第 36 列) **#670 重新確認,裁決不變**:key 有不只一筆記錄的 venue 整批拒絕、零寫入(寫進哪一筆是猜),兩面同一條 service 路徑。 **#567 重新確認,裁決不變、契約有改**:`migrate-venue-variants` 退場後 `variant` 補上 format 14 的寫入閘,format < 14 的 store 上 `add_variant`/`--add-variant` 整個呼叫拒絕、零寫入,訊息指向 bump 到 14;兩面同一道寫入閘(`assertVenueWritable`)。 **#587 重新確認,裁決不變、契約有改**:(a) 兩面同時新增通用 `references`(CLI `--references ''`/MCP `references` 物件陣列,同走 `updateVenue`)——append-only、以 `byteExactKey` 略過位元組相同的(`referencesAlreadyPresent`),物件鍵名與 `akashic_update_person` 的 references 相同。形狀驗證走 `ProvenanceReference` 的平面 init(YAML decode 的同一個入口),寫入面只多四件事:鍵名嚴格、`kind` 要與給的欄位一致、歸屬(verdict 三欄只經 `resolve-venues`、`paginated` 只經 `paginated`/`clear_paginated`——那兩條路同時改值與判定史;**通用面自己只收 `issn` 與 `names`**——R1 verify 收窄:`authorized`、`note` 拒收,因為 venue 的 reference 沒有移除面,`authorized` 的 reference 會讓 `authorize` 換不了對外形、`note` 沒有寫入面)、上限(一次 200 筆、statement 4,096 位元組、rests_on 20 個、其餘字串 65,536 位元組);附著在合進記錄後以 `validateReferenceAttachment` 驗,所以同一次呼叫加的號與名字可以被指向;`field: issn` 的 value 以正規形入庫、`names` 的 value 以記錄上的拼法入庫(相等看 canonical);指向同一次 `remove_issn` 的號是矛盾、整批拒絕。~~**與 `update_person` 有記錄的差異**(person 側不動;兩面契約對齊另案):retrieval 的 `status` 必填(person 側預設 200)、不認得的鍵拒收(person 側靜默忽略)、有上限(person 側沒有)。~~ → **#674 對齊了:兩面是同一個解析函式**(`parseReferenceObjects`,`ReferenceWriteParsing.swift`),往 venue 這邊收——person 側自此也 `status` 必填不預設 200(#542 R2 對 `enrich` 裁掉過同一個預設)、不認得的鍵拒收、有上限,見 `update_person` 那一列;兩面各自的政策只剩「收哪些 field」(person 收 verdict 以外的、venue 只收 issn 與 names)與空陣列(venue 拒絕、person 仍是 no-op——有記錄的差異;#695 起兩面都拒絕)。(b) `add_issn` 收角色:一項是 `NNNN-NNNN` 或 `NNNN-NNNN (print|electronic|linking)`(`IdentifierTokenizer.singleQualified`——遷移的同一套切法、比遷移嚴:一項一個號、至多一個緊跟的註記,角色不在三值內即拒);入庫寫封閉值域的寫法;已在而沒有角色的號補上角色,已記的角色不同整批拒絕(改寫既有角色沒有面)。報告多 `issnAlreadyPresent`/`issnMediumRecorded`/`issnDropped`(整項空白,#556 R2 verify:先前靜默略過)。兩面同一個解析函式,CLI 的 `validate()` 在開 store 之前呼叫它。**#587 R1 verify 重新確認,裁決不變、契約有改**:(c) 通用 `references` 面收窄成 `issn` 與 `names`(見 (a));(d) 與 venue 合併(`resolve-divergence`,CLI-only,該列同步)的兩個耦合關掉:被併者 `field: issn`/`names` 的 reference 隨合併逐位元組搬到倖存者(不搬的話每個查證過的被併者都合併不了),同一個號的角色兩邊不同(或被併者有而倖存者沒有)具名拒絕;出路都指向這裡的 `--add-issn`/`--remove-issn`。**#673 起兩面同時新增 `remove_reference`/`--remove-reference`(references 的移除面)**:在此之前通用 `references` 寫得進去(#587)、刪不掉——寫錯或已不成立的只能手改 YAML(`replace-endnote-and-zotero` 第 4 條)。它是判定(`two-kinds-of-edits` 的 AI 欄),套使用者 2026-09-27 對移除面一族的裁決:理由必填(≤ 4,096 位元組)、只進報告(`referencesRemoved`,全文)、不寫進 store、不改 store format;移除前要求那筆 venue 檔已在 git 裡 commit、乾淨(`assertRecordsRecoverable`,與 `remove_issn` 同一支閘)。**形狀**:JSON 物件陣列(CLI 是一個 JSON 字串,同 `--references`)`{field, value?, reason, …縮小定位的鍵}`——不用 `=理由` 的字串形,因為 value 是任意名字(可含 `=`、`:`),字串切分沒有安全的分隔符。**定位**=`field` + `value` 的位元組相等(與寫入面去重的 `byteExactKey` 同一把:canonical 相等而位元組不同的是另一筆),可再以與 `references` 同名的鍵(`kind`/`url`/`retrieved`/`status`/`media_type`/`content`/`statement`/`rests_on`)縮小;定位不到、定位到多筆(列出各筆的區別讓呼叫端加鍵縮小;**位元組完全相同的重複不判定移哪一筆**)、兩個定位指到同一筆,都整批拒絕、零寫入。**收四格**:`names`/`authorized`/`issn`/`note`——通用**寫入**面只收前兩格(#587 R1),但手改或舊資料可能有 `authorized`/`note` 的 reference,其中 `authorized` 的會讓 `authorize` 換不了對外形(訊息叫人「刪掉它們」,在此之前沒有面刪得掉)。**不在本面**:verdict 三欄(`resolve-venues --demote`/`--reject`)與 `paginated`(`--clear-paginated`:撤回本身是帶理由與證據的判定、翻轉要留史,移除面會讓判定與記錄的值分岔)都具名拒絕並指路。**單獨呼叫**(Claude 代裁):不與 `update_venue` 的任何其他參數組合——其餘腿改記錄的值與名字分割,`remove_issn` 還會連帶刪 reference,與「移除的是哪一筆」交錯;日後放寬是可逆的、收窄不是。只移除 reference,它指的號或名字仍在(移除號是 `remove_issn`)。**沒有乾跑**:`update-venue` 整個命令沒有(同 `--remove-issn`),git 閘與整批拒絕零寫入是退路;~~CLI 也**不過目標 store 確認閘**(`WriteGateRulings` 的 `update-venue` 是命令層一格、`.notGated`,理由明寫這兩條移除腿;同族的 `resolve-venues --drop-venue` 是過閘的、`update-entry` 的移除腿預設乾跑)——`update-venue` 整個命令要不要有乾跑(連帶要不要閘)**待使用者裁決**,不在本列動(b13f R1 verify 第 10/39 列)。~~ → **#564 使用者 2026-10-05 裁決第 4 點:CLI 過目標 store 確認閘**(會刪判定記錄的移除腿一律過閘;`update-venue` 在 `WriteGateRulings` 改成逐腿);乾跑仍沒有。**縮小鍵只能指名有值的欄位**:沒給的鍵是「不參與比對」,不是「要求缺席」,所以寫不出「沒有 `media_type`」——同一 `(field, value)` 有兩筆 retrieval、只差其中一筆帶 `media_type` 時,先移除帶的那一筆、再對剩下的呼叫第二次(拒絕訊息在這個形狀下這樣說)。**有記錄的兩面差異(b13f R1 verify,比照 `akashic_update_entry.add_sources`)**:MCP 面的 `referencesRemoved` 每一筆都列(`field`/`value`/`reason`——理由只在報告裡有一份,不截),只有前 20 筆多帶 reference 的其餘內容(url/statement/rests_on…),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列。寫檔之後 index 重建失敗時呼叫仍回成功、報告多 `indexRebuilt: false`(理由見 `akashic_update_entry` 列)。通用寫入面對 `authorized`/`note` 的收窄**沒有動**——要不要收回通用寫入面是使用者的裁決(#673 明寫落地後重新裁決),本次只保證有了出路。**#675 起兩面同時新增 `edit_name_segment`**(CLI `--edit-name-segment ''`/MCP 物件陣列,同走 `editVenueNameSegments`):名字段(`TemporalValue`)的時間欄位、`source`、`note` 在此之前沒有任何工具面能改,而 #565 起合併對「同名、這幾格不同、又不能並存」的兩段具名拒絕、出路只有手改 YAML(拒絕訊息自此指向這個面,並保留「也可以手改 YAML」)。每項 `{name, match?, set 或 remove, reason}`:`name` canonical 相等定位;`match` 縮小同名多段(給了的鍵都要相符、字串與陣列比位元組,**`null` 是「要求缺席」**——與 `--remove-reference` 的縮小鍵刻意不同,那邊寫不出「沒有」而這邊的拒絕形狀正需要它,代裁);`set` 逐鍵覆寫(`start`/`end`/`ended`/`attested`/`source`/`note`,`null` 清除);`remove: true` 刪掉那一段。**兩面同契約**:判定型、理由必填且只進報告(`nameSegments[].reason`,全文不截)、不寫進 store、不改 store format;改寫前要求該 venue 檔已 commit、乾淨(`assertRecordsRecoverable`,issue `#675`),每一項都沒有變動時不寫檔、不過閘;改完的時間欄位要成立(`Venue.nameSegmentRangeIssue`:不矛盾、ISO 8601 前綴、`start` 不晚於 `end`);這次**造出的** `Venue.validate()` error 在寫之前具名拒絕並歸因到這次呼叫;移除一個名字的最後一段時,名字還在 `authorized`/`variant`/`field: names` 的 reference、或移除後 venue 沒有任何名字,具名拒絕並指路(`variant` 沒有移除面,只能手改 YAML);定位在呼叫前的記錄上,定位不到、多段(列出區別讓呼叫端加 `match`)、兩項指到同一段、逐位元組相同的重複段、輸入錯、venue key 無法唯一定位都整批拒絕零寫入;一次至多 200 筆。**組合規則(代裁,同 `remove_reference`):單獨呼叫**,不與 `update-venue` 的任何其他參數組合、也不與 `remove_reference` 組合——`add_names`/`add_variant`/`authorize` 會增減同一份 `names`,「定位的是哪一段」與報告、git 閘的語意會交錯;日後要放寬是可逆的,要收窄不是。**有記錄的兩面差異**(同 `remove_reference`):MCP 面的 `nameSegments` 每一項都列(`name`/`action`/`reason`),只有前 20 項多帶 `before`/`after`(第三方字串),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列;寫檔之後 index 重建失敗時呼叫仍回成功、報告多 `indexRebuilt: false`。**沒有乾跑**:同 `--remove-reference`,`update-venue` 整個命令沒有——git 閘與整批拒絕零寫入是退路,~~CLI 也不過目標 store 確認閘(`WriteGateRulings` 的 `update-venue` 命令層一格,理由已寫明新腿;要不要有乾跑仍**待使用者裁決**,不在本列動)~~ → 帶 `remove` 的呼叫自 2026-10-05 起 CLI 過目標 store 確認閘(#564 使用者裁決第 4 點;只有 `set` 的不閘),乾跑仍沒有。MCP manifest 描述只寫做什麼、`reason` 只進報告、git 閘與指向 CLI help(#578 的位元組預算)。 **#675 R1 重新確認,裁決不變、契約有改**:`set` 的 `source`/`note` 拒收控制、格式、方向與不可見字元(`UnsafeToEmitScalar` 人可讀輸出要逃脫的集合扣掉私用區,`match` 照收);`attested` 每個點至多 65,536 位元組;寫入前重讀閘回傳的記錄檔、被改過即整批拒絕;顯示名改變時報告多 `displayNameChanged`。兩面同一條 service 路徑;MCP 描述補 `match`/`set` 的鍵名與 `null` 的兩種意思。 **#695 重新確認,裁決不變、契約有改**:`references` 的空陣列拒絕搬進共用的 `parseReferenceObjects`,訊息改成兩面同一句(「references 是空陣列——沒有要附的 reference 就不要給這個鍵」;先前 venue 說「這個參數」);擷取型的形狀檢查搬到 Core 的 `RetrievalWriteShape`,被拒的輸入不變。兩面同一條 service 路徑。R1 verify 起「被拒的輸入不變」不再成立:同一個函式多拒四種,見 `akashic_update_person` 列的 #695 註記。 **#559 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決):兩面同時新增撤回(MCP `unauthorize`/CLI `--unauthorize`,同走 `updateVenue`):逐名、必須是現有的 authorized(相等看 `NameIdentity.canonical`,同 `authorize`),移出 authorized、留在 names、不標 variant——記錄回到「不作任何宣稱」,比照 `clear_paginated`(#500)與 `demote`(#418)。替換與撤回的邏輯自此住在 `AuthorizedDesignation` 一份(#557 的 organization 用它的替換;organization 沒有撤回面),`updateVenue` 呼叫它。兩面同契約:不是現有成員、同一個名字又在 `authorize`、被 `field: authorized` 的 reference 指著(指路 `--remove-reference`),都整批拒絕、零寫入;整項空白回報在 `unauthorizeDropped`;撤回先於 `authorize`(成員資格看呼叫前的狀態);與 `add_variant` 同一個名字是明說降成異寫、照做;`remove_reference`/`edit_name_segment` 的單獨呼叫也不與它組合。報告 `authorizedWithdrawn`(每列 `name`、呼叫前在 authorized 的 `index`);撤回改變預設顯示名時多 `displayNameChanged: {before, after}`(只在有撤回腿的呼叫裡算)。**撤回不是 `authorize` 的精確逆操作**(R1 verify):名字與分類(未標)回得來,但 `authorize` 對不在 authorized 裡的名字接在尾端(只有同書寫系統替換才插回原位),而 `displayName` 取 `authorized.first`——多書寫系統 venue 撤回再指定,預設顯示名可能換書寫系統;遺失的只有 authorized 內的順序,報告逐名帶原 index。**不設 git 閘**(使用者沒有裁決要閘;名字與分類都留在 names,與 `clear_paginated` 一致,遺失的順序由報告的 index 補)。~~判定記錄待 #564(2026-10-01 裁決要留、另案落地)。~~ → #564 已落地(見上):`unauthorize` 每個名字寫一筆 `撤回:理由`,先前的記錄留著;撤回記錄的名字就是 `authorizedWithdrawn` 各列的 `name`(同一份撤回,報告與記錄說的是同一批名字,store 拼法)。沒有有記錄的差異。 **#564 修正輪重新確認(使用者 2026-10-02 裁決),裁決不變、契約有改**:(a) 名字分類記錄只對**同一個名字同一個分割的最後一筆**去重——撤回之後以同一句理由再指定會寫第三筆(先前被當重複丟掉,記錄以撤回結尾而名字是 authorized);(b) `add_variant`/`authorize`/`unauthorize` 合計一次至多 200 個名字(第 4 點),單一名字至多 65,536 位元組;理由開頭不得是組合符號或不可見字元、要有字母或數字;(c) 報告多 `variantConfirmed`(已是異寫、寫了「確認」的名字);(d) `edit_name_segment` 刪掉最後一筆記錄是撤回的名字的最後一段時,記錄一起刪(第 2 點,報告 `judgementRecordsRemoved`);(e) 寫檔之後 index 重建失敗不讓呼叫失敗(報告多 `indexRebuilt: false`,同 organization——撤回的原位置只在報告裡)。兩面同一條 `updateVenue`;MCP 描述把 #564 整合時為舊預算刪掉的拒絕類別寫回(tools/list 預算 60,000,#578/#713)。**有記錄但未改的差異**:`update-venue` 對沒有任何變動的呼叫照樣重寫檔案與重建 index,organization 不寫(#557 R1)——兩個姊妹面對 no-op 的處置相反,留給 `update-venue` 整個命令要不要乾跑那一格一起裁。 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改(只有訊息與報告)**:`edit_name_segment` 對還在 variant 的名字的拒絕不再說「variant 目前沒有移除面,只能手改 YAML」——說出工具面的出口(`authorize` 抬出 variant、`unauthorize`、commit、再 remove)與代價(同書寫系統原本的對外形被換下要再指定回來;一次每書寫系統一個名字),CLI help 同步;`add_names` 的 65,536 位元組上限見 `akashic_add_venue` 那一列;名字分類記錄隨名字刪改成線性。兩面同一條 `updateVenue`。 **#564 b34 重新確認(使用者 2026-10-05 裁決第 4 點),裁決不變、契約有改**:CLI 的三條移除腿——`--remove-issn`、`--remove-reference`、`--edit-name-segment` 帶 `remove` 的呼叫——過 #298 的目標 store 確認閘(未指名 `--library`/`--yes` 時拒絕,拒絕訊息說這條腿沒有乾跑);`WriteGateRulings` 的 `update-venue` 從命令層的 `.notGated` 改成逐腿(其餘腿不刪東西、不閘)。**有記錄的差異**:MCP `remove_issn`/`remove_reference`/`edit_name_segment` 不閘(橫切表 `--yes` 列:active store 是 session 狀態)。兩面同一條 `updateVenue` **#564 b37 重新確認(b36 Y1 第 0/1 列),裁決不變、契約有改(只有報告)**:`edit_name_segment` 報告的 before/after 每段的 `attested` 至多列 20 個,超過時最後一項是「…另 N 個觀測點未列出(共 M 個)」(不新增回應鍵,同 `cappedCitekeys`);讀取面 `akashic_venue` 的 `names[].attested` 照舊全列(`match` 要逐項相同);`edit_name_segment` 的 remove 被拒、出口要經 `authorize`/`unauthorize` 寫記錄的三種(還在 authorized、還在 variant、最後一筆不是撤回)在 store format < 22 時另起一行說出寫入閘。兩面同一條 `updateVenue` | -| `akashic_resolve_venues` | `resolve-venues` | ✅(#304 venue change;兩面契約差異同 #272:MCP 允許 apply+reject 組合、CLI 分兩次呼叫)。**#418 起兩面同時新增 `repoint`**(三段式 id `citekey:venueIndex:newKey`,把**已歸戶**的邊改指到另一個 venue)——它是歸錯戶的退路:`apply` 只做 literal → key 的升格,此前一條已是 key 的邊改不回來,而 person 域有 `resolve-divergence`、venue 域沒有,且 `literal-first-then-key` 的整套論證建立在「誤可逆」上。**這一項兩面契約相同**(與上面那個差異不同):`repoint` **不與** `apply`/`reject` 組合,兩面都拒——它們是不同階段(升格 vs 修正已升格的),混在一次呼叫裡會讓「哪一批寫了」難以判讀,而改指本來就是在修一個錯誤,最需要清楚的失敗語意。失敗語意分兩類(同 #386 的 `judge`):語法錯或前提不符 → **整批拒絕零寫入**;成功則兩側都留 verdict(新的 confirmed、舊的 rejected——少了 rejected,下次提名會把同一配對再提出來)。**同輪另加 `demote`**(`citekey:venueIndex`)把**誤升**的邊退回 literal——`repoint` 只改得到既有 venue,而正確答案可能是「現有的都不對」,那時要能退回 `literal-first-then-key` 所說的**誠實狀態**。原字串**從該 venue 的 confirmed verdict 逐字取回**(第 13 條邊的 `: :: ` 文法),走 `ResolutionLedger.verdicts` 這個唯一解析器;**取不到就拒絕,不拿 venue 顯示名頂替**(顯示名不是那筆記錄原本寫的字——WoS 的 `PSYCHOMETRIKA` vs 正式刊名 `Psychometrika`,頂替會安靜改寫書目資料)。`repoint` 與 `demote` **各自單獨呼叫**,兩面同契約。**#573 起(使用者 2026-09-27 裁決)**:repoint/demote 會刪判定記錄(D20 的退役)時,要求那些 venue 檔已在 git 裡 commit——store 不在 git 工作樹、檔案未 tracked 或有未提交修改都整批拒絕零寫入,兩面同契約;**有記錄的差異**:`verdictsRetired` MCP 面截 20 筆、CLI 面全列(`retiredLimit`,理由同 `validate` 的面級截斷分工)。**change `resolution-verdict-states`(#619)起兩面同時新增 undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedVenues`;id `citekey:venueIndex:venueKey=說明`;單獨呼叫),列表的候選列與歧義條目帶 `undecidedChecks`。**venue 兩面都沒有篩選式批次 apply**(CLI `--apply` 收 id 清單),所以沒有 resolve-people 那種排除——只有揭露 **#628 重新確認,裁決不變、契約有改**:候選所在的 work 無法唯一定位(citekey 重複或與另一筆 work 共用 id)時,apply 逐筆略過並以 `skippedUnlocatable` 具名(D33 的既有契約:store 狀態不符逐筆略過),reject/repoint/demote 整批拒絕;兩面同一條 service 路徑。MCP 列表以 `unlocatableCitekey` 標出(CLI 只收顯式 id,沒有篩選式批次)。**#572 起兩面同時新增移除腿**(CLI `--drop-venue`/MCP `drop_venue`,同走 `dropVenueEdges`;`citekey:venueIndex=理由`):在此之前 venue 邊沒有移除面,同一 work 兩條邊指同一 venue 時 D25/D28 的拒絕與 `Entry.validate()` 的 warning 都只能指「手改 YAML」。**以 index 定位不以值**——重複的兩條邊值相同,值定位分不出要刪哪一條,而那正是這個面要處理的形狀;同一 work 多筆以原始 index、由實作處理位移(同 `split_author`)。**key 邊只在刪完後同一 venue 仍有另一條 key 邊時可刪**——那時 confirmed verdict 仍由剩下的邊實例化;刪唯一的 key 邊要先 `demote`(它處理 verdict),本面不替人決定那筆 verdict 的去留。literal 邊一律可刪。兩面同契約(使用者 2026-09-27 對移除面一族的裁決,同 `update_venue.remove_issn`):理由必填、只進報告(`venueEdgesRemoved`,全文不截斷)、不寫進 store、不改 store format;移除前要求那些 work 檔已 commit、乾淨,否則整批拒絕零寫入;輸入錯整批拒絕;單獨呼叫;CLI 另過 #298/#580 的目標確認閘。刪光一筆 work 的 venue 邊時以 `emptied` 具名——`migrate-venues` 與 Zotero pull 會從來源欄位重新推導。**#648 重新確認,裁決不變、契約有改**:apply/repoint/demote 的寫入前逐筆驗證自此是 writeX 在寫入當下跑的每一道(含 encode 與讀取上限)——venue 是 O(catalog) 的 verdict 持有者(#499),寫出後超過 8 MiB 即整批拒絕、零寫入、錯誤具名記錄與兩個位元組數;兩面同一條 service 路徑。reject 維持逐筆收容 **#641 重新確認,裁決不變、契約有改**:「無法唯一定位」自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),repoint/demote/drop_venue 整批拒絕、apply 逐筆略過——先前 repoint/demote 在寫入當下被拒而撕裂(#631 R2 verify);各腿不必改,同一個 `unlocatableCitekeys`。 **#670 重新確認,裁決不變、契約有改**:候選指向的 venue 的 key 有不只一筆記錄時,apply 逐筆略過並以 `skippedUnlocatable` 具名(D33 的既有契約),reject/repoint(兩端任一)/demote 整批拒絕,undecided 該筆略過並具名;列表的候選列帶 `unlocatableVenueKey`。兩面同一條 service 路徑,CLI 原樣印出 payload。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#712 重新確認,裁決不變、契約有改**:列表(不帶寫入腿)多三個鍵 `suppressed`/`suppressedTotal`/`truncated`——被同 work 同 venue **另一個拼法**的 `reject`/`demote` 以正規化配對壓掉的候選(#554 R12 起的抑制不變,只是不再沉默;「沉底而非隱藏」)。**這一段不是「同 resolve-people 的 `rejected` 段」**(#712 R1 verify 第 4、39 列):people 的 `rejected` 段只列逐字等於被否決拼法的作者位,被同一筆否決以正規化鍵壓掉的兄弟拼法不在其中——resolve-people 與 resolve-organizations 的列表對它們仍然沉默、還會說「任何提名層皆無命中」,追蹤在 #721。判準只有一條(`VenueSuppressedCandidate`):壓住它的 rejected verdict 的 literal 沒有一個與它自己的 literal **位元組相等**——位元組相等是普通的已否決、列表一向不列;只差 NFC/NFD 的是另一個拼法、照列(**使用者 2026-10-05 裁決**:「同一個拼法」比位元組,與 `confirmedLiteral`、`byteExactKey` 一致;先前比 Swift `String ==`,那條邊在 `candidates` 與這一段兩邊都不出現。被否決的拼法也依位元組去重——`ResolutionLedger.rejectedPairings(venues:)` 自此回陣列、不經 `Set`;apply 改寫邊之前也比位元組。兩面同一條 resolver 路徑);歧義(對到 2+ venue)一向不被抑制、不進這一段。每列 `citekey`/`venueIndex`/`literal`/`venueKey`/`rejectedLiterals`(壓住它的拼法,至多 5 個、超過時多 `rejectedLiteralsTotal`;R1 起在 resolver 就截、另記總數,建表是線性的——第一版存全部、到輸出才截,N 列共用 K 個拼法時是 N×K),**刻意沒有 `id`**(不是可 apply 的候選)。永遠回這三個鍵:沒有候選被壓時是空陣列與 0。**有記錄的差異**(同 `verdictsRetired` 的分工):CLI 面全列(傳 `suppressedLimit: nil`)、MCP 面截 20 筆(`suppressedItemsCap`)並另受 `candidateByteBudget` 約束——吃不下的整列不印,`suppressedTotal` 永遠是全數、`truncated` 說被截;`truncated` 與 `verdictsRetired` 那一腿同名,兩者不會出現在同一次回應。venue 的 CLI 列表本來就是服務 JSON 的原樣輸出(沒有另一套人可讀排版),所以兩面的「計數行」是同一個鍵。 | -| `akashic_add_organization` | 無(單筆建檔 MCP-only;批次面 `bootstrap-organizations` 維持 CLI-only,見 CLI-only 表)| ✅ 有理由的單面(#304 移轉裁決:org 重啟後單筆建檔是 #303 campaign 的 LLM 消費流程;操作者規模的批次建檔另有 CLI 面)| -| `akashic_update_organization` | `update-organization` | ✅(#557;使用者 2026-10-01 裁決形狀 2:新增 organization 的部分更新面、兩面各一格,**只提供 `authorize`**)。在此之前 `Organization.authorized` **兩面都零寫入面**(`add_organization` 不收、`OrgBootstrap` 不寫、`authorize-names` 只管 person),`doctor` 的 `no authorized name: … organization` 恆為真且修不掉。寫入面封閉例外形(同 `update-venue`:只回 service payload、不設 `--json`),兩面同走 `AkashicService.updateOrganization`,替換的邏輯是 `AuthorizedDesignation`——與 `update-venue` 的 `authorize` 同一份。兩面同契約:同書寫系統原子替換,被換下的移出 authorized、留在 names;不在 names 的以 canonical 形加進 names;同一次兩個同書寫系統的名字、沒給、空陣列、只有空白項(三者同一件事:過了 vetting 之後沒有要指定的名字)、key 格式不合,讀 store 之前整批拒絕(CLI 用法錯誤 64);key 有不只一筆記錄(#669/#670)、找不到、被換下的名字被 `field: authorized` 的 reference 指著(organization 的 reference 沒有移除面,指路手改 YAML),整批拒絕零寫入;子集與每書寫系統至多一個由 `Organization.validate()` 在寫入閘擋。給的名字都已是對外名稱而同一句理由已記過時成功、報 `alreadyAuthorized`、**不寫檔也不重建 index**(一次沒有變動的寫入會重新序列化人手編過的排版;#564 起對已是對外名稱的名字說「確認」也留一筆記錄,所以第一次說確認是會寫的)。報告 `namesAdded`/`authorizedAdded`/`authorizedRemoved`/`alreadyAuthorized`/`authorizedRewritten`/`authorizeDropped`/`authorizedTotal`(organization 沒有 variant,不報 `liftedFromVariant`);**有事才出現**:`authorizedNotCurrent`(指定的名字在 names 的每一段都已結束——`Organization.authorized` 的「從當前有效的名稱中指定」是慣例、`validate` 不擋,本面是第一個寫得進它的面;不拒絕,`displayName` 會變成那個退役名,所以說出來)、`indexRebuilt: false`/`indexRebuildError`/`indexNote`(寫檔成功而 index 重建失敗:呼叫回成功,做法同移除面一族,`RemovalReportSupport.swift`)。**沒有 `unauthorize`(R1 verify 之後拿掉)**:裁決只說先提供 `authorize`,首輪實作順手加了撤回是實作者的判斷;而它在 organization 上不是 venue 那個「回到誠實的未判定狀態」——organization 的 names 只增不減(當時;#564 修正輪起 `remove_names` 只刪已撤回的名字,仍沒有一般的名字移除面),`authorize` 為了子集關係會把新名字加進 names,撤回後名字留著、authorized 回到空,`displayName` 的 fallback 是 `names.current`(沒有時間欄位時取序列化順序最後一筆),剛加進去的名字因此成為顯示名與匯出的 `name_current`。待使用者裁決(連同 organization 要不要有名字的移除面)。~~判定記錄待 #564(2026-10-01 裁決要留、另案落地)。~~ → **#564 落地(2026-10-01)重新確認,裁決不變、契約有改(BREAKING)**:兩面同時新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`,MCP `judgement`/`rests_on`,同走 `updateOrganizationArguments`)——`authorize` 的名字必附理由、證據可省略(至多 20 個 digest),缺理由整批拒絕、零寫入(讀 store 之前);沒有要指定的名字(沒給、空陣列、全是空白項)時照舊以「沒有要改的」拒絕,理由或證據單獨出現也一樣。每次指定、確認各寫一筆 `field: authorized` 的判定記錄(被換下的舊指定也寫一筆撤回),回報 `judgementsRecorded`,需要 store format ≥ 22;organization 沒有 variant,不會有 `field: variant`。**organization 沒有撤回腿**:`unauthorize` 於 #557 R1 verify 之後拿掉、待使用者裁決,所以 #564 裁決的五個面裡 organization 只有 `--authorize`。CLI 不過目標 store 確認閘(`WriteGateRulings` 的 `update-organization` 格,理由與 `update-venue` 同一格、與它一起待裁)。沒有有記錄的差異 **#564 修正輪重新確認,裁決不變、契約有改**:(a) MCP 對已拿掉的 `unauthorize` 鍵具名拒絕、零寫入(#557 R2 verify:server 對多餘的鍵一律忽略,帶它的呼叫照做 `authorize`、撤回被安靜丟掉並回報成功;CLI 面是未知旗標、exit 64——自此兩面都拒);(b) 兩面同時新增 `remove_names`(CLI `--remove-name`,使用者 2026-10-02 裁決 #564 第 2 點):只刪最後一筆名字分類記錄是撤回的名字(每一段連同記錄),理由只進報告、刪前要求 organization 檔已 commit;organization 沒有一般的名字移除面(仍待裁),這一條只是打錯字的出路;(c) `authorize` 一次至多 200 個名字、單一名字至多 65,536 位元組;(d) `authorizedNotCurrent` 只在機構另有現行名稱時出現(已解散的機構不報),訊息不再說「已結束」(只有觀測點的段不是已結束,#661)。沒有有記錄的差異 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:`remove_names` 的報告每列多 `segments`(刪掉的每一段的 start/end/ended/attested/source/note,與 venue 的 `edit_name_segment` 一致——第 20 列:一個沿革名的整段只剩 git 裡有);沒有記錄的名字被拒時說出「先讓它有一筆撤回」的兩步與代價(第 7/23 列);不在 authorized、最後一筆卻是指定的說「先指定回去再撤回」(第 6 列);「organization 的 names 只增不減」的三處文字(MCP 對 `unauthorize` 的拒絕、CLI abstract、寫入閘理由)改成「沒有一般的名字移除面」(第 8 列:`remove_names` 落地之後那句字面為假)。兩面同一條路徑。organization 刪名字面的範圍本身(#557 R1 的「沒裁過的公開契約先不上」標準,第 25 列)交給使用者。 **#564 b34 重新確認(使用者 2026-10-05 裁決第 4、5 點),裁決不變、契約有改**:`remove_names` 保留(第 5 點);**有記錄的差異**:CLI `--remove-name` 過 #298 的目標 store 確認閘(第 4 點,`WriteGateRulings` 的 `update-organization` 改成逐腿、只有這條腿過閘;沒有乾跑),MCP `remove_names` 不閘(橫切表 `--yes` 列)——上面「CLI 不過目標 store 確認閘」那一句對 `--authorize` 仍成立。`remove_names` 的報告 `segments` 只列帶時間、source 或 note 的段、每個名字至多 20 段(總數在 `segmentsRemoved`,b33 X1 第 22/27 列);沒有記錄的名字被拒時的出口說出換上去的名字成為撤不掉的對外名稱(第 20/29 列,先前說「同書寫系統沒有別的名字時只能手改 YAML」是假話)。兩面同一條路徑。 **#564 b37 重新確認(b36 Y1 第 0/1/7/15 列),裁決不變、契約有改(只有報告與訊息)**:(a) `remove_names` 報告的 `segments` 每段 `attested` 至多 20 個、超過時最後一項說出省略數與總數(先前只截段數,一段兩萬個觀測點一次回 46 萬位元組;不新增回應鍵);(b) `remove_names` 的拒絕在 store format < 22 時說出寫入閘與升級路徑(三條出口都經 `--authorize` 寫記錄;person 那條 b34 補了、這條沒有);(c) 上面「organization 刪名字面的範圍本身交給使用者」那一句由使用者 2026-10-05 裁決第 5 點了結(保留)。兩面同一條路徑 | -| `akashic_resolve_organizations` | `resolve-organizations` | ✅(#304 移轉;CLI-only 表「候補缺席(重啟訊號已觸發)」格的補齊——同 `import-wos`/#290 的移列形) **#628 重新確認,裁決不變、契約有改**:作者位候選所在的 work 無法唯一定位時,MCP 的 apply/reject 整批拒絕、列表以 `unlocatableCitekey` 標出;CLI 的篩選式批次排除並另列。~~**change `resolution-verdict-states`(#619):org 族的未決寫入面兩面都缺席**~~ → **change `org-undecided-leg`(#643)起兩面同時新增 undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedOrganizations`;單獨呼叫)。id 是 `<列表的 id>@=說明`:在每個 `@=` 的位置試切,前綴必須恰為這次列表的已知 id,恰一個才收(使用者 2026-09-25 裁決——org 的 id 帶 literal,第一個或最後一個 `=` 都會切錯);歧義條目也帶 id、可逐個 org 記。兩面同契約(整批拒絕、逐筆略過、`alreadyRecorded`、呼叫層三個上限加單筆 id 長度上限、person/organization 同 key 兩列都在列表上時整批拒絕、format ≥ 19——R2 verify 起 CLI help 與 MCP 描述逐條對齊)。揭露:MCP 候選列與歧義條目帶 `id` 與 `undecidedChecks`(歧義條目是 orgKey → 次數)、頂層 `undecidedTotal`(候選配對,與 CLI 四態計數行同一個定義)與 `ambiguityUndecidedTotal`(CLI 另印一行,同一個定義);CLI 列表逐列印 id 並標「查過未決 N 次」,消毒改了 id 時標出它不能逐字送回。**有記錄的差異**:CLI 篩選式 `--apply` 排除查過未決的候選並另列、全數排除時零寫入非零結束;MCP 逐 id apply 照寫;`--reject` 兩面都不排除(否決是一個判定)。**與 #624 不完全同形**(#643 R1 verify):people 的 CLI 有 `--judge` 這條逐 id 的路,org 的 CLI 在 #647 之前沒有逐 id 的 apply——查過未決的 org 候選在 CLI 上歸戶不了。**#647 起兩面同時新增 judge 腿**(CLI `--judge`/MCP `judge`,同走 `judgeOrganizations`;使用者 2026-09-27 裁決比照 people 的 `--judge`):`<列表的 id>@=理由`,id 解析與未決腿共用、歧義條目也收;歸戶並寫 `org-judged` 層級的 confirmed(judged 層級封閉列舉的第三列,顯式加入)。兩面同契約:輸入錯整批拒絕、store 狀態不符(citekey 重複、上級機構判給自己或會成環、判給歧義條目裡已否決過這個配對的 org——比對用正規化鍵;候選列上否決配對的其他拼法自 #647 R3 起不再列出、拿它的 id 是輸入錯、列表過期)該筆略過並具名、已歸戶而理由存不進去時帶 `verdictNotRecorded`、寫入前整個寫入集合(含 #631 的目的檔檢查)先驗、單獨呼叫;CLI 另過 #298 的目標確認閘。`--undecided` 不接受 `--holder`/`--org`(id 已點名,收窄沒有作用)。**#648 重新確認,裁決不變、契約有改**:apply、reject、judge、undecided 在任何檔落盤之前對整個寫入集合逐筆 preflight(writeX 在寫入當下跑的每一道,含 encode 與讀取上限),寫出後超過 8 MiB 即整批拒絕、零寫入、錯誤具名記錄與兩個位元組數——apply 在此之前先寫 person/org/entry,verdict 那一筆最後才在寫入當下被拒;兩面同一條 service 路徑 **#641 重新確認,裁決不變、契約有改**:以 person 為 holder 的候選(隸屬)同樣接上「無法唯一定位」(`unlocatablePersonKeys`)——MCP 的 apply/reject 整批拒絕、judge/undecided 該筆略過並具名、列表以 `unlocatablePersonKey` 標出(作者位列仍是 `unlocatableCitekey`),CLI 篩選式批次排除並另列。MCP 的 apply 自此只寫真的改到的 person/organization/work,並在第一次寫入之前驗整個寫入集合(含 #631 的目的檔檢查,`judgeOrganizations` 的既有形)——先前重寫全庫每一筆 person,任何一筆寫不進去就在中途撕裂;`peopleRewritten`/`organizationsRewritten` 改數真的改寫的筆數。CLI 篩選式 apply 本來就只寫改到的、逐筆收容。 **#670 重新確認,裁決不變、契約有改**:目標 organization(verdict 寫進它)或 organization holder(上級機構那一格)的 key 有不只一筆記錄時,MCP 的 apply/reject 整批拒絕、judge 與 undecided 該筆略過並具名、列表以 `unlocatableOrganizationKey` 標出;CLI 的篩選式批次排除並另列,逐列標出理由。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_store_source` | `store-source` | ✅(#264;**讀取面慣例**而非寫入面封閉例外——receipt 的 `discardedProvenance` 攜帶「你這份敘述沒被寫入」,人需要看得懂,故 `--json` 原樣轉印+人可讀同源。收**檔案路徑**不收 base64/stdin:MCP 面無 stdin 會讓兩面分岔,base64 把二進位塞進 JSON 會膨脹並整份進 context(#165 的既有威脅模型)。`SourceStore.storeSource` 的寫入面防護 #224 已完成,本格只補呼叫端——先前全樹零 production 呼叫端,能力只有寫 Swift 的人做得到,#206 判準的同形)。**#703 重新確認,裁決不變、契約有改**:單檔上限 256 MiB(`LibraryStore.maxSourceBytes`,唯一一份常數;裁決原話「256 MB」,數值是 2^28)——超過即整個呼叫拒絕、不截斷、零寫入,訊息說出路徑、實際大小與上限;內容逐塊讀、每塊讀完即釋放(R1 verify 之前這裡寫「記憶體與檔案大小無關」,實測不成立:存 128 MiB 的檔尖峰 RSS 約 285 MB;R1 起兩面同一個修正);目錄與 FIFO 以 `fstat` 拒絕,開檔不卡住;~~不支援 `RENAME_EXCL` 的檔案系統(exFAT、FAT32)退到 hard link 或確認後一般改名(R1)。~~(b26 F6:exFAT/FAT32 連 hard link 也沒有;兩條原子放置都不支援的磁碟區現在每一次存檔都具名拒絕——見本格後段與 `changelog/2026-10-02-b26-f6-fixes-703-700.md`。)兩面同一個 `AkashicService.storeSource`,回應鍵不變 **#703 R2 重新確認,裁決不變、契約有改**:位址上已有東西時只有大小相同的普通檔算「已經在了」,位址被目錄、symlink(含懸空的)或大小不同的檔佔住兩面都具名拒絕、不寫 index(R2 之前回成功並寫 index,R1 引入的回歸);回應多 `bytesWritten`(CLI 人可讀面多一行「已存入位元組/位元組早已在」);~~第三條放置路改成排他建立目的檔後複製(R1 的「確認後一般改名」會覆寫兩步之間出現的檔)。~~(b26 F6:第三條放置路整條拿掉,見本格後段與 `changelog/2026-10-02-b26-f6-fixes-703-700.md`。) **b26 F6 重新確認,裁決不變、契約有改**(使用者 2026-10-02 裁決):**第三條放置路拿掉**——`sources/` 所在的磁碟區做不到不覆寫的原子放置(`RENAME_EXCL` 與 `link(2)` 都不支援,exFAT、FAT32)時,兩面每一次存檔都在建立任何檔案之前具名拒絕、零寫入,訊息說要把 store 放在 APFS 或 HFS+;`copy-zotero-attachments` 的批次前置檢查(`preflightStoreSource`)同一道閘,整批拒絕。位址上已有同一份內容也拒絕(「每一次存檔」)。`F_FULLFSYNC` 回真的 I/O 錯誤時存檔被拒、不退到 `fsync`(兩面同一條 `syncFile`)。`bytesWritten` 在 exFAT 上每次都是 false 的問題隨第三條路一併消失。 **b29 V5 重新確認,裁決不變、契約有改**:(a) 磁碟區沒有回報能力旗標時,寫入之前、**任何分支之前**(含「位址上已經有同一份」)在分片目錄裡實際放一次空的探測檔(同一條放置路;探測名是暫存檔的形狀、用完即刪),做不到就同一句拒絕、零寫入、不補 index——先前那一支不經任何放置呼叫就回成功、缺條目時還補一列;拒絕也因此在複製內容之前。預演(`preflightStoreSource`)不寫、不探測:旗標讀不到時 `copy-zotero-attachments` 的乾跑說可以、實跑逐筆拒絕。(b) 同步的「不支援」是封閉的六個 errno(`ENOTSUP`、`EOPNOTSUPP`、`EINVAL`、`ENOTTY`、`ENODEV`、`ENOSYS`),同步失敗的訊息說出 errno 與它的說明。(c) MCP 說明更正:`exclusionVerified:false` 是 store 不在 git 工作樹內、排除沒驗、**照存**(先前寫「false 時拒絕」,與行為相反);sources/ 未被 git 忽略才拒絕。(d) **相對於上一個發行版的行為改變**:#703 之前的 binary(`mcpb/manifest.json` 的 0.12.1)在 exFAT/FAT32 上的 store 存得進去(Foundation 的原子寫入,verify 實測);#703 之後每一次存檔都被拒,含已在的內容與只需補 index 的那一筆——既有的 blob 照樣讀得到,doctor、validate 照常。見 `changelog/2026-10-02-b29-h5-fixes-703-700.md`。 | -| `akashic_enrich_from_zotero` | `enrich-from-zotero` | ✅(#340;**#206/#290 判準的第三次套用**——那句原話是「能不能無損匯入,不該取決於使用者會不會寫 script」,在這裡是「能不能把一筆跌破下限的記錄補回下限,不該取決於面」。兩面同走 `ZoteroEnrichment.plan`。**契約有記錄的差異**:CLI 的 `--apply`走 #298 的破壞性閘、MCP 面用 `dry_run` 且**不設閘**——該閘擋的是「篩選式批次寫入未指名目標」,而本 tool 收的是逐筆顯式指名的 citekey 清單,與 `resolve-people` 的 tier 閘同型不對稱。**與 `akashic_import_zotero` 刻意不同語意**:那是 pull(整份替換 `fields`、重設 `type`、覆寫未歸戶作者),這是 add-only;同一個 store 上兩種語意並存是設計,不是重複) **#628 重新確認,裁決不變、契約有改**:citekey 重複或與另一筆 work 共用 id 的歸新的 `unlocatable` 類,兩面同一個 plan。 **#641**:`unlocatable` 自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | -| `akashic_enrich` | `enrich` | ✅(#458;**#554 R26 D73 重新確認(R27 補記),裁決不變、契約有改**:`applied` 的來源 reference 冪等自此比位元組(`byteExactKey`),同一次呼叫送兩次只差 NFC/NFD 的 judgement 兩筆都落盤、沒有掃描面看得到——缺口記 #582;generic add-only 補值——`akashic_enrich_from_zotero` 那列的**一般化**:政策抽成 `AddOnlyEnrichment`(AkashicCore)一份,Zotero 版降為它的 adapter,兩面同走 `AkashicService.enrich`。輸入以 citekey 或 DOI 定位(DOI 相等是 `identity-is-judged-not-matched` 的識別碼例外,對回 citekey 由程式做;命中 ≥2 筆=`ambiguous`、具名全部 citekey、零寫入——不判定哪筆才對,那是 #459)。雙摘要分鍵(`abstract-`/`abstract-2` → `abstract_es`/`abstract_2`);`sourceDigest` 只回顯不進 store(第 15 條邊的值域是 #450 的裁決;**#517 起不再成立**,見本列後段)。**契約差異沿上一列**:CLI `--apply` 走 #298 閘、MCP `dry_run` 預設 true 且**不設閘**(逐筆顯式指名)。**第二個有記錄的差異**:MCP 面 items 截 20 筆(`counts`/`written`/`writeFailed` 永遠完整、`itemsTotal`/`truncated` 揭露)、CLI 不截——同 #388 `--rows` 那格的理由:上限保護的是消費端,MCP 的輸出進 LLM context。兩面**同一個 JSON 解析器**(`AddOnlyEnrichment.decodeProposals`:CLI 讀 `--from` 檔、MCP 把 `Value` 編回 JSON 再解),頂層未知鍵整批拒絕——把 `abstract` 寫在頂層而非 `fields` 是最容易犯的錯,靜默略過會讓那筆看起來「補了」)。**#517 起 `sourceDigest` 寫得進 store**:提案多收 `sourceURL`/`sourceRetrieved`/`sourceMediaType`/`sourceStatus`(蛇形同收),三欄齊備時每個補進去的欄位寫一筆 `retrieval` reference(**#542 R2 起是四欄**:`sourceStatus` 必要、不再預設 200——一份離線掃描檔曾因此被記成 HTTP 200;給了幾個卻不齊時缺哪些進 `provenanceSkipped`,不只「有 digest」那一種;三個來源字串納入 #519 的長度上限;digest 不合法整批拒絕)(`fields.<鍵>`/識別碼帶 value),與被補的值**同一次寫入**;**只給 digest 仍只回顯**,理由具名進 `provenanceSkipped`——一次取得的 url 與日期沒有別的地方記,digest 單獨湊不出一筆誠實的 retrieval。新鍵**不必逐面加**:兩面本來就同一個解析器(本列既有記載),這是那個設計的第一次兌現——**但這句只對解析器成立,對 MCP 的 input schema 不成立**(#542 R1 verify 兩席 HIGH):proposals item 宣告 `additionalProperties: false` 而只列了 `sourceDigest`,照 schema 呼叫的 client 送不出另外四個欄位。#542 起 schema 列齊五個來源欄位;**#542 重新確認,裁決不變、契約有改**:item 的 provenance 狀態三個鍵擇一(`provenancePlanned`/`provenanceWritten`/`provenanceNotWritten`),兩面同一個 payload;~~date/authors 不產生 reference(#655)~~ → 見本列末 #655 **#628 重新確認,裁決不變、契約有改**:citekey(直接給或由 DOI 對回)無法唯一定位時,該筆歸 `ambiguous`、理由與 DOI 多筆命中分開說,兩面同一個 core plan。 **#641**:`ambiguous` 的無法唯一定位自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)。 **#655 重新確認,裁決不變、契約有改**(使用者 2026-09-28 裁決):補進去的 `date` 在來源四欄齊備時也寫一筆 retrieval reference(第 15 條邊的新格 `field: date`,store format ≥ 20),與其他欄位同走 `provenancePlanned`/`provenanceWritten`/`provenanceNotWritten`;補進去的 `authors` 一律不寫(`field: authors` 是作者位記錄的格子)。兩面同時新增 item 的 **`provenanceOmitted`**(欄位 → 理由):來源齊備、值補了、reference 刻意不寫的欄位——`authors` 一律、`date` 在 store format 低於 20 時(值照補,理由說出 format 與門檻)。它與三個狀態鍵**正交**,刻意不借 `provenanceNotWritten`:那個鍵的語意是「apply 時那一筆寫入失敗」(#542 的「鍵名本身說出事實」),刻意不寫與寫入失敗是兩件事。format 由 service 在寫入前讀 marker 判定(`StoreVersion.workDateReferenceFormat`,與寫入閘同一個常數),兩面同一條 service 路徑;MCP schema 的 `date`/`authors` 描述與 CLI `--from` 的 help 同批改寫,CLI 逐欄印「來源未記:」。 **#654 重新確認,裁決不變、契約有改**:`sourceDigest` 是空內容的 digest(0 byte 的 SHA-256)時整批拒絕、訊息說 0 byte 而不說形狀錯。閘是 `ProvenanceReference.isValidDigest` 本身(兩面同一個 core 驗證),所以收 digest 的其他面——`akashic_update_person` 的 references、`akashic_update_venue` 的 `paginated` rests-on、三個 resolve 族的 undecided rests-on、`akashic_record_divergence` 的依據——兩面同時拒收,那幾列不逐列重述。 **#668 重新確認,裁決不變、契約有改**:store format 低於 `StoreVersion.workFieldReferenceFormat`(17)時,`fields.<鍵>` 的值照補、reference 不寫,理由逐鍵進 `provenanceOmitted`(鍵是 `fields.<鍵>`)——與 `date` 在 20 以下的處置同形,兩面同一個 payload。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#695 重新確認,裁決不變、契約有改(收緊)**(使用者 2026-09-30 裁決「要,共用同一個解析函式」):`sourceURL`/`sourceRetrieved`/`sourceStatus`(與蛇形別名)走 `akashic_update_person`/`akashic_update_venue` 的 `references` 同一個形狀函式(`RetrievalWriteShape.firstIssue`,Core;在 `AddOnlyEnrichment` 的提案驗證裡呼叫,兩面同一個解碼與驗證):url 只收 http/https、主機非空、不含帳密(不回顯),retrieved 是 ISO 8601,status 在 100–599,給了 url/retrieved/media type 就必填、不預設 200。不合的提案整批拒絕、零寫入、具名第幾筆(輸入錯的既有失敗語意)。先前「給了 url/retrieved 卻沒給 status」是值照補、reference 不寫(`provenanceSkipped`),現在是錯誤;只給 `sourceDigest` 仍是回顯(#517,不觸發 status 必填)。兩面的描述(MCP 三個 source 鍵、CLI `--from` 的 help)同批改寫。**R1 verify 更正**:共用的是一個**形狀**函式,不是一整份寫入契約。「這筆算不算擷取型、要不要 status」由兩個面各自判定,刻意不同:references 對不齊的 retrieval 一律拒絕(平面 init 要四欄),`content` 單獨出現也要 status;enrich 對「給了 status 卻缺 url/retrieved」照補值、reference 不寫(`provenanceSkipped`),`sourceDigest` 單獨給是回顯、不觸發 status。理由是兩面的輸入不同:enrich 的來源欄位是補值的附帶資訊,摘要提案只帶 digest(#517)。同一輪收緊了形狀本身,被拒的輸入多四種,見 `akashic_update_person` 列的 #695 註記。 **#709 重新確認(2026-10-05、2026-10-09),裁決不變、兩面契約有改**:DOI 命中兩筆以上的理由裡,哪一筆是 legacy 拷貝只問 load 的標記;命中以完整的 load 算、不經 citekey 去重(同 citekey 的拷貝不再把 entities/ 那份擠掉——先前會說 entities/ 那份「不帶這個 DOI」、或回 notFound);理由的結論與處置排在逐份明細之前,出口截到 512 字時被截的只是明細。回應的鍵與 `ambiguous` 不變,兩面同一個 core plan——細節在本檔 #709 段。 | -| `akashic_s2` | `s2`(`paper`/`match`/`batch`/`references`/`citations`/`recommend`/`author-search`/`author-papers`/`status`)| ✅(#664;Spectra change `semantic-scholar-interface`)。**本表第一個連網的工具**:兩面同走 `AkashicS2`(`S2Endpoints`/`S2Client`/`S2Output`),**不經 `AkashicService`、不開 store**——S2 的回傳是線索,不是寫入;包進綁 store 的服務層只會多一層只轉呼叫的 adapter(design 的否決理由)。網路與 keychain API 只准出現在 `Sources/AkashicS2/`(`akashic-guards network-confinement`,負對照 `network-confinement-mutations`),這個例外的理由在 `web-access-via-safari-browser` 的〈例外〉第 2 類。**兩面同契約**:金鑰只從 keychain(service `semantic-scholar`、account `default`)在程序內非互動讀取,不收參數或環境變數,缺金鑰不退回匿名請求;`x-api-key` 只附給 `https` 且 host 恰為 `api.semanticscholar.org` 的請求;全機合計每秒至多 1 個請求、429 共用退避、同一請求至多重試 3 次;以 `10.` 開頭的裸 DOI 補 `DOI:`;JSON 面(CLI `--json`、MCP、`status --json`)的字串原樣保留、序列化後以 `documentSafeJSON` 無損逃脫,人可讀的 CLI 面(終端機)才經 `displaySafe`;`status` 兩面同一份 JSON(`S2Output.statusJSON`),不連網、不含金鑰也不含它的長度。**錯誤文字兩面逐字相同**:金鑰不可用、限流用盡、S2/網路錯誤都經 `displaySafeErrorMultiline(_:prefix: "Error: ")`,CLI 以結束碼 3/4/5 區分,MCP 回 `isError: true`。CLI 另有兩個碼(MCP 一律 `isError: true`):1=要讀 argv 以外才判得出的錯(環境覆寫被拒、`--ids-file` 讀不到、超過 256 KiB、筆數不合或有一行不像識別碼、節流狀態檔無法使用——#549 的判準),64=只看 argv 判得出的錯(`--limit`/`--offset` 的範圍、識別碼或 `--title`/`--name` 空白、識別碼含 `.`/`..` 路徑片段,在 `validate()` 檢查、早於任何 I/O)。**有記錄的差異**:(a) **位元組上限**——MCP 受 48 KiB(`S2Output.mcpByteBudget`,與 `akashic_doctor` 的 `candidateByteBudget` 同值),只放完整的筆數,以 `total`/`returned`/`truncated`/`offset`/`nextOffset` 揭露;**續查的訊號是 `nextOffset`(非 null 才續,null 是沒有下一頁),不是 `truncated`**(`truncated` 只表示被位元組上限截斷;預設 100 筆剛好放得下而 S2 還有更多時,`truncated` 是 false、`nextOffset` 非 null);CLI 不截。理由同本表 `akashic_doctor` 列與 #388 `--rows` 那格的面級分工:MCP 的輸出進 LLM context,呼叫端收到後無法退回已付的代價;CLI 的輸出進人的終端機。(b) **分頁**——CLI 的 `references`/`citations`/`author-papers` 省略 `--limit` 時翻完全部頁;MCP 的分頁端點省略 `limit` 時一次只向 S2 要 100 筆(`S2Tool.defaultPageLimit`),其餘以 `offset` 續查——一次呼叫不拉回上千筆。`author-search` 省略時兩面都是 100 筆,`recommend` 兩面都是 1–500、預設 100。(c) **輸出形**——CLI 預設印人可讀(一行一筆),`--json` 是 `{source,endpoint,request,fetchedAt,total,data}` 信封,兩者同源;MCP 只回 `{endpoint,total,returned,truncated,offset,nextOffset,data}`。(d) **輸入形**——`batch` 在 CLI 收 `--ids-file`(一行一個 id)、在 MCP 收 `ids` 陣列;端點名 CLI 用連字號(`author-search`)、MCP 用底線(`author_search`)。(e) **`status` 的結束狀態**——CLI 在金鑰不存在或讀不到時以 3 結束(結束碼 3 涵蓋兩種情形,skill 選路要再看 `--json` 的 `keychain.present`:false=找不到、true=項目在而讀不到),MCP 照常回成功、由呼叫端讀 `keychain.present`/`keychain.readable`——與 #624 全數排除「CLI 非零結束、MCP 由呼叫端讀欄位」同形 (f) **MCP 專有的限制**——分頁端點的 `limit` 至多 1000(S2 的一頁),超過在送出任何請求之前拒絕;`paper`/`match`/`batch`/`recommend` 不分頁,被截斷時 `nextOffset` 永遠是 null(少給 id、降低 `limit` 或少要 `fields` 重查);第一筆就超過 48 KiB 時回錯誤並點名 `fields`,不回「成功、0 筆」。CLI 沒有這三項,因為它不截。 | - -**#654 重新確認(2026-09-28),各列裁決不變、兩面契約有一處改**:服務層只看參數的檢查——id 與 `key:shape` 的形狀、理由的空白與長度、同一批重複、一次的上限、rests-on 的 digest、key 的格式、互斥與必填——抽成不碰 store 的 static 函式,服務方法在讀 store 之前呼叫,CLI 的 `validate()` 呼叫同一個函式(同一件事只有一份描述)。兩面拒絕的集合不變,變的是**先後**:一次呼叫同時有參數錯與 store 狀態不符時,兩面都先報參數錯(先前依迴圈順序,可能先報 store 狀態或 format 閘);add_person/add_venue 的 key 格式先前在寫入閘擋、訊息是 store 層的,現在服務先擋。CLI 面對這些錯誤自此回 64(用法錯誤)而不是 1——判準與仍是 1 的站點在 `RuntimeFailure` 的 doc。涵蓋 resolve_people(judge/refute/undecided/split/un-split/drop/attribute-org)、resolve_venues(repoint/demote/drop/undecided)、resolve_organizations(undecided 與 judge 的一次上限、undecided 的 rests-on digest、連一個 `@=` 位置都沒有的 id)、update_venue、add_venue、add_person、update_person(CLI 只在 `--fields` 時)、record_divergence、store_source、person、venue、link、tag、set_status 這幾列,各列不逐列重述。 - -**#705 重新確認(2026-09-30),各列裁決不變、兩面契約有一處改**(使用者裁決 (a):成功側另開欄位):`writeEntry`/`writePerson` 寫進 `entities/` 之後刪不掉 #631 搬移的 legacy 拷貝時,在回報面開的收集範圍(`LegacyCopyLedger`)裡只記下、照常回傳——它寫了;範圍外照舊擲 `legacyCopyNotRemoved`(#702);App(第三面)自 #708 起也開範圍,見本段末。**MCP**:工具分派把每一次呼叫包在範圍裡,成功的 JSON 物件加鍵 `writtenWithLegacyCopy`(每筆 `{kind, key, written, legacyFile, detail}`,不截;已帶這個鍵的回應併進同一個陣列),錯誤回應把同一份人可讀報告放在訊息**最前面**(R1 verify 前附在末尾——呼叫端先讀到 UNIQUE 錯誤);`akashic_import_zotero` 的 index rebuild 失敗時,importer 收下的那幾筆交給分派的範圍、不再嵌在錯誤訊息的 payload 裡(嵌進去會被錯誤出口的 200 行/96 KB 上限截掉);各工具自己的失敗清單(`writeFailed` 等)不含它。**CLI**:進入點把每一個命令包在範圍裡,命令結束後(擲錯也是)印在輸出末尾、錯誤訊息之前;直接印 service JSON 的寫入命令(update-person、update-entry、tag、link、set-status、resolve-venues、enrich 的 --json)把鍵放進那份 JSON,stdout 仍是一份 JSON。兩面的人可讀報告是同一個 `LegacyCopyLeft.reportLines`。~~**有記錄的後果**:work 的兩份共用同一個 citekey,寫入之後的 index rebuild 撞重複——會重建 index 的工具因此回錯誤(報告附在錯誤訊息末,CLI 印在錯誤之前);person 的兩份不擋重建(#670),回成功。~~ → **#709(2026-10-01)起不成立**:使用者 2026-09-30 裁決 index 重建以 `entities/` 那份為準、略過同一種同一個 id 的 legacy 拷貝並回報(判準只有一份:`LibraryStore.markLegacyCopiesShadowedByEntities`),work 與 person 都一樣——會重建 index 的工具在這種狀態下回成功、`writtenWithLegacyCopy` 在成功回應裡,CLI 結束碼 0;兩面契約同時改、沒有有記錄的差異。那一筆本身照舊寫不進去(#641,無法唯一定位),validate/doctor 照舊報兩份並存(#641 的 warning 多一句「index、匯出與 App 以 entities/ 那份為準」,兩面經同一份 `crossRecordIssues`);~~doctor 仍因那一對的 UUID/citekey 重複 error 不重建。~~ 報告只有在寫了之後**別的**步驟失敗時才落在錯誤那一側(最前面,CLI 印在錯誤之前)。**#709 第二個 commit(使用者 2026-10-01 延伸)**:這一對造成的 UUID 共用與 citekey/person key 重複從 error 降為 warning(整組同一個 id、其中一份被標為拷貝才算;兩筆不同的記錄共用照舊是 error)——`akashic_doctor`/`doctor` 照常重建,rename、rename-person、resolve-divergence 不再因它整個 store 停下;resolve-divergence 另對「候選有 legacy 拷貝」具名拒絕(被併者的 legacy 那份會復活),CLI-only 表那一列的裁決不變、契約多這一種拒絕。匯出(`akashic_export`、`export-bib`、`export-tables`)與 App 以 entities/ 那份為準,兩面同一個 helper(`LibraryLoad.withoutShadowedLegacyCopies`),沒有有記錄的差異;只剩這一對的 store,CLI `validate` 結束碼 0。先前這一筆在 import-zotero 同時列在成功清單與 `writeFailed`,在其他逐筆收容的寫入者只算失敗;沒有收容的寫入者擲錯中止,同一批後面的記錄沒寫。涵蓋的 MCP 列即上表各帶 #705 註記的 13 列;CLI-only 表的寫入命令(rename、rename-person、resolve-divergence、migrate 一族、authorize-names 等)由進入點的範圍涵蓋,各列裁決不變、不逐列重述。**R1 verify 更正**:rename 與 rename-person 在寫完新鍵那一份之後自己刪舊鍵的 legacy 檔,那一步原本不經範圍——唯讀目錄讓改名以 Foundation 的原始錯誤中止、引用還沒改寫,上一句對這兩個命令為假;R1 起那一步走同一個搬移刪除,改名做完、legacy 那份進報告。~~App(改名、單筆編輯、裁決台)仍沒有範圍,照舊擲「已寫入……」。~~ → **#708(使用者 2026-09-30 裁決)起 App 也開範圍**:各寫入點(`AppState.mutate` 底下的衍生層編輯、`AppState.rename`、裁決台的 accept 與兩個 orphan 動作)包在 `AppState.recordingLegacyCopies` 裡,留下拷貝的那一筆動作算成功,要清的 legacy 檔列在側欄的非阻斷提示(`AppState.legacyCopyNotice`,文字取自同一個 `LegacyCopyLeft.reportLines`/`message`);留下拷貝之後才失敗時兩樣都給——提示照列,失敗提示是先說寫了什麼的 `LegacyCopyLeftBeforeFailure`(改名之後的重建失敗例外:仍是 `renamedButReloadFailed` 的回執,它本身就說改名已寫入)。與兩面同一個後果:~~work 的兩份在 index 能容忍之前讓重建失敗,App 上那一格是「失敗+提示」;person 的兩份回成功。~~ → **#709 起 work 與 person 一樣**:兩份並存不再讓重建失敗(index 以 `entities/` 那份為準),App 動作成功、側欄提示列出要清的 legacy 檔(`AppLegacyCopyNoticeTests` 的 work 那一格釘住 `mutate` 之後 `legacyCopyNotice` 非空、重建成功,#709 R3 verify);只有 store 裡另有兩筆**不同**的記錄共用 citekey 時才回到「失敗+提示」。App 沒有單一出口可以一次包住,每個寫入點都要在範圍裡由源碼守衛釘住(`AppLegacyCopyNoticeTests.testEveryAppStoreWriteIsInsideTheScope`)。 - -**#705 R2 verify(2026-10-01),各列裁決不變、兩面契約有三處改**:(1) MCP 的 `writtenWithLegacyCopy` 至多列 20 筆(`writtenWithLegacyCopyLimit`),另給 `writtenWithLegacyCopyTotal`/`writtenWithLegacyCopyTruncated`(三個鍵同進同出),錯誤回應的人可讀報告同一個上限、多一行說筆數與去哪裡找;CLI 全列——有記錄的差異,理由同 `resolve-people` 列的 `--rows`(上限保護的是 LLM context;截掉的由 `akashic validate` 逐筆列出兩份並存的記錄),上一段的「不截」自此不成立。(2) 同一個操作稍早已寫入、之後同一筆的寫入被 #631 拒絕(兩份並存)時,兩面同一句拒絕(`legacyCopyLeftEarlierInThisOperation`,住在 `LibraryStore` 的寫入前置,所有寫入者共用),那一筆在成功側標出之後的寫入沒有套用(MCP 列 `laterWriteNotApplied`、人可讀報告附句);`import-zotero` 兩面都不再把那一步列在 `writeFailed`,其他逐筆收容的寫入者仍列在自己的失敗清單(同一句話)。(3) CLI:寫入後失敗時 stderr 第一行說 stdout 上報告過幾筆寫了;印 service JSON 的命令失敗時 stdout 是一份只有那三個鍵的 JSON(先前是 JSON 以外的文字);印 service JSON 的寫入命令必須經 `LegacyCopyReport.payload`,`LegacyCopyPayloadScanTests` 以命令樹與裁決表列舉寫入命令、掃它們的 `print(`,豁免逐格具名。 - -**#705 R3 verify(2026-10-02),各列裁決不變、兩面契約有兩處改**:(1) MCP 成功清單截到 20 筆時,「之後的寫入沒有套用」只標在被截掉的列上就沒有地方說——另給 `writtenWithLegacyCopyNotApplied`(完整的沒套用筆數,在截斷之外;> 0 才出現,沒有這個鍵=零;併進已帶這個鍵的回應時兩邊相加);錯誤回應的「另有 N 筆未列出」那一行補說其中幾筆沒套用;13 份工具說明共用的一句(`Server.legacyCopyNote`)點名這個鍵(2026-10-03 起以「鍵名加 NotApplied」組字指名,完整鍵名不在說明裡;組出來的名字由 `LegacyCopyNoteKeyTests` 與 `StdioE2ETests.testTheSharedLegacyCopyNoteNamesBothCompanionKeys` 釘住,#705 第三次 verify)。CLI 全列,列自己帶標記,JSON 同樣多這個鍵。(2) CLI:命令以沒有訊息的非零結束碼收場(`throw ExitCode(1)`,例如 import-zotero 的 `writeFailed`)時,stderr 先前整個是空的——stdout 上報告過寫了的筆數時,stderr 的第一行照給(`LegacyCopyReport.stderrText`),~~說「不要重跑」與「原因見 stdout」~~(#705 R2 verify 第二輪,2026-10-03:「不要重跑」恰好在要重跑的兩種情形出現——那幾筆有之後的寫入沒套用、或沒有訊息的非零結束本身就是有記錄沒寫成;第一行改成依情形措辭:有沒套用的說要重跑幾筆,都沒有時只說它們不必為了自己重跑,失敗本身另說要處理。MCP 面沒有對應的 stderr,不變;13 份說明共用的一句改短、內容不變)。import-zotero 本身在「只有之後的寫入沒套用、別的都成功」時結束碼仍是 0,~~唯一的訊號是 stdout 那一行(裁決 (a) 的後果,沒有變)~~ → 使用者 2026-10-05 裁決起 stderr 多印一行,見下一段。 - -**#705(使用者 2026-10-05 裁決),各列裁決不變、CLI 契約有一處改**:結束碼 0、但同一個操作之後的寫入沒套用(stdout 上報告過、標了 `laterWriteRefused` 的筆數 > 0)時,stderr 多印一行——結束碼 0 時使用者最可能不看 stdout 的報告與 JSON。那一行說筆數、清單在哪(文字輸出:stdout 的 `writtenWithLegacyCopy` 段;JSON 輸出:`writtenWithLegacyCopy` 各列的 `laterWriteNotApplied`、筆數在 `writtenWithLegacyCopyNotApplied`)、要重跑(與非零結束的第一行同一句 `LegacyCopyReport.rerunAdvice`)。由進入點(`AkashicCLI.main`)在命令成功之後印、只有一個函式(`LegacyCopyReport.successStderrLine`),新命令不必記得接;非零結束照舊走 `stderrText`,兩條不會同時出現。2026-10-05 走得到這一格的 CLI 命令只有 `import-zotero`(同一趟先清 orphan 標記、再更新書目欄位,第二步被 #631 拒絕而被接住);其他寫入者對同一筆在一次呼叫裡只寫一次,第二次寫入被拒時以非零結束、走 `stderrText`。MCP 面沒有 stderr,`writtenWithLegacyCopyNotApplied` 已是它的訊號,不變。 - -**#709 R3 verify(2026-10-02),各列裁決不變、兩面契約有三處改**:(1) `akashic_doctor`/`doctor` 的跨記錄清單 error 先、同 severity 內保持原順序(`StoreHealth.errorsFirst` 的 `ValidationIssue` 版,單一來源)——先前 MCP 面取前 20 則,一對 leftover 先造出兩則 warning,`indexRebuilt:false` 指向的 error 可能排在第 25 位而被截掉。(2) `validate` 的「標題與年份相同但 DOI 不同」與「DOI 被 N 筆 work 共用」不再把同一筆記錄的 legacy 拷貝算成另一筆;`StoreHealth` 的未歸戶作者與 orphan 讀數取 entities/ 那份(與 index、匯出、App 同一個視圖;驗證類的 `crossRecordIssues`/`perRecordIssues` 照舊看到兩份)。(3) 以 entities/ 那份為準的視圖再延伸到三個讀取面~~與三個寫入候選面~~,兩面同一個 helper:`akashic people`/`akashic_people`(列表)、`view show`(`--keys-only` 餵下游腳本)~~、`bootstrap-people`/`bootstrap-organizations`/`bootstrap-venues` 的作者/刊名 literal 來源(legacy 拷貝的內容不流進寫入候選;`existing` 仍用完整 load——改名留下的舊 person key 要算「已存在」)~~(寫入候選那一半在第三次 verify 收回,使用者 2026-10-05 裁決維持收回,見下)。~~**仍然讀完整 load、沒有切換的**(有記錄的殘留,不是遺漏):`akashic_person`/`get-entry`/`library list` 的單筆查詢(排序讓 `entities/` 那份在前)、`resolve-*` 的候選產生、`validate` 結尾的「N entries」計數(兩份算兩筆)。~~(#709 R2 verify:這份清單漏了 doctor 的普查讀數,又把 `library list` 寫成單筆查詢——它是成員計數,排序救不了它。現行清單見下一段。) - -**#709 R2 verify(2026-10-03),各列裁決不變、兩面契約有四處改**:(1) `akashic_doctor`/`doctor` 由記錄**內容**算出來的讀數全部取 entities/ 那份——CLI 的 `unresolved author literals` 改讀 `health`(先前自己從完整的 load 數,與 MCP 分岔),兩面的 `noAuthorizedName`/`no authorized name`、`authorizedOnlyByCitationForm`、`deceasedWithOpenAffiliation`、`digestSources`,CLI 獨有的 zero-dates 與日期值域普查,以及 fatal 分支的 `entries` 計數,同一個視圖、同一組函式;先前一對 legacy 拷貝讓同一份輸出 `entries: 1` 而 `unresolved author literals: 2`、`people: 25` 而 `noAuthorizedName.people: 50`。驗證類(`crossRecordIssues`、`perRecordIssues`、quarantine)照舊看兩份。(2) ~~`bootstrap-organizations` 的 person 隸屬 literal 也取視圖(R3 只換了 entries:拷貝獨有的機構名成為候選、一次出現算兩次而越過 `--min-occurrences`);`organizations` 仍用完整的 load(它沒有 legacy 拷貝,又是「已存在」的判準)。~~(第三次 verify 收回、使用者 2026-10-05 裁決維持收回:literal 來源看完整的 load,計畫讀到拷貝時 `--apply` 整批拒絕,見下。)(3) `library list`/`akashic_libraries` 的 `list` 與 `check`、`set-kind` 回報的成員數與不符清單取視圖。(4) `create-entry`/`akashic_create_entry` 的 `doiHits` 比對表取視圖(citekey 的佔用照舊看完整的 load——legacy 檔佔著那個檔名);~~`authorize-names` 的計畫與「person 總數」取視圖,`--apply` 的拒絕看完整的 load 與視圖兩邊(過濾不讓任何一筆變得可寫)。~~(第三次 verify 收回,見下一段:「不讓任何一筆變得可寫」只對逐筆成立,批次的結果變了。)**仍然讀完整 load、沒有切換的**(有記錄的殘留,不是遺漏):`akashic_person`/`get-entry` 的單筆查詢(排序讓 `entities/` 那份在前);`resolve-*` 的候選產生;`validate` 結尾的「N entries」計數(兩份算兩筆);`references nominate` 的 DOI 與標題比對表(改名留下的拷貝讓同一筆被報成「庫內衝突」並指路 akashic-merge-twins,而 resolve-divergence 對有拷貝的候選拒絕——本輪不碰那個檔);`record-divergence`/`akashic_record_divergence` 的候選存在檢查(改名留下的舊 citekey 收得下、記下一筆與自己拷貝的歧異——只換視圖會讓訊息說「store 內沒有」而檔案就在,要具名拒絕得另寫一則訊息)。**這兩輪把視圖從使用者 2026-10-01 的裁決範圍(index、匯出、App)延伸到 doctor 的普查、~~三個 bootstrap 與 `authorize-names` 的計畫、~~library 計數、DOI 命中~~,其中寫入候選面(bootstrap、authorize-names)待使用者確認~~**~~:手改過的 legacy 拷貝的內容不再成為建檔候選,與已接受的「手改 legacy 那份 index 看不到」同一個取捨,但寫入面是新的~~。(寫入候選那一半在第三次 verify 收回;使用者 2026-10-05 裁決不延伸,見下兩段。) - -**#709 第三次 verify(2026-10-04),各列裁決不變、兩面契約有三處改;寫入候選面與依據判定收回到完整的 load**:使用者 2026-10-01 的裁決範圍是 index、三個匯出面與 App;R3 與 R2 把以 entities/ 為準的視圖延伸到寫入候選面與依據判定,~~那一半待使用者確認,確認之前不出貨(主 session 保守裁定)~~(使用者 2026-10-05 裁決:不延伸——規則與它的封閉例外見下一段)。寫入候選面(含 bootstrap 計畫)看完整的 load,以 entities/ 為準的視圖只用於讀數——依據判定(library 規則的依據)同屬寫入面那一邊。(1) `authorize-names` 的計畫與 `--apply` 的拒絕都看完整的 load——R2 讓計畫取視圖之後,改名留下 legacy 拷貝的 person 從整批拒絕變成寫入(entities/ 那份本來就寫得進去,擋住批次的是 legacy 那份被算進寫入集合);乾跑也算出 `--apply` 會擋的那幾筆(`AuthorizedNameMigration.Report.blockedKeys`),點名並說 `--apply` 會整批拒絕、不再說「確認後執行」。CLI-only,MCP 沒有這一面。(2) `library check`/`list`/`set-kind` 與 `akashic_libraries` 的 `check`/`list`/`set-kind`:依據(`basisProblem`:文件型的文件在庫裡有幾筆、規則的 venue 有幾筆)看完整的 load,與 `library add`、`validate`、App 的加入動作同一份判定;成員數與不符清單仍取視圖——三面同一個函式(`LibraryLoad.membershipReading`)。先前文件自己有 legacy 拷貝時 check 說「全部符合」而 add 拒絕。(3) 三個 bootstrap 的 literal 來源(含 `bootstrap-organizations` 的 person 隸屬)回到完整的 load,有拷貝時開頭印一行說計畫含幾份、計數多算一次(`LibraryLoad.legacyCopiesInPlanNote`,2026-10-09 起改名 `planBlockersNote`;`bootstrap-people --json` 多一個 `legacyCopiesInPlan`);CLI-only。~~**殘留、交使用者裁決**:一對相同的 literal 在完整的 load 上算兩次出現,可能越過 `--min-occurrences`——附註說出來、不修正計數(修正計數要辨認一對,那就是把視圖帶回寫入候選面)。~~(第四次 verify:括號裡的理由不成立——以 id 去重計數、保留拷貝獨有的 literal 不需要視圖;而附註之後 `--apply` 照寫,補救到達時寫入已經發生。使用者 2026-10-05 裁決:計畫讀到拷貝時 `--apply` 整批拒絕,見下一段。)實際影響範圍:2026-10-04 唯讀量測 live store 的 legacy 殘留 `entries/` 0 個檔、`people/` 0 個檔(量法同 `zero-instance-guards` 第 40 列),所以今天沒有任何候選的計數受影響。**純讀數**仍取視圖、兩面同一個 helper:doctor 的普查、library 的成員數與不符清單、`create-entry` 的 DOI 命中、`akashic people`、`view show`,本輪再加 `resolve-people` 無候選時的「literal 作者 N 個」(與 doctor 同一個數)與 `akashic_person` 名字查找的篇數。**仍然讀完整 load、沒有切換的**(有記錄的殘留,取代上一段的清單):`akashic_person`/`get-entry` 的單筆查詢——同 key 的一對靠排序讓 entities/ 那份在前,**改名留下的拷貝**以舊 key 查到的是 legacy 那份的內容(上一段寫「排序讓 entities/ 那份在前」對改名拷貝為假);名字查找的 person 列(一對列兩次,篇數不重複);`resolve-*` 的候選產生;`validate` 結尾的「N entries」;`references nominate`;`record-divergence` 的候選存在檢查;`enrich` 的 DOI 定位(寫入面:改名拷貝讓同一個 DOI 命中兩筆、零寫入,~~理由自本輪起說出那是同一筆記錄的拷貝、不再指 #459~~——第四次 verify 起只對 load 標成拷貝的說拷貝,見下一段;兩面同一個 core plan);`literal-census`(直接掃 YAML、不經 loader——辨認一對的「同一種、同一個 id」判準只有 `markLegacyCopiesShadowedByEntities` 一份,在掃描器裡寫第二份是 `no-compat-fallback` §「同一件事只能有一份描述」禁止的;有拷貝時它比 doctor 多算拷貝裡的 literal)。validate 的 library 成員警告照舊看完整的 load(驗證面),所以成員的 legacy 拷貝不符規則而 entities/ 那份符合時,validate 點名它、`library check` 不列——那一對另有兩份並存的警告。 - -**#709 第四次 verify(2026-10-05),各列裁決不變、兩面契約有一處改、CLI-only 三處改;使用者 2026-10-05 兩項裁決落地**。**規則(使用者 2026-10-05 裁決 1):寫入候選面一律看完整的 load,以 entities/ 為準的視圖只用於讀數。** 例外是封閉列舉,只有使用者 2026-10-01 裁決本身的範圍,不得依性質相似類推:index 重建、三個匯出面、App——App 的裁決台以視圖產生候選並逐筆寫入(`AppState.load`),可以的理由是它逐筆以 citekey/key 定位寫入,而逐筆的可寫性不因過濾改變(`withoutShadowedLegacyCopies` 對完整 load 上無法唯一定位的那一份補上 `unwritableReason`);以批次為單位拒絕的寫入者不在此列。歸為讀數而容易被誤認成寫入面的兩格,理由寫在這裡:`create-entry`/`akashic_create_entry` 的 `doiHits` 只回報、不拒絕(#637),不決定寫什麼;`view show --keys-only` 輸出一個 view 的外延,下游腳本拿它去寫是那個腳本自己的寫入面,而被拿掉的只有改名前的舊 citekey——它指的 legacy 檔在刪掉之前任何寫入面都定位不到(#641)。(1) `akashic_enrich`/`enrich` 以 DOI 定位而命中兩筆以上時,哪一筆是 legacy 拷貝只問 load 的標記(`fileSituation.shadowedLegacyFile`,判準只有 `markLegacyCopiesShadowedByEntities` 一份):被標成拷貝的,理由點名它的 legacy 檔與 entities/ 那份、帶「確認 entities/ 那份是新的之後」;同 id 而沒被標的(兩份都是 legacy),說 UUID 重複是 error、指向 validate,不下「是不是同一篇」的結論;拿掉拷貝之後仍有兩筆以上照舊指 #459。先前以 id 相等當代理,對兩個共用 UUID 的 legacy 檔也說「不是兩篇作品:刪掉 legacy 那份」。兩面同一個 core plan,回應的鍵與 `ambiguous` 不變,tools/list 不變。(2) CLI-only:三個 bootstrap 的附註與 `bootstrap-people --json` 的 `legacyCopiesInPlan` 只數流進該命令 literal 來源的拷貝(`BootstrapPlanSource`:people/venues 數 work,organizations 數 work 與 person);使用者 2026-10-05 裁決 2:計畫讀到拷貝時 `--apply` 整批拒絕、零寫入(`refuseApplyWithPlanBlockers`,結束碼 1,~~逐份點名 legacy 檔~~ 清單在乾跑——見下一段),乾跑照常列出並說明。(3) CLI-only:`authorize-names` 的寫入集合裡有任何一筆的 id 還有 legacy 拷貝時 `--apply` 整批拒絕——先前只擋「legacy 那份本身在寫入集合裡」,改名一對而 legacy 那份已有 authorized 時只寫 entities/ 那份;`blockedKeys` 一併列出。(4) CLI-only:`library check`/`set-kind` 依據不明確時不建議逐筆 `library remove`(MCP 的 `check` 沒有這句建議,不變)。 - -**#709 b36 verify(2026-10-09),各列裁決不變、兩面契約有兩處改、CLI-only 三處改;使用者 2026-10-09 兩項裁決落地**。(1) `akashic_enrich`/`enrich` 的 DOI 歧義理由:命中以完整的 load 算(`plan` 先前以 citekey 為鍵的字典、同 citekey 後者勝,load 先 entities/ 後 legacy,同 citekey 的拷貝把 entities/ 那份擠掉——理由說它「不帶這個 DOI」、叫人刪拷貝「再跑」而真孿生仍在;entities/ 帶 DOI 而拷貝沒有時回 notFound、同一個 store 的 `create-entry --dry-run` 卻說「DOI 已在」);結論與處置(#459、UUID 重複、確認 entities/ 那份是新的之後再刪)排在逐份明細之前,出口 `displaySafe(max: 512)` 截掉的只會是明細。`matches` 同一個 citekey 只列一次。(2) MCP 每一列的 `laterWriteNotApplied`(與 CLI 逐筆列、App 列同一句 `laterWriteRefusedNote`)帶「確認 entities/ 那份是新的之後」、不說「即可」——那一列沒有 explanation 的標題可依;鍵不變,tools/list 不變。(3) CLI-only:三個 bootstrap 的拒絕條件是 `LibraryLoad.planBlockers`——計畫讀到的種類裡 load 標出的拷貝 ∪ 無法唯一定位的記錄(works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`,裁決 2,與 `authorize-names` 一致);`bootstrap-people` 的計畫也讀 person 拷貝(裁決 1——~~person 拷貝只會讓候選少一個~~:拷貝的否決、名字與舊 key 可以讓候選變多、讓 literal 安靜消失、讓新 key 多後綴)。乾跑逐份、逐筆點名,`bootstrap-people --json` 另帶 `unlocatableInPlan`;`--apply` 的拒絕只說筆數、處置與去哪裡看——清單先前被單行出口逃成字面 `\u{000A}`、在 400 處截斷。(4) CLI-only:`authorize-names` 的拒絕點名 entities/ 那份的 key 時,接上它的 legacy 檔(validate 以 legacy 那份的 key 列出兩份並存),清單排在處置之後。(5) CLI-only:結束碼 0 與非零的 stderr 那一行,「要重跑」那一句不再自己說一次無前提的刪除。 +本段已搬到 [`docs/rulings/mcp-cli-parity.md`](../../docs/rulings/mcp-cli-parity.md)(不自動載入);需要查過往裁決或新增一列時讀那份檔。本檔其他段落說的「下表」「上表」「這張表」指那份檔裡的表。 新增下一個工具 = 在這張表加一列。**不得依性質相似類推**「這個工具顯然不用 CLI」 ——那個判斷要寫成表裡的一列(含理由或 issue 編號),不能只存在腦中。 @@ -249,37 +99,7 @@ pre-flight)的操作,MCP 的 LLM 消費者不是該角色;**候補缺席** 消費流程,需求出現即重啟裁決(不是永久判死——重啟訊號刻意不形式化,那是使用 情境判斷)。 -| CLI 能力 | 裁決 | 理由 | -|---|---|---| -| `view`(list/show) | 候補缺席 | 外延查詢對 agent 有潛在價值,但目前無 MCP 端消費流程 | -| `resolve-divergence` | 有理由缺席 | in-code 既有裁決(`AkashicService.recordDivergence` doc):消歧含合併+全庫改寫+刪檔,tracked+clean 前提與人工確認屬 CLI/App 互動面。**#553(2026-09-11)重新確認,裁決不變**:該輪讓本命令多收一個 shape(venue),而缺席的理由是**操作的性質**(不可逆、要乾跑逐筆過目、要 git 工作樹乾淨),與它收幾個 shape 無關——多一個 shape 不會讓那三件事對 LLM 消費面變得可承擔。`record-divergence` 那一面在 MCP 表已有對應(`akashic_record_divergence`),本輪的 `byShape` 擴充**兩面同時生效**,沒有新的不對稱。**#558 R2(D86)重新確認,裁決不變、契約有改**:tracked+clean 前提自此涵蓋**會被改寫的檔**(倖存者、被改指的 entry、塌縮/改名的歧異記錄),不只被刪的——「要乾跑逐筆過目、要 git 工作樹乾淨」那三件事因此對 LLM 消費面更不可承擔,缺席的理由變強不變弱。**#558 R4(D87/D88)再確認,裁決不變、契約有改**:閘多兩種拒絕——HEAD 解析不到(unborn、orphan 分支、HEAD 指向不存在的 ref,各自的訊息)與 index 標了 `assume-unchanged`/`skip-worktree`(R1–R3 三代都對它 fail-open,R3 verify security 席真 binary 刪檔重現);拒絕清單的上限改成自己的常數,超限時補救句叫人一次 commit 全部。**#565 重新確認,裁決不變、契約有改**:venue 合併的名字併入改成整段搬、被併者原本在 variant 的才標 variant、同名段不能並存時具名拒絕零寫入——多一種拒絕、少一種靜默丟失,缺席的理由(操作的性質)不變。**#565/#587 R1 verify 重新確認,裁決不變、契約有改**:(a) 三方合併的 variant 分類與提醒不取決於被併者順序(倖存者原本沒有的名字,任一被併者列在 variant 就標;「倖存者已有」只對原本的倖存者判斷);authorized 降級的提醒依最終結果;(b) 名字併入是線性的;「完全相同的段」比 source/note 的 UTF-8 位元組,不再用 Swift `String ==`;(c) 不能並存的同名段清單至多列 20 組並給總數,出路依衝突的另一方(倖存者/先併入的被併者)分開寫;(d) 被併者 `field: issn`/`names` 的 reference 逐位元組搬到倖存者(報告 `referencesCarried`,dry-run 也預告),`paginated` 等其餘欄位照舊拒絕並具名;(e) 同一個 ISSN 的角色(兩邊不同、或被併者有而倖存者沒有)具名拒絕。**#709(2026-10-01/10-02)重新確認,裁決不變、契約有改**:同一筆記錄的 legacy 拷貝(同一種、同一個 id)降為 warning 之後,這個命令的拒絕改由兩處擋——候選有 legacy 拷貝(被併者只刪 `entities/.yaml`、legacy 那份會復活)具名拒絕(`StoreIOError.legacyCopyPresent`,在版控檢查之前),以及**會被改指的 entry** 在第一次寫入之前過 #631 的寫入前置(`assertRewriteTargetsWritable`,與 `commitResolution` 的 `entryWritePlan` 同一個函式;#709 R3 verify:先前只有 `commitResolution` 在擋,`--dry-run` 對「第三筆記錄引用候選、自己有 leftover」印「參照將改寫」而實跑被拒)。兩處都在 preview 與實跑共用的 `validateResolvePreconditions` 裡,兩邊擲一樣的錯;`assertNoCrossRecordErrors` 只剩兩筆**不同**的記錄共用 citekey/key/UUID 的 error。缺席的理由(操作的性質)不變 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:合併搬名字分類記錄按被併者的順序接、每一筆只與那個名字那個分割此刻的最後一筆比(先前以整份歷史去重,倖存者會以撤回結尾而名字仍是對外形);合併後最後一筆與分類矛盾、而那是這次帶進來的,preview 與實跑都拒絕(`wouldContradictClassificationTail`);person 記錄的名字要在合併後的 names 裡(只差空白的拼法 preview 與實跑都拒,先前 dry-run 說搬、實跑在寫入閘失敗);`--dry-run` 的「將隨合併遷移」印出每筆的 statement、依接上去的順序。缺席的理由不變——拒絕多了、預覽說得更多,對 LLM 消費面只是更不可承擔 **#564 b37 重新確認(b36 Y1 第 11/12 列),裁決不變、契約有改**:被併者自己的名字分類記錄就與分類矛盾(手改,或修正輪之前的工具)時,preview 與實跑一律拒絕、說出那筆被併者——先前看全部接完之後的最後一筆,被併者依 key 排序,同樣兩份歷史只換 key 拼法就從放行翻成拒絕;venue variant「不在 variant 卻以指定結尾」的出口改成兩邊都 `--add-variant`(只在被併者上做會被「分類不同」拒絕)。缺席的理由不變 | -| `bootstrap-people` | 有理由缺席 | 批次建檔屬操作者規模;單筆由 `akashic_add_person` 覆蓋(#250)。**#547 重新確認,裁決不變**,但**理由要改寫**:該輪新增的是同一命令的報告欄位(`pendingMutual`)+ 一個 `--json` 出口。上一版的理由是「**不是新能力**,所以不觸發本檔的義務」——那是**用重新分類迴避問題**。誠實的版本是:本檔的機械稽核 ② 枚舉 subcommand、④ 枚舉橫切 `ParsableArguments`,**兩者都枚舉不到 per-command 的 `@Flag`/`@Option`**(實測全 CLI 38 個 `@Flag`、91 個 `@Option`)。那是**收錄機制的缺口**,跟「這算不算新能力」無關——一個 per-command 旗標若真的長出 MCP 面該有的能力,今天沒有任何程序會發現。本格的裁決仍是缺席,理由與 #388 的 `--rows` 同型:`--json` 的消費端是把組別餵給 `add-person` 的人工消歧迴圈,而 MCP 面的輸出進 LLM context、呼叫端無法在收到後丟棄已付的代價;要一次讀完 3,000+ 筆的是操作者,不是 LLM。**缺口本身另案追蹤:#551。** **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名;~~`--json` 的 `legacyCopiesInPlan` 只數 work 拷貝~~——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:person 拷貝也在計畫裡(`legacyCopiesInPlan` 也數它),無法唯一定位的記錄(work 與 person)同樣讓 `--apply` 拒絕,`--json` 另帶 `unlocatableInPlan`;拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | -| `bootstrap-organizations` | 有理由缺席 | 批次建檔屬操作者規模(同 `bootstrap-people`)。原第二理由「org 建模先停(#63/#70)」已由 #304(2026-08-16)廢止——org 重啟後**單筆**建檔的 MCP 面已於 venue change 補齊(`akashic_add_organization`,見 MCP 表);批次面維持 CLI-only **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work 或 person)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:無法唯一定位的 work 與 person 同樣讓 `--apply` 拒絕;拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | -| `bootstrap-venues`(#367;#548 重新確認;#554 R6 重新確認;#563 重新確認) | 有理由缺席 | 批次建檔屬操作者規模(同 `bootstrap-people`/`bootstrap-organizations` 的既有裁決)。**三個 bootstrap 命令的裁決一致不是巧合**:它們的共同形狀是「掃全庫的 literal、按門檻建實體、dry-run 供人審」,而那個規模與審閱動作屬操作者,不是 LLM 消費面。單筆建檔的 MCP 面已有 `akashic_add_venue`(見 MCP 表)。**#548 重新確認裁決不變**:該輪新增的是同一命令的報告欄位(`pendingResolution`——與既有 venue 寬鬆共鍵的群)與一道扣留,不是新命令、也不是新能力面;`bootstrap-organizations` 同批同形。與 #547 對 `bootstrap-people` 的處置一致。**#554 R6/R7 重新確認裁決不變**:該輪讓報告多一個 `reason` 欄位(`Dropped.reason`——被謂詞拒的 literal 從「零字」變成印出並附理由,與產不出 key 的分得開)並把「已有 venue」的檢查移到謂詞之前(只差一個 Cf 的既有刊名 literal 走 resolve-venues 歸戶、不印成「修來源欄位」);仍是同一命令的報告欄位與扣留順序,不是新能力面(R6 verify 第 11 列抓到沒記)。**#563 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決):建檔不再寫 `authorized: [names[0]]`、留空——與 `add-venue` 的 #227「建檔不機械偽造」一致,`displayName` 退到 names 的第一段、顯示不變;指定對外形走 `update-venue --authorize`/`akashic_update_venue.authorize`(兩面都有)。既有的機械值不動(#600 逐本判定)。`doctor` 的 `no authorized name` 只數 person/organization,這一改不多出任何診斷——同一個事實也表示新建 venue 的空 authorized 沒有任何掃描面(`docs/store-format.md` §3.1 照實寫),所以 R1 verify 之後乾跑與 `--apply` 的輸出各多一句:新建的 venue 不指定對外形、指定走 `update-venue --authorize`。另一個回報:倖存者的 authorized 為空時,合併的降級提醒句改說「倖存者沒有這個書寫系統的對外形」,不再說「對外形不變」 **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:無法唯一定位的 work 同樣讓 `--apply` 拒絕(person 不在這個計畫裡);拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | -| `fmt` | 有理由缺席 | 全庫改寫=維運例外 | -| `migrate` | 有理由缺席 | 格式遷移=維運例外 | -| `migrate-provenance` | 有理由缺席 | 同上 | -| `migrate-person-identity`(#227/#241) | 有理由缺席 | 格式遷移=維運例外(同 `migrate`/`migrate-provenance`);且不可逆、要求 store 工作樹乾淨的人工 pre-flight,MCP 的 LLM 消費者不是該角色 | -| `migrate-venues`(#304 venue change) | 有理由缺席 | 格式遷移=維運例外(同 `migrate` 族);per-file trackedness pre-flight+部署鏈(release → migrate → validate → 手動 bump format 11)屬操作者角色 | -| ~~`migrate-work-types`~~(#325,**已退場**) | 有理由缺席 → 退場 | 格式遷移=維運例外(同 `migrate` 族);不可逆、要求檔案受 git 追蹤的人工 pre-flight,MCP 的 LLM 消費者不是該角色。**#325 階段二起命令不存在**——它讀不到舊值(舊值在階段二的 decode 就被拒),留著只會是一個永遠無事可做卻看似可用的命令(`no-compat-fallback` 的「退場即刪」)。列保留但劃掉:刪掉會丟失裁決史,而那正是本檔存在的理由 | -| ~~`migrate-venue-variants`~~(#422,**#567 已退場**) | 有理由缺席 → 退場 | **#567(2026-09-29)命令、實作與測試刪除**(`no-compat-fallback` 第 3 條,形狀同 `migrate-work-types` 列):補集規則在 #554 之後為假、live store 的遷移工作已歸零,而 D41 之後它只剩一個永遠拒絕的 `--apply` 與一個唯讀乾跑——看似可用、永遠無事可做。一併裁掉的:`variant` 的 format 14 寫入閘自此補上(`LibraryStore.assertVenueWritable`——「有沒有 pre-bump 遷移」的答案變成沒有,設閘零成本;format-13 store 零實例);仍在 format 13 的 store 要標異寫,先 bump 再用 `update-venue --add-variant` 逐筆標。列保留但劃掉:刪掉會丟失裁決史。以下是退場前的記錄。**2026-09-15(#554 R14 verify regression 第 9 列)**:只用散文禁止的命令仍可執行、會靜默推翻 D1 的未標——CLI 的 `--apply` 自 R15 起 throw(零成本閘,#567 落地前的中間項)。**2026-09-12(#554 R2 verify 第 3 列)**:它的補集規則「names − authorized → variant」在 #554 之後為假——`--authorize` 刻意把換下來的舊指定留在未標(D1),而那筆記錄恰好滿足它的四個條件;live store 的遷移工作已歸零。依 `no-compat-fallback` 退場即刪(同 `migrate-work-types` 列的形狀),本列在 #567 落地時劃掉。原裁決:格式遷移=維運例外(同 `migrate` 族);不可逆、per-file trackedness pre-flight、且**手動 bump format 是分開的一步**(13 → 14)。**這一列不是從 `migrate-venues` 類推來的**:它多一個該族沒有的性質——它做的是**重新分類**而非搬值(`names` 的名字不動,只是多一個 `variant` 標記),所以誤標的出口是**乾跑逐筆過目**而不是 rollback。實測 35 筆,人工可行 | -| `migrate-identifiers`(#394) | 有理由缺席 | 格式遷移=維運例外(同 `migrate` 族與 `migrate-venues`);不可逆、要求每個將被改寫的檔**自身**受 git 追蹤的人工 pre-flight,MCP 的 LLM 消費者不是該角色。**這一列不是從 `migrate-venues` 類推來的**:它多一個該族沒有的性質——它會**跨記錄搬動資料**(work 的 `issn` 移位到它的 venue),所以一次失敗的部分寫入會讓兩邊都不對,而不只是某些檔沒遷到。乾跑是預設、`--apply` 才寫,且乾跑刻意**不**受 #298 的破壞性閘管制——它不寫東西,且正是用來確認目標的手段 **#641 重新確認,裁決不變**:無法唯一定位的 work(`unlocatableCitekeys` 三類)整筆列進 blockers、不寫——它的 ISSN 照樣進 venue 計畫,號不會消失;先前兩份並存的那一筆在寫入迴圈中途才被 #631 拒,前一筆的 ISSN 已從 work 移除、venue 還沒寫,重跑救不回。 **#669 重新確認,裁決不變**:venue key 有不只一筆記錄、而它是 ISSN 的搬移目標時,乾跑與 apply 都在任何寫入之前拒絕(ISSN 要搬進哪一筆無法判定)。 | -| `repair-venue-names`(#575) | 有理由缺席 | 維運例外:它改寫既有記錄,而被改寫前的位元組只剩 git 那一份——`--apply` 要求那些 venue 檔已 commit、乾淨(`assertRecordsRecoverable`),乾跑逐筆過目「venue/清單[index]:before → after」之後才寫;那是在檔案系統與版控旁的操作者角色,MCP 的 LLM 消費者不是。**這一列不是從 `fmt` 或 `migrate` 族類推來的**,差別有兩個:(1) 它改的是名字的**內容**位元組(NFC、空白收斂),不是序列化或格式——`fmt` 對帶 error 的記錄刻意拒絕(它的語意是 canonical 序列化,不修內容,#554 R6),而這正是 #575 立案的原因;(2) 它有一半輸出是**不動**的:要判斷的違反(不可見或控制字元、無字母數字、近重複、正規化後仍不合法、有 reference 指著舊拼法)只具名與理由,給人讀。也不是候補缺席:它修的是舊 binary 與手改留下的違反,MCP 的寫入工具寫不出非 canonical 的名字(`vetVenueNames` 先 canonical),沒有 LLM 流程會產生需求。**有記錄的面不對稱**:名字不變式的 error 經 `akashic_doctor` 的 `recordIssues` 到得了 MCP 面,那則訊息指向這個 CLI 命令——修復面只有 CLI 一面是這一列的裁決,不是遺漏。乾跑不受 #298 閘管制、`--apply` 受(`DestructiveTargetGate.destructiveCommands` 同批加入) | -| `copy-zotero-attachments`(#606) | 有理由缺席 | 維運例外+批次屬操作者規模:它走訪 Zotero 資料目錄的 `storage/`、一趟把全部 `zotero:` 附件的位元組複製進 `sources/` 並連到 `akashic.sources`(追加)——實跑要求被改寫的 work 檔已在 git 裡 commit、乾淨,且 `sources/` 已被版控排除(第三方版權位元組不得進 remote,`replace-endnote-and-zotero` 的承重閘),乾跑逐筆過目後才 `--apply`;那是在檔案系統與版控旁的操作者角色,MCP 的 LLM 消費者不是。**兩面的能力沒有缺**(判準是「可用性該不該取決於用的是哪一面」):單檔的同一件事 MCP 面本來就有——`akashic_store_source`(收檔案路徑,存位元組與取得記錄)加 `akashic_update_entry` 的 `add_sources`(#614,連到 `akashic.sources`);CLI 這一格是同一條路徑的批次形(同 `create-entry` 陣列形的 #455 裁決:兩面同一條實作,缺的只是「一次送多筆」與「自己走訪 Zotero 的 `storage/`」)。**這一列不是從 `import-zotero` 類推來的**:`akashic_import_zotero` 唯讀 `zotero.sqlite`、只寫記錄;本命令複製第三方檔案的位元組,代價落在 `sources/`(不進 git、換機器要重取)與磁碟。日後若出現 LLM 流程需要一次複製上百個檔,重新裁決的是 MCP 面的位元組預算與阻塞(`--rows` 那格的同一個論證),不是加參數。CLI 面過 #298 的目標確認閘(`WriteGateRulings`:`--apply` 才閘,乾跑不閘);**不改寫 `attachments`**——它是 Zotero 擁有的區塊(§2.5),pull 會還原,且它同時是副本的來源記錄。 **#606 R2 verify 重新確認,裁決不變、契約有改**(CLI 一面):位元組在而 index 沒有條目的已連 digest 另列「已補記」(`recordRestored`,補上取得記錄、不改連結);補存擲錯另列(`restoreFailed`,照樣非零結束),不再記進 work 的 `writeFailed`;只補存時標題不說「已寫入」、結尾不叫人 commit;丟棄的取得記錄逐行說位元組是早已在還是這一次才存。缺席的理由不變——單檔的同一件事 MCP 面仍是 `akashic_store_source`+`akashic_update_entry` 的 `add_sources`。 **#703 重新確認,裁決不變、契約有改**(CLI 一面):超過 256 MiB 的附件逐檔具名略過(印路徑與大小,乾跑就印);digest 與複製逐塊;已在 `sources/` 的同一個 digest(已連過、要補記、新連結遇到既有存檔)逐塊比對內容,不符另列「內容與 Zotero 原檔不符」(`storedBlobMismatch`)——不覆寫、不補記、不新連,`--apply` 遇到時非零結束。**R1 verify 之後**:新連結遇到位址上是目錄、symlink 或讀不到的存檔也具名略過、不連(先前只擋內容不符,照樣連上並報「已複製」);同一筆內內容相同的第二個附件跟著第一個的結果(不再一律算已連過);存檔前置在第一次寫入之前也問暫存檔的路徑。缺席的理由不變:MCP 面的單檔對應 `akashic_store_source` 同批加了同一個上限。 | -| `validate` | 有理由缺席(理由於 #416 換過)| **原理由「讀取檢查由 `akashic_doctor` 覆蓋(功能重疊)」被量測否掉**:`Entry.validate()` 在全樹只有一個呼叫點(CLI),`doctor()` 與 App 面各 0。**落差是 warning 一族**(#416 R1 更正:原本寫「有一條是 error 級」,實測那條到不了——load 的 quarantine 先擋下,而 quarantine 本來就在 `StoreHealth` 裡)。#416 把 per-record 驗證抽進 `StoreHealth.perRecordIssues`,兩個消費面因此都拿得到——**現在**才真的重疊。CLI 面維持獨立命令的理由改為:它**不加面級截斷**、逐則列出,而 MCP 面截斷 20 則(輸出進 LLM context,呼叫端無法在收到後丟棄已付的代價,#236 的既有威脅模型)。這是**呈現粒度**的差異,不是能力缺席。**#554 R24(D66)重新確認,裁決不變、理由收窄**(R23 verify Codex 第 2 列):R14–R26 陸續在 `validate()`/`StoreHealth` **產生訊息時**就套了 per-record 上限(`Entry.perRecordWarningCap`),**只在組合式的六族**:venue 名字內容、venue 的 names/authorized/variant 近重複、**person 近重複**(R26 D72、#576)、重複 venue 邊、confirmed literal、重複判定記錄——各族每筆記錄至多 20 則加一句概括;venue 名字近重複與 person 近重複另各有**整筆 100,000 對**的求值總量上限(R18 D52/R27 D76:venue error 級整筆拒絕、person warning 級只計數並自 R28 起小組先評估,R27 verify 第 25 列指出這一格只列了四層)。那是**求值的安全上限**(讀取路徑對未信任的 store 內容跑,R14 verify security 第 18 列):這六族的則數是每筆記錄的**組合**(配對數/組數),會被一筆記錄撐爆;死 verdict、矛盾 verdict、本機缺存檔、拆分後孤兒 verdict、拆分/移除記錄、記錄檔逼近讀取上限(venue 族與 person/organization 族)、沒有合併管線的歧異記錄、重複的 reference(#582,每組一則)這幾族**沒有上限**(2026-09-27 #582 R1 verify:這份名單先前漏了中間三族,#582 又加一族),它們的則數是每筆 reference/配對/記錄/組各一則——與那筆記錄持有的資料項數**線性**(1,352 筆 verdict 的 holder 全退役就是 1,352 則,不是「每筆至多一則」),上限的理由(組合數遠超資料項數)對它們不成立、上界就是資料項數(R25 D70 → R26 D72;R24 verify 第 12/14/37 列:R24 在 App help 寫成「每筆記錄每族」、對五族以外為假;R25 verify DA 第 2 列 HIGH、第 6/11/15 列:R25 寫「五族」漏掉 person 近重複——它當時是組合式且無上限、真 binary 一筆記錄 19,900 則——又寫「其餘線性、每筆至多一則、撐不爆」,對六族全假。兩輪各換掉一句假話又寫了另一句:這一格的措辭自此逐族點名、不再用「其餘」)。上限所在的三個面共有——所以「逐行、無截斷的完整報表」這句在那之後為假:被截的記錄在 CLI 也只有概括句。~~**不加「完整列舉模式」**:那等於替 CLI 開一條繞過求值上限的路,而上限存在的理由正是讀取路徑不能被 store 內容撐爆;概括句說了「另有 N 個未列出」,沒有東西是沉默的;要全部只能讀 YAML——**那個缺口記成 #581**(R24 verify 第 5/17 列:`replace-endnote-and-zotero` §4 要求能力缺口記 issue,不得靠「讀 YAML」帶過;出口的形狀要在本檔兩面一起裁)。~~ → **#581 裁決(使用者 2026-09-28):出口是單筆完整明細,不是全庫的完整列舉模式**——上一句拒絕的那種(對整份 store 放寬)仍然不做。`validate --owner :` 對**指名的一筆**記錄不套組合式六族的列出上限(`PerRecordListing.full`):上限存在的理由是對整份未信任 store 求值的界限,呼叫端指名一筆之後,輸出的上界是那一筆自己的組合數,不重開那個洞。**不放寬的三樣**(寫出來,免得「完整明細」被讀成「沒有任何上限」):近重複的組內 5,000 對與整筆 100,000 對**求值**上限(觸頂時概括句照樣出現、不再說「每筆記錄最多列 20 組」——那句在完整明細裡是假話);訊息內部的列舉上限(一組列 3 對、一則列 5 個拼法);MCP 面的位元組預算。kind 必填、不猜(跨 kind 同 key 實測 2 個:organization×venue),找不到或同 kind 同 key 兩筆以上都拒絕;範圍是那一筆的 per-record 問題(含掃描族裡 owner 是它的那幾則),跨記錄檢查與 quarantine 不屬於任何一筆、不在範圍內,輸出末行說出這一點。MCP 的對應是 `akashic_doctor` 的 `owner`(見 MCP 表那一列:受位元組預算、`total`/`truncated` 揭露);不帶 `--owner` 的 `validate` 在有記錄被截時多印一行「被截的記錄: N」並指路。`ValidatePerRecordCapCLITests` 走真 binary 釘住這個契約(不帶 `--owner`:25 個配對 → 恰 20 則 +「另有 5 個配對未列出」+指路行;帶 `--owner`:25 則全部具名、沒有概括句)。CLI 與 MCP 的差別自此只剩**面級**截斷有無 | -| `rename` | 有理由缺席 | 高風險身分操作(citekey 遷移含 verdict value 重寫,#232)=維運例外。**#642 R1 verify 重新確認,裁決不變、契約有改**:改名同批遷移 library 的成員規則(第 16 條邊:文件型的文件、規則型的排除清單),registry 檔要 tracked 且 clean、與其他被改寫的記錄同一批預檢;新 citekey 已被規則指涉或被隔離的 registry 檔提到時拒絕;遷移逐條列在輸出裡(CLI 一段、App 回執一行)。App 的改名(第三面)走同一個 `renameEntry`,回執多一行「library 成員規則已遷移」 | -| `rename-person`(#395) | 有理由缺席 | 同 `rename` 的理由,**而且遷移面更大**:除 verdict value(`person:` holder,掛在 person、organization 與 venue 三處——R21 verify 第 35 列:這裡曾寫「兩處」,#463 起 venue 迴圈也在)外,還含全庫 `authors[].key` 與 divergence 的 `candidates`/`judgement.prefers`。**這一列不是從上一列類推來的**——它的參照集合是照 `entity-backlink-completeness` 的封閉列舉逐條窮舉出來的(第 1、9、10、13 條),與 citekey 的那組**不重疊**。維運例外的理由因此更強而非更弱:漏一格的後果是安靜的(檔案照樣載入,只是某些邊指向不存在的 key) | -| `authorize-names` | 有理由缺席 | 批次策展=操作者規模 **#564 重新確認,裁決不變、契約有改(BREAKING)**:`--apply` 必附 `--judgement`(整批一句,早於開 store 的用法錯誤;乾跑不需要),每個寫入的 person、每個被採用或提名的名字各寫一筆 `field: authorized` 的 `指定:理由`(不帶證據),需要 store format ≥ 22;**不再寫 store marker**(升 marker 是使用者的動作)。本面維持 CLI-only:批次策展屬操作者規模。~~名字分類的單筆 person 面不存在(person 的 authorized 經 `authorize-names` 或手改,`update-person` 的 references 明文拒收名字分類記錄)~~ → **#564 R1 verify(b26 F2 第 2/5/11 列)指出這句為假**:`update-person` 的 `fields.names` 全量替換一直寫得進 authorized,只是不附理由、不留記錄。使用者 2026-10-02 裁決(#564 第 1 點)把它補成單筆的名字分類面——見 `akashic_update_person` 那一列;`update-person` 的 references 仍拒收名字分類記錄(只經名字分類面寫)。批次的 `authorize-names` 不設一次 200 個名字的上限(#564 第 4 點):它每個 person 至多寫三筆(每書寫系統一個對外形),上限防的是單筆記錄被同一句理由灌爆,這裡不會發生。 **#641 重新確認,裁決不變**:寫入集合裡有無法唯一定位的 person(`unlocatablePersonKeys`)時 `--apply` 在第一次寫入之前整批拒絕、零寫入——先前逐筆寫到一半才被拒,留下已指定卻沒 bump marker 的 store;乾跑照常出報告。 **#709(2026-10-05)重新確認,裁決不變**:寫入集合裡有任何一筆的 id 還有 legacy 拷貝時同樣整批拒絕、零寫入(`blockedKeys` 一併列出)——先前只擋「legacy 那份本身在寫入集合裡」。**#709(2026-10-09)重新確認,裁決不變**:點名 entities/ 那份的 key 時接上它的 legacy 檔(`blockedCopyFiles`)——validate 以 legacy 那份的 key 列出兩份並存。 | -| `export-tables --view`(#274) | 有理由缺席 | 匯出物是檔案樹,MCP 的回傳形狀未定——#274 註記的正式落位 | -| `file add`/`file remove`(#700 R1 verify 第 9 則) | 有理由缺席 | **註冊與除名 store 是改 registry——部署層的名冊,不是呼叫層的能力**,與橫切選項表 `--config` 那一格同一個理由:讓 LLM 消費者改寫「有哪些 store」這個部署決定。MCP `akashic_files` 收 `list` 與 `use`——`use` 只在名冊**內**切換 session 的 active store、不改名冊(`WriteGateRulings` 把 `file use` 記為只改 registry 的 current);`file add` 寫 registry 並確保佈局(缺的目錄與格式標記才建立——新建一個 store 時是整個佈局;另建化身 id 檔 `incarnation`;既有的 `.gitignore` 沒有 sources 標記區塊時在尾端以位元組附加一段,#66 的排除,原有的位元組不動——b29 V5 LOW 13:先前寫「在佈局不存在時建立」,比行為窄;#700 b31 W5:「既有的行不動」先前只對可讀的 UTF-8 一般檔成立,讀不懂的檔整份被換成只含區塊、symlink 被換成一般檔,現在讀不懂的(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)在建立任何東西之前具名拒絕、不寫 registry),`file remove` 只除名、不刪任何資料。上方 MCP 表 `akashic_files` 那一列的 ✅ 是 `file` 家族的 list/use 功能重疊,**不含** add/remove——#700 的逐腿守衛(`ToolPayloadLegs.commands`)找到這個落差時,它只寫在測試註解與 changelog,這一列補上裁決。**不是維運例外**(沒有不可逆操作:add 只新增一筆名冊、同 key 或同路徑已註冊時具名拒絕、不覆寫(b26 F6:先前這裡與 CLI help 都寫「add 冪等」,不成立——重跑同一個 `file add` 會以「已存在」拒絕,只有佈局的確保是冪等的——標記區塊已在就不再附加);remove 只除名、不刪資料),**也不是候補缺席**:沒有 LLM 流程需要在 session 內註冊 store(store 由伺服器啟動時的環境決定)。**觸發條件**:出現需要在 MCP session 內註冊新 store 的流程時重開——那時要裁的是要不要給 `akashic_files` 加 `add`/`remove` action,並一起重裁 `--config` 那一格 **#700 b33 verify**:不改寫的再加四類(同 `akashic_doctor` 那一格);開檔、寫入時才發現的原因在佈局建好之後拒絕、同時跑的程序加好區塊時照常成功——`file add --help` 寫明。 **#700 b36 verify**:判讀同 `akashic_doctor` 那一格的 b36 補記——`.gitignore` 是 symlink(不論指向的內容有沒有區塊)、規則行帶行首空白或行尾 tab、等鎖逾時,`file add` 一律拒絕(先前 symlink 指向有區塊的檔、規則行帶前後空白時照常成功,而 git 不排除 `sources/`);`file add --help` 寫明。 | -| `create-entry` 的 JSON 陣列與 `library` add/remove 的多 citekey(批次語意,#455) | 有理由缺席 | **批次屬操作者規模**(同三個 `bootstrap-*` 的既有裁決):一次 load、批次內消解 citekey 碰撞、可預期失敗整批擋零寫入、I/O 逐筆收容、一次 rebuild,是目錄匯入(1545 筆量到 O(n²))的使用形;MCP 的 `akashic_create_entry`/`akashic_libraries` 維持單筆——LLM 消費面逐筆顯式指名,且兩面**同一條實作路徑**(單筆是 `createEntries([draft])`/`setMembership(citekeys: [ck])` 的薄包裝),所以缺的只是「一次送多筆」的參數形狀,不是能力。日後若出現 LLM 流程需要一次建千筆,重新裁決的是 MCP 面的**位元組預算**(#388 的同一個論證),不是加參數 | -| `references`(`extract`/`nominate`;#617) | 有理由缺席 | **skill 的內部步驟,不是 store 能力**:輸入是 `akashic-work-references` 經本機工具(`pdftotext`)與 safari-browser 取得後寫下的暫存檔,輸出是下一步(模型逐筆判定)的中間資料;不寫 store(`nominate` 唯讀查候選的 DOI 是否已在庫,那是這次比對的一個欄位,不是對外提供的查詢能力)。開 MCP 面等於要 LLM 呼叫端把 PDF 全文當工具參數送進來——呼叫端為大輸入付代價,與 #388 位元組預算論證同一個方向。不是維運例外(沒有不可逆操作),也不是候補缺席(缺的不是消費流程:唯一的消費流程就是那個 skill,而它在 shell 裡直接呼叫 CLI) | -| `literal-census`(#629,由 literal-census.sh 移植) | 候補缺席 | **唯讀量測,是 `akashic-promote-literals` skill 每輪開場與收尾各跑一次的步驟**(進度數字落一筆 issue comment);目前唯一的消費流程就是那個 skill 在 shell 裡直接呼叫 CLI。MCP 的 `akashic_doctor` 給的是 store 健康計數(含 `unresolvedAuthorLiterals`),口徑不同:普查是**四域**(author/venue/affiliation/org-parents)三個口徑(總邊/literal 邊/distinct),而且要在**讀端拒開的 store 上照樣出數字**(不經 `openStore()`,marker 壞掉或版號太新正是它標「這一輪的數字不能拿去定批次範圍」的情形)——兩者不重疊。**不是維運例外**(沒有不可逆操作)。需求出現(例如有 LLM 端自動跑 campaign 的流程要讀進度)即重啟裁決 | -| `scan-yaml-profile`(#629,由 scan-yaml-profile.py 移植) | 有理由缺席 | **repo 開發者重測 #33(store YAML 輸入 profile)決策的證據工具**,不是使用者能力:對象是**檔案樹**(任何一個 store 目錄,含讀端拒開的),輸出是給人讀的統計表,用途是「corpus 有沒有長出 profile 外的語法」。MCP 的 LLM 消費者不做 corpus 遷移成本評估。不是維運例外,也不是候補缺席——沒有需要它的消費流程。**#629 R1:型別放在 `AkashicCore` 但存取層級是 `package`**(同一個 package 的 target——CLI、MCP、AppKit、測試——看得到,package 外的使用者,例如 Xcode 的 App 專案,看不到;R2 更正了先前「MCP 看不到」的說法),因為它不是使用者能力;根不是資料夾、讀不進來的檔、零個 `.yaml` 都具名失敗,不印全零 | -| `fulltext`(`verify`/`bot-signals`/`jitter`/`fetch`/`take`/`calibrate`/`contract`;#629,由 `akashic-fetch-fulltext/scripts/*` 與 `fetch-fulltext.sh` 移植;#613 刪 `url-rule`、加 `take`;#613 R3 加 `contract`) | 有理由缺席 | **取全文 skill 的內部步驟,不是 store 能力**:輸入是本機 PDF、頁面文字、使用者的 Safari 視窗與 Crossref 回應檔,輸出是下一步(agent 判斷、`store-source`)的中間資料或一個結束碼;**全部不寫 store**(`take` 只寫 `--out` 指定的檔,且拒絕寫進沒有 ignore 它的 git 工作樹——全文是第三方版權內容;#613 起 `fetch` 不寫任何輸出檔,只在 store 之外的每日嘗試帳本記一筆)。開 MCP 面等於要 LLM 呼叫端把 PDF 全文或使用者的瀏覽器 session 當工具參數送進來——呼叫端為大輸入付代價(與 #388 位元組預算論證同一方向),而 `fetch` 要動使用者的 Safari,那是使用者授權給**這個 agent 的這個 skill** 的動作,不是一個能對任何 MCP 客戶端開放的能力(中止條款要求出事時分頁留給使用者看)。不是維運例外(沒有不可逆的 store 操作),也不是候補缺席(缺的不是消費流程:唯一的消費流程就是那個 skill,而它在 shell 裡直接呼叫 CLI)。結束碼是對 agent 的契約,見 `FulltextFetch` 的型別註解。**#629 R1 重新確認,裁決不變、契約有改**:`fetch` 的 `--expect-profile` 命令列必填、`--landing`/`--prime` 只收 https、頁面 DOM 的 PDF 連結 origin 要與頁面相同、輸出目的地不得是目錄/symlink/特殊檔(覆寫為原子替換)、git 閘 fail-closed;`calibrate` 一個檔案都沒量到回 4、缺記錄列完整網址 **#629 R2/R3 重新確認,裁決不變、契約有改**(兩輪都只動 CLI 面,沒有 MCP 面可對):(a) R2:`--landing`/`--prime` 的主機要**長得像**一般網域名稱——不收 localhost、IP 位址、埠號、帳密與幾個已知的私有字尾;這是字面的形狀檢查、不解析 DNS,解析到私有位址的名稱照樣通過(R3 verify 更正「公開主機」的說法);整串不得有引號、反斜線、反引號、`$`、`#`、空白或控制字元(所以規則檔記的「對 `--landing` 加一次性 fragment」要先改這道檢查);頁面 DOM 的 PDF 連結要是頁面裡解析好的同源 `https://` 絕對網址;原子替換的暫存檔名也進 git 閘——只逐字忽略三個最終檔名的工作樹現在被拒;(b) R3:取 PDF 的請求被轉址到別的 origin 時不讀本文、結束碼 1(stderr `redirected off-site`;先前同源檢查只綁第一跳);`--landing` 是 doi.org 網址時的 DOI,scheme 與主機不分大小寫、只認 `https://doi.org/` 與 `https://dx.doi.org/`(`http://doi.org/` 本來就走不到)。**誠實邊界**:git 閘只問 `--out` 的目錄,下載中的暫存目錄在 `$TMPDIR` 底下、不經檢查 **#613 重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對):(a) `fetch` 只在使用者的 Safari 裡導航到頁面自己的 PDF 連結、交給人——不在頁內取檔、不取位元組、不寫輸出檔;`--out`/`--title`/`--pages`/`--doi`/`--prime` 拿掉(舊的呼叫以 64 失敗,不會被讀成「存好了」);結束碼改為 7 交給人、8 等人驗證(`--resume-tab`/`--resume-origin` 在同一個分頁接著走)、9 每站每日上限、6 整批暫停、3、1,沒有 0;(b) 新增 `take`:收人存下來的本機檔,驗證、git 閘、存檔,不碰瀏覽器、不連網——驗證與 git 閘是原本 `fetch` 的那一份,搬過來;(c) `url-rule` 與出版商拼網址規則刪除;(d) `bot-signals` 加 `--kind`(`verify`/`pause`),命中的結束碼仍一律 0。不開 MCP 面的理由不變,而且更強:`fetch` 的每一步都假設旁邊有使用者(交給人、等人驗證)。**帳本不在 store 裡**:`~/Library/Application Support/akashic/fulltext-attempts.jsonl`(`~/.akashic` 是 `main` store 的 root,放進去會變成資料 repo 裡未追蹤的檔)**#613 修正輪(2026-10-02)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;使用者 2026-10-02 兩則裁決與六個程式錯誤):(a) `fetch` 導航之後分頁到了別的主機而沒有任何驗證/封鎖/登入的標記:新主機顯示 PDF → 交給人(7,`pdf-shown`)、其他頁面 → 這一筆交給人(7,新原因 `left-site`)、DOI 解不開 → 7(`doi-not-resolved`),批次繼續;登入、驗證、封鎖頁維持 6;(b) 等人驗證(8)只在文章站本身或已知的驗證服務(`BotSignals.isKnownVerificationService`,封閉清單)上成立,其他主機上的驗證字樣一律 6;HTTP 429 一律 6;(c) 新旗標 `--resume-stage article|followed`(結束碼 8 在導航之後印 `followed`):導航之後的驗證接著走「分頁顯示什麼」的判斷,不再讀連結、不再導航、不再記嘗試,分頁可以在別的主機;(d) 查數與記錄是一步(`FulltextAttemptLedger.reserve`,跨行程的 `flock`)、記錄之前補檔尾換行;(e) `take` 的 `--title` 必填、不得是空的(沒給或空字串是 64,先前跳過驗證仍結束碼 0)、驗證本身跑不起來(截斷的下載、缺 poppler)是 1 而不是 5;(f) `take` 不再印非 PDF 檔的內容(只印位元組數、前 8 個位元組與是否像 HTML);`handover:` 行去掉網址的查詢與片段 **#613 R2 修正輪(2026-10-02)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;R2 verify 與使用者 2026-10-02 兩則裁決):(a) `fetch` 導航之後分頁到了別的主機:**先問 PDF**(`document.contentType`)→ 7 `pdf-shown`,不再先看標題與網址的長相(PDF 分頁的標題常是文章標題);不是 PDF 的頁面要等它載完(最多 60 秒,沒載完是 6 卡住),載完之後標題、網址、頁面文字與 HTTP 狀態一起判訊號(429 一律 6,已知驗證服務上也是),登入/驗證頁的長相(網址的主機字詞與**整段**路徑、標題的**整字**片語,封閉清單)→ 6,其餘 → 7 `left-site`;(b) 「還在 doi.org」看主機不看字串;`doi-not-resolved` 要 doi.org 自己的查無證據(標題或頁面 `DOI Not Found`、或 404),否則 6;doi.org 上的驗證字樣是 6、不印 `resume:`;DOI 落地頁的網址是登入頁的長相(EZproxy、SAML 帶 DOI 的查詢字串)→ 6;(c) `--resume-stage followed` 只接文章站、已知驗證服務、或顯示 PDF 的分頁,其他是 1;(d) 結束碼 8 的 stderr 改成請使用者看頁面、絕不貼上或執行頁面要求的東西、只在使用者說完成之後接著走(文章站本身擋不住假的 CAPTCHA,裁決文章站算);(e) 停止與錯誤訊息、`page:` 一行的網址也去查詢、帳密與 `;jsessionid`;`doi-not-resolved` 不再叫人把存下的檔交給 `take`;(f) 每日帳本的站名在寫入與查數時也正規化(小寫、去空白) **#613 R3(2026-10-04)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b31 W4):(a) 新子命令 `contract`:印 `fulltext-contract `(`FulltextFetch.contractVersion`,這一輪是 4),唯讀、不碰瀏覽器、不連網、不讀不寫任何檔;SKILL 第 0 步以它分辨比 SKILL 舊的 CLI(先前 `take` 的兩條探測分不出 R1、R2)。不開 MCP 面:它只回答「這個 CLI 照哪一版契約走」,消費者是 skill 的 shell 步驟;(b) `fetch` 別的主機上讀不到的分頁(同一個網址連三次讀不到、那個網址上從沒讀到過一般網頁)→ 7 `unverifiable`,不再因標題或網址的字樣得 6;網址在已知驗證服務上、或標題與網址帶著驗證服務自己的標記(封閉的六個標籤)照訊號處理;讀到過「還在載入」的網址等不到是 6 卡住;(c) DOI 落地頁與 `article` 接續:登入頁的長相先於頁面文字判(文字寫著 CAPTCHA 的登入頁也是 6),驗證頁的長相仍在訊號之後;(d) 別的主機上的判斷用同一份快照,讀的當中分頁換了頁就回去等;(e) 等落定以時鐘約 60 秒計(也不超過 30 次輪詢);(f) 訊息、`attempt:` 一行與帳本的站名不帶主機前的帳密;網址另去掉 `;名=值` 的路徑參數與 ASP.NET 的 `(S(…))` 段;`pdf-shown` 的訊息說依據是頁面讀得到的 `document.contentType`;`followed` 接續的分頁空白時不稱它「我們的分頁」 **#613 b34(2026-10-05)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b33 X3 與使用者 2026-10-05 四則裁決):(a) `contract` 印 5;(b) `fetch` 文章站上讀不到的分頁與別的主機同一套(不因標題或路徑的一般字樣得 8 或 6,交給人 `unverifiable`),讀不到的分頁停在登入主機上(只看主機)是 6;文章站上 HTML 頁的判斷與交給人印的網址用同一份快照,讀不到的計數改成同一個網址上連續;(c) 導航之前的接續:主機的登入長相是 6 且不送 JS,分頁顯示 PDF 先交給人(路徑有登入字詞也是),驗證頁的網址而沒有訊號時等它轉走(約 60 秒,只讀網址),沒轉走是 6;(d) 主機的登入字詞只認最左邊的標籤與四個 IdP 服務名(`journals.auth.gr` 不再停),網頁副檔名多 `cfm`、`cfml`、`xhtml`、`jsf`、`jspx`、`faces`、`shtml`、`phtml`,登入字詞拆複合段(`/sso-login`);(e) 頁面連結的 origin 不帶帳密比較與印出(帶帳密的落地頁上同站連結照常跟);訊息另去掉百分比編碼的路徑參數、多群組的 ASP.NET 段、沒有 scheme 的帳密,safari-browser 自己的錯誤訊息裡的網址同樣去憑證;每日帳本的 `landing` 不帶查詢 **#613 b37(2026-10-09)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b36 Y2 與使用者 2026-10-09 裁決):(a) `contract` 印 6;(b) 複合路徑段只認明確的登入字(`login`、`logon`、`signin`、四個 IdP 服務名,與 `sign-in`、`log-in` 兩種連字號寫法),`cas`、`auth`、`idp`、`sso`、`authenticate` 只在整段比(`hepatitis-c-as-…` 這種 slug 不再在落地頁停);(c) 讀頁面的當中分頁換到同站的登入頁時,那一頁先判登入長相(6,不印 `resume:`)——落地頁、讀連結之前、導航之前的接續;導航之後同站的 HTML 頁仍交給人 `html-page`;(d) safari-browser 失敗時只轉印它 stderr 的第一行(其餘的行可能列出其他分頁與其他 profile 的網址);(e) 印出的網址與站的比對用同一個主機(`URLSplit.hostPort`:反斜線結束 authority、沒有 scheme 的 `//` 先判) | -| `crossref-match`(#629,由 `akashic-bootstrap/scripts/crossref_match.py` 移植) | 有理由缺席 | **skill 的內部步驟**:輸入是 skill 經 safari-browser 取回、存在本機目錄的 Crossref 回應與一份作品清單,輸出是判定 JSON 檔(`confident`/`probable`/`needs_review`/`no_result`);不寫 store、**不連網**(重播式:缺的請求以 JSON 列出、exit 3,由 skill 取回後重跑)。MCP 沒有「取回」這一半,另一半(比對)沒有 store 狀態可讀,開面只會把回應檔的內容當工具參數送進 LLM context。判定的下游是 `akashic-bootstrap` 的人工核對,不是任何 MCP tool | -| `web-read`(`url`/`origin`/`landing`/`check`;#692 R4,由 web-access.md 裡要照抄執行的 check-read.py 移植;`url` 是 b33 verify 加的開分頁前檢查) | 有理由缺席 | **skill 讀頁面時的內部步驟,不是 store 能力**(與 `references`、`fulltext` 同一類):輸入是 `safari-browser documents --json` 的輸出與 skill 寫進暫存目錄的檔(讀回的 JSON、Safari 回報的主機),輸出是一行判定與一個暫存檔(驗過的落地主機、剔除後的頁面文字);**不寫 store、不連網、不碰瀏覽器**。開 MCP 面等於要 LLM 呼叫端把未剔除的第三方頁面文字當工具參數送進來——那正是這個檢查要擋在 context 外的東西(剔除在它之前做才有意義),也與 #388 位元組預算論證同一方向。唯一的消費流程是 web-access.md 的 bash 區塊,它在 shell 裡直接呼叫 CLI。不是維運例外(沒有不可逆操作),也不是候補缺席 **b33 verify**:`landing`/`check` 會刪 `--out`/`--raw` 指的檔——只刪一般檔或 symlink,目錄與其他型態在刪任何東西之前拒絕(先前對目錄整棵遞迴刪除);路徑由呼叫端給、程式不限制位置,「寫的是 skill 的暫存目錄」是 web-access.md 區塊的約定,不是程式強制的(`WriteGateRulings` 那三格同此寫實)。 | -| `abstracts-to-proposals`(#629,由 `akashic-venue-works/scripts/ndjson-abstracts-to-proposals.py` 移植) | 有理由缺席 | **一個 adapter**:輸入是 `akashic-venue-works` 階段 B 的私有產物(NDJSON,形狀不是 store 的契約),輸出是 core 的公開格式 `[Proposal]`(唯一解析器 `AddOnlyEnrichment.decodeProposals`,`akashic enrich --from`/MCP `akashic_enrich` 的 `proposals` 都吃它)。它自己不寫 store(只讀 `sources/` 的存檔、只寫 `--out`);寫入的那一步是 `akashic_enrich`,兩面都有。#516 當時把它放在 skill 目錄、不進 CLI,理由是「把私有形狀焊進 CLI 要在 parity 表加一列」;#629 的 `swift-is-the-implementation-language` 要求 skill 目錄裡不得有 Python,於是它成為子命令並在這裡加一列——**私有形狀只焊進 CLI 這一面、不進 MCP**,換一個來源形狀時是另一個 adapter | +本表的列(CLI 能力/裁決/理由)已搬到 [`docs/rulings/mcp-cli-parity.md`](../../docs/rulings/mcp-cli-parity.md)(不自動載入),接在 MCP 工具表後面;新增 CLI subcommand 時在那裡加一列。下面兩段說的「本表」指那份檔裡的這張表。 **機械檢查(CLI→MCP 方向)**:上方稽核程序的 ② 枚舉 CLI 全部註冊型別後, 每個命令必須出現在 **MCP 表的「CLI 對應」欄**或**本表**其中之一——兩處都 @@ -301,51 +121,9 @@ pre-flight)的操作,MCP 的 LLM 消費者不是該角色;**候補缺席** **這個方向是 #325 才補的**——#259 雙向化時只想到「命令長出來」,沒想到 「命令退場」,因為當時還沒有任何命令退場過。 -## 識別碼寫入面的裁決(#394,2026-08-24) - -本 change 新增了五個識別碼欄位(`venue.issn`/`organization.ror`/work 的 `doi`・`pmid`・ -`isbn`),而**兩面都沒有為它們新增參數**。這不是遺漏,是一個要寫下來的裁決——依本檔的 -規則,新增能力時必須裁決兩面,而「兩面都不給」也是一種裁決。 - -| 面 | 現況 | 裁決 | -|---|---|---| -| MCP 寫入面(~~add_venue~~/~~update_venue~~/~~add_organization~~/~~create_entry~~)| ~~不收識別碼參數~~ → **四格全部補齊(#394,2026-08-28)**:`update_venue.add_issn`/`add_venue.issn`/`add_organization.ror`/`create_entry` 的 `doi`・`pmid`・`isbn` | ✅ **這一列已關閉** | -| CLI 寫入面(同名命令)| `update-venue --add-issn`/`add-venue --issn`/`create-entry` 的 JSON draft 收 `doi`・`pmid`・`isbn`;`add-organization` 是 **MCP-only**(本檔既有裁決,不是缺口)| ✅ | -| 既有的 person ORCID 欄位 | **兩面都收**(經 generic `fields` JSON object,#68 的既有形狀)| ✅ 既有,本 change 只改型別不改介面 | - -> **這張表的第一欄刻意不用反引號包 token。** `parity-table-drift.py` 把表格第一欄的 -> 反引號 token 一律當成「宣稱存在的 CLI subcommand」並去 `CLI.swift` 對照——第一版把 -> `person.orcid`(一個**欄位**)放在第一欄,於是守衛報「表列了 `person.orcid` 而它已不在 -> CLI.swift」。守衛沒錯,是我的表格形狀在對它說謊。 - -**缺席的理由**:識別碼的來源目前只有兩條——遷移(`migrate-identifiers`,一次性)與匯入 -(`import-wos`/`import-zotero`,走 `fields` 殘留再由遷移升格)。**沒有第三條路徑產生 -識別碼**,所以現在加寫入參數是替一個還不存在的流程造介面。 - -**但這一列必須標記為弱,理由要寫出來**:遷移之後,若使用者查到一筆 work 的 DOI, -**沒有任何面寫得進結構化欄位**——實測 `entry.doi` / `v.issn` / `org.ror` 在 -`Sources/` 內的 production 寫入路徑**零命中**(`IdentifierMigration` 與 YAML 編解碼除外)。 -唯一的路是手改 YAML。 - -這正是 `replace-endnote-and-zotero` 第 4 條要防的形狀: - -> 使用者說「這個功能我回去用 Zotero 做」一旦變成常態,就是取代失敗的樣子——而且它是 -> **安靜的**失敗,因為每次個別繞過都看起來很合理。 - -**venue 那一格已於 2026-08-28 補齊**(`update-venue --add-issn`/`akashic_update_venue` 的 `add_issn`)。動手的理由不是等到了實例,是相反的:`Venue.issn` 的欄位、型別與正規化都已存在,而**只差一個參數**——把它留著等一個實例,等於讓「查到 ISSN 卻只能手改 YAML」這件事在下次真的發生時才被修,而手改 YAML 的失敗方式是安靜的(2026-08-28 實測差點弄丟一筆 DOI)。 - -**`add_venue` 與 `add_organization` 同日補齊,理由與 venue 那格同**:欄位、型別、正規化都已存在,只差一個參數;而建檔時本來就知道刊物的 ISSN 與機構的 ROR,少了它得「先建再更新」——一次操作變兩次,中間有一個識別碼不在的狀態。 - -**形狀刻意不同**:ISSN 是**清單**(print 與 electronic 是兩個真的號),ROR 是**純量**(一個機構只有一個)。兩者都是「不合法即整個呼叫拒絕、零寫入」——建檔面的拒絕比更新面更強:若守衛只擋識別碼而讓記錄建了出來,結果是一筆「使用者以為帶識別碼、實際沒有」的記錄,比明確失敗更糟。 - -**`create_entry` 同日補齊,這一列因此關閉。** 理由與前三格相同(欄位、型別、正規化都已存在,只差一個參數)。 - -**`.bib` 匯入路徑刻意不帶結構化識別碼**:`.bib` 的 `DOI = {...}` 進 `fields`,走**殘留路徑**由 `migrate-identifiers` 升格。在匯入端順手升格會製造第二條升格路徑,而那條路徑對「解不了的 token」的處置與遷移端不同(遷移把它留在殘留並報出來),兩者會分岔——這正是同日 #424 修掉的那個 bug 的形狀(一個 `return` 綁兩個決定)。 +## 識別碼寫入面的裁決(已搬出) -**這一列關閉後,`replace-endnote-and-zotero` 第 4 條在識別碼這一格不再有「回去用 Zotero 做」的理由**——查到一個識別碼之後,四個寫入面都收得下。那時要裁的 -不只是加參數,還有**它該長什麼形狀**——`person.orcid` 走 generic `fields` object,而 -`venue`/`entry` 的寫入面目前沒有對應的 generic 通道(`update-venue` 只收 `key`)。 -追蹤:#394 close 前不處理;本列即是那個「已知且具名」的缺口。 +本段(識別碼欄位的寫入面裁決與它的理由,#394)已搬到 [`docs/rulings/mcp-cli-parity.md`](../../docs/rulings/mcp-cli-parity.md)(不自動載入);需要查過往裁決時讀那份檔。 ## CLI 橫切選項裁決表(封閉列舉——#310 一次性補裁 2 項、#298 增第 3 項;恰 3 項,一格不多一格不少。**不得依性質相似類推第四項**) diff --git a/.claude/rules/measurement-commands-self-prove.md b/.claude/rules/measurement-commands-self-prove.md index 3c75d243..ede1771d 100644 --- a/.claude/rules/measurement-commands-self-prove.md +++ b/.claude/rules/measurement-commands-self-prove.md @@ -3,8 +3,8 @@ 從 [`zero-instance-guards`](zero-instance-guards.md) 的量測段抽出來的通則(#711,使用者 2026-10-05 裁決:規則檔裡本身是通則的段落抽成獨立的小規則)。 那份表有幾十條「重跑指令」,原本各自寫「同第 13 列的自證」,而那個「自證」的寫法與理由只住在第 13 列與第 19 列的量測段裡(現在都在量測文件)。 -適用於**零實例裁決表的量測**——規則檔與 [`docs/zero-instance-measurements.md`](../../docs/zero-instance-measurements.md)(量測文件)裡給人重跑的指令; -`zero-instance-rows-audit` 兩份都掃。其他地方(changelog、issue comment、別的規則檔)寫給人照抄重跑的量測,用同一個寫法——守衛不掃那裡 +適用於**零實例裁決表的量測**——規則檔、裁決表 [`docs/rulings/zero-instance-guards.md`](../../docs/rulings/zero-instance-guards.md)(2026-10-15 從規則檔搬出)與 [`docs/zero-instance-measurements.md`](../../docs/zero-instance-measurements.md)(量測文件)裡給人重跑的指令; +`zero-instance-rows-audit` 三份都掃。其他地方(changelog、issue comment、別的規則檔)寫給人照抄重跑的量測,用同一個寫法——守衛不掃那裡 (2026-10-09 #711 b37:`.claude/rules/` 底下別的規則檔裡數 binary 輸出的量測只有 `identity-is-judged-not-matched` 一條,已改成這個寫法)。 ## 規則 @@ -35,16 +35,16 @@ **第 2、3 條的既有違反**(#711 b37,b36 verify 第 5 列;逐列列名、只減不增,改到那一段時順手修,新腳本不得照抄):量測文件裡第 13、23 列 的行級 grep 數 store(第 2 條);第 18、41、64、77 列的腳本對讀不到的檔或 index 列靜默略過(第 3 條,第 24 列的量測段記過第 18 列)。 -4. **每個數字旁寫日期與條件**(`# 2026-09-29:…`):數字會過期,寫出條件,重跑的人才知道比的是什麼。`measured-numbers-audit` 對規則檔與量測文件裡 +4. **每個數字旁寫日期與條件**(`# 2026-09-29:…`):數字會過期,寫出條件,重跑的人才知道比的是什麼。`measured-numbers-audit` 對規則檔、裁決表與量測文件裡 的「實測 N」要求時間錨或可重跑的指令。 5. **新增合模板的量測之後,把量測文件的棘輪下限調高**(``):它記依閘去重後至少幾條,少於下限守衛就紅。 ## 守衛與它管不到的 -`zero-instance-rows-audit` 掃規則檔與量測文件:提到 binary 又有計數的單位就是一條量測,不合模板即紅、具名那一行;量測文件裡一條都掃不到也紅; +`zero-instance-rows-audit` 掃規則檔、裁決表與量測文件:提到 binary 又有計數的單位就是一條量測,不合模板即紅、具名那一行;量測文件裡一條都掃不到也紅; 合模板的量測依閘去重後少於棘輪下限也紅。負控是 `audit-guards-mutations` 裡 `zi-rows:` 開頭的格子。 -這兩份檔 PR 改得到,所以守衛的判讀是線性的,另有三道上限:每份檔 4 MiB、單一 fence 或段落 1,500 行與 128 KiB、依閘去重 500 條—— +這三份檔 PR 改得到,所以守衛的判讀是線性的,另有三道上限:每份檔 4 MiB、單一 fence 或段落 1,500 行與 128 KiB、依閘去重 500 條—— 超過即具名紅、不判讀那一塊(#711 b37)。接近上限時先問那份檔是不是該拆。 **認不出來、所以不紅的寫法是開放的**:binary 的輸出先存進變數或檔案、在另一個單位計數;以別名、複本或名字不以 akashic 開頭的變數執行 binary; diff --git a/.claude/rules/two-kinds-of-edits.md b/.claude/rules/two-kinds-of-edits.md index 444a532e..0477b6be 100644 --- a/.claude/rules/two-kinds-of-edits.md +++ b/.claude/rules/two-kinds-of-edits.md @@ -25,52 +25,9 @@ `resolve-venues`/`resolve-organizations` 就是這個形:`LooseNameKey` 的提名是 recall、由程式做;`apply`/`judge` 的判定由 AI 做且留 verdict;寫回 store 與收攏由程式做。 -### 裁決史(封閉列舉——列數以下表為準;新的寫入面加一列,不得依性質相似類推) +### 裁決史(已搬出) -| 寫入面 | 種類 | 一句話理由 | -|---|---|---| -| `resolve-people apply`/`reject`/`judge`(#272/#303/#386) | AI | `literal → key` 是身分判定;Jaccard 實測同一人 0.40、不同人 0.50,字串謂詞站在錯的一側。**change `resolution-verdict-states`(#636)**:同一配對的提名層判定(apply/reject)與逐篇判定(judge/refute)**兩筆並存**——判定層級(nominated/judged,封閉二值、由 rule 導出)參與記錄鍵;在此之前逐篇判定被當成重複丟掉,查證理由無處可寫 | -| `resolve-people split-author`(#443) | AI | 「這一格裝了兩個人」是判定;但落地是程式(切出的每一段必然是原文子字串)——持久化見 #450 | -| `resolve-people un-split`(#513) | 程式 | **split 的具名逆操作**——合回去的字串逐字取自 #450 寫下的記錄 value,不做任何判定。同輸入必得同輸出;整批擋零寫入;三種歧義(同 value 多筆記錄、各段出現多處、各段不連續)一律**拒絕不判定**(形狀取自 `enrich` 對 DOI 命中 ≥2 筆的既有處置)。段已升格為 `.key`/`.organization` 時拒絕並指向 `demote`——那一格才是判定的逆轉。**#659 起刪記錄之前要求 work 檔已 commit、乾淨**(比照 #573):被刪的拆分記錄只剩 git 裡的副本 | -| `resolve-people drop-author`(#457) | AI | **「這一格裝的不是作者」是判定**——要知道 `No authorship indicated` 是 PsycInfo 的無署名佔位字串,字串謂詞單獨做不出來。所以理由必填、記錄必留(`Entry.references` 的 `field: authors`、statement `移除:理由`,與作者位改寫同一次寫入,需要 store format ≥ 17)。**沒有具名逆操作**,而那不是「還沒做」:記錄留著被移除的字串逐字,但**刻意不留位置**(同拆分記錄不存索引的既有理由——索引在同一批的前一次操作之後會位移),所以還原無從定位。真出現要還原的需求時是一次顯式裁決。它把作者位的數量改成 N-1(可到 0),與 `split-author`/`un-split` 同族,各自單獨呼叫 | -| `resolve-people attribute-org`(#443) | AI | 團體作者的升格是判定,org key 由呼叫端顯式給 | -| `resolve-venues apply`/`reject`/`repoint`/`demote`(#304/#418) | AI | 同 people;`demote` 是判定的逆轉,原字串從 verdict 逐字取回、取不到寧可拒絕;≥2 個不同 literal 時同樣拒絕不判定(#554 R9,D23)。**翻轉判定時退役相反的那筆**(#554 R9,D20):`repoint`/`demote` 寫 rejected 時把同 holder 上同一配對的 confirmed 移除(反向 repoint 對 rejected 亦然)——verdict 沒有時間戳,「後者為準」只有寫入面知道,留著就是 #486 的矛盾對;歷史留 git(同 un-split 的取捨)。**前提是配對只由一條邊實例化**(#554 R10,D25):verdict 不帶 venue index,同一 work 兩條邊指同一 venue時退役會把另一條邊的證據一起刪——那種 work 具名拒絕零寫入;退役的每筆逐字回報(截 20 筆、總數揭露,R11)。**唯一性對寫入後的邊集合驗,生產端也擋**(#554 R11,D27/D28):R10 的 D25 只看原始 entry 的 from——`repoint` 改指到本 work 已有邊的 venue、`apply` 對兩條同刊名的 literal 邊各 apply 一次,都造得出 D25 宣告不得存在的形;現在 `apply`/`repoint` 對寫入後會出現「同一 work 兩條 key 邊指同一 venue」的整批拒絕零寫入,同一批同一 work 的兩個 move 不得帶同一個 literal(逐 move 的退役會互相覆蓋)。既有的這種 work 由 `Entry.validate()` 報 warning;刪掉多餘邊的面另案 #572——落地前出路只有手改 YAML。**R12(D33):`apply` 改逐筆略過並具名**(`skippedDuplicateVenueEdge`)——整批拒絕讓一筆毒候選殺掉同批無關的候選、既有的重複邊鎖死不相干的歸戶;store 狀態不符是「該筆略過並具名」(`judge` 的先例),語法錯才整批拒。D27 只看被動到的邊、同 literal 的 move 只在 venue 集合相交時拒;否決抑制改比 `matchingKey`(與其餘三處同粒度——代價寫在兩面描述:對一個拼法的 reject/demote 會壓住同 work 同 venue 的其他拼法,R14 verify regression 第 21 列;撤回這樣的否決至今沒有工具面——#559 上線的只有 `update-venue --unauthorize`,與此無關,#712 R1 verify 第 16、26、30 列;被壓住的候選自 #712 起列在列表的 `suppressed`)。**R15(D38):目的 venue 已對該 work 持有另一個正規化後不同的 confirmed literal(沒有對應的邊)時,`apply` 逐筆略過並具名(`skippedConflictingConfirmedLiteral`)、`repoint` 對預測後的 verdict 集合驗、整批拒絕**——R14 verify Codex 第 1 列:生產端的 fail-closed 只在合併有,apply/repoint 寫下第二個 confirmed 就把那條邊鎖進 D23。**R16(D43/D44):閘的相等比位元組、同一 literal 的另一個拼法也略過/拒**(R15 verify 第 3 列 HIGH、第 5 列:R15 以 `matchingKey` 比、放行同鍵異拼法——新拼法沒寫入,之後 demote 還回目的 venue 的舊拼法而不是這條邊的原字串,正是 `confirmedLiteral` 承諾不做的事;D23 比位元組,閘要同一把);apply 的閘移到 D28/D33 的重複邊檢查**之後**(第 2 列 HIGH:重複來源欄位這個最常見情境被分進錯的桶、出路叫人刪掉有邊的 verdict)。**R17(D50)**:閘的訊息兩桶同時非空時兩桶都說、出路分開列(R16 verify DA 第 9 列:只印第一桶、用單數「那筆」,照著刪掉一筆之後 validate 全綠、再跑才撞第二桶) | -| `resolve-people undecided`/`resolve-venues undecided`/`resolve-organizations undecided`(#619;change `resolution-verdict-states`;org 族 #643,change `org-undecided-leg`) | AI | **「查過、判不出來」也是判定工作的產出**:寫它的人查了名字以外的證據而結論是不足以判定——那個結論與它依據的內容(rests-on)正是 `identity-is-judged-not-matched` 要求留下的東西,只是方向是「不下判定」。所以它走 AI 欄的義務:說明必填、記錄必留、可附證據 digest。**它不是判定的逆轉也不動 entry**:未決不抑制提名、不構成矛盾,配對被判定之後保留為查證歷史(不退役)。已判定的配對寫未決=逐筆略過(未決不改變已判定配對的狀態)。落地是程式(整批拒絕、逐筆略過、完全相同=no-op) | -| `resolve-organizations`(#304) | AI | 同上 | -| `resolve-divergence`(#71 一族)/攣生合併(#456/#459/#553) | AI | 兩筆記錄是否同一實體是判定;合併含全庫改寫+刪檔,所以落地那一半是程式且要乾跑過目。**#553 起 venue 走同一個面**(不另開列——列是「寫入面」,而這是同一個面多收一個 shape)。venue 側有一格與 person 側**相反**且必須寫出來:被併者的 `authorized` 在 person 是**拒絕條件**(#81:「哪個名字對外」是判定),在 venue 只是**提醒**——實測 479/479 筆 venue 的 `authorized` 恰好等於 `[names[0]]`,唯一寫入者**當時**是 `VenueBootstrap` 的建檔慣例,所以它不承載判定(#563 起 bootstrap 不再寫它,既有的機械值不動)。拿一個不做判定的操作的副產品當拒絕條件,會讓合併對它要解決的 7 組重複全部無用。**#554 起有面可寫(`update-venue --authorize`),這一格 2026-09-12 重開過一次、裁決不變**——理由換了:那個面不留 judgement(#564),所以「人確認過的 names[0]」與機械值在 store 裡仍然長得一樣,合併端拿不到可以承重的東西。#564 裁「留」且落地後升成拒絕條件(`DivergenceResolve.authorizedDemotedByMerging` 的 doc 記著觸發條件)。**2026-10-01(#564 落地):那個觸發條件成立,這一格升成拒絕**——被併者的 authorized 名字若帶名字分類的判定記錄(`field: authorized` 的指定/確認/撤回,format 22),合併拒絕(`wouldDemoteJudgedAuthorized`,preview 與實跑共用前置;出路是在被併者 `--unauthorize`、或在倖存者 `--authorize`,都要理由);沒有記錄的機械值仍是提醒。判準是「有任何記錄」不是「最後一筆是指定或確認」(最後一筆是撤回卻仍在 authorized 只可能是手改,保守地拒絕)。其餘名字分類記錄在名字於合併後的倖存者上分類與被併者相同時逐位元組搬(`referencesCarried`),不同時以 `wouldLoseFields` 拒絕並說出名字與兩邊分類。**#554 R12 兩條新拒絕(D31/D32,person 與 venue 同)**:倖存者與被併者對同一配對持相反判定時拒——合併不裁決哪一筆對,那是兩個判定的衝突(#486 的矛盾對,處置沒有工具面);venue 合併把同一 work 的兩條 key 邊塌成一條而兩筆正規化後不同的 confirmed literal 都留下時拒(塌完之後 D23 讓那條邊永久不可 demote)。被併者的 verdict 遷移以 `verdictEqualityKey` 去重——R11 之前比位元組,「(field, value) 冪等」只在位元組層為真。**R13**:D31 累積比對(三方合併的 doomed↔doomed)且裝在 holder 遷移那一步(work/person 合併把第三方 holder 的 verdict 改寫成倖存者後不得矛盾——R12 verify DA 真 binary 全工具面重現);D32 守不變式本身(這次新增的 confirmed literal 讓倖存者對某 work ≥2 個正規化後不同的 confirmed 即拒,不看塌不塌邊;倖存者既有的違反不擋);去重丟掉的判定逐筆回報在 `verdictsCollapsed`。**R14(D34/D35)**:三處閘收成同一個 delta 謂詞——合併後與合併前比、只擋這次帶進來的(相反判定三種 shape;≥2 個 confirmed literal 只對 venue 記錄),合併前就有的違反不擋(validate 的 warning 負責——R13 的 holder 側是絕對謂詞、keeper 側漏單一被併者內部的矛盾與雙 literal);holder 遷移那條路兩半都算(R13 只補了相反判定,work 合併純工具面造得出 D23 從此拒的邊);訊息印出處與遷移前的原值;dry-run 與實跑對 `verdictsCollapsed` 同源。**R15(D37/D40)**:差集在**索引層**算——R14 以配對鍵比未改寫的 before 與改寫後的 after,被併鍵上的既有矛盾對與雙 literal 改寫後鍵變了、被判成新(R14 verify logic 第 2 列 HIGH,四席同指);現在 after 的違反是既有的當且僅當構成它的索引裡有一組在合併前就構成同一類違反,含倖存者自己持有的 `person:<被併>` 矛盾對;雙 literal 的拒絕把帶進來的與倖存者既有的分開列;收攏丟列印遷移前的原值並逐段截(rename 同一個生產者)。**R16(D45):「既有」看倖存配對**(R15 verify 第 12/13/20 列:R15 問「有沒有某一個合併前分區已持有全部」——holder 對 doom 持 {A, B}、對 keep 持 {A},doom 那一區 ⊇ 全部就放行,而 (holder, keep) 合併前只有一個 literal、可以 demote,合併後被 D23 鎖住;末句「含住在被併鍵上的不擋」與判準互相矛盾):不擋的只有倖存配對合併前就持有整組、或倖存配對合併前一筆都沒有而整組原樣從單一被併鍵搬來;整組住在被併記錄裡的(R13 的裁決不變)、或讓倖存配對的既有歧義變大的,都擋——矛盾對同一條規則。**R17(D47):收攏的勝者政策**(R16 verify DA 第 1 列 HIGH,真 binary 重現:#468 的「弱血統優先」排在「未改寫者勝」之前,被併記錄弱血統的 `VEE JOURNAL` 贏過倖存者使用者確認的 `Vee Journal`、連位元組一起取代,之後 demote 把邊寫成不是它原本記的字)——碰撞的 literal **位元組不同**時倖存配對自己的(未改寫)那筆勝,同拼法時 #468 三層照舊(留弱只多一句警告、不動任何字串);被丟的那筆連同留下的拼法印在 `verdictsCollapsed`(三條收攏路徑——holder 遷移、keeper 合併、rename——同一個描述函式)。這是對 #468 使用者裁決的邊界收窄,使用者可翻。**R18(D51/D53):倖存配對不是倖存邊**(R17 verify Codex 第 1 列 HIGH:keeper 對某 work 持有的 confirmed 可能**沒有邊**——手改、舊 binary、D38 具名的那種輸入——而 work 的唯一邊指向被併 venue,被併記錄的那筆才是那條邊記錄的字;R17 的 keeper 路徑一律 keeper 勝、三方合併由陣列順序決定、rename 沒有任何政策):三條收攏路徑同一個勝者函式(`collapseWinner`)——拼法位元組不同時先留**活著的邊**那一筆(合併前 work 對該 venue/person 真的有邊),再留與倖存配對自己位元組相同的,再 #468 三層,最後首見;rename 只收攏它動到的鍵,被改寫的那筆對上早已指向新鍵的死 verdict 時留活的,兩筆都沒動到的碰撞留著(validate 照報);**R19(D55)**:以鍵整組算——R18 的單一槽位記帳讓三方碰撞由 YAML 順序決定(R18 verify Codex 第 1 列 HIGH),現在被改寫的全留、早已指向新鍵的全丟。**代價寫出來**(R17 verify regression 第 9 列):D47/D51 留強丟弱時,弱血統那筆的 `rule` 尾註從 store 消失——它的消費端不只合併那一刻的收攏列,還有之後每一次提名(`PersonResolver`/`VenueResolver` 的 confirmed-elsewhere 從 `confirmedByLiteral` 讀 rule 揭露弱出身);#468 保的那個警告在這一格自此只在 `verdictsCollapsed` 出現一次。使用者可翻的是這個取捨。**R20(D58;R19 verify DA 第 1 列 HIGH)**:D55 以含 field 的鍵分組,早已指向新鍵的 rejected(異 field)逃過「全丟」、rename 後復活成 #486 矛盾對——D58 改成不論 field 與拼法一律丟;被改寫的同拼法重複走 `collapseWinner`(#468 那一層在 rename 之前沒跑,R19 留首見);拼法不同的都留——venue×work 以外的六格沒有掃描面,rename 不替它們判定(§3.5 誠實邊界)。**R21(D60;R20 verify 五席)**:D58 的「一律丟」寫在第一段後的早退之後,holder 只持有死 verdict 時整筆記錄原樣通過、rename 後復活(四席同指,真 binary);而 DA 指出即使修好,那也是程式編輯**單方面銷毀判定編輯的產物**——無乾跑、無逆操作、無 git 閘,被丟的可能是人對另一筆仍存在的記錄親自下的判定。現在與 merge 的 D31/D34 同向:目的鍵上已有 verdict(同 holderKind、不論 field 與拼法)即 rename 具名拒絕、零寫入(`assertNoVerdictAlreadyAt`,逐筆列 holder/欄位/value/rests-on,出路是改那一行的 value 或刪掉它)。**R22(D61/D62;R21 verify)**:母體含 quarantined 檔(行級比對,DA 真 binary:R21 讓一筆 validate 本來會報的死 verdict 在 rename 後變成沒有任何面會報的活斷言);折疊只折完全相同的重複(R21 verify DA 第 14 列:同拼法而 judgement/rests-on 不同的曾被折成一筆,digest 永久消失)——「rename 不做任何判定的刪除」自此才是真話。**R23(D63/D64;R22 verify,6 席齊)**:行級比對被 YAML 折行擊穿(needle 含空白,四席同指、live store 2 筆)→ 位元組比對 `:`,與本列 merge 隔離檔閘同一種紀律與同一種誠實措辭;D62 留下的同鍵異 judgement 有了掃描面(`duplicateVerdictRecords`),merge 收攏它時在 `verdictsCollapsed` 回報、`describeDedupedVerdict` 自此也印 rests-on(Codex 第 6 列:R20 只補了 `describeCollapsedVerdict`)。**R25(D69;R24 verify 第 9/26/29 列、DA 第 39 列)**:`fieldsLostByMerging` 的「倖存者身上已經有這筆 reference」改比 `byteExactKey`——canonical `==` 曾把被併記錄的 NFD 拼法判成「已有」、那組位元組隨檔案消失而報告說沒遺失,是 D65 在合併面的同一個缺陷;`verdictsRetired` 的描述自此也印 rests-on(Codex 第 2 列:同一族的第三個生產者)。**R26(D73;R25 verify DA 第 17 列 MEDIUM、requirements 第 21 列)**:venue 的 `fieldsLostByMerging` 自此也比 references——R25 之前那一份**完全不比**、`mergeVerdicts` 只搬 verdict 欄位,被併 venue 的 `paginated` judgement 與 rests-on digest 在純量相同時隨檔案靜默消失,正是本列 R21 D60 為 rename 關掉的「程式編輯銷毀判定編輯的產物」在合併面的同型;三份遺失偵測的訊息自此說出「倖存者有正規化後相等、位元組不同的一筆」(第 14 列)。**#565(2026-09-29):venue 合併的名字併入改成程式編輯該有的樣子——只搬、不替人判定**:被併者的 `names` 段整段搬(時間欄位、source、note 原樣——#553 以 `TemporalValue(value:)` 重建、丟掉沿革段的時間);被併者原本在 `variant` 的才標 variant,其餘(含它的 authorized)未標(#554 D1 同一個論證:「兩筆是同一本刊」不蘊含「它們都是倖存者對外形的異寫」);倖存者已有的名字不改分類、被併者對它說的 variant 只提醒;同名段與倖存者那一段時間不同而不能並存(不是兩段都帶不相交時間的沿革)時具名拒絕、零寫入——哪一段的時間對是判定,合併不做。**#565/#587 R1 verify(2026-09-29)**:分類不取決於被併者的處理順序——倖存者原本沒有的名字,任一被併者列在 variant 就標(未標「不作任何宣稱」,不構成反對);「倖存者已有」只對原本的倖存者判斷,先併入的被併者帶進來的不算;「完全相同的段」比 source/note 的 UTF-8 位元組(Swift `String ==` 會把只差 NFC/NFD 的 note 當成相同而靜默丟掉一份);被併者 `field: issn`/`names` 的 reference 逐位元組搬到倖存者(合併前置與實跑共用同一份計算),同一個 ISSN 兩邊角色不同具名拒絕——這兩格原本讓每個經過 #587 查證流程的被併者都合併不了、或角色靜默消失。 **#709 R3 重新確認(2026-10-02),種類不變**:落地那一半(程式)的前置多一道——會被改指的 entry 自己有 legacy 拷貝(同一種、同一個 id)時,改寫會被 #631 拒絕;這一格先前只在實跑的預檢裡,`--dry-run` 預測成功而實跑被拒,現在住在 preview 與實跑共用的前提段(`assertRewriteTargetsWritable`),兩邊擲同一個錯、零寫入。它是落地前置的完整化,不是新的判定,也不改變上面任何一條拒絕。**#564 R2 verify 重新確認(2026-10-03),種類不變**:搬名字分類記錄(落地那一半)按被併者的順序接、每一筆只與那個名字那個分割此刻的最後一筆比位元組(`appendCollecting`,與寫入面同一個規則)——先前以整份歷史去重,被併者「指定 R → 撤回 S → 指定 R」只搬撤回,倖存者的名字仍是對外形、最後一筆卻是撤回(b29 V1 第 0/2/6 列);合併後最後一筆與分類矛盾、而那是這次帶進來的,preview 與實跑都拒絕(`wouldContradictClassificationTail`);person 記錄的名字要在合併後的 names 裡(只差空白的拼法不併入,它的記錄會成孤兒——先前 dry-run 說搬、實跑在寫入閘失敗,第 3/12 列)。都是落地的完整化,判定(分類一致才搬)不變。**#564 b34 重新確認(2026-10-05,b33 X1),種類不變**:搬名字分類記錄改成被併者依 key 排序、倖存者已有而此刻尾端與分類一致的不重搬(`carryCollecting`——兩筆攣生歷史相同時不把整段再接一遍,第 4/10 列)、接續線性(第 9 列);尾端矛盾的保險以**原始倖存者**為基準、全部接完之後算一次差集(第 1/21 列:先前逐被併者累加,結論隨順序翻轉)。落地的決定論變強了(結果不再取決於被併者的給定順序),判定不變 **#564 b37 重新確認(b36 Y1 第 12 列),種類不變**:被併者自己的名字分類記錄就與分類矛盾時一律拒絕(`doomedTailConflicts`)——先前看全部接完之後的最後一筆,而被併者依 key 排序,同樣兩份歷史只換 key 拼法就從放行翻成拒絕:store 沒有時間資訊說哪一筆較新,依字典序決勝是讓政策預設代替判定(`disambiguate-before-irreversible-writes`)。拒絕多一類(被併者自己不一致),結論從此與 key 拼法無關;這是一處在零實例的形狀上變嚴(live store 名字分類記錄 0 筆),使用者可翻 | -| `update-venue --paginated`(#406) | AI | 「本刊是否使用頁碼」是判定,必附 judgement 與 rests-on | -| `update-venue --remove-issn`/`akashic_update_venue.remove_issn`(#588) | AI | 「這個號不屬於這本刊」是判定——號本身合法(過得了檢查碼),錯的是它掛在哪裡,字串謂詞做不出來。理由必填(`=理由`)。**理由不進 store、只進報告**(使用者 2026-09-27 裁決「只進報告與 git 歷史」;git 保存的是移除前的檔——被移除的號與它的 provenance——理由要留在 git 裡,得由操作者寫進後續的 commit message,工具不代寫。#588 R1 verify 指出先前寫成「理由進 git 歷史」是過度宣稱),所以「留 verdict」這一格在 store 內是**有記錄地不兌現**:兌現它的一半是報告(全文、不截斷)、一半是 git 的移除前副本——移除前要求那筆 venue 檔已 commit、乾淨(`assertRecordsRecoverable`,#573 的閘一般化),否則整批拒絕零寫入。逆操作是 `--add-issn` 加回來——只加回號;同一次移除一併刪掉的 `field: issn` provenance(R1 起,報告逐號列筆數)要從 git 的移除前副本取回,沒有工具面。落地是程式(不合法的號、理由空白或過長、同號重複、venue 沒有這個號、同一個號同時加與移除,都整批拒絕零寫入) **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘——會刪判定記錄的移除腿一律過閘;MCP 不閘 | -| `update-venue --references`/`akashic_update_venue.references`(#587) | AI | 「這個號(這個名字)出自這份來源」是查證的產出——寫它的人查了名字以外的證據,而識別碼終結指涉:沒有來源的號錯了沒有跡象(`identity-is-judged-not-matched`)。每一筆就是那份記錄本身(retrieval 帶 url/日期/HTTP 狀態/digest,judgement 帶理由與 rests-on),所以「留 verdict」由記錄自己兌現。**具名逆操作是 `--remove-reference`**(#673,見下一列;在它之前 venue 的 reference 沒有移除面,唯一會刪它的是 `--remove-issn` 對被移除號的連帶刪除,寫錯的只能手改 YAML)。落地是程式:形狀走 `ProvenanceReference` 平面 init、附著走載入的同一個驗證、append-only 以位元組去重、任一筆不合整批拒絕零寫入。verdict 欄位與 `paginated` 判定不收——它們各有自己的面,通用面寫進去會讓判定與記錄的值分岔。**#587 R1 verify 起通用面只收 `issn` 與 `names`**:`authorized` 與 `note` 也拒收——當時沒有移除面,每多收一格就多一個死角(`authorized` 的 reference 會鎖住 `authorize` 換對外形,只能手改 YAML 解開;#673 起有移除面,要不要收回通用寫入面待使用者裁決);合併(`resolve-divergence`)自此把被併者這兩格的 reference 逐位元組搬到倖存者,所以來源記錄不會讓合併卡住 | -| `update-person` 的 references/`akashic_update_person.references`(#68;#674 起與 venue 同一個解析函式) | AI | 理由同上一列:每一筆就是那份查證記錄本身(retrieval 帶 url/日期/HTTP 狀態,judgement 帶理由與 rests-on),「留 verdict」由記錄自己兌現。append-only、以位元組去重;**沒有具名逆操作**,person 的 reference 沒有移除面、寫錯的只能手改 YAML(有記錄的缺口)。verdict 欄位對拒收,只經 resolve 流程。落地是程式(同一個 `parseReferenceObjects`,任一筆不合整個呼叫拒絕零寫入)。**這一列只裁 references**:同一個命令的其他欄位(`orcid`、`profile`)是另一件事,沒有在這裡歸類;`names` 動到 authorized 的那一格見下一列(#564 修正輪)。名字分類的判定記錄(`field: authorized` 的「指定/確認/撤回:理由」)不收——只經名字分類面寫 | -| `update-person` 的 `fields.names` 動到 authorized/`akashic_update_person` 的 `names`+`judgement`(#564 修正輪,使用者 2026-10-02 裁決第 1 點) | AI | 「這個名字是他的對外形」是判定——與 `authorize-names` 同一句話的單筆形(R1 verify b26 F2 第 0/2/5/20 列:先前這條全量替換能不附理由、不留記錄地改 authorized,而 mcp-cli-parity 寫「person 的單筆名字分類面不存在」)。替換讓名字**進**或**出** authorized 時理由必填(`--judgement`/MCP `judgement`,同 venue 的理由檢查;證據可空),比照 venue 寫 `field: authorized` 的記錄:進的「指定」、~~仍在的「確認」(附了理由時)~~ → 仍在的不寫(b34)、出的「撤回」;只動 variant 的替換不必附理由(person 的 variant 是「其他名字」,不是判定的分割,所以沒有 `field: variant` 的記錄)。與那個名字最後一筆記錄位元組相同的不重寫。落地是程式:替換拿掉一個有記錄的名字具名拒絕並指出口(先移到 variant 寫撤回、再 `--remove-name`);寫入閘對 format < 22 具名拒絕;有 `--dry-run`(`judgementsToRecord`)。**#564 R2 verify 重新確認(2026-10-03),種類不變**:~~讓**沒有記錄**的對外形整個離開 names 改為放行(附理由;b29 V1 第 4 列——整份替換改正對外形拼寫的既有用法被修正輪改成三步,而那個名字沒有判定可推翻、沒有記錄會成孤兒),它沒有地方錨定記錄、列在報告 `authorizedRemovedWithoutRecord`——理由只寫在進入的名字上~~ → b34 退場(見本格末);沒附理由的拒絕在 format < 22 時說出寫入閘與升級路徑;一次上限只數 authorized(variant 不寫記錄)。~~「只動 variant 的不必附理由」是實作對裁決第 1 點(「動到 authorized/variant」)的讀法,待使用者確認;附理由時仍在 authorized 的每個名字都寫確認(呼叫端沒辦法只確認幾個),也待使用者看過。~~ → 使用者 2026-10-05 裁決:前者確認,後者改為不寫(見本格末 b34)。**#564 b34 重新確認(使用者 2026-10-05 裁決第 1–3 點),種類不變**:上面 R2 的放行退場——對外形在替換裡整個離開 names 一律拒絕(移出要寫「撤回:理由」、記錄錨定 names,所以留在 variant;R2 那條路的理由既不入庫也不在回應,b33 X1);只替進出的名字寫記錄,仍在 authorized 的不寫確認,附理由而沒有名字進出的拒絕(理由不靜默丟掉);只動 variant 不必理由(使用者確認了讀法)。「留 verdict」因此對每一次 authorized 的進出都兌現 **#564 b37 重新確認(b36 Y1 第 2/14 列),種類不變**:`fields` 路徑(含乾跑)對兩筆不同的 person(id 不同)共用同一個 key 整批拒絕——判定記錄寫進先載入的那一筆是猜,落地那一半的前置補齊(檔案處境那一類由寫入當下的 #631 前置擋,單檔寫入沒有撕裂)。**給了理由而沒有名字進出 authorized 就拒絕**這一格比 2026-10-05 的裁決嚴(裁決說只替有變動的寫、只動 variant 不必理由,沒說附了理由要拒絕),b36 Y1 第 4 列指出,交給使用者裁 | -| `update-person --remove-name`/`update-organization --remove-name`(MCP `remove_names`)(#564 修正輪,使用者 2026-10-02 裁決第 2 點) | AI | 「這個名字不該在(打錯字、錯拼法)」是判定。**只收最後一筆名字分類記錄是「撤回」的名字**——人已經說過它不是對外形,刪它不推翻仍成立的判定;名字(organization 是它的每一段)連同它的名字分類記錄一起刪。套用移除面一族的裁決:理由必填、只進報告、不寫進 store,「留 verdict」在 store 內**有記錄地不兌現**——兌現的是報告與 git 的刪除前副本(刪之前要求那個檔已 commit、乾淨)。沒有記錄、最後一筆不是撤回(出口:先撤回,再刪)、還是對外名稱、被 `field: names` 的 reference 指著(兩種實體的 reference 都沒有移除面)、organization 刪完沒有名字,都整批拒絕零寫入。organization 沒有一般的名字移除面(待裁,#557)——這一條只是打錯字的出路。venue 的同一件事走既有的 `--edit-name-segment` 的 remove(下方那一列)。單獨呼叫;一次至多 200 個。**#564 R2 verify 重新確認(2026-10-03),種類不變**:拒絕依名字的處境給出口(b29 V1 第 6/7/23 列:沒有記錄的——person 指向 `fields.names` 整份替換,organization 說出「先讓它有一筆撤回」的兩步與代價;不在分割裡、最後一筆卻是指定的——先指定回去再撤回);person 刪完沒有名字也拒(第 18 列,與 organization、venue 同);organization 的報告逐段回時間欄位、source、note(第 20 列);刪記錄改成線性(第 16 列)。**#564 b34 重新確認(使用者 2026-10-05 裁決第 4、5 點),種類不變**:CLI 的 `--remove-name` 過 #298 的目標 store 確認閘(會刪判定記錄的面過閘;person 的 `--dry-run` 不擋,organization 沒有乾跑),MCP 不閘;organization 的 `remove_names` 保留。出口的文字補上代價與寫入閘(b33 X1 第 6/17/20/26/28/29/30/31 列) | -| `update-venue --remove-reference`/`akashic_update_venue.remove_reference`(#673) | AI | 「這筆來源記錄不成立」是判定——寫它的人當初讀過來源(#587 的 `--references`),收回它要重新讀過才知道錯在哪,字串謂詞做不出來(例:兩本姊妹刊的 ISSN Portal 頁面網址只差一碼,`url` 貼到另一本)。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的一半是報告(`referencesRemoved`:**理由全文不截**;reference 本身在報告裡是截過的形——`value` 200、`url`/`statement` 300 字元、`rests_on` 只列前 5 個——**足以認出是哪一筆,不足以無損重建**;MCP 面另只有前 20 筆帶 reference 的內容、其後只回 `field`/`value`/`reason`)、一半是 git 的移除前副本——實跑要求那筆 venue 檔已 commit、乾淨(`assertRecordsRecoverable`),**無損還原以 git 為準**。逆操作是 `--references` 把同一筆加回去(append-only、位元組相同的略過)——要有那筆的**逐字**內容,所以以 git 的移除前副本為準(報告的截過的形對超過上限的 value/url/statement 與多於 5 個的 `rests_on` 不夠),且限 `issn`/`names`(通用寫入面不收 `authorized`/`note`,那兩格只能從 git 還原);移除本身不動 reference 指的號或名字。落地是程式:定位=`field`+`value` 的位元組相等(可再以與 `--references` 同名的鍵縮小),定位不到、定位到多筆、兩個定位指到同一筆、verdict 與 `paginated` 欄位(各有自己的面,`paginated` 是判定且要留史)、理由空白或過長、一次超過 200 筆都整批拒絕零寫入;**單獨呼叫**,不與 `update-venue` 的其他參數組合 **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘——會刪判定記錄的移除腿一律過閘;MCP 不閘 | -| `update-venue --edit-name-segment`/`akashic_update_venue.edit_name_segment`(#675) | AI | 「這一段的時間是錯的」「這一段不該存在」是判定——`names` 的一段(`TemporalValue`)帶時間欄位、`source`、`note`,哪一格對要讀來源才知道,字串謂詞做不出來(例:兩本刊合併時同名的兩段只差 `source`,哪一段的來源才對)。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的一半是報告(`nameSegments[].reason`:**理由全文不截**;`before`/`after` 各欄位截在 300 字元、時間欄位 40,超過的形只用來認得,MCP 面只有前 20 項帶它們),另一半是 git 的改寫前副本(改寫前要求該 venue 檔已 commit、乾淨;每一項都沒有變動時不寫檔也不過閘)。逆操作是再呼叫一次把值改回去——`before` 在報告裡,所以改寫是**有具名逆操作的**;刪掉的一段用 `--add-name` 補名字、再以本面的 `set` 補回時間、`source`、`note`(`before` 在報告裡),或從 git 取回。落地是程式:形狀走封閉的鍵集合、定位是 canonical 名字加給了的鍵的位元組相等(`null` 是要求缺席)、改完的時間欄位過 `Venue.nameSegmentRangeIssue`、這次造出的 `validate()` error 在寫之前歸因到這次呼叫、任何一項不成立整批拒絕零寫入。**不與其他腿組合**(單獨呼叫,同 `--remove-reference`):其餘腿增減同一份 `names`,混在一次呼叫裡「定位的是哪一段」交錯。**程式不替人動同一筆記錄的其他判定**:移除一個名字的最後一段時,它若還在 `authorized`/`variant`/`field: names` 的 reference 裡就具名拒絕並指路(`--authorize`/`--remove-reference`)——`authorize` 遇到被 reference 指著的舊指定同一條紀律(D60 同向)。它若有名字分類的判定記錄(#564):最後一筆是「撤回」的,記錄隨名字一起刪(使用者 2026-10-02 裁決 #564 第 2 點;報告 `judgementRecordsRemoved`,內容在 git 的上一版);不是撤回的具名拒絕(先撤回,再刪)。合併(`resolve-divergence`)對同名而不能並存的段拒絕、出路指向這個面——合併是程式落地、這一面是人/AI 的判定,兩者的分工與 `identity-is-judged-not-matched` 的「提名(程式)→ 判定(AI)→ 落地(程式)」一致 **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘(只算帶 `remove` 的呼叫)——會刪判定記錄的移除腿一律過閘;MCP 不閘 | -| `resolve-venues --drop-venue`/`akashic_resolve_venues.drop_venue`(#572) | AI | 「這條邊不該在」是判定——兩條同刊名的邊哪條多餘、publisher 推出來的邊是不是載體,字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決,同上一列 `--remove-issn`),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的是報告與 git 的移除前副本(移除前要求 work 檔已 commit、乾淨)。**key 邊的前提把判定的範圍收窄**:只刪「同一 venue 還有另一條 key 邊」的那種,verdict 因此不受影響;唯一的 key 邊要先走 `demote`(那一格處理 verdict)。落地是程式(index 位移、整批拒絕零寫入) | -| `update-venue --authorize`/`akashic_update_venue.authorize`(#554) | AI | 「這個名字是本刊的對外形」是判定——與 `authorize-names`(#81)同一句話換到 venue。語意是同 `WritingSystem` 原子替換(不是 append:「每書寫系統至多一個」對 470 筆機械值必擋 append);被換下來的舊指定**移出 authorized、留在 names、不標 variant**——程式不替呼叫端多說「它是異寫」(R1 verify D1)。~~**不留 judgement**(與同列的 `authorize-names`、#471 `add_variant` 一致)——這是**有記錄的裁決不是遺漏**:三個名字分類面要不要留、留什麼形狀一次裁(#564);**#564 已於 2026-10-01 裁決要留**(名字分類面全部,含確認既有值與 #559 的撤回、#557 的 organization),另案落地(需要 store format bump);在落地之前這一格對「留 verdict」的義務是**未兌現的**,而它的代價住在上面 #553 那列(合併端分不出判定與機械值)。~~ → **#564 落地(2026-10-01):留。** 理由必填(`--judgement`/MCP `judgement`,至多 4,096 位元組)、證據 digest 可空(`--rests-on`,至多 20 個),每次指定寫一筆 `field: authorized` 的 `指定:理由`、對已是對外形的名字寫一筆 `確認:理由`(先前是無聲的 no-op);同書寫系統被換下的舊指定、被抬出 variant 的名字各寫一筆 `撤回:`(理由由程式在前面說出原因)。一次呼叫的理由套用到該次每一筆記錄;位元組完全相同的不重寫(報告 `judgementsRecorded`);缺理由整批拒絕零寫入,需要 store format ≥ 22。記錄錨定在 names 不在分割,所以撤回之後判定史仍在、`authorize`/`unauthorize` 的 pinned 檢查只看名字分類記錄**以外**的 `field: authorized` reference。**這一格對「留 verdict」的義務自此兌現**,合併端因此分得出人判定過的與機械值(上面 `resolve-divergence` 列)。與 `paginated` 不同一次呼叫(兩個判定各要自己的理由)。落地是程式(入口的輸入驗證整批拒絕零寫入;名字內容的不變式住在 `Venue.validate()`——五條的規範文字只在 `docs/store-format.md` §5.7(這裡不複述:R6 verify 第 10 列抓到本列是七份副本裡第一個分岔的)——所有寫入者存 canonical,D8;分割互斥同樣由 `Venue.validate()` 擋、不重造) | -| `update-venue --add-variant`/`akashic_update_venue.add_variant`(#471) | AI | 「這個名字是那個名字的異寫」是判定,不是「不在對外清單裡」的推論(#471 的論證:補集規則是一個不做判定的操作充當判定寫入者,已由 #567 退場刪除)。先前這一格沒有列——#471 寫入面新增時漏記;本列補上。**#564 落地(2026-10-01):留記錄**:每個被標的名字寫一筆 `field: variant` 的 `指定:理由`(已是異寫的寫 `確認:理由`),理由必填(`--judgement`)、證據可空,需要 store format ≥ 22。不在 names 的一併 append 進 names(兩個分割都是對 names 的標記);被 `--authorize` 抬出 variant 時連帶寫一筆 `field: variant` 的 `撤回:`。**沒有撤回 variant 的獨立面**(有記錄的缺口:venue 的 variant 只有被抬出時連帶撤回;要不要補面是另一個裁決)。記錄只由名字分類面寫:`--remove-reference` 明文拒收 `variant`。落地是程式(名字的 vetting 整批拒絕零寫入、互斥由 `Venue.validate()` 擋) | -| `update-venue --unauthorize`/`akashic_update_venue.unauthorize`(#559) | AI | 「這個名字不是(或不再是)本刊的對外形」是判定——撤回回到 `venue-entity` spec 的未標(「不作任何宣稱」),與 `clear_paginated`(#500)、`resolve-venues --demote`(#418)同一格:替換只能換成另一個名字,只有一個名字的 venue 在此之前 authorized 一旦在就永遠在(使用者 2026-10-01 裁決)。逐名、必須是現有成員(相等看 `NameIdentity.canonical`,同 `--authorize`),移出後留在 names、不標 variant——程式不替呼叫端多說「它是異寫」(D1 同一個立場);與 `--authorize` 共用 `AuthorizedDesignation` 一份(#557 起 organization 的 `--authorize` 也用它;organization 沒有撤回面)。**`--authorize` 不是精確逆操作**(R1 verify):名字與分類(未標)回得來,但它對不在 authorized 裡的名字接在尾端,而預設顯示名取 `authorized.first`——撤回再指定,多書寫系統 venue 的預設顯示名可能換書寫系統,遺失的只有 authorized 內的順序,所以報告逐名帶原 `index`、預設顯示名因此改變時多 `displayNameChanged`。**不設 git 閘**(使用者沒有裁決要閘;名字與分類都留在 names)。~~判定記錄:#564 已於 2026-10-01 裁決名字分類面全部要留(含撤回),另案落地(需要 store format bump);在落地之前不寫記錄,報告的 `authorizedWithdrawn`(含 `index`)是這個判定唯一的痕跡。~~ → **#564 落地(2026-10-01)**:每個被撤回的名字寫一筆 `field: authorized` 的 `撤回:理由`,理由必填(`--judgement`),先前的記錄留著(判定史);需要 store format ≥ 22;報告的 `authorizedWithdrawn`(`name`/`index`)與記錄說的是同一批名字(store 拼法)。落地是程式(不是現有成員、同一個名字又在 `--authorize`、被 `field: authorized` 的一般 reference 指著(名字分類的判定記錄不算——它們錨定 names),都整批拒絕零寫入;整項空白回報在 `unauthorizeDropped`;分割互斥與子集仍由 `Venue.validate()` 擋) | -| `update-organization --authorize`/`akashic_update_organization`(#557) | AI | 「這個名字是本機構的對外名稱」是判定——與 `update-venue --authorize`(#554)同一句話換到 organization,**同一份邏輯**(`AuthorizedDesignation`;`AuthorizedNames` 的 doc:venue 與 organization 的「哪個名字對外」是同一個問題,不該有兩套答案)。在此之前 `Organization.authorized` 零寫入面(使用者 2026-10-01 裁決形狀 2:新增部分更新面,先提供 `--authorize`)。同書寫系統原子替換,被換下的移出 authorized、留在 names(organization 沒有 variant)。**沒有撤回面**:首輪實作順手加了 `--unauthorize`,超出裁決、且在 organization 上不是「回到未判定」(names 只增不減——當時;#564 修正輪起 `--remove-name` 只刪已撤回的名字,仍沒有一般的名字移除面——撤回後剛加進 names 的名字成為 fallback 顯示名),R1 verify 之後拿掉,待使用者裁決。~~判定記錄:#564 已於 2026-10-01 裁決名字分類面全部要留(列舉的五個面裡 organization 只有 `--authorize`),另案落地(需要 store format bump);在落地之前不寫記錄。~~ → **#564 落地(2026-10-01)**:organization 新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`),`authorize` 的名字必附理由、證據可空;每次指定、確認各寫一筆 `field: authorized` 的記錄(被換下的舊指定也寫一筆撤回),需要 store format ≥ 22;organization 沒有 variant,不會有 `field: variant`。organization 沒有撤回腿(`--unauthorize` 於 #557 R1 verify 之後拿掉、待使用者裁決),所以 #564 的五個面裡 organization 只有 `--authorize`。落地是程式(入口的輸入驗證整批拒絕零寫入;子集與每書寫系統至多一個由 `Organization.validate()` 在寫入閘擋,不重造;key 重複整批拒絕,#669/#670;都已是對外名稱而同一句理由已記過時不寫檔,第一次說「確認」會寫一筆記錄) | -| `update-entry --remove-field`/`akashic_update_entry.remove_fields`(#544) | AI | 「這個值不是來源給這個欄位的資料」是判定——#544 的 21 筆 `abstract` 是 Crossref 的錯誤頁、兩筆的 `journaltitle`/`publisher` 是補助計畫名稱與總統令字號,要讀內容才知道,字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決),store 內「留 verdict」有記錄地不兌現——兌現的是報告與 git 的移除前副本(實跑要求 work 檔已 commit、乾淨)。**這個不兌現是 Claude 代裁、待使用者確認**:2026-09-27 的裁決是為 #588/#572/#586 下的,延伸到 `fields` 的值沒有 #544 自己的裁決紀錄;issue 要的 #450 形記錄(work 側 `references` 的退役記錄)沒有做。逆操作:**一般字串鍵**(`abstract`、`journaltitle`、`publisher` 之類)的值可由 `enrich` 補回(鍵已缺席,add-only 收得下);其餘——`doi`/`pmid`/`isbn` 的殘留(`enrich` 把它們導向結構化欄位,不是殘留的 `fields[…]`)、`enrich` 拒收的鍵(`issn`、`type`、`title`、`venues`)——以 git 還原那個 work 檔。同一次移除一併刪掉的 `fields.<鍵>` reference 要從 git 的移除前副本取回,沒有工具面。**反面:同一個值會被 `import-wos` 回填、`enrich`、Zotero pull 補回來**——store 不記得它被判定過不屬於這裡(報告的 `reintroductionNote`)。落地是程式(鍵不存在、同鍵兩次、理由空白或過長都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫 | -| `update-entry --add-source`/`akashic_update_entry.add_sources`(#614) | 程式 | 把已存的內容寫進 `akashic.sources`——**落地不是判定**:「這份 PDF 是這篇」的判定在上游(`akashic-fetch-fulltext` 的驗證步驟:頁數、首頁標題、DOI),證據隨 `store-source` 的 note 記在 `sources/index.jsonl`;本面收的是已判定的 (citekey, digest),同輸入必得同輸出。add-only、冪等(已在的不重寫、集合語意),可預期失敗(digest 不合法、本機沒有存檔、孤兒 blob、index 壞到判不出來、同一次重複)整批擋零寫入。**具名逆操作是 `--remove-source`**(#677,見下一列):在它之前沒有移除腿,連錯了只能以 git 還原那個 work 檔。預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--remove-zotero-source`/`--remove-source`)組合(兩種編輯不混在一個呼叫;四條腿兩兩互斥) | -| `copy-zotero-attachments`(#606) | 程式 | 把 Zotero 附件的位元組複製進 `sources/` 並追加連到 `akashic.sources`——**搬運與落地,不是判定**:「這個附件屬於這一筆」的關係是 Zotero 的 child item 早就記下的(`attachments` 的 `zotero:` 記錄,由 pull 產出),本面不重新判斷它、也不動它。同輸入必得同輸出(digest 由位元組決定);冪等(digest 已連過、位元組在本機且 index 有條目的不重複——連結在而本機缺位元組的只補存、缺取得記錄的只補記,都不改連結;已存的 blob 不重寫、index 不重複 append);可預期失敗(`sources/` 沒被版控排除、index 有壞行、寫入閘、被改寫的 work 檔未 commit)整批擋零寫入,逐檔的問題(路徑不合、檔案不在、不是普通檔、0 byte、計畫之後內容變了)具名略過而其餘照跑;代價量測(乾跑列出每個檔的大小與 digest)。#703 起「位元組在本機」要**內容相符**(逐塊比對既有 blob),不符具名列出、不覆寫;超過 256 MiB 的檔具名略過——兩者都是決定論式的比較,種類不變(R1 起位址上是目錄、symlink 或讀不到而判不出來的,新連結也具名略過——判不出來不是判定,是不做)。**具名逆操作是判定型的另一格**:`update-entry --remove-source =理由`(#677,理由必填、只進報告)收回連結,blob 與取得記錄留在 `sources/`。預設乾跑、`--apply` 才寫;不與判定型的腿混在一次呼叫裡(它只做這一件事)。 | -| `update-entry --remove-zotero-source`/`akashic_update_entry.remove_zotero_sources`(#680) | AI | 「這個 Zotero 來源不屬於這一筆」是判定——來源是主來源還是附加來源、是記錯了還是真的屬於這篇,要讀那個條目與這筆記錄的內容才知道(#610 的多筆宣稱、#679 的沒記 `library_id` 的附加來源,處置都是這個判定),字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內有記錄地不兌現——兌現的是報告與 git 的移除前副本(實跑要求 work 檔已 commit、乾淨)。**沒有具名逆操作**:被移除的來源只在 git 的移除前副本與報告裡;重新匯入不會把它接回這一筆(那個條目若在 Zotero 端仍在,是否另建一筆新 entry 依移除之後還有沒有別的 entry 宣稱這個來源而定——`reimportNote`/`reimportEffect` 逐來源說),要接回只能手改 YAML 或走攣生合併。主來源移除時附加來源**不升格**(同 App 的「與 Zotero 脫鉤」),所以本面不替呼叫端決定哪個來源接管欄位的改寫權。落地是程式(來源鍵不在這筆、同來源兩次、形狀錯、理由空白或過長都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--add-source`/`--remove-source`)組合 | -| `update-entry --remove-source`/`akashic_update_entry.remove_sources`(#677) | AI | 「這份內容不是這篇的副本」是判定——`--add-source` 收的是上游已判定的連結(`akashic-fetch-fulltext` 的驗證:頁數、首頁標題、DOI),而**收回**要重新讀內容才知道連錯了,字串謂詞做不出來。理由必填(`=理由`)、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內是**有記錄地不兌現**:兌現它的一半是報告(`sourcesRemoved`,全文)、一半是 git 的移除前副本——實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`)。逆操作是 `--add-source` 再連回去——digest 沒有變、blob 與取得記錄本來就沒動(本面只收回宣告,不刪 `sources/`),所以還原是無損的(只要本機仍有那份存檔,`--add-source` 的存在性閘才過)。落地是程式(digest 形狀不對、不在清單上、理由空白或過長、同一個 digest 兩次都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--add-source`/`--remove-zotero-source`)組合 | -| `dismiss-divergence`/`akashic_dismiss_divergence`(#586) | AI | 「這個問題不成立/不再延後判定」是判定——與 `record-divergence` 的「當場記錄而非當場判斷」相對:那一格記下判定尚未做出,這一格判定它不必再做。理由必填、**只進報告**(移除面一族的裁決),store 內「留 verdict」有記錄地不兌現——兌現的是報告與 git 的刪除前副本(要求記錄檔已 commit、乾淨)。只刪記錄、候選實體與參照都不動,所以沒有判定被連帶銷毀 | -| App 裁決台 Orphans:移到垃圾桶/與 Zotero 脫鉤(#605)/拿掉已刪除的附加來源(#609) | AI | 「上游刪了,要不要放棄等待」是判定——Zotero 端的刪除可能是暫時的(同步途中、誤刪後救回),程式不替人決定放棄;三個動作都在動作當下重新讀盤確認形狀(`Entry.zoteroLinkState`,TOCTOU)後才寫。**拿掉已刪除的附加來源**套用移除面一族的使用者裁決(2026-09-27):理由必填、只進回傳的結果(不寫進 store),移除前要求記錄檔已 commit、乾淨——所以「留 verdict」在 store 內**有記錄地不兌現**,兌現的是結果與 git 的移除前副本。**#683 起 git 閘的外層與理由上限與 service 的移除面是同一份**(StoreIO 的 `LibraryStore.recordRecoverability`/`LibraryStore.maxStatementBytes`):先前 App 自己寫了一份(自己的路徑解析、自己的措辭、自己的 `4_096`),改一邊另一邊不會跟著變。**「與 Zotero 脫鉤」早於那一族裁決**(#605 R1 使用者裁決),不帶理由與 git 閘——同一個裁決台上兩個移除動作的閘不一致,是有記錄的缺口,要不要補是另一個裁決;垃圾桶走系統 Trash,可救回。三者都只在 App(CLI/MCP 沒有 orphan 裁決面)。**#609 R1 verify 補記**:(1) 三個動作共用同一條定位守衛(citekey 重複或與另一筆共用 id 時拒絕、零寫入);(2) 拿掉已刪除的附加來源拿掉的只能是使用者看到並確認的那一組(呼叫端傳入看到的來源鍵,動作當下的那一組不同就拒絕),git 閘通過後記錄若又被改動也拒絕;(3) **#609 把「與 Zotero 脫鉤」的無理由、無 git 閘延伸到「沒有主來源、附加來源全部已刪除」這個新形狀**(先前脫鉤只作用在主來源 orphan 的形狀)——這是 #605 R1 使用者裁決的延伸,與 2026-09-27 移除面一族裁決(新增的移除路徑都要理由與 git 閘)不一致,要不要補閘待使用者決定 | -| `record-divergence`(#77) | AI | 「當場記錄而非當場判斷」——記下判定尚未做出,本身也是判定型工作的一部分。**#611 起記錄不只由這個面寫**:`import-zotero` 以程式寫 DOI 提名(見 `import-wos`/`import-zotero` 那一列,種類是程式),那種記錄沒有判斷;補上判斷走本面對同一組候選的重錄(同組候選=同一筆記錄,「無→有」本來就允許) | -| `rename`/`rename-person`(#35/#232/#395) | 程式 | 改名不改身分;參照集合由封閉列舉逐條窮舉、可機械檢查。**#554 R21/R22/R23(D60/D63/D62/D64)**:目的鍵上已有 verdict(三種 holder 與 quarantined 檔都掃——quarantined 檔以**位元組**比對 `:`,不含空白、YAML 折行打不斷;R22 的行級比對被折行擊穿,R22 verify 四席同指)即具名拒絕、零寫入——程式編輯不得銷毀判定編輯的產物;唯一的收攏是完全相同(含 judgement 與 rests-on)的重複,零資訊損失(R21 verify 第 18 列:D60 曾只記在 `resolve-divergence` 列,本列才是它的家)——**「完全相同」是位元組層的話**(R24 D65,`ProvenanceReference.byteExactKey`;R23 verify Codex 第 1 列:R23 以合成 `Hashable` 當鍵,Swift `String` 的相等是 canonical 的,NFC/NFD 兩種拼法被折成一筆——R16 D42 修過同一個缺陷的另一處);留下的同鍵異 judgement 由 `StoreHealth.duplicateVerdictRecords` 報(D64)——rename 那一步零損失,下一次合併會收攏、回報在合併的 `verdictsCollapsed`。**#642 R1 verify:library 的成員規則(第 16 條邊)也在同批遷移裡**——換掉的只是規則裡的 citekey,不判定;registry 檔要 tracked 且 clean(同 #573 一族的可回溯閘),與其他被改寫的記錄同一批預檢 | -| `migrate-*` 一族/`fmt`(#227/#304/#325/#394/#422) | 程式 | 格式遷移;不可逆但決定論,前置是 git 追蹤,乾跑逐筆過目 | -| `repair-venue-names`(#575) | 程式 | venue 名字不變式(§5.7)的機械修復,**只改確定性的那一類**:字串只違反 canonical 形(`NameIdentity.canonical`=NFC+空白收斂——收錄條件是「空白不是名字的一部分、NFC 不改 Swift 的相等」),正規化後通過名字不變式,整筆記錄改完通過 `Venue.validate()`(沒造出近重複、authorized ⊆ names 與分割互斥仍成立),且沒有 reference 以位元組指著舊拼法。同輸入必得同輸出,改完再跑一次是空計畫(冪等);逆操作是 git——`--apply` 要求那些 venue 檔已 commit、乾淨,任一筆過不了寫入閘整批零寫入。**判斷一律不做**:刪哪個不可見字元、近重複留哪一筆、指著舊拼法的 reference 要不要跟著改——只具名與理由(D8 的「不猜、不靜默修」);同一筆記錄還有任何判斷項時,它的確定性改寫**全部延後**(記錄還有 error 就寫不進去)。**不併進上一列**:那一列是格式遷移與序列化對齊,本列改的是名字的內容位元組,而 `fmt` 對帶 error 的記錄刻意拒絕。**誠實邊界**:NFC 的單一碼位替換(CJK 相容表意文字 U+FA10 → U+585A)是位元組層有損的——它仍是 canonical 形的定義、仍算確定性,乾跑逐碼位說出來 | -| `import-wos`/`import-zotero`(#206) | 程式 | 對映歸對映、收集歸收集;conflict 交人,不猜。**#611 起 `import-zotero` 另寫歧異提名,種類不變**(使用者 2026-10-01 裁決「照建,並自動記一筆歧異提名」):新建的 work 與另一筆 work(既有的,或同一趟也新建的)共用 DOI 時照建,並以 `recordDivergence` 寫一筆 work 形狀、**沒有判斷**的歧異記錄,候選是那一對。它是**提名**(recall)不是判定:DOI 相等在攣生上不裁決(`identity-is-judged-not-matched`「識別碼終結指涉,不終結描述」——勘誤與原文共用 DOI、一筆作品可有多個 DOI),而「哪兩筆共用 DOI」是同輸入必得同輸出的比對。judgement/prefers 一律不填;已有涵蓋這一對的記錄不重寫(重寫會換掉 question、可能碰到人補上的判斷);無法唯一定位的候選不點名、只報告。**提名要有資訊量才記**(R1 verify):共用同一個 DOI 的 work 超過 10 筆時一對都不記、改報一列——那多半是書或資料集的概念 DOI,逐對提名是 C(k,2) 筆沒有資訊量的 recall(DA 實測 k=80 → 3,160 筆);歧異記錄的 id 只由候選 key 決定、不含形狀,所以同 id 的另一種形狀記錄既不算涵蓋、也不覆寫(報 failed)。判定與合併仍是 `resolve-divergence` 那一列(AI)——提名(程式)→ 判定(AI)→ 落地(程式)的第一段 | -| `create-entry`/`createEntries`(#206/#455) | 程式 | citekey 由規則生成;批次內碰撞由 `existing` 累積消解;可預期失敗整批擋 | -| `enrich-from-zotero`/generic enrich(#340/#458) | 程式 | 只補不存在的鍵;來源給什麼收什麼,不判定 | -| `bootstrap-people`/`-organizations`/`-venues`(#367;#547/#548/#563 補註) | 程式 | 門檻建檔是提名不是判定——建出來的實體仍待 `resolve-*` 判定歸戶。**#547 起 `bootstrap-people` 多扣住一類**:彼此寬鬆共鍵而兩邊都還沒有記錄的群(`pendingMutual`),與既有的「與既有 person 共鍵」對稱。**種類不變,仍是程式編輯**——它用 `LooseNameKey` 做決定論式**提名**(同輸入必得同輸出),判定留給人/AI;`identity()` 一行不動,因為放寬它就會變成程式做身分判定(`identity-is-judged-not-matched`)。這一列**不另開新列**:新增的是既有寫入面的一個報告欄位與一道扣留,不是新的寫入面,而本表的列是「寫入面」。**#548 對 venue/org 做同一件事,種類同樣不變**——但判準是另一套(`LooseTitleKey`:標點/`&`/前導冠詞),不是把 `LooseNameKey` 平移過去。理由是人名與刊名的變異形狀不同:人名會被索引系統重排(`Hsu, Yung-Fong`)、刊名不會,而 token 集合相等對刊名是誤判來源。那是一次裁決,寫在 `LooseTitleKey` 的檔頭。**#563 起(2026-10-01,使用者裁決)`bootstrap-venues` 建檔不寫 `authorized`**:先前每筆寫 `[names[0]]`(字母序第一個寫法),那是判定形狀的寫入由不做判定的操作寫出——#471 對 `variant` 修掉的同一形狀,#554 補了判定面卻沒動這個寫入者。現在與 `add-venue`(#227)一致留空,`displayName` 退到 names 的第一段、顯示不變;三個 bootstrap 自此都不寫 authorized(`bootstrap-people` 本來就不寫,指定走 `authorize-names`)。既有的 470 筆機械值不動,#600 裁決不跑全量 campaign、按需判定——它們仍分不出是不是人確認過的,下面 `resolve-divergence` 列的「提醒不擋」因此不變(**#564 落地後**:機械值沒有名字分類記錄、人判定過的有,合併端分得出兩者——機械值仍是提醒、有記錄的拒絕;見該列)。**#709 第三次 verify(2026-10-04)重新確認,種類不變**:literal 來源看完整的 load(R3 曾改用以 entities/ 為準的視圖,那是寫入候選面、超出使用者 2026-10-01 的裁決範圍,確認之前收回);計畫含同一筆記錄的 legacy 拷貝時開頭說出幾份、計數多算一次(`legacyCopiesInPlanNote`,2026-10-09 起改名 `planBlockersNote`)——提名的輸入照實,不安靜略過拷貝裡的 literal。**#709 第四次 verify(2026-10-05,使用者裁決)重新確認,種類不變**:literal 來源維持完整的 load(裁決 1:寫入候選面一律看完整資料);計畫讀到拷貝時 `--apply` 整批拒絕、零寫入(裁決 2)——拷貝讓一對相同的 literal 算兩次、可能越過 `--min-occurrences`,那是可預期的失敗,程式編輯欄的義務是整批擋,不是附註之後照寫。只數流進該命令 literal 來源的種類(`BootstrapPlanSource`)。**#709 b36 verify(2026-10-09,使用者裁決)重新確認,種類不變**:拒絕條件改成「計畫讀到的種類裡 load 標出的拷貝 ∪ 無法唯一定位的記錄」(`LibraryLoad.planBlockers`)——`bootstrap-people` 的計畫也讀 person 拷貝(裁決 1:它讀 person 的 key、名字與否決/確認史,拷貝可以讓候選變多、讓 literal 安靜消失、讓新 key 多後綴,「只會讓候選少一個」是假的),works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`(裁決 2)。理由同上一句:計數的輸入有兩份、store 說不出哪一份才算,是可預期的失敗 | -| `library add`/`remove`、`tag`、`link`、`set-status`(#219/#258/#455) | 程式 | 集合語意,冪等。**#642 起 `library add` 對規則型/文件型 library 逐筆比對成員規則、不符的不寫並具名,種類不變**:比對是決定論式的(venue key 邊、type 集合、排除清單、文件的 `cites`——同一份 store 對同一筆必得同一個答案),不是判定;判定(這筆作品屬於哪本刊、這篇文件引了什麼)早已由 `resolve-venues` 與 `cites` 的寫入者做完並留下記錄,add 只是讀它。venue 只有未歸戶 literal 的不寫(`literal-first-then-key`:不猜),指路 `resolve-venues`;未標性質的 library 拒絕 add(查不到依據) | -| `library set-kind`/`akashic_libraries(set-kind)`(#642) | 程式 | 宣告 library 收什麼是使用者的決定——`source-of-truth-over-consent` 表「library 的性質與成員規則」一列(#642 同批加表),使用者的話就是依據;寫入本身是整值替換、冪等,規則指涉的 venue/文件要在庫。**它不判定現有成員**:不符新規則的成員列出來、不自動移除(移除是另一個寫入,`library remove`)。**#642 R1 verify:整值替換回顯先前的值,替換既有的性質要求 registry 檔已 commit**——程式編輯的義務是「冪等或有具名逆操作、代價要量測」,而舊規則(排除清單是人的裁決)在沒有 commit 時沒有逆操作;所以與 #573 一族同一支閘。**標成 topic 等於關掉檢查**,工具不替呼叫端擋(那是使用者的話),但規則與 skill 明寫不得為了讓 add 通過而替使用者標 topic | -| order-insensitive collapse(#461)、verdict holder 遷移(#463) | 程式 | 對已判定結果的機械搬移;只收本次觸及、不碰未觸及 | -| `authorize-names`(#81) | AI | 「哪個名字對外」是人的判斷,建檔不得機械偽造(#227)。**#564 落地(2026-10-01)**:`--apply` 必附 `--judgement`(整批一句、不帶證據:一個批次共用同一組 digest 等於宣稱每個人的名字都依據同一份文件),每個寫入的 person、每個被採用或提名的名字各一筆 `field: authorized` 的 `指定:理由`;本面只對 authorized 為空的人寫入,所以不寫「確認」;乾跑不需要理由;需要 store format ≥ 22。**不再寫 store marker**(先前 `--apply` 結尾無條件把 marker 寫成 supported,在 format 22 下會安靜地替使用者升 marker——升 marker 是使用者的動作)。先前不留記錄。**#709 第三次 verify(2026-10-04)重新確認,種類不變**:計畫與 `--apply` 的拒絕都看完整的 load(R2 曾讓計畫取視圖,改名留下拷貝的 person 因此從整批拒絕變成寫入);乾跑算出 `--apply` 會擋的那幾筆並說出來(`blockedKeys`),乾跑與實跑對同一個 store 說同一件事。**#709 第四次 verify(2026-10-05)補記,種類不變**:寫入集合裡有任何一筆的 id 還有 legacy 拷貝時也整批拒絕——先前只擋「legacy 那份本身在寫入集合裡」,改名一對而 legacy 那份已有 authorized 時只寫 entities/ 那份,兩份各帶不同的 authorized 狀態;與三個 bootstrap 對同類狀態的處置一致(使用者 2026-10-05 裁決 2) | -| `enrich`/`akashic_enrich`(#458) | 程式 | generic add-only:只補不存在的鍵、來源給什麼收什麼、不判定——同輸入必得同輸出。DOI → citekey 由程式做是**識別碼例外**(`identity-is-judged-not-matched`),命中 ≥2 筆時**拒絕不判定**(`ambiguous`)——判定屬 #459 的攣生管線。`enrich-from-zotero` 自此是它的 adapter,種類不變。**#517 起它寫來源 reference,而 kind 必然是 `retrieval` 不是 `judgement`**——那不只是形狀偏好:judgement 是 AI 編輯欄的產出物(「留 verdict、必附理由」),一個決定論式的補值面**不該發出判定**。「這個值取自那份存檔」是一次取得的記錄,不是推理。**#655 重新確認,種類不變**:補進去的 `date` 也寫 retrieval(新格 `field: date`,store format ≥ 20),理由同上;補進去的 `authors` **不寫**——`field: authors` 那一格只收 judgement(作者位記錄的拆分/移除,value 是已退役的 literal),一個決定論式的補值面寫進去,要嘛發出判定、要嘛讓同一個 field 承載兩種相反的語意,兩者都不可接受;不寫的理由進報告(`provenanceOmitted`) | +本段已搬到 [`docs/rulings/two-kinds-of-edits.md`](../../docs/rulings/two-kinds-of-edits.md)(不自動載入);需要查過往裁決或新增一列時讀那份檔。本檔其他段落說的「下表」「上表」「這張表」指那份檔裡的表。 新增下一個寫入面 = 在這張表加一列,並在 `mcp-cli-parity` 的表裡同時裁決它的兩面。 diff --git a/.claude/rules/zero-instance-guards.md b/.claude/rules/zero-instance-guards.md index 1a97a812..bd3c701c 100644 --- a/.claude/rules/zero-instance-guards.md +++ b/.claude/rules/zero-instance-guards.md @@ -49,7 +49,7 @@ **尚未實作的正面能力**,不是相容路徑;那條規則第 2 條要量的「還有誰在走這條路」,正是第 24 列的觸發條件之二 (含 org 候選的 divergence 記錄數)。#555 R1 verify DA 第 22 列。 -**量測與歷輪補記住在量測文件**(#711,使用者 2026-10-05 裁決):本檔每個 session 自動載入,所以每一列只留情形、裁決、理由的核心;各列的量測腳本、逐輪 verify 的補記與後來補上的段落,原樣放在 [`docs/zero-instance-measurements.md`](../../docs/zero-instance-measurements.md)(下稱**量測文件**,不自動載入),依列號分節,每一列尾端的括注指向它。`zero-instance-rows-audit` 與 `measured-numbers-audit` 兩份都讀。 +**量測與歷輪補記住在量測文件**(#711,使用者 2026-10-05 裁決):本檔每個 session 自動載入,所以每一列只留情形、裁決、理由的核心;各列的量測腳本、逐輪 verify 的補記與後來補上的段落,原樣放在 [`docs/zero-instance-measurements.md`](../../docs/zero-instance-measurements.md)(下稱**量測文件**,不自動載入),依列號分節,每一列尾端的括注指向它。`zero-instance-rows-audit` 與 `measured-numbers-audit` 兩份都讀;2026-10-15 起裁決表(含每一列的核心)與「各列共通的東西」也搬到 [`docs/rulings/zero-instance-guards.md`](../../docs/rulings/zero-instance-guards.md)(同樣不自動載入),兩支守衛連它一起讀。 ## 規則 @@ -60,272 +60,13 @@ 會在邊界上自己長出沒人同意的答案,而那句話與下表**是兩份不會一起改的規格**。要判斷新情形, 讀下表的理由欄,然後**加一列**。 -## 裁決史(封閉列舉——現有 102 列,一列不多一列不少) +## 裁決史(已搬出) -| # | 情形 | 裁決 | 理由 | -|---|---|---|---| -| 1 | **零實例、成本一行、前件精確**(#254:`duplicate-rationale` 擴及同命題內兩條 relation。全 corpus 被 >1 條共用的 rationale 種類數 = 0)| ✅ **寫** | 成本是一行前件;而不寫的話,那個形狀第一次出現時**不會有任何跡象**——它會通過 validate、通過測試,只在有人逐條讀 corpus 時才看得出來 | -| 2 | **零實例、成本一行、但前件寫寬了會誤傷**(#253:issue 的 Expected 寫「對所有 status 檢查」。若照字面實作成「任一 relation」會 flag 上百條)| ✅ **寫,但先實測界定前件** | 判準不是「要不要寫」而是「前件多寬」。實測「rationale 引用了 issue」恰好命中 issue 具名的三筆、零附帶損害;「任一 relation」則會 flag 上百條。**沒有那次量測就不該動手** | -| 3 | **未涵蓋不得冒充通過**(#326:`BibValidator` 的必要欄位表只涵蓋 7 個 entry type,store 另有 47 筆不在表內)| ✅ **寫「未涵蓋」的出口** | 這一列不是「要不要寫守衛」而是「守衛沉默時要不要說話」。若只回 issues,「沒被檢查」與「檢查過且乾淨」在輸出上完全一樣——`lossless-intake` 執行細節 3 的「靜默是最糟的形式」 | -| 4 | **猜錯不會有跡象,而猜的代價是整組欄位需求**(#367:`bootstrap-venues` 的型別衝突守衛。同一個刊名來自不同種類的來源欄位時不建檔、交人裁。實測 441 個 literal 全部只對應一個欄位種類 → 0 實例)| ✅ **寫** | 前三列的代價都落在「看不見」;這一列的代價是**看得見但看起來是對的**。`VenueType` 決定「哪些欄位存在」(#324 的 §11 判準),所以取錯 type 不是標籤錯而是**整組欄位需求錯**——而那筆記錄會通過所有檢查,因為它確實滿足了(錯的)那一組。守衛的成本是一個 `guard`,不寫的代價是一筆看起來健康的壞資料 | -| 5 | **重複看起來像多一份覆蓋**(#407 R67c:負控 harness 裡兩個 case 若輸出逐字相同即互相不可區分。實測 35 個 case → 未具名的重複 0 組)| ✅ **寫** | 前四列的代價都落在「被守衛的東西」上;這一列落在**守衛自己身上**。一個重複的 case 會讓計數從 35 變 36、多印一個 `✓`,維護者讀成「又多檢查了一件事」——而那是同一件事查了兩次。它不是看不見(第 1 列)、不是假訊號(第 4 列)、不是沉默的歧義(第 3 列),是**對測試自己的覆蓋率說謊**。成本是零:既有迴圈本來就跑完全部 case,這裡只是分組 | -| 6 | **假紅會讓所有紅燈失效**(#407 R67e:ROBUST 負控拿「守衛在未注入 copy 上的輸出」當 oracle、要求逐字相同。那個前提是輸出決定性——實測 2 支守衛各跑兩次皆逐字相同、無 temp 路徑洩漏 → 0 實例)| ✅ **寫** | 前五列的失敗都**只傷到自己那一格**;這一列的失敗會外溢。守衛若哪天開始印時間/耗時/temp 路徑/走訪順序,逐字比對就偶發變紅——而**偶發的紅**比漏報更貴:維護者學會「這支有時候會紅,重跑就好」,那個習慣會套用到**所有**守衛身上。所以本 harness 唯一一個「失敗會傷到其他守衛」的機制,自己要被檢查。成本是每支 ROBUST 守衛多跑一次未注入(實測 2 支、約 2 秒) | -| 7 | **守衛兌現的是一句散文,而它只兌現得了一部分**(#414:`booktitleCarrierTypes` 的成員資格判準只存在於 doc comment,「不得依性質相似類推第三個」沒有可執行的程序。守衛改成「本表成員不得帶 `editor`」——實測 54 筆成員記錄觸發 0)| ✅ **寫,但明寫它只是必要條件** | 前六列的守衛,通過時至少**指向**它們要證的那個性質;這一列連指都沒指到——它查的是一個**嚴格更弱的命題**。這一列不是——一個不帶 `editor` 的新型別**仍未必**該進表,那一步永遠是人工裁決。所以本列的裁決有兩半:寫(它讓一句空頭承諾變成部分可兌現),**以及在守衛旁邊明寫它兌現不了的那一半**。只做前半會把一個空頭承諾換成另一個——讀者會以為「守衛綠了=成員資格對了」,而那正是 #414 指出的原病 | -| 8 | **守衛防的是一個當下不可達的狀態**(#416 R1:`errorsFirst` 讓 error 排在 warning 之前,因為 MCP 面截斷 20 則。實測**五族的 error 級 per-record 檢查全部是 key 合法性檢查,而 load 對每族做同樣檢查並 quarantine 整個檔**——那些 error 分支對載入後的記錄結構上不可達,0 實例)| ✅ **寫,但把「為什麼是零」也釘住** | 前七列的零實例都是「那個形狀還沒出現」;這一列是「那個形狀**目前走不到**」——不可達性由**別處的**驗證(load 的 quarantine)造成,而那是我沒有控制、也沒有守衛盯著的東西。所以本列的裁決有兩半:寫那個排序(成本一行,且它一旦可達就是**看不見的漏報**),**以及寫一條測試釘住「現在為什麼是零」**。**前提的失效日期**:「per-record 的 error 級檢查全是 key 檢查」對 venue/organization **自 #227(authorized ⊆ names)/#422/#473 起就不成立**——那些內容檢查在寫入期、decode 不驗,載入後可達;本列的 pin test `testNoPerRecordErrorIsReachableFromALoadedStore` 只對 entry/person/divergence 改壞 key,證的是「key 錯會 quarantine」而不是這句前提,是弱測試。排序守衛因此早已是承重結構,不是裝飾品;本列的裁決(寫)不變,只是理由的第二半(釘零)從那時起就沒釘住。(歷輪補記見量測文件第 8 列) | -| 9 | **一個零實例的欄位,而缺席會被讀成「不可能」**(#394:`Organization.ror`。實測 8 筆 organization、**0 筆帶 ror**;`Venue.issn` 有 39、`Entry.doi` 有 664,只有 organization 這一格是空的) | ✅ **寫** | **前八列講的都是守衛,這一列講的是欄位**——它不查任何東西,所以「失敗」的意思不同。不寫的代價不是漏報,是**模型會說一句它沒打算說的話**:一個讀者問「Akashic 有沒有模型化機構的識別碼」,會看到 person 有 `orcid`、venue 有 `issn`、organization 什麼都沒有,於是合理推論「機構沒有識別碼可記」。而那是假的——ROR 存在、`identity-is-judged-not-matched` 的封閉例外節具名列了它,我們只是還沒有資料。**缺席在這裡不是中性的,它是一個關於世界的斷言**,而那個斷言是錯的。成本是一個 `ROR?` 欄位(additive,舊 binary tolerant-preserve)| -| 10 | **零實例、成本高,而形狀取決於一個還不存在的用途**(#365:`Author.correspondingAuthor`。APA7 的參考文獻**不印**通訊作者,所以它服務的是 CV 產生或作者查詢——而那兩個功能都不存在。實測 55 筆 work 的作者含 `che-cheng`,而「其中幾筆是通訊作者」在 store 內**不是可求值的命題**) | ❌ **不寫** | **本表第一個「不寫」,而它是本檔自己預測過的**(見下方「還沒出現過的情形」——那段寫著「零實例且成本高的守衛……那一列的理由**很可能是『不寫』**」)。理由不是成本高本身,是**形狀取決於用途而用途不存在**:`Bool` 還是 `Set` 只有那個場景能決定,現在選一個等於用猜的固定一個介面。與第 9 列(`Organization.ror`)的關鍵差別:那一列的**形狀是確定的**(ROR 是純量),缺的只是資料。**觸發條件**(逐字取自 2026-08-28 裁決 §1):出現第一個需要它的場景(CV 產生器、或一個「我是哪幾篇的通訊作者」的查詢)——那時形狀才能被決定。這一格沒有機械量測,需要人指認 | -| 11 | **零實例、成本高,而既有欄位已經承載它**(#365:`Author.role`。實測 `fields` 的 role 一族恰 **2 筆**(`editor` ×2),其餘八個相關欄位全零;而那 2 筆已由 `fields["editor"]` 承載、#350 已讓 `EDITOR` 在編著作品上滿足 APA7 下限) | ❌ **不寫** | 與第 10 列同為「不寫」而**理由完全不同**:這一列的形狀是確定的,缺的是**理由**——為 2 筆把 `Author` 從 `enum` 改成 `struct`(非 additive、bump store format、**`Sources/` 54 行/26 檔,含 `Tests/` 153 行/64 檔**——量法與重跑指令見量測文件;先前寫「46 個呼叫點」而三個來源給出三個數,見 #481)不成比例。**觸發條件可檢查**:`fields` 的 role 一族 ≥ 20 筆,**或**出現一個 role 是 `fields` 承載不了的(同一人在同一篇既是作者又是譯者——那時 `fields["translator"]` 與 `authors` 會各自說一半)(量測見量測文件第 11 列) | -| 12 | **零實例,而它屬於另一個型別**(#365:`Author.pseudonym`。筆名是「這個人也用那個名字發表」——關於**人**的事實,`PersonNames` 已有可承載它的 `variant` 分區;authorship 側只在「同一人在**不同作品**用不同筆名、且兩個名字都要進各自的參考文獻」時才需要 per-occurrence 的名字選擇,且與 #386 的 per-work judged authorship 是同一個位置。) | ❌ **不寫** | 與第 10、11 列同為「不寫」而理由是**第三種:位置錯了**。第 10 列缺用途、第 11 列缺理由,這一列連問題都問錯了型別——把筆名放進 `Author` 會讓「人的名字」這個事實有兩個家(`Person.names` 與每一筆 authorship)。APA7 對筆名的處置(用出版時的名字、必要時方括號補真名)是**呈現層**規則,讀的是 `Person.names`——它已經有兩個分區可用。觸發條件見情形欄;**這一格沒有機械量測,需要人指認**(store 沒有筆名的表示法,連查詢都寫不出來——與第 11 列不同,那列至少有一個可數的析取項)。落點在 #386 那一族,不是 `Author.pseudonym`(歷輪補記與量測見量測文件第 12 列) | -| 13 | **零實例,但同族的結構缺口已經出現三次、stale 實際累積過一次——抓到它的三個機制全是場外的,沒有一個是守衛**(#464:死 verdict 掃描。resolution verdict 的 value 指向一個沒有載入的 holder。2026-09-03 實測 live store 2,700 條 verdict(confirmed 與 rejected 合計),死引用 **0**。) | ✅ **寫** | 前面各列的零實例都是「這個形狀還沒發生過」;這一列的零是**清理過之後的零**——形狀在 #460 真的發生過(205 條),只是被場外機制(人肉、verify lens、一次性腳本)掃乾淨了,store 裡因此看不到。所以本列的理由不是第 1 列的「不寫就沒有跡象」(跡象有,在 issue 史裡),而是**跡象住在錯的地方**:每一次都要一個人記得去看,而下一條 holder 退役路徑不會有人記得。掃描放進 `StoreHealth` 是把跡象搬到工具會自己看的地方。**severity 是 warning**:升 error 要與第 8 列一起重開——死 verdict 若是 error,就是第一個既非 key 檢查、又對載入後記錄可達的 per-record error,第 8 列釘住的零會翻掉,所以本列繼承第 8 列的釘零義務;今天仍沒有修復路徑,裁決未重開。warning 級的 `validate` exit 仍為 0——掃得到、不叫醒。**觸發條件可檢查**:「死 verdict」數應恆為 0;非零時先看 quarantined 清單——被 quarantine 的 holder 是「讀不到」不是「退役」,其餘才指向一條漏了遷移的退役路徑。(歷輪補記與量測見量測文件第 13 列) | -| 14 | **零實例、成本一個掃描,而判準取決於一個還不存在的相等定義**(#464 的第二個掃描項——同一 owner 對同一配對同時持有 `resolution-confirmed` 與 `resolution-rejected`。2026-09-03 實測 live store 以 (檔, holder) 與 (檔, 完整 value) 兩種收攏法各算一次,並存數皆 **0**;追蹤 #486,其 `### Blocking` 記 #470) | ✅ **寫**(2026-09-09 翻轉——觸發條件成立) | 與第 10 列同形——判準取決於一個還不存在的東西——而對象不同:那一列是**形狀**取決於用途,這一列形狀確定(一個並存掃描),是它要比較的**相等**取決於 #470:寫入面位元組精確、讀取面 `NameNormalization.matchingKey` 正規化,兩面今天對同一筆 verdict 答案不同。現在寫任一個,都是在 #470 之前偷偷定案第三份相等定義,裁決後必然分岔(`no-compat-fallback` 記過的形狀)。**觸發條件已成立**:#470 於 2026-09-09 選定相等定義(正規化,且只住在鍵裡),本列依它自己寫下的條件改裁「寫」,在 #486 實作——`StoreHealth.contradictoryVerdictIssues`,warning 級,2026-09-09 實測 live store 仍 **0**。**翻轉的方式本身值得留著**:這一列不是被人想起來才改的,是它自己寫下了一個**機械可檢查**的觸發條件(另一張 issue 的 state),而那張 issue 一 close 就到期。(歷輪補記見量測文件第 14 列) | -| 15 | **零實例,而零是本機的——同一份 store 在別台機器上全是實例**(#453:本機缺承重存檔的掃描。記錄的 provenance 指向一個本機 `sources/` 沒有的 digest。) | ✅ **寫** | 前面各列的零,成立與否**不取決於在哪台機器上量**;這一列的零**只在這台機器上為真**——`sources/` 不進 git(`replace-endnote-and-zotero` 的承重閘:第三方版權 PDF 住在那裡),所以每一台新 clone 上這個數字都是「全部」。與第 13 列(清理過之後的零)最像而不同:那一列的零是**時間上**的(曾經非零、掃乾淨了),這一列的零是**空間上**的(換一台機器就非零)——而任何只在本機量的守衛都會對它報綠。**severity 是 warning**:記錄合法可載入、缺的是位元組,而其他 clone 上「全部 dangling」是常態,error 會讓 `hasErrors` 翻紅擋住 export 類流程。**觸發條件可檢查**:本機數字應恆等於「不是合法 digest 的引用數」;多出來的那些指向沒同步的 `sources/` 或真的捏造——先同步,同步後仍在的才是後者(歷輪補記與量測見量測文件第 15 列) | -| 16 | **零實例,而它守的是一個已裁決「現在不改形狀」的 O(n) 增長**(#499:venue 的記錄檔逼近讀取上限的 warning——2026-09-26 起量記錄檔的位元組、門檻是讀取上限 8 MiB 的一半;原本以 verdict 數換算 decode 預算的節點,量錯了軸,更正見量測文件第 16 列。沒有一本刊接近門檻 → 0 實例。使用者裁決(2026-09-04):候選 3——不改序列化位置、半預算處出聲、達門檻重開裁決;候選 2(sidecar ledger)是那時的形狀。) | ✅ **寫** | 前面各列的守衛守的是「某個形狀出現」;這一列守的是**一條已知會漲、且裁決了暫不改形狀的曲線**——它的零不是「還沒發生」,是「還沒漲到」。不寫的代價與第 1 列同形(撞上硬預算時整檔 quarantine、venue 消失,而在那之前沒有任何跡象),但理由多一層:**裁決本身依賴這個守衛**。候選 3 之所以可接受,是因為「達門檻時重開」被承諾為一個工具會自己看的門檻,而不是散文觸發條件(`blocked-issues-must-be-scannable` 的誠實邊界:散文命題沒有機制會叫醒任何人)。拿掉守衛,裁決就退化成「等它壞」。**觸發條件可檢查**:任一 venue 的 warning 出現即重開第 13 條邊的規模化裁決,不要只放寬預算。(歷輪補記與量測見量測文件第 16 列) | -| 17 | **零實例,而零的來源是「寫入面剛長出來」**(#450:拆分後錨失效的兩種 warning——某 person/organization 持有的 resolution verdict 其 literal 已被 work 的拆分記錄退役(孤兒 verdict)、拆分記錄各段全不在作者位。) | ✅ **寫** | 前面各列的零各有來源——還沒發生(第 1 列)、走不到(第 8 列)、掃乾淨了(第 13 列)、在這台機器上(第 15 列)、還沒漲到(第 16 列);這一列的零是**形狀已經發生過 4 次、只是沒被記下**——寫入面在本 change 才長出來,實例從第 5 次拆分起才可能出現。不寫的代價與第 1 列同形而更具體:下一次拆分後,一條指向已退役作者位的 `resolution-rejected` 會讓 `resolve-people` 永遠不再提名那一段(否決抑制以 (citekey, literal) 配對),而沒有任何跡象——那正是 `literal-first-then-key` 說的「誤不可逆」在 verdict 側的形。severity 是 warning(處置是人的重新消歧,不是修檔;`validate` exit 仍 0——「掃得到」不「叫醒」,#464 的同一條界線)。零由三支測試釘住(`OrphanedSplitVerdictScanTests`:乾淨為零、形狀出現即報、鍵是 (citekey, literal) 不是 literal)。**觸發條件可檢查**:live store 第一次跑 `--split-author` 之後兩個計數才可能非零;非零時先看孤兒 verdict——那是要人動手的那種,各段全不在只是提醒記錄留著供 un-split(歷輪補記與量測見量測文件第 17 列) | -| 18 | **零實例,而零只存在於「已經進來的資料」上——守衛守的是入口**(#519:`AddOnlyEnrichment` 對單一字串的 65,536 位元組上限。離上限還有 **15.5 倍**,今日零實例。**但那個形狀已經被造出來過**:#516 verify 用一份 5 MB 摘要走**同一條鏈**實測 RSS 46.6 MB、`proposals.json` 5.0 MB 原樣流進 store 欄位——是上限的 **80 倍**。) | ✅ **寫** | 前十七列的零都是關於**已經在庫裡的東西**——還沒發生(第 1 列)、走不到(第 8 列)、掃乾淨了(第 13 列)、在這台機器上(第 15 列)、還沒漲到(第 16 列)、寫入面剛長出來(第 17 列)。這一列的零**量的是出口,而守衛裝在入口**:store 的最長值是 4,220 bytes,是因為進得來的都小;而這道檢查管的是 adapter **這一次遞過來**的字串,那個長度不受庫內歷史約束。與第 16 列(已知會漲的曲線)最像而不同:**這裡沒有曲線**——實例不會由目錄成長慢慢逼近,它會由一次外部呼叫**一步跨過**,而 #516 verify 已經一步跨過 80 倍。**裁決的第二半是語意**:超限**整批拒絕、零寫入、具名,不截斷**——截斷會讓一個不是來源給的值進 store 且不出聲。(歷輪補記與量測見量測文件第 18 列) | -| 19 | **零實例,而被守的是「辯護」不是事實**(#479:本表每一列都要在「各列共通的東西」有一條 bullet 講它自己的理由。) | ✅ **寫** | 前十八列守的都是**可判定的性質**——欄位在不在、數字對不對、檔案存不存在、記錄形狀合不合法。這一列守的是**這張表存在的理由本身**:表是規格,理由欄是它的辯護,而共通段是那個辯護的第二層(它說明每一列的理由**彼此不同**,那正是本檔不寫總括判準的依據)。**少一條 bullet 不會讓任何檢查變錯**——沒有任何數字會偏、沒有任何斷言會紅——它只會讓下一個人拿判準去類推,而那是本檔開宗明義禁止的動作。所以這一列的失敗不是「漏報」也不是「假訊號」,是**規格的辯護少了一角而規格本身看起來完好**。**觸發條件可檢查**:未被引用的列數應恆為 0(歷輪補記與量測見量測文件第 19 列) | -| 20 | **零實例,而作者對同一形狀的窮舉自己漏了一格**(#526:workflow 的 `run:` 跑一支不存在的腳本。#521 的四個實例裡**有兩個**是這個形狀——`census-parity.yml` 與 `ci.yml` 各跑一支在 `989ac64`(#433「Python 歸零」)刪掉的 `.py`——而**實例 4 是 verify 席找到的**:當時的封閉列舉漏了它。) | ✅ **寫** | 這一列的零是**剛剛被人工修完的**,而修它的那一輪自己證明了人工窮舉不可靠:四個實例裡第四個是**別人**找到的,且它與前三個形狀完全相同。所以理由不是第 1 列的「不寫就沒有跡象」(跡象有,就在同一張 issue 裡),也不是第 13 列的「跡象住在錯的地方」(它就在眼前)——是**看過了、看的是對的地方,仍然漏了一個**。守衛換掉的不是人的注意力,是「這件事需要注意力」這個前提。後果不對稱使它值得:一個掛掉的 step 會擋住其後**全部**步驟,`ci.yml:94` 那次擋掉的包含 AkashicApp(`swift test` 涵蓋不到的那塊,#101 的立案理由)。**觸發條件可檢查**:「不存在」應恆為 0;非零時那個 step 已經是壞的,修路徑或刪 step,不要放寬檢查(歷輪補記與量測見量測文件第 20 列) | -| 21 | **零實例,而既有守衛的謂詞對這一類成員結構上不可能為真**(#522:受保護清單少了成員。`trigger-coverage` 的 `missing` 問的是「清單裡列的路徑還在不在磁碟上」——而 **glob 產生的成員永遠不會「列了卻不存在」**,它只會停止被列出。) | ✅ **寫** | 這一列不同:**守衛在,而且天天跑,只是它的謂詞對 70 個成員裡的 **48** 個永遠不可能為真(glob 與 `swiftGuards()` 衍生的那些;顯式字面 22 個,2026-09-27 重量,量法見量測文件——立案時是 58/37/21;#629 移植後 2026-09-29 重量是 61 個成員、43 個衍生、18 個顯式,比例同形)**。它不是漏看,是問了一個那一類成員答不錯的問題。所以理由既不是「缺跡象」(第 1 列)也不是「跡象住在錯的地方」(第 13 列)——**跡象根本無法產生**。**觸發條件可檢查**:差異應恆為 0;非零時先讀是「少了」還是「多了」——前者確認刪檔是有意的,後者確認新成員真的有讀者(往已在 CI paths 的路徑根加零讀者的檔可以零成本灌水)(歷輪補記與量測見量測文件第 21 列) | -| 22 | **零實例,而它是一條 spec Requirement 不是程式**(#474:venue 的 `names` 時間軸——刊名沿革。#422 把它保留而收窄(`variant` 不得帶時間),於是沿革成了一個「保留位置」的形狀。2026-09-09 實測 live store:venue **406** 筆、`names` 帶任一時間欄位(`start`/`end`/`ended`/`attested`)的 **0** 筆。) | ✅ **保留** | 這一列講的是 **spec 裡的一條 Requirement**,而它的失敗方式是第三種:守衛失敗是**漏報**、欄位缺席是**模型說了一句它沒打算說的話**,而一條零實例的 Requirement 的失敗是——**有人把它當死重刪掉**(「405 筆沒有一筆用到,留著幹嘛」),然後那個形狀到達時沒有位置可落。**保留而不刪的理由是那個現象是真的**:JRSS Series B/C 的分裂、`bulletin-of-the-institute-of-mathematics-academia-sinica` 的新舊系列都是 store 今天**表達不了**的實例(`entity-backlink-completeness` 的「分裂/繼承」那一節記著同一組例子)。**零實例的成因也具名**:異寫法佔著它的位置——#422 之前 `names` 時間軸同時裝沿革與異寫,收窄之後異寫搬到 `variant`,而沿革還沒有人填。**觸發條件可檢查**:帶時間欄位的 venue 數 > 0 即代表沿革開始被用——那時第 16 列(venue verdict 預算)與 `which-side-does-a-relation-live-on` 的「分裂/繼承」觸發條件 ② 也一起到期,三處要一起讀(歷輪補記與量測見量測文件第 22 列) | -| 23 | **零實例,而製造它的那條路徑是本輪自己開的**(#457:移除記錄與作者位互相矛盾——某 work 帶一筆「移除:理由」說某個 literal 已退役,而它現在又在作者位上。2026-09-09 實測 live store:移除記錄 **0** 筆——寫入面(`--drop-author`)在本 change 才存在,所以矛盾今天必為零。) | ✅ **寫** | 第 17 列的零也是「寫入面剛長出來」,而這一列多一件事:**矛盾的可達路徑是本 change 自己造出來的**。移除之後 `authors` 變成**空的**,而 `enrich --include-absent-authors` 的既有契約正好是「只在 authors 完全為空時補」——於是同一個字串補得回去,那時 store 同時斷言「它已退役」與「它是作者」。所以這不是「還沒發生的形狀」,是**新面把一個原本不可達的狀態變成可達**,而讓它可達的那一步與守衛必須在同一個 change 裡(`entity-backlink-completeness` 引 3.325 的立場:矛盾寫不出來最好,寫得出來就要出聲)。severity 是 warning(記錄合法,失效的是證據錨——同 `staleSplitRecords` 的既有分級;`validate` exit 仍 0)。**觸發條件可檢查**:計數應恆為 0;非零時先確認是不是補值面把它加回來的——要嘛再移除一次,要嘛刪掉那筆記錄,不要兩者並存(歷輪補記與量測見量測文件第 23 列) | -| 24 | **零實例,而它是一條「記得起來、解不掉」的半吊子管線——且零是雙重的**(#555:organization 的攣生合併。`recordDivergence` 的 `byShape` 收 org、`resolveDivergence` 對它擲 `unsupportedShape`。2026-09-11 實測 live store:organization **13** 筆、寬鬆共鍵的重複群 **0** 組、含 org 候選的 divergence 記錄 **0** 筆——沒有重複可合、也沒有人記過。) | ⚠ **暫不做——既不實作也不拿掉** | 前二十三列裡只有第 22 列同樣是「不動既有的東西」——它的對象是一條 spec Requirement、失敗方式是被當死重刪掉;本列的對象是一條**程式**的半吊子管線,而不動它可以接受的理由是**它已經誠實了**:#553 讓 `unsupportedShape` 的訊息從一份與分支對帳的清單生成——「organization 的合併管線尚未實作——支援的是 person/work/venue」——使用者撞到時看到的是真話。動它的兩個方向代價都更高:**實作**要替 `parents` 時間軸設計合併形狀,零實例時做等於猜(同第 10 列「形狀取決於一個還不存在的用途」);**拿掉**(`byShape` 移除 org)是關掉記錄面——一組真的 org 攣生在合併面落地之前只能留在人的腦裡,正是 #71「判斷留不下來」的病;venue 在 #553 之前就是這個姿態,而 #553 把記錄面與合併面綁在同一個 change,因為兩者分開時記錄面先開會撞牆(本列)、合併面先開則沒有輸入。**觸發條件**:org 重複群 > 0 **或** 出現第一筆含 org 候選的 divergence 記錄——任一成立即重開,那時要選的是實作或拿掉,不再是暫不做。後者自 R2 起由工具出聲(`StoreHealth.unmergeableDivergences`,warning——第 16 列的紀律:散文觸發條件沒有機制會叫醒任何人,R1 verify 第 13 列)。重開時 `entity-backlink-completeness` 第 9 條邊要一起改。**誠實邊界**:重複群看不到兩種形狀——一筆把幾個機構黏在一格的 org、同一機構的單/複數誤植(`LooseTitleKey` 刻意不摺詞形)——所以「重複群 0 組」承載的比它讀起來弱。本列與第 8 列同型(零的來源在別處),繼承它的釘零義務(`OrgBootstrapResolveTests.testLooseKeyCollisionWithExistingOrgRoutesToPendingResolution`)。**若實作**,`DivergenceResolve.swift` 裡帶 `org-merge-slot` 標記的每一格要同批補。(歷輪補記與量測見量測文件第 24 列)| -| 25 | **零實例,而實例全部是在 verify 裡被造出來的——守衛裝在 store 邊界,零量的是五個寫入者的出口**(#554 R5,使用者裁決 D8:venue 名字內容的不變式——canonical 形、無控制/格式/不可見字元、至少一個字母或數字、三張清單各無近重複對(names 的例外:兩段都帶不相交時間的沿革改回舊名,R6)——住在 `Venue.validate()`,error 級;謂詞一份在 `NameIdentity.wellFormednessIssue`、與輸出閘 `UnsafeToEmitScalar` 共用危險 scalar 的定義,「不可見」用 Unicode 的 `Default_Ignorable_Code_Point`,ZWJ/ZWNJ 只在兩個脈絡合法——(a) 掛在同一文字**字母**上的 virama 之後(virama 與中間的標記都是基底那個文字的;右鄰居若在要是同一文字的字母/數字,空白視同沒有)、(b) 左鄰居是 join-control 文字的字母/標記/數字、右鄰居是**同一文字**的字母/數字或同一 Indic 文字的 virama,U+2800 顯式列入不可見。) | ✅ **寫(error 級、在 store 邊界)** | 第 18 列的理由是「零量的是出口而守衛裝在入口」;這一列多一件事:**入口有五個**(`updateVenue` 的三個名字迴圈、`addVenue`、`VenueBootstrap`)。所以本列的裁決有兩半:**寫**(零實例但實例已被造出四次),**以及寫在 store 邊界而不是任何一個寫入面**——寫入面(`vetVenueNames`)留作入口的好訊息,不再是防線。**severity 是 error** 而非 warning,理由是 live store 0 筆違反(提級不拒絕任何既有記錄——但別的 clone 若持有手改的記錄,`akashic validate` 對它會 exit 1)且違反的後果是 displayName 直接壞掉(`displayName` 讀 `authorized`,#475)。量測文件第 25 列的 Python 對照腳本鏡射 `NameIdentity.wellFormednessIssue` 與 `joinerIsLegal`——改一邊要同批改另一邊。**觸發條件可檢查**:`akashic validate` 對 venue 的名字內容 error 應恆為 0;非零時那筆是手改或舊 binary 寫的,修法是人改 YAML(同 dated-variant 的立場,不猜、不靜默修;訊息自 R6 起逐條說改什麼)——**第 1 條(canonical 形)的確定性那一類自 #575 起另有機械修復面** `akashic repair-venue-names`(歷輪補記與量測見量測文件第 25 列) | -| 26 | **零實例,而它守的是三個閘共同的前提——閘擋工具面、守衛掃非工具面**(#554 R10 verify 第 2/5/10 列,Claude 代裁 D28:同一 work 兩條 key 邊指同一 venue。D25(repoint/demote 拒)、D27(repoint 後不得出現)、D28(apply 不得造出)三個閘守的都是「配對只由一條邊實例化」,而 store 另有手改與舊 binary 兩個寫入者不經任何閘;R10 verify 用真 binary 三次造出這個形(`apply` 兩條同刊名的 literal 邊、`repoint` 改指到本 work 已有邊的 venue),`validate`/`doctor`/App 全綠,使用者直到想 demote 才知道。) | ✅ **寫(warning 級、在 `Entry.validate()`)** | 第 25 列的理由是「入口有五個」而守衛裝在 store 邊界;這一列的入口在 R11 之後只剩**不經工具的兩個**(手改、舊 binary)——venue 合併**不是**入口,而那兩個正是閘擋不到的。**守衛與閘是同一條不變式的兩半**:閘讓工具面造不出這個形,守衛讓已經在庫裡的看得見——少了守衛,D25 的拒絕就是使用者第一次知道自己的 store 走進這一格的時刻,而那時他要做的操作已經被擋。**severity 是 warning**,理由與第 13 列同型:記錄合法可載入(兩條邊各自指向存在的 venue),失效的是判定逆轉的前提;升 error 會讓一筆只能手改才修得好的 work 擋住自己所有的寫入,而處置面當時還不存在。**觸發條件可檢查**:計數應恆為 0;非零時先看那筆是不是 R11 之前的 binary 寫的(`apply` 的兩次歸戶)——修法是刪掉多餘的邊,以 `resolve-venues --drop-venue` 刪(歷輪補記與量測見量測文件第 26 列) | -| 27 | **零實例,而它是同一句不變式的另一半——第一半的守衛結構上照不到它**(#554 R13 verify DA 第 16 列,Claude 代裁 D36:同一 (venue, work) 上 ≥2 個正規化後不同的 confirmed literal。§3.5 把配對唯一性寫成一句 normative 的兩半,第 26 列守的是第一半(work 的邊),第二半住在 venue 的 references 上——`Venue.validate()` 不掃 verdict 配對、`StoreHealth` 沒有對應項,全樹「個不同的 confirmed literal」只出現在 `confirmedLiteral` 的**拒絕訊息**,零 validate/doctor 呼叫端。) | ✅ **寫(warning 級、在 `Venue.validate()`)** | 第 26 列的理由是「閘與守衛是同一條不變式的兩半」——閘擋工具面、守衛掃非工具面;這一列是**同一條不變式的另一半要有自己的守衛**:兩半在偵測面上互斥(造出第二半的路徑不點亮第一半的燈),所以第 26 列的量測「應恆為 0」只兌現了一半,而本檔自己的紀律是「寫出條件而不是只寫數字」。裁決有兩半:寫(零實例但實例已在 verify 裡被造出、且 R14 之前純工具面造得出),**以及不改成只修散文**——DA 給的另一個出口是把 §3.5 那句改成「第二半今天沒有掃描面」,那等於把一個已知可達的狀態留在「只有 demote/repoint 撞上時才知道」。**severity 是 warning**,理由與第 26 列同型:記錄合法可載入,失效的是判定逆轉的前提(D23 從此拒那筆 work 的 demote/repoint);處置是手改 YAML 留一筆——#572 的 `--drop-venue` 刪的是邊不是 verdict,這一格至今沒有工具面,升 error 會讓一筆只能手改才修得好的 venue 擋住自己所有的寫入。**鍵不是一把是兩把,掃描兩類都掃**:以位元組相異的 confirmed 分組,正規化後不同=不變式的違反、只差位元組=重複的判定記錄(工具面以 `verdictEqualityKey` 去重、寫不出它),兩類同一家族前綴(`Venue.confirmedLiteralAmbiguityPrefix`)、措辭分開;`StoreHealth.confirmedLiteralAmbiguities` 有計數;每筆 venue 最多列 20 筆 work、其餘一句概括(第 16/18 列:讀取路徑上對未信任內容跑,鄰居剛為此加了上限)。**觸發條件可檢查**:兩個計數應恆為 0;非零時那筆是 R14 之前的 work 合併、手改或舊 binary 寫的——修法是刪掉不屬於那條邊的 confirmed verdict(歷輪補記與量測見量測文件第 27 列) | -| 28 | **零實例,而它守的是另一條裁決刻意留下的狀態——「零資訊損失」把一筆判定從當場刪改成留著,而留著的那一格沒有任何面看得見**(#554 R22 verify security 第 14 列,Claude 代裁 D64:同一筆記錄對同一配對持有 ≥2 筆同 field 的判定記錄(`verdictEqualityKey` 相同)。2026-09-16 實測 live store:`重複的判定記錄` **0**。) | ✅ **寫(warning 級、在 `StoreHealth`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列多一件事:**可達性是本 change 的另一條裁決刻意造出來的**——D62 為了不刪判定而留下這個狀態,代價正是它在中間那一格看不見。守衛不是在替 D62 擦拭,是 D62 的另一半:留著而不出聲,等於把「rename 當場刪、有回報」換成「合併時刪、回報在另一個命令的輸出裡」,後者未必更好。severity 是 warning,理由與第 13/26 列同型:記錄合法可載入,兩筆各自都是一個人的判定,error 會讓一筆只能手改才修得好的記錄擋住自己所有的寫入。**觸發條件可檢查**:計數應恆為 0;非零時先看是不是剛 rename 過——那兩筆在舊鍵上就已經並存,處置是留一筆或改其中一筆的 value。(歷輪補記與量測見量測文件第 28 列) | -| 29 | **零實例,而它守的是另一個守衛的前提——一個常數必須大於等於全樹每個 sink 的輸出上限**(#554 R31/R32:`ErrorDisplay.inputScalarCeiling`=4,096。D83 讓逃脫只讀每行前 ceiling 個輸入 scalar,等價性的前提是「任一 sink 的輸出上限 ≤ ceiling」——每個輸入 scalar 至少產生一個輸出 scalar,所以輸出上限之內的字串不受截短影響;前提一破,被 ceiling 截短的行會冒充完整的行、且每個 sink 都綠。) | ✅ **寫(四族各自有地板;第四族可為空)** | 第 16 列的理由是「裁決依賴守衛」(門檻到了才重開);這一列更緊:**另一個守衛的正確性依賴它**——`testInputCeilingDoesNotChangeClippedOutput` 證的是「有界=無界」,而那個等式只在 sink 上限 ≤ ceiling 時成立,本列守的就是那個前提。零的來源與前面各列都不同:不是還沒發生、不是掃乾淨了、不是在這台機器上,是**兩個數字今天恰好對齊**(最大 sink 4,096=ceiling 4,096),而改任一邊都是一行 diff、不會有任何測試因此變錯(每個 sink 自己的測試只看自己的數)。**地板要分族**:R31 的單一地板由 57 個 `displaySafeError` 站點撐著,`maxLineLength:` 那一半通過的方式是「什麼都沒找到」——與第 3 列「未涵蓋不得冒充通過」、R29 NC4「空掃描不是通過」同一個形狀,在同一支守衛裡再犯一次。**觸發條件可檢查**:超過 ceiling 的站點數應恆為 0;非零時要一起抬 ceiling(並重量線性),不是放寬那一個 sink(歷輪補記與量測見量測文件第 29 列) | -| 30 | **零實例,而上限守的記錄設計上只增不減**(#619 R1 verify security:未決腿的三個上限——一次 200 個 id、20 個 digest、單句說明 4,096 位元組。) | ✅ **寫(整批拒絕、零寫入、不截斷)** | 第 18 列的上限守的是一次遞進來的字串,並夾在兩個量測錨點之間;這一列多一件事:**被守的記錄設計上不退役**——未決是查證歷史,只收位元組相同的重複,一個會迴圈的呼叫端每跑一次就多留一筆。說明與 digest 的上限各有量測錨點(687 位元組的約 6 倍、3 個的約 7 倍);**id 數 200 沒有可量的母體**——它限的是一次呼叫的批次大小、不是記錄內容,取 CLI 單批 triage 的量級,這一格是推估不是量測。**誠實邊界**:上限只約束一次呼叫、不約束累積——累積由預算 warning 出聲:venue 側是第 16 列,person 與 organization 側是第 31 列(#645;organization 側由 #643 的未決腿加入)。**觸發條件可檢查**:任一筆 reference 的說明或 rests-on 逼近上限時重開。(歷輪補記與量測見量測文件第 30 列) | -| 31 | **零實例,而它是一條已有守衛的曲線換了持有者**(#645:person/organization 的記錄檔逼近讀取上限。第 16 列守 venue 側;person 側的增長來源是一個人的著作數,#619 起多了不退役的 `resolution-undecided`(一筆說明可寫到 4,096 位元組),#643 起 organization 同。) | ✅ **寫(warning 級、在 `StoreHealth`,門檻與量法同第 16 列)** | 第 16 列的理由是「裁決依賴守衛」——暫不改形狀之所以可接受,是因為漲到門檻時工具會出聲;這一列是**同一條曲線的另一個持有者一直沒有那盞燈**,而讓它開始漲的是一個新面(未決腿)。所以理由不是第 1 列的「不寫就沒有跡象」,是**第 30 列自己寫下的誠實邊界**(上限只擋單次呼叫、不擋累積)要有工具面兌現,否則那句邊界就是一句沒有後續的散文。**門檻不另立常數**(`recordFileWarningBytes` 兩族共用——同一個預算的第二份描述會分岔)。warning 仍只在讀取面計算:它管漸進的增長,單步的跳躍由寫入閘擋。**觸發條件可檢查**:計數應恆為 0;非零時先看那筆記錄的 `resolution-undecided` 筆數(歷輪補記與量測見量測文件第 31 列) | -| 32 | **零實例,而謂詞寫寬的方向是 Unicode——前件看起來是「數字」,實際是「任何書寫系統的數字」**(#589:ISSN/ISBN/ORCID 共用的 `idCompact` 以 `CharacterSet.alphanumerics` 過濾、以 `wholeNumberValue` 取值,兩者都認 Unicode 數字;全形與阿拉伯-印度數字寫成的號過得了 mod-11、原樣成為 `normalized`。) | ✅ **寫(在 `idCompact`,非 ASCII 的英數字元換成必定失敗的 `?`;已入庫的會在載入時被隔離)** | 第 2 列的理由是「前件寫寬了會誤傷」;這一列相反——前件寫寬了會**放行**,而放行的東西看起來是對的(第 4 列的偽裝性):一個全形寫成的 ISSN 過得了 check digit,而 `identity-is-judged-not-matched` 讓識別碼相等**單獨**就能做身分判定,所以它是一個看起來像決定性證據的假號。修在 `idCompact` 而不是三個呼叫端,因為三個呼叫端共用它,修一處就全部關掉;**換成 `?` 而不是濾掉**,因為濾掉會讓夾在 ASCII 號裡的非 ASCII 字元被靜默吞掉、號照樣通過。**已入庫的處置是隔離而不是 warning**:這一族在 decode 時本來就以 `ISSN(v) != nil` 驗證、不合法整檔隔離(`IdentifierYAML.make`),收緊謂詞之後非 ASCII 號自然落進同一條路,與其他不合法的號同級;零實例所以不需要遷移(`no-compat-fallback`)。**觸發條件可檢查**:非 ASCII 數應恆為 0;非零時那筆在新 binary 下會被隔離,修法是把號改寫成 ASCII(歷輪補記與量測見量測文件第 32 列) | -| 33 | **一半零實例、一半兩筆,而同一個檢查只寫了三分之一**(#579、#652:work 的三種參照邊——`.key` 作者、`.organization` 作者、`venues[].key`——只有第一種有懸空檢查(`crossRecordIssues` 的「作者 key 沒有對應的 people 檔」)。2026-09-26 新 binary 對 live store:懸空的 venue 邊 **0** 筆;懸空的團體作者 **2** 筆(`anon1954technical` 的 `American Psychological Association`、`anon2014pisa` 的 `OECD`——2026-08-20 #340 批次手寫,payload 是名稱不是 key,而且不是合法 StoreKey)。) | ✅ **寫(warning,在 `crossRecordIssues`;key 不是合法 StoreKey 時訊息另說「不可能對應任何記錄」)** | 第 13 列的理由是「跡象住在錯的地方」;這一列更早一步——**跡象根本不存在**,而同形的檢查就在隔壁:person 那一半有、另外兩半沒有,是檢查寫的時候只有 person 那一種邊。所以它不是新守衛的裁決,是把既有守衛的前件補完(第 2 列的「前件多寬」,方向是寫窄了)。**severity 是 warning**,理由與既有的懸空作者相同:懸空參照讓畫面少東西、不毀資料,而解析中途本來就會有;#579 另提的「error、與 person/organization 的 key 檢查對齊」不採——那些是**記錄自身**的 key,load 就 quarantine;這裡是**參照**,quarantine 整筆 work 會讓一條壞邊把整篇作品藏起來。非法 key 另附說明,因為記錄的 key 在 load 時就驗過,一條非法 key 的邊不是「目標還沒建」而是「永遠建不出來」。**觸發條件可檢查**:venue 邊的數應恆為 0;團體作者那兩筆的處置是人的判斷——建 org 記錄再改 key,或改回 `literal:` 交給 `resolve-organizations`(目前沒有把 `.organization` 退回 literal 的工具面,只能手改 YAML)(歷輪補記與量測見量測文件第 33 列) | -| 34 | **零實例,而第 33 列補完 work 側之後,同一種懸空在 person 與 organization 側仍然沒有檢查**(#660:第 7 條邊 `Person.profile.affiliations` 與第 8 條邊 `Organization.parents` 的 `.key` 指向不存在的 organization。) | ✅ **寫(warning,在 `crossRecordIssues`;key 不是合法 StoreKey 時同第 33 列另說「不可能對應任何記錄」)** | 第 33 列的理由是「同一個檢查只寫了三分之一」;這一列是**那一次補完只補了 work 那一側**——封閉列舉裡指向 organization 的邊有五條——第 1 條的 `.organization` 作者、第 7 條、第 8 條,以及第 9 條(divergence 候選的 organization shape)與第 13 條(verdict value 的 `organization:` holder);後兩條早有檢查(懸空候選的 warning、死 verdict 掃描)。**severity 是 warning**,理由同第 33 列:懸空參照讓畫面少東西、不毀資料,quarantine 整筆 person 會讓一條壞隸屬把整個人藏起來。**literal 不報**:未歸戶的隸屬是誠實狀態(`literal-first-then-key`)。**觸發條件可檢查**:兩個數應恆為 0;非零時處置是人的判斷——建那筆 organization,或把 key 改回 `literal:` 交給 `resolve-organizations`(歷輪補記與量測見量測文件第 34 列) | -| 35 | **零實例,而可達路徑是一次相等定義的替換打開的**(#582:同一筆記錄裡 ≥2 筆**非判定** reference 彼此 canonical 相等——只差 NFC/NFD,或位元組完全相同。) | ✅ **寫(warning,`StoreHealth.duplicateReferences`;不設 per-record 上限)** | 第 28 列的理由是「可達性是本 change 的另一條裁決刻意造出來的」;這一列同形而**對象不同**:那一列是 rename 為了不刪判定而留下的判定記錄重複,這一列是位元組相等的去重為了不丟位元組而收下的非判定 reference 變體——同一個取捨(零資訊損失)在兩類欄位上各開一格,第 28 列只照判定那一格。issue 給的另一個出口是「裁決變體是合法並存、不報」:不採,因為並存的兩筆對同一件事說了兩次,合併時 `fieldsLostByMerging` 以位元組比對,倖存者只有其中一種拼法時合併會被擋下(issue 記載的路徑),那時才第一次出聲、而且出在錯的操作上。**位元組完全相同的也報**、措辭分開,一組裡兩種都有時兩件事都說。**不設上限**:一組一則,則數至多是該記錄非判定 reference 數的一半——線性家族(與死 verdict 同類),不是第 28 列那種組合式家族。**severity 是 warning**:兩筆都合法,處置是人決定留哪一筆。**觸發條件可檢查**:計數應恆為 0;非零時先看是不是 NFC/NFD 變體——那是位元組相等的去重收下的,確認兩筆說的是同一件事後留一筆。(歷輪補記與量測見量測文件第 35 列) | -| 36 | **零實例,而製造它的通道是新開的,且它的錯看起來是對的**(#588:同一個 ISSN 掛在 2 個以上 venue。ISSN 的 mod-11 檢查碼擋得住亂碼,擋不住**合法但屬於姊妹刊的號**——JRSS-A/B/C 在模糊搜尋下都會命中,而 #556 起 `akashic-verify-venue` 把查到的 ISSN 寫進 venue。) | ✅ **寫(warning,在 `crossRecordIssues`,以正規形比對)** | 第 4 列的理由是「錯誤的偽裝性」——猜錯的結果看起來是對的;這一列同形而更尖:`identity-is-judged-not-matched` 讓識別碼相等**單獨**就能做身分判定,所以一個掛錯刊的號不只是一筆錯的欄位,是一個看起來像決定性證據的假身分宣稱,之後以它為據的攣生判定都建在上面。**severity 是 warning**:兩本刊共用一個號可能是其中一本記錯,也可能兩筆是同一本刊(那時要走合併),那是判斷不是修檔;訊息把兩個出口都說出來。**移除面與守衛同批**:只有守衛而沒有移除面,warning 出現時唯一的出路是手改 YAML(`replace-endnote-and-zotero` 第 4 條)。**觸發條件可檢查**:計數應恆為 0;非零時先查兩本刊是不是同一本,不是就移除記錯的那個號(歷輪補記與量測見量測文件第 36 列) | -| 37 | **零實例,而舊稽核的謂詞對新長出來的寫入面結構上不可能為真——而正確的謂詞不存在**(#658:CLI 的每一個葉命令、`resolve-people`/`resolve-venues`/`resolve-organizations` 的每一條腿,都要在 `Sources/akashic/WriteGateRulings.swift` 的裁決表有一格——過閘、不閘並寫出這一格自己的理由、或不寫 store。舊稽核(`DestructiveTargetGateTests`)只認布林的 `--apply`/`--reject` 宣告:預設就寫的命令與逐 id 的寫入腿長出來時它不會紅。) | ✅ **寫(測試:執行期命令樹雙向比對、不閘的格理由非空、過閘的集合由表現算且與閘的呼叫點一致、逐腿以真 binary 驗)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」——那一列換一個謂詞(棘輪)就解決了;這一列換不了,因為**「該不該閘」是裁決不是性質**:#653 的「只閘不可逆的」沒有一句可機械判定的「可逆」(`resolve-people` 的逐篇判定沒有把作者位退回 literal 的工具面,而它不閘)。所以守衛不判斷對錯,只要求**每一格都有裁決、不閘的寫出自己的理由**,並釘住兩件可機械核對的事:表說過閘的命令原始碼真的呼叫閘、反之亦然;逐腿的裁決對真 binary 成立(過閘的腿未指名目標時被擋、不閘的腿的樣本走得到參數檢查之後)。這是第 7 列的形:它兌現的是一個嚴格更弱的命題——通過不代表理由對,理由仍是人寫的。**觸發條件可檢查**:兩個差集應恆為空;非空時在表加一格——過閘、不閘(寫理由)或不寫 store,不從鄰居類推(歷輪補記與量測見量測文件第 37 列) | -| 38 | **零實例,而它守的是讀取面對「哪一筆」的回答——兩筆同 kind 同 key 的記錄會讓一份「單筆明細」變成兩筆的聯集**(#581:`validate --owner`/MCP `akashic_doctor` 的 `owner` 以 (kind, key) 定位一筆記錄,per-record 問題以 (族名, key) 篩出——同 kind 同 key 兩筆以上時篩出的是它們的聯集,而輸出的標題說「這一筆」。) | ✅ **寫(拒絕、不猜——訊息列出各筆的檔名 UUID;CLI exit 1、MCP 回錯誤)** | 第 4 列的理由是「錯誤的偽裝性」——猜錯的結果看起來是對的;這一列同形而位置不同:前面各列守的是 store 的內容,這一列守的是**讀取面對「哪一筆」的回答**。聯集裡的每一則都是真的,錯在標題——而使用者正是為了看「那一筆」才指名的;在全庫輸出裡兩筆同 key 至少還有 citekey/person key 的跨記錄 error 旁證,在單筆明細裡什麼都沒有。**不選「兩筆都列、各自標記」**:`OwnedIssue` 以 (族名, key) 為身分,兩筆同 key 的記錄在那個結構裡本來就分不開,要分得開得讓每一則帶 UUID——那是換 `OwnedIssue` 的形狀、動到三個面,為一個零實例的形狀付出不成比例的代價。**觸發條件可檢查**:同 kind 重複 key 應恆為 0;非零時先修重複(citekey 與 person key 由 `validate` 的跨記錄 error 指路,venue 與 organization 要看檔),再用 owner(歷輪補記與量測見量測文件第 38 列) | -| 39 | **零實例,而它守的是單步的跳躍——一次寫入把一筆讀得回來的記錄推過讀取上限,漸進的預警看不到**(#648,使用者 2026-09-28 裁決兩件都做:(a) 寫出的位元組上限是讀取上限 `AliasEventBudget.maxBytes`(8 MiB),`writePathMultiplier` 的 2 倍只給**不增長**的改寫——寫出的位元組 ≤ 目的檔目前的位元組(`writeByteLimit(replacing:)`);(b) resolve-people 的 judge/refute 理由上限 4,096 位元組,與未決腿同一個常數(`maxStatementBytes`)。) | ✅ **寫(encode 層具名拒絕、零寫入;寬限只給不增長的改寫;多檔寫入面在任何檔落盤之前逐筆 preflight;judge/refute 理由整批拒絕、不截斷)** | 第 31 列的理由是「同一條曲線換了持有者」,它自己寫下的誠實邊界是「warning 只在讀取面、一次夠大的寫入從門檻之下直接跳過讀取上限」——這一列兌現那句邊界的另一半:**漸進的增長由第 16/31 列出聲,單步的跳躍要在寫入端擋**。與第 18 列(入口的字串上限)同形而對象不同:那一列限一個字串,這一列限寫出的整筆記錄,而兩種上限各自都不夠——字串上限擋不住很多個字串(未決腿一次 200 × 4,096 位元組,控制字元經 YAML 跳脫成 4 倍,約 3.3 MB),記錄上限擋不住多檔面的撕裂。所以裁決有三半:寫入閘在 encode(`AliasEventBudget.checkWriteSize`,六個 encoder 共用,拒絕具名記錄與兩個位元組數)、多檔寫入面逐筆 `preflightWrite`(writeX 在寫入當下跑的每一道,含 #631 目的檔與 encode)、judge/refute 的理由在輸入端先擋。**觸發條件可檢查**:超過讀取上限的檔數應恆為 0;最大檔逼近上限時第 16/31 列的 warning 先響,那時同時查是否有單一呼叫帶進大量內容(歷輪補記與量測見量測文件第 39 列) | -| 40 | **零實例,而拒絕一直都在——只是問在錯的時間點**(#641:legacy 殘留(`entries/.yaml`/`people/.yaml`)加上三者之一——未受 git 追蹤或有未 commit 的修改、目的檔 `entities/.yaml` 被隔離或是另一種記錄、同一個 id 兩份並存——的 work/person,寫入當下一定被 #631 的前置拒絕;那個拒絕發生在多檔寫入者的中途時,前面幾筆已經落盤。) | ✅ **寫(load 時以同一組前置判斷標註,併進 `unlocatableCitekeys` 與新增的 `unlocatablePersonKeys`;validate 報 warning)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」;這一列的謂詞沒有問題——#631 的前置判斷對這些記錄答得完全正確,錯在**問的時間**:寫入當下才問,「拒絕」到達時同一個操作的前幾筆已經落盤。所以修法不是換謂詞,是**把同一組判斷(`assertEntitiesDestination`、`legacyMoveCandidate`、`filesNotSafelyRecoverable`)搬到 load 再問一次**,不另寫一份(`no-compat-fallback` §同一件事只能有一份描述);答案記在記錄旁(`FileSituation`:不參與相等、不序列化),併進「無法唯一定位」的定義,#627/#628 已接好的各面閘因此在第一次寫入之前就拒絕或具名略過——那些面一行不改就拿到第 3 類;person 側的寫入者(judge/refute/apply/reject/undecided、org 以 person 為 holder 的各腿、App 的 accept、authorize-names)另接新的 `unlocatablePersonKeys`。**severity 是 warning**:記錄讀得到、內容完好,擋的是寫入;升 error 會讓 `assertNoCrossRecordErrors` 擋下不相干的改名與合併。**觸發條件可檢查**:legacy 殘留數應恆為 0;非零時 `akashic validate` 逐筆說哪一筆寫入時會被拒、為什麼,處置是人的——把 legacy 檔 commit(之後寫入會搬移它)、刪掉兩份裡的一份、或修好被隔離的目的檔(歷輪補記與量測見量測文件第 40 列) | -| 41 | **零實例,而同一個值在兩層有相反的身分——作為位址合法、作為引用不合法**(#654:空內容的 digest,即 0 byte 的 SHA-256 `sha256:e3b0c442…`。) | ✅ **寫(error 級、在 `ProvenanceReference.isValidDigest`;位址層另用只看形狀的 `isWellFormedDigest`)** | 第 32 列的理由是「前件寫寬的方向是放行」;這一列同形而放行的不是假號,是一個真的、形狀完全合法的值——它的錯不在形狀,在於它是常數、對所有空輸入都相同,不指認任何一份存檔,所以只看形狀的檢查全部報綠。**閘放在謂詞本身而不是 `enrich` 入口**(使用者 2026-09-28 裁決):引用端的寫入面不只一個、再加上載入,逐面補會重演第 25 列「入口有五個」的形;修在共用謂詞一處全部關掉,同第 32 列。**裁決的第二半是把謂詞拆成兩個**:`sources/` 的路徑解析與 `index.jsonl` 的文法、以及 `AuthorListFingerprint` 的持久形只看形狀。live store 的 index 有一列指向空 blob,若 index 文法也排除它,那一列會被判成無法解析,而 index 有無法解析的行時 `store-source` 對**所有**新內容拒寫——一次為了守零實例而加的收緊會把一個能用的 store 關掉。**已入庫的處置是隔離**:digest 在 decode 時本來就驗、不合法整檔隔離,收緊謂詞之後空內容 digest 自然落進同一條路(同第 32 列),零實例所以不需要遷移(`no-compat-fallback`);訊息說「0 byte」而不是「形狀必須是 …」——對一個形狀合法的值說形狀錯是假話。**觸發條件可檢查**:指向空內容 digest 的值應恆為 0;非零時那筆在新 binary 下會被隔離,修法是重新取得內容、`store-source` 存檔後改引用新的 digest(歷輪補記與量測見量測文件第 41 列) | -| 42 | **零實例,而重複 key 在兩個形狀有檢查、在另兩個形狀沒有——沒有檢查的那兩個,讀取時不是少報,是崩潰**(#669:兩筆 venue 或兩筆 organization 持同一個 key。2026-09-28 實測 live store:venue 485、organization 13,同 kind 重複 key 0 組。) | ✅ **寫(error 級、在 `crossRecordIssues`;讀取端不 trap;以 key 定位寫入的兩個面拒絕;源碼掃描守衛)** | 第 33 列的理由是「同一個檢查只寫了三分之一」;這一列同形而後果更重:沒有檢查的那一格讓讀取端**崩潰**——`uniqueKeysWithValues` 對重複鍵是 precondition failure,不是可捕捉的錯誤,一筆手改的 YAML 就讓 `export-bib` 與 MCP server 整個停掉。**severity 是 error**,與 citekey、person key 同級:key 是 `.key(…)` 參照、verdict holder 與每一個寫入面的定位依據,重複時改名與合併(`assertNoCrossRecordErrors`)要先停;第 38 列的定址拒絕原本是這兩個 kind 的重複**唯一**出聲的地方,它的訊息自本列起指路 validate。**寫入面拒絕**:attribute-org(把 verdict 寫進那個 organization)與 migrate-identifiers(把 ISSN 搬進那個 venue)在重複時整批拒絕、零寫入——同 #627 對 citekey。**觸發條件可檢查**:重複 key 應恆為 0;非零時先在 YAML 改掉其中一筆的 key——這個 error 在時改名與合併都先停,所以合併不是第一步;若兩筆其實是同一本刊,改完之後再走 record-divergence/resolve-divergence(歷輪補記與量測見量測文件第 42 列) | -| 43 | **零實例,而它守的是合併把沿革安靜地拿掉**(#565:venue 合併時,被併者的名字段與倖存者(或先併入的被併者)同名,時間、source、note 不完全相同,又不能並存。先前 `mergedVenueKeeper` 以 `TemporalValue(value:)` 重建被併者的名字,時間、source、note 一律丟掉,同名段被 canonical 濾除而不出聲。) | ✅ **寫(不能並存的同名段具名拒絕、零寫入;其餘名字整段搬)** | 第 22 列替沿革在 spec 裡留了位置;這一列守那個位置的**另一端**:合併正是把落進位置的沿革拿掉的那一步,而結果是一筆看起來健康的 venue(第 4 列的偽裝性)。哪一段的時間才對是判斷,合併不猜,所以不能並存的同名段具名拒絕;能並存的整段搬(時間、source、note 都保留),被併者原本在 variant 的才標 variant,其餘併入後未標(#554 D1 的論證用在合併端)。**觸發條件可檢查**:帶時間、source 或 note 的名字段數 > 0——那時第 22 列的觸發條件也一起到期(歷輪補記與量測見量測文件第 43 列) | -| 44 | **零實例,而設閘的判準早就寫在程式裡,只是答案剛變**(#567:format < 14 的 store 寫出帶 `variant` 的 venue。`migrate-venue-variants` 退場刪除之後,「有沒有 pre-bump 遷移」的答案從「有」變成「沒有」;`LibraryStore` 的註解本來就寫著「#567 退場時一併裁」。2026-09-29:live store format 18,format-13 的 store 零實例) | ✅ **寫(同 `paginated` 的閘:format < 14 且 variant 非空即拒)** | 與第 1 列同形——不寫的話,format-13 的 binary 會把異寫當一般名字顯示,而沒有任何跡象;差別在於這道閘不是新的判準,是既有判準在遷移退場後的答案。**誠實邊界**:`fmt` 不經 `assertVenueWritable`,不受這道閘管。**觸發條件可檢查**:`VenueStoreTests.testVariantWriteRefusedBelowFormat14` 釘住閘;live store 的 format 見量測文件(量測見量測文件第 44 列) | -| 45 | **零實例,而上限守的是一個新開的通用寫入面**(#587:`update-venue --references`/MCP `references` 一次 200 筆、statement 4,096 位元組、rests-on 20 個、其餘字串 65,536 位元組;`add_issn` 對認不出的角色與角色衝突拒收。) | ✅ **寫(整批拒絕、零寫入、不截斷)** | 與第 30 列同形——上限只約束單次呼叫,累積由第 16 列的預算 warning 出聲;錨點取既有常數(`maxStatementBytes`、`maxRestsOnPerCall`、`AddOnlyEnrichment.maxValueBytes`),不另立數字。一次 200 筆同第 30 列的 id 數:限的是批次大小,沒有可量的母體,是推估。角色的拒收比遷移嚴:遷移面對既有資料,對認不出的寫法保留原值、報 warning;寫入面不寫一個 `validate` 會報的值。(歷輪補記與量測見量測文件第 45 列) | -| 46 | **零實例,而路由用的字典安靜地選一筆**(#610:同一個 `(library_id, zotero_key)` 被兩筆以上 entry 宣稱,主來源與附加來源都算。三張路由索引都是後寫覆蓋先寫的字典,匯入只更新其中一筆,沒有任何地方說出來。) | ✅ **寫(warning 在 `crossRecordIssues`;匯入端不更新、不新建,報 `ambiguousSourceClaims`)** | 與第 33/42 列同形:隔壁的重複(DOI、標題)有檢查,宣稱者的重複沒有;後果是把 Zotero 的書目欄位寫進別的記錄。**severity 是 warning**:升 error 會讓 `assertNoCrossRecordErrors` 擋下改名與合併,而合併正是修復的出路。可達路徑:手改、舊 binary、#607 修掉的 legacy 認領。**觸發條件可檢查**:宣稱者 ≥2 的來源數應恆為 0(歷輪補記與量測見量測文件第 46 列) | -| 47 | **零實例,而同一個形狀已經真實發生過一次——零來自規則還不存在,不是來自清理**(#642:規則型/文件型 library 的既有成員不符規則、規則指向不在庫的 venue 或文件,以及未標性質的 library。) | ✅ **寫(warning;未標性質是 per-record,不符規則與懸空規則是跨記錄,每個 library 一則、點名前 5 筆)** | 第 13 列的零是「清理過之後的零」;這一列的形狀發生過一次,而今天的零來自 live store 的 4 個 library **都還沒標性質**——規則根本不存在,當然沒有東西不符。一旦有人 `set-kind`,零就取決於兩件事:`library add` 的逐筆比對(寫入端的閘,擋工具面),以及這盞燈。燈照的是閘擋不到的來源:手改 YAML、`set-kind` 當下就已經不符的既有成員(set-kind 只列出、不移除),以及規則建好之後依據才出問題的形狀(venue key 變成重複、文件被刪、排除的 citekey 被刪,#642 R1 verify 補)。這是第 26 列「閘與守衛是同一條不變式的兩半」的形。**severity 是 warning**:成員關係錯了不毀資料,error 會讓 `assertNoCrossRecordErrors` 擋下不相干的改名與合併。**未標性質也報**:未標就沒有依據,`library add` 一律拒絕;不報的話使用者只會看到 add 被拒,看不到原因住在哪裡。**觸發條件可檢查**:不符數應恆為 0;非零時用 `library check ` 看是哪幾筆、為什麼,處置是人的——remove,或規則寫錯了就 `set-kind` 改規則(歷輪補記與量測見量測文件第 47 列) | -| 48 | **零實例,而角色是新開的寫入通道每次查證都會寫的東西**(#587 R1 verify:venue 合併不比同一個 ISSN 的角色——被併者有角色而倖存者沒有、或兩邊不同(含遷移留下認不出的寫法),合併之後角色安靜消失。#587 起 `add_issn` 收角色、`akashic-verify-venue` 查證時會寫;同一輪 `field: issn`/`names` 的 reference 開放寫入,而合併原本對它們一律拒絕。) | ✅ **寫(角色遺失具名拒絕、零寫入、出路逐格;`field: issn`/`names` 的 reference 隨合併逐位元組搬)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形——角色掉了之後記錄仍是一筆看起來健康的 venue,`validate` 不會報——而可達性是 #587 的寫入面造成的(第 23 列的形),所以裁決與新面同批。角色比 `qualifierRaw` 的位元組(認不出的寫法也算,否則遷移留下的 `Online` 會被安靜覆寫);被併者有而倖存者沒有、或兩邊不同,都具名拒絕,出路各寫(`--add-issn "號 (角色)"`;角色不同時先 `--remove-issn` 再 `--add-issn`,並說明會連帶刪那個號的 reference;認不出的寫法手改 YAML)。reference 那一半不是拒絕而是搬:`field: issn`/`names` 的值在倖存者上存在就逐位元組搬(`byteExactKey` 去重),乾跑與實跑共用 `venueReferenceCarry` 一份;`paginated`、手改的 `authorized`、`note` 仍拒絕——那幾格沒有工具面能搬。**觸發條件可檢查**:含 venue 候選的 divergence 數 > 0 時這兩道才會被走到;守衛測試是 `VenueMergeReferencesAndISSNRolesTests` 的角色四支與 reference 四支(歷輪補記與量測見量測文件第 48 列) | -| 49 | **零實例,而形狀是一次裁決之後才合法、先前哪一面都看不見**(#609:附加來源已刪除而主連結仍在,以及沒有主來源、附加來源全部已刪除——#605 R1 裁決之後兩者都是合法狀態,但 `doctor`、index 的 `orphaned` 欄、App 裁決台都只看主來源。) | ✅ **寫(列表面:`StoreHealth.orphanedCitekeys`/`orphanedAdditionalSourceCitekeys`;判準只有 `Entry.zoteroLinkState` 一份;處置在 App 裁決台)** | 第 46 列的理由是「路由字典安靜地選一筆」;這一列與它成對而守的東西不同:**合法狀態的可見性**。前面各列的零是壞形狀還沒發生,這一列的形狀是合法的,缺的是能看見它的判準——`.orphaned` 的母體在 #605 R1 之後擴大,每個 orphan 介面的判準卻還停在只看主來源,於是一筆作品已經從每一個連結的 library 消失,卻在每個介面都看不見。不寫的代價與第 1 列同形(第一次發生時沒有跡象);成本是一個三值判準。它不是守衛(沒有 severity、不進 `validate`),所以這一列記的是「為什麼列表算數」。**觸發條件可檢查**(腳本見量測文件):兩個計數任一 > 0 即出現在 `akashic doctor`(`orphaned:`/`orphaned additional sources:`)、MCP `akashic_doctor`(`orphaned`/`orphanedAdditionalSources`)與 App 側欄(歷輪補記與量測見量測文件第 49 列) | -| 50 | **零實例,而檢查只問「在不在」,沒問「是不是一份檔」**(#614 b11c verify R1:`update-entry --add-source`/MCP `add_sources` 要求 digest 的內容已存在本機 `sources/`,而先前用 `fileExists(atPath:)` 判斷——它對同名的**目錄**也回 true;blob 被換成目錄或 symlink、index 那一列還在時,add-source 會宣告一份讀不到的副本而不出聲。) | ✅ **寫(`SourcePresence.notRegularFile`,以 lstat 的類型判定;目錄、symlink、特殊檔案具名拒絕,乾跑與實跑一致、零寫入)** | 第 1 列的理由是「失敗的不可見性」;這一列同形——不寫的話,那個形狀第一次出現時 add-source 照常回報成功,而宣告的那份副本打不開。與第 15 列(本機缺承重存檔)相鄰而不同:那一列問的是位元組在不在這台機器上,這一列問的是在那個位置上的東西是不是一份檔。前件精確(lstat 的類型不是 regular),成本是一個屬性檢查。**symlink 也拒**:`sources/` 由 `store-source` 寫成普通檔,store 沒有任何一條路徑會寫出 symlink;若日後有意用 symlink 共用 blob,要回來改這一列。**誠實邊界**:只看類型、不重新雜湊內容(位元組與 digest 是否相符不在這道檢查裡)。**觸發條件可檢查**(腳本見量測文件):非普通檔數應恆為 0;非零時先查是誰放的,再依 add-source 的拒絕訊息處置(歷輪補記與量測見量測文件第 50 列) | -| 51 | **零實例,而「沒有東西可檢查」與「檢查過且乾淨」在輸出上分不開**(#629 移植 `rule-coverage` 成 `akashic-guards rule-coverage`:某個 plugin 根的 `rules/` 底下一條 `.md` 都沒有時,舊 shell 版因 glob 不命中而**意外**變紅,Swift 版的空迴圈會印綠燈。2026-09-29 量測:兩個 plugin 根(`plugin`、`plugins/akashic-discovery`)各 2 條規則,空的 `rules/` 0 個) | ✅ **寫(`RuleCoverage.swift` 對零條規則顯式 rc=1 並具名)** | 第 3 列的理由是「未涵蓋不得冒充通過」;這一列同形,差別在它是**移植時才可能長出來的沉默**:舊實作的紅是 glob 的副作用,不是裁決,換一個語言就換掉了。所以移植要把那個副作用寫成顯式的判定。成本一個分支。**觸發條件可檢查**:`ls /rules/*.md | wc -l` 應恆 ≥ 1;等於 0 時守衛紅,處置是補規則檔或把那個根移出 `plugin-roots`(歷輪補記與量測見量測文件第 51 列) | -| 52 | **零實例,而少算一個檔的普查與「查完歸零」無法區分**(#629 移植 `literal-census.sh` 成 `akashic literal-census`:記錄檔讀不進來或不是一般檔時,Python 版 traceback、shell 版少算,Swift 版具名 exit 3 拒絕輸出計數(`LiteralCensus.Failure.unreadableRecord`)。) | ✅ **寫(exit 3、具名那個檔,不輸出任何計數)** | 第 3 列的理由是「守衛沉默時要不要說話」;這一列同形而後果更具體:普查的輸出是 `literal-first-then-key` 終局的**進度量測**(literal 邊數的趨勢),少算一個檔會讓數字往「歸零」那一側偏,而那正是量測要回答的問題。所以不是略過並警告,是整次拒絕輸出。**觸發條件可檢查**(腳本見量測文件):不可讀或非一般檔數應恆為 0;非零時先修那個檔的權限或類型,再跑普查(歷輪補記與量測見量測文件第 52 列) | -| 53 | **零實例,而同一個不明確的依據設定時被拒、使用時被放行**(#642 b11d verify R1:規則型 library 的 venue key 之後變成重複、或排除清單的 citekey 打錯或已不在庫——`create`/`set-kind` 本來就拒絕重複的 venue key,`library add` 卻只比 key 字串;排除清單則從不查存在,打錯一個字排除就無聲失效。) | ✅ **寫(寫入閘與 warning 同批:`LibraryMembershipCheck` 的 `ruleVenueAmbiguous`,三面都不寫、`library check` 揭露;`create`/`set-kind` 對不在庫的排除 citekey 具名拒絕;validate 報兩種 warning)** | 第 47 列的理由是「零來自規則還不存在」,燈照的是寫入閘擋不到的來源;這一列補兩個它沒照到的:依據本身出了問題(venue key 重複)與排除清單懸空。前者是第 26 列「閘與守衛是同一條不變式的兩半」的形——同一個依據在設定時被閘擋下、在使用時卻被放行,等於那條不變式只有一半;後者讓同一份程式對「排除清單是不是參照」給出相反的答案(改名與合併的守衛把它當參照,建立時卻不查)。**severity 是 warning**:成員關係錯了不毀資料,與第 47 列同級。**觸發條件可檢查**(腳本見量測文件):兩個數應恆為 0;非零時用 `library check ` 看是哪一條規則、哪個 citekey(歷輪補記見量測文件第 53 列) | -| 54 | **零實例,而先前的守衛是一條走不通的出路**(#642 b11d verify R1:改名碰到被 library 規則指涉的 citekey——文件型的 `document`、規則型的 `excluded`。#642 首版一律拒絕並指路「先 set-kind 改規則」,而文件型的 `set-kind` 要求新 citekey 已在庫、`rename` 要求它不在庫,出路是死路。) | ✅ **寫(`renameEntry` 同批遷移規則裡的 citekey,registry 檔進同一批預檢與寫入,逐條列在 `RenameReport.libraryRulesRewritten`;三個前置拒絕、零寫入:新 citekey 已被規則指涉、被隔離的 registry 檔提到舊或新 citekey、要遷移的 registry 檔不可回溯)** | 改名不改身分,而 library 規則是關係邊第 16 條——改名本來就該遷移指向舊 key 的邊(第 13 條邊的 verdict value 是先例)。三個前置拒絕各擋一種遷移會造成的安靜錯誤:新 citekey 已被規則指涉時,改名會讓一條懸空的規則「復活」成指向另一筆;被隔離的 registry 檔看不到,遷移會漏掉它;registry 檔不可回溯時,被改寫的規則沒有舊值可取回。work 與 venue 合併維持拒絕,訊息改成可照做的 `set-kind` 命令、不經過 topic。**觸發條件可檢查**:規則型或文件型 library 出現之後,改名一筆被指涉的 citekey 時報告會多一段「library 成員規則已遷移」;守衛測試是 `LibraryMembershipStoreTests` 的改名六支(歷輪補記見量測文件第 54 列) | -| 55 | **零實例,而整值替換會丟掉人的裁決,舊值只剩 git 那一份**(#642 b11d verify R1:`set-kind` 替換一個既有的成員性質時,排除清單這類逐筆寫下的裁決整份被換掉,先前不回顯舊值、也不要求舊值取得回來。2026-09-29 唯讀量測 live store:4 個 library 全未標性質,從未標標成任何一種不經這道閘,所以零實例) | ✅ **寫(替換既有性質時要求 `libraries/.yaml` 已 commit 且乾淨;兩面回顯先前的性質;可回溯閘抽成共用核心 `LibraryStore.recoverabilityRefusal`,與 #573 那一族同一支)** | 第 44 列的理由是「既有判準的答案變了」;這一列同形而對象不同:#573 那一族的閘原本只看 `entities/` 的記錄檔,registry 檔在 `libraries/`,於是移除面一族的判準(舊值要能從 git 取回,使用者 2026-09-27 裁決)在這裡沒有套上。**Claude 代裁**:閘也涵蓋把 topic 換成別的性質(topic 沒有參數可失去,比必要的嚴)。**沒做的**:把規則放寬(降成 topic、清掉排除清單、換 venue)時要不要另外要求明確確認——待使用者決定;目前的防線只有回顯與 git。**觸發條件可檢查**:有 library 標了性質之後,替換它而 registry 檔未 commit 時 `set-kind` 拒絕;測試是 `LibraryMembershipServiceTests.testReplacingAnExistingMembershipRequiresACommittedRegistryFile`(回顯由 `testSetKindEchoesThePreviousMembership` 釘住)(歷輪補記見量測文件第 55 列) | -| 56 | **零實例,而 decode 接受它、唯一守著它的合併閘只擋一條進入路徑**(#679:附加 Zotero 來源沒記 `library_id`——匯入端與 `ZoteroSourceClaims.claims(of:)` 都對不回它,沒有別的 entry 宣稱那個條目時再匯入會另建一筆 twin;先前註解宣稱「由合併閘保證不會出現」,而合併閘只擋合併新收,管不到手改與舊檔。) | ✅ **寫(warning、在 `Entry.validate()`,每筆 entry 一則、至多列 5 個來源鍵;訊息說出後果與兩條出路:補真正的 `library_id`,或以 `update-entry --remove-zotero-source` 移除)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形——那筆記錄看起來連著 Zotero,而下一次匯入會安靜地另建一筆,結果是一對看起來各自健康的攣生。與第 33 列也同形:同一族的檢查(#610 的多筆宣稱)只照到有 `library_id` 的那一半。**severity 是 warning**:記錄合法可載入;decode 拒收會讓既有的檔整個被隔離(`writeEntry` 不對 `Entry.validate()` 的 error 設閘,所以升 error 也擋不住寫入,那不是理由)。**觸發條件可檢查**(腳本見量測文件):`LC_ALL=C grep -a -q '附加 Zotero 來源沒記 library_id' "$(command -v akashic)" && "$(command -v akashic)" validate 2>&1 | grep -c '附加 Zotero 來源沒記 library_id'` 應恆為 0;非零時照訊息給的定位鍵處置(歷輪補記與量測見量測文件第 56 列) | -| 57 | **零實例,而錯的回應看起來是對的**(#629 第二塊:`akashic crossref-match` 以重播協定讀 skill 經 safari-browser 取回的 Crossref 回應,回應的 DOI 與請求的 DOI 不同時拒絕;DOI 插進網址前先驗形狀(`^10\.[0-9]{4,9}/` 後接不含空白、引號、反斜線、`$`、反引號、`#`、`?`、`%` 的字元,且沒有 `.`/`..` 路徑段)。指令從未對真回應跑過。) | ✅ **寫(身分不符具名拒絕、形狀不過的 DOI 不組成請求)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形:把另一個 DOI 的回應讀成這一個,比對照樣成功、結束碼 0,而結果是一筆看起來查證過的錯資料。這個形狀在頁內 fetch 已經發生過一次(PsychQuant/safari-browser#190:一批讀到上一批的結果、結束碼全為 0),所以讀回後核對身分是 `web-access-via-safari-browser` 的既有紀律,這裡把它落實成程式。形狀檢查與 `web-access.md` 的形狀表同一條。**觸發條件可檢查**:形狀不過的 DOI 數應恆為 0;身分檢查由 `CrossrefMatchTests.testReverseVerifyRejectsAResponseAboutAnotherDOI` 與 `testAnUnsafeCandidateDOIIsNeverTurnedIntoARequest` 釘住(歷輪補記與量測見量測文件第 57 列) | -| 58 | **零實例,而半套的數字讀起來像完整的**(#629 第二塊:`akashic fulltext calibrate` 讀本機的 Crossref 回應目錄計分;缺任何一筆記錄時列出缺的網址並以結束碼 3 結束、不出數字,`--partial` 才照已有的量。校準從未在本機跑過——2026-09-24 那批語料本機找不到,重取要連網) | ✅ **寫(缺記錄預設不出數字)** | 第 3 列的理由是「未涵蓋不得冒充通過」;這一列同形:`22/28`、`0/808` 這類數字若只涵蓋有記錄的那一部分,讀的人會當成全部。拒絕輸出是預設,放行要顯式帶 `--partial`,而那時輸出本身就說明它是部分的。**誠實邊界**:移植時舊校準數字(自己 22/28、別篇 0/808)沒有重跑,替代證據是新舊判定規則的差分——它證明新舊判得一樣,不證明那組數字在新語料上仍成立。**觸發條件可檢查**:`SkillToolsCLITests.testCalibrateListsMissingCrossrefRecordsAndMeasuresTheRest` 釘住結束碼 3 與缺記錄清單(歷輪補記與量測見量測文件第 58 列) | -| 59 | **零實例,而「移哪一筆」對兩筆完全相同的記錄沒有判定內容可依**(#673:`update-venue --remove-reference` 以 field+value+位元組鍵定位;兩筆 reference 的 `byteExactKey` 完全相同時具名拒絕、不挑一筆移。2026-09-29 唯讀量測 live store:venue 485、帶通用 reference 的 venue 0、位元組相同的重複組 0。) | ❌ **不寫(不給完全相同的重複一條移除路徑)** | 與第 10 列同形:形狀取決於一個還沒出現的用途——要嘛加 ordinal 指定第幾筆,要嘛一條「重複全收成一筆」的腿,兩者的語意不同,而今天沒有實例可以告訴我們哪一種才是使用者要的。先具名拒絕是可逆的;選錯一個形狀則是固定了一個介面。這種重複本來就由 `StoreHealth.duplicateReferences`(第 35 列)報出。**觸發條件可檢查**(腳本見量測文件):位元組相同的重複組 > 0,或 `akashic validate` 對 venue 報「重複的 reference:」——那時重開,由使用者選形狀(歷輪補記見量測文件第 59 列) | -| 60 | **零實例,而上限沒有量測依據、也不是位元組界線**(#673:venue 讀取面(CLI `venue`、MCP `akashic_venue`)顯示非判定的 references,至多 `venueReferencesCap`=25 筆、每筆 `rests_on` 至多 5 個,以 `referencesTotal`/`referencesTruncated`/`rests_on_total` 揭露。2026-09-29 唯讀量測 live store:帶通用 reference 的 venue 0,單筆 venue 最多 0 筆) | ✅ **寫(第 1 類,守衛:上限 25 筆、`rests_on` 前 5 個,截斷一律揭露)** | 第 18 列的理由是「零量的是出口而守衛裝在入口」;這一列同形,差別在錨點:第 18 列夾在兩個量測之間,這一列**一個量測都沒有**——25 是**選的數字**(live store 沒有任何一筆通用 reference 可量),量級考量是一本刊的 ISSN 與名字各有幾筆來源記錄、再留一個數量級的餘裕。**它實際做的事**:不讓 `references` 這一段跟著一本異常刊無限長(筆數上界);單筆的字串各有字元上限。不設上限的代價是讀取面對未信任的 store 內容線性長大(第 16 列那一族)。**誠實邊界**:要限位元組得另設預算(`candidateByteBudget` 的形),此刻沒有實例;讀取面對 value 截在 200、url/statement 截在 300,逐字定位要對照 YAML。**觸發條件可檢查**:任一 venue 的輸出帶 `referencesTruncated: true`——那時才有實際分布可量,重量上限(並一併決定要不要設位元組預算)(歷輪補記見量測文件第 60 列) | -| 61 | **零實例,而上限守的是「理由只在報告裡」的那份輸出**(#680、#677、#673 的 b13f verify R1:移除面一族的報告輸出有上限——`update-entry --remove-zotero-source` 的 `zoteroSourcesRemaining` 兩面都截 20(`zoteroSourcesRemainingCap`,附總數);`remove_sources` 的取得記錄(`sourcesAddedCap`)與 `remove_reference` 的 reference 內容(`removalDetailCap`)只在 MCP 面截 20,其後仍逐筆列定位鍵與理由,以 `detailsTruncated`/`detailsListed` 揭露。) | ✅ **寫(輸出上限,截斷一律揭露;理由逐筆不截)** | 第 60 列的理由是「上限只有算術可依」;這一列同形(20 是選的數字、限筆數不限位元組),差別在這一族每筆都帶理由,而理由依 2026-09-27 的裁決只在報告裡有一份——所以不截整筆、只截第三方的內容,與 `add_sources` 截整筆的先例刻意不同:截掉第 21 筆以後的理由就無處可記。**誠實邊界**:省略的內容只在 git 的移除前副本與 CLI 輸出裡;上限不保證輸出位元組。**觸發條件可檢查**(腳本見量測文件):出現 `detailsTruncated: true`,或 `zoteroSourcesRemainingTotal` > 20 時重量(歷輪補記與量測見量測文件第 61 列) | -| 62 | **零實例,而失敗是刪掉使用者已有的資料、而且發生在「下載成功」之後**(#629 R1 verify:`akashic fulltext fetch` 的輸出步驟對已存在的目的地「先刪再搬」——`--out` 是非空目錄時遞迴刪光,是普通檔時搬失敗前原檔已毀。2026-09-29 唯讀量測:`~/Downloads` 兩層內名為 `*.pdf` 的目錄或 symlink **0**。) | ✅ **寫(三個目的地先 lstat、同目錄暫存檔+rename)** | 第 1 列的理由是不寫就沒有跡象;這一列多一層:**跡象指向錯的方向**——命令回報下載成功,被刪的是使用者原本就有的東西。成本是一次 lstat。釘住 `FulltextFetchHardeningTests.testANonEmptyDirectoryAtOutIsRefusedAndKept` 與 `testAWriteFailureAfterTheTempFileWasCreatedKeepsTheOriginalFile`(後者以 `RLIMIT_FSIZE` 讓寫入在暫存檔建立後失敗)。#613 起這道閘在 `akashic fulltext take`(`fetch` 只導航、不寫任何檔),兩支測試名不變、改對 `take` 跑;目的地剩 `--out` 與 `*.unverified.pdf` 兩個(量測見量測文件第 62 列) | -| 63 | **零實例,而「git 答不出來」與「不在 repo 裡」在輸出上分不開,閘又是隱私閘**(#629 R1 verify:fetch 的 git 閘寫成「git 回 0 才算在 repo 裡」,git 起不來、rev-parse 非零、dubious ownership 全被讀成「不在工作樹」而放行——PDF 可能寫進一個會推上 remote 的 repo。2026-09-29 唯讀量測:`$TMPDIR`、`~`、`~/Downloads`、`/tmp` 逐層往上都沒有 `.git`) | ✅ **寫(祖先沒有 `.git` 由檔案系統事實判;有才問 git,答不出來一律拒絕)** | 第 3 列同形——沒被檢查冒充成檢查過且乾淨——而後果更重:外流進 remote 不可逆(git 隱私邊界)。git 呼叫改走 `LibraryStore.hardenedGit`(#585 那一份),不另寫第三份。釘住 `FulltextFetchHardeningTests` 的 git 一族七個測試(#613 起這道閘在 `akashic fulltext take`,測試改對 `take` 跑,名稱不變)(量測見量測文件第 63 列) | -| 64 | **零實例,而兩個 JSON 解析器對同一份輸入安靜地給出不同答案**(#629 R1 verify:`JSONSerialization` 對重複鍵取第一個、吃掉字串開頭的 U+FEFF、接受尾隨逗號、拒 NaN,Python `json.loads` 四處都相反;從 Python 移植過來的 `abstracts-to-proposals`、`crossref-match` 因此可能對同一份檔讀出不同的欄位值。) | ✅ **寫(`PyJSONParser`,照 `json.loads` 的文法)** | 第 4 列的偽裝性:解析成功、欄位有值,只是那個值不是來源的意思;`lossless-intake` 的「來源給什麼收什麼」在解析這一層就可能失真。釘住 `PyJSONParserTests`(38 個 Python 案例)與 `AbstractProposalsPythonJSONTests`。**誠實邊界**:仍有三處與 Python 不同(深度上限 512、孤立代理對拒絕、超出 `Int` 的整數退成 `Double`),寫在型別 doc(歷輪補記與量測見量測文件第 64 列) | -| 65 | **零實例,而同一個推導有四份、各自讀起來都通順**(#663:CLI、MCP、App 三個讀取面與匯出端各自推導「這個人的隸屬現況」,只被觀測到的段在三個面說成「曾隸屬」、匯出端說 `undetermined`,要拿兩者對照才看得出。) | ✅ **寫(源碼掃描守衛)** | 第 13 列同形(清理過之後的零):四份併成 `TimelineOf.standing` 一份之後,第五個讀取面若又自己寫 `current ?? latestPastSegment`,不會有任何跡象。`entity-backlink-completeness` 執行細節 2 的「一個讀取面只能有一條實作路徑」在這裡由源碼掃描兌現。守衛 `TimelineStandingTests.testNoReadSurfaceCallsLatestPastSegmentDirectly`,負控 `testTheScanRecognisesAViolationAndAllowsTheCoreFiles`(歷輪補記與量測見量測文件第 65 列) | -| 66 | **零實例,而帳密一旦寫進 git 追蹤的 YAML 就收不回來**(#674:person 與 venue 的 references 寫入面對 retrieval 的 `url` 只收 http/https 且不含帳密、`retrieved` 是 ISO 8601、`status` 在 100–599。) | ✅ **寫(入口拒絕,整批零寫入)** | 第 18 列同形:零量的是出口,守衛裝在入口。多一層:url 裡的帳密是秘密(`lossless-intake` 不收的第 1 類),寫進去之後 git 歷史裡永遠在。**誠實邊界**:只擋 userinfo,query 或路徑裡的 token 擋不到;~~`enrich` 的來源欄位沒有跟著收緊,是另一份契約——而 live store 僅有的 33 筆 retrieval url 正是經它寫入的,追蹤於 #695~~ → **#695(2026-09-30)起不成立**:守衛搬到 Core 的 `RetrievalWriteShape`,`enrich` 的 `sourceURL`/`sourceRetrieved`/`sourceStatus` 呼叫同一個函式(整批拒絕);那 33 筆全數通過(2026-09-30 重量)(歷輪補記與量測見量測文件第 66 列) | -| 67 | **零實例,而欄位的語意改了、卻沒有任何讀者**(#684:index 的 `entries.orphaned` 在 #609 改由 `Entry.zoteroLinkState == .orphaned` 判定,`schemaVersion` 仍是 5。2026-09-29 量測:`Sources/` 讀這一欄的地方 0 處、建表恰 1 處) | ❌ **暫不做(不 bump)** | 第 10 列同形——要不要做取決於一個還不存在的東西,這裡是讀者。index 是衍生物,store 一寫就因 mtime 過期重建,舊 binary 建的 index 到那時自癒;沒有讀者就沒有東西受漂移影響,bump 只會讓每個使用者白重建一次。**觸發條件**:出現第一個讀者時,`schemaVersion` 與 `PRAGMA user_version = 5` 兩處同步升 6(後者是寫死的重複)(量測見量測文件第 67 列) | -| 68 | **零實例,而清單會由一次匯入一步跨過**(#684:MCP `akashic_import_zotero` 的 `ambiguousSourceClaims` 至多 20 個來源、每個來源 20 個宣稱者,揭露 `ambiguousSourceClaimsTotal`/`ambiguousSourceClaimsTruncated`;CLI 全列。2026-09-29 唯讀量測:work 2,572、Zotero 來源 535、被 ≥2 筆宣稱 0、最大宣稱者數 1。) | ✅ **寫** | 與第 18、30 列同形:沒有曲線,實例會由一次匯入一步跨過;上限保護的是 LLM context,不是 store。**觸發條件**:任一次匯入回 `ambiguousSourceClaimsTruncated: true` 就重開(量測見量測文件第 68 列) | -| 69 | **零實例,而它是沿革位置的第一個寫入面**(#675:`update-venue --edit-name-segment`/MCP `edit_name_segment` 改名字段的時間欄位、`source`、`note` 或刪段。上限沿用既有常數——一次 200 項、理由 4,096 位元組、`source`/`note` 65,536 位元組;`attested` 200 點是推估、沒有母體(R1 起每個點也有 65,536 位元組上限);ISO 8601 前綴與區間有效性只在這個入口驗。) | ✅ **寫(整批拒絕、零寫入、不截斷;時間欄位入口驗)** | 與第 45 列同形:上限只擋單次,累積由第 16/31 列出聲。差別在對象:第 22 列替沿革保留位置、第 43 列守合併時不把它拿掉,而在此之前**沒有任何面寫得進去**——這是第一個。ISO 只在入口驗、不升成 `validate()` 的 error:載入端對 venue 日期不驗是既有裁決(#85),升級會讓手改的值從「載入得了」變成「所有寫入被拒」。**誠實邊界**:手改的非 ISO 值仍載入得了,沿革豁免不認它。**觸發條件**:帶時間、source 或 note 的名字段數 > 0——那時第 22、43 列同時到期(歷輪補記與量測見量測文件第 69 列) | -| 70 | **零實例,而它是清單清空之後留著的機制**(#690:`trigger-coverage` 的「宣告範圍裡不在受保護集合的檔」檢查,與它的已知缺口清單 `.githooks/acknowledged-ci-gaps.txt`。2026-09-30 量測:宣告範圍的缺口 **0**、清單條目 **0**。) | ✅ **寫檢查、保留清單(清單是資料檔,每條分支各有負控;#711 起清單記缺口大小,變大即紅)** | 第 13 列的零是「清理過之後的零」;這一列同形,多一件事:**清空它的是一次計費裁決**。`paths` 若因成本再收窄,同一個缺口立刻回來,那時要的是「列管並追蹤」,而不是只有紅燈或什麼都沒有。留著的代價是一條可能變成豁免的路,所以四道閘:每一條必附 `#`、缺口消失而條目還在是缺口(過期)、格式不對是缺口、**缺口變大是缺口**(#711,見下)。**第四道閘是 #710 R1 verify 留下的另一半**:清單原本不記缺口多大,issue 接受的是「那一刻的那些檔」,而範圍長了新檔時 rc 仍是 0——接受從一個具體的缺口悄悄變成任意大小的缺口。**觸發條件可檢查**(指令見量測文件):清單有條目時,逐條確認它的 issue 還開著;issue 關了而條目還在,把條目拿掉——缺口若也還在,守衛照常紅(歷輪補記與量測見量測文件第 70 列) | -| 71 | **零實例,而它守的是另一支守衛的輸入讀不讀得懂**(#689:`migrated-guard-control` 的兩個出口——一支 harness 以命名慣例或 `// negative-control-for:` 宣告了某支守衛、source 裡卻沒有執行它的寫法(「宣告是空的」);`run-guards.sh` 裡一處 `.build/debug/akashic-guards` 不落在任何被抽取認得的呼叫裡(「抽取認不出」)。) | ✅ **寫(兩個出口都計入 rc,各有負控)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」,換一個謂詞就好;這一列的兩個出口補的是**另一支守衛(負控的抽取)自己的輸入**,而補丁有一個第 21 列沒有的失效方式:**它的失敗條件若與它要補的那個抽取相同,抽取漏掉的寫法它也漏掉**,零實例量到的就只是「看不到」而不是「沒有」。**觸發條件可檢查**(指令見量測文件):兩個數應恆為 0;非零時先看是不是 runner 或 harness 換了寫法——改成獨立一行、或讓宣告的守衛真的被執行,不要放寬偵測。**2026-10-01(#707)補記:兩個出口隨文字判準一起退場,本列的裁決(寫)不變、出口換了**——`migrated-guard-control` 改讀 `run-guards.sh` 這一次執行留下的紀錄(格式與寫入點在 `Sources/akashic-guards/GuardRunLog.swift`)。(歷輪補記與量測見量測文件第 71 列) | -| 72 | **零實例,而上限是一次裁決給的數字,守的是一步就跨得過的入口**(#703:`sources/` 單份內容的上限 `LibraryStore.maxSourceBytes`=268,435,456 bytes(256 MiB;裁決原話「256 MB」),`store-source`(CLI/MCP `akashic_store_source`)、`SourceStore.storeSource`、`ZoteroStorageFile` 的定位與開檔、`copy-zotero-attachments`(含乾跑)都讀這一個常數。) | ✅ **寫(超過的不截斷、不存、具名大小;單檔入口整個拒絕,批次入口逐檔略過)** | 第 18 列是入口的單一字串上限,值夾在兩個量測錨點之間;這一列同形而對象是整份內容,而且**只有一個錨點**:上限是使用者 2026-09-30 的裁決,約為最大單檔的 48.8 倍(以 MB 粗算 256/5.5 ≈ 46),不是兩個量測夾出來的。與第 18 列不同的是拒絕的形狀:`copy-zotero-attachments` 的其他「這個檔不能收」(檔案不在、不是普通檔、0 byte)本來就逐檔具名略過,超過上限是同一類,整批拒絕會讓一個大附件擋住其餘全部——所以 `lossless-intake` 有界拒絕的第 1 條(整批)在這個入口換成逐檔、具名、附大小,該檔同批寫明為什麼仍不是靜默丟棄;單檔的 `store-source` 就是一整批,照第 1 條整個拒絕。**觸發條件可檢查**(腳本見量測文件):最大單檔逼近上限時回 #703 重開——那時先問那份檔是不是一份文獻的全文。(歷輪補記與量測見量測文件第 72 列) | -| 73 | **零實例,而殘留與進行中長得一樣**(#703 R1 verify 第 13、18、22、23 則:`writeBlob` 的暫存檔 `.<62 hex>.incoming-` 只靠 `defer` 清理,行程在複製途中被殺掉(SIGKILL、斷電、逾時)時留在分片目錄裡,一份可以到上限那麼大;`auditSourceIndex` 只認 62 hex 的檔名,doctor 看不到、也沒有清理面。) | ✅ **寫(warning 級:doctor 的 sources 一致性逐行列路徑與大小,MCP 與 App 讀同一份 audit;只報不刪)** | 第 15 列的零是本機的、第 23 列的可達路徑是本 change 自己開的;這一列兩者都有一點而理由不同:暫存檔是 #703 自己開始管的(先前的 `Data.write(.atomic)` 由 Foundation 管暫存,窗口只有一次寫入那麼長;現在是逐塊複製兩遍 I/O 的時間),它**必然**會在被殺的行程之後留下,而它與一個正在進行的存檔在磁碟上完全相同——所以守衛只能報、不能刪。以年齡判斷「一定是殘留」是猜(一份 256 MiB 的檔在慢磁碟上要寫多久沒有上界),猜錯的代價是刪掉別人正在寫的暫存、讓那一次存檔失敗;報而不刪的代價是人看一眼。**severity 是 warning**:記錄與 blob 都沒有壞,佔的是磁碟;它仍計入 `hasFindings`(要人看一眼,與孤兒 blob 同級)。**觸發條件可檢查**(指令見量測文件):數目應恆為 0;非零時先確認沒有 `store-source`/`copy-zotero-attachments` 在跑,再刪掉那幾個檔。(歷輪補記與量測見量測文件第 73 列) | -| 74 | **零實例,而寫入點分散、沒有單一出口可以一次包住**(#708:App 的每一個 `writeEntry(`/`writePerson(`/`renameEntry(`/`renamePerson(` 呼叫要在 `AppState.recordingLegacyCopies` 的範圍裡——在範圍外,寫進 `entities/` 而 legacy 拷貝刪不掉的那一筆擲 `legacyCopyNotRemoved`,畫面上是「操作失敗」,與 CLI/MCP 把它記在成功那一側(#705)是兩種說法。) | ✅ **寫(源碼掃描守衛)** | 第 65 列同形(清理過之後的零,由源碼掃描守):新的寫入點忘了開範圍,同一件事在 App 回到「操作失敗」而沒有任何跡象——它只在 legacy 拷貝刪不掉時才發生,平常的測試碰不到。判準是**字面包含**:寫入呼叫要在某個 `recordingLegacyCopies {` 的大括號裡。(歷輪補記與量測見量測文件第 74 列) | -| 75 | **零實例,而數不出來等於沒有上限**(#613:`akashic fulltext fetch` 每次準備導航到 PDF 連結(或把頁面的下載按鈕交給人)就在 store 之外的帳本記一筆,同一站每個 Asia/Taipei 日曆日最多 10 次(使用者 2026-10-01)。帳本讀不懂——不是普通檔、有一行不是 `{"at": 帶偏移的 ISO 8601, "site": 主機}`、`at` 沒有明確的時區——時在開任何分頁之前拒絕、說出第幾行。) | ✅ **寫(fail-closed:整個拒絕,不略過壞行)** | 第 52 列的形狀(少算的普查與「查完歸零」分不開)落在一個上限上:略過一行讀不懂的就是少算一次嘗試,而少算正是上限要擋的方向——漏停的代價是出版商封鎖使用者機構的 session,誤停的代價是使用者修一行帳本。沒有時區的時間不知道是哪一天,同樣拒收(全域 CLAUDE.md〈時區〉:寫進檔案的時間一律帶 offset),寫入端一律寫 `+08:00`。**誠實邊界**:`--ledger` 與 `HOME` 仍能換一個帳本——上限對一個被引導的 agent 是自願遵守的;CLI 對非預設的 `--ledger` 在 stderr 警告、SKILL 明寫禁止,但程式不擋(測試要用它)。**觸發條件可檢查**:`ls -la "$HOME/Library/Application Support/akashic/"`;`fetch` 以結束碼 1 報帳本讀不懂時,照訊息指的那一行修或刪,不要刪整個帳本來繞過上限(歷輪補記見量測文件第 75 列) | -| 76 | **零實例,而收進來的檔是別人給的路徑**(#613:`akashic fulltext take --from` 收使用者存下來的本機檔。`--from` 必須是普通檔——`lstat` 拒 symlink、目錄、FIFO,`O_NOFOLLOW`+`O_NONBLOCK` 開(lstat 與 open 之間被換成 FIFO 時不卡住)、`fstat` 再確認——大小不超過 `LibraryStore.maxSourceBytes`,讀取至多讀「上限+1」位元組(讀取當下還在增長的檔不撐爆記憶體);只讀一次,同一份位元組先驗證、再存檔。) | ✅ **寫(整個拒絕、什麼都不寫)** | 第 62 列守的是輸出目的地,這一列守輸入:跟隨一個 symlink 會把別的路徑上的內容收成這一篇,而驗證與存檔若各讀一次,兩次之間被換掉的檔會以驗證過的名字存下別的位元組——所以只讀一次,驗的就是存的。上限不另立數字:超過 `store-source` 上限的檔收進來也存不進 store(第 72 列的常數,同一份)。**誠實邊界**:讀取上限的「讀至多上限+1」沒有測試能量到記憶體用量(只量得到拒絕);`take` 沒有算 digest,驗證與入庫之間只靠 `--out` 不被改(歷輪補記見量測文件第 76 列) | -| 77 | **零實例,而製造它的是本張自己的回歸——位址被佔住時工具說成功**(#703 R2 verify 第 4、5、6、15 則:`sources/` 的位址上是目錄、symlink(含懸空的)、特殊檔案,或普通檔的大小與 `index.jsonl` 那一列的 `bytes` 不同。2026-10-01 唯讀量測 live store:普通檔 blob 103、位址上不是普通檔 0、index 列 103、大小與 index 不符 0。) | ✅ **寫(warning 級:doctor 三個面的 sources 一致性列出位址問題,只報不刪;寫入端——`store-source`、`--add-source`——具名拒絕、不寫 index)** | 第 26 列的理由是「閘與守衛是同一條不變式的兩半」;這一列同形而**閘與守衛是同一批裡一起長出來的**:位址上的東西只有一個分類(`sourceOccupant`),寫入端以它拒絕、audit 以它報告——兩處各自判,正是 R2 之前四個讀者(`writeBlob`、`checkStoredBlob`、`sourcePresence`、audit)對同一個狀態給出三種答案的原因。**severity 是 warning、計入 `hasFindings`**(與孤兒 blob 同級:記錄沒有壞,位址上的內容不是它說的那一份)。**觸發條件可檢查**(腳本見量測文件):兩個數應恆為 0;非零時照訊息把那個位置移走,再對原檔重跑 `store-source`(或 `copy-zotero-attachments`);大小不符時錯的也可能是 index 那一列的 `bytes`——重存不會改既有的列(append-only),要手改 `index.jsonl` 那一行(歷輪補記與量測見量測文件第 77 列) | -| 78 | **零實例,而製造它的寫入面是本輪自己開的,且拒絕的依據是另一條裁決刻意留下的提醒**(#564:名字分類的判定記錄。venue 合併對「被併者的 authorized 會被降級、而它帶名字分類的判定記錄」改為拒絕;寫入名字分類記錄要求 store format ≥ 22。) | ✅ **寫(拒絕:`DivergenceResolveError.wouldDemoteJudgedAuthorized`,住在 `validateVenuePreconditions`,preview 與實跑共用;寫入閘:`LibraryStore.assertNameClassificationRecordsWritable`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列同形而多一層:**讓這個狀態可達的寫入面(五個名字分類面)與拒絕必須同批落地**,而拒絕的位置早就被預告過——`authorizedDemotedByMerging` 的 doc 明寫「#564 裁「留」且落地後,對有 judgement 的 authorized 升成拒絕條件、對機械值維持提醒」。**寫入閘是另一半**:format-21 binary 對這種記錄整檔 quarantine(三個成因各自足夠,`StoreVersion` 的 22 那一段),所以在 marker 升到 22 之前寫入面一律具名拒絕——零寫入而不是寫出舊 binary 讀不懂的檔。**機械值仍是提醒**:沒有記錄的降級照舊合併並出 warning(`NameClassificationMergeTests.testMechanicalDemotionStillMergesWithAWarning` 釘住)——不得把拒絕放寬成「一律拒」,那會讓合併對 470 筆機械值全部無用。**觸發條件可檢查**(腳本見量測文件):名字分類記錄數 > 0 即代表合併拒絕與寫入閘從零實例變成有實例——那時重跑 `NameClassificationMergeTests`,並確認第 13 列的死 verdict 掃描與第 35 列的重複 reference 掃描對這種記錄沒有誤報(歷輪補記與量測見量測文件第 78 列) | -| 79 | **零實例,而降級把一道拒絕從共用段挪到只有一邊走得到的地方**(#709:同一筆記錄的 legacy 拷貝——同一種、同一個 id,一份在 `entities/`——造成的 UUID 共用與 citekey/key 重複,自使用者 2026-10-01 的裁決起降為 warning,不再讓共用的 `assertNoCrossRecordErrors` 擋下改名與合併。原本由那個 error 順帶擋住的兩格:(a) `resolve-divergence` 的**候選**有 legacy 拷貝(被併者只刪 `entities/.yaml`、legacy 那份在下一次 load 復活成唯一的一份);(b) **會被改指的 entry** 自己有 leftover(改寫它會被 #631 拒絕)。) | ✅ **寫(`DivergenceResolve.assertRewriteTargetsWritable`,住在 preview 與實跑共用的 `validateResolvePreconditions`;測試把 preview 與實跑逐一配對:work/person/venue 三種合併、改名留下的拷貝、對照組)** | 第 40 列的理由是「拒絕問在錯的時間點」,第 8 列是「零的來源在別處」;這一列的拒絕沒有錯時間、零也不由別處造成,是**降級改變了它在哪裡**:共用段的 error 閘一旦變窄,順帶被它擋住的格子就各自找新家,而只裝在實跑那一邊的新家讓 dry-run 說謊——正是 `validateResolvePreconditions` 的 doc 記過的 #139 F1。**位置是裁決的一半**:寫在共用段而不是 `previewResolveDivergence`,兩邊才不可能再分岔;`commitResolution` 裡那一道留著當最後一道防線(它的名單由實跑迴圈算出,兩份名單一致只由測試釘住)。負載下的絕對值不可比,只當量級;**觸發條件可檢查**(指令見第 40 列):legacy 殘留數 > 0 時 `resolve-divergence --dry-run` 與實跑對每筆合併要擲同一個錯——`ResolveDivergencePreviewLegacyCopyTests` 釘住(歷輪補記見量測文件第 79 列) | -| 80 | **零實例,而上限是一個沒人量過的裁量值,且它讓一條使用者裁決在尾端不成立**(#611:`DOINomination.maxGroupSize`=10。Zotero 匯入新建的 work 與另一筆 work 共用 DOI 時,使用者 2026-10-01 的裁決是「照建,並自動記一筆歧異提名」;一個 DOI 被超過 10 筆 work 共用時(這一趟新建的加上其餘的)一對都不記、只回一列 `groupTooLarge`——那一格**偏離**該裁決的字面,而使用者沒有裁決過這個數字:10 是 #611 第一輪修正裁量的,不是量出來的。) | ✅ **寫(超過的整個 DOI 一對都不記、回一列 `groupTooLarge`,匯入照建;沒記下來的列讓 CLI 以非零結束)** | 第 18 列的上限夾在兩個量測錨點之間、第 72 列有一個使用者給的數字;這一列**沒有任何錨點**——門檻不是量出來的、也不是使用者給的,誠實的說法是它是裁量。不寫的代價是 C(k,2) 筆記錄:k=80 → 3,160 筆、約 10 秒(#611 R1 verify DA 實測),而一個 DOI 被這麼多筆共用幾乎必是書或資料集的概念 DOI,逐對提名沒有資訊量。**誰決定的**:Claude 在第一輪修正裁量,**使用者尚未確認**;使用者的原話只涵蓋「照建並記一筆」,沒有說超過某個大小時怎麼辦——所以這一列的狀態是「待使用者確認」,不是「已定案」。**重開觸發(可檢查,腳本見量測文件)**:(1) live store 出現共用同一個 DOI 的 work ≥ 10 筆的組——門檻開始碰到實資料,那時要用實例重新決定這個數字;(2) 使用者對「超過門檻時怎麼辦」給出裁決,或裁決 `groupTooLarge` 不該讓 CLI 非零(歷輪補記與量測見量測文件第 80 列) | -| 81 | **零實例,而製造它的是本張自己改的覆寫語意——同一個 id 不得換成另一組候選**(#611:`recordDivergence` 在磁碟上同一個 id 的歧異記錄、候選 (shape, key) 與這次不同時整個拒絕(`StoreIOError.divergenceIdHeldByOtherCandidates`),先前互相覆寫、不出聲。) | ✅ **寫(拒絕、不猜、原記錄逐位元不動;訊息點名現有那一筆的候選、說原因是 key 還是形狀、出路是 `dismiss-divergence` 或 `resolve-divergence`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列同形而方向相反:先前**不拒絕**——兩者互相覆寫、不出聲(#611 R1 verify 第 1/21 列立案的缺陷)——所以這道拒絕是把一個先前靜默發生的覆寫變成可見的失敗。零實例是因為 live store 只有一筆歧異記錄而且它的 id 沒過期,**不是因為這個形狀不可達**:citekey 改名之後 `renameEntry` 把歧異記錄的候選 key 就地改成新名、id 不換(真實路徑),之後 Zotero 匯入再新建一筆取回舊 citekey,記一對就撞上同一個 id。**重開觸發(可檢查,腳本見量測文件)**:量測腳本的「id 已過期」數 > 0——這個形狀已經在 store 裡,那時要決定的是 `rename` 改寫候選時要不要像合併一樣重算 id 並改名舊檔(需要遷移)(歷輪補記與量測見量測文件第 81 列) | -| 82 | **零實例,而上限、理由形狀與 person 的理由要求守的是同一批名字分類面的入口——它們的失敗都會變成一筆刪不掉的記錄**(#564 修正輪,使用者 2026-10-02 裁決第 1、4 點與 R1 verify 的理由形狀:名字分類的寫入面一次至多 200 個名字(venue 三條腿合計、organization `authorize`、person `fields.names` 的 authorized——R2 起 variant 不寫記錄、不計、`remove_names`)、單一名字至多 65,536 位元組;理由開頭不得是組合符號、格式或不可見字元、要有字母或數字;person 的 `fields.names` 讓名字進出 authorized 時理由必填。) | ✅ **寫(入口整批拒絕、零寫入、不截斷;三個數字都不另立:200=`maxSpecsPerCall`、65,536=`AddOnlyEnrichment.maxValueBytes`、理由的 L/N 類與名字不變式同一個謂詞)** | 第 30/45 列的上限只擋單次呼叫、累積由預算 warning 出聲;這一列同形而多一層:**被守的記錄只追加、移除面不刪**——名字分類記錄是判定史,唯一刪得掉的路是修正輪的「最後一筆是撤回的名字連同記錄一起刪」(第 2 點),而那條路要先撤回;一次寫進一千五百筆記錄的錯誤要逐名撤回再刪。所以入口要先擋,不是事後清。理由形狀的那一半不是上限、是文法:以 Grapheme_Extend 字元開頭的理由會與動作前綴的全形冒號併成同一個字,只看 Character 的讀者把它讀成一般 reference(`parse` 已改比 scalar 前綴,入口另外拒收,兩道一起);只有不可見字元的理由則是「必填」被字面滿足、什麼都沒說。**觸發條件可檢查**(腳本見量測文件):某筆記錄的名字分類記錄數逼近讀取上限的預算 warning(第 16/31 列)時,回來看是不是有呼叫端繞過了入口(歷輪補記與量測見量測文件第 82 列) | -| 83 | **零實例,而零是 live store 在 APFS 上的零——同一份程式在別種磁碟區上,每一次存檔都是實例**(#703 b26 F6,使用者 2026-10-02 裁決:`sources/` 所在的磁碟區做不到不覆寫的原子放置——`renamex_np(RENAME_EXCL)` 與 `link(2)` 都不支援,exFAT、FAT32 即此類。2026-10-02 唯讀量測 live store:`sources/` 所在的磁碟區是 APFS,所以今天這道閘不會觸發。) | ✅ **寫(拒絕:每一次存檔在建立任何檔案之前具名拒絕、零寫入;`LibraryStore.assertSourcesVolumeCanPlace`,`storeSource` 與 `preflightStoreSource` 共用;旗標讀不到時 `writeBlob` 在任何分支之前以 `assertDirectoryCanPlace` 實際放一次——b29 V5)**;**不寫替代放置路** | 第 15 列的零是「資料在別台機器上缺席」;這一列的零是**這台機器的磁碟區種類**——同一份程式換一種檔案系統行為就變。裁決有兩半。**寫拒絕**:R1 把存檔改成 `RENAME_EXCL` 之後 exFAT 上每一個新 blob 都失敗,先前存得進去——那是回歸,由使用者裁決「不支援」而不是設法支援。替代路為一種檔案系統造出一組「只在那裡才會走到」的程式,而那組程式的不變式恰好是那種檔案系統打破的。**觸發條件可檢查**:使用者要把 store 放在不支援的磁碟區上時重開——那時要裁的是「磁碟區上的替代放置法」,不是回頭補第三條路(歷輪補記與量測見量測文件第 83 列) | -| 84 | **零實例,而它們收回的是一則裁決被實作放寬過頭的部分**(#613 R2 verify,使用者 2026-10-02 裁決 1、2 與主 session 對 MEDIUM 的裁定:`akashic fulltext fetch` 把裁決 1 的「別的主機上沒有標記 → 交給人、批次繼續」實作得比裁決寬——還在載入的頁面第一眼就被說成「沒有標記」、網址只是提到 doi.org 的登入頁被說成「DOI 解不開」、離線時 Safari 的錯誤頁也是「DOI 解不開」、`--resume-stage followed` 對位置漂移到的任何分頁跑讀頁 JS。) | ✅ **寫(往整批暫停或拒絕收;別的主機上交給人只留給四種:PDF、載完而沒有命中清單的頁面、讀不到而可能是 PDF 檢視器的分頁、doi.org 自己的查無頁)** | 第 75 列守的是一個上限、第 76 列守的是收進來的路徑;這一列守的是**一則放寬中止條款的裁決的邊界**:裁決 1 把「別的主機」從整批暫停改成交給人,而實作在三個地方把「交給人」給了裁決沒有點名的頁面(沒載完的、登入頁、Safari 的錯誤頁)。交給人時批次繼續,所以每多一個不該交給人的頁面,就是下一篇照常對同一個站發請求——中止條款當初要防的事。判準因此是裁決的字面:裁決點名的交給人,其餘往停的那一邊倒。**觸發條件要人觀察**(b31 W4 第 11 則:先前寫「可檢查」,而帳本只記嘗試、不記交給人的原因,`ls` 只說得出 `fetch` 跑過沒有):交給人的 `left-site`/`unverifiable`/`doi-not-resolved` 經使用者看過其實是登入頁、驗證頁或錯誤頁,那一格就是這一列漏掉的形狀,回來加閘。`ls -la "$HOME/Library/Application Support/akashic/"` 是前提:帳本出現之前不可能有實例(歷輪補記與量測見量測文件第 84 列) | -| 85 | **零實例,而它守的是一道分類規則與它造出的記錄之間的一致——合併接上去的記錄若與名字的分類矛盾,之後按位置讀記錄的兩道閘會得出相反的結論**(#564 R2 verify:b29 V1 第 0/2/6 列。保險:合併後倖存者上若有名字的最後一筆與分類矛盾——撤回而仍在分割裡,或指定/確認而不在——而那是這次合併帶進來的,preview 與實跑都拒絕。) | ✅ **寫(拒絕:`DivergenceResolveError.wouldContradictClassificationTail`,住在 `validatePersonPreconditions`/`validateVenuePreconditions`,與實跑同一個 `mergedPersonKeeper`/`mergedVenueKeeper` 算;delta——倖存者原本就有的不擋)** | 第 78 列守的是「合併不替人改判定」(降級帶記錄的對外形);這一列守的是**合併接上去的記錄本身說的話**:刪名字閘(`latestAction`:最後一筆是撤回才可刪)與寫入面的去重(只比最後一筆)都按位置讀,尾端一旦與分類矛盾,前者會放行刪一個仍是對外形的名字、或叫人去「撤回」一個不在分割裡的名字。主修在搬法(按位置接);保險擋的只剩來源本身就不一致的被併者——手改,或修正輪之前的 binary(那時寫入面的去重比整份歷史,「指定 R → 撤回 → 再以 R 指定」的第三筆會被丟),零實例而且不靠人記得。**觸發條件可檢查**(腳本見量測文件):矛盾的(分割, 名字)應恆為 0;非零時那筆是手改或舊 binary 寫的,出口是在那筆記錄上重下一次與分類一致的判定。(歷輪補記與量測見量測文件第 85 列) | -| 86 | **零實例,而三種實體裡只有它刪得到沒有名字——兩個鄰居早就擋了**(#564 R2 verify:b29 V1 第 18 列。`update-person --remove-name`/MCP `remove_names` 刪完 person 沒有任何名字時整批拒絕、零寫入,乾跑同樣拒絕;organization 的 `remove_names` 與 venue 的 `edit_name_segment`(`.noNamesLeft`)在修正輪就擋了。) | ✅ **寫(入口拒絕;不改 `Person.validate()`)** | 第 25 列把名字的不變式裝在 store 邊界,因為入口有五個;這一列的形~~只有一個入口造得出~~有兩個入口——`remove_names` 與 `fields.names` 的空替換,所以擋在入口、與兩個鄰居同一句話。**不改 validate** 的理由:沒有名字的 person 讀得進來、不毀資料(`doctor` 已經報「no authorized name」),升成 error 會讓手改出來的那一筆擋住它自己所有的寫入;這一格要的只是工具面不再造出它。**觸發條件可檢查**(腳本見量測文件):沒有名字的 person 數應恆為 0;非零時那筆是手改或 R2 verify 之前的 binary 寫的,`update-person --fields` 的 names 補一個名字即可(歷輪補記與量測見量測文件第 86 列) | -| 87 | **零實例,而它關掉的是一個把「讀不懂」折成「空的」的回退——接在後面的是整份替換**(#700 b31 W5 MEDIUM 0、1:`ensureLayout` 對 store 根目錄 `.gitignore` 的處置先前是 `(try? String(contentsOf:encoding:.utf8)) ?? ""`,讀不出來(非 UTF-8、UTF-16、權限 000)就當成空檔,再以只含 sources 區塊的內容原子替換——使用者原有的規則(擋 raw 逐字稿的 `*.srt` 之類)全部消失、結束碼 0、沒有任何訊息;symlink 被換成一般檔。) | ✅ **寫(讀不懂的不改寫:`file add` 在建立任何東西之前具名拒絕、`doctor` 與兩個匯入照常建佈局並印 warning(匯入是使用者 2026-10-05 的裁決:匯入不寫 `sources/`,防線在 `store-source`);要加區塊時只附加位元組;`LibraryStore.inspectSourcesIgnore`/`applySourcesIgnore`,`SourcesIgnorePolicy`。b36 verify 起「區塊已在」要與 git 的判讀一致:規則行照 git 的切法、symlink 不論指向的內容都不算已在——git 不讀 symlink 的 `.gitignore`)** | 第 3 列是「沒被檢查」與「檢查過且乾淨」在輸出上分不開;這一列是同一種折疊落在**輸入**上:「讀不懂」與「空的」在程式裡是同一個值,而接在後面的動作是整份替換,所以一個錯的折疊把一次附加變成一次刪除。零實例不降低它的份量——失敗是安靜的(結束碼 0),被刪的是隱私邊界的規則,`git add -A` 之後外流不可逆。裁決有三半:**標記以位元組比對**(已有區塊時不必讀懂整個檔,尾端有非 UTF-8 位元組也不動);**加區塊只附加位元組**(原有位元組不經解碼、不重寫);**讀不懂就不動**——`file add` 拒絕、診斷面與兩個匯入改報(診斷工具最該說話的時候不是最該掛掉的時候,`doctor` 不因此失敗)。**觸發條件可檢查**(腳本見量測文件):註冊的 store 裡 `.gitignore` 不是 UTF-8 一般檔、又沒有區塊的數應恆為 0;非零時那個 store 的 `file add` 會拒絕、`doctor` 與兩個匯入會報 warning,處置是人把它轉成 UTF-8 或自己加那段(歷輪補記與量測見量測文件第 87 列) | -| 88 | **零實例,而守衛只對它認得出的單位求值——認不出的寫法讓量測與它的閘一起消失、守衛照樣綠**(#711 R3 verify:b31 W6 第 0、2–7、20 列。`zero-instance-rows-audit` 的自證閘自 R2 起只收一種寫法,但「哪些文字是一個單位、哪些單位是量測」沒有寫出來:R2 的單位是 fence 內一個實體行、fence 外同一實體行裡的一段 inline code,binary 與計數拆在兩行(`\`、行尾的豎線或 `&&`、跨行的 inline code)、寫在 fence 與 inline code 以外(縮排區塊、`
`、引用區塊裡的 fence)、或計數的寫法沒被認出(`grep '-c'`、樣式含豎線而 `-c` 在後、`uniq -c`、預設值展開),一條量測就不成單位、不被計入;把 34 條裡的 33 條改寫成預設值展開,閘全部消失而守衛 rc=0。) | ✅ **寫(辨識補寬+條數地板+退場區塊個數;認不出的三類照實列出、由地板兜底)**;R4:認不出時偏向「是量測」與「同一個單位」、地板依閘去重、退場標記拿掉、認不出的寫法改寫成開放清單 | 第 21 列的謂詞對那一類成員結構上不可能為真,換一個謂詞就好;第 37 列連正確的謂詞都不存在。這一列在兩者之間:謂詞(模板)是對的,錯在它只對**認得出的單位**求值,而「哪些文字是一個單位」是一個沒寫出來的前提——R2 寫「封閉的模板沒有旁路可找」,對模板為真、對辨識為假。所以裁決有兩半:把那個前提寫出來並補寬(邏輯行、引用區塊、CommonMark 配對、fence 外的文字、不切註解、引號與位置變化的計數選項),**以及**承認辨識不會完整——以變數或檔案當來源、`awk` 自己加總、binary 名與計數不在同一個邏輯行,這三類仍然認不出——所以條數要有一個不靠辨識的地板(同第 16 列:裁決依賴守衛;地板過期只擋得住整批、擋不住少量,新增量測後由人調高)。**R4 更正兩處**:認不出的不只三類(開放的,見 [`measurement-commands-self-prove`](measurement-commands-self-prove.md));退場區塊的個數釘得住、內容釘不住(加一行、換一行都綠),所以 R4 拿掉退場標記、照掃那個區塊。**觸發條件可檢查**(指令見量測文件):棘輪的訊息數應恆為 0;標題列的「合模板」比下限多出許多時調高下限(歷輪補記與量測見量測文件第 88 列) |
-| 89 | **零實例,而防線先前住在一份抄本裡——跑的是模型照文件抄寫的程式,測試釘的是文件**(#692 R4 verify:讀頁面的五道閘——(a) Safari 回報的網址在解析器會分岔的形狀(反斜線、帳密、百分比編碼、IPv6 字面值、空白與控制字元)一律認不出主機;(b) `LAND` 預設 `-`(不比對落地主機)時主機仍要形狀合格;(c) 頁面自報的 `rawLength` 限 0–1,000,000,000;~~(d) 主機不合而換到的是已知的驗證服務時整批暫停(結束碼 2),不當成不可達~~ → (d) 分頁在已知的驗證服務上——不論換到或直接落在——不當內容讀,以頁面文字分等人驗證 3/整批暫停 2(b35,使用者 2026-10-05);(e) 輸出檔與落地主機檔在第一個可能失敗的瀏覽器指令之前刪,讀回的 JSON 由 `trap … EXIT` 刪。) | ✅ **寫(在 `akashic web-read`,剔除集合是 `UnsafeToEmitScalar`(b33 verify 起加 noncharacter,與 `escapesInLLMDocument` 同一個加項);清理順序在區塊本身)** | 第 84 列守的是一則裁決的邊界、第 83 列的零是磁碟區種類的零;這一列的零是**取得路徑從沒跑過**的零,而它的特別之處在防線的**位置**:R3 把這類閘寫成文件裡要照抄執行的程式,跑的是抄本、測試釘的是文件,兩者之間沒有任何東西核對;剔除集合也是 `UnsafeToEmitScalar` 的第二份描述,在 macOS 的系統 Python(Unicode 13)上安靜地少了九個碼位。所以裁決有兩半:寫這五道閘,**以及把它們放進 CLI**——放在抄本裡的閘,通過時證明的是文件,不是實際跑的那一份。**觸發條件可檢查**:verify-venue SKILL 的「沒有實跑過」一段改寫的那一天(對出版商頁實跑過 safari-browser),回來看哪一道閘在真 Safari 上有實例、哪一道的前提(例如 WebKit 回報正規化網址)量過。(歷輪補記與量測見量測文件第 89 列) |
-| 90 | **零實例,而擋它的寫法看起來已經擋住了——`${V:?…}` 只停在管線的一段,計數照印 0**(#711 R4 verify:b33 X6 第 1、3、5、14、15 列。R3 把第 18、46、49、73、77 列的 `--library "$STORE"` 改成 `--library "${STORE:?…}"`,散文寫「沒設時 shell 停下」。) | ✅ **寫(模板加前置條件;fence 與 inline code 裡任何單位把 `${V:?…}` 寫在管線裡即紅)** | 第 13 列的理由是「沒被檢查」與「檢查過且乾淨」在輸出上分不開,自證閘擋的成因是舊 binary;這一列是同一個分不開換一個成因——binary 根本沒跑。它比第 13 列多一層:擋它的寫法**看起來已經擋住了**(`${V:?…}` 的字面意思是「沒設就停」),文字照著寫,三輪 verify 才量出它只停管線的一段。所以不能只改散文——散文改對了,下一條量測照樣可以寫成同一個形狀。裁決是把前置條件寫進模板(`: "${V:?…}"` 在 `&&` 串最前面,整條不跑;`<參數>` 用到的變數都要列在那裡),`--library "$V"` 另要 `test -f "$V/store.yaml"`(打錯路徑時 `doctor` 會建 store),並在 fence 與 inline code 的**任何**單位把管線裡的 `${V:?…}` 判成錯誤——不只量測:第 73 列的 `find …` 接 `wc -l` 不提 binary。**觸發條件可檢查**(指令見量測文件):兩種訊息數應恆為 0(歷輪補記與量測見量測文件第 90 列) |
-| 91 | **零實例,而揭露與寫入在同一次執行裡——附註的補救到達時寫入已經發生**(#709 第四次 verify MEDIUM 5、LOW 6/13/15、LOW 22,使用者 2026-10-05 裁決 2:三個 bootstrap 的計畫讀到同一筆記錄的 legacy 拷貝時,拷貝裡的 literal 也算一次出現——一篇 work 加它的拷貝讓只被用一次的名字以 ×2 越過 `--min-occurrences`。先前開頭印一行附註「先刪掉拷貝再跑,計數才準」,`--apply` 照寫;真 binary 重現建出 `doe-a`、`journal-of-alpha-things`。) | ✅ **寫(`LibraryLoad.refuseApplyWithPlanBlockers`:計畫讀到拷貝時 `--apply` 整批拒絕、零寫入,拒絕說筆數與處置、清單在乾跑;只數計畫讀到的種類——`BootstrapPlanSource`,2026-10-09 起 `bootstrap-people` 也讀 person 拷貝;`authorize-names` 的寫入集合裡有任何一筆的 id 還有 legacy 拷貝時同樣整批拒絕)** | 第 3 列的理由是「守衛沒說話」;這一列的守衛說了話、卻沒有擋——揭露只有在它到達與動作之間還有一個人時才有用,而 `--apply` 把兩者放在同一次執行裡。所以裁決不是把附註寫得更清楚,是在寫入之前擋(`two-kinds-of-edits` 程式編輯欄的「可預期失敗整批擋零寫入」);乾跑照常列出並說明,那是人還來得及讀的地方。**誠實邊界**:拒絕比計數嚴——拷貝與 entities/ 那份內容相同、計數只是乘二而不越過門檻時也擋;那是裁決本身的取捨(「有拷貝就拒絕」,不是「計數越過門檻才拒絕」)。**觸發條件可檢查**:計畫讀到的種類裡有被 load 標成拷貝的記錄時(`akashic validate` 的「同一筆記錄的 legacy 拷貝還在」警告;只有 legacy 檔、沒有 entities/ 那份的不算拷貝——那一類歸第 102 列),該命令的 `--apply` 以結束碼 1 拒絕——`ShadowedLegacyCopyReadersCLITests`/`ShadowedPairCensusCLITests`/`BootstrapPlanBlockersCLITests` 釘住,對照組是只有 person 拷貝時 venues 照寫。~~legacy 殘留數 > 0 時三個 bootstrap 都拒絕~~(b36 verify LOW 11、18:殘留數 > 0 不蘊含拷貝,也不蘊含每個 bootstrap 都讀到它)(歷輪補記見量測文件第 91 列) |
-| 92 | **零實例,而防線移植時刪除的範圍被放大了**(#692 b33 verify X2 第 0/1 列:`akashic web-read landing`/`check` 刪 `--out`/`--raw` 指的路徑用 `FileManager.removeItem`——對目錄**整棵遞迴刪除**,而且在讀任何輸入之前就刪;被移植的 Python 用 `os.remove`,遇到目錄失敗、不刪。) | ✅ **寫(`WebRead.removalProblem`/`removeFile`:參數先驗,不存在、一般檔、symlink(刪連結本身)才刪,目錄與其他型態以 1 具名拒絕、什麼都不刪;只呼叫 `unlink`;刪不掉除 ENOENT 外具名回報,讀回的 JSON 刪不掉時 READ-OK 改成 READ-FAIL)** | 第 89 列的理由是防線住在抄本裡;這一列是**搬進 CLI 的那一步自己改了語意**:同一個「刪掉上一輪的檔」,Python 的 `os.remove` 對目錄是失敗、Foundation 的 `removeItem` 是遞迴刪除,而兩者在一般檔上長得一樣,所以移植時看不出差異。裁決回到移植前的語意(只刪檔),並把「先驗後刪」寫成順序:任何一個參數不合就什麼都不刪。**觸發條件可檢查**:`WebReadRemovalCLITests`(真 binary:三種參數各指向含 sentinel 的目錄、`--out .`、symlink)全綠即零;實例要看的是使用者回報(刪除沒有留下任何紀錄)(歷輪補記見量測文件第 92 列) |
-| 93 | **零實例,而同一個主機形狀的判斷有兩份,開分頁前的那一份比較寬、也比較早**(#692 b33 verify X2 第 16/39 列:開分頁之前的「完整網址」檢查是 web-access.md 裡的一條正則,由模型照著看;落地之後的 `WebRead.hostIsAcceptable` 是程式。) | ✅ **寫(`akashic web-read url --file`:整串形狀同那條正則,主機另過 `hostIsAcceptable`;`privateSuffixes` 加五個特殊用途名稱;文件那一格改成呼叫它,〈開始前〉第 4 點多一條探測。b36 verify 起整串形狀的頂層名稱也收 `xn--` 形(先前只收字母,比 `hostIsAcceptable` 嚴)、網址檔不修剪(檢查的就是 `$(cat …)` 開出去的那一份),開分頁、導航、取 API 的區塊自己跑它)** | 第 37 列與第 21 列講的是謂詞答不錯或不存在;這一列的謂詞存在、也答得對,**錯在有第二份**——寫在散文裡的那一份由模型執行,比程式那一份寬,而且排在它之前、在請求送出之前。所以裁決不是修正則,是把它換成呼叫同一個函式(`swift-is-the-implementation-language` 放置表第 3 列);特殊用途名稱一併補進那一份(只有一份了,補一次就兩邊都有)。**觸發條件可檢查**:`WebReadTests.testUrlModeSharesTheHostCheckWithLanding` 釘住兩邊同一個判斷;web-access.md 的「完整網址」一格裡不再有正則(`grep -c '\^https://(\[A-Za-z0-9-\]+' plugin/skills/akashic-bootstrap/references/web-access.md` 應為 0)(歷輪補記見量測文件第 93 列) |
-| 94 | **零實例,而它守的是「先看、後寫」之間的窗口——比對 inode 與大小只縮小它**(#700 b33 verify X5 第 0 列 MEDIUM:`ensureLayout` 先 `inspectSourcesIgnore`、建完佈局才 `applySourcesIgnore`;附加前以 device/inode/大小比對代替鎖。live registry 的 store 已有區塊——不會再走到寫入,0 實例) | ✅ **寫(開檔後 `flock(LOCK_EX)`、確認路徑仍指著這個檔、鎖內重讀內容再決定;先看在讀內容之前取 `flock(LOCK_SH)`、等持鎖的寫入者寫完(先看與鎖內重看是同一個判斷,b34 修正輪補);`EEXIST` 與換檔重看(至多 4 次),重看之後區塊已在就算成功;回滾只截這一次寫的、`ftruncate` 的結果要看。同輪補四個不改寫的形狀:有其他硬連結、大到 git 不讀、有標記沒有排除規則、symlink 打不開)** | 第 87 列關掉的是「讀不懂折成空的」;這一列是同一段程式**看與寫之間**的事:一個只縮小窗口的比對被文件寫成保證,而失敗的樣子是兩個程序都以為自己是唯一的寫入者。所以裁決要能排序,不是更精細的比對——鎖讓同時跑的 akashic 排隊,鎖內重讀讓排在後面的那一個看到前一個的結果;`EEXIST` 從「被換掉」改成「重看」,因為它的意思通常正是「別人剛做了你要做的事」。**觸發條件可檢查**(量測文件第 87 列的腳本):註冊的 store 裡會被拒的形狀數應恆為 0;並行的回歸由 `GitignoreConcurrencyCLITests` 釘住(含「持鎖、寫到一半時先看要等」那一支)(歷輪補記見量測文件第 94 列) |
-| 95 | **零實例,而它守的是一句理由會不會落地——沒有記錄要寫時,寫入閘與報告都看不到它**(#564 b34,使用者 2026-10-05 裁決第 1、2 點;b33 X1 第 0/2/3/8/11/13/18/23 列。person `fields.names` 的替換兩道拒絕:(a) 對外形在替換裡整個離開 names——有沒有記錄都一樣,移出要寫「撤回:理由」、記錄錨定 names,所以要留在 variant;(b) 附了理由而沒有名字進出 authorized——只替進出的名字寫,留下的不寫「確認」,理由沒有地方放。) | ✅ **寫(入口整批拒絕、零寫入;拒絕說出放在 variant 的出口,format < 22 時另說寫入閘)** | 第 82 列守的是入口的上限與理由形狀;這一列守的是**理由有沒有地方落**:「判定必附理由」只在理由寫進記錄時兌現,一條不寫記錄的路照樣收理由,就是讓必填變成字面。兩道拒絕各封一條這樣的路——(a) 名字離開 names 時記錄沒有地方錨定,(b) 沒有名字進出時沒有記錄要寫。拒絕而不是自動把名字留在 variant:整份替換的契約是「提及的欄位整個換」,程式替呼叫端改它送的清單會讓結果與請求不同(這一句是實作對裁決第 1 點的讀法,~~交給使用者~~ → 使用者 2026-10-05 05:18Z 裁:維持現狀)。**觸發條件可檢查**(腳本見量測文件):這兩道拒絕沒有 store 上的實例可數;量的是前提——名字分類記錄數與有對外形的 person 數;記錄數開始長時(format 升到 22 之後),回來看有沒有對外形在沒有撤回記錄的情況下從 names 消失(歷輪補記與量測見量測文件第 95 列) |
-| 96 | **零實例,而回應的體積由 store 內容決定、沒有上限**(#564 b34;b33 X1 第 22/27/34 列。organization 的 `remove_names` 報告每個刪掉的名字回 `segments`——R2 加的,段數與每段的 attested 項數都不封頂,一個名字 3,000 段時一次回 1,968,449 位元組,MCP 面整份進 LLM context;沒有時間、source、note 的段回 `{}`、不帶任何資訊。b36 Y1 第 0/1/13 列:b34 只截了段數,每段的 attested 照樣整列——一段兩萬個觀測點一次回 460,506 位元組。) | ✅ **寫(報告截在 20 段,總數照報;不新增回應鍵。b37 起每段的 attested 也截在 20 個,最後一項說出省略數與總數——venue `edit_name_segment` 的 before/after 同一個上限,讀取面全列)** | 第 30/45 列的上限擋的是單次呼叫送進來的量;這一列擋的是**單次呼叫送出去的量**——它由 store 內容決定,呼叫端無法在收到之後丟掉已付的代價(#236 的威脅模型),而同一族的 venue `edit_name_segment` 早在 20 項截斷。截在服務層、CLI 與 MCP 同一份(不分面:刪掉的段的完整內容在 git 的上一版,報告只是索引)。**觸發條件可檢查**(腳本見量測文件):同一個名字的段數逼近 20 時,回來看報告的截斷是否還夠用(歷輪補記與量測見量測文件第 96 列) |
-| 97 | **零實例,而一次搬家讓只該有一份的東西各多了一個可以住的地方**(#711 b35 M1,使用者 2026-10-05 裁決:規則檔拆成規則檔+量測文件,量測寫法抽成規則 `measurement-commands-self-prove`。那份規則引用的模板是守衛 `selfProofTemplate` 的第二份描述;棘輪標記原本與量測同檔,現在兩份檔都放得下它。2026-10-05 量測:規則與守衛的模板一致、規則檔裡的棘輪標記 0 個。)| ✅ **寫(規則裡的模板與 `selfProofTemplate` 逐字相同,規則不見也紅;棘輪標記只住量測文件,規則檔裡出現即紅;量測文件不在即中止)** | 第 13 列的自證與第 88 列的棘輪都假定模板與地板各只有一處;這一列是那個假定被一次搬家打破:同一份模板有了第二份描述(`no-compat-fallback`〈同一件事只能有一份描述〉——兩份說的是同一件事,而規則要給寫量測的人讀,不能只寫一句引用),同一個地板有了第二個可以寫的檔(兩份各一個時下限有兩個數,哪個算數沒有答案)。不寫的代價是安靜的分岔:照規則寫的量測被守衛判紅、或規則放行的寫法守衛不收,兩邊各自看起來都對。量測文件不在時中止、不當成沒有量測,是第 3 列。**觸發條件可檢查**:守衛這三則訊息的數應恆為 0(量測見量測文件第 97 列) |
-| 98 | **零實例,而合併的結論取決於被併者的 key 拼法**(#564 b36 Y1 第 12 列:合併搬名字分類記錄時被併者依 key 排序,第 85 列的保險只看全部接完之後的最後一筆——一筆被併者自己就不一致(以撤回結尾而名字仍在分割裡,或以指定/確認結尾而不在:手改,或修正輪之前的工具),另一筆的一致記錄排在它後面就蓋住矛盾、放行;把不一致那一筆的 key 改成排在後面,同一份內容被拒。2026-10-09 唯讀量測 live store:名字分類記錄 0 筆、store format 18。) | ✅ **寫(被併者帶來的某個(分割, 名字)在它自己的記錄裡最後一筆就與分類矛盾時,preview 與實跑一律拒絕並說出那筆被併者,不論後面接上的記錄會不會蓋住它;倖存者原本就有的矛盾照舊不擋)** | 第 85 列的保險假定「全部接完之後的最後一筆」說得出合併有沒有帶進矛盾;這一列是那個最後一筆**由誰排在最後決定**,而排序的依據是 key——一個不承載任何意義的字串。store 沒有時間資訊說兩筆被併者的記錄哪一筆較新,依字典序決勝就是讓政策預設代替判定(`disambiguate-before-irreversible-writes`),而合併不可逆。拒絕而不是改成「誰都不蓋住誰」:被併者自己不一致的那一格只有人能判斷哪一筆對,出口在拒絕訊息裡(在那筆被併者上補一筆與分類一致的記錄)。這一格在零實例的形狀上變嚴:b34 的測試曾把「一致的記錄排在後面就放行」寫成期望(`testTheDecisionDoesNotDependOnTheOrderTheDoomedAreGiven` 等三支),同批改寫。**觸發條件可檢查**(腳本見量測文件第 98 列):名字分類記錄開始長時(format 升到 22 之後),回來看有沒有被併者自己不一致的記錄 |
-| 99 | **零實例,而被掃的檔 PR 改得到——判讀的成本本身就是要守的東西**(#711 b37,b36 verify 第 1、2、19 列:`zero-instance-rows-audit` 在一長串空白行與計數選項的回溯上是二次方,verify 量到四萬個空行 41 秒;b37 再找到三條同類的路徑——行中一長串空白、相鄰 inline code 的接續、表的編號逐列掃 Sources。`census-parity.yml` 以 `pull_request` 觸發、跑在 macOS runner,`audit-guards-mutations` 再在副本上跑這支幾十次。2026-10-09 量測:最大的檔 256,003 位元組、最大的 fence 152 行、依閘去重 41 條,離上限都在十倍以上,repo 裡超過上限的 0 個。) | ✅ **寫(判讀改成線性;檔 4 MiB、fence 與段落各 1,500 行與 128 KiB、依閘去重 500 條各一道上限,超過即具名紅、不判讀那一塊;`audit-guards-mutations` 以 10 倍輸入的後設檢查與限時的格子釘住)** | 前面各列的失敗都是**答案錯**——漏報、假訊號、說了一句沒打算說的話;這一列的失敗是**答案對、但要很久才給**,而那個「很久」由別人寫的檔決定。不寫的代價落在 runner 的帳單與一個沒有逾時的 job 上。上限超過時**紅而不截斷**:截斷了再判讀會把截掉的量測當成不存在——第 3 列「未涵蓋不得冒充通過」。線性與上限兩者都要:只有上限,上限之下的二次方仍在(幾千個各 1,500 行的 fence);只有線性,常數乘上一個上百 MB 的檔仍是幾分鐘。**觸發條件可檢查**:守衛的上限訊息數應恆為 0(量測見量測文件第 99 列);接近上限時先問那份檔是不是該拆,再談提高上限 |
-| 100 | **零實例,而前置條件比的是名字、不是那個路徑**(#711 b37,b36 verify 第 0、8、9、15、20 列:R4 的前置條件只認 `--library` 後接一個空白或 `=`、只比變數名,`--library"$V"`、`--library "$V/sub"` 配 `test -f "$V/store.yaml"`、`$1` 都過關;`doctor` 不帶 `--library` 也過關,而第 73 列的散文要它帶;辨識認不出 `"$AKASHIC_BIN"`、`<(…)`、寫檔再讀、`<<<`。2026-10-09 量測:量測文件 40 條合模板的量測全部滿足新條件,用 `doctor`、`enrich`、`add-venue` 的都帶 `--library` 與對應的 `test -f`。) | ✅ **寫(`--library` 的分隔與 shell 相同、`test -f` 逐字是它的引數加 `/store.yaml`、`$` 後面不是名字即紅、`akashic` 的子命令不是 `WriteGateRulings` 的 `.readOnly` 時要 `--library`;變數名以 akashic 開頭的展開、`<(`、`<<<`、寫檔再讀都算 binary 的輸出送進一個命令)** | 第 90 列把前置條件寫進模板;這一列是那個前置條件**比對的對象錯了**:它比變數名,而 shell 執行的是整個引數。辨識那一半同形:R4 的「進任何命令都算」只看管線,而 binary 的輸出還有三條路進一個命令。唯讀子命令的清單**不另列**:讀 `WriteGateRulings` 的 `.readOnly` 格(`no-compat-fallback`:同一件事一份描述),讀不到時每一條 `akashic` 的量測都要 `--library`——偏向紅(使用者 2026-10-05 裁決)。**觸發條件可檢查**:守衛對前置條件的訊息數應恆為 0(量測見量測文件第 100 列) |
-| 101 | **零實例,而背書的範圍寫寬了——小節標題的編號說的是哪一列,不是何時量的**(#711 b37,b36 verify 第 17 列:`measured-numbers-audit` 把「標題到該行之間有 `#NNN`」當成時間錨;量測文件的歷輪補記小節標題都是「### 第 N 列(#NNN)」,那些小節下的數字都被標題自動背書,b35 補的負控又插在沒有編號的標題之下。2026-10-09 量測:標題的 `#NNN` 不算之後,四個來源裡新出現的裸數字 0 個。) | ✅ **寫(標題那一行的 `#NNN` 不算時間錨,標題裡的日期照算;負控插在真的小節標題之下)** | 第 2 列的理由是「前件寫寬了會誤傷」;這一列方向相反——寫寬了會**放行**,而放行時守衛照樣數到那些數字、照樣印「全部都有時間錨」。負控插錯位置是同一件事的另一面:它在一個不具代表性的地方證明守衛會紅,對實際結構下守衛看不到的那一大塊沒有暴露任何東西——第 5 列「守衛對自己的覆蓋率說謊」。**觸發條件可檢查**:守衛報的裸數字應恆為 0(量測見量測文件第 101 列) |
-| 102 | **零實例,而拒絕只認一種重複——判準照著一個標記寫,另外兩種讓計數加倍的狀態不帶那個標記**(#709 b36 verify MEDIUM 4,使用者 2026-10-09 裁決 2。第 91 列的拒絕只問 load 的拷貝標記,而標記要求 entities/ 有同一種、同一個 id 的那份;兩個 legacy 檔共用 id、或 entities/ 與 legacy 同 citekey(同 key)而 id 不同,validate 都報 error、計數同樣加倍,先前乾跑沒有附註,`--apply` 寫入 person 之後才以 index 的 UNIQUE 失敗收場。2026-10-09 唯讀量測 live store:legacy 殘留 `entries/` 0、`people/` 0,entities/ 裡重複的 citekey 0、重複的 person key 0。) | ✅ **寫(`LibraryLoad.planBlockers`:拒絕與附註的條件是「load 標出的拷貝 ∪ 計畫來源種類中無法唯一定位的記錄」——works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`,與 `authorize-names` 一致;乾跑逐筆點名,同一對拷貝的兩份不重複列)** | 第 91 列的理由是揭露與寫入在同一次執行裡;這一列是那道拒絕的**判準照著一個標記寫**:標記是為 index 的「以 entities/ 為準」設計的,只認同一筆記錄的兩份,而讓 bootstrap 計數失準的是「store 說不出哪一份才算」——那正是「無法唯一定位」的定義,各寫入面早已問它。所以不另寫一份重複的判準,問同一個集合(`no-compat-fallback`〈同一件事只能有一份描述〉)。**誠實邊界**:「無法唯一定位」的第 3 類(load 判定檔案寫入時會被拒——legacy 單份不受 git 追蹤、有未提交修改、目的檔被隔離)不讓計數加倍,也照樣擋——那是裁決的字面(與 `authorize-names` 一致),代價是 store 不在 git 裡時,計畫讀到的那一種只要有一份 legacy 殘留,`--apply` 就拒絕,直到它被搬移或刪掉(2026-10-09 真 binary 實測,`BootstrapPlanBlockersCLITests` 釘住);venue/organization 的重複 key(#669)不在條件裡,裁決沒有列它們。**觸發條件可檢查**(腳本見量測文件):重複的 citekey、person key 與 legacy 殘留數應恆為 0;非零時 `akashic validate` 逐筆說出原因,三個 bootstrap 的乾跑點名受影響的那幾筆 |
+本段(裁決表,每一列是情形/裁決/理由的核心)已搬到 [`docs/rulings/zero-instance-guards.md`](../../docs/rulings/zero-instance-guards.md)(不自動載入);需要查過往裁決或新增一列時讀那份檔,各列的量測與歷輪補記仍在量測文件。本檔其他段落說的「下表」「上表」「這張表」指那份檔裡的表。
 
-對本表四類中的任一個做出下一次裁決(新增、不新增、保留、拿掉)= 在這張表加一列。新列的量測寫進量測文件(寫法見 [`measurement-commands-self-prove`](measurement-commands-self-prove.md));既有列日後的補記也寫進量測文件該列的小節,本檔那一列只改核心。
+對本表四類中的任一個做出下一次裁決(新增、不新增、保留、拿掉)= 在這張表加一列。新列的量測寫進量測文件(寫法見 [`measurement-commands-self-prove`](measurement-commands-self-prove.md));既有列日後的補記也寫進量測文件該列的小節,裁決表那一列只改核心。
 
-## 各列共通的東西(觀察,不是判準)
-
-第 1–9 列與第 13、14 列的裁決都是「寫」(第 14 列是 2026-09-09 從「不寫」翻過來的)、第 10–12 列(皆出自 #365)是「不寫」,但**理由各不相同**,這正是不寫總括判準的原因:
-
-- 第 1 列的理由是**失敗的不可見性**(不寫 → 第一次發生時沒跡象)
-- 第 2 列的理由是**前件的精確度**(要不要寫沒有疑問,寬度才是問題)
-- 第 3 列的理由是**沉默的歧義**(守衛本身沒問題,它的「沒說話」有問題)
-
-- 第 4 列的理由是**錯誤的偽裝性**(守衛沒問題、沉默也沒問題,問題是猜錯的結果**看起來
-  是對的**)
-- 第 6 列的理由是**訊號可信度的外溢**——前五列的失敗都關在自己那一格裡,這一列的
-  失敗會讓人開始不相信別格的紅燈
-- 第 5 列的理由是**覆蓋率的自我謊報**——前四列講的都是「被守衛的東西」出了什麼事,
-  這一列講的是**守衛自己**:它多印了一個 ✓,而那個 ✓ 沒有對應到任何新的事實
-- 第 7 列的理由是**兌現範圍的誠實**——它的守衛**通過了也不代表對**,所以它的裁決
-  包含「必須在它旁邊寫出它管不到什麼」
-- 第 9 列的理由是**缺席本身在說話**——前八列問「這個守衛該不該存在」,這一列問「這個
-  欄位不存在時,讀者會推論出什麼」。答案是「機構沒有識別碼可記」,而那是假的
-- 第 8 列的理由是**零的來源在別處**——前七列的「還沒發生」是關於世界;這一列的
-  「走不到」是關於**另一段程式**(load 的 quarantine),而那段程式可以改而不會有人
-  想到這裡。所以它的裁決包含「釘住為什麼是零」,不只是「寫這個守衛」
-- 第 10、11、12 列的理由是三個**不同的**「不寫」——缺用途(形狀由用途決定)、缺理由(既有欄位承載、
-  不成比例)、**位置錯了**(筆名是 Person 側的事實)。三列同出自 #365 而不可互換,正是這張表
-  不寫總括判準的理由再一次實例。區辨全在**觸發後會發生什麼**:第 11 列越過門檻後欄位**進** `Author`、
-  第 10 列用途出現後進 authorship 側(`Author` 上的 `Bool`、或 `Entry` 上的 `Set`——由那個場景決定)、第 12 列**永遠不進** `Author`(落點在 #386 那一族)
-- 第 13 列的理由是**跡象住在錯的地方**——它與第 1 列(缺跡象)、第 8 列(零的來源在別處)最像而都不同:跡象存在(三張 issue 的家族史),零也是真的零(#460 清理過之後的零),問題是每次都靠人記得去看。守衛各列(第 1–8 列)問「守衛該不該存在」,這一列問「已經發生過的形狀,為什麼還是零實例」——答案是場外機制,而場外機制不會自己跑
-- 第 16 列的理由是**裁決依賴守衛**——前十五列的守衛是裁決的結果,這一列的守衛是裁決的**前提**:「暫不改形狀」只有在「漲到門檻時工具會出聲」為真時才站得住。它的零是「還沒漲到」,與第 1 列(還沒發生)、第 13 列(掃乾淨之後)、第 15 列(在這台機器上)都不同
-- 第 15 列的理由是**零是本機的**——前十四列的零不取決於在哪台機器上量,這一列換一台機器就非零;它與第 13 列(時間上的零)成對:一個是「掃乾淨之後」,一個是「在這台機器上」。任何只在本機量的守衛都會對它報綠,所以掃描要住在每台機器都會跑的 `StoreHealth` 裡
-- 第 19 列的理由是**被守的是辯護不是事實**——前十八列的失敗都會讓某個可判定的性質變錯(數字偏了、檔案不在、記錄形狀壞了),這一列的失敗**不讓任何東西變錯**,只讓「每列理由彼此不同」這個說明缺一角。而那個說明正是本檔不寫總括判準的依據,所以它缺一角就等於判準悄悄回來了
-- 第 18 列的理由是**零量的是出口而守衛裝在入口**——前十七列的零都是關於已經在庫裡的東西,這一列的零(庫內最長 4,220 bytes)與它要防的東西(adapter 這一次遞過來的字串)不是同一個母體。與第 16 列(已知會漲的曲線)最像而不同:沒有曲線,實例會由一次外部呼叫一步跨過,而 #516 verify 已經一步跨過 80 倍
-- 第 17 列的理由是**寫入面剛長出來**——與第 13 列(掃乾淨之後)最像而相反:那一列是形狀發生過、被場外機制清掉;這一列是形狀發生過 4 次、從未被記下,因為記錄它的值域在本 change 才存在。零從第 5 次拆分起才可能被打破,而守衛必須在那之前就在
-- 第 22 列的理由是**它不是程式**——前二十一列講守衛與欄位,這一列講 spec 裡的一條 Requirement,而它的失敗是「被當死重刪掉」,不是漏報也不是錯誤的缺席斷言
-- 第 21 列的理由是**既有守衛的謂詞對那一類成員結構上不可能為真**——它不是漏看,是問了一個 glob 成員答不錯的問題(「列了卻不存在」對只會變少的成員永遠為假)。前二十列的零都是關於「還沒有這個守衛」,這一列的守衛在、而且天天跑
-- 第 20 列的理由是**窮舉自己漏了一格**——與第 1 列(缺跡象)、第 13 列(跡象住在錯的地方)都不同:跡象就在同一張 issue 裡、就在眼前,而窮舉它們的人仍然漏了第四個,是**別人**找到的。守衛換掉的不是注意力,是「這件事需要注意力」這個前提
-- 第 23 列的理由是**可達性是本 change 自己造出來的**——第 17 列的零同樣是「寫入面剛長出來」,但那一列的形狀早已在庫外發生過 4 次;這一列的**矛盾狀態**在本 change 之前根本不可達(沒有面能把作者位清空),是新面把它變成可達的。讓它可達的那一步與守衛因此必須同批落地
-- 第 14 列的理由是**判準的輸入未定**——與第 10 列(缺用途)同形而不同:那一列缺的是形狀由什麼決定,這一列形狀確定、缺的是它要比較的「相等」的定義,而定義在 #470 手上。寫了就是替 #470 預先裁決。**2026-09-09 #470 裁決後本列翻成「寫」**,理由隨之變成第二個、也是本表目前唯一的一個:**觸發條件是機械可檢查的,所以它自己到期**——不像第 10、12 列要人指認
-- 第 25 列的理由是**入口有五個**——第 18 列的「零量的是出口而守衛裝在入口」對它也成立,但那一列只有一個入口;這一列三輪 verify 各修一個入口,第四、第五個仍開著。守衛的位置本身是裁決的一半:寫在 store 邊界,不寫在任何一個面
-- 第 27 列的理由是**同一條不變式的另一半要有自己的守衛**——第 26 列的閘與守衛守的是「一條邊」那一半;「一個 literal」那一半的違反由另一條路徑造出、第一半的燈不亮。兩半各自要一盞燈,否則「應恆為 0」只量了一半
-- 第 26 列的理由是**閘與守衛是同一條不變式的兩半**——第 25 列把守衛裝在 store 邊界,因為入口有五個;這一列的三個閘(D25/D27/D28)已把工具面全關,剩下的入口是閘擋不到的手改與舊 binary,守衛守的正是那兩個。與第 23 列成鏡像:那一列是新面讓不可達變可達,這一列是新閘讓可達變不可達(對工具面)
-- 第 29 列的理由是**另一個守衛的正確性依賴它**——第 16 列是裁決依賴守衛(門檻到了才重開),這一列是等價性證明的前提(sink 上限 ≤ ceiling)由它守;零是兩個數字今天恰好對齊,改任一邊都是一行 diff 而每個 sink 自己的測試都不會紅。地板要分族,否則一族的零命中被另一族撐成通過(R31 verify 第 6 列)
-- 第 28 列的理由是**可達性是本 change 的另一條裁決刻意造出來的**——第 23 列是新面讓不可達變可達;這一列是為了不刪判定(D62)而選擇留下一個狀態,守衛是那個選擇的另一半:留著而看不見,等於把刪除延後到合併、把回報搬到另一個命令
-- 第 30 列的理由是**被守的記錄不退役**——第 18 列守的是一次遞進來的字串,這一列守的記錄只增不減,所以上限只擋得住單次呼叫,累積要另一盞燈(venue 側是第 16 列、person 與 organization 側是第 31 列)
-- 第 32 列的理由是**前件寫寬的方向是放行**——第 2 列怕寬了誤傷,這一列寬了會放行一個看起來是決定性證據的假號;修在共用的謂詞,而且換成必定失敗的字元而不是濾掉
-- 第 33 列的理由是**同一個檢查只寫了三分之一**——跡象不是住錯地方(第 13 列),是根本不存在,而同形的檢查就在隔壁;補的是既有守衛的前件,不是新守衛
-- 第 34 列的理由是**補完只補了一側**——第 33 列照 work 的欄位窮舉,指向同一個目標(organization)而住在別的形狀上的兩條邊沒被看到;窮舉的軸要是「指向誰」,不只是「住在誰身上」
-- 第 35 列的理由是**同一個取捨在另一類欄位上開的格**——第 28 列照判定記錄那一格,位元組相等的去重在非判定 reference 上收下的變體沒有燈
-- 第 36 列的理由是**假號的來源是合法的號掛錯地方**——第 4 列是猜錯的結果看起來對、第 32 列是謂詞寬到放行假號;這一列的號本身合法,錯在它屬於哪本刊,只有跨記錄比對看得到,且守衛要與移除面同批,否則 warning 沒有工具面的出口
-- 第 37 列的理由是**舊謂詞對新形狀結構上不可能為真,而正確的謂詞不存在**——第 21 列換一個謂詞就好;這一列換不了,因為「該不該閘」是裁決不是性質,所以守衛只要求每一格都有裁決並寫出自己的理由(第 7 列的形:通過不代表理由對)
-- 第 38 列的理由是**錯在標題不在內容**——第 4 列是猜錯的結果看起來對;這一列聯集裡的每一則都是真的,錯的是「這一筆」這個標題,而讀的人正是為了那個標題才指名。守的是讀取面的定址,不是 store 的內容
-- 第 40 列的理由是**拒絕問在錯的時間點**——第 21 列是謂詞問了那一類成員答不錯的問題、第 13 列是跡象住在錯的地方;這一列的謂詞答得對、跡象也在,錯在答案到達時寫入已經進行到一半。修法是同一組判斷換一個時間點再問一次,不是寫第二份判斷
-- 第 41 列的理由是**同一個值在兩層有相反的身分**——第 32 列放行的是形狀看起來對的假號;這一列的值形狀本來就對,錯在它不指認任何東西。閘在共用謂詞,而且要把謂詞拆成位址與引用兩個——否則為了守零實例而加的收緊會把 live store 的 `store-source` 關掉
-- 第 42 列的理由是**沒有檢查的那一格讓讀取端崩潰**——第 33 列的缺口是少報,這一列的缺口是 trap;重複 key 的檢查補在 `crossRecordIssues`,查找表改成不 trap,以 key 定位寫入的面拒絕
-- 第 43 列的理由是**合併是拿掉沿革的那一步**——第 22 列替沿革留位置,這一列守位置的另一端:落進去的東西不得在合併時安靜消失,而哪一段的時間對是判斷,合併不猜
-- 第 44 列的理由是**既有判準的答案變了**——閘不是新的裁決,是遷移退場之後「有沒有 pre-bump 遷移」從有變成沒有
-- 第 45 列的理由是**上限只擋單次**——同第 30 列,錨點取既有常數;累積由第 16 列出聲
-- 第 46 列的理由是**路由字典安靜地選一筆**——隔壁的重複有檢查、宣稱者的重複沒有,匯入時把欄位寫進別的記錄
-- 第 47 列的理由是**零來自規則還不存在**——第 13 列的零是清理過之後的零;這一列的形狀發生過一次(52 筆),今天的零是因為 live store 的 library 都還沒標性質。燈照的是寫入閘擋不到的三個來源(舊 binary、手改、set-kind 當下就不符的既有成員),同第 26 列閘與守衛的分工
-- 第 48 列的理由是**新開的通道讓偽裝可達**——同第 4 列,角色掉了之後記錄看起來健康;可達性是 #587 的寫入面造成的(第 23 列的形),所以閘與新面同批,而 reference 那一半改成搬而不是拒絕
-- 第 49 列的理由是**合法狀態的可見性**——前面各列的零是壞形狀還沒發生;這一列的形狀是合法的,缺的是能看見它的判準,而判準只有一份
-- 第 50 列的理由是**「在不在」不等於「是不是一份檔」**——同第 1 列不寫就沒有跡象;與第 15 列相鄰:那一列問位元組在不在這台機器上,這一列問那個位置上是不是一份檔
-- 第 51 列的理由是**移植會換掉意外的紅燈**——同第 3 列未涵蓋不得冒充通過;舊實作的紅是 glob 的副作用,換語言時要寫成顯式判定
-- 第 52 列的理由是**進度量測不得往終局偏**——普查的數字就是 literal 歸零的進度,少算一個檔等於假進步,所以整次拒絕輸出
-- 第 53 列的理由是**同一個依據兩個時間點給相反的答案**——設定時擋、使用時放,是第 26 列那條不變式只剩一半;排除清單懸空則讓同一份程式對「是不是參照」答兩種
-- 第 54 列的理由是**守衛的出路要走得通**——一律拒絕而出路是死路的守衛等於沒有出路;改名本來就該遷移指向舊 key 的邊,三個前置拒絕擋住遷移本身的安靜錯誤
-- 第 55 列的理由是**判準已經有了,只是沒套到這種檔**——同第 44 列既有判準的答案變了;registry 檔不在 `entities/`,移除面一族的可回溯要求原本照不到它
-- 第 56 列的理由是**守著它的閘只擋一條路**——同第 4 列,對不回的來源讓再匯入安靜地造出攣生;合併閘管不到手改與舊檔
-- 第 57 列的理由是**讀回後核對身分**——同第 4 列,別筆的回應看起來是對的;那個形狀在頁內 fetch 已經發生過,這裡把紀律落實成程式
-- 第 58 列的理由是**半套的數字不得冒充全部**——同第 3 列,缺記錄時預設不出數字
-- 第 59 列的理由是**形狀取決於還沒出現的用途**——同第 10 列,ordinal 與「全收成一筆」語意不同,先具名拒絕是可逆的
-- 第 60 列的理由是**上限沒有量測依據、也不是位元組界線**——同第 18 列的出口與入口,差別在錨點:25 是選的數字;首版拿各字串的字元上限相加當位元組的算術並不成立(b13f R1 verify)
-- 第 61 列的理由是**理由只有一份,所以只截第三方內容**——同第 60 列上限是選的數字;差別在逐筆帶理由,截整筆就等於丟理由
-- 第 62 列的理由是**失敗看起來像成功**——第 1 列的不可見性再加一層:命令回報成功,被刪的是使用者原本就有的資料
-- 第 63 列的理由是**答不出來被讀成否**——第 3 列同形,而閘是隱私閘,外流進 remote 不可逆
-- 第 64 列的理由是**解析成功而值不是來源的意思**——第 4 列的偽裝性落在解析層,兩個實作對同一份輸入安靜地分岔
-- 第 65 列的理由是**第五份推導不會有跡象**——第 13 列同形,四份併成一份之後由源碼掃描守單一路徑
-- 第 66 列的理由是**秘密一進 git 就收不回來**——第 18 列的入口守衛,對象是 url 裡的帳密
-- 第 67 列的理由是**沒有讀者就沒有漂移的受害者**——第 10 列同形的「不做」,觸發條件是第一個讀者
-- 第 68 列的理由是**清單由一次匯入一步跨過**——同第 18、30 列,上限保護的是 LLM context
-- 第 69 列的理由是**第一個寫得進沿革位置的面**——同第 45 列的單次上限;第 22 列保留位置、第 43 列守合併,這一列守寫入
-- 第 70 列的理由是**清空它的是一次計費裁決**——第 13 列同形的「清理過之後的零」;機制留著是因為成本決定可以翻,而它留著的那條路要自己有負控(清單因此是資料檔);#711 起清單還記缺口多大,因為「接受」是對一個具體大小說的
-- 第 71 列的理由是**偵測不得與它要補的抽取共用失敗條件**——第 21 列換一個謂詞就好;這一列補的是另一支守衛的輸入,補丁若與原抽取共用 regex,漏掉的寫法兩邊一起漏,零實例量到的只是「看不到」
-- 第 72 列的理由是**上限是一次裁決給的數字**——第 18 列的上限夾在兩個量測錨點之間,這一列只有一個錨點(最大單檔的 48.8 倍);批次入口逐檔略過而不整批拒絕,因為那個入口的其他「這個檔不能收」本來就逐檔具名
-- 第 73 列的理由是**殘留與進行中長得一樣**——第 15 列的零是本機的、第 23 列的可達路徑是本 change 自己開的;這一列的東西是工具自己的失敗路徑必然留下的,而它與正在進行的存檔在磁碟上分不開,所以守衛只報不刪
-- 第 74 列的理由是**沒有單一出口**——第 65 列同形,由源碼掃描守;CLI/MCP 的範圍開在進入點與分派層,新命令不必記得,App 沒有那一層,每個寫入點都得自己開,而忘了開的後果只在 legacy 拷貝刪不掉時才看得到
-- 第 75 列的理由是**少算正是上限要擋的方向**——第 52 列同形(略過讀不懂的等於少算),落在一個保護使用者機構 session 的上限上,所以讀不懂就拒絕、不略過
-- 第 76 列的理由是**驗的與存的必須是同一份位元組**——第 62 列守輸出,這一列守輸入:別人給的路徑可能是 symlink,也可能在兩次讀之間被換掉
-- 第 77 列的理由是**閘與守衛同批長出、共用一個分類**——第 26 列的閘與守衛各自成形;這一列兩者讀同一個 `sourceOccupant`,因為 R2 之前四個讀者各自判,對同一個佔用給出三種答案。與第 73 列成對:那一列報自己的失敗留下的,這一列報別人留在位址上的
-- 第 78 列的理由是**讓狀態可達的面與拒絕同批落地,而拒絕的位置早被預告**——第 23 列的矛盾狀態由新面造出、第 26 列的閘與守衛是同一條不變式的兩半;這一列兩者兼有:名字分類面讓「人判定過的對外形」寫得進 store,合併拒絕讓它不被安靜降級,寫入閘讓舊 binary 不被餵讀不懂的記錄。與第 8 列成鏡像:那一列的零由別處的程式造成、可能被改掉而沒人知道;這一列的零由 store marker 還在 18 造成,marker 一升就變成有實例,所以拒絕的測試與「機械值仍是提醒」的釘零要在 marker 升級之前就存在
-- 第 79 列的理由是**降級把拒絕從共用段挪到只有一邊走得到的地方**——第 40 列的拒絕問在錯的時間點、第 8 列的零由別處的程式造成;這一列兩者都不是,是降級讓共用的閘變窄、順帶被擋住的格子各自找新家,而只裝在實跑那一邊的讓 dry-run 說謊(#139 F1 的同一個形狀)
-- 第 80 列的理由是**上限是一個沒人量過的裁量值,而且它讓一條使用者裁決在尾端不成立**——第 72 列的上限至少有一個使用者給的數字、第 18 列夾在兩個量測錨點之間;這一列兩者都沒有,所以誠實寫成「Claude 裁量、使用者尚未確認」,並把它的代價(沒記下來的列讓 CLI 非零)與可檢查的重開觸發寫在同一列
-- 第 81 列的理由是**先前靜默的覆寫變成具名的拒絕,而零是因為資料少、不是因為形狀不可達**——第 23 列的矛盾狀態由新面造出,這一列相反:它擋的是舊行為(互相覆寫);零實例來自 live store 只有一筆歧異記錄,rename 之後的過期 id 是真實路徑。第一版借用泛用的 #631 拒絕、把原因說成形狀、出路指向循環——訊息與守衛同批要對,否則守衛擋對了、使用者卻被引到錯的處置
-- 第 82 列的理由是**被守的記錄只追加、移除面不刪**——第 30/45 列的上限只擋單次、累積由預算出聲;這一列的記錄是判定史,唯一的刪除路要先撤回,所以入口先擋;理由形狀的那一半是文法不是上限(開頭的組合字元讓記錄讀不出來)
-- 第 83 列的理由是**零是磁碟區種類的零,且不補替代路**——第 15 列的零是本機的(資料換一台機器就缺席),這一列是同一份程式換一種檔案系統就變;與第 13 列對照:那一列清理過之後的零靠人記得,這一列靠一道在建立任何檔案之前問磁碟區的閘。替代路被實測否掉過一次(inode 漂移讓清理全部失效),所以裁決是拒絕而不是再補一條
-- 第 84 列的理由是**收回一則放寬裁決被實作放寬過頭的部分**——第 75、76 列守的是一個上限與一條路徑;這一列守的是裁決的邊界:裁決 1 把別的主機從整批暫停改成交給人,實作把交給人給了裁決沒有點名的頁面(沒載完的、登入頁、Safari 的錯誤頁),而交給人時批次繼續。判準是裁決的字面,不是「看起來沒事」
-- 第 85 列的理由是**被守的是一道規則與它造出的記錄之間的一致**——第 78 列守合併不替人改判定,這一列守合併接上去的記錄本身說的話;尾端矛盾時,按位置讀記錄的刪名字閘與去重會得出與分類相反的結論。主修在搬法,保險只剩來源本身就不一致的那一格
-- 第 86 列的理由是**三種實體裡只有它刪得到沒有名字**——第 25 列因為入口有五個而把不變式放在 store 邊界;這一列的形只有一個入口造得出,所以擋在入口、與兩個鄰居同一句話,不升成 validate 的 error(讀得進來、不毀資料)
-- 第 87 列的理由是**「讀不懂」被折成「空的」,而接在後面的是整份替換**——第 3 列的折疊在輸出(沒被檢查與乾淨長得一樣),這一列在輸入(讀不懂與空的長得一樣);後果不是少報,是一次附加變成一次刪除。所以裁決不是加一道檢查,是讓「讀不懂」有自己的狀態:~~寫入類命令拒絕~~ `file add` 拒絕、診斷面與兩個匯入改報(匯入自使用者 2026-10-05 的裁決起)、能附加時只附加位元組
-- 第 88 列的理由是**辨識是守衛沒寫出來的前提**——第 21 列換一個謂詞就好,第 37 列連正確的謂詞都不存在;這一列的謂詞(模板)是對的,錯在它只對認得出的單位求值。補寬辨識之外另加一個不靠辨識的地板,因為認不出的寫法仍在,而地板是承認這件事的那一半;R4 再承認一次:辨識寫成列舉會在下一輪漏,所以反過來寫(認不出時偏向是量測),地板依閘去重,退場標記拿掉
-- 第 89 列的理由是**防線住在抄本裡**——第 84 列守的是一則裁決的邊界,這一列守的閘本身不新奇(形狀、上限、清理順序),新的是它們先前的位置:文件裡要模型照抄執行的程式,通過時證明的是文件、不是實際跑的那一份;剔除集合也是另寫的一份。裁決因此含「搬進 CLI」這一半,不只是「寫」
-- 第 90 列的理由是**擋它的寫法看起來已經擋住了**——第 13 列的「沒被檢查與乾淨分不開」換一個成因(binary 根本沒跑),而 `${V:?…}` 的字面意思就是「沒設就停」,文字照著寫、三輪 verify 才量出它只停管線的一段;所以把前置條件寫進模板、在任何單位裡判管線中的 `${V:?…}`,不只改散文
-- 第 91 列的理由是**揭露與寫入在同一次執行裡**——第 3 列的守衛沒說話;這一列的守衛說了話卻沒有擋,而 `--apply` 讓揭露與動作之間沒有人。附註留給乾跑,寫入之前擋
-- 第 92 列的理由是**移植改了刪除的語意**——第 89 列把閘從抄本搬進 CLI,這一列是搬的那一步自己把「刪不掉目錄」換成「遞迴刪除」,在一般檔上兩者長得一樣,所以看不出來;裁決回到原本的語意,並把「先驗後刪」寫成順序
-- 第 93 列的理由是**同一個判斷有第二份,而且排在前面**——謂詞本身沒錯,錯在散文裡另有一份比較寬、由模型執行、在請求送出之前的版本;裁決是換成呼叫同一個函式,不是把正則修到一樣
-- 第 94 列的理由是**看與寫之間的比對被寫成保證**——第 87 列管「讀不懂」,這一列管同一段程式在並行時的窗口:比對只縮小它,鎖加鎖內重看才關得掉;`EEXIST` 的意思通常是「別人剛做完」,所以重看而不是拒絕
-- 第 95 列的理由是**理由有沒有地方落**——第 82 列守入口的上限與理由形狀;這一列守的是收了理由之後它寫不寫得進記錄:一條不寫記錄的路照樣收理由,必填就成了字面,而寫入閘因為沒有記錄要寫也看不到它
-- 第 96 列的理由是**送出去的量由 store 內容決定**——第 30/45 列擋送進來的量;這一列擋回應的體積,呼叫端收到之後才付的代價丟不掉
-- 第 31 列的理由是**同一條曲線換了持有者**——第 16 列的燈只照 venue;新面(未決腿)讓 person 與 organization 也開始累積,第 30 列寫下的誠實邊界要有工具面兌現,否則就是一句沒有後續的散文
-- 第 39 列的理由是**單步的跳躍預警看不到**——第 16/31 列的燈在讀取面、照的是漸進的增長;一次寫入從門檻之下直接越過讀取上限時,燈來不及響,擋它的只能是寫入端。而那道閘要擋的不只是位元組:多檔寫入面在它觸發之前已有檔落盤,所以閘與零寫入的 preflight 同批
-- 第 24 列的理由是**半吊子已經誠實**——前二十三列裡只有第 22 列同樣是「不動既有的東西」(比的是裁決的**動作**:那一列的對象是 spec 文字、失敗是被當死重刪掉;本列的對象是程式的半吊子管線、失敗是使用者撞牆或被當成待修殘留而被人動手)。留著的代價不是零(記了就刪不掉——#586 在 2026-09-28 補了移除面,代價從「刪不掉」降為「要人判定放棄」),而是今天未兌現、且一出現就會出聲;動它的兩個方向(實作/拿掉)代價都更高。與第 10 列(缺用途)最像的是理由的**形**:實作那一半同樣是「形狀取決於還不存在的用途」;但第 10 列的對象根本不存在,這一列的對象已經在、且已經誠實
-- 第 97 列的理由是**一次搬家讓只該有一份的東西有了第二個家**——第 13、88 列假定模板與地板各只有一處;拆成兩份檔之後,規則裡的模板與量測文件裡的棘輪標記都可能與另一處分岔,而分岔的兩邊各自看起來都對
-- 第 98 列的理由是**結論取決於一個不承載意義的東西**——第 85 列的保險看全部接完之後的最後一筆,而接上去的順序依 key 排序:被併者的 key 拼法決定了誰的記錄蓋住誰的矛盾;store 沒有時間資訊說哪一筆較新,依字典序決勝是讓政策預設代替判定,所以被併者自己不一致就拒絕
-- 第 99 列的理由是**答案對、但要很久才給**——前面各列的失敗都是答案錯;這一列的成本由別人寫的檔決定,所以判讀要線性、三道上限讓常數也有界,超過即紅而不截斷(第 3 列)
-- 第 100 列的理由是**前置條件比對的對象錯了**——第 90 列把前置條件寫進模板,這一列是它比變數名、而 shell 執行的是整個引數;辨識那一半同形:binary 的輸出除了管線還有三條路進一個命令
-- 第 101 列的理由是**背書的範圍寫寬了**——第 2 列怕寬了誤傷,這一列寬了會放行,而且放行時守衛照樣數到那些數字;負控插在不具代表性的位置,是同一件事的另一面
-- 第 102 列的理由是**判準照著一個標記寫**——第 91 列的拒絕只認 load 的拷貝標記,而標記只認同一筆記錄的兩份;另外兩種讓計數加倍的重複不帶它。改問各寫入面早已共用的「無法唯一定位」,不另寫一份
-
-**「完備」在第 1–7 列裡有三種強度,不是兩種**(#414 R1 自審更正——這一段原本寫
-「前六列在前件內都完備」,而那對第 5、6 兩列為假):
-
-| 強度 | 哪幾列 | 通過代表什麼 |
-|---|---|---|
-| 檢查**就是**那個性質 | 1–4 | 前件成立即違規,通過即乾淨 |
-| 檢查是那個性質,但由**有限抽樣**建立 | 5、6 | 強證據不是證明——第 6 列跑兩次逐字相同,證不到「輸出是決定性的」這個全稱命題;第 5 列找不到逐字重複,也不排除「兩個 case 印不同的字卻查同一件事」 |
-| 檢查是一個**嚴格更弱的命題** | 7 | 通過只排除一個已知會錯的形狀(帶 `editor`),對真正的問題(「這個型別該不該在表裡」)**不提供任何證據** |
-
-第 7 列與第 5、6 列的差別因此是**種類**而非程度:後者的缺口是知識論的(樣本數),
-前者的缺口是結構的(問了另一個問題)。
-
-一句「成本低就寫」會把這些理由壓成同一件事,然後在下一個還沒出現過的情形上給出沒人同意的答案。
-
-(這句與下方同型的那句**刻意不寫序數**。它們寫過序數,而序數在本檔過期了兩次:「第十個情形」在第 10、11 列進表(2026-08-28)後一直留到 2026-09-02 的 diff 仍未改;「第 12 個」被改成「第 13 個」的同一天,#464 已排定在表尾加第 13 列——把正在修的缺陷複製到下一列。表頭的列數有 `measured-numbers-audit` 盯著,散文裡的「第 N 個」沒有守衛認得,所以不寫。#365 verify DA,2026-09-03。)
-
-**第 4 列與第 1 列的差別值得看一眼**(兩者最像):第 1 列不寫的話,那個形狀第一次出現時
-**不會有任何跡象**;第 4 列不寫的話,會有跡象——一筆完全正常的 venue 記錄——只是那個跡象
-指向錯的方向。前者是缺訊號,後者是**假訊號**。
-
-~~**還沒出現過的情形**:零實例且**成本高**的守衛。真出現時要加一列,而那一列的理由
-**很可能是「不寫」**~~ → **2026-08-28 出現了,而且一次三個**(第 10、11、12 列,皆出自 #365;第 12 列於 2026-09-02 補進表——2026-08-28 的裁決 comment 三項都裁了,只有前兩列當時被寫進來)。
-
-**那個預測對了三分之一**:三列的裁決確實都是「不寫」,但它預測的理由(成本高)**只涵蓋第 11
-列**。第 10 列的理由是**形狀取決於一個還不存在的用途**——成本高只是背景,真正擋住它的是
-「現在選一個形狀等於用猜的固定一個介面」;第 12 列的理由是**位置錯了**——成本根本不是問題,
-問題是它不該進這個型別。
-
-**三列的理由不可互換,這是本表存在的理由的再一次實例**:一句「零實例且成本高就不寫」會把
-它們壓成同一件事,然後在下一個還沒出現過的情形上給出沒人同意的答案(序數刻意不寫,理由見上)。
-
-**現在還沒出現過的**:零實例、成本**低**、而裁決是「不寫」的情形。**「成本與裁決兩軸完全相關」這句話
-曾寫在這裡,被本表自己否掉了**(#365 verify DA,2026-09-03):第 3、7 列根本沒有陳述成本,第 12 列的成本
-在上一段被明文否認為理由——12 個資料點裡 2 個沒有成本值、1 個的成本值不相干,剩下的「相關」是讀情形欄
-的樣板前綴得來的,而拿樣板前綴當性質正是本檔禁止的「依性質相似類推」。誠實的版本是:**陳述了成本的
-那幾列**裡,低成本的都「寫」、高成本的都「不寫」。結論不依賴那個統計,且在相關被削弱後更強——連相關都
-不完整,就更不該拿它當判準。真出現時要加一列,不要從「成本低就寫」推導。
-
-→ **2026-09-03 出現了**:第 14 列成本低、裁決「不寫」——理由是判準的輸入(相等定義)取決於 #470,與成本無關。預測對的是「會出現」;它的理由又是一個新的,這正是不寫總括判準的理由再一次實例。
-
-→ **而它在 2026-09-09 又消失了**:#470 一 close,第 14 列依自己寫下的觸發條件翻成「寫」。所以「零實例、成本低、裁決不寫」這一格**現在又是空的**——那不是預測失效,是那一列的觸發條件真的到期了。留著這兩句是因為只寫最新狀態會讓下一個人以為這格從未有過成員。
+「各列共通的東西」(每一列的理由彼此不同的說明,本檔不寫總括判準的依據)也搬到同一份檔,接在裁決表後面。
 
 ## 為什麼是「一列一列」而不是判準
 
@@ -335,7 +76,7 @@
 
 本檔就是那條規則的直接應用:**表是規格,理由欄是它的辯護,兩者在同一列所以不會分岔。**
 
-**失敗史仍寫在文件裡,只是分成兩份**(#711):理由的主幹與裁決同列、留在本檔;逐輪的補記與量測在量測文件該列的小節,本檔每一列的尾端指向它。拆開的理由是本檔每個 session 自動載入——補記不是被總括掉,是換了一個不必每次讀的地方。
+**失敗史仍寫在文件裡,只是分成兩份**(#711):理由的主幹與裁決同列、留在裁決表(2026-10-15 起住在 [`docs/rulings/zero-instance-guards.md`](../../docs/rulings/zero-instance-guards.md));逐輪的補記與量測在量測文件該列的小節,裁決表每一列的尾端指向它。拆開的理由是本檔每個 session 自動載入——補記不是被總括掉,是換了一個不必每次讀的地方。
 
 ## 跟其他規則的關係
 
diff --git a/.githooks/protected-ratchet.txt b/.githooks/protected-ratchet.txt
index 231129db..b6bc1133 100644
--- a/.githooks/protected-ratchet.txt
+++ b/.githooks/protected-ratchet.txt
@@ -56,6 +56,9 @@ Sources/akashic-mcp/Server.swift
 Sources/akashic/CLI.swift
 Sources/akashic/CreateEntryCommand.swift
 Sources/akashic/WriteGateRulings.swift
+docs/rulings/mcp-cli-parity.md
+docs/rulings/two-kinds-of-edits.md
+docs/rulings/zero-instance-guards.md
 docs/zero-instance-measurements.md
 mcpb/manifest.json
 plugin/.claude-plugin/plugin.json
diff --git a/.github/workflows/census-parity.yml b/.github/workflows/census-parity.yml
index e9d30747..2ed6f11a 100644
--- a/.github/workflows/census-parity.yml
+++ b/.github/workflows/census-parity.yml
@@ -88,6 +88,9 @@ on:
       - ".claude/rules/**"
       # 零實例裁決表的量測與歷輪補記(#711):zero-instance-rows-audit 與 measured-numbers-audit 讀它。
       - "docs/zero-instance-measurements.md"
+      # 三份規則的裁決表(2026-10-15 從 .claude/rules 搬出、不自動載入):zero-instance-rows-audit、
+      # parity-table-drift、measured-numbers-audit 讀它們,trigger-coverage 以檔名把它們算進讀 .claude/rules 同名檔的守衛。
+      - "docs/rulings/**"
       # parity-table-drift 拿這兩支當真值來源(#407 R50)。
       - "Sources/akashic-mcp/Server.swift"
       - "Sources/akashic/*.swift"
diff --git a/CLAUDE.md b/CLAUDE.md
index 38d0a21f..fe27f901 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -104,7 +104,7 @@ Foresay 管**人機確認的格式與迴圈**,本 repo 的規則管**哪些寫
 | [no-compat-fallback.md](.claude/rules/no-compat-fallback.md) | 不留相容 fallback——要改格式就一次改完全部;例外須離開 default 位置、附退場量測、退場即刪 |
 | [replace-endnote-and-zotero.md](.claude/rules/replace-endnote-and-zotero.md) | 目標是完全取代 EndNote 與 Zotero——檔案住 Akashic、位元組複製進 store、對 Zotero 的 pull 是過渡、能力缺口記 issue 不得靠「回去用 Zotero」帶過 |
 | [apa7-is-the-work-floor.md](.claude/rules/apa7-is-the-work-floor.md) | 一筆 work 的資訊下限是「能產出正確的 APA7 參考文獻」——ch10 的 113 例是驗收矩陣、`Entry.type` 的值域須**細分**(非等於)ch10 的 16 節;下限不是上限,分類可更細不可更粗 |
-| [zero-instance-guards.md](.claude/rules/zero-instance-guards.md) | 為「還沒發生過的形狀」寫守衛是一列一列裁決出來的——封閉決策表+理由欄同列,刻意不給總括判準(那會在邊界上長出沒人同意的答案);各列的量測腳本與逐輪補記住在不自動載入的 [`docs/zero-instance-measurements.md`](docs/zero-instance-measurements.md),規則檔每一列只留核心(#711)|
+| [zero-instance-guards.md](.claude/rules/zero-instance-guards.md) | 為「還沒發生過的形狀」寫守衛是一列一列裁決出來的——封閉決策表+理由欄同列,刻意不給總括判準(那會在邊界上長出沒人同意的答案);各列的量測腳本與逐輪補記住在不自動載入的 [`docs/zero-instance-measurements.md`](docs/zero-instance-measurements.md);裁決表(含每一列的核心)住在同樣不自動載入的 [`docs/rulings/zero-instance-guards.md`](docs/rulings/zero-instance-guards.md)(#711;2026-10-15 搬出)|
 | [measurement-commands-self-prove.md](.claude/rules/measurement-commands-self-prove.md) | **數 binary 輸出的量測只有一種寫法**——自證閘(舊 binary 印 `0` 與「檢查過且乾淨」要分得開)、`LC_ALL=C`、必要變數的前置條件寫在整條最前面;讀 store 用 YAML 解析、讀不到的檔要計數、數字旁寫日期與條件;`zero-instance-rows-audit` 掃零實例的規則檔與量測文件(#711 從量測段抽出)|
 | [blocked-issues-must-be-scannable.md](.claude/rules/blocked-issues-must-be-scannable.md) | 被阻塞的 issue 必須把「在等什麼」寫在工具掃得到的三個位置之一,不得只寫在散文裡——四次「空等」的實測(#314)+哪些「等」需要標記的封閉裁決表 |
 | [two-kinds-of-edits.md](.claude/rules/two-kinds-of-edits.md) | 每個寫入面只能是兩種編輯之一——AI 編輯(判定型、依規則,必留 verdict 且可逆轉)或程式編輯(決定論式,冪等、整批擋、附量測);混合面拆成「提名(程式)→ 判定(AI)→ 落地(程式)」;封閉的歸類表逐列理由(#505) |
diff --git a/Sources/AkashicAppKit/AdjudicationViews.swift b/Sources/AkashicAppKit/AdjudicationViews.swift
index 6df6e6fa..77045f99 100644
--- a/Sources/AkashicAppKit/AdjudicationViews.swift
+++ b/Sources/AkashicAppKit/AdjudicationViews.swift
@@ -132,6 +132,14 @@ struct OrphanView: View {
     /// 理由草稿:失敗之後重開同一筆還在(不必重打),空理由時破壞性按鈕停用。
     @State private var removalDraft = RemovalReasonDraft()
 
+    /// 標題的串接拆到 body 之外、三元先定型成 String:寫在 `.navigationTitle(…)` 的引數裡時,CI 對整個 body 報
+    /// type-check 逾時(unable to type-check this expression in reasonable time)。組出來的字串逐字不變
+    private var navigationTitleText: String {
+        let additional: String = (model?.orphanedAdditionalSourceEntries.count ?? 0) > 0
+            ? "+\(model!.orphanedAdditionalSourceEntries.count) 附加來源" : ""
+        return "Orphans(\(model?.orphans.count ?? 0)" + additional + ")"
+    }
+
     var body: some View {
         Group {
             if let model {
@@ -187,9 +195,7 @@ struct OrphanView: View {
                 }
             }
         }
-        .navigationTitle("Orphans(\(model?.orphans.count ?? 0)"
-                         + ((model?.orphanedAdditionalSourceEntries.count ?? 0) > 0
-                            ? "+\(model!.orphanedAdditionalSourceEntries.count) 附加來源" : "") + ")")
+        .navigationTitle(navigationTitleText)
         .task(id: state.reloadCount) { model = OrphanModel(state: state) }
         .confirmationDialog("刪除這筆 entry?檔案會移到垃圾桶(可救回)。",
                             isPresented: Binding(
@@ -225,9 +231,11 @@ struct OrphanView: View {
         } message: { presented in
             switch presented {
             case .confirmRemoval(let pending):
-                Text("將拿掉這幾個在 Zotero 端已刪除的來源:\(pending.sourcesLabel)。"   // display-safe-exempt: pending.sourcesLabel 是 `displayOrphanedAdditionalSources` 的消毒投影
-                     + "主來源與活著的來源不動,書目欄位不動。"
-                     + "記錄檔要先 commit——移除前的版本只留在 git 裡。理由不寫進 store,請寫進 commit message。")
+                // `verbatim:` 讓串接直接定型成 String(同上:body 的 type-check 逾時);`+` 的結果本來就是 String、
+                // 走的就是不翻譯的那個 init,顯示逐字不變
+                Text(verbatim: "將拿掉這幾個在 Zotero 端已刪除的來源:\(pending.sourcesLabel)。"   // display-safe-exempt: pending.sourcesLabel 是 `displayOrphanedAdditionalSources` 的消毒投影
+                               + "主來源與活著的來源不動,書目欄位不動。"
+                               + "記錄檔要先 commit——移除前的版本只留在 git 裡。理由不寫進 store,請寫進 commit message。")
             case .removed(let report):
                 Text(report)   // display-safe-exempt: 報告在 kit 層逐項消毒
             case .failed(let message):
diff --git a/Sources/AkashicStoreIO/DivergenceResolve.swift b/Sources/AkashicStoreIO/DivergenceResolve.swift
index 64542a43..a9cd1896 100644
--- a/Sources/AkashicStoreIO/DivergenceResolve.swift
+++ b/Sources/AkashicStoreIO/DivergenceResolve.swift
@@ -146,25 +146,33 @@ public enum DivergenceResolveError: Error, LocalizedError, SanitizedErrorDescrip
                  + "或在倖存者上 update-venue \(displaySafeInvisible(survivor, max: 200)) --authorize <名字> --judgement <理由> 指定它"
                  + "(同書寫系統原本的指定會被換下、寫一筆撤回),再合併"
         case let .wouldContradictClassificationTail(survivor, details, exits):
+            // 含 closure 與三元的運算元先拆成具型別的 let:寫在同一串 `+` 裡時 CI 的 type-check 逾時(unable to type-check this
+            // expression in reasonable time)。輸出逐字不變
+            let detailLines: String = details.prefix(5).map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: details 擲出端逐項 displaySafeInvisible
+            let detailMore: String = details.count > 5 ? "\n  …共 \(details.count) 個名字" : ""   // display-safe-exempt: Int
+            let exitLines: String = exits.map { "  – " + $0 }.joined(separator: "\n")   // display-safe-exempt: exits 是 tailConflictExits 的字面常量句子,不含 store 內容
             return "拒絕合併:併入「\(displaySafeInvisible(survivor, max: 200))」之後,名字分類記錄的最後一筆會與那個名字的分類矛盾"
                  + "(被併者自己的記錄就與它的分類不一致——手改,或 #564 修正輪之前的 binary 寫的;或這次合併改了倖存者某個名字的分類):"
                  + "比的是原始倖存者,倖存者合併前就有的矛盾不擋\n"
-                 + details.prefix(5).map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: details 擲出端逐項 displaySafeInvisible
-                 + (details.count > 5 ? "\n  …共 \(details.count) 個名字" : "")   // display-safe-exempt: Int
+                 + detailLines
+                 + detailMore
                  + "\n合併不替人改判定。出口(在帶來它的被併者上做;「倖存者自己的」那一格在把它標成 variant 的被併者上做——補一筆與分類一致的記錄,"
                  + "接過去就成為最後一筆),每一種情形各一句:\n"
-                 + exits.map { "  – " + $0 }.joined(separator: "\n")   // display-safe-exempt: exits 是 tailConflictExits 的字面常量句子,不含 store 內容
+                 + exitLines
                  + "\n讓它的最後一筆與分類一致,再合併"
         case let .doomedRecordInvalid(merged, why):
             return "拒絕合併:被併的「\(displaySafeInvisible(merged, max: 200))」自己違反 venue 的寫入期不變式——\(why)"   // display-safe-exempt: why 由 NameIdentity 的固定訊息與已 displaySafe 的名字組成
                  + "。先修它的 YAML(docs/store-format.md §5.7)再合併;不猜、不靜默修"
         case let .namedByLibraryRule(merged, shape, libraries, steps):
+            // 同上:兩個 closure 先拆成具型別的 let(type-check 逾時);輸出逐字不變
+            let libraryList: String = libraries.map { displaySafeInvisible($0, max: 200) }.joined(separator: "、")
+            let stepLines: String = steps.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: steps 由 libraryRuleMergeSteps 組裝、逐項 displaySafeInvisible
             return "拒絕合併:被併的\(shape)「\(displaySafeInvisible(merged, max: 200))」被 library「"   // display-safe-exempt: shape 是呼叫端字面量(work/venue)
-                 + libraries.map { displaySafeInvisible($0, max: 200) }.joined(separator: "、")
+                 + libraryList
                  + "」的成員規則指涉,合併不遷移規則、被併鍵消失之後規則會安靜懸空。"
                  + "出路(不經過 topic:規則全程都在,只是換了指涉的對象;set-kind 替換既有規則要求 registry 檔已 commit)——"
                  + "每個 library 各做一次,做完重跑這次消歧:\n"
-                 + steps.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: steps 由 libraryRuleMergeSteps 組裝、逐項 displaySafeInvisible
+                 + stepLines
                  + "\n(要先讀出完整規則:akashic library check ;#642)"
         case let .wouldLoseFields(merged, survivor, losses):
             return "拒絕合併:被併的「\(displaySafeInvisible(merged, max: 200))」帶有倖存者"
@@ -185,20 +193,27 @@ public enum DivergenceResolveError: Error, LocalizedError, SanitizedErrorDescrip
             // 刪第一筆重跑仍被拒):帶進來的才是要處理的;倖存配對既有的列出供對照,明說不擋。**「帶進來的」自 R16 起包含合併前住在
             // 被併鍵上的**(R15 verify 第 20 列:R15 末句寫「含住在被併鍵上的不擋」,而被搬到別的配對上、把它的歧義變大的那幾筆正是
             // 住在被併鍵上——訊息與判準互相矛盾;D45 的判準寫在末句)
-            return "拒絕合併:併入「\(displaySafeInvisible(survivor, max: 200))」之後,\(Self.whoWouldHold(record, survivor: survivor))對 work"   // display-safe-exempt: Self.whoWouldHold、record、survivor:whoWouldHold 內部逐項 displaySafe
-                 + "「\(displaySafeInvisible(citekey, max: 200))」會持有兩個以上正規化後不同的 confirmed literal——這次合併帶進來的(合併前住在被併記錄裡、或住在被併鍵上):\n"
-                 + brought.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: brought 由 describeVerdictSource 組裝且已在擲出端截到 5 筆(R28),store 字串已逐項 displaySafeInvisible(displaySafe 不冪等)
-                 + (broughtTotal > brought.count ? "\n  …共 \(broughtTotal) 筆" : "")   // display-safe-exempt: Int
-                 + (existing.isEmpty ? "" : "\n倖存配對合併前就持有的(不擋,列出供對照):\n"
-                    + existing.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: existing 同上
-                    + (existingTotal > existing.count ? "\n  …共 \(existingTotal) 筆" : ""))   // display-safe-exempt: Int
-                 + "\nverdict 不帶 index,之後那筆 work 的邊在 resolve-venues 的 demote/repoint 上都會被拒(D23)。"
+            // 含 closure 與三元的運算元先拆成具型別的 let,末段的出路說明也另成一個 let:寫在同一串 `+` 裡時 CI 的 type-check
+            // 逾時。existingBlock 與原本的巢狀三元組出同一個字串(existing 為空時是 "");輸出逐字不變
+            let broughtLines: String = brought.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: brought 由 describeVerdictSource 組裝且已在擲出端截到 5 筆(R28),store 字串已逐項 displaySafeInvisible(displaySafe 不冪等)
+            let broughtMore: String = broughtTotal > brought.count ? "\n  …共 \(broughtTotal) 筆" : ""   // display-safe-exempt: Int
+            let existingLines: String = existing.map { "  • " + $0 }.joined(separator: "\n")   // display-safe-exempt: existing 同上
+            let existingMore: String = existingTotal > existing.count ? "\n  …共 \(existingTotal) 筆" : ""   // display-safe-exempt: Int
+            let existingBlock: String = existing.isEmpty ? "" : "\n倖存配對合併前就持有的(不擋,列出供對照):\n" + existingLines + existingMore
+            let truncationHint: String = broughtTotal > brought.count ? ";標「…共 N 筆」時其餘幾筆要開倖存記錄與被併記錄的 YAML 找" : ""   // display-safe-exempt: Int 比較與字面常量(R30:R29 的版本在不截時印出「如上,);」——R29 verify 第 29 列)
+            let remedy: String = "\nverdict 不帶 index,之後那筆 work 的邊在 resolve-venues 的 demote/repoint 上都會被拒(D23)。"
                  + "出路:把不屬於這條邊的那筆 confirmed verdict 從它所在記錄的 YAML 刪掉(先看「這次合併帶進來的」那幾筆——列出的出處如上"
-                 + (broughtTotal > brought.count ? ";標「…共 N 筆」時其餘幾筆要開倖存記錄與被併記錄的 YAML 找" : "")   // display-safe-exempt: Int 比較與字面常量(R30:R29 的版本在不截時印出「如上,);」——R29 verify 第 29 列)
+                 + truncationHint
                  + ");"
                  + "若那筆 work 另有一條邊指向被併者,也可以先用 resolve-venues --demote 把那條邊退回 literal。"
                  + "不擋的只有:倖存配對合併前就持有整組的違反,以及倖存配對合併前一筆都沒有、整組原樣從單一被併鍵搬來的(holder 遷移;validate 照報 warning);"
                  + "整組住在被併記錄裡的(那個檔要刪、出處會消失——先在它的 YAML 修掉)、或讓倖存配對的既有歧義變大的,都擋"
+            return "拒絕合併:併入「\(displaySafeInvisible(survivor, max: 200))」之後,\(Self.whoWouldHold(record, survivor: survivor))對 work"   // display-safe-exempt: Self.whoWouldHold、record、survivor:whoWouldHold 內部逐項 displaySafe
+                 + "「\(displaySafeInvisible(citekey, max: 200))」會持有兩個以上正規化後不同的 confirmed literal——這次合併帶進來的(合併前住在被併記錄裡、或住在被併鍵上):\n"
+                 + broughtLines
+                 + broughtMore
+                 + existingBlock
+                 + remedy
         case let .quarantinedPresent(files):
             let listed = files.prefix(3).map { displaySafeInvisible($0, max: 200) }
                 .joined(separator: "、") + (files.count > 3 ? "…" : "")
diff --git a/Sources/akashic-guards/AuditGuardsMutations.swift b/Sources/akashic-guards/AuditGuardsMutations.swift
index 66ebb51a..20fc261d 100644
--- a/Sources/akashic-guards/AuditGuardsMutations.swift
+++ b/Sources/akashic-guards/AuditGuardsMutations.swift
@@ -123,6 +123,9 @@ func auditGuardsMutations() -> Int32 {
         // 「受保護清單裡有不存在的路徑」全紅,`zero-instance-rows-audit` 也會以「找不到量測文件」中止。
         try? fm.createDirectory(atPath: tmp + "/docs", withIntermediateDirectories: true)
         try? fm.copyItem(atPath: "\(repoRoot)/\(zeroInstanceMeasurementsPath)", toPath: tmp + "/" + zeroInstanceMeasurementsPath)
+        // **裁決表**(2026-10-15):三份規則的裁決史搬到 `docs/rulings/`(不自動載入),它們是受保護檔,
+        // `zero-instance-rows-audit`/`parity-table-drift`/`measured-numbers-audit` 讀它們——少了它們 baseline 同樣全紅。
+        try? fm.copyItem(atPath: "\(repoRoot)/docs/rulings", toPath: tmp + "/docs/rulings")
 
         // **同一個 path 的多個 edit 依序套用,套完才檢查「有沒有改到東西」。**
         //
diff --git a/Sources/akashic-guards/AuditGuardsMutationsData.swift b/Sources/akashic-guards/AuditGuardsMutationsData.swift
index f7bb966a..3ab7a2ea 100644
--- a/Sources/akashic-guards/AuditGuardsMutationsData.swift
+++ b/Sources/akashic-guards/AuditGuardsMutationsData.swift
@@ -122,13 +122,13 @@ let agmCases: [AGMCase] = [
         AGMEdit(path: ".claude/rules/blocked-issues-must-be-scannable.md", kind: "replaceFirst", a: "(哪些「等」需要標記——封閉列舉,現有 4 列)", b: "(哪些「等」需要標記——封閉列舉,現有十五列)"),
     ], expect: ["現有十五列", "而下方的表有 4 列"]),
     AGMCase(desc: "numbers:標題宣稱列數、自己沒有表,而下一節有(不得借用)", guardRel: "akashic-guards measured-numbers-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "ziVerdictHeading", a: "## 前言(封閉列舉——現有 6 列)\n\n散文一句。\n\n## 裁決史(一列不多一列不少)", b: ""),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "ziVerdictHeading", a: "## 前言(封閉列舉——現有 6 列)\n\n散文一句。\n\n## 裁決史(一列不多一列不少)", b: ""),
     ], expect: ["最近的表在「## 裁決史(一列不多一列不少)」之後"]),
     AGMCase(desc: "numbers:標題的數字解析不出來(不得靜默略過)", guardRel: "akashic-guards measured-numbers-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "ziVerdictHeading", a: "## 前言(封閉列舉——現有 廿 列)\n\n散文一句。\n\n## 裁決史(一列不多一列不少)", b: ""),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "ziVerdictHeading", a: "## 前言(封閉列舉——現有 廿 列)\n\n散文一句。\n\n## 裁決史(一列不多一列不少)", b: ""),
     ], expect: ["解析不出來"]),
     AGMCase(desc: "numbers:宣稱與表之間隔了兩個標題(診斷要說「隔了 2 個」並具名)", guardRel: "akashic-guards measured-numbers-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", a: "| # | 情形 | 裁決 | 理由 |", b: "## 插入的空節甲\n\n## 插入的空節乙\n\n| # | 情形 | 裁決 | 理由 |"),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "| # | 情形 | 裁決 | 理由 |", b: "## 插入的空節甲\n\n## 插入的空節乙\n\n| # | 情形 | 裁決 | 理由 |"),
     ], expect: ["隔了 2 個標題", "插入的空節甲", "插入的空節乙"]),
     AGMCase(desc: "numbers:CLAUDE.md 裡出現一個裸的 `實測 N`", guardRel: "akashic-guards measured-numbers-audit", edits: [
         AGMEdit(path: "CLAUDE.md", kind: "replaceFirst", a: "## Rules", b: "## 補充\n\n實測 77 支。\n\n## Rules"),
@@ -137,29 +137,29 @@ let agmCases: [AGMCase] = [
         AGMEdit(path: ".claude/rules/blocked-issues-must-be-scannable.md", kind: "replaceFirst", a: "| 4 | 等時間累積", b: "| 3.5 | 等一個外部帳務事件 | ✅ **`### Blocking`** | 佔位 |\n| 4 | 等時間累積"),
     ], expect: ["現有 4 列", "而下方的表有 5 列"]),
     AGMCase(desc: "numbers:標題宣稱了列數卻沒有表(錨不存在)", guardRel: "akashic-guards measured-numbers-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "removeTableSeparator", a: "", b: ""),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "removeTableSeparator", a: "", b: ""),
     ], expect: ["找不到表——錨不存在"]),
     AGMCase(desc: "parity:程式新增一個 MCP tool 而表沒補", guardRel: "akashic-guards parity-table-drift", edits: [
         AGMEdit(path: "Sources/akashic-mcp/Server.swift", kind: "replaceFirst", a: "Tool(name: \"akashic_doctor\"", b: "Tool(name: \"akashic_brandnew\""),
     ], expect: ["在程式裡但**不在規則的 MCP 表**"]),
     AGMCase(desc: "parity:表列了一個程式沒有的 tool", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceFirst", a: "| `akashic_doctor` |", b: "| `akashic_ghost` |"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceFirst", a: "| `akashic_doctor` |", b: "| `akashic_ghost` |"),
     ], expect: ["但程式裡**沒有這個 tool**"]),
     AGMCase(desc: "parity:命令名只出現在某列的理由欄", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceAll", a: "| `fmt` | 有理由缺席", b: "| `fmt-x` | 有理由缺席"),
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceAll", a: "全庫改寫=維運例外", b: "全庫改寫=維運例外(同 `fmt`)"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceAll", a: "| `fmt` | 有理由缺席", b: "| `fmt-x` | 有理由缺席"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceAll", a: "全庫改寫=維運例外", b: "全庫改寫=維運例外(同 `fmt`)"),
     ], expect: ["`fmt`", "規則檔裡完全沒提到"]),
     AGMCase(desc: "parity:某命令只在散文被提到、不在任何表列", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceFirst", a: "| `fmt` |", b: "| `fmt-x` |"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceFirst", a: "| `fmt` |", b: "| `fmt-x` |"),
     ], expect: ["`fmt`", "規則檔裡完全沒提到"]),
     AGMCase(desc: "parity:規則檔完全不提某個 CLI subcommand", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceAll", a: "`doctor`", b: "`doktor`"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceAll", a: "`doctor`", b: "`doktor`"),
     ], expect: ["`doctor`", "規則檔裡完全沒提到"]),
     AGMCase(desc: "parity:表把某命令標成退場但它仍註冊著(表→命令方向)", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceFirst", a: "| ~~`migrate-work-types`~~", b: "| ~~`doctor`~~"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceFirst", a: "| ~~`migrate-work-types`~~", b: "| ~~`doctor`~~"),
     ], expect: ["仍註冊在 CLI.swift"]),
     AGMCase(desc: "parity:退場的列沒劃掉(孤兒列)", guardRel: "akashic-guards parity-table-drift", edits: [
-        AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceFirst", a: "| ~~`migrate-work-types`~~", b: "| `migrate-work-types`"),
+        AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceFirst", a: "| ~~`migrate-work-types`~~", b: "| `migrate-work-types`"),
     ], expect: ["已不在 CLI.swift"]),
     AGMCase(desc: "zi-rows:某一列裁決「寫」而它引用的編號在 Sources 裡不存在", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
         // **改既有列的編號,不再注入新列**(#365 fix,2026-09-02)。前兩版都靠注入一列:
@@ -175,7 +175,7 @@ let agmCases: [AGMCase] = [
         // **前提**:第 9 列只引用 #394 這一個編號——守衛的條件是「該列引用的編號**全部**在
         // Sources 找不到才報」。日後若第 9 列多引一個存在於 Sources 的編號,本 case 會**大聲**
         // 失敗(期望 rc=1 卻得 0),不會安靜通過。
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", a: "(#394:`Organization.ror`。", b: "(#9999:`Organization.ror`。"),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "(#394:`Organization.ror`。", b: "(#9999:`Organization.ror`。"),
     ], expect: ["在 Sources/ 裡都找不到"]),
     // **#479 補的兩格**:`zero-instance-rows-audit` 有四條失敗路徑,先前只有兩條有負控
     // (「✅ 而編號不在 Sources」「完全不引用編號」)。沒有負控的兩條正是 #365 於
@@ -187,23 +187,23 @@ let agmCases: [AGMCase] = [
     AGMCase(desc: "zi-rows:某一列在共通段沒有 bullet 講它", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
         // 錨在**第 1 列的 bullet 文字**上。bullet 不會像注入的新列那樣與真列撞號
         // (上一段記過的那個坑是「注入一列並寫死它的編號」,性質不同)。
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "- 第 1 列的理由是**失敗的不可見性**", b: "- 這一列的理由是**失敗的不可見性**"),
         // expect 錨在那則訊息的**後半段**,不錨「沒有任何 bullet 講它」:那是第 19 列自證閘的片段,寫在這裡就編進
         // `akashic-guards`,真的檢查不在時閘照樣成立(#711 R1 verify 第 6、11 列;守衛自 R1 起擋這件事)。
     ], expect: ["每列理由**彼此不同**的說明"]),
     AGMCase(desc: "zi-rows:列號跳號(一列被整個跳過而守衛照樣綠)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "| 1 | **零實例、成本一行、前件精確**", b: "| 2 | **零實例、成本一行、前件精確**"),
     ], expect: ["裁決表的列號跳號"]),
     AGMCase(desc: "zi-rows:多一個 `|` 讓裁決欄讀到別欄", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
         // 插在 issue 編號**之後**——插在前面會先命中「沒有引用任何 issue 編號」那條,
         // 蓋掉本 case 要驗的分支(同一份檔案上面那段記過的同型錯誤)。
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "rationale 種類數 = 0)|", b: "rationale 種類數 = 0|)|"),
     ], expect: ["那不像裁決"]),
     AGMCase(desc: "zi-rows:某一列完全不引用 issue 編號", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", a: "(#254:", b: "(無編號:"),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "(#254:", b: "(無編號:"),
     ], expect: ["沒有引用任何 issue 編號"]),
     // ── #711:`zero-instance-rows-audit` 的第五條失敗路徑——量測指令的自證閘 ─────────────────────────
     // R2 起一條對 binary 輸出計數的量測只有一種合法寫法:`LC_ALL=C grep -a -q '<片段>'  &&  <參數> 2>&1 | grep -c '<樣式>'`,
@@ -288,8 +288,8 @@ let agmCases: [AGMCase] = [
     AGMCase(desc: "zi-rows:量測指令一條都沒掃到", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
         // R4:binary 的輸出經管線送進不在「已知不計數」清單裡的命令也算計數,而樣式裡的 `\|` 被當成管線(誤認的方向)——把 `grep -c`
         // 換成 `grep -q` 不再讓每一條都消失。改成讓 binary 的名字消失:變數名不含 akashic 的 `"$B1"` 是誠實邊界裡的「以變數執行」。
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceAll", a: "\"$(command -v akashic)\"", b: "\"$B1\""),
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceAll", a: ".build/debug/akashic-guards", b: "\"$B2\""),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceAll", a: "\"$(command -v akashic)\"", b: "\"$B1\""),
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceAll", a: ".build/debug/akashic-guards", b: "\"$B2\""),
         AGMEdit(path: "docs/zero-instance-measurements.md", kind: "replaceAll", a: "\"$(command -v akashic)\"", b: "\"$B1\""),
         AGMEdit(path: "docs/zero-instance-measurements.md", kind: "replaceAll", a: ".build/debug/akashic-guards", b: "\"$B2\""),
     ], expect: ["的指令一條都沒掃到"]),
@@ -335,7 +335,7 @@ let agmCases: [AGMCase] = [
     ], expect: ["的 fence 到檔尾都沒有收尾"]),
     // #711 R2(verify 第 9、18 列):`text` fence 照掃——R1 對任何 `text` fence 一律不掃,換個語言標記就能讓量測安靜通過。
     AGMCase(desc: "zi-rows:text fence 照掃", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "\n## 各列共通的東西",
                 b: "\n```text\n\"$(command -v akashic)\" validate 2>&1 | grep -c '死 verdict'\n```\n\n## 各列共通的東西"),
     ], expect: ["的 `text` fence 裡對 binary 的輸出計數"]),
@@ -344,7 +344,7 @@ let agmCases: [AGMCase] = [
     // 每一種各帶一個獨有的記號(`zi-a1-…`),expect 逐一點名——其中一種退回靜默時,那個記號不會出現在輸出裡,這一格就紅。
     // 跨行:`\`、行尾 `|`、`&&`、`||`、`|` 之後的 `#` 註解、下一行開頭的 `|`。
     AGMCase(desc: "zi-rows:跨行的量測(\\、行尾 |/&&/||、# 註解後的 |、下一行開頭的 |)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "\n## 各列共通的東西",
                 b: "\n```bash\n\"$(command -v akashic)\" validate 2>&1 \\\n  | grep -c 'zi-a1-backslash'\n\n\"$(command -v akashic)\" validate 2>&1 |\n  grep -c 'zi-a2-pipe'\n\n\"$(command -v akashic)\" validate 2>&1 &&\n  grep -c 'zi-a3-andand' /dev/null\n\n\"$(command -v akashic)\" validate 2>&1 ||\n  grep -c 'zi-a4-oror' /dev/null\n\n\"$(command -v akashic)\" validate 2>&1 |   # 計數\n  grep -c 'zi-a5-comment'\n\n\"$(command -v akashic)\" validate 2>&1\n  | grep -c 'zi-a6-leading'\n```\n\n## 各列共通的東西"),
     ], expect: ["起跨 2 行的單位", "zi-a1-backslash", "zi-a2-pipe", "zi-a3-andand", "zi-a4-oror", "zi-a5-comment", "zi-a6-leading"]),
@@ -352,7 +352,7 @@ let agmCases: [AGMCase] = [
     // 最後一個 expect 要「輸出計數,卻不是」這一段:不剝 `>` 時三個反引號會被當成一段跨行的 inline code,訊息是「跨行不是唯一合法的寫法」,
     // 只比「不是唯一合法的寫法」會讓這一格對那個退化照綠(守衛的 mutant 實測過)。
     AGMCase(desc: "zi-rows:fence 與 inline code 以外的量測(縮排區塊、
、散文、引用區塊裡的 fence)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [
-        AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst",
+        AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst",
                 a: "\n## 各列共通的東西",
                 b: "\n縮排區塊:\n\n    akashic validate 2>&1 | grep -c 'zi-b1-indent'\n\n
\nakashic validate 2>&1 | grep -c 'zi-b2-pre'\n
\n\n" + "散文裡的 akashic validate 2>&1 | grep -c 'zi-b3-prose' 一句。\n\n" @@ -361,7 +361,7 @@ let agmCases: [AGMCase] = [ // inline code 的配對(CommonMark):跨行的 span、只隔空白而以 `|` 相連的兩段、同一行前面一個孤立的反引號(後面的量測落在散文裡)、 // 前面一段雙反引號 span(R2 的逐行 regex 讓後面的配對錯位)。 AGMCase(desc: "zi-rows:inline code 的配對(跨行的 span、只隔空白的兩段、孤立的反引號、雙反引號 span)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n跨行:`\"$(command -v akashic)\" validate 2>&1 |\ngrep -c 'zi-c1-span'` 結束。\n\n" + "兩段:`\"$(command -v akashic)\" validate 2>&1 |` `grep -c 'zi-c2-adjacent'`\n\n" @@ -371,7 +371,7 @@ let agmCases: [AGMCase] = [ // 計數與 binary 的辨識:選項加引號、`-c` 在含 `|` 的樣式之後、BRE 的 `\|`、`uniq -c`、預設值展開 `${X:-akashic}`、`wc -l`、 // `rg --count`、`egrep -c`、`grep --count`、`~~~` fence。記號放在 <參數> 裡的(`uniq`、`wc`)沒有樣式可放。 AGMCase(desc: "zi-rows:計數與 binary 的辨識(引號、樣式之後的 -c、uniq -c、${X:-akashic}、wc、rg、egrep、--count、~~~ fence)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`\"$(command -v akashic)\" validate 2>&1 | grep '-c' 'zi-d1-quoted'`\n\n" + "`\"$(command -v akashic)\" validate 2>&1 | grep -e 'zi-d2|alt' -c`\n\n" @@ -387,7 +387,7 @@ let agmCases: [AGMCase] = [ "zi-d8-egrep", "zi-d9-long", "zi-d10-tilde"]), // 不再切註解:`$'…'` 裡的 `\'` 讓 R2 的詞法器把後半當註解、shell 照跑(第 15 列);<參數> 裡的 `#` 讓計數整段落進註解,R3 起模板不收。 AGMCase(desc: "zi-rows:$'…' 與 <參數> 裡的 #(守衛不再切註解)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n```bash\n\"$(command -v akashic)\" validate $'\\' # ' 2>&1 | grep -c 'zi-e1-ansi'\n" + "LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate # zi-e2-hash 2>&1 | grep -c '死 verdict'\n" @@ -411,7 +411,7 @@ let agmCases: [AGMCase] = [ // 每一種寫法各帶一個獨有的記號(`zr4-…`),其中一種退回靜默時那個記號不出現,這一格就紅。 // 單位:管線之後的空白行與純註解行照接;下一個有內容的行以 `|` 開頭也照接(第 0 列)。 AGMCase(desc: "zi-rows:管線跨空白行與純註解行(fence)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n```bash\n\"$(command -v akashic)\" validate 2>&1 |\n\n grep -c 'zr4-a1-blank'\n\n" + "\"$(command -v akashic)\" validate 2>&1 |\n# 計數\ngrep -c 'zr4-a2-comment'\n\n" @@ -419,7 +419,7 @@ let agmCases: [AGMCase] = [ ], expect: ["起跨 3 行的單位", "zr4-a1-blank", "zr4-a2-comment", "zr4-a3-lead"]), // 單位:以 `|` 開頭的行只有在 GFM 表格裡才是表格列;多行 inline code 裡以 `|` 開頭的續行不切段(第 2 列)。段落、清單項目、引用區塊各一。 AGMCase(desc: "zi-rows:以 | 開頭而不是表格列的續行", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n段落:`\"$(command -v akashic)\" validate 2>&1\n| grep -c 'zr4-b1-para'` 結束。\n\n" + "- 清單:`\"$(command -v akashic)\" validate 2>&1\n | grep -c 'zr4-b2-list'`\n\n" @@ -427,14 +427,14 @@ let agmCases: [AGMCase] = [ ], expect: ["起跨 2 行的單位", "zr4-b1-para", "zr4-b2-list", "zr4-b3-quote"]), // 單位:段落最後一段 inline code 以接續運算子結尾、下一段落以 inline code 開頭時接成一個;兩段之間只隔一個接續運算子時也接(第 20 列)。 AGMCase(desc: "zi-rows:跨段落、以運算子相連的 inline code", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`\"$(command -v akashic)\" validate --zr4-c1-paragraphs 2>&1 |`\n\n`grep -c 'x'`\n\n" + "`\"$(command -v akashic)\" validate --zr4-c2-operator 2>&1` | `grep -c 'x'`\n\n## 各列共通的東西"), ], expect: ["起跨 2 行的單位", "zr4-c1-paragraphs 2>&1 | grep -c 'x'", "zr4-c2-operator 2>&1 | grep -c 'x'"]), // 接起來的內容:沒接上時前一段單獨也紅(管線之後沒有命令),只比記號會漏 // 計數:名字以 `grep` 結尾的命令、以 `--cou` 開頭的長選項、binary 的輸出經管線送進不在「已知不計數」清單裡的命令(第 4、7、12、20 列)。 AGMCase(desc: "zi-rows:計數的辨識(以 grep 結尾的命令、--count-matches、管線進任何命令)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`\"$(command -v akashic)\" validate 2>&1 | ggrep -c 'zr4-d1-ggrep'`\n\n" + "`\"$(command -v akashic)\" validate 2>&1 | zgrep -c 'zr4-d2-zgrep'`\n\n" @@ -454,21 +454,21 @@ let agmCases: [AGMCase] = [ "zr4-d7-nl", "zr4-d8-sed", "zr4-d9-python", "zr4-d10-grepn", "zr4-d11-env", "zr4-d12-procsub", "zr4-d13-procsub"]), // binary:參數展開不帶冒號的預設值、名字的大小寫(第 4、20 列;macOS 的檔案系統不分大小寫)。 AGMCase(desc: "zi-rows:binary 的辨識(${X-akashic}、大小寫)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`\"${AKASHIC_BIN-akashic}\" validate 2>&1 | grep -c 'zr4-e1-dash'`\n\n" + "`~/bin/Akashic validate 2>&1 | grep -c 'zr4-e2-case'`\n\n## 各列共通的東西"), ], expect: ["不是唯一合法的寫法", "zr4-e1-dash", "zr4-e2-case"]), // `${V:?…}` 寫在管線裡只結束那一段的子 shell,計數照印 0(第 1、3、5 列):量測與非量測的單位都紅。 AGMCase(desc: "zi-rows:管線裡的 ${V:?…}", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate --library \"${STORE:?zr4-f1-args}\" 2>&1 | grep -c '死 verdict'`\n\n" + "`find \"${STORE:?zr4-f2-find}/sources\" -name 'x' | wc -l`\n\n## 各列共通的東西"), ], expect: ["寫在管線裡", "zr4-f1-args", "zr4-f2-find"]), // 前置條件:<參數> 用到的變數要在最前面的 `: "${V:?…}"` 裡;`--library "$V"` 要 `test -f "$V/store.yaml"`(第 1、3、5、15 列)。 AGMCase(desc: "zi-rows:量測缺前置條件", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate --library \"$STORE\" --zr4-g1 2>&1 | grep -c '死 verdict'`\n\n" + "`: \"${STORE:?x}\" && LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate --library \"$STORE\" --zr4-g2 2>&1 | grep -c '死 verdict'`\n\n## 各列共通的東西"), @@ -478,7 +478,7 @@ let agmCases: [AGMCase] = [ AGMEdit(path: "docs/zero-instance-measurements.md", kind: "replaceFirst", a: "out=$(.build/debug/akashic-guards migrated-guard-control); echo \"rc=$?\" # 2026-09-30:rc=0\n", b: "\"$(command -v akashic)\" validate 2>&1 | grep -c 'zr4-h1-replaced'\n"), - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n\n```text\n\"$(command -v akashic)\" validate 2>&1 | grep -c 'zr4-h2-marker'\n```\n\n## 各列共通的東西"), ], expect: ["`text` fence 照掃", "zr4-h1-replaced", "zr4-h2-marker"]), @@ -487,7 +487,7 @@ let agmCases: [AGMCase] = [ AGMEdit(path: "docs/zero-instance-measurements.md", kind: "replaceFirst", a: "`LC_ALL=C grep -a -q '本機缺承重存檔' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate 2>&1 | grep -c '本機缺承重存檔:'`", b: "`out=$(\"$(command -v akashic)\" validate 2>&1)`、`printf '%s' \"$out\" | grep -c '本機缺承重存檔:'`"), - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\" && \"$(command -v akashic)\" validate 2>&1 | grep -c '死 verdict'`\n\n## 各列共通的東西"), ], expect: ["棘輪標記的下限是"]), @@ -496,7 +496,7 @@ let agmCases: [AGMCase] = [ // 接續與段落邊界各一個記號。段落邊界那幾格的記號放在「`:`」之後:邊界在時那一段是 inline code、訊息是「卻不是唯一合法的寫法 `…`:`<內容>`」, // 邊界被拿掉時前一行孤立的反引號與它的開頭配對、量測落進散文,訊息換成「在 fence 與 inline code 以外…:`<內容>`」——只比記號兩種都綠。 AGMCase(desc: "zi-rows:接續與段落邊界(行尾 |&、下一行開頭的 &&、反斜線奇偶、清單、引用加深、HTML 註解、標題)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n```bash\n\"$(command -v akashic)\" validate --zr4-i1-pipeamp 2>&1 |&\ngrep -c 'x'\n\n" + "LC_ALL=C grep -a -q '不在載入集合,且沒有任何檔宣稱它' \"$(command -v akashic)\"\n&& \"$(command -v akashic)\" validate --zr4-i2-leadand 2>&1 | grep -c '死 verdict'\n\n" @@ -511,7 +511,7 @@ let agmCases: [AGMCase] = [ "`:`\"$(command -v akashic)\" validate --zr4-j4-heading"]), // 不經管線的計數:計數命令在前、binary 在 `<( … )` 裡——管線的判準管不到,只靠計數命令與計數選項的辨識。 AGMCase(desc: "zi-rows:不經管線的計數(ag、ack、uniq、wc、jq length、--cou)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n`ag -c 'zr4-i3-ag' <(\"$(command -v akashic)\" validate 2>&1)`\n\n" + "`ack -c 'zr4-i4-ack' <(\"$(command -v akashic)\" validate 2>&1)`\n\n" @@ -523,7 +523,7 @@ let agmCases: [AGMCase] = [ // 散文的同一組判斷(R4 起與 fence、inline code 同一把):縮排區塊裡隔著空白行、下一行以 `|` 開頭;引用區塊裡沒收尾的 inline code 接到 // 下一行沒有 `>` 的 lazy continuation;HTML 實體寫的 `|`。三種在 R4 之前都切成兩半或認不出管線。 AGMCase(desc: "zi-rows:散文跨空白行、引用區塊的 lazy continuation、實體寫的 |", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n akashic validate --zr4-j8-indent 2>&1\n\n | grep -c 'x'\n\n" + "> 見 `\"$(command -v akashic)\" validate --zr4-j7-lazy 2>&1 |\ngrep -c 'x'` 結束\n\n" @@ -538,9 +538,9 @@ let agmCases: [AGMCase] = [ AGMCase(desc: "zi-rows:量測文件不見(不得當成沒有量測)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ AGMEdit(path: "docs/zero-instance-measurements.md", kind: "delete", a: "", b: ""), ], expect: ["找不到 docs/zero-instance-measurements.md", "自證閘與棘輪都沒有輸入"]), - // 棘輪標記搬回規則檔:兩份各一個時地板有兩個數。量測文件那一個留著,只多規則檔這一個。 - AGMCase(desc: "zi-rows:棘輪標記出現在規則檔", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + // 棘輪標記搬回裁決表所在的檔(2026-10-15 起是裁決史檔):兩份各一個時地板有兩個數。量測文件那一個留著,只多這一個。 + AGMCase(desc: "zi-rows:棘輪標記出現在裁決史檔", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n\n\n## 各列共通的東西"), ], expect: ["棘輪標記住在 docs/zero-instance-measurements.md", "裡不得有(找到 1 個)"]), @@ -553,12 +553,29 @@ let agmCases: [AGMCase] = [ AGMCase(desc: "zi-rows:量測寫法規則不見", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ AGMEdit(path: ".claude/rules/measurement-commands-self-prove.md", kind: "delete", a: "", b: ""), ], expect: ["找不到 .claude/rules/measurement-commands-self-prove.md"]), - // 規則檔仍被掃:表格裡沒有閘的量測照樣紅(第 56 列的量測寫在表格裡)。錨在第 56 列那一條的閘上,拿掉閘。 - AGMCase(desc: "zi-rows:規則檔表格裡的量測少了閘", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + // 裁決史檔仍被掃(2026-10-15 起表格住在那裡):表格裡沒有閘的量測照樣紅(第 56 列的量測寫在表格裡)。錨在第 56 列那一條的閘上,拿掉閘。 + AGMCase(desc: "zi-rows:裁決史檔表格裡的量測少了閘", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "`LC_ALL=C grep -a -q '附加 Zotero 來源沒記 library_id' \"$(command -v akashic)\" && ", b: "`"), - ], expect: [".claude/rules/zero-instance-guards.md 第", "不是唯一合法的寫法", "grep -c '附加 Zotero 來源沒記 library_id'"]), + ], expect: ["docs/rulings/zero-instance-guards.md 第", "不是唯一合法的寫法", "grep -c '附加 Zotero 來源沒記 library_id'"]), + // ── 2026-10-15:裁決表與「各列共通的東西」搬到不自動載入的 docs/rulings/zero-instance-guards.md。守衛的每一條新分支各一格。 + // 裁決史檔不在:不得當成「沒有列」——列號、實作與共通段的檢查都沒有輸入時要說輸入不在。 + AGMCase(desc: "zi-rows:裁決史檔不見(不得當成沒有列)", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "delete", a: "", b: ""), + ], expect: ["找不到 docs/rulings/zero-instance-guards.md", "列號、實作與共通段的檢查都沒有輸入"]), + // 表格列寫回自動載入的規則檔:那一列不會被列號與實作檢查讀到,兩份各有一段表、只有一段被檢查。 + AGMCase(desc: "zi-rows:表格列寫回自動載入的規則檔", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ + AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + a: "\n## 跟其他規則的關係", + b: "\n| 1 | x | ❌ 不寫 | y |\n\n## 跟其他規則的關係"), + ], expect: [".claude/rules/zero-instance-guards.md 裡有 1 個裁決表的列"]), + // 棘輪標記出現在自動載入的規則檔:表格搬走之後規則檔仍被查(三份各一個時地板有三個數)。 + AGMCase(desc: "zi-rows:棘輪標記出現在自動載入的規則檔", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ + AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + a: "\n## 跟其他規則的關係", + b: "\n\n\n## 跟其他規則的關係"), + ], expect: ["棘輪標記住在 docs/zero-instance-measurements.md,.claude/rules/zero-instance-guards.md 裡不得有(找到 1 個)"]), // ── #711 b37(b36 verify):每一則 MEDIUM 與補寬的辨識各一格。 // `--library` 的分隔與 shell 相同(第 0 列:TAB、多個空白都不被 R4 認出,`test -f` 不被要求)。 AGMCase(desc: "zi-rows:--library 以 TAB 或多個空白分隔、前面沒有 test -f", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ @@ -628,7 +645,7 @@ let agmCases: [AGMCase] = [ AGMEdit(path: "Sources/AkashicCore/Models.swift", kind: "replaceFirst", a: "public var authors:", b: "public var seeAlso: [String] = []\n public var authors:"), ], expect: ["seeAlso", "新欄位未經裁決"]), AGMCase(desc: "parity:含空白的命令 token 退場了卻沒劃掉", guardRel: "akashic-guards parity-table-drift", edits: [ - AGMEdit(path: ".claude/rules/mcp-cli-parity.md", kind: "replaceFirst", a: "| `export-tables --view`", b: "| `ghost-command --view`"), + AGMEdit(path: "docs/rulings/mcp-cli-parity.md", kind: "replaceFirst", a: "| `export-tables --view`", b: "| `ghost-command --view`"), ], expect: ["已不在 CLI.swift"]), AGMCase(desc: "ratchet:Swift 多一個非純量欄位而沒被裁決", guardRel: "akashic-guards backlink-field-ratchet", edits: [ AGMEdit(path: "Sources/AkashicCore/Models.swift", kind: "replaceFirst", a: "public var authors:", b: "public var brandNewEdge: [VenueRef] = []\n public var authors:"), @@ -648,7 +665,7 @@ let agmRobust: [AGMCase] = [ // (`che-akashic`)、沒有管線的 `${V:?…}`(簡單指令讓 shell 停下)、表格列裡的 `|`(欄的分隔,不是管線)都不改變任何事。 // (R2/R3 這裡是「前一行是退場標記的 text fence 不掃」;R4 拿掉了退場標記,那一格改成須紅的「退場標記不再是出口」。) AGMCase(desc: "zi-rows:顯示命令、別的名字、沒有管線的 ${V:?…}、表格列的 | 不是量測", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n```bash\n\"$(command -v akashic)\" validate 2>&1 | head -1\n\"$(command -v akashic)\" doctor 2>&1 | grep -E '^orphaned'\n" + "che-akashic validate 2>&1 | grep -c 'x'\npython3 - \"${STORE:?x}\" <<'PY'\nPY\n```\n\n" @@ -657,7 +674,7 @@ let agmRobust: [AGMCase] = [ // #711 R4(b33 X6 第 19 列):顯示命令前面的環境設定、路徑、反斜線不改變它是顯示命令;名字以 grep 結尾、沒有計數選項的命令只挑行; // 散文裡 HTML 標記內的 `|` 不是管線(標記先去掉)。 AGMCase(desc: "zi-rows:顯示命令的前綴與 HTML 標記裡的 | 不是計數", guardRel: "akashic-guards zero-instance-rows-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "\n## 各列共通的東西", b: "\n```bash\n\"$(command -v akashic)\" validate 2>&1 | LC_ALL=C head -1\n\"$(command -v akashic)\" validate 2>&1 | /usr/bin/head -1\n" + "\"$(command -v akashic)\" validate 2>&1 | \\head -1\n\"$(command -v akashic)\" doctor 2>&1 | zgrep -E '^orphaned'\n```\n\n" @@ -688,7 +705,7 @@ let agmRobust: [AGMCase] = [ b: " run: bash .githooks/run-guards.sh && cat .githooks/run-guards.sh | bash"), ], expect: ["全部存在"]), AGMCase(desc: "numbers:宣稱與表之間有一行以 #407 開頭的散文(不得當成標題)", guardRel: "akashic-guards measured-numbers-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", a: "| # | 情形 | 裁決 | 理由 |", b: "#407 R67l 的量測見下表。\n\n| # | 情形 | 裁決 | 理由 |"), + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "| # | 情形 | 裁決 | 理由 |", b: "#407 R67l 的量測見下表。\n\n| # | 情形 | 裁決 | 理由 |"), ], expect: ["列數宣稱皆相符"]), AGMCase(desc: "numbers:標題與表之間夾一段 fence 包住的示範表(不得被當成真的表)", guardRel: "akashic-guards measured-numbers-audit", edits: [ AGMEdit(path: ".claude/rules/blocked-issues-must-be-scannable.md", kind: "replaceFirst", a: "| # | 情形 | 裁決 | 理由 |", b: "```markdown\n| 示範 | 表 |\n|---|---|\n| a | b |\n```\n\n| # | 情形 | 裁決 | 理由 |"), @@ -698,7 +715,7 @@ let agmRobust: [AGMCase] = [ AGMEdit(path: ".claude/rules/blocked-issues-must-be-scannable.md", kind: "replaceFirst", a: "\n\n## 跟其他規則的關係", b: "\n| 另一張 | 表 |\n|---|---|\n| x | y |\n\n## 跟其他規則的關係"), ], expect: ["列數宣稱皆相符"]), AGMCase(desc: "在散文(非標題)加一句「只有 2 條」,其後有表(不得被誤判為不符)", guardRel: "akashic-guards measured-numbers-audit", edits: [ - AGMEdit(path: ".claude/rules/zero-instance-guards.md", kind: "replaceFirst", a: "## 裁決史", b: "不得類推:本檔的封閉性只有 2 條依據。\n\n## 裁決史"), + AGMEdit(path: "docs/rulings/zero-instance-guards.md", kind: "replaceFirst", a: "## 裁決史", b: "不得類推:本檔的封閉性只有 2 條依據。\n\n## 裁決史"), ], expect: ["列數宣稱皆相符"]), AGMCase(desc: "Swift 裡多一個不平衡的大括號字串字面(不得讓區段暴走)", guardRel: "akashic-guards parity-table-drift", edits: [ AGMEdit(path: "Sources/akashic/CreateEntryCommand.swift", kind: "replaceFirst", a: "struct CreateEntryCmd: ParsableCommand {", b: "struct CreateEntryCmd: ParsableCommand {\n static let brace = \"{\""), @@ -722,6 +739,9 @@ let agmWatched: [String] = [ "Sources/akashic-mcp/Server.swift", "Sources/AkashicCore/Models.swift", ".claude/rules/zero-instance-guards.md", + // 2026-10-15:裁決史表格從上面兩份規則檔搬到 docs/rulings/(不自動載入) + "docs/rulings/mcp-cli-parity.md", + "docs/rulings/zero-instance-guards.md", "Sources/akashic/CreateEntryCommand.swift", // #711:量測文件與量測寫法的規則也被注入(只在 copy 裡) "docs/zero-instance-measurements.md", diff --git a/Sources/akashic-guards/MeasuredNumbersAudit.swift b/Sources/akashic-guards/MeasuredNumbersAudit.swift index 6f53a6bb..a8a104b8 100644 --- a/Sources/akashic-guards/MeasuredNumbersAudit.swift +++ b/Sources/akashic-guards/MeasuredNumbersAudit.swift @@ -22,6 +22,7 @@ // trigger-coverage: reads .claude/rules/*.md // trigger-coverage: reads CLAUDE.md // trigger-coverage: reads docs/zero-instance-measurements.md +// trigger-coverage: reads docs/rulings/*.md import Foundation @@ -127,6 +128,9 @@ func measuredNumbersAudit() -> Int32 { // #711(使用者 2026-10-05 裁決):零實例裁決表的量測與歷輪補記從規則檔搬到 docs/zero-instance-measurements.md(不自動載入)——那些「實測 N」 // 原本在 `.claude/rules/` 裡受這支守衛檢查,搬走之後要跟著讀,否則搬一次就等於拿掉檢查。 (zeroInstanceMeasurementsPath, fileExists(zeroInstanceMeasurementsPath) ? [zeroInstanceMeasurementsPath] : []), + // 2026-10-15:三份規則的裁決表搬到 docs/rulings/(不自動載入),表格裡的「實測 N」原本在 `.claude/rules/` 裡受檢查—— + // 理由同上一列:搬一次不得等於拿掉檢查。逐來源,所以整個目錄不見也會出聲。 + ("docs/rulings/*.md", globFiles("docs/rulings/*.md")), ] let empty = sources.filter { $0.1.isEmpty } if !empty.isEmpty { diff --git a/Sources/akashic-guards/ParityTableDrift.swift b/Sources/akashic-guards/ParityTableDrift.swift index f76b9c4c..abcdd369 100644 --- a/Sources/akashic-guards/ParityTableDrift.swift +++ b/Sources/akashic-guards/ParityTableDrift.swift @@ -17,6 +17,11 @@ // 會比它要防的漂移更脆弱。與規則檔自己記過的不對稱一致:誤擋可見可逆,漏報安靜。 // // trigger-coverage: reads .claude/rules/mcp-cli-parity.md +// trigger-coverage: reads docs/rulings/mcp-cli-parity.md +// +// **兩份檔**(2026-10-15):三張裁決表與機械稽核程序搬到 `docs/rulings/mcp-cli-parity.md` +// (不自動載入),規則檔只留陳述與指向那裡的一行。本支把兩份接在一起當成一份讀—— +// 表在哪一份不影響判讀;裁決史檔讀不到時整個失敗,不當成表是空的。 import Foundation @@ -88,12 +93,19 @@ private func noExoticStrings(_ srcs: String) -> String? { func parityTableDrift() -> Int32 { let rulePath = ".claude/rules/mcp-cli-parity.md" - guard let rule = readFile(rulePath), + let rulingsPath = "docs/rulings/mcp-cli-parity.md" + guard let ruleText = readFile(rulePath), let server = readFile("Sources/akashic-mcp/Server.swift"), let cli = readFile("Sources/akashic/CLI.swift") else { print("✗ 讀不到 \(rulePath)/Server.swift/CLI.swift 之一") return 1 } + guard let rulingsText = readFile(rulingsPath) else { + print("✗ 讀不到 \(rulingsPath)——裁決表住在那裡(2026-10-15 從規則檔搬出),它不在時兩面的表都沒有輸入") + return 1 + } + // 規則檔與裁決史檔接成一份:表在哪一份都算數,而兩份都在掃描範圍內。 + let rule = ruleText + "\n" + rulingsText var fails: [String] = [] // ① MCP 面:嚴格集合相等,兩個方向都驗。 diff --git a/Sources/akashic-guards/PluginRootsMutations.swift b/Sources/akashic-guards/PluginRootsMutations.swift index ba1f4481..134876d3 100644 --- a/Sources/akashic-guards/PluginRootsMutations.swift +++ b/Sources/akashic-guards/PluginRootsMutations.swift @@ -107,6 +107,11 @@ func pluginRootsMutations() -> Int32 { withIntermediateDirectories: true) try? fm.copyItem(atPath: "\(repoRoot)/\(f)", toPath: tmp + "/" + f) } + // 2026-10-15:三份規則的裁決表搬到 `docs/rulings/`(受保護檔) + if fm.fileExists(atPath: "\(repoRoot)/docs/rulings") { + try? fm.createDirectory(atPath: tmp + "/docs", withIntermediateDirectories: true) + try? fm.copyItem(atPath: "\(repoRoot)/docs/rulings", toPath: tmp + "/docs/rulings") + } return body(tmp) } diff --git a/Sources/akashic-guards/ProtectedInventory.swift b/Sources/akashic-guards/ProtectedInventory.swift index a9e05cc8..978420cc 100644 --- a/Sources/akashic-guards/ProtectedInventory.swift +++ b/Sources/akashic-guards/ProtectedInventory.swift @@ -185,6 +185,9 @@ func protectedInventory() -> (guards: [String], data: [String]) { // #711(使用者 2026-10-05 裁決):零實例裁決表的量測與歷輪補記搬到這份不自動載入的文件,`zero-instance-rows-audit` // (自證閘、棘輪標記)與 `measured-numbers-audit`(「實測 N」)讀它。它不在 `.claude/rules/` 的 glob 裡,所以顯式列。 DATA += [zeroInstanceMeasurementsPath] + // 2026-10-15:三份規則的裁決表(`mcp-cli-parity`、`two-kinds-of-edits`、`zero-instance-guards`)搬到 `docs/rulings/`(不自動載入)。 + // `zero-instance-rows-audit`、`parity-table-drift`、`measured-numbers-audit` 讀它們,而它們不在 `.claude/rules/` 的 glob 裡,所以顯式列。 + DATA += globFiles("docs/rulings/*.md").sorted() // #711 b37:`zero-instance-rows-audit` 讀 `WriteGateRulings` 命令層的 `.readOnly` 格,決定量測的哪些子命令不必 `--library`。 // 那張表改了(例如 `doctor` 被誤標成唯讀),這支守衛的判讀跟著變,所以它是輸入。 DATA += [selfProofWriteGateRulingsPath] diff --git a/Sources/akashic-guards/TriggerCoverageMutations.swift b/Sources/akashic-guards/TriggerCoverageMutations.swift index 5c3be853..1b450513 100644 --- a/Sources/akashic-guards/TriggerCoverageMutations.swift +++ b/Sources/akashic-guards/TriggerCoverageMutations.swift @@ -85,6 +85,12 @@ func triggerCoverageMutations() -> Int32 { try? FileManager.default.copyItem(atPath: s, toPath: tmp + "/" + f) } } + // 2026-10-15:三份規則的裁決表搬到 `docs/rulings/`(受保護檔,同上一格的形狀:少了它全部 case 一起紅)。 + let rulingsSrc = "\(repoRoot)/docs/rulings" + if FileManager.default.fileExists(atPath: rulingsSrc) { + try? FileManager.default.createDirectory(atPath: tmp + "/docs", withIntermediateDirectories: true) + try? FileManager.default.copyItem(atPath: rulingsSrc, toPath: tmp + "/docs/rulings") + } for e in edits { let p = tmp + "/" + e.path guard let t = try? String(contentsOfFile: p, encoding: .utf8) else { return nil } diff --git a/Sources/akashic-guards/ZeroInstanceRowsAudit.swift b/Sources/akashic-guards/ZeroInstanceRowsAudit.swift index 607e83af..294eb983 100644 --- a/Sources/akashic-guards/ZeroInstanceRowsAudit.swift +++ b/Sources/akashic-guards/ZeroInstanceRowsAudit.swift @@ -16,6 +16,9 @@ // // **兩份檔**(#711,使用者 2026-10-05 裁決):裁決表住在規則檔,各列的量測、歷輪補記與棘輪標記住在不自動載入的 // `docs/zero-instance-measurements.md`。表格、列號與共通段的檢查只讀規則檔;自證閘兩份都掃(表格裡仍可能有量測)。 +// **三份檔**(2026-10-15):規則檔每個 session 自動載入,裁決表與「各列共通的東西」搬到不自動載入的 +// `docs/rulings/zero-instance-guards.md`(裁決史檔)。表格、列號與共通段的檢查改讀裁決史檔;規則檔只留陳述與指向那裡的一行, +// 表格列寫回規則檔即紅;自證閘三份都掃。 // 量測寫法的通則抽成規則 `measurement-commands-self-prove`,它引用的模板是 `selfProofTemplate` 的第二份描述,所以另查兩者逐字相同。 // // **誠實邊界(三條,與 Python 版同)**: @@ -39,6 +42,9 @@ let zeroInstanceRulePath = ".claude/rules/zero-instance-guards.md" /// 各列的量測與歷輪補記(#711,使用者 2026-10-05 裁決):規則檔每個 session 自動載入,量測腳本與逐輪補記搬到這份不自動載入的文件。 /// 自證閘兩份都掃;棘輪標記住在這裡。 let zeroInstanceMeasurementsPath = "docs/zero-instance-measurements.md" +/// 裁決表與「各列共通的東西」(2026-10-15 從規則檔搬出):規則檔每個 session 自動載入,表格佔了它大部分的位元組,搬到這份不自動載入的文件。 +/// 列號、實作與共通段的檢查讀這裡;規則檔只留陳述與指向這裡的一行,表格列寫回規則檔即紅。 +let zeroInstanceRulingsPath = "docs/rulings/zero-instance-guards.md" /// 量測寫法的通則從規則檔抽成的獨立規則(#711)。它引用 `selfProofTemplate`——那是第二份描述,所以守衛查兩者逐字相同。 let selfProofTemplateRulePath = ".claude/rules/measurement-commands-self-prove.md" @@ -49,7 +55,8 @@ let selfProofTemplateRulePath = ".claude/rules/measurement-commands-self-prove.m // 「線性」的常數也有界。**超過即具名紅、不判讀那一塊**——不是放行,也不是截斷了再判讀(那會把截掉的量測當成不存在)。 // 上限都取 2026-10-09 的實測值十倍以上,數字旁寫著量到多少;要提高就改這裡並寫理由。 -/// 被掃的檔(規則檔、量測文件)各自的位元組上限。2026-10-09(加入第 98–100 列之後):規則檔 181,439、量測文件 256,003 位元組。 +/// 被掃的檔(規則檔、裁決史檔、量測文件)各自的位元組上限。2026-10-09(加入第 98–100 列之後):規則檔 181,439、量測文件 256,003 位元組。 +/// 2026-10-15 搬出表格之後:規則檔 9,175、裁決史檔 180,620 位元組。 let selfProofMaxFileBytes = 4 << 20 /// 一個 fence 或一個段落的行數上限。每一個單位都在一個 fence 或段落裡(跨段落的接續至多接兩個段落),所以這兩道也是單位的上限。 /// 2026-10-09:量測文件最大的 fence 152 行,最大的段落 20 行(規則檔的表格一列一段;量法見量測文件第 99 列)。 @@ -100,9 +107,15 @@ func zeroInstanceRowsAudit() -> Int32 { + "它不在時自證閘與棘輪都沒有輸入\n").utf8)) return 1 } - // **檔案大小的上限**(#711 b37):這兩份檔 PR 改得到(`census-parity.yml` 以 `pull_request` 觸發),判讀的工作量要有界。 + // 裁決表不在不是「沒有列」:搬出規則檔之後,表格、列號與共通段的檢查都讀這份(2026-10-15) + guard let rulings = readFile(zeroInstanceRulingsPath) else { + FileHandle.standardError.write(Data(("✗ 找不到 \(zeroInstanceRulingsPath)——裁決表與「各列共通的東西」住在那裡(2026-10-15 從規則檔搬出)," + + "它不在時列號、實作與共通段的檢查都沒有輸入\n").utf8)) + return 1 + } + // **檔案大小的上限**(#711 b37):這三份檔 PR 改得到(`census-parity.yml` 以 `pull_request` 觸發),判讀的工作量要有界。 // 超過就不掃、具名紅——不是放行 - let oversized = [(rulePath, rule), (zeroInstanceMeasurementsPath, measurements)] + let oversized = [(rulePath, rule), (zeroInstanceRulingsPath, rulings), (zeroInstanceMeasurementsPath, measurements)] .filter { $0.1.utf8.count > selfProofMaxFileBytes } if !oversized.isEmpty { for (path, text) in oversized { @@ -140,8 +153,9 @@ func zeroInstanceRowsAudit() -> Int32 { // 改成整行切欄:`|` 兩側的空白一律 trim,與 markdown 的實際語意一致。 let rowRe = try! NSRegularExpression(pattern: #"^\|\s*(\d+)\s*\|(.*)$"#, options: [.anchorsMatchLines]) - let ns = rule as NSString - let rows = rowRe.matches(in: rule, range: NSRange(location: 0, length: ns.length)) + // 裁決表住在裁決史檔(2026-10-15 從規則檔搬出) + let ns = rulings as NSString + let rows = rowRe.matches(in: rulings, range: NSRange(location: 0, length: ns.length)) guard !rows.isEmpty else { FileHandle.standardError.write( Data("✗ 裁決表一列都沒讀到——抽取式與表的寫法脫節了\n".utf8)) @@ -172,6 +186,13 @@ func zeroInstanceRowsAudit() -> Int32 { let issueRe = try! NSRegularExpression(pattern: #"#(\d{2,4})"#) let present = zeroInstanceIssuePrefixes(in: src) var fails: [String] = [] + // **表格列不得寫回規則檔**(2026-10-15):規則檔每個 session 自動載入,表格搬到裁決史檔就是為了讓它保持小;寫回去的列也不會被 + // 列號與實作檢查讀到(它們只讀裁決史檔),於是兩份各有一段表、只有一段被檢查 + let strayRows = rowRe.numberOfMatches(in: rule, range: NSRange(location: 0, length: (rule as NSString).length)) + if strayRows > 0 { + fails.append("\(rulePath) 裡有 \(strayRows) 個裁決表的列——裁決表住在 \(zeroInstanceRulingsPath)(2026-10-15 從規則檔搬出,不自動載入)," + + "規則檔只留陳述與指向那裡的一行;把列搬過去") + } for m in rows { let num = ns.substring(with: m.range(at: 1)) @@ -238,8 +259,8 @@ func zeroInstanceRowsAudit() -> Int32 { // 於是擷取到的會是**量測區塊的尾巴**而不是真的段落,結果是 16 列被誤報成沒有 bullet。 // 實地踩到——加完量測腳本的下一次執行就紅了。同型的坑本 repo 記過: // `parity-table-drift` 的表格第一欄不得用反引號,因為守衛會把那些 token 當成宣稱。 - if let ci = rule.range(of: "\n## 各列共通的東西") { - let after = rule[ci.upperBound...] + if let ci = rulings.range(of: "\n## 各列共通的東西") { + let after = rulings[ci.upperBound...] let common = after.range(of: "\n## ").map { String(after[..<$0.lowerBound]) } ?? String(after) // **只認「以 `- 第 N 列的理由是` 開頭的 bullet」,不是段落裡任何一次提到 N** // (#479,第一版就是後者而它太弱)。那一段裡到處都是跨列比較——「第 4 列與 @@ -265,16 +286,17 @@ func zeroInstanceRowsAudit() -> Int32 { fails.append("找不到「## 各列共通的東西」——這一段是理由欄的第二層,不得消失") } - // **量測指令的自證閘**(#711)。規則檔與量測文件各掃一次——表格裡仍可能有量測(第 56 列),量測文件是它們主要住的地方。 + // **量測指令的自證閘**(#711)。規則檔、裁決史檔(2026-10-15)與量測文件各掃一次——表格裡仍可能有量測(第 56 列),量測文件是它們主要住的地方。 // 空掃描不是通過:量測文件裡一條都沒掃到,代表抽取式與檔的寫法脫節了。規則檔裡零條是正常的(量測本來就該住在量測文件)。 let ruleProof = selfProofIssues(in: rule, file: rulePath) + let rulingsProof = selfProofIssues(in: rulings, file: zeroInstanceRulingsPath) let docProof = selfProofIssues(in: measurements, file: zeroInstanceMeasurementsPath) if docProof.checked == 0 { fails.append("\(zeroInstanceMeasurementsPath) 裡對 binary 輸出計數的指令一條都沒掃到——" + "抽取式與檔的寫法脫節了,自證閘的檢查等於沒跑") } - fails += ruleProof.issues + docProof.issues - let gates = ruleProof.gates + docProof.gates + fails += ruleProof.issues + rulingsProof.issues + docProof.issues + let gates = ruleProof.gates + rulingsProof.gates + docProof.gates // 閘的片段的條件見 `selfProofNeedleIssues`(條數不寫在這裡——寫死的計數會與那份清單分岔,#711 R4 verify 第 18、34 列)。 // 每一道閘的片段要在 Sources 的字面段裡各找一次,工作量是條數乘上 Sources 的大小,所以條數有上限(#711 b37) let distinctGates = selfProofDistinctGates(gates) @@ -285,10 +307,10 @@ func zeroInstanceRowsAudit() -> Int32 { fails += selfProofNeedleIssues(gates: gates) } // #711 R3:條數的地板不靠辨識;R4 起依閘去重(`selfProofRatchetIssues`);標記住在量測文件 - let ratchet = selfProofRatchetIssues(rule: rule, measurements: measurements, gates: gates) + let ratchet = selfProofRatchetIssues(rule: rule, rulings: rulings, measurements: measurements, gates: gates) fails += ratchet.issues fails += selfProofTemplateCopyIssues(readFile(selfProofTemplateRulePath)) - let checked = ruleProof.checked + docProof.checked + let checked = ruleProof.checked + rulingsProof.checked + docProof.checked // 標題列印出條數與棘輪下限(R3 verify 第 7 列:條數掉了,審閱的人在輸出裡要看得到) var out = "══ zero-instance 裁決表:\(rows.count) 列;量測指令 \(checked) 條數 binary 輸出" @@ -1120,12 +1142,14 @@ func selfProofDistinctGates(_ gates: [SelfProofGate]) -> Int { Set(gates.map { $0.binary + "\u{0}" + $0.needle }).count } -func selfProofRatchetIssues(rule: String, measurements: String, gates: [SelfProofGate]) -> (issues: [String], floor: Int?) { - // 標記住在量測文件(#711):規則檔裡出現一個是搬錯了地方——兩份各一個時地板有兩個數,哪個算數沒有答案 - let inRule = matches(rule, selfProofRatchetPattern).count - guard inRule == 0 else { - return (["棘輪標記住在 \(zeroInstanceMeasurementsPath),\(zeroInstanceRulePath) 裡不得有(找到 \(inRule) 個)" - + "——量測搬到量測文件之後,地板跟著量測住"], nil) +func selfProofRatchetIssues(rule: String, rulings: String = "", measurements: String, gates: [SelfProofGate]) -> (issues: [String], floor: Int?) { + // 標記住在量測文件(#711):規則檔或裁決史檔(2026-10-15 從規則檔搬出的表格)裡出現一個是搬錯了地方——兩份各一個時地板有兩個數,哪個算數沒有答案 + for (path, text) in [(zeroInstanceRulePath, rule), (zeroInstanceRulingsPath, rulings)] { + let n = matches(text, selfProofRatchetPattern).count + guard n == 0 else { + return (["棘輪標記住在 \(zeroInstanceMeasurementsPath),\(path) 裡不得有(找到 \(n) 個)" + + "——量測搬到量測文件之後,地板跟著量測住"], nil) + } } let found = captures(measurements, selfProofRatchetPattern, multiline: true) let marks = matches(measurements, selfProofRatchetPattern) diff --git a/changelog/2026-10-15-rulings-out-of-autoload.md b/changelog/2026-10-15-rulings-out-of-autoload.md new file mode 100644 index 00000000..92411d3d --- /dev/null +++ b/changelog/2026-10-15-rulings-out-of-autoload.md @@ -0,0 +1,67 @@ +# 2026-10-15 裁決史表格搬出自動載入的規則目錄 + +## 為什麼 + +`.claude/rules/` 下的每一個檔在每個 session 開頭都會自動載入。2026-10-15 在 base `f2a321e` 量(`cat .claude/rules/*.md | wc -c`): +規則目錄共 **738,145 bytes**,加上 `CLAUDE.md` 的 42,246 bytes,一開場就是 780,391 bytes。以 UTF-8 的中文每字 3 bytes 粗估, +這是 30 萬 token 以上(依位元組推估,沒有用 tokenizer 實量)。雲端 session 因此在開始工作前就撞上 context 上限。 + +其中三份檔佔了 542,455 bytes(73%),而它們的大頭都是**裁決史**——逐列累積的歷輪補記,不是每次寫程式都需要讀的規則本身: + +| 檔 | base(bytes) | +|---|---| +| `mcp-cli-parity.md` | 281,520 | +| `zero-instance-guards.md` | 187,777 | +| `two-kinds-of-edits.md` | 73,158 | + +## 做了什麼 + +把這三份檔的裁決史表格(以及 `mcp-cli-parity` 的識別碼寫入面那一節)**逐位元組不改地**搬到 `docs/rulings/<同名>.md`(不自動載入)。 +規則檔留下規則本身、各節標題與指向新位置的一句話;新增一列的地方改成那份檔。表格的內容一個字都沒改—— +搬移以腳本切出原文段落、寫到新檔,再比對「新檔的段落=原檔被移除的段落」。 + +| | base(bytes) | 之後(bytes) | +|---|---|---| +| `.claude/rules/` 合計 | 738,145 | 235,377 | +| `CLAUDE.md` | 42,246 | 42,381 | +| **自動載入合計** | **780,391** | **277,758** | +| `mcp-cli-parity.md`(規則檔) | 281,520 | 23,893 | +| `zero-instance-guards.md`(規則檔) | 187,777 | 9,175 | +| `two-kinds-of-edits.md`(規則檔) | 73,158 | 6,463 | +| `docs/rulings/mcp-cli-parity.md` | — | 259,453 | +| `docs/rulings/zero-instance-guards.md` | — | 180,620 | +| `docs/rulings/two-kinds-of-edits.md` | — | 67,915 | + +`CLAUDE.md` 只改了一句:`zero-instance-guards` 那一列原本說「規則檔每一列只留核心」,搬移之後規則檔裡已經沒有列,那句話為假, +改成指向 `docs/rulings/`。其餘不動。 + +## 守衛(一個都沒有放寬) + +讀這些表格的守衛改成讀新位置;舊位置出現表格內容時守衛照樣紅: + +| 檔 | 改了什麼、為什麼 | +|---|---| +| `ZeroInstanceRowsAudit.swift` | 裁決表的列、「各列共通的東西」與自證閘改從 `docs/rulings/zero-instance-guards.md` 讀;規則檔裡又出現表列即紅(防表格分成兩半);棘輪標記在規則檔與裁決史檔都不得出現;檔案上限的檢查也涵蓋裁決史檔;裁決史檔不在時中止,不當成「零列」 | +| `ParityTableDrift.swift` | 把規則檔與 `docs/rulings/mcp-cli-parity.md` 接成一份再抽表;裁決史檔讀不到時紅 | +| `MeasuredNumbersAudit.swift` | 掃描來源加入 `docs/rulings/*.md`(數字搬家之後仍要有時間錨) | +| `ProtectedInventory.swift` | `docs/rulings/*.md` 進受保護清單 | +| `.githooks/protected-ratchet.txt` | 對應加入三個檔(棘輪要求與清單逐字相等) | +| `.github/workflows/census-parity.yml` | `paths` 加 `docs/rulings/**`,讓改裁決史檔的 push 與 PR 會跑讀它們的守衛(trigger-coverage 的逐對要求) | +| `AuditGuardsMutationsData.swift` | 負控的注入點改指向表格的新位置(每一個 needle 在新位置恰好找到一次,`replaceAll` 的命中數與 base 相同);新增三格負控:裁決史檔被刪、一列表格被放回規則檔、棘輪標記被注入規則檔 | +| `AuditGuardsMutations.swift`、`TriggerCoverageMutations.swift`、`PluginRootsMutations.swift` | 負控的暫存副本也複製 `docs/rulings/`,否則被測的守衛在副本裡讀不到表 | + +## 驗證與誠實邊界 + +這一輪的環境沒有 Swift toolchain,**沒有編譯、沒有跑 `swift test`、沒有跑任何 `akashic-guards` 子命令**。做過的是以 python 照守衛的邏輯模擬: + +- 負控的每一個 needle 在新目標檔裡找得到(51 個字面 needle,0 個找不到;`replaceAll` 的命中數與 base 相同) +- parity:`Server.swift` 的 35 個 MCP tool 與表格第一欄零差集;表格第一欄的 token 集合與 base 相同(65 個) +- zero-instance-rows-audit:102 列連續、表頭列數相符、規則檔 0 列、「共通」段引用全部列、棘輪標記只在量測文件 +- measured-numbers-audit:23 個來源檔,沒有時間錨的「實測 N」0 個 +- trigger-coverage:三個新檔與讀它們的守衛逐對有 workflow 覆蓋(缺口 0);trigger-coverage 負控的 needle 與 base 相同 +- 守衛引用的 `docs/rulings/` 路徑全部存在 + +模擬不是編譯器:Swift 端的型別錯誤、`copyItem` 的行為與實際輸出字串只能交給 macOS CI 的 `census-parity.yml` 確認。 + +程式碼註解裡仍有三處寫「見 `.claude/rules/zero-instance-guards.md` 第 N 列」(`VenueBootstrap.swift`、`Organization.swift`、 +`VenueBootstrapTests.swift`)——那份規則檔指向新位置,這次沒有改註解。 diff --git a/docs/rulings/mcp-cli-parity.md b/docs/rulings/mcp-cli-parity.md new file mode 100644 index 00000000..4ff9be15 --- /dev/null +++ b/docs/rulings/mcp-cli-parity.md @@ -0,0 +1,244 @@ +# mcp-cli-parity:裁決表 + +從 [`.claude/rules/mcp-cli-parity.md`](../../.claude/rules/mcp-cli-parity.md) 搬出(2026-10-15)。規則檔每個 session 自動載入,而搬移前 `.claude/rules/` 底下的檔合計 738,145 位元組,雲端 session 一開場就被它們撐爆 context;所以把只有查過往裁決、或新增一列時才需要讀的段落搬到這裡——`docs/rulings/` 不自動載入。 + +搬移是逐位元組的:下面各段與搬移前規則檔裡的那幾段一字不差,新增的只有本段說明與分隔用的標題。所以讀的時候: + +- 文中的「本檔」指規則檔;「上表」「下表」照搬移前的順序讀(表原本住在規則檔裡)。 +- 文中指向同一個目錄的相對連結是從 `.claude/rules/` 解析的,在這裡會點不開。 + +`parity-table-drift` 把這份檔與規則檔合起來讀;新增工具或命令時在這裡加列。 + +## 裁決史(封閉列舉——現有 35 工具,一格不多一格不少) + +| MCP 工具 | CLI 對應 | 裁決 | +|---|---|---| +| `akashic_search` | `query`(filter flags)| ✅ 功能重疊 | +| `akashic_relations` | `query`(relation flags)| ✅ 功能重疊 | +| `akashic_graph` | `graph` | ✅ | +| `akashic_export` | `export-bib`/`export-tables` | ✅ 功能重疊(tables 面與 #274 的 `--view` 為 **CLI-only**——正式裁決見 CLI-only 表的 `export-tables --view` 列,#259)| +| `akashic_doctor` | `doctor` | ✅ **#581 重新確認,裁決不變、契約有改**:MCP 多一個 `owner` 參數(`:`,kind 是 work/person/organization/venue/divergence 之一、必填),帶它時回**那一筆記錄的完整 per-record 明細**——不套組合式六族每筆 20 則的列出上限,近重複的組內 5,000 對與整筆 100,000 對求值上限照舊(那是 CPU 的界限,觸頂時訊息自己會說);輸出受 `candidateByteBudget`(48 KiB)約束、每則截 1,000 字元,以 `total`/`truncated` 揭露;唯讀,不重建 index、不含跨記錄檢查與 quarantine。**CLI 面是 `validate --owner`,不是 `doctor --owner`**:CLI 的 `doctor` 本來就不印 per-record 家族(它們住在 `validate`),出口放在印那些家族的命令上,使用者在看到概括句的同一份輸出旁邊就找得到它。兩面走同一個 `perRecordIssues(from:owner:)`、同一套定址(`RecordAddress.parse`)與拒絕:kind 缺漏或不在值域、key 不合 StoreKey(divergence 要 UUID)、找不到、同 kind 同 key 兩筆以上——都拒絕、不猜(kind 必填的理由是跨 kind 同 key 真的存在:2026-09-28 實測 live store organization 與 venue 共用 2 個 key)。**有記錄的差異**:CLI 逐行不截、沒有位元組預算;MCP 受位元組預算——理由同 CLI-only 表 `validate` 列的面級截斷分工(輸出進 LLM context,呼叫端無法在收到後丟棄已付的代價) **#609 重新確認,裁決不變、契約有改**:兩面多一項「附加來源已刪除、主連結仍在」的 entry(MCP `orphanedAdditionalSources`、CLI `orphaned additional sources:` 一行),與 `orphaned` 不相交;`orphaned` 的判準改為 `Entry.zoteroLinkState`——沒有主來源而附加來源全部已刪除也算(先前兩面都看不見)。CLI `doctor` 的 orphaned 一行改讀 `health`,不再自己推導。處置在 App 裁決台(CLI/MCP 沒有 orphan 裁決面,與 #605 之前相同)。 **#609 R1 verify 補缺席理由**:這一格是**候補缺席**——處置是判定型動作(Zotero 端的刪除可能是暫時的),要人看過清單、逐筆確認「這一組」之後才拿掉,並要求記錄檔已 commit;目前沒有 LLM 消費面的流程需要它(doctor 只列出),需求出現即重啟裁決。 **#703 R1 重新確認,裁決不變、契約有改**:`sources` 一致性多一類——殘留的暫存檔(`.<62 hex>.incoming-`,存檔在複製途中被中斷留下的)。CLI 逐行 ⚠ 路徑與大小(至多 20 行,其餘一句概括),MCP `sources.strayTemporaryFiles`(至多 20 筆,每筆路徑與 bytes)加 `strayTemporaryFilesTotal`;兩面與 App 讀同一個 `auditSourceIndex`。只報不刪——正在進行的存檔也長這樣。 **#703 R2 重新確認,裁決不變、契約有改**:`sources` 再多一類——位址問題(位址上是目錄、symlink、特殊檔案,或普通檔的大小與 index 那一列的 `bytes` 不同):CLI 逐行 ⚠(至多 20 行),MCP `sources.occupantProblems`(每筆 `path`、`kind`,至多 20 筆)加 `occupantProblemsTotal`;殘留暫存檔每筆多最後修改的時間(CLI「最後修改於 N 秒前」、MCP `ageSeconds` 與 `possiblyInProgress`),一小時內還在動的不計入 `hasFindings`(App 的健康區塊不因它亮起)。三面同一個 `auditSourceIndex`、同一個 `sourceOccupant` 分類。 **b26 F6 重新確認,裁決不變、契約有改**:殘留暫存檔的修改時間在未來超過 300 秒(時鐘被往回撥、備份還原、跨時區的 FAT 卷)時不信——MCP `ageSeconds` 是 null(與「讀不到」同一個值)、~~`possiblyInProgress` 是 false~~、算舊;CLI 說「修改時間在未來」而不是「0 秒前…不要刪」。 **b29 V5 重新確認,裁決不變、契約有改**:時間不可信(`ageSeconds` 是 null)時 MCP `possiblyInProgress` 是 **true**——「算舊」只管 `hasFindings` 與 App 計數(要人看一眼),「可不可以刪」的疑問往不刪的方向;CLI 對同一個檔不再說「中斷的存檔留下的——…可以刪掉」,改說判不出是中斷留下的還是正在進行。兩面同一個 `StrayTemporaryFile.possiblyInProgress`。說明補 `orphanBlobs`/`danglingIndexEntries`/`malformedIndexLines`/`unreadableShards`(b26 F6 以位元組預算為由沒寫)與「`ageSeconds` null=判不出」,見 `changelog/2026-10-02-b29-h5-fixes-703-700.md`。位址問題的訊息兩面都說兩邊(存檔被截短、接長或換掉:移走後重存;index 那一列的 `bytes` 記錯:重存不會改既有的列,要手改 `index.jsonl`)。`akashic_doctor` 的說明把 R2 新增的兩個陣列(`occupantProblems[]`、`strayTemporaryFiles[]`)與它們的元素鍵寫齊(兩個 `*Total` 以速記涵蓋;`tools/list` 的位元組預算要求這一輪淨增 ≤ 0),由 `SourceAddressServiceTests.testTheDoctorDescriptionNamesEveryKeyOfTheSourcesPayload` 釘住——`sources{}` 在 `ToolPayloadNestedPaths` 之外(使用者裁決 (a)),守衛看不到這一層。 **#700 b31 W5 重新確認,裁決不變**:CLI `doctor` 經 `ensureLayout` 建佈局,`.gitignore` 沒有 sources 標記區塊時只在尾端附加位元組;讀不懂的(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)不改寫,改印一則 warning(附要自己加的那段)、結束碼不變——先前整份換成只含區塊。MCP `akashic_doctor` 不建佈局、不碰 `.gitignore`,這一格沒有對應。 **#700 b33 verify 重新確認,裁決不變、契約有改**:同時跑的另一個 akashic 剛加好區塊時不再報假 warning(開檔後取 `flock`、鎖內重讀內容);不改寫的再加四類(symlink 打不開、有其他硬連結、大到 git 不讀、有標記沒有排除規則);寫到一半截不回來時 warning 說「寫到一半、沒能收回」。CLI `doctor --help` 自此寫出 `.gitignore` 的行為。 **#700 b36 verify 重新確認,裁決不變、契約有改**:「區塊已在」改成與 git 的判讀一致——規則行照 git 的切法比(行首帶空白或 tab、行尾帶 tab 的不算,先前剝掉後判成已在、不出聲);`.gitignore` 是 symlink 時不論指向的內容有沒有區塊都報(git 2.32 起不讀 symlink 的 `.gitignore`,先前指向的內容有區塊就判成已在);等鎖有上限(10 秒,逾時報 warning、不讀不寫);warning 首句改成「沒有生效的 sources 排除區塊」,處置不再對已有區塊的檔叫人再加一份。 | +| `akashic_files` | `file` | ✅ | +| `akashic_libraries` | `library` | ✅(#455 起 add/remove 兩面同走 `setMembership`:CLI 收多 citekey 是批次形、MCP 維持單 citekey——批次面的裁決見 CLI-only 表) **#628 重新確認,裁決不變、契約有改**:add/remove 指名的 citekey 重複或與另一筆 work 共用 id 時兩面都整批拒絕。 **#641 重新確認,裁決不變、契約有改**:「無法唯一定位」自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),併進同一個 `unlocatableCitekeys`,兩面照舊整批拒絕;訊息共用 `UnlocatableReason` 一句話。 **#642 重新確認,裁決不變、契約有改**(使用者 2026-09-28 照提案定案):library 多一個成員性質(`topic`/`rule`/`document`,`Library.membership`),兩面同時新增兩個 action——`set-kind`(CLI `library set-kind`,標性質與規則)與 `check`(CLI `library check`,列出不符規則的成員,唯讀);`create` 自此**要求性質**(CLI `--kind`、MCP `kind`,規則指涉的 venue/文件要在庫);`list` 帶性質、規則與不符數。**add 對規則型/文件型逐筆比對、不符的不寫並具名**(不是拒絕整批、也不是同意後照寫——Clarity row 1:改成正確的再寫),**未標性質的 library 拒絕 add、零寫入**(查不到依據就不寫),remove 不查依據。兩面同一條 service 路徑(`parseLibraryMembership`/`setMembership`/`libraryViolations`),判定只有 `LibraryMembershipCheck` 一份,App 的加入動作(第三面)也問它。**有記錄的差異**:CLI `library add` 全部不符時非零結束,MCP 照常回成功、由呼叫端讀 `written`/`skipped`(#624 全數排除的同形);CLI `library check` 不截,MCP `check` 受 `candidateByteBudget`、以 `total`/`truncated` 揭露(#388 的分工);CLI `set-kind` 全列現有不符的成員,MCP `set-kind` 列前 20 筆加計數。 **#642 R1 verify 重新確認,裁決不變、契約有改**:(a) `set-kind` 是整值替換,兩面的回應都回顯**先前**的成員性質(CLI 印完整的舊規則含每一個排除的 citekey;MCP 回 `previous`/`previousBasis`,`excluded` 至多 50 筆加 `excludedTotal`——同 `membershipPayload` 的既有上限);替換一條**既有**的性質時要求 registry 檔 tracked 且 clean(同 #573 一族的閘,`LibraryStore.recoverabilityRefusal` 涵蓋 `libraries/.yaml`),從未標性質標成任何一種不需要;重送相同的值不需要。**「放寬規則要明確確認」不做**(列為待使用者決定)。(b) 規則指涉的東西要在庫**含排除清單的 citekey**(create 與 set-kind 兩面同一個 `requireMembershipReferents`)。(c) 規則的 venue key 有不只一筆記錄時 add 三面都不寫(`LibraryMembershipViolation.ruleVenueAmbiguous`,判定只有 `LibraryMembershipCheck` 一份,`violation(of:)` 對未標性質也 fail-closed 回 `.libraryUnmarked`);`check` 兩面都揭露依據問題(CLI 印 ⚠、MCP 回 `basisProblem`),零成員的 library 也看得到。 **有記錄的差異**:CLI `library check` 印**完整**規則(含全部 excluded),MCP 的 `membership` 載荷仍截 50 筆——輸出進 LLM context 的既有分工(#388)。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_import_zotero` | `import-zotero` | ✅(#658 起 CLI 面過 #298 的目標確認閘——沒有乾跑,所以無條件閘、拒絕在建佈局之前;MCP 面不閘,理由見橫切選項表的 `--yes` 列:active store 是 session 狀態) **#607/#610 重新確認,裁決不變、契約有改**:比對順序改成主來源 → 附加來源 → legacy 裸 key,裸 key 的「已被其他 library 持有」看附加來源;同一個 `(library_id, zotero_key)` 被兩筆以上 entry 宣稱(兩筆以上 legacy 同裸 key 亦同)時不更新任何一筆、不新建,報告多一個 `ambiguousSourceClaims`(MCP 只在非空時出現,CLI 逐行印)。兩面同一個 `ZoteroImporter`。 **#607/#610 R1 verify 再確認,裁決不變、契約有改**:(a) 兩筆以上 legacy 同裸 key 的判斷先於「其他 library 已持有」,別的 library 也持有同一個裸 key 時歧義照樣被報、條目照樣不新建;(b) 宣稱者的定義只有 `ZoteroSourceClaims` 一份(含 `?:<裸 key>` 桶),所以兩種鍵在載入時都由 `validate`/`akashic_doctor` 的跨記錄檢查報 warning,CLI 標題行指向的警告真的存在;(c) MCP 工具描述點名 `ambiguousSourceClaims`(只在非空時出現,不讀描述的呼叫端沒有理由去檢查它);index rebuild 失敗時錯誤訊息帶成功時同一份 payload(先前只帶四個計數,歧義被吞)。 ~~有記錄的差異:`ambiguousSourceClaims` 兩面都沒有筆數上限(鍵數受本機 Zotero 條目數限制、值是宣稱者的 citekeys)——與相鄰的 `authorsPreserved`、`quarantineConflicts` 同慣例,MCP 面沒有 `enrich` 那種 `total`/`truncated` 揭露;一個大量 entry 宣稱同一來源的 store 會讓單次回應膨脹,出現時比照 D30 重開。~~ → **#684(2026-09-29)重開並裁決:MCP 面加上限、CLI 維持全列**(見本列末段;`authorsPreserved`、`quarantineConflicts` 與其他清單維持原慣例,不在這次範圍——#696 補上,見本列末段)。 **#682 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):被多筆 entry 宣稱的來源,本趟仍**不更新書目欄位、不動 version/hash、不新建**,但 orphan 標記照常處理——Zotero 端復原時各宣稱者身上這個來源的標記都清(主來源的進 `orphanCleared`、附加來源的進 `secondarySourceRestored`,只寫標記),刪除時各宣稱者本來就都被標(偵測迴圈逐筆看每一筆自己的來源,pin 在 `ZoteroSourceRoutingTests`);`ambiguousSourceClaims` 照舊列出。先前連標記的清除一起略過,App 的 Orphans 頁會把復原的 item 列成「已刪除」,而那一頁的動作是破壞性的(移到垃圾桶、脫鉤)。舊檔那一桶(`?:`)只在歸屬沒有爭議(沒有別的 library 持有同一個裸 key)時才清,與單一舊檔的認領同一條件。 **#684 重新確認,裁決不變、契約有改**:MCP 的 `ambiguousSourceClaims` 至多 20 個來源、每個來源至多 20 個宣稱者(依來源鍵、citekey 排序留前面的),並多兩個鍵 `ambiguousSourceClaimsTotal`(完整的來源數)與 `ambiguousSourceClaimsTruncated`(來源被截或任一來源的宣稱者被截即 true),三個鍵同進同出——沒有歧義時都不出現;index rebuild 失敗時錯誤訊息裡的那份報告同一個上限。**CLI 逐行全列**(有記錄的兩面差異,理由同 `akashic_enrich` 的 items 上限與 #388 的 `--rows`:MCP 的輸出進 LLM context、呼叫端無法在收到後丟棄已付的代價,CLI 的輸出進人的終端機、可捲可 pipe)。上限是 service 的 `claimLimit` 參數、由 server 傳入(`AkashicMCPServer.ambiguousClaimsLimit`),沒給=全列;`StdioE2ETests.testImportZoteroCapsTheAmbiguousClaimsAtTheServer` 釘住 server 有傳。 **#608 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):報告多一格 `secondarySourceHashOnly`(MCP payload 與 CLI 一行,與 `secondarySourceChanged` 同形)——附加來源的 hash 與存下的不同、**而 Zotero 的 `version` 沒動**~~(沒有人改這個條目;變的是 hash 的算法或子項附件集合,報告不宣稱原因)~~ → (見本列末段 #608 verify R1:判準改成 Zotero 那一列已同步、`version` > 0 且沒動,報告不宣稱原因);這種情形先前與「內容真的變了」報在同一格(`secondarySourceChanged`),mapping 定義一演進每個附加來源都被列成「有人改了」。`version` 前進或倒退而 hash 不同仍是 `secondarySourceChanged`(倒退時不對沒把握的事下結論)。不改 store 存的任何東西。 **#608 verify R1/#694/#696 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) #608 verify R1:「只有 hash 不同」的判準改成**有舊 hash 且不同、而 Zotero 那一列已同步(`items.synced` = 1)、`version` > 0 且與存下的相同**(`ZoteroImporter.isHashOnlyDifference`,一份判準)。初版只看 `version` 沒動,並把那一格說成「沒有人改這個條目」——但 `version` 只在同步時才變:還沒同步的本機修改(`synced` = 0)與從未同步的 library(`version` = 0)是內容變動,初版把它們報成只有 hash 不同。現在這兩種與「資料庫沒有 `synced` 欄」都回到 `secondarySourceChanged`;兩面的說明只說觀察到的事實、~~不宣稱原因(多半是 mapping 定義演進,或子項附件增減)~~ → 可能的原因(mapping 定義改了、子項附件增減)並列、不排序(R1 verify:「不宣稱原因」接著「多半是」自相矛盾)。(b) #694:主來源多一格 `updatedHashOnly`(同一份判準;MCP payload 永遠在,與 `updated` 同形;CLI 非空時另印一行),不列在 `updated`。**寫入不變**:兩格走同一段改寫、同一次寫入,分格只改報告(`ZoteroImportTests.testWriteIsIdenticalWhicheverBucketTheUpdateLandsIn` 比對兩格寫出的檔逐位元組相同)。(c) #696:MCP 其餘 citekey 清單——`created`、`updated`、`updatedHashOnly`、`orphaned`、`orphanCleared`、四個 `secondarySource*`、`unnormalizedDates`、`authorsPreserved`、~~`quarantineConflicts`、`writeFailed`(截的是條目數)~~(R1 verify 起不截,見本列末段)——各至多 20 筆(依 citekey 排序留前面的);揭露是**一對鍵**:`listTotals`(清單名 → 完整筆數,十三個名字都在、空的是 0)與 `truncatedLists`(被截的清單名;沒有截就是空陣列),兩個鍵永遠在。~~形照 `akashic_enrich` 的 `counts`(封閉列舉的名字 → 完整筆數)加截斷揭露~~(R1 verify:這個先例只截明細、不截失敗清單,對 `writeFailed`/`quarantineConflicts` 是過度宣稱);**不**逐清單加 `…Total`/`…Truncated`——十三對鍵名要寫進工具描述,會吃掉 tools/list 的位元組預算(#578)。`ambiguousSourceClaims` 維持 #684 自己的鍵。計數(`unchanged`、`skippedLinkedAttachments`)照舊完整;index rebuild 失敗時錯誤訊息裡的報告同一個上限;上限只截報告、不截寫入。上限是 service 的 `listLimit` 參數、由 server 傳入(`AkashicMCPServer.importListLimit`),沒給=全列;`StdioE2ETests.testImportZoteroCapsEveryListAtTheServer` 釘住 server 有傳,`ImportZoteroReportSurfaceTests.testEveryReportCollectionIsCappedOrNamed` 讓 `ImportReport` 新長出的集合欄位不是有上限就是具名寫理由。`residualFields` 不截(鍵是 Zotero 的欄位名,筆數受 schema 的欄位表限制、不隨一次匯入的筆數成長)。**有記錄的兩面差異**:MCP 截、CLI 不受此上限(理由同 #684:MCP 的輸出進 LLM context)。但 CLI 本來就不是每一格都全列——`created` 只印筆數、`unnormalizedDates` 只印前 8 筆,這是 #696 之前就有的差異,本次未改。 **#608/#694/#696 R1 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) MCP payload 多兩格 `authorsOverwritten`(pull 覆寫的未歸戶作者,citekeys;有上限、進 `listTotals`)與 `fieldsRemovedByPull`(被整份替換拿掉的欄位名 → 次數;不截,同 `residualFields`),都只在非空時出現——先前只有 CLI 印,而 `updatedHashOnly` 那一格與 `updated` 同一段整份替換,讀起來無害的分類底下照樣拿掉手加的欄位、覆寫 literal 作者(`lossless-intake`:丟棄必須可見,兩面都要看得到);(b) **失敗清單不截**:`writeFailed`、`quarantineConflicts` 全列、只在非空時出現、不在 `listTotals`——沒寫進去的記錄在 store 裡沒有痕跡、原因只在這份報告,重跑是再寫一次、不是重播,截掉的就拿不回來;(c) 仍截的清單描述的都是**寫進去了**的記錄:結果在 store 裡,這一趟改了哪些檔在 store 的 git diff 裡看得到——截掉的成員從那裡找得回來,只有落在哪一格(例如 `updated` 還是 `updatedHashOnly`)找不回來(不加分頁、不存完整報告)。~~`authorsPreserved`/`authorsOverwritten`/`fieldsRemovedByPull` 在寫入之前就記下,沒寫進去的那一筆也在不截的 `writeFailed` 或 `quarantineConflicts` 裡~~(#702 起改為寫入成功後才記,見本列末段);(d) index rebuild 失敗時錯誤訊息改說「報告如下——清單有上限」(先前說「完整報告」,而清單已截);(e) CLI 的只有 hash 不同那兩行列出可能的原因、不排序,主來源那一行指向 `authors overwritten`/`fields removed by pull`。 **#702 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):主來源更新分支的 `authorsPreserved`、`authorsOverwritten`、`fieldsRemovedByPull` 改在寫入成功之後才記。先前在寫入之前就記,目的檔被隔離或寫入擲錯的那一筆同時出現在失敗清單與「已覆寫/已拿掉」清單,而那一筆沒有寫。現在它只在 `quarantineConflicts` 或 `writeFailed` 裡。附加來源、新建、orphan 各分支本來就在寫入之後才記,不變。R1 verify 補一個例外:#631 的搬移寫完之後刪 legacy 檔失敗(`StoreIOError.legacyCopyNotRemoved`)時那一筆**寫了**,照常記在各清單,`writeFailed` 的訊息以「已寫入」開頭、說明留下兩份;兩面同一個 `ZoteroImporter`,CLI 那一行的標題不再說「已略過」。 **#705 重新確認,裁決不變、契約有改**:上一句的例外改掉——那一筆不再進 `writeFailed`,改記在成功那一側的 `writtenWithLegacyCopy`(`ZoteroImporter.run` 自己的收集範圍收下,報告欄位 `ImportReport.writtenWithLegacyCopy`;MCP payload 只在非空時出現、不截),照常記在各清單;CLI 那一行的標題回到「已略過續跑」,這幾筆另印一段、排在 index rebuild 之前。同一趟稍早一步已寫入、之後的步驟被 #631 拒絕的那一筆(#702 R2 verify),`writeFailed` 那一則說出前一步寫了;同一筆多則以「;」串接、不覆寫。兩面同一個 `ZoteroImporter`。 **#611 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決「照建,並自動記一筆歧異提名」;兩面同一個 `ZoteroImporter`):這一趟新建的 work 與另一筆 work(既有的,或同一趟也新建的)共用 DOI(`canonicalDOIs` 的正規形)時照建,並寫一筆 work 形狀、沒有判斷的歧異記錄——候選是那一對,一對一筆——交給 `resolve-divergence` 判定;DOI 相等只是提名。只在新建時觸發:再匯入不新建就不提名,被 `dismiss-divergence` 放棄的一對不會被重記。已有涵蓋這一對的 work 歧異記錄(同一組或更大的一組)不重寫;無法唯一定位的候選(`unlocatableCitekeys`,加上這一趟寫入後留下 legacy 拷貝的)不點名。報告多一格 `doiNominations`,每列 `created`/`other`/`dois`/`status`/`divergence`/`error`,status 封閉~~四值 recorded/alreadyRecorded/unlocatable/failed~~ → 五值(R1 verify 加 groupTooLarge,見下)。MCP 只在非空時出現;~~recorded/alreadyRecorded 兩種列受 `listLimit`(歧異記錄在 store 裡、`akashic_divergences` 列得出來),unlocatable/failed 不截(同失敗清單)~~ → 兩種列各自受 `listLimit`(見下);`listTotals` 永遠有這一格;index rebuild 失敗時錯誤訊息裡的報告同一份。CLI 逐行全列。~~寫不進去的(legacy 佈局、store format < 5)報 failed、不改變結束狀態(兩筆都已照建,`akashic validate` 的跨記錄 DOI 警告照樣報這一對)~~ → 結束狀態見下。工具描述加一句,`tools/list` 51,997 → 52,376 bytes(預算 54,000——當時的預算;現行預算見〈工具描述有位元組預算〉)。 **#611 R1 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`):(a) status 多第五值 `groupTooLarge`:一個 DOI 被超過 10 筆 work 共用(這一趟新建的加上其餘的)時**一對都不記**,改報一列(一個 DOI 一列、`dois` 是那一個 DOI、帶 `groupSize`、不帶 `other`、`created` 是這一趟新建的 citekey 最小的一筆);一對一筆在 k 筆共用時寫 C(k,2) 筆記錄(DA 實測 k=80 → 3,160 筆、10.2 秒;修後 0 筆、0.25 秒),一個 DOI 被這麼多筆共用幾乎必是書或資料集的概念 DOI。沒有新建的 work 的組不觸發;一對同時共用另一個沒超過門檻的 DOI 時仍由那個 DOI 提名。(b) MCP 的兩種列**各自**受 `listLimit`:歧異記錄在 store 裡的(recorded/alreadyRecorded)與沒記下來的(unlocatable/failed/groupTooLarge)各留前 N 列(先前後三種不截,legacy 佈局上一個大群組會回出數十萬列);`listTotals["doiNominations"]` 仍是全部列數。(c) **沒記下來的提名重新匯入不會再提名**(只在新建時觸發):MCP payload 多一個鍵 `doiNominationsUnrecorded`(沒記下來的列數,永遠完整、非零才出現),CLI 印摘要行並**以非零結束**(與 `writeFailed` 同一條慣例;先前寫「不改變結束狀態」——那讓重新匯入救不回的一對除了 `validate` 的 DOI 警告之外沒有任何出聲);處置是先處理各列說的原因、再用 `record-divergence`/`akashic_record_divergence` 手記。**有記錄的兩面差異**:MCP 不以錯誤結束、呼叫端讀 `doiNominationsUnrecorded`(同 #624 全數排除的同形差異)。(d) 記錄路徑(`recordDivergence`)的兩個改動影響 `record-divergence`/`akashic_record_divergence` 兩面:同一個 id 的既有記錄若候選(形狀與 key)不同——歧異記錄的 id 只由候選 key 決定、不含形狀,所以 person 的 `{a, b}` 與 work 的 `{a, b}` 是同一個檔——**拒絕覆寫**(~~`destinationHoldsAnotherRecord`~~ → R2 起是專用的 `divergenceIdHeldByOtherCandidates`,見 R2 段;先前互相覆寫、不出聲);既有判斷與 prefers 的保護改為**寫入當下重新讀磁碟上那一個檔**,不看呼叫端的快照(匯入可能跑數分鐘)。`recordDivergence(…against:)` 收的是建一次的 `DivergenceCandidatePool`(候選存在的 key 集合),不再是整份 `LibraryLoad`。本輪 `tools/list` 53,403 → 53,772 bytes(真 binary,預算 54,000——當時的預算,現行 60,000)。 **#611 R2 verify 重新確認,裁決不變、契約有改**(兩面同一個 `ZoteroImporter`;`record-divergence`/`akashic_record_divergence` 同走記錄路徑,見那一列):(e) 同一個 id 被另一組候選占著的拒絕改用專用的 `StoreIOError.divergenceIdHeldByOtherCandidates`:訊息點名現有那一筆的候選、說原因是 key 不同(`rename` 就地改寫候選而不重算 id)還是只有形狀不同、出路是 `akashic divergences`/`resolve-divergence`/`dismiss-divergence`——先前借用 #631 的泛用拒絕,把原因一律說成形狀、出路叫人「看那個檔、修好、移走」(那是給被 quarantine 的記錄的),而 `record-divergence` 撞同一個拒絕、指它手記是循環。(f) 提名寫入被拒時**再讀一次磁碟**:那一對若已被別的程序記下(可能帶判斷),報 `alreadyRecorded`、不報 `failed`——匯入要確認它在、不是重錄它,記錄路徑對「無判斷的重錄」的拒絕(#133/#159)對匯入沒有意義,報 failed 會讓 CLI 以 1 結束、叫人手記已存在的記錄;涵蓋判斷也改成**每一個**候選都是 work(先前只看第一個候選的形狀)。(g) CLI 摘要行的 N 只數真的記下的對,沒記下的另標「(另有 M 對沒記下)」,不再把它們數進「照建,並記一筆…」。(h) MCP `doiNominations` 的排序鍵含 DOI(`groupTooLarge` 的 `other` 恆為空,兩個過大群組 `created` 相同時順序不再取決於排序的穩定性)。(i) **`akashic_import_zotero` 的說明還原**:f96e922a 為了擠進當時的 54,000 預算修剪的承重語意(`groupSize` 是共用的 work 數、`groupTooLarge` 列不帶 `other`、一對都不記、DOI 相等只是提名、recorded/alreadyRecorded 與沒記下的各是哪幾種、`updatedHashOnly` 的「Zotero 已同步」與「不宣稱原因」)全部補回——預算隨後調到 60,000,修剪卻留著;`StdioE2ETests.testImportZoteroDescriptionStatesTheDOINominationAndHashOnlyContracts` 釘住這八句。**沒有改的、要使用者確認的取捨**:`groupTooLarge`(刻意的不提名,不是寫入失敗)仍讓 CLI 以非零結束,已有 >10 筆共用 DOI 的 store 之後每新建一個該群組的成員那一次匯入就以 1 結束——見 `zero-instance-guards` 第 80 列;另一個同形的角落:這一趟寫入後留下 legacy 拷貝的 work(#705 的成功那一側)若與新建的 work 共用 DOI,提名報 `unlocatable`、同樣讓 CLI 以 1 結束,兩者都等使用者確認。 **#700 b31 W5 重新確認,裁決不變、契約有改**:兩面都經 `ensureLayout`,`.gitignore` 讀不懂時(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)在建立任何東西之前具名拒絕、`.gitignore` 不動,訊息附上要自己加的那段——先前整份換成只含區塊、使用者原有的規則全部消失;有區塊或可附加時照常。 **#700 b33 verify 重新確認,裁決不變、契約有改**:同時跑的另一個程序剛加好區塊時不再假拒絕;拒絕的原因多四類(同 `akashic_doctor` 那一格);開檔、取鎖、寫入時才發現的原因在佈局建好之後擲出,訊息說「佈局已建好」。~~**有記錄的缺席**:MCP 說明不寫這些拒絕(位元組預算;錯誤訊息自己說出原因並附要加的那段),CLI `import-zotero --help` 寫出。~~ **#700 b35 重新確認(使用者 2026-10-05 裁決第 1 項),裁決不變、契約有改**:讀不懂的 `.gitignore` 不再擋匯入——兩面都經 `ensureLayout(sourcesIgnore: .report)`,`.gitignore` 不動、照常匯入;CLI 在讀 zotero.sqlite 之前把 warning 印到 stderr(stdout 是匯入報告、結束碼不因此改變),MCP 回應帶 `gitignoreWarning`(只在有問題時出現;成功回應與 index rebuild 失敗時錯誤訊息裡的報告都帶)。兩面與 `doctor` 同一份文字(`SourcesIgnoreProblem.warningLines`,多一句匯入照常完成、防線在 store-source)。匯入本身不寫 `sources/`,擋存檔進版控的防線在 `store-source`(store 在 git 裡時以 git 驗排除、未生效拒寫)。上面那些拒絕自此只剩 `file add`。**有記錄的缺席**:MCP 說明不寫 `gitignoreWarning`(位元組預算;值自己說出原因並附要加的那段),payload 鍵守衛以 advisory 具名豁免;CLI `import-zotero --help` 寫出。**誠實邊界**:MCP 的匯入在 `.gitignore` 那一步之後自己擲錯(zotero.sqlite 讀不出來之類)時,回應只有那個錯誤、沒有這則 warning——佈局已建好、`.gitignore` 沒動,下一次匯入會再說。 **#700 b36 verify 重新確認,裁決不變、契約有改**:判讀同 `akashic_doctor` 那一格的 b36 補記(規則行照 git 的切法、symlink 一律報、等鎖有上限);warning 那一句改成「匯入不因此中止」(先前「匯入照常完成」——CLI 在讀 zotero.sqlite 之前就印它,之後失敗時兩句矛盾)、處置的「之後怎麼再跑」改成「跑 akashic doctor(或重跑匯入)」,兩面同一份文字(`SourcesIgnoreProblem.importRerun`)。 | +| `akashic_import_wos` | `import-wos` | ✅(#290;#259 CLI-only 盤點唯一「需要」格的補齊——#206 鏡像判準) **#641 重新確認,裁決不變、契約有改**:回填(只多不少)遇到無法唯一定位的既有記錄(`unlocatableCitekeys` 三類)時,該列具名進 `skippedRows`、零寫入、其餘照跑——先前寫入當下被拒時整趟匯入擲出、前面幾列已落盤而報告丟掉;兩面同一個 `WoSImport.run`。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#700 b31 W5 重新確認,裁決不變、契約有改**:MCP 面經 `ensureLayout`(含 `dry_run`),`.gitignore` 讀不懂時在建立任何東西之前具名拒絕、不改寫(同 `akashic_import_zotero` 那一格)。**有記錄的差異**:CLI `import-wos` 開既有 store(`openStore`)、不建佈局、不碰 `.gitignore`——這個差異早於 #700,本輪只記下。 **#700 b33 verify**:MCP 面同 `akashic_import_zotero` 那一格。說明裡的「建議先 dry_run」不改:`dry_run` 也經 `ensureLayout`(既有行為:乾跑也建佈局、也會附加區塊),讀不懂時同樣拒絕、錯誤訊息自己說明——說明不加(位元組預算),預覽面要不要改成只看不寫沒有裁決。 **#700 b35 重新確認(使用者 2026-10-05 裁決第 1 項),裁決不變、契約有改**:MCP 面(含 `dry_run`)同 `akashic_import_zotero` 那一格——照常匯入、回應帶 `gitignoreWarning`;index rebuild 失敗的錯誤訊息末尾也附上那則 warning。上一句的「讀不懂時同樣拒絕」自此不成立。CLI `import-wos` 照舊開既有 store、不碰 `.gitignore`(有記錄的差異不變)。 **#700 b36 verify 重新確認,裁決不變、契約有改**:MCP `dry_run` 的參數說明改成照實說「不寫記錄(佈局與 .gitignore 區塊照建)」——先前寫「不寫檔」,而它建佈局、會附加區塊(使用者 2026-10-05 的裁決把乾跑納入「警告後繼續」,行為不改,說明改;tools/list 淨 −1 位元組)。warning 的文字同 `akashic_import_zotero` 那一格的 b36 補記。 | +| `akashic_resolve_people` | `resolve-people` | ✅(#272 起兩面契約有記錄的差異:MCP 允許 apply+reject 組合(兩段式、按腿回報);CLI 維持分兩次呼叫——互動面天然序列,組合是 LLM 批次 triage 的需求。#303 起兩面同步帶 `tier`(封閉四值 exact/confirmed-elsewhere/reorder/initials,信心降冪):MCP 每列 `tier` 欄、CLI 按 tier 分組標頭與 `--tier` 篩選(裸 `--apply` 對寬鬆 tier 拒絕)、App 候選列標示;apply id 升三段形 `citekey:authorIndex:personKey`(釘 person,兩段 legacy 收);否決抑制改與提名同一套正規化、淘汰而得的唯一命中在 reason 揭露——R1 verify 後 apply 語意有這些**有記錄的變更**,非純 additive。R3 裁決的**面不對稱**:tier 閘只在 CLI 篩選式批次(MCP per-id 顯式+tier 可見,刻意不閘;tier-acknowledgment 參數列 follow-up);rejected/applied 回音均三段 pinned 形。**#624 起另一個有記錄的差異**:淘汰而得的唯一候選(此位置其他人選已被否決,`eliminatedPairings > 0`)——CLI 的篩選式 `--apply` 排除它、逐筆列出並指路 `--judge`,全被排除時零寫入、非零結束;MCP 的逐 id apply **照寫**(顯式指名,理由同 tier 閘的面不對稱)。MCP 列表帶結構化的 `eliminatedPairings`;CLI 沒有 `--json`,同一件事以文字揭露——列表每列標「淘汰而得」、reason 帶「已被否決」、`--apply` 時另列排除清單。兩段 legacy id 若指到淘汰所得會被拒(只指到位置、沒點名人),適用於 MCP 的 apply/reject 與 CLI 的 `--reject`(CLI 的 `--apply` 只送三段 id),要用三段形;**#627 起 citekey 重複時一律不猜**:兩面的 apply/reject(兩段與三段 id)與 split/un-split/drop/attribute-org 整批拒絕,judge/refute 該筆具名略過,CLI 篩選式 `--apply` 排除並另列;「無法唯一定位」自 R2 起含與另一筆共用 id 的 work(`unlocatableCitekeys` 一個定義,各腿共用——寫入以 id 定檔,改它等於改兄弟的檔);`PersonResolver.apply` 本身也不套用這些位置、以陣列位置就地改寫(最後一道防線);App 裁決台的 accept 具名拒絕(App 列表沒有標記,第三面的已知差異);MCP 列表以 `unlocatableCitekey` 先標出,CLI 列表以 ⟨citekey 重複或共用 id⟩ 標出——這兩面同一件事;confirm 的 verdict 只寫給真的改到的作者位,apply 以 `notApplied` 回報;judge 同一次呼叫把同一作者位判給兩個人整批拒絕。**有記錄的差異**:judge/refute 全部略過時 CLI 非零結束、MCP 照常回成功由呼叫端讀 `skipped`(同 #624 全數排除的同形差異);作者位早已歸給同一個人時,兩面同契約:已有同一句理由的逐篇判定=no-op 成功(`alreadyJudged`),理由不同則具名略過;由其他規則歸戶的具名略過(升級面缺口 #636——**自 change `resolution-verdict-states` 起不成立**,見本列 (c))。**#635 起 judge/refute 各自單獨呼叫**:兩面都顯式拒絕它們與彼此或 apply/reject 的組合(先前靠提早返回隱含達成,其餘腿被靜默丟掉、仍回成功)——同結構腿(#443)的既有契約。造出這些狀態的儲存層路徑見 #631。同型但不屬 resolve-people 族的寫入面見 #628。tier 閘看排除後的套用集。刻意不給 CLI 關掉排除的旗標——那會把排除變回一鍵可關的預設;查過未決的配對兩面都看不到,繫於 #619)。**change `resolution-verdict-states`(#619/#636,2026-09-25)重新確認,裁決不變、契約有改**:(a) 兩面同時新增 **undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedAuthorships`),記下「查過、判不出來」與查了什麼——兩面同契約:整批拒絕六類、逐筆略過四類、完全相同=`alreadyRecorded`、單獨呼叫;上一句「兩面都看不到」自此不成立。(b) 查過未決的配對在列表揭露(MCP `undecidedChecks`/`counts.undecided`/`undecidedTotal`,CLI「查過未決 N 次」與四態計數行);**CLI 的篩選式 `--apply` 排除它、另列並指路 `--judge`,MCP 逐 id apply 照寫**——與 #624 淘汰所得同形的有記錄差異,理由相同;App 裁決台(第三面)同樣標「查過未決 N 次」,Accept 是逐筆顯式、照常可用(R1 verify:初稿漏了這一面)。(c) `judge`/`refute` 對「已由提名層同向判過」的配對改為寫入並存的逐篇判定(回應帶 `coexistsWith`,需 store format ≥ 19),兩面同契約。(d) R2/R3 verify:judge 與 attribute-org 對 literal 作者位照常歸戶、理由存不進去時該列帶 `verdictNotRecorded`(CLI 印 ⚠),兩面同契約;同批 refute 碰撞具名略過;undecided 腿的整批拒絕另含三個上限(200 個 id、20 個 digest、4,096 位元組)。**#597 起多一個有記錄的差異**:CLI 列表模式(不帶 `--apply`)的 `--citekey`/`--person`/`--tier` 收窄候選列與歧義條目,並印出收窄後/全部的計數;MCP 的列表面沒有對應參數。理由同 `--rows` 那格:CLI 的輸出進人的終端機,逐篇查證要的是「這一篇還有誰被提名」;MCP 呼叫端拿得到 `candidateTotal` 與逐列 id,可以自己篩,而為它加參數會讓一個 LLM 呼叫端以為沒列出的候選不存在——那正是 CLI 用「全部 M 個」那一行防的誤會,MCP 面沒有對應的一行。MCP 呼叫端若需要逐篇收窄,要重開這一格並一起裁位元組預算) **#641 重新確認,裁決不變、契約有改**:(a)「無法唯一定位」的 work 多一類——load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),併進 `unlocatableCitekeys`,各腿既有的處置不變;(b) person 側新增 `unlocatablePersonKeys`(person key 重複、或 load 判定它的檔案寫入時會被拒——作品側的「共用 id」不收,理由在型別 doc):apply/reject 整批拒絕、judge/refute/undecided 該筆略過並具名、CLI 篩選式 `--apply` 排除並另列、MCP 列表以 `unlocatablePersonKey` 標出、App 裁決台的 accept 具名拒絕(`AdjudicationError.unlocatablePerson`)——三面同一個定義。在此之前 judge 與 apply 先寫 work 再寫 person,person 那一格在寫入當下被拒時作者位已升格、verdict 沒寫(#631 R2 verify 真 binary 重現)。拒絕與略過訊息共用 `UnlocatableReason`。 。**#648 重新確認,裁決不變、契約有改**:judge/refute 的理由上限 4,096 位元組(與 undecided 的說明同一個常數 `maxStatementBytes`),超過整批拒絕、零寫入、不截斷,在任何 store 狀態分支之前擋——兩面同契約(CLI `--judge`/`--refute` 的 help 與 MCP `judge`/`refute` 的描述都寫明上限);judge 的寫入集合(entry 與 person)與 undecided 的 person 在任何檔落盤之前逐筆 preflight,寫出後超過讀取上限(8 MiB)即整批拒絕、錯誤具名記錄與兩個位元組數(在此之前 judge 先寫 entry、person 在寫入當下才被拒——作者位歸戶而 verdict 沒寫);attribute_org/split_author/un_split/drop_author 的「整批驗證通過才寫」自此也含 encode 與讀取上限。apply/reject 維持既有的逐筆收容(寫入失敗逐筆回報),拒絕自此具名**#663 重新確認,裁決不變、契約有改**:歧義條目對隸屬的說法自此由 `TimelineOf.standing`(AkashicCore;與匯出端 `researcher.status` 同一個推導,CLI、MCP、App 三個面與匯出各自推導的四份併成一份)決定。`formerAffiliation`/`formerAffiliationEnd` 只給**宣稱已結束**的段(有 end 或 ended-unknown);只被觀測到的段(attested、沒有 end)改送新鍵 `observedAffiliation`/`observedAffiliationAt`(最近的一個觀測點)——被看到過不等於離開了,匯出端對這種人說 `undetermined`(#661)。混合情形(有 end 的段加上觀測段)兩組都送。**舊鍵 `formerAffiliationAttested` 退場、不沿用**:讀它的呼叫端得到缺席,不是一個換了意思的值;**`formerAffiliation` 的意思收窄為「宣稱已結束」**——先前它也被送給只被觀測到的人,那是同一個鍵名在改動前後指不同的事,所以在這裡寫明。CLI 措辭同步:`曾隸屬:X(…)` 只給已結束的段,只被觀測到的是 `觀測到隸屬:X(觀測點)`,混合情形兩者都印;App 裁決台同。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | + +**#457 起另有一個「把作者位移除」的面**(`--drop-author` / `drop_author`,兩面同走 +`dropAuthors`):`Author` 的三態(#323)都假設那一格背後有一個作者,而實測(2026-09-09, +全庫 3,885 個 distinct literal 逐一掃過)有一個不是——PsycInfo 的 `No authorship indicated`, +21 筆。在此之前**沒有任何面到得了 0 個作者位**:`apply` 升格、`attribute_org` 改歸屬、 +`split_author` 增加數量、`un_split` 減到 1。唯一的路是手改 YAML。 + +它今天的代價不是「還沒歸戶」,是出貨的 `.bib` 裡有一個**被捏造**出來的人 +(`AUTHOR = {indicated, No authorship}`),citekey 也照它生。APA7 §9.12 對無署名作品 +以標題起首,那要求作者位是**空的**。 + +**以值定位不用索引**(同 `un_split`):索引在同一批的前一次移除之後會位移,而「這個字串 +不是人」本來就是關於字串的宣稱。同一筆 work 的作者位裡出現多次 → **拒絕不判定**。 +**兩面同契約**:per-id 顯式、理由必填、整批驗證通過才寫、單獨呼叫不與其餘腿組合。 +**#658 起 CLI 另過 #298 的目標確認閘**(使用者 2026-09-28 裁決:它沒有具名逆操作、被移除的作者位不留位置,寫錯 store 要靠 git 收拾);閘在參數組合檢查之後、開 store 之前。MCP 面不閘,理由同橫切選項表的 `--yes` 列。 + +**同輪動到 export 面**:`apa7Report` 對帶移除記錄的 work 抑制 `AUTHOR` 的 required 檢查 +——形狀取自 #406 的 `paginated`,且**空的 `authors` 本身不足以抑制**(那是「還沒記」, +正是該檢查要抓的東西;同 `paginated` 的 `nil` 照報)。少了這一半,21 個被捏造的 byline +會變成 21 個 error。 + +**#443 起另有一個「把作者位拆開」的面**(`--split-author` / `split_author`,兩面同走 +`splitAuthors`):一個 literal 裝了兩個人時,在此之前**沒有任何面拆得開**——`apply` +只能把它整個升格成一個 person,而那會建出一個不存在的人。實測 4 筆「某人與雷庚玲」。 + +**收分隔符而不是拆好的名字**:後者等於讓呼叫端**編造**,打錯一個字就寫進 store 而沒有 +任何東西擋得住;收分隔符則拆出的每一段必然是原文的子字串。切出空段即拒絕(分隔符選錯); +換行等空白段同拒;段數有上界(32,且在 materialization 之前生效——literal 是未信任 +輸入,高頻分隔符不得先被切成無界陣列再拒);分隔符**在語法上無法**含 `=`(第一個 `=` +之後一律是理由——誤解析由報告的 separator/judgement 欄揭露、有測試釘住,根治需 +結構化參數:MCP 收 object 陣列、CLI 形式另議。**觸發條件(#451 (b)):出現一個真實 literal +其正確分隔符含 `=` 時重開;在此之前不做**——實務上含 `=` 的人名分隔符零實例,先造介面等於 +替一個還不存在的輸入設計形狀)。分隔符、**原始 literal 與理由**都被丟棄,而報告逐筆印出 +「原文、用什麼切、切成什麼、為什麼」(`lossless-intake` 的丟棄必須可見;揭露的是 +消毒顯示形——`displaySafe` 200/300 上限,#165 的既有取捨)——~~**store 不留原文與 +理由是本面的誠實邊界**(R1 verify):拆分沒有「被判定的另一方」可落 verdict,work 側 references +值域目前只收識別碼;un-split 所需資訊在 store 內不可回復(只在 store 的 git 歷史),持久化需要 +值域的顯式裁決(follow-up)。~~ → **自 #450 起不成立**(Spectra change +`split-verdict-historical-reference`,2026-09-07):拆分的判定持久化到 work 側 `references` +(`{field: authors, value: 原 literal 逐字, judgement: 拆為 ⟦a⟧ ⟦b⟧:理由, rests-on: []}`——第 15 +條邊值域的顯式擴充、statement 走 `SplitRecordValue` 單一解析器、需要 store format 16),與作者位 +改寫在**同一次**寫入,所以拆分永遠不會沒有記錄、也不會記兩次;段含文法保留字元 `⟦⟧` 拒絕。 +報告的 `original`/`judgement` 仍是消毒顯示形,`recorded: true` 說記錄寫了。un-split 所需資訊 +自此在 store 內;**un-split 操作面另開 issue**,兩面都還沒有。 + +**拆出來的仍是 `.literal`**——拆是**形狀**修正不是身分判定,每一段各自走既有消歧路徑。 +**兩面同契約**:per-id 顯式、理由必填、**同一個作者位一次只能拆一次**(去重以解析後的 +(citekey, index) 為鍵——字面去重會被同 slot 異拼法繞過,R1 verify HIGH)、**單獨呼叫 +不與其餘腿組合且組合被顯式拒絕**(#418 樣式;R1 verify 前靠分支順序隱含達成、其餘腿被 +靜默忽略——那不是「不可組合」而是隱藏優先序。空陣列與 JSON null 同拒)。它改的是 +作者位的**數量**,混在一批裡會讓其他腿的 index 意義改變。同一筆 work 的多個位置一起拆時 +**index 位移由實作處理**(由大到小),呼叫端給的是原始索引(報告的 `authorIndex` 也是 +原始索引,非寫入後位置)。 + +**#513 起多一個把上面那個拆分合回去的面**(`--un-split` / `un_split`,兩面同走 `unsplitAuthors`):#450 讓拆分的判定持久化到 work 側,於是 **un-split 所需的全部資訊自此在 store 內**——但沒有面把它合回去,還原只能手改 YAML(三個動作要一致,而那是 `replace-endnote-and-zotero` 第 4 條要防的安靜失敗)。 + +**以值定位**(`citekey:原literal`,`ProvenanceReference` 的既有立場 D2),**同 value 多筆記錄拒絕不判定**——那時兩筆記錄的 `parts` 可能不同,而「哪幾個作者位屬於哪一筆」store 裡沒有東西說得出來(形狀取自 `akashic_enrich` 對 DOI 命中 ≥2 筆的 `ambiguous`)。**還原後刪掉那筆記錄**:它的存在理由是「`authors` 已經沒有原 literal 了」,還原之後那句話為假——留著會讓 store 斷言一件假的事,並點亮 `staleSplitRecords`(一次合法的 un-split 製造一條永久 warning)。歷史留在 git,與 #443 那 4 筆的既有取捨相同。**#659 起兩面同時要求那個 git 真的在**:刪記錄之前比照 #573 檢查那些 work 檔已 commit、乾淨,否則整批拒絕、零寫入(使用者 2026-09-27 對移除面一族的裁決——要求已 commit、不改 store format——套在這裡,所以不改成留一筆「已還原」記錄)。 + +**兩面同契約**:per-id 顯式、整批拒絕零寫入、**與 `split_author`/`attribute_org` 三者互斥且都不與其餘腿組合**(它同樣改作者位的數量,N → 1)。段已升格為 `.key`/`.organization` 時拒絕並**指向 `demote`**——把已歸戶的身分塞回黏著的 literal 是判定的逆轉,不屬本面。 + +**它不是新的一列,是既有 `akashic_resolve_people` 的一個參數**——加列會讓上表的封閉列舉與機械稽核(`grep -oE 'Tool\(name: "akashic_[a-z0-9_]+"'`)對不上。#513 的 Expected 寫「兩張表各加一列」,那個寫法會壞掉本檔自己的稽核程序。 + +**#443 起多一個團體作者的升格面**(`--attribute-org` / `attribute_org`,兩面同走 +`attributeToOrganizations`):`.literal` → **`.organization`**。`Author` 的三態 +(#323)在此之前只有兩態接得起來——`apply` 升格成 `.key`,而團體作者**只能在建檔時 +指定**,既有記錄改不了(實測 #443:兩筆機構被記成 `.literal` 作者,唯一出路是手改 +YAML)。**放在 `resolve-people` 是因為作用對象相同**(work 的一個作者位),不是因為 +它是消歧——org key 由呼叫端顯式給,不經提名,所以沒有 tier 也沒有候選清單。 +**兩面同契約**:per-id 顯式、judgement 必填、不提供批次形式、**不與 apply/reject 組合** +(升格目標是另一個值域,混在一批裡會讓「哪些寫了」難以判讀——同 #418 對 `repoint`/ +`demote` 的既有裁決)。失敗語意:**整批驗證通過才寫**,任一筆前提不符即零寫入。**#669 起**:目標 organization 的 key 有不只一筆記錄時同樣整批拒絕(寫進哪一筆是猜;在此之前那裡以 `uniqueKeysWithValues` 建表,重複直接讓 process trap),兩面同一條 service 路徑。 + +**#386 起多一個 per-id 判定面**(`judge` / `--judge`,兩面同走 `judgeAuthorships`): +收 `citekey:authorIndex:personKey=理由`,**歧義列也適用**——歧義的意思是提名器分不出來, +不是人/AI 分不出來(`identity-is-judged-not-matched`)。**契約有記錄的差異**:CLI **不提供** +篩選式批次形式(judgement 必填形成摩擦,批次會讓它退化成罐頭);MCP 面沿用該面既有的 +per-id 顯式契約。兩面的失敗語意相同且刻意分兩類:輸入語法錯整批拒絕零寫入、store 狀態不符 +該筆略過並具名。 + +**#388 起 CLI 多一個 `--rows`(歧義段列數上限的旋鈕),MCP 面刻意不加**——這是本列 +第三個有記錄的差異。理由不是「還沒做」: + +- **消費端不同,而上限保護的是消費端。** CLI 的輸出進人的終端機(可捲、可 pipe、 + 可 `grep`);MCP 的輸出進 LLM context(更貴、且呼叫端無法在收到後丟棄已付的代價)。 + 兩面的位元組預算本來就不同值且刻意如此——CLI 128 KB、MCP 48 KB。給 MCP 一個放寬 + 列數的旋鈕,等於讓呼叫端有辦法撐爆自己的 context。 +- **MCP 已有對等的揭露**:`candidateTotal` 送分母讓程式判斷「我看到的是不是全部」, + 而 CLI 那面是印差額讓人一眼看到。缺的從來不是資訊,是**人**這一側取回全部的手段。 +- **不對稱的方向與既有兩條一致**:本列的另外兩個差異(tier 閘、批次形式)也都是 + 「CLI 給人多一個把手/多一道摩擦」。三者同向,不是各自為政。 + +日後若 MCP 面真的需要(例如出現一個要一次讀完全部歧義的 LLM 流程),要重新裁決的是 +**該面的位元組預算**而不是列數——列數上限追不上內容,這是 #236 R4 已經量過的 +(列數與 ref 上限都在時仍產出 3,844,596 bytes)。 +| `akashic_record_divergence` | `record-divergence` | ✅(**#611 重新確認,裁決不變、契約有改**,兩面同走 `recordDivergence`:(a) 同一個 id 的既有記錄若候選 (shape, key) 與這次不同——歧異記錄的 id 只由候選 key 決定、不含形狀,`rename` 就地改寫候選而不重算 id——**整個拒絕、不覆寫**(`StoreIOError.divergenceIdHeldByOtherCandidates`,訊息點名現有候選、說原因是 key 還是形狀、出路是 `resolve-divergence`/`dismiss-divergence`;先前互相覆寫、不出聲);(b) 既有判斷與 prefers 的保護改為寫入當下重新讀磁碟上那一個檔,不看呼叫端的快照。兩面的說明(MCP 描述、CLI abstract)各加一句;`docs/store-format.md` §5.8 第 7 條是 normative 的版本)。**#611 R3**:訊息改成逐行、出路在第二行——兩面的錯誤出口都逐行截 400,R2 的單行版在兩面都被截在出路之前;出路同時寫出 CLI 的 `dismiss-divergence` 與 MCP 的 `akashic_dismiss_divergence`,並註明 `resolve-divergence` 只有 CLI。MCP 描述的出路改成 `akashic_dismiss_divergence 或合併`(-5 bytes) | +| `akashic_dismiss_divergence` | `dismiss-divergence` | ✅(#586;Spectra change `divergence-dismissal`)。歧異記錄在此之前記得下去、解不掉(org shape)、也刪不掉,唯一出路是手改 YAML。**它與 `resolve-divergence` 的 CLI-only 裁決不矛盾**:那一格缺席的理由是操作的性質(合併+全庫改寫+刪檔,不可逆、要逐筆過目);本面只刪一筆記錄、候選實體與參照都不動、刪除前要求記錄檔已 commit——被刪的東西在 git 裡有副本,LLM 消費面記錯的一筆可以由同一面收回(`akashic_record_divergence` 是 MCP 面,只有記錄面沒有移除面會讓記錯的代價落在手改 YAML)。兩面同契約:以 UUID 定位、理由必填且只進報告(使用者 2026-09-27 對移除面一族的裁決)、不寫進 store、不改 store format、乾跑只回報(CLI `--dry-run`、MCP `dry_run`,兩面預設都是實跑)。~~**CLI 不過目標 store 確認閘**:以 UUID 定位,指錯 store 只會找不到那筆記錄——閘防的「在錯的 store 上照樣對得上」在這裡不成立~~ → **#586 R1 verify 起 CLI 實跑過目標 store 確認閘**:那個前提是假的——歧異記錄的 UUID 由候選 key 決定(`DeterministicUUID.forDivergence`),clone、備份、或兩個各自記下同一組候選的 store 裡,同一筆記錄的 UUID 相同,正是 #580 裁決的形狀(錯的 store 上照樣對得上)。乾跑不閘;MCP 面不閘,理由同橫切選項表的 `--yes` 列 | +| `akashic_update_person` | `update-person` | ✅(#68)。**#554 R26 D73 重新確認(R27 補記),裁決不變、契約有改**:references 的 append-only 去重自此比位元組(`byteExactKey`)——canonical 相等而位元組不同的兩筆都會進 store(先前是靜默 no-op);重複的偵測面缺口記 #582(R26 verify requirements 第 10 列、regression 第 54 列:R26 改了語意而這一列沉默)。**#674 重新確認,裁決不變、契約有改(收緊)**:`references`(MCP 在 `fields` 裡、CLI 在 `--fields` 的 JSON 內)與 `akashic_update_venue` 的 `references`(CLI `--references`)改呼叫**同一個解析函式**(`parseReferenceObjects`;解析與驗證都在讀 store 之前,CLI 的 `validate()` 經 `checkUpdatePersonFields` 呼叫同一個函式,違反是用法錯誤 64)。往 venue 那一邊收,被拒的輸入類別(先前寫得進去、現在整個呼叫拒絕):(1) retrieval 沒給 `status`(先前預設 200,會把離線掃描檔記成 HTTP 200);(2) `status` 不是整數(boolean、小數、字串——先前 boolean 被當 1、字串被當 200)或不在 100–599;(3) 不認得的鍵(先前靜默忽略,打錯的欄位名不出聲);(4) `value`/`media_type` 不是字串、`rests_on` 有非字串元素(先前靜默變成 nil/略過那些元素);(5) `statement` 超過 4,096 位元組、`rests_on` 超過 20 個、一次超過 200 筆(先前無上限);(6) `url` 不是 http/https(`file:`、`ftp:`、無 scheme)或主機為空或含帳密(userinfo);(7) `retrieved` 不是 ISO 8601 日期/日期時間(裸日期 `YYYY-MM-DD` 照收);(8) `field` 只有空白(先前只擋空字串)。~~**有記錄的差異**:空陣列(`"references": []`)在 person 仍是 no-op(`fields` 裡被提及的一格,先前就是),在 venue 是錯(獨立參數);~~ → **#695 起不成立**(見本列末);兩面各自收哪些 field(person 收 verdict 以外、venue 只收 issn/names)是 holder 的政策不是契約。~~`enrich` 的 `sourceURL`/`sourceRetrieved`/`sourceStatus` 仍是各自的契約(沒有 url/retrieved/status 範圍的檢查)——不在本次範圍,追蹤於 #695(待使用者裁決)~~ → **#695 起不成立**(見 `akashic_enrich` 那一列) **#695 重新確認,裁決不變、契約有改**(使用者 2026-09-30 裁決「兩面都拒絕」):`"references": []` 在 person 改為拒絕(先前 no-op),與 venue 同一句話——拒絕住在共用的 `parseReferenceObjects`,兩面自然同句;CLI 的 `validate()` 經 `checkUpdatePersonFields` 呼叫同一個函式,違反是用法錯誤 64。status/url/retrieved 的形狀搬到 Core 的 `RetrievalWriteShape`(`enrich` 的來源欄位呼叫同一個函式),本面的訊息不變。R1 verify 起同一個函式多拒四種(兩面、三個入口一起):`@` 後接組合符號/ZWJ/VS16 的帳密(定界符改在 scalar 上找)、url 含控制/格式/零寬字元或空白、url 的主機含反斜線、media_type 含這類字元或前後空白。R2 verify(2026-10-01)起再多拒三種:url 主機部分的全形/相容形定界符(`@`、`:`、`℀` 這類,相容分解含 `@ : / ? # \`;IDN 主機照收)、不是 ASCII 數字的 port、空的或只有空白的 media_type;`https://` 前綴改在 scalar 上比;retrieved 的原值排在理由之後、以逃脫後的長度截;控制/格式字元的補救改說百分比編碼或 punycode(拒絕範圍不變,仍涵蓋路徑與 query)——兩面、三個入口一起。enrich 面另兩處:`sourceDigest` 以送來的原值驗(前後空白拒絕),空的 `sourceMediaType` 不寫進 reference。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#564 修正輪重新確認(使用者 2026-10-02 裁決第 1、2 點),裁決不變、契約有改(BREAKING)**:兩面同時新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`)與 `remove_names`(CLI `--remove-name`)。`fields.names` 讓名字進或出 authorized 時理由必填(缺了整批拒絕、零寫入——先前不附理由也寫得進),寫 `field: authorized` 的~~指定/確認/撤回~~ → 指定/撤回(b34 起留下的不寫確認)記錄(需要 store format ≥ 22,回報 `judgementsRecorded`;乾跑回 `judgementsToRecord`);只動 variant 的不必。替換拿掉有記錄的名字、或讓對外形整個離開 names,具名拒絕並指出口。理由與證據只伴隨 `fields.names`。`remove_names`:`<名字>=理由`,只收最後一筆名字分類記錄是撤回的 variant 名字,連同記錄一起刪,理由只進報告(`namesRemoved`),刪前要求 person 檔已 commit(乾跑不查);單獨呼叫(MCP 不帶 `fields`、CLI 不帶 `--fields`)。一次至多 200 個名字(`fields.names` 的 authorized——R2 起 variant 不計、`remove_names`;b33 X1 第 15 列:這一句曾留著「兩個分割合計」)。兩面同一條 `updatePerson`;CLI 的 `validate()` 呼叫同一個 `checkUpdatePersonArguments`(`--fields` 從 stdin 來時組合的檢查在執行期)。沒有有記錄的差異 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:(a) ~~讓**沒有記錄**的對外形整個離開 names 改為放行(附 `judgement`;先前一律拒絕,整份替換改正對外形拼寫的既有用法被改成三步——第 4 列),回應多 `authorizedRemovedWithoutRecord`(dry_run 同;它沒有地方錨定記錄)~~ → b34 退場(見下方 b34 的 (a));(b) 沒附理由的拒絕在 store format < 22 時說出寫入閘與升級路徑(第 5 列);(c) 一次上限只數 authorized(variant 不寫記錄、不計——第 9 列;讀 store 之後另以這次要寫的記錄數再擋);(d) `remove_names` 刪完沒有名字整批拒絕(第 18 列,與 organization、venue 同),沒有記錄的名字被拒時指向 `fields.names` 整份替換、不在 authorized 而最後一筆是指定的指向「先指定回去再撤回」(第 6/23 列)。MCP 描述那一句改短(「拿掉有記錄的名字整批拒絕」),細節在 CLI help 與 store-format §3.5。兩面同一條 `updatePerson`。~~**交給使用者**:「只動 variant 的不必附理由」是實作對裁決第 1 點的讀法(第 24 列);附理由時仍在 authorized 的每個名字都寫確認(第 10 列)~~ → 使用者 2026-10-05 裁決:前者確認,後者改為只替進出的名字寫(見下方 b34 的 (b)) **#564 b34 重新確認(使用者 2026-10-05 裁決),裁決不變、契約有改(BREAKING)**:(a) 上面 R2 的 (a) 退場:對外形在替換裡整個離開 names 一律整批拒絕(有沒有記錄都一樣——移出要寫「撤回:理由」、記錄錨定 names,出口是放在 variant),`authorizedRemovedWithoutRecord` 不再出現(b33 X1:那條路的理由不入庫、format < 22 的寫入閘沒觸發);(b) 只替進出的名字寫記錄(指定/撤回),留下的不寫確認;附理由而沒有名字進出 authorized 整批拒絕;只動 variant 不必理由(使用者確認了上一格的讀法);(c) 替換不把 person 刪到沒有名字;(d) `remove_names` 的拒絕在 format < 22 時說出寫入閘、刪到沒有名字與 canonical 孿生拼法說出工具面的出口;(e) **有記錄的差異**:CLI `--remove-name` 過 #298 的目標 store 確認閘(裁決第 4 點;`--dry-run` 不擋),MCP `remove_names` 不閘(橫切表 `--yes` 列:active store 是 session 狀態)。MCP 描述那一句改寫(「只替進出的名字寫指定/撤回」「對外形整個離開 names、拿掉有記錄的名字都整批拒絕」),淨減 4 位元組。兩面同一條 `updatePerson` **#564 b37 重新確認(b36 Y1 第 2/3/14 列),裁決不變、契約有改**:(a) `fields` 路徑(一般欄位、`names` 附 `judgement`,含 dry_run)對兩筆**不同**的 person(id 不同)共用同一個 key 整批拒絕、零寫入——先前寫進先載入的那一筆,名字分類記錄也一樣。不問涵蓋更廣的 `unlocatablePersonKeys`:同一筆記錄的 legacy 拷貝與其他「load 判定那筆的檔寫入時會被拒」的是給多檔寫入者擋撕裂的,這裡只寫一個檔,由寫入當下的 #631 前置擋下、訊息說出同一個操作稍早寫過(#705);(b) MCP `judgement` 屬性說明改成實話(「名字進出 authorized 時必填,否則拒收」——先前仍寫「仍是對外形的寫確認」,只讀屬性說明的呼叫端會以為附理由能替沒動的名字留確認),淨減 9 位元組,真 binary 的 `tools/list` 由 `NameClassificationSchemaTextTests` 釘住。兩面同一條 `updatePerson` | +| `akashic_update_entry` | `update-entry` | ✅(#544;work 的部分更新面,兩面同走 `AkashicService.updateEntry`)。**預設乾跑**:CLI `--apply`、MCP `dry_run`(預設 true),比照 `enrich`;CLI 的 `--apply` 過 #298 的目標確認閘(`WriteGateRulings` 的 `update-entry` 格),MCP 不閘(橫切選項表 `--yes` 列的理由)。**`remove_fields`/`--remove-field`(#544)**:`<鍵>=理由` 移除一個 `fields` 的值——在此之前沒有任何面刪得掉它(`enrich` 只補不存在的鍵、`import-zotero` 只對 Zotero 來源的記錄整份替換),唯一的路是手改 YAML(#544:21 筆 `abstract` 是 Crossref 的錯誤頁,兩筆的 `journaltitle`/`publisher` 是補助計畫名稱與總統令字號)。它是判定(`two-kinds-of-edits` 的 AI 欄),套使用者 2026-09-27 對移除面一族的裁決:理由必填、只進報告(`fieldRemovals`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`)。#544 的 body 寫「留記錄、形狀取自 #450」——**這一條沒有照做,而且那個決定不是使用者下的**:09-27 的裁決是為 #588/#572/#586(邊、識別碼、divergence 記錄)下的,把它**延伸**到 `fields` 的值是 Claude 代裁(b11c 實作,2026-09-29),**待使用者確認**——#544 自己沒有裁決紀錄,issue 要的 #450 形記錄(work 側 `references` 的退役記錄)沒有做;要補的話是第 15 條邊的值域擴充與 store format 的變更,需要顯式裁決,不是本列的旗標。指向被移除鍵的 `fields.<鍵>` reference 一併刪除(#588 `remove_issn` 的同一處置:留著它,那筆的語意會從「值出自這份來源」安靜地翻成 #517 的負結果「查過了、沒有」)。**為什麼不違反 `lossless-intake`**:那條規則管**進來的那一刻**,禁的是匯入時靜默丟掉來源給的東西;本面是事後的更正——人讀過值、判定它不是來源給這個欄位的資料(錯誤頁被當成內容收下,本身是那條規則的反面),而且不靜默(理由必填、逐欄回報、值的前 300 字印在報告)、不讓資訊不可回復(移除前的檔在 git)。兩面同契約:鍵不存在、同鍵兩次、理由空白或過長、一次超過 200 個——整批拒絕、零寫入;work 無法唯一定位(`unlocatableCitekeys`)時拒絕。由被移除值推導的 literal venue 邊不動、具名在 `venueEdgesFromRemovedValues`(刪它是 `resolve-venues --drop-venue` 的判定,#572);**已歸戶的 key 邊連同 venue 上的 confirmed verdict 也不動、具名在 `venueKeyEdgesFromRemovedValues`**(處置是先 `--demote` 把邊退回 literal、再 `--drop-venue`;b11c R1:先前只列 literal 邊,而「補助計畫名稱被記成期刊」若已被歸戶,錯的歸屬會繼續有效而報告沉默)。**會把值補回去的路徑有三條,都在 `reintroductionNote` 說出來**:`import-wos` 回填(只多不少,缺席的鍵會被補進去)、`enrich`(add-only)、Zotero pull(主來源是 Zotero 的記錄另附 `zoteroNote`)——store 不記得這個值被判定過不屬於這裡,重跑同一份來源它會回來(報告先前只點名 Zotero)。移除 APA7 必要欄位時附 `apa7RequiredNowMissing`(用既有的必要欄位表 `BibExport.apa7Report`、只報這次新增的缺漏;`validate` 不報、`export-bib` 才印 ERROR)。`remove_fields` 兩面同一個 payload、沒有有記錄的差異,CLI 不設 `--json`(寫入面封閉例外形);MCP 的 `citekey` 缺少或空白是缺參數錯誤(該工具自己的必填檢查,不是「找不到:citekey「」」)。**#614 起兩面同時新增 `add_sources`/`--add-source`**:把已存進 `sources/` 的內容宣告為這篇的副本(`akashic.sources`,store-format §2.4.1)。在此之前沒有任何入口寫得進它——`store-source` 只存位元組與取得記錄、`enrich` 的 `sourceDigest` 寫的是欄位層級的 reference(值 ← 證據,不是作品 ← 副本)、`link` 只管 cites/related。它是落地不是判定(`two-kinds-of-edits` 的程式欄):「這份 PDF 是這篇」在上游判定(`akashic-fetch-fulltext` 的驗證)。兩面同契約:digest 必須合法(空內容的 digest 以 #654 的原句拒絕)、新加的 digest 已在本機 `sources/` 且 index 有取得記錄(`LibraryStore.sourcePresence`;已連過的是 no-op、不檢查——全是已連過的就不讀 index;blob 的位置必須是普通檔,目錄與 symlink 具名拒絕)、同一次不重複、至多 200 個,否則整批拒絕零寫入;add-only 冪等(`sourcesAlreadyPresent`,沒有新東西就不寫);報告逐個帶 index 的取得記錄——**有記錄的差異(b11c R1)**:MCP 面的 `sourcesAdded` 截 20 筆(`sourcesAddedTotal`/`truncated` 揭露)、CLI 全列,理由同 `--rows` 那格(每筆帶最多約 2 KB 的第三方字串,輸出進 LLM context、呼叫端無法丟棄已付的代價)。**讀取面**:`get-entry`/`akashic_get_entry` 自 b11c R1 起列出 `akashic.sources`(同一條 `entryDict` 路徑;在此之前能寫不能讀,#218/#219 的形狀)。**四條腿兩兩各自單獨呼叫**(`remove_fields`/`add_sources`/`remove_zotero_sources`/`remove_sources`,共 6 對,任兩條組合都整批拒絕、零寫入——移除是判定、加入是落地,被移除的東西也各不相同,混在一次呼叫裡報告與 git 閘的語意也混在一起;`ServiceArgvExitCodeTests.testUpdateEntryLegsAreMutuallyExclusive` 與 `StdioE2ETests.testUpdateEntryDefaultsToDryRun` 逐對釘住)。**#680 起兩面同時新增 `remove_zotero_sources`/`--remove-zotero-source`**:移除 work 記下的 Zotero 來源(主來源 `provenance` 或附加來源 `provenance_additional`)。在此之前活著的 Zotero 來源沒有任何移除面——#610 對「其中一筆記錯了」的處置寫的是手改 YAML,#679 對「附加來源沒記 `library_id`」的出路也只能手改(`replace-endnote-and-zotero` 第 4 條的形狀);App 的裁決台只處理**已在 Zotero 端刪除**的來源。**這一條腿不是 `add_sources` 的逆操作**(那條腿寫的是 `akashic.sources` 的副本 digest,逆操作是 #677 的 `remove_sources`);名字刻意帶 `zotero`。它是判定(`two-kinds-of-edits` 的 AI 欄):套使用者 2026-09-27 對移除面一族(#572/#588/#586)的裁決——`<來源鍵>=理由`、理由必填且只進報告(`zoteroSourceRemovals`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`);預設乾跑,乾跑不需要 git。**來源鍵**與跨記錄警告、`ambiguousSourceClaims` 同一種(`ZoteroSourceClaims.key`):`:`,沒記 `library_id` 的來源是 `?:`(#679 的出路);`05:K` 與 `5:K` 同一個來源。每個來源鍵都要在這筆 work 命中,否則整批拒絕、零寫入;同一來源兩次、形狀錯、理由空白或過長、一次超過 200 個同樣整批拒絕。同一筆 work 的主來源與附加來源恰好是同一個來源時兩處都拿掉。**主來源被移除而附加來源仍在時,附加來源不升格為主來源**(升格會把書目欄位的改寫權交給另一個 library,與 App 的「與 Zotero 脫鉤」同一條裁決;`primaryRemovedNote`);連結狀態的前後照 `Entry.zoteroLinkState` 的既有定義具名(`zoteroLinkState.before/after`,本面不改那個定義);書目欄位不動。**移除對下一次 `import-zotero` 的意義依「移除之後還有沒有別的 entry 宣稱這個來源」而定**(b13f R1 verify:首版無條件說「另建一筆新 entry(已沒有 entry 宣稱它了)」,在 #610 的主場景與 #679 的出路兩個形狀都是假的)——`reimportNote` 逐來源說、每個被移除的來源另帶 `reimportEffect`(封閉四值:`newEntry` 沒有任何 entry 宣稱它、Zotero 端仍有那個條目時另建一筆;`routesToOther` 仍由另一筆宣稱、匯入照常路由到那一筆;`stillAmbiguous` 仍有兩筆以上、匯入仍略過;`notAClaim` 沒記 `library_id` 的附加來源本來就不算宣稱者、移除它不改變匯入行為),宣稱者用 `ZoteroSourceClaims` 那一份定義算,行為由 `ZoteroSourceRemovalReimportTests` 與 `ZoteroSourceRemovalReimportBehaviourTests` 釘住;已在 Zotero 端刪除的來源不說。移除讓這筆變成 orphaned 時另附 `orphanedNote`(App 的 Orphans 頁會列它並提供破壞性動作,而這筆的 Zotero 條目並沒有被刪)。**#677 起兩面同時新增 `remove_sources`/`--remove-source`**:`=理由` 收回一條 `akashic.sources` 上的宣告——在此之前 `add_sources` 只能加,連錯了(連到別篇的 PDF)唯一的路是手改 YAML 或以 git 還原那個 work 檔。它是判定(`two-kinds-of-edits` 的 AI 欄:「這份內容不是這篇的副本」要讀內容才知道),套使用者 2026-09-27 對移除面一族的裁決:理由必填(≤ 4,096 位元組)、只進報告(`sourcesRemoved`,全文)、不寫進 store、不改 store format;實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`,與 `remove_fields`、`remove_zotero_sources` 同一支閘)。兩面同契約:digest 形狀不對、不在該 work 的 `akashic.sources` 上(具名列出)、理由空白或過長、同一個 digest 兩次、一次超過 200 個——整批拒絕、零寫入;work 無法唯一定位時拒絕。**只移除宣告**:`sources/` 的 blob 與 `index.jsonl` 的取得記錄不動(同一份內容可能被別筆 work 宣告或被欄位層級的 reference 引用——刪存檔是另一件事,`#544` 同族的移除面,另案),`Entry.references` 也不動(§2.4.1:兩種關係不得合併)。**不要求本機有位元組**:別台 clone 上 `sources/` 本來就可能不在,連錯的宣告在那裡照樣要收得回來;報告帶 index 的取得記錄(origin、mediaType、note…)只是讓乾跑的人認得出是哪份內容,本機沒有存檔或 index 讀不到時省略(`contentInfoUnreadable`)。**有記錄的兩面差異(b13f R1 verify,比照 `add_sources`)**:MCP 面的 `sourcesRemoved` 每一筆都列(`digest`+`reason`——理由只在報告裡有一份,不截),只有前 20 筆多帶 index 的取得記錄(每筆至多約 2 KB 的第三方字串),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列。與 `add_sources` 不同的是**不截整筆**:截掉第 21 筆以後的理由就無處可記。**寫檔之後 index 重建失敗(b13f R1 verify)**:三條移除腿與 `remove_fields`(以及 `update-venue --remove-reference`)呼叫仍回成功、報告多 `indexRebuilt: false`/`indexRebuildError`/`indexNote`——不擲錯,因為錯誤出口逐行截 400 字元、一段長理由會被截掉,而移除已落盤、理由只在報告裡、重試會被拒;`import-zotero` 的先例(b11b R1)擲錯並把整份報告放進錯誤訊息,那份報告是計數與 citekey 清單,短。呼叫端要讀 `indexRebuilt`;CLI 的 `query` 不看 mtime,要跑 `akashic doctor`(MCP 讀取面依 mtime 自動重建)。 **WriteGate**:`update-entry` 整個命令是一格(過閘,`--apply` 才閘),第三、第四條腿都不新增格;一格就夠的理由(b13f R1 verify 第 14/43 列):裁決表的粒度是命令,只有 `resolve-*` 與三個逐腿命令才分腿(它們的腿各有不同的閘決定);`update-entry` 的四條腿決定相同——閘在命令入口、看 `--apply`,每條腿都預設乾跑、實跑才改寫既有記錄檔——所以新增第五條腿只要也預設乾跑就自動在同一格之內,不預設乾跑的腿要重新裁決(那時才需要逐腿);兩面同一個 payload、沒有有記錄的差異(`zoteroSourcesRemaining` 兩面都截 20 筆並附 `zoteroSourcesRemainingTotal`——一筆 work 的附加來源數不受本工具限制)。同輪 `Entry.validate()` 對沒記 `library_id` 的附加來源報 warning(#679,per-record 的 warning,走既有的 `validate`/`akashic_doctor` 面,沒有新工具面),跨記錄的多筆宣稱 warning(#610)的出路改指這一條腿。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_create_entry` | `create-entry` | ✅(#206;#455 起兩面同走 `createEntries`——MCP 單筆是薄包裝,CLI 的 JSON 陣列是批次形,批次面的裁決見 CLI-only 表)。**#637 重新確認,裁決不變、契約有改**:DOI 已在庫(或同一批稍早的一筆已用過)時照常建,兩面同一份 `doiHits` 具名回報命中的 citekey(MCP 回應鍵、CLI 印 ⚠)——只回報不拒絕:DOI 相等依 `identity-is-judged-not-matched` 足以判定同一篇,但 erratum 會與原文共用 DOI,拒絕會擋掉這類合法記錄;CLI 的 `--dry-run` 也回報(寫入前看得到),MCP 面寫入前的查法是 `akashic_enrich` 的乾跑| +| `akashic_person` | `person` | ✅(#218)| +| `akashic_people` | `people` | ✅(#219)| +| `akashic_get_entry` | `get-entry` | ✅(#219)。**b11c R1(#614)重新確認,裁決不變、契約有改**:回應多 `akashic.sources`(連好的副本 digest;CLI 人可讀面多一行 `sources`)——`update-entry --add-source` 讓 store 第一次出現帶它的記錄,讀取面先前對它靜默 | +| `akashic_link` | `link` | ✅(#219) **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_tag` | `tag` | ✅(#219;契約已收斂——零參數兩面皆拒,守衛下沉 service 單一路徑,#258)。**#628 起 tag/link/set-status 在 citekey 重複或與另一筆 work 共用 id 時兩面都拒絕**(同走 `requireEntry`) **#641 起「無法唯一定位」含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)**。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_set_status` | `set-status` | ✅(#219;契約已收斂——省略拒絕、清除須顯式(CLI `--clear`/MCP `clear:true`),守衛下沉 service,#258) **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_add_person` | `add-person` | ✅(#250;寫入面封閉例外形。實證需求:#238 對 unkeyable 作者的處置就是單筆指定 key)| +| `akashic_divergences` | `divergences` | ✅(#250;讀取面 `--json`+人可讀同源。#218 同形:能寫不能讀的格)| +| `akashic_venue` | `venue` | ✅(#304 venue change;讀取面 `--json`+人可讀同源;編年 list 由反向邊現算)。**#673 重新確認,裁決不變、契約有改**:多一個 `references`——通用 references(不是 verdict、不是 `paginated` 判定的那些:`issn`/`names` 的來源記錄,手改或舊資料另可能有 `authorized`/`note`),鍵名同 `akashic_update_venue.references` 的輸入形(`media_type`/`statement`/`rests_on`),所以讀取面看到的就是移除面收的形狀;有界(`AkashicService.venueReferencesCap`=25 筆,`referencesTotal`/`referencesTruncated` 揭露)——**25 是選的數字、沒有量測依據**(live 為 0),**限的是筆數、不保證輸出位元組**(CJK 與逃脫會膨脹;`akashic_venue` 沒有單一輸出的位元組預算,`works` 編年清單本來就無界,b13f R1 verify);沒有就不輸出這些鍵。在此之前 reference 寫得進去(#587)、讀取面看不到(`verdicts` 與 `paginatedJudgements` 各有自己的鍵,其餘沒有),定位一筆要移除的得先開 YAML。CLI 人可讀面逐筆印(judgement 的證據份數讀 `rests_on_total`、被截時標出,b13f R1 verify);兩面同一條 `venue()` 路徑,沒有有記錄的差異。字串逐一消毒且有長度上限(`value` 200 字元、`url`/`statement` 300;`rests_on` 只列前 5 個,`rests_on_total` 揭露),超過上限的形只用來認得,`--remove-reference` 的定位仍以逐字的值為準。**#675 重新確認,裁決不變、契約有改**:`names[]` 自此多帶 `source`/`note`(各截 300 字元;沒有就不輸出)——`update-venue --edit-name-segment` 的 `match`/`set` 收這兩個鍵,而讀取面在此之前看不到它們,同名的段只差 `source` 或 `note` 時(#565 起合併會拒絕的那種)呼叫端認不出要改哪一段(能寫不能讀,#218/#219 的形狀);鍵名同 `match`/`set`(`ended` 不是 `ended_unknown`);CLI 人可讀面在該段後印 `source 「…」`/`note 「…」`,兩面同一條 `venue()` 路徑、沒有有記錄的差異;讀取面截在 300 字元,超過的段要對照 YAML **#564 重新確認,裁決不變**:`references` 自此常態帶名字分類的判定記錄(`field: authorized`/`variant` 的「指定/確認/撤回:理由」),與 `issn`/`names` 的來源記錄共用 25 筆的上限——`referencesTruncated` 為 true 時沒列完,要看全部讀 YAML(#564 R1 verify F2 第 15 列) | +| `akashic_venues` | `venues` | ✅(#304 venue change;讀取面同源)| +| `akashic_add_venue` | `add-venue` | ✅(#304 venue change;寫入面封閉例外形,同 `add-person`)。**#554 R5 重新確認,裁決不變、契約有改**:`names` 走與 `update_venue.add_names` 同一個入口(`vetVenueNames`——canonical 入庫、空白項略過、近重複留一筆、含不合法字元或無字母無數字整個呼叫拒絕、全空白拒絕),兩面描述於 R6 同步(R5 verify 第 14 列抓到只改了 update 面的描述) **#587 重新確認,裁決不變、契約有改**:`issn` 收角色(與 `update_venue.add_issn` 同一個解析函式),報告多 `issnMediumRecorded`/`issnDropped`;兩面同契約。 **#564 修正輪重新確認(R2 verify 補記,b29 V1 第 21 列),裁決不變、契約有改**:`names` 經 `vetVenueNames` 之後單一名字至多 65,536 位元組(`AddOnlyEnrichment.maxValueBytes`,在 canonical 之前擋),超過整個呼叫拒絕——修正輪為名字分類面加的上限,同一個入口也套在 `add_names`/`add-venue --names` 上;修正輪之前的 binary 收 70,000 位元組的名字。兩面同一個入口。 | +| `akashic_update_venue` | `update-venue` | ✅(#306;寫入面封閉例外形;append 語意——整組替換刻意不提供,R3F-2 教訓)。**#471 起兩面同時新增 variant 寫入**(MCP `add_variant`/CLI `--add-variant`,append 語意同 `add_names`):在此之前 variant **兩面都沒有寫入面**,唯一的寫入者是 `migrate-venue-variants`(#567 退場刪除)——而它用的是「`authorized` 的補集」。**一個不做判定的操作成了唯一的判定寫入者**,正面撞上 `identity-is-judged-not-matched`:「這個名字是那個名字的異寫」是判定,不是「不在對外清單裡」的推論(`two-kinds-of-edits` 同向:判定型寫入要有自己的面,不能是決定論式遷移的副產品)。**不在 `names` 的一併 append 進 `names`**——兩個分割都是對 names 的標記,標一個 names 沒有的字串會造出孤兒,而孤兒 variant 自 #473 起是 error。分割互斥與孤兒由 `Venue.validate()` 擋,寫入面不重造。**#554 起兩面同時新增 authorized 的判定寫入**(MCP `authorize`/CLI `--authorize`)——**它不是 append,這是端到端測出來的**:`AuthorizedNames.validate` 對 authorized 有「每書寫系統至多一個」的內容約束,而 2026-09-11 實測 470 筆 venue 已有一個 latin authorized(`VenueBootstrap` 取第一個名字的機械值;`addVenue` 則**不寫**,走 #227 的「建檔不機械偽造」語意),append 第二個 latin 名必被擋——第一版就是照 `add_variant` 做 append,對那 470 筆全部無用。所以語意是**同書寫系統原子替換**:X 成為對外形、原本的 Y **移出 authorized、留在 names、不標 variant**(R1 verify D1,使用者 2026-09-12 裁決:呼叫端只說了「X 是對外形」,程式不替它多說「Y 是異寫」——`venue-entity` spec 的未標是「不作任何宣稱」的誠實狀態;而 Y 若是沿革前身(names 帶時間欄位),標成 variant 會撞 variant 的時間不變式、整個呼叫被拒,DA 席實測)、報告 `authorizedRemoved` 逐筆印出;X 若原本在 variant(被 #553 降過去的)從那個分割移出、報告 `liftedFromVariant`;已是 authorized 的報告 `alreadyAuthorized`(no-op 但不沉默)。**「書寫系統」是 `WritingSystem` 的三個值 han/latn/other**——不同值之間才是 append,`.other` 是一個桶(西里爾與假名互相替換,logic 席實測)。這是 #553 攣生合併「authorized → variant」降級**在該名字上**的逆操作(#565 起合併不再標 variant——被併者的 authorized 併入後未標,`authorize` 對未標的名字同樣收)——不是「精確」逆操作(#553 改一個名字的分類,`authorize A` 改兩個:A 升、同書寫系統的 S 移出),命名回到 `authorize` 而非 `add_*`,因為叫 add 會說謊。不在 `names` 的一併 append 進 `names`、分割互斥仍由 `Venue.validate()` 擋、不重造——與 variant 那段相同。**入口的輸入驗證**(都是**輸入**驗證,不是分割互斥的第二份副本——它們擋的是呼叫端的參數矛盾,不是 store 狀態;有幾道不寫數字,R3 verify 抓到「兩道」在同一個 diff 裡就過期):同一次呼叫把同一個字串既送 `add_variant` 又送 `authorize` 是兩句矛盾的話;同一次呼叫**兩個同 `WritingSystem` 的名字**也是(R1 verify 第 1 列、四席各自重現:迴圈逐一處理時第 N+1 輪會把第 N 輪剛升的移出,陣列順序決勝)——兩者都整批拒絕零寫入。兩個檢查都在空白過濾**之後**算(空白是「沒說話」,不是矛盾)。**相等走 `NameIdentity.canonical`,與 store 守衛同一條**(R2 verify 第 1 列,五路獨立命中):R2 之前入口是精確 `String ==`,而 R1 report 寫的「近重複 fail-closed」**只對撞到 variant 成立**——D1 讓被換下來的舊名留在未標,`Venue.validate()` 對 names 沒有 near-duplicate 檢查,於是 `--authorize "X "`(尾隨空白)對既有 authorized `[X]` **寫入成功**:帶空白的字串進 names、真名被移出、帶空白版成為 displayName。那句「fail-closed」是 D1 之前的量測被抄進 D1 之後的規則,沒有重量(`assertions-must-be-measured` §2 的形狀)。**R4(D6)→ R5(D8,使用者 2026-09-12):名字內容的不變式住在 store 邊界,不在任何一個寫入面**。R2→R4 三輪把閘裝在 `updateVenue` 的三個迴圈裡(精確相等 → 只 authorize 走 canonical → 三個迴圈一致),每一輪都修在看見的那一圈,R4 verify 指出同一個 `names` 欄位還有 `addVenue`(原樣存入、連空字串都收)與 `VenueBootstrap`(只 trim)兩個寫入者——`add-venue --names "X "` 種下的髒條目讓乾淨拼法永遠進不了。`Venue.swift` 的 dated-variant 守衛 doc 早就寫著答案:「守衛住在 validate → writeVenue 的交會處才擋得住所有路徑」。現在:**`Venue.validate()` 對 names/authorized/variant 逐條驗(error 級)**——canonical 形(前後/連續空白、tab、NFD 都不是)、不含控制/格式字元(與輸出閘 `UnsafeToEmitScalar` **同一份**定義再加整個 Cf/Cc/Zl/Zp 類別——R4 曾是 19 個例子的列舉、170 個 Cf 漏 149:ALM、TAG 字元;**不可見**用 Unicode 的 `Default_Ignorable_Code_Point`(R6——R5 verify 第 2 列:VS16/CGJ 是 Mn、Hangul filler 是 Lo,四類 generalCategory 都放行,`心\u{FE0F}理學報` 存成第二筆「不同」名字、`add-venue --names $'\u{3164}'` 建出 displayName 空白的 venue);ZWJ/ZWNJ 只在兩個脈絡合法——(a) 掛在同一文字字母上的 virama 之後(右鄰居若在,要是同一文字的字母/數字)、(b) 左鄰居是 join-control 文字的字母/標記/數字、右鄰居是同一文字的字母/數字(R6——R5 verify 第 1 列五路命中:「兩側是字母」對拉丁字母 fail-open、對 Malayalam chillu 與波斯數字 fail-closed;R7 再收:區塊裡的標點不算鄰居、連續 joiner 不算——R6 verify 第 1/19 列;R8 再收:virama 的基底要是**字母**、不是數字或標記(Codex,R7 verify 第 1 列)、兩側要同一文字、joiner 之後不能直接接標記或 virama(R7 verify 第 26/33 列);`NameIdentity.joinerIsLegal` 是封閉的兩個脈絡,區塊表補進 Mandaic/Syriac Supplement/Adlam 等草書文字——第 14/22 列);「至少一個字母或數字」的字母是 generalCategory 的 L 類,不是 `isAlphabetic`(R7 verify 第 15 列:一個孤立的 Arabic fatha 曾寫得進去);U+2800 BRAILLE PATTERN BLANK 顯式列入不可見(第 20 列))、至少一個字母**或數字**(純數字刊名 *1843*/*2600* 是真的,R4 曾寫「至少一個字母」)、三張清單各無 canonical-相等對(names 的例外:兩段都帶不相交時間的沿革改回舊名——R6,R5 verify 第 4 列:Sankhyā 1933–1960 → 2002–2007 合回同名那筆記錄在 R5 寫不進去;R7 起「不相交」用 `Venue.segmentsAreDisjoint`——一段有 end、另一段有 start、**兩端都是 ISO 8601 前綴**(R8,R7 verify 第 2/3/7 列三席:手改的 `2003-1`/`民國49` 曾解鎖豁免,`ISO8601Prefix.isValid` 早就在)、較粗粒度截斷後嚴格早於;端點相等與 attested-only 都不豁免——R6 verify 第 9/35/47 列指出 `DateRange.overlaps` 對混合粒度 fail-open,而這裡是放行條件;掃描先以 canonical 鍵分組,O(n) 不是 O(n²)——第 21 列);謂詞一份在 `NameIdentity.wellFormednessIssue`,訊息對操作者說改什麼(R6)。**D8 不進 `venue-entity` spec 是一個要記下來的裁決**(R6 verify 第 3 列:R6 說記了、實際沒記):#554 的裁決是「既有 tool 的新參數,不走 Spectra」,D8 把它擴成 store 不變式時沒有重開;規範文字住 `docs/store-format.md` §5.7(store 契約,與 §3.4 同級),spec 的 Requirement 是 #570。`canonical` 只丟 `White_Space` scalar、不刪任何其他 scalar(R6——R5 在 Character 上切,「空白+combining mark」整個 cluster 被刪,`add-venue --names $'Jour ́nal'` 存成 `Jour nal` 零回報)。**所有寫入者存 canonical**:`updateVenue`(`vetVenueNames`:canonical → 逐項驗 → 去重,順序不能反——R4 曾先去重再驗,`["New Journal", "New\tJournal"]` 兩種順序得到不同結果)、`addVenue`(同一個入口)、`VenueBootstrap`。相等只有一條:canonical 查找、回傳 **store 條目**——「先精確」在 Swift 裡不存在(`String ==` 是 canonical equivalence,R4 曾讓 NFD 輸入把 NFD 位元組寫進 authorized、非 Swift 讀者看到 `authorized ⊄ names`)。手改出來的違反(近重複對、非 canonical)在下一次寫入被 validate 具名擋下、零寫入——不猜、不靜默修(**含 `fmt`**:R6 起 `.venue` 分支與 `.person` 同樣跑 validate——R5 的裸 `encode(decode)` 讓 `fmt --check` 對一個 validate 報 error 的 store 印「✓ 全部已是 canonical form」;**含合併的 dry-run**:預測的 keeper 過同一道 `assertVenueWritable`;**`resolve-venues` apply/repoint/demote 改成 venue 先過閘再寫 entry**——R5 是 entry 先落盤、venue 才 throw,手改一筆尾隨空白的 venue 後 apply 會留下升格的 entry 與沒落的 verdict,Claude 代裁 D11;R7 補 `repoint` 對懸空 from-key 的具名拒絕——base 既有的 `venuesByKey[k]!` 在 D11 之前是「entry 已落盤再 crash」、之後是「零寫入再 crash」,對 MCP 面都是以合法參數殺死 server 的路徑,R6 verify 第 4/27 列(R8 把訊息的指路從 `--demote` 改成「救回檔案或手改 YAML」——`--demote` 對同一狀態也是 notFound,R7 verify 第 4 列);**R8 起 `repoint` 的 verdict literal 從 from-venue 的 confirmed verdict 逐字取回**,取不到就拒絕——#418 用 work 的 `title` 當 literal,之後 `--demote` 會把 venue 邊改寫成論文標題(DA,R7 verify 第 6 列;既有缺陷,兩輪在同一函式上動刀而沒看到);**work/person 合併對持有被併鍵 verdict 的 venue 在 commit 之前過寫入閘**(R8 當時的 `assertVenueHoldersWritable`——R9 起是涵蓋三種 holder 的 `assertHoldersWritable`,見後——preview 共用——post-commit 的 holder 遷移曾是第七個沒過閘的 venue 寫入者,dry-run 說 OK、apply 刪了檔、venue 留死 verdict,DA,R7 verify 第 5 列);**合併的被併記錄自己違反不變式時,訊息指向被併者**(`doomedRecordInvalid`,第 28 列:那個字串只在 doomed 的 YAML);拒絕訊息的參數名兩面各自正確(`add_names(--add-name)`,第 45 列)、原字串截 120 讓理由不被截掉(第 7/12 列));唯一的自我修復路是 authorized 裡同名不同位元組(NFD)的舊指定在 `--authorize` 同名時被換成 canonical——R5 至 R9 報在 `authorizedRemoved`,R10 起報在自己的桶 `authorizedRewritten`(見下方 R10 段;R10 verify 第 11 列抓到這一句仍以現在式說 `authorizedRemoved`,同一列內兩份描述)。**誠實邊界**:NFC 對 CJK 相容表意文字有損(U+FA10 塚 → U+585A;位元組層有損、Swift 層無損);~~`displaySafe`/`UnsafeToEmitScalar` 對 Cf 字元的**輸出**逃脫仍是列舉(TAG 字元在錯誤訊息裡原樣印出)——輸出閘另裁(#569)。~~ → **#569 已裁(2026-09-27)**:輸出閘改成性質,與名字閘、`escapingInvisibleScalars` 同一份定義;人可讀輸出保留 ZWJ/ZWNJ,JSON 出口逃脫它們。**#560 的程式缺口由本輪關掉**(`zero-instance-guards` 第 25 列)。**R9(R8 verify 39 列,DA 席缺席)**:joiner 的 (a) 支要求 virama 與跳過的標記都是基底的文字、詞尾 chillu 後的空白視同沒有(D22);(b) 支放行同一 Indic 文字的 virama(Bengali ya-phalaa ``,Microsoft Devanagari/Bengali 音節文法 `+H` 實取確認——R8 的「沒有正字法意義」對 Bengali 為假,D21);區塊表補 Devanagari Extended/Extended-A、Myanmar Extended-A/B;`repoint`/`demote` 寫 verdict 時退役同 holder 上同一配對的相反判定(D20——R8 逐字帶原 literal 之後,每一次合法的 repoint 都在 from-venue 留下 #486 的矛盾對,處置「刪掉另一個」沒有工具面),報告 `verdictsRetired`;from-venue 上同一 work 有 ≥2 個正規化後不同的 confirmed literal 時具名拒絕、零寫入(D23,verdict 不帶 index;live store 2026-09-12 實測 0);「找不到 confirmed verdict」的拒絕補出路(改回 `- literal:` 再 `--apply`);work/person 合併的commit 前閘擴到三種 holder(`assertHoldersWritable`,D24——R8 的 doc 說 person/organization 沒有內容不變式,假的:兩者的寫入閘都跑 `validate()`)。R8 對 `zero-instance-guards.md` 的 splice 錨到 row-18 的 heredoc、把第 17/18/20/21/22 列的量測段整段蓋掉——R9 從 a03933d 逐字復原,第 25 列的Python 鏡射自此只有一份(ISO 鏡射 `ISO8601Prefix.isValid`、`dated` 鏡射 `makesTemporalClaim`)。**R10(R9 verify 29 列,6 席齊)**:D20 的退役只在**配對由一條邊實例化**時做——verdict 不帶 venue index,同一 work 兩條邊指同一 venue 時只有一筆 confirmed,退役它讓另一條邊在任何工具面上都救不回來且 #486 的 warning 一起消失(六路命中、DA 真 binary 重現),`repoint`/`demote` 對這種 work 具名拒絕零寫入(D25;live store 2026-09-14:2,411 筆 work、同 venue 兩條 key 邊 0、兩條 literal 邊同配對 0);`verdictsRetired` 從整數改成逐筆具名(venue、欄位、value、statement——#553 合併會把被併 venue 的顯式 `--reject` 搬進 keeper,日後一次 repoint 退役它時被併檔已不在);`confirmedLiteral` 的去重改位元組相等(`matchingKey` 去重會把另一條邊的字還回去——DA 第 9 列);(b) 支收 virama 只限 Devanagari/Bengali 且 virama 之後要接同文字的字母(D26——R9 對十個 Indic 文字一起放行且不看右脈絡,`क\u{200D}\u{094D}` 與 `क्` 渲染相同);`add_venue` 回報存入的名字與 `namesDropped`(R4 起走 `vetVenueNames` 但 payload 仍回原輸入);`--authorize` 的 NFD 自我修復報在 `authorizedRewritten`(R9 讓同一字串同時落在 `alreadyAuthorized` 與 `authorizedRemoved`);`Venue.validate()` 的近重複訊息每組最多列 3 對(O(k²) 則訊息由讀取路徑對未信任 store 觸發);holder 閘移到 `fieldsLostByMerging` 之後。**R11(R10 verify 29 列,5 席——DA 席 errored)**:D25 只驗原始 entry 裡的 from 配對,`newKey` 不看——`repoint` 自己就能造出 `[key V, key V]`(logic 席真 binary 重現:alpha 上兩筆 confirmed、beta 的 confirmed 被退役、validate 全綠、之後兩條邊都動不了),而 `apply` 對兩條同刊名的 literal 邊各 apply 一次也造得出(requirements/regression 席:R10 自己的測試就是證據)。**D27**:配對的唯一性對改指**之後**的邊集合驗,同一批同一 work 的兩個 move 不得帶同一個 literal(verdict 以 (work, literal) 為鍵,逐 move 的退役會互相覆蓋、留下哪一側取決於輸入順序;literal 不同時交換是對的);**D28**:`apply`/`repoint` 在會造出同一 work 兩條 key 邊指同一 venue 時具名拒絕零寫入(生產端 fail-closed),`Entry.validate()` 對既有的這種 work 報 warning(`zero-instance-guards` 第 26 列),移除面另案 #572(作者位有 `--drop-author`/`--un-split`,venue 邊沒有——`replace-endnote-and-zotero` 第 4 條);D25 的 `.literal` 支只對 repoint 算(demote 沒有 to-venue,literal 邊在該 venue 上不可能持有 confirmed,R10 對 demote 也拒且出路等於叫人手做那次 demote);**D30**:`verdictsRetired` 截 20 筆、`verdictsRetiredTotal`/`truncated` 揭露(`akashic_enrich` 的形——它是 payload 裡唯一由 store 內容決定體積的欄位;它迴送 verdict 的 value 與 statement,#569 的迴送點 4 → 6);`Venue.validate()` 的近重複掃描求值也設上限(5,000 對 ≈ 100 筆同名段,超過即 error——R10 的上限只綁訊息,全豁免的沿革組仍 O(k²),真 binary 4,500 段 20 秒全綠),「另至多 M 對」改說「未評估」(已評估而豁免的對同樣沒被列出);`addVenue`/`update_venue.add_names` 的報告拆成 `namesRewritten`(canonical 形在 store、位元組不同)與 `namesDropped`(真的沒進——R10 用位元組相等,NFD 輸入同時落在 `names` 與 `namesDropped`);**兩面描述補 D20(會刪掉一筆人的判定記錄)、D23、D25、D27、D28、`verdictsRetired`,MCP `authorize` 補 `authorizedRewritten`、`add_venue` 補兩個桶**(R10 verify 第 6/12 列:R6 抓過只改一面,這次兩面都沒改);joiner 的 help 改寫成左右不對稱(右鄰不收標記);venue 合併的 `doomedRecordInvalid` 移到 `fieldsLostByMerging` 之後(與 R9 對 person/work 的先後原則對齊)。**R12(R11 verify 32 列,6 席齊——DA 回來了)**:兩個 HIGH 都在合併路徑,四席都沒碰到、DA 真 binary 重現——`mergedVenueKeeper`(與 person 側逐字同型)的 verdict 遷移用位元組相等去重而 `supersede`/#486 用正規化鍵,一次合法合併就造出永久的矛盾對;合併把同一 work 的兩條 key 邊塌成一條而兩筆正規化後不同的 confirmed literal 都留下,D23 之後那條邊永久不可 demote/repoint 且 validate 零診斷。**D31**:遷移以 `verdictEqualityKey` 去重,倖存者與被併者對同一配對持相反判定時合併具名拒絕(person 同);**D32**:塌邊會留兩筆不同 confirmed literal 時拒(出路:先 demote 一條)。**D33**:`apply` 的 D28 閘從整批拒絕改成**逐筆略過並具名**(`skippedDuplicateVenueEdge`)——一筆毒候選曾讓同批無關的候選零寫入、每次重列都再提(campaign 是照 listing 全量 apply),既有的重複邊(手改/舊 binary)曾把同一 work 上不相干的歸戶鎖死、訊息還把因果歸給這次 apply;store 狀態不符是「該筆略過並具名」那一類(`judge` 的先例)。D27 收窄:唯一性只看**被動到的邊**,同 literal 的兩個 move 只在 venue 集合相交時才拒(不相交的各在自己的檔裡退役、與順序無關——R11 的訊息在那一格為假、出路「分兩次呼叫」直接到達被拒的狀態)。`VenueResolver` 的否決抑制改比 `matchingKey`(與 `verdictEqualityKey`/#486/D25 一族同粒度,`PersonResolver` 早就這樣;R11 放寬 demote 的 literal 支後,apply → demote → apply 三步全工具面造出永久矛盾對)。沿革豁免要求兩段本身是有效區間(倒置日期曾解鎖放行)。`verdictsRetired` 以性質逃脫不可見 scalar(Cc/Cf/Zl/Zp/DI——#569 的局部圍堵);no-op 早退也帶 D30 三鍵;訊息索引改「index N」(0-based)且列舉有上限;`namesRewritten` 拆成 `namesFolded`(這次折過才存)與 `namesAlreadyPresent`(本來就在,不論位元組——冪等但要說);**R28 起同批位元組相同的重複也進 `namesFolded`**(R27 verify Codex 第 7 列:兩面描述承諾回報同批去重,而 `afterBytes` 是集合、對 `["Journal", "Journal"]` 零回報);§5.7 補第 5 條(求值上限)、配對唯一性搬到 §3.5 成 normative 段;guards 第 26 列改寫「合併不是入口」。**R13(R12 verify 43 列,6 席齊)**:七個 HIGH 全在 R12 剛加的合併閘上——D31 只比 keeper↔doomed(三方合併的 doomed↔doomed 相反判定兩筆都進 keeper,五席同指)、且只裝在 person/venue 自己的 references 上(DA 真 binary 全工具面:work 合併的 holder 遷移把 `confirmed :: work:keep` 與 `rejected :: work:doom` 都改寫成 keep,#486 矛盾對)→ 累積比對+閘裝在 `assertHoldersWritable`(三種 shape 共用);D32 的 `hits.count > 1` 守的是「塌邊」這個症狀(DA:不塌邊也到得了同一終局;logic:keeper 既有的違反擋住無關的合併、出路被 D25 堵死)→ 改守不變式本身(這次新增的 confirmed literal 讓 keeper 對某 work ≥2 個正規化後不同的 confirmed 即拒,keeper 既有的不擋);遷移去重丟掉的判定記錄零回報(四席)→ 逐筆進 `verdictsCollapsed`;repoint 同 literal 的相交檢查只比相鄰兩筆(Codex+logic:三個 move 的第 1 與第 3 個相交逃過)→ 全配對;MCP 組合腿的 `skippedBecauseRejected` 只比 id 字面而抑制已比正規形(DA:reject 已提交、apply 回一句指錯路的 notFound)→ 以正規化配對算;性質式逃脫從 `verdictsRetired` 下沉到 Core 的 `displaySafeInvisible`,名字不變式的訊息、`doomedRecordInvalid`、`confirmedLiteral` 的 literal 列、合併拒絕訊息共用,補零到四位、加 U+2800/非 ASCII 的 Zs/Co(security 四列);`skippedDuplicateVenueEdge` 的訊息分「既有的 key 邊」與「同一批稍早的候選」(先到先寫,順序由呼叫端決定——兩面描述寫明);`add_variant`/`authorize` 的全空白項回報在 `variantDropped`/`authorizeDropped`;`fmt` 的 `.organization` 分支補語意驗證(三個同形 shape 只修了兩個);apply 的重複邊預掃改 `Set`(O(entries×requested) → O(entries));`assertKeyEdgesAreUnique` 的 doc 不再說 apply 會拒;`assertPairingHasOneEdge` 的索引排序;§5.7 兩處「四條」→「五條」、§3.5 補 R13 三個收緊與「合併本身是一種移除面」;`supersede` 退役判定記錄卻沒有 trackedness 前置(security)另案 #573。**R14(R13 verify 33 列,6 席齊)**:三個 HIGH 中兩個是同一格——holder 遷移的閘只補了「相反判定」那一半,work 合併純工具面(`--apply` 兩個刊名變體 + `resolve-divergence`)造得出 D23 從此拒的邊、validate 零診斷(Codex 第 1 列、DA 第 3 列);第三個是 dry-run 對去重丟掉的判定沉默(regression 第 2 列)。收成一個 delta 謂詞(D34:合併後 vs 合併前,keeper 與 holder 兩路同一份、兩半都算;既有的違反不擋——R13 的 holder 側是絕對謂詞、keeper 側漏單一被併者內部)、訊息印出處與遷移前原值、preview 與實跑對 `verdictsCollapsed` 同源(D35)、`Venue.validate()` 對同一 work ≥2 個 confirmed literal 報 warning(D36,`zero-instance-guards` 第 27 列)、近重複與 bootstrap 的訊息以性質逃脫、三則入口拒絕訊息項數截 10、`skippedDuplicateVenueEdge` 列出既有重複邊的全部索引、repoint 相交訊息印兩個拼法、兩面描述補 `variantDropped`/`authorizeDropped`(R13 verify 第 26 列:第三次只做一半)、sink 守衛認得 `displaySafeInvisible(`。**R15(R14 verify 31 列,6 席齊)**:兩個 HIGH——D34 的差集在配對鍵層算而配對鍵含 holder,被併鍵上的既有違反被判成新(logic 第 2 列,四席同指;D37 改在索引層算、訊息分「帶進來的」與「既有的」);生產端的 fail-closed 只在合併有,apply/repoint 對「目的 venue 已對該 work 持有另一個 confirmed literal」照寫(Codex 第 1 列;D38 兩面同契約:`apply` 逐筆略過並回報 `skippedConflictingConfirmedLiteral`、`repoint` 整批拒絕);D36 只掃 `matchingKey` 不同的而 D23 比位元組(Codex 第 3/requirements 第 5 列;D39 兩類都掃、家族前綴、`StoreHealth` 計數、每筆 20 則上限);收攏丟列印遷移後的 value(DA 第 10 列;D40 印原值、逐段截、rename 的 CLI/App sink 同一種逃脫);`migrate-venue-variants --apply` 一律拒絕(regression 第 9 列;D41);MCP/CLI 描述補 D38 與否決抑制的正規化鍵(regression 第 21 列);guards 第 27 列的 Python 鏡射對齊 Swift 的空白集合與 grapheme 層連字號(logic 第 17/DA 第 23 列);malformed 回退鍵加不可碰撞的前綴(security 第 28 列)。**R16(R15 verify:30 findings 合併成 19 列,6 席齊;列號=報告公布的合併列號——R17 verify 第 6 列:R16/R17 兩句先前用的是合併前的 finding 編號,越界且對不上)**:D42(第 1 列)、D43(第 3 列)、D44(第 2 列)、D45(第 6 列)、D46(第 5 列;#574);第 4/7/8/9/10/11/12/13/14 列逐項修;第 15/16/17/18 列記錄不動——細節在 changelog「R15 verify」節與 §3.5,本列自 R16 起只記編號(第 18 列)。**R17(R16 verify:31 findings 合併成 24 列,6 席齊)**:D47(第 1 列)、D48(第 2 列)、D50(第 7 列)、D46 更正(第 8 列;#574);第 3/4/5/6/9/10/11/12/13/14/15/17/18/19 列逐項修;第 16/20/21/22/23 列記錄不動;第 24 列開 #575。**R18(R17 verify:31 findings 合併成 20 列,6 席齊)**:D51(第 1/2 列)、D52(第 7/8 列)、D53(第 3 列)、D54(第 5 列);第 4/6/10/11/12/15/17/18 列逐項修;第 9/14/19 列記錄不動;第 13 列開 #576。**R19(R18 verify:5 席在起跑就撞上 session limit、只有 Codex 席回 3 列——不是一次完整的 ensemble,記為不完整、R19 落地後重跑)**:D55(第 1 列 HIGH)、D56(第 2 列)、D57(第 3 列);搭車:`Package.swift` 為 `AkashicKitTests` 補宣告 `AkashicMCPKit`(Xcode 27 的預設建置系統 swiftbuild 只連宣告過的依賴;三個外部探針測試又寫死 native 佈局,pre-push 暫釘 `--build-system native`——#577)。**R20(R19 verify:34 列,5 席齊、Codex 席 HTTP 429 至 09-19)**:D58(第 1 列 HIGH:rename 對早已指向新鍵的 verdict 不論 field 一律丟、收攏列印 rests-on)、D59(第 7 列:`count`/`errors`/`total` 是訊息則數、被截時同為下限);doctor 名冊補 `deadVerdicts`/`contradictoryVerdicts`、App 補後者、反射守衛改掃源碼(第 12 列——R18 的守衛只看 stored property,對 computed 的家族從沒生效);`VerdictEdgeSet` 驗 StoreKey、分隔符 U+0000(第 19 列);`Package.swift` 的閉包不變式由 `PackageManifestTests` 守(第 14 列);hook 重指失敗中止、`ci.yml` 釘 native(第 10/30/34 列);§3.5 寫明第二半的掃描面只有 venue×work(第 4 列)。**R21(R20 verify:26 列,5 席齊、Codex 席仍 429)**:D60(第 1–5 列 HIGH:D58 的丟棄只在 holder 另有被改寫 verdict 時生效,且生效時是無乾跑、無逆操作的判定刪除——改成 rename 對目的鍵上已有 verdict 的 holder 具名拒絕、零寫入,與 merge 的 D31/D34 同向);hook 的重指移進守衛階段並驗 readlink(第 6/13 列:build 後重指讓以 repo root 為 cwd 的 hook 測試改寫真工作樹;`ln -sfn` 對真目錄建巢狀連結回 0);`ci.yml` 的 release 步驟同樣釘 native(第 7 列);`PackageManifestTests` 遞迴、讀不到即紅、母體取自 `.testTarget(`、target 名 `-`→`_` 映射、掃描數與 `Tests/` 對帳(第 12/16/17/19/22 列);App 的 rename sink 上限對齊 CLI 的 1,000(第 14 列);doctor `first` 註解不再說「count 完整」(第 11 列);App help 的 20 改讀 `Entry.perRecordWarningCap`(第 20 列);第 8 列(manifest 位元組預算)開 #578、第 15 列(`venues[].key` 不驗 StoreKey)開 #579;第 9/10 列的外部寫入更正(#554 body、R19 report 第 1 列 errata、#561 的 pointer 真的貼出)。**R22(R21 verify:38 列,5 席齊、Codex 席仍 429)**:D61(第 1/8/12/16/36 列:D60 的母體補 quarantined 檔,行級比對、讀不到即拒)、D62(DA 第 14 列:折疊只折完全相同的重複);拒絕訊息每個 digest 自己一行、至多 20 筆、出路不指 `repoint`(第 3/7/9/10/11/15/17/24/25/28 列);`renameEntry` 補 `oldKey != newKey`(第 4/20 列);`census-parity.yml` 與 `ci.yml` 的 `swift run` 釘 native、兩處 ln 驗 readlink(第 5/22/26/38 列);hook 對真目錄在 ln 之前就拒(第 26 列);`PackageManifestTests` 改問目錄在不在(`enumerator` 對不存在的目錄回非 nil——第 13/32 列)、模組名碰撞即紅(第 23/29 列);§3.5 的 D55 段改成過去式(第 6/30 列);本列「兩處」→「三處」(第 35 列)、`--yes` 列的「6 個」→ 量測(第 33 列)。**R23(R22 verify:37 列,6 席齊——Codex 席回來了)**:D63(第 1/2/3/4 列四席 HIGH:D61 的行級 needle 含空白、被 YAML 折行擊穿,live store 2 筆——改位元組比對 `:`,quarantined 命中先列、上限走 `Entry.perRecordWarningCap`)、D64(第 14 列:D62 留下的同鍵異 judgement 有掃描面 `duplicateVerdictRecords`,doctor/App 各一格);CLI 兩處收攏抬頭改 D62 語意(第 7/10/15/26 列)、兩份 report doc 與 `migratedVerdicts` 不再說「同一條不變式」(第 8/11 列)、`describeDedupedVerdict` 印 rests-on(第 6 列)、D62 折疊改 `Hashable` 字典(第 5/21/24/35 列)、400 的算術改成量測且只截一次(第 9/23/37 列)、hook 訊息(第 34 列)、§3.5 R18 段標記 rename 退出(第 20 列)、`--yes` 列補 `resolve-venues` 不在表(第 31 列);第 17/19/27/30/36 列記錄。**R28/R29(R27 verify 50 列→D80;R28 verify 50 列、11 HIGH→D81)**:兩面的錯誤出口收成同一條邊界——store 字串在擲出端逃脫一次、之後每一層只截;`ServiceError` 的 140 個建構點與 CLI 的 `ValidationError` 包裝納入同一條規則,MCP 的 per-tool 錯誤出口與 `Main.swift`、App 的六個 errorMessage、CLI 的五個 `ValidationError(error…)` 包裝改走 `displaySafeErrorMultiline`/`displaySafeError`(R28 verify 第 1/7/15/17 列:MCP 面對每一則已消毒的訊息再逃一次,`\A[a-z0-9]…\z` 被改寫成不存在的正則);自帶消毒的錯誤型別以 `SanitizedErrorDescription` protocol 宣告、守衛掃全樹(第 5/13/14/19/37 列:R28 的 `is` 鏈住在 StoreIO、`ServiceError` 在鏈外)。**這一格兩面契約不變**——變的是錯誤訊息的位元組,兩面自此對同一個錯誤印出同一個字串(R28 之前 CLI 只截、MCP 再逃,同一個 store 兩面看到不同的訊息)。**R30(R29 verify 40 列、9 HIGH→D82)**:「同一個字串」在 R29 只對沒被截斷的訊息成立——MCP 先截再加 `Error: `、CLI 先加再截,差 7 個字元(第 9 列);R30 把前綴接進 `displaySafeErrorMultiline` 再截,CLI 的 `ValidationError` 包裝改送**逃一次不截**的 `displaySafeErrorText`、只在頂層 sink 截一次,兩面自此對被截斷的訊息也逐字相同(`testTwoFacesAgreeEvenWhenTruncated`)。同輪:`StoreVersionError`/`AuthorshipCompletenessValidationError`/`CorpusDiagnostic` 補 conform、`StoreIncarnationError.unreadable` 兩端各逃一次改成擲出端逃、11 個 `"\(error)"` 入口收攏、`displaySafeAssembled` 也退讓、96 KB 上限量最終輸出、`displaySafeError` 的 `max` 改成輸出上限(MCP 的 `applyDict["error"]` 真的是 512 了);守衛的型別集合與 needle 全部由原始碼現算、payload 粒度比對。**R31(R30 verify 42 列、1 CRITICAL/10 HIGH→D83)**:R30 的「先逃完再截」讓逃脫沒有上界——400,000 個 ZWSP 的單行讓 `akashic validate` 跑 20 秒(接近二次,R29 是有界的);R31 把**輸入上限**(`ErrorDisplay.inputScalarCeiling`=4,096,每行只讀這麼多)與**輸出上限**(sink 的 `max`)分開,截出來的字串與無界版本逐字相同、工作量線性;單行家族先折 LF 再截、96 KB 以位元組計、CLI 頂層對非 ArgumentParser 的原始錯誤也先逃一次(R30 只有五個 `ValidationError` 包裝走新入口,直接傳到頂層的 Yams/Foundation 錯誤 ZWSP 原樣落 stderr);守衛的豁免只看註記文字(R30 一句不相干的 `display-safe-exempt: Int` 讓整個語句免檢)、裸回傳 `return why` 判成「擲出端必逃」(R30 把 `ServiceError.invalid` 的 149 個站點判成沒用到、一個都沒檢查)、conform 的判準只看 Error → 文字那條路徑。**這一格兩面契約不變**,`sourcesAuditError` 進 MCP payload 前再截 512(R30 以 2,400 進去,是 doctor payload 唯一沒再截的欄位)。**R32(R31 verify 47 列、1 HIGH→D84)**:只截支(`displaySafeClipOnly`/`displaySafeAssembled`)的預算改以**輸出** scalar 計——R31 只把 LF 折成八個字元,TAB/CR/LS 在自帶消毒的描述裡照樣八倍膨脹;守衛的豁免只對非字面裸引數整段放行、字面逐插值(R31 一句 `label 是字面常量` 讓同一字面裡 store 名字的消毒免檢)、`"字面" + expr` 的運算元也掃、helper 查表連 owner、UUID 由描述端的 `.uuidString` 成員鏈釘型別;`updateVenue` 的同書寫系統衝突清單改性質式(rename 閘與 atomicWrite 遮罩不是本工具的能力、不在本列——R32 verify 第 29 列,記在 changelog)。**R33(R32 verify 39 列、0 HIGH——PASS,in-scope fix)**:`authorize` 對被 `field: authorized` reference 指著的舊指定具名拒絕、零寫入(Codex 第 2 列:R32 之前由寫入時的孤兒附著驗證拒、不具名也不給出路;程式不替人改判定,D60 同向——value 改成新的對外形或刪掉它都是人的事);`updateVenue`/`addVenue` 回傳的名字鍵性質式(第 9 列:同一函式的錯誤路徑 R32 已改、成功 payload 仍列舉式)。**兩面契約不變**。**`migrate-venue-variants` 在 #554 之後不得再跑**:它的補集規則會把 D1 留在未標的舊指定重新標成 variant(R2 verify 第 3 列,DA 席實測);#567 已退場刪除。~~**不留 judgement**:與 person `authorize-names`(#81)、`add_variant`(#471)同為不留,三個名字分類面一次裁(#564)——這一格因此是有記錄的裁決,不是遺漏;後果是 #553 合併端仍分不出「人確認過的 names[0]」與機械值,那格維持提醒不擋、觸發條件改綁 #564。~~ → **#564 落地(2026-10-01)重新確認,裁決不變、契約有改(BREAKING)**:`add_variant`/`authorize`/`unauthorize` 三條腿自此**必附 `judgement`**(CLI `--judgement`)、證據 `rests_on`(`--rests-on`)可省略;兩面同走 `updateVenueArguments`,缺理由整批拒絕、零寫入,排在名字 vetting 與兩句矛盾的檢查之後、讀 store 之前(#654 的形狀,CLI 的 `validate()` 呼叫同一個函式)。每次指定、確認(對已在分割內的名字再說一次)、撤回、標異寫各寫一筆 `field: authorized`/`variant` 的判定記錄,回報 `judgementsRecorded`;需要 store format ≥ 22。與 `paginated`/`clear_paginated` **不同一次呼叫**(兩個判定各要自己的理由)。**沒有有記錄的差異**:兩面同一條路徑,CLI 只回 service payload。撤回面(把名字從 authorized 移出而不放新的進去)是 #559 的 `unauthorize`;variant 沒有獨立的撤回面(只有被 `authorize` 抬出時連帶撤回)。**#394 起兩面同時新增 ISSN 寫入**(MCP `add_issn`/CLI `--add-issn`,append 語意同 `add_names`——ISSN 本來就是清單,print 與 electronic 是兩個真的號)。**這是本檔「識別碼寫入面的裁決」那一節記為「最弱的一列」的第一格補齊**:在此之前識別碼只有遷移路徑(從 `fields` 殘留搬值),查到一個**新的** ISSN 時唯一的路是手改 YAML。相等看正規形(與 `IdentifierMigration.normalizedUnique` 同一條規則——兩面若用不同的相等,對「這本刊有幾個 ISSN」會給出不同答案);任一個不合法即整批拒絕、零寫入,與同函式既有的 `type` 值域檢查同型)。**#406 起兩面同時新增 `paginated` 判定寫入**(MCP `paginated`+`judgement`+`rests_on`/CLI `--paginated --judgement --rests-on`,同走 `updateVenue`):「本刊是否使用頁碼」是**判定**不是設定——必附理由與證據 digest(缺任一整個呼叫拒絕、零寫入;rests-on 的非空與形狀由 `ProvenanceReference` 平面 init 驗,單一驗證入口);(field,value,kind) 冪等、翻轉判定留史;`nil` 是誠實的未判定狀態,floor 檢查(`apa7Report` 的 `PAGES` recommended)只對 **false** 抑制、nil 與 true 照報。**#588 起兩面同時新增 ISSN 移除**(MCP `remove_issn`/CLI `--remove-issn`,同走 `updateVenue`;`=理由`):`add_issn` 補齊之後,掛錯刊的號(合法但屬於姊妹刊)仍只能手改 YAML,而 `akashic-verify-venue`(#556)是會寫進 ISSN 的通道。兩面同契約:理由必填、只進報告(`issnRemoved`,全文不截斷;使用者 2026-09-27 裁決)——git 保存的是移除前的檔,理由要進 git 得由操作者寫進 commit message;移除前要求 venue 檔已 commit、乾淨,否則整批拒絕零寫入;指向被移除號的 `field: issn` provenance 一併移除、逐號回報筆數(R1 verify:留著會讓寫入閘以孤兒 provenance 拒絕,而 venue 的 reference 沒有移除面,#587);不合法的號、同號重複、venue 沒有這個號、同一個號同時在 `add_issn` 與 `remove_issn`,都整批拒絕零寫入。同輪 `validate` 對同一個 ISSN 掛在 2+ venue 報 warning(`zero-instance-guards` 第 36 列) **#670 重新確認,裁決不變**:key 有不只一筆記錄的 venue 整批拒絕、零寫入(寫進哪一筆是猜),兩面同一條 service 路徑。 **#567 重新確認,裁決不變、契約有改**:`migrate-venue-variants` 退場後 `variant` 補上 format 14 的寫入閘,format < 14 的 store 上 `add_variant`/`--add-variant` 整個呼叫拒絕、零寫入,訊息指向 bump 到 14;兩面同一道寫入閘(`assertVenueWritable`)。 **#587 重新確認,裁決不變、契約有改**:(a) 兩面同時新增通用 `references`(CLI `--references ''`/MCP `references` 物件陣列,同走 `updateVenue`)——append-only、以 `byteExactKey` 略過位元組相同的(`referencesAlreadyPresent`),物件鍵名與 `akashic_update_person` 的 references 相同。形狀驗證走 `ProvenanceReference` 的平面 init(YAML decode 的同一個入口),寫入面只多四件事:鍵名嚴格、`kind` 要與給的欄位一致、歸屬(verdict 三欄只經 `resolve-venues`、`paginated` 只經 `paginated`/`clear_paginated`——那兩條路同時改值與判定史;**通用面自己只收 `issn` 與 `names`**——R1 verify 收窄:`authorized`、`note` 拒收,因為 venue 的 reference 沒有移除面,`authorized` 的 reference 會讓 `authorize` 換不了對外形、`note` 沒有寫入面)、上限(一次 200 筆、statement 4,096 位元組、rests_on 20 個、其餘字串 65,536 位元組);附著在合進記錄後以 `validateReferenceAttachment` 驗,所以同一次呼叫加的號與名字可以被指向;`field: issn` 的 value 以正規形入庫、`names` 的 value 以記錄上的拼法入庫(相等看 canonical);指向同一次 `remove_issn` 的號是矛盾、整批拒絕。~~**與 `update_person` 有記錄的差異**(person 側不動;兩面契約對齊另案):retrieval 的 `status` 必填(person 側預設 200)、不認得的鍵拒收(person 側靜默忽略)、有上限(person 側沒有)。~~ → **#674 對齊了:兩面是同一個解析函式**(`parseReferenceObjects`,`ReferenceWriteParsing.swift`),往 venue 這邊收——person 側自此也 `status` 必填不預設 200(#542 R2 對 `enrich` 裁掉過同一個預設)、不認得的鍵拒收、有上限,見 `update_person` 那一列;兩面各自的政策只剩「收哪些 field」(person 收 verdict 以外的、venue 只收 issn 與 names)與空陣列(venue 拒絕、person 仍是 no-op——有記錄的差異;#695 起兩面都拒絕)。(b) `add_issn` 收角色:一項是 `NNNN-NNNN` 或 `NNNN-NNNN (print|electronic|linking)`(`IdentifierTokenizer.singleQualified`——遷移的同一套切法、比遷移嚴:一項一個號、至多一個緊跟的註記,角色不在三值內即拒);入庫寫封閉值域的寫法;已在而沒有角色的號補上角色,已記的角色不同整批拒絕(改寫既有角色沒有面)。報告多 `issnAlreadyPresent`/`issnMediumRecorded`/`issnDropped`(整項空白,#556 R2 verify:先前靜默略過)。兩面同一個解析函式,CLI 的 `validate()` 在開 store 之前呼叫它。**#587 R1 verify 重新確認,裁決不變、契約有改**:(c) 通用 `references` 面收窄成 `issn` 與 `names`(見 (a));(d) 與 venue 合併(`resolve-divergence`,CLI-only,該列同步)的兩個耦合關掉:被併者 `field: issn`/`names` 的 reference 隨合併逐位元組搬到倖存者(不搬的話每個查證過的被併者都合併不了),同一個號的角色兩邊不同(或被併者有而倖存者沒有)具名拒絕;出路都指向這裡的 `--add-issn`/`--remove-issn`。**#673 起兩面同時新增 `remove_reference`/`--remove-reference`(references 的移除面)**:在此之前通用 `references` 寫得進去(#587)、刪不掉——寫錯或已不成立的只能手改 YAML(`replace-endnote-and-zotero` 第 4 條)。它是判定(`two-kinds-of-edits` 的 AI 欄),套使用者 2026-09-27 對移除面一族的裁決:理由必填(≤ 4,096 位元組)、只進報告(`referencesRemoved`,全文)、不寫進 store、不改 store format;移除前要求那筆 venue 檔已在 git 裡 commit、乾淨(`assertRecordsRecoverable`,與 `remove_issn` 同一支閘)。**形狀**:JSON 物件陣列(CLI 是一個 JSON 字串,同 `--references`)`{field, value?, reason, …縮小定位的鍵}`——不用 `=理由` 的字串形,因為 value 是任意名字(可含 `=`、`:`),字串切分沒有安全的分隔符。**定位**=`field` + `value` 的位元組相等(與寫入面去重的 `byteExactKey` 同一把:canonical 相等而位元組不同的是另一筆),可再以與 `references` 同名的鍵(`kind`/`url`/`retrieved`/`status`/`media_type`/`content`/`statement`/`rests_on`)縮小;定位不到、定位到多筆(列出各筆的區別讓呼叫端加鍵縮小;**位元組完全相同的重複不判定移哪一筆**)、兩個定位指到同一筆,都整批拒絕、零寫入。**收四格**:`names`/`authorized`/`issn`/`note`——通用**寫入**面只收前兩格(#587 R1),但手改或舊資料可能有 `authorized`/`note` 的 reference,其中 `authorized` 的會讓 `authorize` 換不了對外形(訊息叫人「刪掉它們」,在此之前沒有面刪得掉)。**不在本面**:verdict 三欄(`resolve-venues --demote`/`--reject`)與 `paginated`(`--clear-paginated`:撤回本身是帶理由與證據的判定、翻轉要留史,移除面會讓判定與記錄的值分岔)都具名拒絕並指路。**單獨呼叫**(Claude 代裁):不與 `update_venue` 的任何其他參數組合——其餘腿改記錄的值與名字分割,`remove_issn` 還會連帶刪 reference,與「移除的是哪一筆」交錯;日後放寬是可逆的、收窄不是。只移除 reference,它指的號或名字仍在(移除號是 `remove_issn`)。**沒有乾跑**:`update-venue` 整個命令沒有(同 `--remove-issn`),git 閘與整批拒絕零寫入是退路;~~CLI 也**不過目標 store 確認閘**(`WriteGateRulings` 的 `update-venue` 是命令層一格、`.notGated`,理由明寫這兩條移除腿;同族的 `resolve-venues --drop-venue` 是過閘的、`update-entry` 的移除腿預設乾跑)——`update-venue` 整個命令要不要有乾跑(連帶要不要閘)**待使用者裁決**,不在本列動(b13f R1 verify 第 10/39 列)。~~ → **#564 使用者 2026-10-05 裁決第 4 點:CLI 過目標 store 確認閘**(會刪判定記錄的移除腿一律過閘;`update-venue` 在 `WriteGateRulings` 改成逐腿);乾跑仍沒有。**縮小鍵只能指名有值的欄位**:沒給的鍵是「不參與比對」,不是「要求缺席」,所以寫不出「沒有 `media_type`」——同一 `(field, value)` 有兩筆 retrieval、只差其中一筆帶 `media_type` 時,先移除帶的那一筆、再對剩下的呼叫第二次(拒絕訊息在這個形狀下這樣說)。**有記錄的兩面差異(b13f R1 verify,比照 `akashic_update_entry.add_sources`)**:MCP 面的 `referencesRemoved` 每一筆都列(`field`/`value`/`reason`——理由只在報告裡有一份,不截),只有前 20 筆多帶 reference 的其餘內容(url/statement/rests_on…),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列。寫檔之後 index 重建失敗時呼叫仍回成功、報告多 `indexRebuilt: false`(理由見 `akashic_update_entry` 列)。通用寫入面對 `authorized`/`note` 的收窄**沒有動**——要不要收回通用寫入面是使用者的裁決(#673 明寫落地後重新裁決),本次只保證有了出路。**#675 起兩面同時新增 `edit_name_segment`**(CLI `--edit-name-segment ''`/MCP 物件陣列,同走 `editVenueNameSegments`):名字段(`TemporalValue`)的時間欄位、`source`、`note` 在此之前沒有任何工具面能改,而 #565 起合併對「同名、這幾格不同、又不能並存」的兩段具名拒絕、出路只有手改 YAML(拒絕訊息自此指向這個面,並保留「也可以手改 YAML」)。每項 `{name, match?, set 或 remove, reason}`:`name` canonical 相等定位;`match` 縮小同名多段(給了的鍵都要相符、字串與陣列比位元組,**`null` 是「要求缺席」**——與 `--remove-reference` 的縮小鍵刻意不同,那邊寫不出「沒有」而這邊的拒絕形狀正需要它,代裁);`set` 逐鍵覆寫(`start`/`end`/`ended`/`attested`/`source`/`note`,`null` 清除);`remove: true` 刪掉那一段。**兩面同契約**:判定型、理由必填且只進報告(`nameSegments[].reason`,全文不截)、不寫進 store、不改 store format;改寫前要求該 venue 檔已 commit、乾淨(`assertRecordsRecoverable`,issue `#675`),每一項都沒有變動時不寫檔、不過閘;改完的時間欄位要成立(`Venue.nameSegmentRangeIssue`:不矛盾、ISO 8601 前綴、`start` 不晚於 `end`);這次**造出的** `Venue.validate()` error 在寫之前具名拒絕並歸因到這次呼叫;移除一個名字的最後一段時,名字還在 `authorized`/`variant`/`field: names` 的 reference、或移除後 venue 沒有任何名字,具名拒絕並指路(`variant` 沒有移除面,只能手改 YAML);定位在呼叫前的記錄上,定位不到、多段(列出區別讓呼叫端加 `match`)、兩項指到同一段、逐位元組相同的重複段、輸入錯、venue key 無法唯一定位都整批拒絕零寫入;一次至多 200 筆。**組合規則(代裁,同 `remove_reference`):單獨呼叫**,不與 `update-venue` 的任何其他參數組合、也不與 `remove_reference` 組合——`add_names`/`add_variant`/`authorize` 會增減同一份 `names`,「定位的是哪一段」與報告、git 閘的語意會交錯;日後要放寬是可逆的,要收窄不是。**有記錄的兩面差異**(同 `remove_reference`):MCP 面的 `nameSegments` 每一項都列(`name`/`action`/`reason`),只有前 20 項多帶 `before`/`after`(第三方字串),其後的省略並以 `detailsTruncated`/`detailsListed` 揭露;CLI 全列;寫檔之後 index 重建失敗時呼叫仍回成功、報告多 `indexRebuilt: false`。**沒有乾跑**:同 `--remove-reference`,`update-venue` 整個命令沒有——git 閘與整批拒絕零寫入是退路,~~CLI 也不過目標 store 確認閘(`WriteGateRulings` 的 `update-venue` 命令層一格,理由已寫明新腿;要不要有乾跑仍**待使用者裁決**,不在本列動)~~ → 帶 `remove` 的呼叫自 2026-10-05 起 CLI 過目標 store 確認閘(#564 使用者裁決第 4 點;只有 `set` 的不閘),乾跑仍沒有。MCP manifest 描述只寫做什麼、`reason` 只進報告、git 閘與指向 CLI help(#578 的位元組預算)。 **#675 R1 重新確認,裁決不變、契約有改**:`set` 的 `source`/`note` 拒收控制、格式、方向與不可見字元(`UnsafeToEmitScalar` 人可讀輸出要逃脫的集合扣掉私用區,`match` 照收);`attested` 每個點至多 65,536 位元組;寫入前重讀閘回傳的記錄檔、被改過即整批拒絕;顯示名改變時報告多 `displayNameChanged`。兩面同一條 service 路徑;MCP 描述補 `match`/`set` 的鍵名與 `null` 的兩種意思。 **#695 重新確認,裁決不變、契約有改**:`references` 的空陣列拒絕搬進共用的 `parseReferenceObjects`,訊息改成兩面同一句(「references 是空陣列——沒有要附的 reference 就不要給這個鍵」;先前 venue 說「這個參數」);擷取型的形狀檢查搬到 Core 的 `RetrievalWriteShape`,被拒的輸入不變。兩面同一條 service 路徑。R1 verify 起「被拒的輸入不變」不再成立:同一個函式多拒四種,見 `akashic_update_person` 列的 #695 註記。 **#559 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決):兩面同時新增撤回(MCP `unauthorize`/CLI `--unauthorize`,同走 `updateVenue`):逐名、必須是現有的 authorized(相等看 `NameIdentity.canonical`,同 `authorize`),移出 authorized、留在 names、不標 variant——記錄回到「不作任何宣稱」,比照 `clear_paginated`(#500)與 `demote`(#418)。替換與撤回的邏輯自此住在 `AuthorizedDesignation` 一份(#557 的 organization 用它的替換;organization 沒有撤回面),`updateVenue` 呼叫它。兩面同契約:不是現有成員、同一個名字又在 `authorize`、被 `field: authorized` 的 reference 指著(指路 `--remove-reference`),都整批拒絕、零寫入;整項空白回報在 `unauthorizeDropped`;撤回先於 `authorize`(成員資格看呼叫前的狀態);與 `add_variant` 同一個名字是明說降成異寫、照做;`remove_reference`/`edit_name_segment` 的單獨呼叫也不與它組合。報告 `authorizedWithdrawn`(每列 `name`、呼叫前在 authorized 的 `index`);撤回改變預設顯示名時多 `displayNameChanged: {before, after}`(只在有撤回腿的呼叫裡算)。**撤回不是 `authorize` 的精確逆操作**(R1 verify):名字與分類(未標)回得來,但 `authorize` 對不在 authorized 裡的名字接在尾端(只有同書寫系統替換才插回原位),而 `displayName` 取 `authorized.first`——多書寫系統 venue 撤回再指定,預設顯示名可能換書寫系統;遺失的只有 authorized 內的順序,報告逐名帶原 index。**不設 git 閘**(使用者沒有裁決要閘;名字與分類都留在 names,與 `clear_paginated` 一致,遺失的順序由報告的 index 補)。~~判定記錄待 #564(2026-10-01 裁決要留、另案落地)。~~ → #564 已落地(見上):`unauthorize` 每個名字寫一筆 `撤回:理由`,先前的記錄留著;撤回記錄的名字就是 `authorizedWithdrawn` 各列的 `name`(同一份撤回,報告與記錄說的是同一批名字,store 拼法)。沒有有記錄的差異。 **#564 修正輪重新確認(使用者 2026-10-02 裁決),裁決不變、契約有改**:(a) 名字分類記錄只對**同一個名字同一個分割的最後一筆**去重——撤回之後以同一句理由再指定會寫第三筆(先前被當重複丟掉,記錄以撤回結尾而名字是 authorized);(b) `add_variant`/`authorize`/`unauthorize` 合計一次至多 200 個名字(第 4 點),單一名字至多 65,536 位元組;理由開頭不得是組合符號或不可見字元、要有字母或數字;(c) 報告多 `variantConfirmed`(已是異寫、寫了「確認」的名字);(d) `edit_name_segment` 刪掉最後一筆記錄是撤回的名字的最後一段時,記錄一起刪(第 2 點,報告 `judgementRecordsRemoved`);(e) 寫檔之後 index 重建失敗不讓呼叫失敗(報告多 `indexRebuilt: false`,同 organization——撤回的原位置只在報告裡)。兩面同一條 `updateVenue`;MCP 描述把 #564 整合時為舊預算刪掉的拒絕類別寫回(tools/list 預算 60,000,#578/#713)。**有記錄但未改的差異**:`update-venue` 對沒有任何變動的呼叫照樣重寫檔案與重建 index,organization 不寫(#557 R1)——兩個姊妹面對 no-op 的處置相反,留給 `update-venue` 整個命令要不要乾跑那一格一起裁。 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改(只有訊息與報告)**:`edit_name_segment` 對還在 variant 的名字的拒絕不再說「variant 目前沒有移除面,只能手改 YAML」——說出工具面的出口(`authorize` 抬出 variant、`unauthorize`、commit、再 remove)與代價(同書寫系統原本的對外形被換下要再指定回來;一次每書寫系統一個名字),CLI help 同步;`add_names` 的 65,536 位元組上限見 `akashic_add_venue` 那一列;名字分類記錄隨名字刪改成線性。兩面同一條 `updateVenue`。 **#564 b34 重新確認(使用者 2026-10-05 裁決第 4 點),裁決不變、契約有改**:CLI 的三條移除腿——`--remove-issn`、`--remove-reference`、`--edit-name-segment` 帶 `remove` 的呼叫——過 #298 的目標 store 確認閘(未指名 `--library`/`--yes` 時拒絕,拒絕訊息說這條腿沒有乾跑);`WriteGateRulings` 的 `update-venue` 從命令層的 `.notGated` 改成逐腿(其餘腿不刪東西、不閘)。**有記錄的差異**:MCP `remove_issn`/`remove_reference`/`edit_name_segment` 不閘(橫切表 `--yes` 列:active store 是 session 狀態)。兩面同一條 `updateVenue` **#564 b37 重新確認(b36 Y1 第 0/1 列),裁決不變、契約有改(只有報告)**:`edit_name_segment` 報告的 before/after 每段的 `attested` 至多列 20 個,超過時最後一項是「…另 N 個觀測點未列出(共 M 個)」(不新增回應鍵,同 `cappedCitekeys`);讀取面 `akashic_venue` 的 `names[].attested` 照舊全列(`match` 要逐項相同);`edit_name_segment` 的 remove 被拒、出口要經 `authorize`/`unauthorize` 寫記錄的三種(還在 authorized、還在 variant、最後一筆不是撤回)在 store format < 22 時另起一行說出寫入閘。兩面同一條 `updateVenue` | +| `akashic_resolve_venues` | `resolve-venues` | ✅(#304 venue change;兩面契約差異同 #272:MCP 允許 apply+reject 組合、CLI 分兩次呼叫)。**#418 起兩面同時新增 `repoint`**(三段式 id `citekey:venueIndex:newKey`,把**已歸戶**的邊改指到另一個 venue)——它是歸錯戶的退路:`apply` 只做 literal → key 的升格,此前一條已是 key 的邊改不回來,而 person 域有 `resolve-divergence`、venue 域沒有,且 `literal-first-then-key` 的整套論證建立在「誤可逆」上。**這一項兩面契約相同**(與上面那個差異不同):`repoint` **不與** `apply`/`reject` 組合,兩面都拒——它們是不同階段(升格 vs 修正已升格的),混在一次呼叫裡會讓「哪一批寫了」難以判讀,而改指本來就是在修一個錯誤,最需要清楚的失敗語意。失敗語意分兩類(同 #386 的 `judge`):語法錯或前提不符 → **整批拒絕零寫入**;成功則兩側都留 verdict(新的 confirmed、舊的 rejected——少了 rejected,下次提名會把同一配對再提出來)。**同輪另加 `demote`**(`citekey:venueIndex`)把**誤升**的邊退回 literal——`repoint` 只改得到既有 venue,而正確答案可能是「現有的都不對」,那時要能退回 `literal-first-then-key` 所說的**誠實狀態**。原字串**從該 venue 的 confirmed verdict 逐字取回**(第 13 條邊的 `: :: ` 文法),走 `ResolutionLedger.verdicts` 這個唯一解析器;**取不到就拒絕,不拿 venue 顯示名頂替**(顯示名不是那筆記錄原本寫的字——WoS 的 `PSYCHOMETRIKA` vs 正式刊名 `Psychometrika`,頂替會安靜改寫書目資料)。`repoint` 與 `demote` **各自單獨呼叫**,兩面同契約。**#573 起(使用者 2026-09-27 裁決)**:repoint/demote 會刪判定記錄(D20 的退役)時,要求那些 venue 檔已在 git 裡 commit——store 不在 git 工作樹、檔案未 tracked 或有未提交修改都整批拒絕零寫入,兩面同契約;**有記錄的差異**:`verdictsRetired` MCP 面截 20 筆、CLI 面全列(`retiredLimit`,理由同 `validate` 的面級截斷分工)。**change `resolution-verdict-states`(#619)起兩面同時新增 undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedVenues`;id `citekey:venueIndex:venueKey=說明`;單獨呼叫),列表的候選列與歧義條目帶 `undecidedChecks`。**venue 兩面都沒有篩選式批次 apply**(CLI `--apply` 收 id 清單),所以沒有 resolve-people 那種排除——只有揭露 **#628 重新確認,裁決不變、契約有改**:候選所在的 work 無法唯一定位(citekey 重複或與另一筆 work 共用 id)時,apply 逐筆略過並以 `skippedUnlocatable` 具名(D33 的既有契約:store 狀態不符逐筆略過),reject/repoint/demote 整批拒絕;兩面同一條 service 路徑。MCP 列表以 `unlocatableCitekey` 標出(CLI 只收顯式 id,沒有篩選式批次)。**#572 起兩面同時新增移除腿**(CLI `--drop-venue`/MCP `drop_venue`,同走 `dropVenueEdges`;`citekey:venueIndex=理由`):在此之前 venue 邊沒有移除面,同一 work 兩條邊指同一 venue 時 D25/D28 的拒絕與 `Entry.validate()` 的 warning 都只能指「手改 YAML」。**以 index 定位不以值**——重複的兩條邊值相同,值定位分不出要刪哪一條,而那正是這個面要處理的形狀;同一 work 多筆以原始 index、由實作處理位移(同 `split_author`)。**key 邊只在刪完後同一 venue 仍有另一條 key 邊時可刪**——那時 confirmed verdict 仍由剩下的邊實例化;刪唯一的 key 邊要先 `demote`(它處理 verdict),本面不替人決定那筆 verdict 的去留。literal 邊一律可刪。兩面同契約(使用者 2026-09-27 對移除面一族的裁決,同 `update_venue.remove_issn`):理由必填、只進報告(`venueEdgesRemoved`,全文不截斷)、不寫進 store、不改 store format;移除前要求那些 work 檔已 commit、乾淨,否則整批拒絕零寫入;輸入錯整批拒絕;單獨呼叫;CLI 另過 #298/#580 的目標確認閘。刪光一筆 work 的 venue 邊時以 `emptied` 具名——`migrate-venues` 與 Zotero pull 會從來源欄位重新推導。**#648 重新確認,裁決不變、契約有改**:apply/repoint/demote 的寫入前逐筆驗證自此是 writeX 在寫入當下跑的每一道(含 encode 與讀取上限)——venue 是 O(catalog) 的 verdict 持有者(#499),寫出後超過 8 MiB 即整批拒絕、零寫入、錯誤具名記錄與兩個位元組數;兩面同一條 service 路徑。reject 維持逐筆收容 **#641 重新確認,裁決不變、契約有改**:「無法唯一定位」自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存),repoint/demote/drop_venue 整批拒絕、apply 逐筆略過——先前 repoint/demote 在寫入當下被拒而撕裂(#631 R2 verify);各腿不必改,同一個 `unlocatableCitekeys`。 **#670 重新確認,裁決不變、契約有改**:候選指向的 venue 的 key 有不只一筆記錄時,apply 逐筆略過並以 `skippedUnlocatable` 具名(D33 的既有契約),reject/repoint(兩端任一)/demote 整批拒絕,undecided 該筆略過並具名;列表的候選列帶 `unlocatableVenueKey`。兩面同一條 service 路徑,CLI 原樣印出 payload。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#712 重新確認,裁決不變、契約有改**:列表(不帶寫入腿)多三個鍵 `suppressed`/`suppressedTotal`/`truncated`——被同 work 同 venue **另一個拼法**的 `reject`/`demote` 以正規化配對壓掉的候選(#554 R12 起的抑制不變,只是不再沉默;「沉底而非隱藏」)。**這一段不是「同 resolve-people 的 `rejected` 段」**(#712 R1 verify 第 4、39 列):people 的 `rejected` 段只列逐字等於被否決拼法的作者位,被同一筆否決以正規化鍵壓掉的兄弟拼法不在其中——resolve-people 與 resolve-organizations 的列表對它們仍然沉默、還會說「任何提名層皆無命中」,追蹤在 #721。判準只有一條(`VenueSuppressedCandidate`):壓住它的 rejected verdict 的 literal 沒有一個與它自己的 literal **位元組相等**——位元組相等是普通的已否決、列表一向不列;只差 NFC/NFD 的是另一個拼法、照列(**使用者 2026-10-05 裁決**:「同一個拼法」比位元組,與 `confirmedLiteral`、`byteExactKey` 一致;先前比 Swift `String ==`,那條邊在 `candidates` 與這一段兩邊都不出現。被否決的拼法也依位元組去重——`ResolutionLedger.rejectedPairings(venues:)` 自此回陣列、不經 `Set`;apply 改寫邊之前也比位元組。兩面同一條 resolver 路徑);歧義(對到 2+ venue)一向不被抑制、不進這一段。每列 `citekey`/`venueIndex`/`literal`/`venueKey`/`rejectedLiterals`(壓住它的拼法,至多 5 個、超過時多 `rejectedLiteralsTotal`;R1 起在 resolver 就截、另記總數,建表是線性的——第一版存全部、到輸出才截,N 列共用 K 個拼法時是 N×K),**刻意沒有 `id`**(不是可 apply 的候選)。永遠回這三個鍵:沒有候選被壓時是空陣列與 0。**有記錄的差異**(同 `verdictsRetired` 的分工):CLI 面全列(傳 `suppressedLimit: nil`)、MCP 面截 20 筆(`suppressedItemsCap`)並另受 `candidateByteBudget` 約束——吃不下的整列不印,`suppressedTotal` 永遠是全數、`truncated` 說被截;`truncated` 與 `verdictsRetired` 那一腿同名,兩者不會出現在同一次回應。venue 的 CLI 列表本來就是服務 JSON 的原樣輸出(沒有另一套人可讀排版),所以兩面的「計數行」是同一個鍵。 | +| `akashic_add_organization` | 無(單筆建檔 MCP-only;批次面 `bootstrap-organizations` 維持 CLI-only,見 CLI-only 表)| ✅ 有理由的單面(#304 移轉裁決:org 重啟後單筆建檔是 #303 campaign 的 LLM 消費流程;操作者規模的批次建檔另有 CLI 面)| +| `akashic_update_organization` | `update-organization` | ✅(#557;使用者 2026-10-01 裁決形狀 2:新增 organization 的部分更新面、兩面各一格,**只提供 `authorize`**)。在此之前 `Organization.authorized` **兩面都零寫入面**(`add_organization` 不收、`OrgBootstrap` 不寫、`authorize-names` 只管 person),`doctor` 的 `no authorized name: … organization` 恆為真且修不掉。寫入面封閉例外形(同 `update-venue`:只回 service payload、不設 `--json`),兩面同走 `AkashicService.updateOrganization`,替換的邏輯是 `AuthorizedDesignation`——與 `update-venue` 的 `authorize` 同一份。兩面同契約:同書寫系統原子替換,被換下的移出 authorized、留在 names;不在 names 的以 canonical 形加進 names;同一次兩個同書寫系統的名字、沒給、空陣列、只有空白項(三者同一件事:過了 vetting 之後沒有要指定的名字)、key 格式不合,讀 store 之前整批拒絕(CLI 用法錯誤 64);key 有不只一筆記錄(#669/#670)、找不到、被換下的名字被 `field: authorized` 的 reference 指著(organization 的 reference 沒有移除面,指路手改 YAML),整批拒絕零寫入;子集與每書寫系統至多一個由 `Organization.validate()` 在寫入閘擋。給的名字都已是對外名稱而同一句理由已記過時成功、報 `alreadyAuthorized`、**不寫檔也不重建 index**(一次沒有變動的寫入會重新序列化人手編過的排版;#564 起對已是對外名稱的名字說「確認」也留一筆記錄,所以第一次說確認是會寫的)。報告 `namesAdded`/`authorizedAdded`/`authorizedRemoved`/`alreadyAuthorized`/`authorizedRewritten`/`authorizeDropped`/`authorizedTotal`(organization 沒有 variant,不報 `liftedFromVariant`);**有事才出現**:`authorizedNotCurrent`(指定的名字在 names 的每一段都已結束——`Organization.authorized` 的「從當前有效的名稱中指定」是慣例、`validate` 不擋,本面是第一個寫得進它的面;不拒絕,`displayName` 會變成那個退役名,所以說出來)、`indexRebuilt: false`/`indexRebuildError`/`indexNote`(寫檔成功而 index 重建失敗:呼叫回成功,做法同移除面一族,`RemovalReportSupport.swift`)。**沒有 `unauthorize`(R1 verify 之後拿掉)**:裁決只說先提供 `authorize`,首輪實作順手加了撤回是實作者的判斷;而它在 organization 上不是 venue 那個「回到誠實的未判定狀態」——organization 的 names 只增不減(當時;#564 修正輪起 `remove_names` 只刪已撤回的名字,仍沒有一般的名字移除面),`authorize` 為了子集關係會把新名字加進 names,撤回後名字留著、authorized 回到空,`displayName` 的 fallback 是 `names.current`(沒有時間欄位時取序列化順序最後一筆),剛加進去的名字因此成為顯示名與匯出的 `name_current`。待使用者裁決(連同 organization 要不要有名字的移除面)。~~判定記錄待 #564(2026-10-01 裁決要留、另案落地)。~~ → **#564 落地(2026-10-01)重新確認,裁決不變、契約有改(BREAKING)**:兩面同時新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`,MCP `judgement`/`rests_on`,同走 `updateOrganizationArguments`)——`authorize` 的名字必附理由、證據可省略(至多 20 個 digest),缺理由整批拒絕、零寫入(讀 store 之前);沒有要指定的名字(沒給、空陣列、全是空白項)時照舊以「沒有要改的」拒絕,理由或證據單獨出現也一樣。每次指定、確認各寫一筆 `field: authorized` 的判定記錄(被換下的舊指定也寫一筆撤回),回報 `judgementsRecorded`,需要 store format ≥ 22;organization 沒有 variant,不會有 `field: variant`。**organization 沒有撤回腿**:`unauthorize` 於 #557 R1 verify 之後拿掉、待使用者裁決,所以 #564 裁決的五個面裡 organization 只有 `--authorize`。CLI 不過目標 store 確認閘(`WriteGateRulings` 的 `update-organization` 格,理由與 `update-venue` 同一格、與它一起待裁)。沒有有記錄的差異 **#564 修正輪重新確認,裁決不變、契約有改**:(a) MCP 對已拿掉的 `unauthorize` 鍵具名拒絕、零寫入(#557 R2 verify:server 對多餘的鍵一律忽略,帶它的呼叫照做 `authorize`、撤回被安靜丟掉並回報成功;CLI 面是未知旗標、exit 64——自此兩面都拒);(b) 兩面同時新增 `remove_names`(CLI `--remove-name`,使用者 2026-10-02 裁決 #564 第 2 點):只刪最後一筆名字分類記錄是撤回的名字(每一段連同記錄),理由只進報告、刪前要求 organization 檔已 commit;organization 沒有一般的名字移除面(仍待裁),這一條只是打錯字的出路;(c) `authorize` 一次至多 200 個名字、單一名字至多 65,536 位元組;(d) `authorizedNotCurrent` 只在機構另有現行名稱時出現(已解散的機構不報),訊息不再說「已結束」(只有觀測點的段不是已結束,#661)。沒有有記錄的差異 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:`remove_names` 的報告每列多 `segments`(刪掉的每一段的 start/end/ended/attested/source/note,與 venue 的 `edit_name_segment` 一致——第 20 列:一個沿革名的整段只剩 git 裡有);沒有記錄的名字被拒時說出「先讓它有一筆撤回」的兩步與代價(第 7/23 列);不在 authorized、最後一筆卻是指定的說「先指定回去再撤回」(第 6 列);「organization 的 names 只增不減」的三處文字(MCP 對 `unauthorize` 的拒絕、CLI abstract、寫入閘理由)改成「沒有一般的名字移除面」(第 8 列:`remove_names` 落地之後那句字面為假)。兩面同一條路徑。organization 刪名字面的範圍本身(#557 R1 的「沒裁過的公開契約先不上」標準,第 25 列)交給使用者。 **#564 b34 重新確認(使用者 2026-10-05 裁決第 4、5 點),裁決不變、契約有改**:`remove_names` 保留(第 5 點);**有記錄的差異**:CLI `--remove-name` 過 #298 的目標 store 確認閘(第 4 點,`WriteGateRulings` 的 `update-organization` 改成逐腿、只有這條腿過閘;沒有乾跑),MCP `remove_names` 不閘(橫切表 `--yes` 列)——上面「CLI 不過目標 store 確認閘」那一句對 `--authorize` 仍成立。`remove_names` 的報告 `segments` 只列帶時間、source 或 note 的段、每個名字至多 20 段(總數在 `segmentsRemoved`,b33 X1 第 22/27 列);沒有記錄的名字被拒時的出口說出換上去的名字成為撤不掉的對外名稱(第 20/29 列,先前說「同書寫系統沒有別的名字時只能手改 YAML」是假話)。兩面同一條路徑。 **#564 b37 重新確認(b36 Y1 第 0/1/7/15 列),裁決不變、契約有改(只有報告與訊息)**:(a) `remove_names` 報告的 `segments` 每段 `attested` 至多 20 個、超過時最後一項說出省略數與總數(先前只截段數,一段兩萬個觀測點一次回 46 萬位元組;不新增回應鍵);(b) `remove_names` 的拒絕在 store format < 22 時說出寫入閘與升級路徑(三條出口都經 `--authorize` 寫記錄;person 那條 b34 補了、這條沒有);(c) 上面「organization 刪名字面的範圍本身交給使用者」那一句由使用者 2026-10-05 裁決第 5 點了結(保留)。兩面同一條路徑 | +| `akashic_resolve_organizations` | `resolve-organizations` | ✅(#304 移轉;CLI-only 表「候補缺席(重啟訊號已觸發)」格的補齊——同 `import-wos`/#290 的移列形) **#628 重新確認,裁決不變、契約有改**:作者位候選所在的 work 無法唯一定位時,MCP 的 apply/reject 整批拒絕、列表以 `unlocatableCitekey` 標出;CLI 的篩選式批次排除並另列。~~**change `resolution-verdict-states`(#619):org 族的未決寫入面兩面都缺席**~~ → **change `org-undecided-leg`(#643)起兩面同時新增 undecided 腿**(CLI `--undecided`/`--rests-on`,MCP `undecided`/`rests_on`,同走 `recordUndecidedOrganizations`;單獨呼叫)。id 是 `<列表的 id>@=說明`:在每個 `@=` 的位置試切,前綴必須恰為這次列表的已知 id,恰一個才收(使用者 2026-09-25 裁決——org 的 id 帶 literal,第一個或最後一個 `=` 都會切錯);歧義條目也帶 id、可逐個 org 記。兩面同契約(整批拒絕、逐筆略過、`alreadyRecorded`、呼叫層三個上限加單筆 id 長度上限、person/organization 同 key 兩列都在列表上時整批拒絕、format ≥ 19——R2 verify 起 CLI help 與 MCP 描述逐條對齊)。揭露:MCP 候選列與歧義條目帶 `id` 與 `undecidedChecks`(歧義條目是 orgKey → 次數)、頂層 `undecidedTotal`(候選配對,與 CLI 四態計數行同一個定義)與 `ambiguityUndecidedTotal`(CLI 另印一行,同一個定義);CLI 列表逐列印 id 並標「查過未決 N 次」,消毒改了 id 時標出它不能逐字送回。**有記錄的差異**:CLI 篩選式 `--apply` 排除查過未決的候選並另列、全數排除時零寫入非零結束;MCP 逐 id apply 照寫;`--reject` 兩面都不排除(否決是一個判定)。**與 #624 不完全同形**(#643 R1 verify):people 的 CLI 有 `--judge` 這條逐 id 的路,org 的 CLI 在 #647 之前沒有逐 id 的 apply——查過未決的 org 候選在 CLI 上歸戶不了。**#647 起兩面同時新增 judge 腿**(CLI `--judge`/MCP `judge`,同走 `judgeOrganizations`;使用者 2026-09-27 裁決比照 people 的 `--judge`):`<列表的 id>@=理由`,id 解析與未決腿共用、歧義條目也收;歸戶並寫 `org-judged` 層級的 confirmed(judged 層級封閉列舉的第三列,顯式加入)。兩面同契約:輸入錯整批拒絕、store 狀態不符(citekey 重複、上級機構判給自己或會成環、判給歧義條目裡已否決過這個配對的 org——比對用正規化鍵;候選列上否決配對的其他拼法自 #647 R3 起不再列出、拿它的 id 是輸入錯、列表過期)該筆略過並具名、已歸戶而理由存不進去時帶 `verdictNotRecorded`、寫入前整個寫入集合(含 #631 的目的檔檢查)先驗、單獨呼叫;CLI 另過 #298 的目標確認閘。`--undecided` 不接受 `--holder`/`--org`(id 已點名,收窄沒有作用)。**#648 重新確認,裁決不變、契約有改**:apply、reject、judge、undecided 在任何檔落盤之前對整個寫入集合逐筆 preflight(writeX 在寫入當下跑的每一道,含 encode 與讀取上限),寫出後超過 8 MiB 即整批拒絕、零寫入、錯誤具名記錄與兩個位元組數——apply 在此之前先寫 person/org/entry,verdict 那一筆最後才在寫入當下被拒;兩面同一條 service 路徑 **#641 重新確認,裁決不變、契約有改**:以 person 為 holder 的候選(隸屬)同樣接上「無法唯一定位」(`unlocatablePersonKeys`)——MCP 的 apply/reject 整批拒絕、judge/undecided 該筆略過並具名、列表以 `unlocatablePersonKey` 標出(作者位列仍是 `unlocatableCitekey`),CLI 篩選式批次排除並另列。MCP 的 apply 自此只寫真的改到的 person/organization/work,並在第一次寫入之前驗整個寫入集合(含 #631 的目的檔檢查,`judgeOrganizations` 的既有形)——先前重寫全庫每一筆 person,任何一筆寫不進去就在中途撕裂;`peopleRewritten`/`organizationsRewritten` 改數真的改寫的筆數。CLI 篩選式 apply 本來就只寫改到的、逐筆收容。 **#670 重新確認,裁決不變、契約有改**:目標 organization(verdict 寫進它)或 organization holder(上級機構那一格)的 key 有不只一筆記錄時,MCP 的 apply/reject 整批拒絕、judge 與 undecided 該筆略過並具名、列表以 `unlocatableOrganizationKey` 標出;CLI 的篩選式批次排除並另列,逐列標出理由。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_store_source` | `store-source` | ✅(#264;**讀取面慣例**而非寫入面封閉例外——receipt 的 `discardedProvenance` 攜帶「你這份敘述沒被寫入」,人需要看得懂,故 `--json` 原樣轉印+人可讀同源。收**檔案路徑**不收 base64/stdin:MCP 面無 stdin 會讓兩面分岔,base64 把二進位塞進 JSON 會膨脹並整份進 context(#165 的既有威脅模型)。`SourceStore.storeSource` 的寫入面防護 #224 已完成,本格只補呼叫端——先前全樹零 production 呼叫端,能力只有寫 Swift 的人做得到,#206 判準的同形)。**#703 重新確認,裁決不變、契約有改**:單檔上限 256 MiB(`LibraryStore.maxSourceBytes`,唯一一份常數;裁決原話「256 MB」,數值是 2^28)——超過即整個呼叫拒絕、不截斷、零寫入,訊息說出路徑、實際大小與上限;內容逐塊讀、每塊讀完即釋放(R1 verify 之前這裡寫「記憶體與檔案大小無關」,實測不成立:存 128 MiB 的檔尖峰 RSS 約 285 MB;R1 起兩面同一個修正);目錄與 FIFO 以 `fstat` 拒絕,開檔不卡住;~~不支援 `RENAME_EXCL` 的檔案系統(exFAT、FAT32)退到 hard link 或確認後一般改名(R1)。~~(b26 F6:exFAT/FAT32 連 hard link 也沒有;兩條原子放置都不支援的磁碟區現在每一次存檔都具名拒絕——見本格後段與 `changelog/2026-10-02-b26-f6-fixes-703-700.md`。)兩面同一個 `AkashicService.storeSource`,回應鍵不變 **#703 R2 重新確認,裁決不變、契約有改**:位址上已有東西時只有大小相同的普通檔算「已經在了」,位址被目錄、symlink(含懸空的)或大小不同的檔佔住兩面都具名拒絕、不寫 index(R2 之前回成功並寫 index,R1 引入的回歸);回應多 `bytesWritten`(CLI 人可讀面多一行「已存入位元組/位元組早已在」);~~第三條放置路改成排他建立目的檔後複製(R1 的「確認後一般改名」會覆寫兩步之間出現的檔)。~~(b26 F6:第三條放置路整條拿掉,見本格後段與 `changelog/2026-10-02-b26-f6-fixes-703-700.md`。) **b26 F6 重新確認,裁決不變、契約有改**(使用者 2026-10-02 裁決):**第三條放置路拿掉**——`sources/` 所在的磁碟區做不到不覆寫的原子放置(`RENAME_EXCL` 與 `link(2)` 都不支援,exFAT、FAT32)時,兩面每一次存檔都在建立任何檔案之前具名拒絕、零寫入,訊息說要把 store 放在 APFS 或 HFS+;`copy-zotero-attachments` 的批次前置檢查(`preflightStoreSource`)同一道閘,整批拒絕。位址上已有同一份內容也拒絕(「每一次存檔」)。`F_FULLFSYNC` 回真的 I/O 錯誤時存檔被拒、不退到 `fsync`(兩面同一條 `syncFile`)。`bytesWritten` 在 exFAT 上每次都是 false 的問題隨第三條路一併消失。 **b29 V5 重新確認,裁決不變、契約有改**:(a) 磁碟區沒有回報能力旗標時,寫入之前、**任何分支之前**(含「位址上已經有同一份」)在分片目錄裡實際放一次空的探測檔(同一條放置路;探測名是暫存檔的形狀、用完即刪),做不到就同一句拒絕、零寫入、不補 index——先前那一支不經任何放置呼叫就回成功、缺條目時還補一列;拒絕也因此在複製內容之前。預演(`preflightStoreSource`)不寫、不探測:旗標讀不到時 `copy-zotero-attachments` 的乾跑說可以、實跑逐筆拒絕。(b) 同步的「不支援」是封閉的六個 errno(`ENOTSUP`、`EOPNOTSUPP`、`EINVAL`、`ENOTTY`、`ENODEV`、`ENOSYS`),同步失敗的訊息說出 errno 與它的說明。(c) MCP 說明更正:`exclusionVerified:false` 是 store 不在 git 工作樹內、排除沒驗、**照存**(先前寫「false 時拒絕」,與行為相反);sources/ 未被 git 忽略才拒絕。(d) **相對於上一個發行版的行為改變**:#703 之前的 binary(`mcpb/manifest.json` 的 0.12.1)在 exFAT/FAT32 上的 store 存得進去(Foundation 的原子寫入,verify 實測);#703 之後每一次存檔都被拒,含已在的內容與只需補 index 的那一筆——既有的 blob 照樣讀得到,doctor、validate 照常。見 `changelog/2026-10-02-b29-h5-fixes-703-700.md`。 | +| `akashic_enrich_from_zotero` | `enrich-from-zotero` | ✅(#340;**#206/#290 判準的第三次套用**——那句原話是「能不能無損匯入,不該取決於使用者會不會寫 script」,在這裡是「能不能把一筆跌破下限的記錄補回下限,不該取決於面」。兩面同走 `ZoteroEnrichment.plan`。**契約有記錄的差異**:CLI 的 `--apply`走 #298 的破壞性閘、MCP 面用 `dry_run` 且**不設閘**——該閘擋的是「篩選式批次寫入未指名目標」,而本 tool 收的是逐筆顯式指名的 citekey 清單,與 `resolve-people` 的 tier 閘同型不對稱。**與 `akashic_import_zotero` 刻意不同語意**:那是 pull(整份替換 `fields`、重設 `type`、覆寫未歸戶作者),這是 add-only;同一個 store 上兩種語意並存是設計,不是重複) **#628 重新確認,裁決不變、契約有改**:citekey 重複或與另一筆 work 共用 id 的歸新的 `unlocatable` 類,兩面同一個 plan。 **#641**:`unlocatable` 自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 | +| `akashic_enrich` | `enrich` | ✅(#458;**#554 R26 D73 重新確認(R27 補記),裁決不變、契約有改**:`applied` 的來源 reference 冪等自此比位元組(`byteExactKey`),同一次呼叫送兩次只差 NFC/NFD 的 judgement 兩筆都落盤、沒有掃描面看得到——缺口記 #582;generic add-only 補值——`akashic_enrich_from_zotero` 那列的**一般化**:政策抽成 `AddOnlyEnrichment`(AkashicCore)一份,Zotero 版降為它的 adapter,兩面同走 `AkashicService.enrich`。輸入以 citekey 或 DOI 定位(DOI 相等是 `identity-is-judged-not-matched` 的識別碼例外,對回 citekey 由程式做;命中 ≥2 筆=`ambiguous`、具名全部 citekey、零寫入——不判定哪筆才對,那是 #459)。雙摘要分鍵(`abstract-`/`abstract-2` → `abstract_es`/`abstract_2`);`sourceDigest` 只回顯不進 store(第 15 條邊的值域是 #450 的裁決;**#517 起不再成立**,見本列後段)。**契約差異沿上一列**:CLI `--apply` 走 #298 閘、MCP `dry_run` 預設 true 且**不設閘**(逐筆顯式指名)。**第二個有記錄的差異**:MCP 面 items 截 20 筆(`counts`/`written`/`writeFailed` 永遠完整、`itemsTotal`/`truncated` 揭露)、CLI 不截——同 #388 `--rows` 那格的理由:上限保護的是消費端,MCP 的輸出進 LLM context。兩面**同一個 JSON 解析器**(`AddOnlyEnrichment.decodeProposals`:CLI 讀 `--from` 檔、MCP 把 `Value` 編回 JSON 再解),頂層未知鍵整批拒絕——把 `abstract` 寫在頂層而非 `fields` 是最容易犯的錯,靜默略過會讓那筆看起來「補了」)。**#517 起 `sourceDigest` 寫得進 store**:提案多收 `sourceURL`/`sourceRetrieved`/`sourceMediaType`/`sourceStatus`(蛇形同收),三欄齊備時每個補進去的欄位寫一筆 `retrieval` reference(**#542 R2 起是四欄**:`sourceStatus` 必要、不再預設 200——一份離線掃描檔曾因此被記成 HTTP 200;給了幾個卻不齊時缺哪些進 `provenanceSkipped`,不只「有 digest」那一種;三個來源字串納入 #519 的長度上限;digest 不合法整批拒絕)(`fields.<鍵>`/識別碼帶 value),與被補的值**同一次寫入**;**只給 digest 仍只回顯**,理由具名進 `provenanceSkipped`——一次取得的 url 與日期沒有別的地方記,digest 單獨湊不出一筆誠實的 retrieval。新鍵**不必逐面加**:兩面本來就同一個解析器(本列既有記載),這是那個設計的第一次兌現——**但這句只對解析器成立,對 MCP 的 input schema 不成立**(#542 R1 verify 兩席 HIGH):proposals item 宣告 `additionalProperties: false` 而只列了 `sourceDigest`,照 schema 呼叫的 client 送不出另外四個欄位。#542 起 schema 列齊五個來源欄位;**#542 重新確認,裁決不變、契約有改**:item 的 provenance 狀態三個鍵擇一(`provenancePlanned`/`provenanceWritten`/`provenanceNotWritten`),兩面同一個 payload;~~date/authors 不產生 reference(#655)~~ → 見本列末 #655 **#628 重新確認,裁決不變、契約有改**:citekey(直接給或由 DOI 對回)無法唯一定位時,該筆歸 `ambiguous`、理由與 DOI 多筆命中分開說,兩面同一個 core plan。 **#641**:`ambiguous` 的無法唯一定位自此含load 以 #631 的同一組前置判斷標出的、寫入時一定會被拒的 work(legacy 殘留加上未受 git 追蹤、目的檔被隔離或兩份並存)。 **#655 重新確認,裁決不變、契約有改**(使用者 2026-09-28 裁決):補進去的 `date` 在來源四欄齊備時也寫一筆 retrieval reference(第 15 條邊的新格 `field: date`,store format ≥ 20),與其他欄位同走 `provenancePlanned`/`provenanceWritten`/`provenanceNotWritten`;補進去的 `authors` 一律不寫(`field: authors` 是作者位記錄的格子)。兩面同時新增 item 的 **`provenanceOmitted`**(欄位 → 理由):來源齊備、值補了、reference 刻意不寫的欄位——`authors` 一律、`date` 在 store format 低於 20 時(值照補,理由說出 format 與門檻)。它與三個狀態鍵**正交**,刻意不借 `provenanceNotWritten`:那個鍵的語意是「apply 時那一筆寫入失敗」(#542 的「鍵名本身說出事實」),刻意不寫與寫入失敗是兩件事。format 由 service 在寫入前讀 marker 判定(`StoreVersion.workDateReferenceFormat`,與寫入閘同一個常數),兩面同一條 service 路徑;MCP schema 的 `date`/`authors` 描述與 CLI `--from` 的 help 同批改寫,CLI 逐欄印「來源未記:」。 **#654 重新確認,裁決不變、契約有改**:`sourceDigest` 是空內容的 digest(0 byte 的 SHA-256)時整批拒絕、訊息說 0 byte 而不說形狀錯。閘是 `ProvenanceReference.isValidDigest` 本身(兩面同一個 core 驗證),所以收 digest 的其他面——`akashic_update_person` 的 references、`akashic_update_venue` 的 `paginated` rests-on、三個 resolve 族的 undecided rests-on、`akashic_record_divergence` 的依據——兩面同時拒收,那幾列不逐列重述。 **#668 重新確認,裁決不變、契約有改**:store format 低於 `StoreVersion.workFieldReferenceFormat`(17)時,`fields.<鍵>` 的值照補、reference 不寫,理由逐鍵進 `provenanceOmitted`(鍵是 `fields.<鍵>`)——與 `date` 在 20 以下的處置同形,兩面同一個 payload。 **#705 重新確認,裁決不變、契約有改**:寫進 `entities/`、搬移後的 legacy 拷貝沒刪掉的那一筆記在成功那一側的 `writtenWithLegacyCopy`,不進失敗清單(兩面的出口見表下 #705 段)。 **#695 重新確認,裁決不變、契約有改(收緊)**(使用者 2026-09-30 裁決「要,共用同一個解析函式」):`sourceURL`/`sourceRetrieved`/`sourceStatus`(與蛇形別名)走 `akashic_update_person`/`akashic_update_venue` 的 `references` 同一個形狀函式(`RetrievalWriteShape.firstIssue`,Core;在 `AddOnlyEnrichment` 的提案驗證裡呼叫,兩面同一個解碼與驗證):url 只收 http/https、主機非空、不含帳密(不回顯),retrieved 是 ISO 8601,status 在 100–599,給了 url/retrieved/media type 就必填、不預設 200。不合的提案整批拒絕、零寫入、具名第幾筆(輸入錯的既有失敗語意)。先前「給了 url/retrieved 卻沒給 status」是值照補、reference 不寫(`provenanceSkipped`),現在是錯誤;只給 `sourceDigest` 仍是回顯(#517,不觸發 status 必填)。兩面的描述(MCP 三個 source 鍵、CLI `--from` 的 help)同批改寫。**R1 verify 更正**:共用的是一個**形狀**函式,不是一整份寫入契約。「這筆算不算擷取型、要不要 status」由兩個面各自判定,刻意不同:references 對不齊的 retrieval 一律拒絕(平面 init 要四欄),`content` 單獨出現也要 status;enrich 對「給了 status 卻缺 url/retrieved」照補值、reference 不寫(`provenanceSkipped`),`sourceDigest` 單獨給是回顯、不觸發 status。理由是兩面的輸入不同:enrich 的來源欄位是補值的附帶資訊,摘要提案只帶 digest(#517)。同一輪收緊了形狀本身,被拒的輸入多四種,見 `akashic_update_person` 列的 #695 註記。 **#709 重新確認(2026-10-05、2026-10-09),裁決不變、兩面契約有改**:DOI 命中兩筆以上的理由裡,哪一筆是 legacy 拷貝只問 load 的標記;命中以完整的 load 算、不經 citekey 去重(同 citekey 的拷貝不再把 entities/ 那份擠掉——先前會說 entities/ 那份「不帶這個 DOI」、或回 notFound);理由的結論與處置排在逐份明細之前,出口截到 512 字時被截的只是明細。回應的鍵與 `ambiguous` 不變,兩面同一個 core plan——細節在本檔 #709 段。 | +| `akashic_s2` | `s2`(`paper`/`match`/`batch`/`references`/`citations`/`recommend`/`author-search`/`author-papers`/`status`)| ✅(#664;Spectra change `semantic-scholar-interface`)。**本表第一個連網的工具**:兩面同走 `AkashicS2`(`S2Endpoints`/`S2Client`/`S2Output`),**不經 `AkashicService`、不開 store**——S2 的回傳是線索,不是寫入;包進綁 store 的服務層只會多一層只轉呼叫的 adapter(design 的否決理由)。網路與 keychain API 只准出現在 `Sources/AkashicS2/`(`akashic-guards network-confinement`,負對照 `network-confinement-mutations`),這個例外的理由在 `web-access-via-safari-browser` 的〈例外〉第 2 類。**兩面同契約**:金鑰只從 keychain(service `semantic-scholar`、account `default`)在程序內非互動讀取,不收參數或環境變數,缺金鑰不退回匿名請求;`x-api-key` 只附給 `https` 且 host 恰為 `api.semanticscholar.org` 的請求;全機合計每秒至多 1 個請求、429 共用退避、同一請求至多重試 3 次;以 `10.` 開頭的裸 DOI 補 `DOI:`;JSON 面(CLI `--json`、MCP、`status --json`)的字串原樣保留、序列化後以 `documentSafeJSON` 無損逃脫,人可讀的 CLI 面(終端機)才經 `displaySafe`;`status` 兩面同一份 JSON(`S2Output.statusJSON`),不連網、不含金鑰也不含它的長度。**錯誤文字兩面逐字相同**:金鑰不可用、限流用盡、S2/網路錯誤都經 `displaySafeErrorMultiline(_:prefix: "Error: ")`,CLI 以結束碼 3/4/5 區分,MCP 回 `isError: true`。CLI 另有兩個碼(MCP 一律 `isError: true`):1=要讀 argv 以外才判得出的錯(環境覆寫被拒、`--ids-file` 讀不到、超過 256 KiB、筆數不合或有一行不像識別碼、節流狀態檔無法使用——#549 的判準),64=只看 argv 判得出的錯(`--limit`/`--offset` 的範圍、識別碼或 `--title`/`--name` 空白、識別碼含 `.`/`..` 路徑片段,在 `validate()` 檢查、早於任何 I/O)。**有記錄的差異**:(a) **位元組上限**——MCP 受 48 KiB(`S2Output.mcpByteBudget`,與 `akashic_doctor` 的 `candidateByteBudget` 同值),只放完整的筆數,以 `total`/`returned`/`truncated`/`offset`/`nextOffset` 揭露;**續查的訊號是 `nextOffset`(非 null 才續,null 是沒有下一頁),不是 `truncated`**(`truncated` 只表示被位元組上限截斷;預設 100 筆剛好放得下而 S2 還有更多時,`truncated` 是 false、`nextOffset` 非 null);CLI 不截。理由同本表 `akashic_doctor` 列與 #388 `--rows` 那格的面級分工:MCP 的輸出進 LLM context,呼叫端收到後無法退回已付的代價;CLI 的輸出進人的終端機。(b) **分頁**——CLI 的 `references`/`citations`/`author-papers` 省略 `--limit` 時翻完全部頁;MCP 的分頁端點省略 `limit` 時一次只向 S2 要 100 筆(`S2Tool.defaultPageLimit`),其餘以 `offset` 續查——一次呼叫不拉回上千筆。`author-search` 省略時兩面都是 100 筆,`recommend` 兩面都是 1–500、預設 100。(c) **輸出形**——CLI 預設印人可讀(一行一筆),`--json` 是 `{source,endpoint,request,fetchedAt,total,data}` 信封,兩者同源;MCP 只回 `{endpoint,total,returned,truncated,offset,nextOffset,data}`。(d) **輸入形**——`batch` 在 CLI 收 `--ids-file`(一行一個 id)、在 MCP 收 `ids` 陣列;端點名 CLI 用連字號(`author-search`)、MCP 用底線(`author_search`)。(e) **`status` 的結束狀態**——CLI 在金鑰不存在或讀不到時以 3 結束(結束碼 3 涵蓋兩種情形,skill 選路要再看 `--json` 的 `keychain.present`:false=找不到、true=項目在而讀不到),MCP 照常回成功、由呼叫端讀 `keychain.present`/`keychain.readable`——與 #624 全數排除「CLI 非零結束、MCP 由呼叫端讀欄位」同形 (f) **MCP 專有的限制**——分頁端點的 `limit` 至多 1000(S2 的一頁),超過在送出任何請求之前拒絕;`paper`/`match`/`batch`/`recommend` 不分頁,被截斷時 `nextOffset` 永遠是 null(少給 id、降低 `limit` 或少要 `fields` 重查);第一筆就超過 48 KiB 時回錯誤並點名 `fields`,不回「成功、0 筆」。CLI 沒有這三項,因為它不截。 | + +**#654 重新確認(2026-09-28),各列裁決不變、兩面契約有一處改**:服務層只看參數的檢查——id 與 `key:shape` 的形狀、理由的空白與長度、同一批重複、一次的上限、rests-on 的 digest、key 的格式、互斥與必填——抽成不碰 store 的 static 函式,服務方法在讀 store 之前呼叫,CLI 的 `validate()` 呼叫同一個函式(同一件事只有一份描述)。兩面拒絕的集合不變,變的是**先後**:一次呼叫同時有參數錯與 store 狀態不符時,兩面都先報參數錯(先前依迴圈順序,可能先報 store 狀態或 format 閘);add_person/add_venue 的 key 格式先前在寫入閘擋、訊息是 store 層的,現在服務先擋。CLI 面對這些錯誤自此回 64(用法錯誤)而不是 1——判準與仍是 1 的站點在 `RuntimeFailure` 的 doc。涵蓋 resolve_people(judge/refute/undecided/split/un-split/drop/attribute-org)、resolve_venues(repoint/demote/drop/undecided)、resolve_organizations(undecided 與 judge 的一次上限、undecided 的 rests-on digest、連一個 `@=` 位置都沒有的 id)、update_venue、add_venue、add_person、update_person(CLI 只在 `--fields` 時)、record_divergence、store_source、person、venue、link、tag、set_status 這幾列,各列不逐列重述。 + +**#705 重新確認(2026-09-30),各列裁決不變、兩面契約有一處改**(使用者裁決 (a):成功側另開欄位):`writeEntry`/`writePerson` 寫進 `entities/` 之後刪不掉 #631 搬移的 legacy 拷貝時,在回報面開的收集範圍(`LegacyCopyLedger`)裡只記下、照常回傳——它寫了;範圍外照舊擲 `legacyCopyNotRemoved`(#702);App(第三面)自 #708 起也開範圍,見本段末。**MCP**:工具分派把每一次呼叫包在範圍裡,成功的 JSON 物件加鍵 `writtenWithLegacyCopy`(每筆 `{kind, key, written, legacyFile, detail}`,不截;已帶這個鍵的回應併進同一個陣列),錯誤回應把同一份人可讀報告放在訊息**最前面**(R1 verify 前附在末尾——呼叫端先讀到 UNIQUE 錯誤);`akashic_import_zotero` 的 index rebuild 失敗時,importer 收下的那幾筆交給分派的範圍、不再嵌在錯誤訊息的 payload 裡(嵌進去會被錯誤出口的 200 行/96 KB 上限截掉);各工具自己的失敗清單(`writeFailed` 等)不含它。**CLI**:進入點把每一個命令包在範圍裡,命令結束後(擲錯也是)印在輸出末尾、錯誤訊息之前;直接印 service JSON 的寫入命令(update-person、update-entry、tag、link、set-status、resolve-venues、enrich 的 --json)把鍵放進那份 JSON,stdout 仍是一份 JSON。兩面的人可讀報告是同一個 `LegacyCopyLeft.reportLines`。~~**有記錄的後果**:work 的兩份共用同一個 citekey,寫入之後的 index rebuild 撞重複——會重建 index 的工具因此回錯誤(報告附在錯誤訊息末,CLI 印在錯誤之前);person 的兩份不擋重建(#670),回成功。~~ → **#709(2026-10-01)起不成立**:使用者 2026-09-30 裁決 index 重建以 `entities/` 那份為準、略過同一種同一個 id 的 legacy 拷貝並回報(判準只有一份:`LibraryStore.markLegacyCopiesShadowedByEntities`),work 與 person 都一樣——會重建 index 的工具在這種狀態下回成功、`writtenWithLegacyCopy` 在成功回應裡,CLI 結束碼 0;兩面契約同時改、沒有有記錄的差異。那一筆本身照舊寫不進去(#641,無法唯一定位),validate/doctor 照舊報兩份並存(#641 的 warning 多一句「index、匯出與 App 以 entities/ 那份為準」,兩面經同一份 `crossRecordIssues`);~~doctor 仍因那一對的 UUID/citekey 重複 error 不重建。~~ 報告只有在寫了之後**別的**步驟失敗時才落在錯誤那一側(最前面,CLI 印在錯誤之前)。**#709 第二個 commit(使用者 2026-10-01 延伸)**:這一對造成的 UUID 共用與 citekey/person key 重複從 error 降為 warning(整組同一個 id、其中一份被標為拷貝才算;兩筆不同的記錄共用照舊是 error)——`akashic_doctor`/`doctor` 照常重建,rename、rename-person、resolve-divergence 不再因它整個 store 停下;resolve-divergence 另對「候選有 legacy 拷貝」具名拒絕(被併者的 legacy 那份會復活),CLI-only 表那一列的裁決不變、契約多這一種拒絕。匯出(`akashic_export`、`export-bib`、`export-tables`)與 App 以 entities/ 那份為準,兩面同一個 helper(`LibraryLoad.withoutShadowedLegacyCopies`),沒有有記錄的差異;只剩這一對的 store,CLI `validate` 結束碼 0。先前這一筆在 import-zotero 同時列在成功清單與 `writeFailed`,在其他逐筆收容的寫入者只算失敗;沒有收容的寫入者擲錯中止,同一批後面的記錄沒寫。涵蓋的 MCP 列即上表各帶 #705 註記的 13 列;CLI-only 表的寫入命令(rename、rename-person、resolve-divergence、migrate 一族、authorize-names 等)由進入點的範圍涵蓋,各列裁決不變、不逐列重述。**R1 verify 更正**:rename 與 rename-person 在寫完新鍵那一份之後自己刪舊鍵的 legacy 檔,那一步原本不經範圍——唯讀目錄讓改名以 Foundation 的原始錯誤中止、引用還沒改寫,上一句對這兩個命令為假;R1 起那一步走同一個搬移刪除,改名做完、legacy 那份進報告。~~App(改名、單筆編輯、裁決台)仍沒有範圍,照舊擲「已寫入……」。~~ → **#708(使用者 2026-09-30 裁決)起 App 也開範圍**:各寫入點(`AppState.mutate` 底下的衍生層編輯、`AppState.rename`、裁決台的 accept 與兩個 orphan 動作)包在 `AppState.recordingLegacyCopies` 裡,留下拷貝的那一筆動作算成功,要清的 legacy 檔列在側欄的非阻斷提示(`AppState.legacyCopyNotice`,文字取自同一個 `LegacyCopyLeft.reportLines`/`message`);留下拷貝之後才失敗時兩樣都給——提示照列,失敗提示是先說寫了什麼的 `LegacyCopyLeftBeforeFailure`(改名之後的重建失敗例外:仍是 `renamedButReloadFailed` 的回執,它本身就說改名已寫入)。與兩面同一個後果:~~work 的兩份在 index 能容忍之前讓重建失敗,App 上那一格是「失敗+提示」;person 的兩份回成功。~~ → **#709 起 work 與 person 一樣**:兩份並存不再讓重建失敗(index 以 `entities/` 那份為準),App 動作成功、側欄提示列出要清的 legacy 檔(`AppLegacyCopyNoticeTests` 的 work 那一格釘住 `mutate` 之後 `legacyCopyNotice` 非空、重建成功,#709 R3 verify);只有 store 裡另有兩筆**不同**的記錄共用 citekey 時才回到「失敗+提示」。App 沒有單一出口可以一次包住,每個寫入點都要在範圍裡由源碼守衛釘住(`AppLegacyCopyNoticeTests.testEveryAppStoreWriteIsInsideTheScope`)。 + +**#705 R2 verify(2026-10-01),各列裁決不變、兩面契約有三處改**:(1) MCP 的 `writtenWithLegacyCopy` 至多列 20 筆(`writtenWithLegacyCopyLimit`),另給 `writtenWithLegacyCopyTotal`/`writtenWithLegacyCopyTruncated`(三個鍵同進同出),錯誤回應的人可讀報告同一個上限、多一行說筆數與去哪裡找;CLI 全列——有記錄的差異,理由同 `resolve-people` 列的 `--rows`(上限保護的是 LLM context;截掉的由 `akashic validate` 逐筆列出兩份並存的記錄),上一段的「不截」自此不成立。(2) 同一個操作稍早已寫入、之後同一筆的寫入被 #631 拒絕(兩份並存)時,兩面同一句拒絕(`legacyCopyLeftEarlierInThisOperation`,住在 `LibraryStore` 的寫入前置,所有寫入者共用),那一筆在成功側標出之後的寫入沒有套用(MCP 列 `laterWriteNotApplied`、人可讀報告附句);`import-zotero` 兩面都不再把那一步列在 `writeFailed`,其他逐筆收容的寫入者仍列在自己的失敗清單(同一句話)。(3) CLI:寫入後失敗時 stderr 第一行說 stdout 上報告過幾筆寫了;印 service JSON 的命令失敗時 stdout 是一份只有那三個鍵的 JSON(先前是 JSON 以外的文字);印 service JSON 的寫入命令必須經 `LegacyCopyReport.payload`,`LegacyCopyPayloadScanTests` 以命令樹與裁決表列舉寫入命令、掃它們的 `print(`,豁免逐格具名。 + +**#705 R3 verify(2026-10-02),各列裁決不變、兩面契約有兩處改**:(1) MCP 成功清單截到 20 筆時,「之後的寫入沒有套用」只標在被截掉的列上就沒有地方說——另給 `writtenWithLegacyCopyNotApplied`(完整的沒套用筆數,在截斷之外;> 0 才出現,沒有這個鍵=零;併進已帶這個鍵的回應時兩邊相加);錯誤回應的「另有 N 筆未列出」那一行補說其中幾筆沒套用;13 份工具說明共用的一句(`Server.legacyCopyNote`)點名這個鍵(2026-10-03 起以「鍵名加 NotApplied」組字指名,完整鍵名不在說明裡;組出來的名字由 `LegacyCopyNoteKeyTests` 與 `StdioE2ETests.testTheSharedLegacyCopyNoteNamesBothCompanionKeys` 釘住,#705 第三次 verify)。CLI 全列,列自己帶標記,JSON 同樣多這個鍵。(2) CLI:命令以沒有訊息的非零結束碼收場(`throw ExitCode(1)`,例如 import-zotero 的 `writeFailed`)時,stderr 先前整個是空的——stdout 上報告過寫了的筆數時,stderr 的第一行照給(`LegacyCopyReport.stderrText`),~~說「不要重跑」與「原因見 stdout」~~(#705 R2 verify 第二輪,2026-10-03:「不要重跑」恰好在要重跑的兩種情形出現——那幾筆有之後的寫入沒套用、或沒有訊息的非零結束本身就是有記錄沒寫成;第一行改成依情形措辭:有沒套用的說要重跑幾筆,都沒有時只說它們不必為了自己重跑,失敗本身另說要處理。MCP 面沒有對應的 stderr,不變;13 份說明共用的一句改短、內容不變)。import-zotero 本身在「只有之後的寫入沒套用、別的都成功」時結束碼仍是 0,~~唯一的訊號是 stdout 那一行(裁決 (a) 的後果,沒有變)~~ → 使用者 2026-10-05 裁決起 stderr 多印一行,見下一段。 + +**#705(使用者 2026-10-05 裁決),各列裁決不變、CLI 契約有一處改**:結束碼 0、但同一個操作之後的寫入沒套用(stdout 上報告過、標了 `laterWriteRefused` 的筆數 > 0)時,stderr 多印一行——結束碼 0 時使用者最可能不看 stdout 的報告與 JSON。那一行說筆數、清單在哪(文字輸出:stdout 的 `writtenWithLegacyCopy` 段;JSON 輸出:`writtenWithLegacyCopy` 各列的 `laterWriteNotApplied`、筆數在 `writtenWithLegacyCopyNotApplied`)、要重跑(與非零結束的第一行同一句 `LegacyCopyReport.rerunAdvice`)。由進入點(`AkashicCLI.main`)在命令成功之後印、只有一個函式(`LegacyCopyReport.successStderrLine`),新命令不必記得接;非零結束照舊走 `stderrText`,兩條不會同時出現。2026-10-05 走得到這一格的 CLI 命令只有 `import-zotero`(同一趟先清 orphan 標記、再更新書目欄位,第二步被 #631 拒絕而被接住);其他寫入者對同一筆在一次呼叫裡只寫一次,第二次寫入被拒時以非零結束、走 `stderrText`。MCP 面沒有 stderr,`writtenWithLegacyCopyNotApplied` 已是它的訊號,不變。 + +**#709 R3 verify(2026-10-02),各列裁決不變、兩面契約有三處改**:(1) `akashic_doctor`/`doctor` 的跨記錄清單 error 先、同 severity 內保持原順序(`StoreHealth.errorsFirst` 的 `ValidationIssue` 版,單一來源)——先前 MCP 面取前 20 則,一對 leftover 先造出兩則 warning,`indexRebuilt:false` 指向的 error 可能排在第 25 位而被截掉。(2) `validate` 的「標題與年份相同但 DOI 不同」與「DOI 被 N 筆 work 共用」不再把同一筆記錄的 legacy 拷貝算成另一筆;`StoreHealth` 的未歸戶作者與 orphan 讀數取 entities/ 那份(與 index、匯出、App 同一個視圖;驗證類的 `crossRecordIssues`/`perRecordIssues` 照舊看到兩份)。(3) 以 entities/ 那份為準的視圖再延伸到三個讀取面~~與三個寫入候選面~~,兩面同一個 helper:`akashic people`/`akashic_people`(列表)、`view show`(`--keys-only` 餵下游腳本)~~、`bootstrap-people`/`bootstrap-organizations`/`bootstrap-venues` 的作者/刊名 literal 來源(legacy 拷貝的內容不流進寫入候選;`existing` 仍用完整 load——改名留下的舊 person key 要算「已存在」)~~(寫入候選那一半在第三次 verify 收回,使用者 2026-10-05 裁決維持收回,見下)。~~**仍然讀完整 load、沒有切換的**(有記錄的殘留,不是遺漏):`akashic_person`/`get-entry`/`library list` 的單筆查詢(排序讓 `entities/` 那份在前)、`resolve-*` 的候選產生、`validate` 結尾的「N entries」計數(兩份算兩筆)。~~(#709 R2 verify:這份清單漏了 doctor 的普查讀數,又把 `library list` 寫成單筆查詢——它是成員計數,排序救不了它。現行清單見下一段。) + +**#709 R2 verify(2026-10-03),各列裁決不變、兩面契約有四處改**:(1) `akashic_doctor`/`doctor` 由記錄**內容**算出來的讀數全部取 entities/ 那份——CLI 的 `unresolved author literals` 改讀 `health`(先前自己從完整的 load 數,與 MCP 分岔),兩面的 `noAuthorizedName`/`no authorized name`、`authorizedOnlyByCitationForm`、`deceasedWithOpenAffiliation`、`digestSources`,CLI 獨有的 zero-dates 與日期值域普查,以及 fatal 分支的 `entries` 計數,同一個視圖、同一組函式;先前一對 legacy 拷貝讓同一份輸出 `entries: 1` 而 `unresolved author literals: 2`、`people: 25` 而 `noAuthorizedName.people: 50`。驗證類(`crossRecordIssues`、`perRecordIssues`、quarantine)照舊看兩份。(2) ~~`bootstrap-organizations` 的 person 隸屬 literal 也取視圖(R3 只換了 entries:拷貝獨有的機構名成為候選、一次出現算兩次而越過 `--min-occurrences`);`organizations` 仍用完整的 load(它沒有 legacy 拷貝,又是「已存在」的判準)。~~(第三次 verify 收回、使用者 2026-10-05 裁決維持收回:literal 來源看完整的 load,計畫讀到拷貝時 `--apply` 整批拒絕,見下。)(3) `library list`/`akashic_libraries` 的 `list` 與 `check`、`set-kind` 回報的成員數與不符清單取視圖。(4) `create-entry`/`akashic_create_entry` 的 `doiHits` 比對表取視圖(citekey 的佔用照舊看完整的 load——legacy 檔佔著那個檔名);~~`authorize-names` 的計畫與「person 總數」取視圖,`--apply` 的拒絕看完整的 load 與視圖兩邊(過濾不讓任何一筆變得可寫)。~~(第三次 verify 收回,見下一段:「不讓任何一筆變得可寫」只對逐筆成立,批次的結果變了。)**仍然讀完整 load、沒有切換的**(有記錄的殘留,不是遺漏):`akashic_person`/`get-entry` 的單筆查詢(排序讓 `entities/` 那份在前);`resolve-*` 的候選產生;`validate` 結尾的「N entries」計數(兩份算兩筆);`references nominate` 的 DOI 與標題比對表(改名留下的拷貝讓同一筆被報成「庫內衝突」並指路 akashic-merge-twins,而 resolve-divergence 對有拷貝的候選拒絕——本輪不碰那個檔);`record-divergence`/`akashic_record_divergence` 的候選存在檢查(改名留下的舊 citekey 收得下、記下一筆與自己拷貝的歧異——只換視圖會讓訊息說「store 內沒有」而檔案就在,要具名拒絕得另寫一則訊息)。**這兩輪把視圖從使用者 2026-10-01 的裁決範圍(index、匯出、App)延伸到 doctor 的普查、~~三個 bootstrap 與 `authorize-names` 的計畫、~~library 計數、DOI 命中~~,其中寫入候選面(bootstrap、authorize-names)待使用者確認~~**~~:手改過的 legacy 拷貝的內容不再成為建檔候選,與已接受的「手改 legacy 那份 index 看不到」同一個取捨,但寫入面是新的~~。(寫入候選那一半在第三次 verify 收回;使用者 2026-10-05 裁決不延伸,見下兩段。) + +**#709 第三次 verify(2026-10-04),各列裁決不變、兩面契約有三處改;寫入候選面與依據判定收回到完整的 load**:使用者 2026-10-01 的裁決範圍是 index、三個匯出面與 App;R3 與 R2 把以 entities/ 為準的視圖延伸到寫入候選面與依據判定,~~那一半待使用者確認,確認之前不出貨(主 session 保守裁定)~~(使用者 2026-10-05 裁決:不延伸——規則與它的封閉例外見下一段)。寫入候選面(含 bootstrap 計畫)看完整的 load,以 entities/ 為準的視圖只用於讀數——依據判定(library 規則的依據)同屬寫入面那一邊。(1) `authorize-names` 的計畫與 `--apply` 的拒絕都看完整的 load——R2 讓計畫取視圖之後,改名留下 legacy 拷貝的 person 從整批拒絕變成寫入(entities/ 那份本來就寫得進去,擋住批次的是 legacy 那份被算進寫入集合);乾跑也算出 `--apply` 會擋的那幾筆(`AuthorizedNameMigration.Report.blockedKeys`),點名並說 `--apply` 會整批拒絕、不再說「確認後執行」。CLI-only,MCP 沒有這一面。(2) `library check`/`list`/`set-kind` 與 `akashic_libraries` 的 `check`/`list`/`set-kind`:依據(`basisProblem`:文件型的文件在庫裡有幾筆、規則的 venue 有幾筆)看完整的 load,與 `library add`、`validate`、App 的加入動作同一份判定;成員數與不符清單仍取視圖——三面同一個函式(`LibraryLoad.membershipReading`)。先前文件自己有 legacy 拷貝時 check 說「全部符合」而 add 拒絕。(3) 三個 bootstrap 的 literal 來源(含 `bootstrap-organizations` 的 person 隸屬)回到完整的 load,有拷貝時開頭印一行說計畫含幾份、計數多算一次(`LibraryLoad.legacyCopiesInPlanNote`,2026-10-09 起改名 `planBlockersNote`;`bootstrap-people --json` 多一個 `legacyCopiesInPlan`);CLI-only。~~**殘留、交使用者裁決**:一對相同的 literal 在完整的 load 上算兩次出現,可能越過 `--min-occurrences`——附註說出來、不修正計數(修正計數要辨認一對,那就是把視圖帶回寫入候選面)。~~(第四次 verify:括號裡的理由不成立——以 id 去重計數、保留拷貝獨有的 literal 不需要視圖;而附註之後 `--apply` 照寫,補救到達時寫入已經發生。使用者 2026-10-05 裁決:計畫讀到拷貝時 `--apply` 整批拒絕,見下一段。)實際影響範圍:2026-10-04 唯讀量測 live store 的 legacy 殘留 `entries/` 0 個檔、`people/` 0 個檔(量法同 `zero-instance-guards` 第 40 列),所以今天沒有任何候選的計數受影響。**純讀數**仍取視圖、兩面同一個 helper:doctor 的普查、library 的成員數與不符清單、`create-entry` 的 DOI 命中、`akashic people`、`view show`,本輪再加 `resolve-people` 無候選時的「literal 作者 N 個」(與 doctor 同一個數)與 `akashic_person` 名字查找的篇數。**仍然讀完整 load、沒有切換的**(有記錄的殘留,取代上一段的清單):`akashic_person`/`get-entry` 的單筆查詢——同 key 的一對靠排序讓 entities/ 那份在前,**改名留下的拷貝**以舊 key 查到的是 legacy 那份的內容(上一段寫「排序讓 entities/ 那份在前」對改名拷貝為假);名字查找的 person 列(一對列兩次,篇數不重複);`resolve-*` 的候選產生;`validate` 結尾的「N entries」;`references nominate`;`record-divergence` 的候選存在檢查;`enrich` 的 DOI 定位(寫入面:改名拷貝讓同一個 DOI 命中兩筆、零寫入,~~理由自本輪起說出那是同一筆記錄的拷貝、不再指 #459~~——第四次 verify 起只對 load 標成拷貝的說拷貝,見下一段;兩面同一個 core plan);`literal-census`(直接掃 YAML、不經 loader——辨認一對的「同一種、同一個 id」判準只有 `markLegacyCopiesShadowedByEntities` 一份,在掃描器裡寫第二份是 `no-compat-fallback` §「同一件事只能有一份描述」禁止的;有拷貝時它比 doctor 多算拷貝裡的 literal)。validate 的 library 成員警告照舊看完整的 load(驗證面),所以成員的 legacy 拷貝不符規則而 entities/ 那份符合時,validate 點名它、`library check` 不列——那一對另有兩份並存的警告。 + +**#709 第四次 verify(2026-10-05),各列裁決不變、兩面契約有一處改、CLI-only 三處改;使用者 2026-10-05 兩項裁決落地**。**規則(使用者 2026-10-05 裁決 1):寫入候選面一律看完整的 load,以 entities/ 為準的視圖只用於讀數。** 例外是封閉列舉,只有使用者 2026-10-01 裁決本身的範圍,不得依性質相似類推:index 重建、三個匯出面、App——App 的裁決台以視圖產生候選並逐筆寫入(`AppState.load`),可以的理由是它逐筆以 citekey/key 定位寫入,而逐筆的可寫性不因過濾改變(`withoutShadowedLegacyCopies` 對完整 load 上無法唯一定位的那一份補上 `unwritableReason`);以批次為單位拒絕的寫入者不在此列。歸為讀數而容易被誤認成寫入面的兩格,理由寫在這裡:`create-entry`/`akashic_create_entry` 的 `doiHits` 只回報、不拒絕(#637),不決定寫什麼;`view show --keys-only` 輸出一個 view 的外延,下游腳本拿它去寫是那個腳本自己的寫入面,而被拿掉的只有改名前的舊 citekey——它指的 legacy 檔在刪掉之前任何寫入面都定位不到(#641)。(1) `akashic_enrich`/`enrich` 以 DOI 定位而命中兩筆以上時,哪一筆是 legacy 拷貝只問 load 的標記(`fileSituation.shadowedLegacyFile`,判準只有 `markLegacyCopiesShadowedByEntities` 一份):被標成拷貝的,理由點名它的 legacy 檔與 entities/ 那份、帶「確認 entities/ 那份是新的之後」;同 id 而沒被標的(兩份都是 legacy),說 UUID 重複是 error、指向 validate,不下「是不是同一篇」的結論;拿掉拷貝之後仍有兩筆以上照舊指 #459。先前以 id 相等當代理,對兩個共用 UUID 的 legacy 檔也說「不是兩篇作品:刪掉 legacy 那份」。兩面同一個 core plan,回應的鍵與 `ambiguous` 不變,tools/list 不變。(2) CLI-only:三個 bootstrap 的附註與 `bootstrap-people --json` 的 `legacyCopiesInPlan` 只數流進該命令 literal 來源的拷貝(`BootstrapPlanSource`:people/venues 數 work,organizations 數 work 與 person);使用者 2026-10-05 裁決 2:計畫讀到拷貝時 `--apply` 整批拒絕、零寫入(`refuseApplyWithPlanBlockers`,結束碼 1,~~逐份點名 legacy 檔~~ 清單在乾跑——見下一段),乾跑照常列出並說明。(3) CLI-only:`authorize-names` 的寫入集合裡有任何一筆的 id 還有 legacy 拷貝時 `--apply` 整批拒絕——先前只擋「legacy 那份本身在寫入集合裡」,改名一對而 legacy 那份已有 authorized 時只寫 entities/ 那份;`blockedKeys` 一併列出。(4) CLI-only:`library check`/`set-kind` 依據不明確時不建議逐筆 `library remove`(MCP 的 `check` 沒有這句建議,不變)。 + +**#709 b36 verify(2026-10-09),各列裁決不變、兩面契約有兩處改、CLI-only 三處改;使用者 2026-10-09 兩項裁決落地**。(1) `akashic_enrich`/`enrich` 的 DOI 歧義理由:命中以完整的 load 算(`plan` 先前以 citekey 為鍵的字典、同 citekey 後者勝,load 先 entities/ 後 legacy,同 citekey 的拷貝把 entities/ 那份擠掉——理由說它「不帶這個 DOI」、叫人刪拷貝「再跑」而真孿生仍在;entities/ 帶 DOI 而拷貝沒有時回 notFound、同一個 store 的 `create-entry --dry-run` 卻說「DOI 已在」);結論與處置(#459、UUID 重複、確認 entities/ 那份是新的之後再刪)排在逐份明細之前,出口 `displaySafe(max: 512)` 截掉的只會是明細。`matches` 同一個 citekey 只列一次。(2) MCP 每一列的 `laterWriteNotApplied`(與 CLI 逐筆列、App 列同一句 `laterWriteRefusedNote`)帶「確認 entities/ 那份是新的之後」、不說「即可」——那一列沒有 explanation 的標題可依;鍵不變,tools/list 不變。(3) CLI-only:三個 bootstrap 的拒絕條件是 `LibraryLoad.planBlockers`——計畫讀到的種類裡 load 標出的拷貝 ∪ 無法唯一定位的記錄(works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`,裁決 2,與 `authorize-names` 一致);`bootstrap-people` 的計畫也讀 person 拷貝(裁決 1——~~person 拷貝只會讓候選少一個~~:拷貝的否決、名字與舊 key 可以讓候選變多、讓 literal 安靜消失、讓新 key 多後綴)。乾跑逐份、逐筆點名,`bootstrap-people --json` 另帶 `unlocatableInPlan`;`--apply` 的拒絕只說筆數、處置與去哪裡看——清單先前被單行出口逃成字面 `\u{000A}`、在 400 處截斷。(4) CLI-only:`authorize-names` 的拒絕點名 entities/ 那份的 key 時,接上它的 legacy 檔(validate 以 legacy 那份的 key 列出兩份並存),清單排在處置之後。(5) CLI-only:結束碼 0 與非零的 stderr 那一行,「要重跑」那一句不再自己說一次無前提的刪除。 + +## CLI-only 裁決表(表頭與引言留在規則檔) + +| CLI 能力 | 裁決 | 理由 | +|---|---|---| +| `view`(list/show) | 候補缺席 | 外延查詢對 agent 有潛在價值,但目前無 MCP 端消費流程 | +| `resolve-divergence` | 有理由缺席 | in-code 既有裁決(`AkashicService.recordDivergence` doc):消歧含合併+全庫改寫+刪檔,tracked+clean 前提與人工確認屬 CLI/App 互動面。**#553(2026-09-11)重新確認,裁決不變**:該輪讓本命令多收一個 shape(venue),而缺席的理由是**操作的性質**(不可逆、要乾跑逐筆過目、要 git 工作樹乾淨),與它收幾個 shape 無關——多一個 shape 不會讓那三件事對 LLM 消費面變得可承擔。`record-divergence` 那一面在 MCP 表已有對應(`akashic_record_divergence`),本輪的 `byShape` 擴充**兩面同時生效**,沒有新的不對稱。**#558 R2(D86)重新確認,裁決不變、契約有改**:tracked+clean 前提自此涵蓋**會被改寫的檔**(倖存者、被改指的 entry、塌縮/改名的歧異記錄),不只被刪的——「要乾跑逐筆過目、要 git 工作樹乾淨」那三件事因此對 LLM 消費面更不可承擔,缺席的理由變強不變弱。**#558 R4(D87/D88)再確認,裁決不變、契約有改**:閘多兩種拒絕——HEAD 解析不到(unborn、orphan 分支、HEAD 指向不存在的 ref,各自的訊息)與 index 標了 `assume-unchanged`/`skip-worktree`(R1–R3 三代都對它 fail-open,R3 verify security 席真 binary 刪檔重現);拒絕清單的上限改成自己的常數,超限時補救句叫人一次 commit 全部。**#565 重新確認,裁決不變、契約有改**:venue 合併的名字併入改成整段搬、被併者原本在 variant 的才標 variant、同名段不能並存時具名拒絕零寫入——多一種拒絕、少一種靜默丟失,缺席的理由(操作的性質)不變。**#565/#587 R1 verify 重新確認,裁決不變、契約有改**:(a) 三方合併的 variant 分類與提醒不取決於被併者順序(倖存者原本沒有的名字,任一被併者列在 variant 就標;「倖存者已有」只對原本的倖存者判斷);authorized 降級的提醒依最終結果;(b) 名字併入是線性的;「完全相同的段」比 source/note 的 UTF-8 位元組,不再用 Swift `String ==`;(c) 不能並存的同名段清單至多列 20 組並給總數,出路依衝突的另一方(倖存者/先併入的被併者)分開寫;(d) 被併者 `field: issn`/`names` 的 reference 逐位元組搬到倖存者(報告 `referencesCarried`,dry-run 也預告),`paginated` 等其餘欄位照舊拒絕並具名;(e) 同一個 ISSN 的角色(兩邊不同、或被併者有而倖存者沒有)具名拒絕。**#709(2026-10-01/10-02)重新確認,裁決不變、契約有改**:同一筆記錄的 legacy 拷貝(同一種、同一個 id)降為 warning 之後,這個命令的拒絕改由兩處擋——候選有 legacy 拷貝(被併者只刪 `entities/.yaml`、legacy 那份會復活)具名拒絕(`StoreIOError.legacyCopyPresent`,在版控檢查之前),以及**會被改指的 entry** 在第一次寫入之前過 #631 的寫入前置(`assertRewriteTargetsWritable`,與 `commitResolution` 的 `entryWritePlan` 同一個函式;#709 R3 verify:先前只有 `commitResolution` 在擋,`--dry-run` 對「第三筆記錄引用候選、自己有 leftover」印「參照將改寫」而實跑被拒)。兩處都在 preview 與實跑共用的 `validateResolvePreconditions` 裡,兩邊擲一樣的錯;`assertNoCrossRecordErrors` 只剩兩筆**不同**的記錄共用 citekey/key/UUID 的 error。缺席的理由(操作的性質)不變 **#564 R2 verify 重新確認(b29 V1),裁決不變、契約有改**:合併搬名字分類記錄按被併者的順序接、每一筆只與那個名字那個分割此刻的最後一筆比(先前以整份歷史去重,倖存者會以撤回結尾而名字仍是對外形);合併後最後一筆與分類矛盾、而那是這次帶進來的,preview 與實跑都拒絕(`wouldContradictClassificationTail`);person 記錄的名字要在合併後的 names 裡(只差空白的拼法 preview 與實跑都拒,先前 dry-run 說搬、實跑在寫入閘失敗);`--dry-run` 的「將隨合併遷移」印出每筆的 statement、依接上去的順序。缺席的理由不變——拒絕多了、預覽說得更多,對 LLM 消費面只是更不可承擔 **#564 b37 重新確認(b36 Y1 第 11/12 列),裁決不變、契約有改**:被併者自己的名字分類記錄就與分類矛盾(手改,或修正輪之前的工具)時,preview 與實跑一律拒絕、說出那筆被併者——先前看全部接完之後的最後一筆,被併者依 key 排序,同樣兩份歷史只換 key 拼法就從放行翻成拒絕;venue variant「不在 variant 卻以指定結尾」的出口改成兩邊都 `--add-variant`(只在被併者上做會被「分類不同」拒絕)。缺席的理由不變 | +| `bootstrap-people` | 有理由缺席 | 批次建檔屬操作者規模;單筆由 `akashic_add_person` 覆蓋(#250)。**#547 重新確認,裁決不變**,但**理由要改寫**:該輪新增的是同一命令的報告欄位(`pendingMutual`)+ 一個 `--json` 出口。上一版的理由是「**不是新能力**,所以不觸發本檔的義務」——那是**用重新分類迴避問題**。誠實的版本是:本檔的機械稽核 ② 枚舉 subcommand、④ 枚舉橫切 `ParsableArguments`,**兩者都枚舉不到 per-command 的 `@Flag`/`@Option`**(實測全 CLI 38 個 `@Flag`、91 個 `@Option`)。那是**收錄機制的缺口**,跟「這算不算新能力」無關——一個 per-command 旗標若真的長出 MCP 面該有的能力,今天沒有任何程序會發現。本格的裁決仍是缺席,理由與 #388 的 `--rows` 同型:`--json` 的消費端是把組別餵給 `add-person` 的人工消歧迴圈,而 MCP 面的輸出進 LLM context、呼叫端無法在收到後丟棄已付的代價;要一次讀完 3,000+ 筆的是操作者,不是 LLM。**缺口本身另案追蹤:#551。** **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名;~~`--json` 的 `legacyCopiesInPlan` 只數 work 拷貝~~——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:person 拷貝也在計畫裡(`legacyCopiesInPlan` 也數它),無法唯一定位的記錄(work 與 person)同樣讓 `--apply` 拒絕,`--json` 另帶 `unlocatableInPlan`;拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | +| `bootstrap-organizations` | 有理由缺席 | 批次建檔屬操作者規模(同 `bootstrap-people`)。原第二理由「org 建模先停(#63/#70)」已由 #304(2026-08-16)廢止——org 重啟後**單筆**建檔的 MCP 面已於 venue change 補齊(`akashic_add_organization`,見 MCP 表);批次面維持 CLI-only **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work 或 person)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:無法唯一定位的 work 與 person 同樣讓 `--apply` 拒絕;拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | +| `bootstrap-venues`(#367;#548 重新確認;#554 R6 重新確認;#563 重新確認) | 有理由缺席 | 批次建檔屬操作者規模(同 `bootstrap-people`/`bootstrap-organizations` 的既有裁決)。**三個 bootstrap 命令的裁決一致不是巧合**:它們的共同形狀是「掃全庫的 literal、按門檻建實體、dry-run 供人審」,而那個規模與審閱動作屬操作者,不是 LLM 消費面。單筆建檔的 MCP 面已有 `akashic_add_venue`(見 MCP 表)。**#548 重新確認裁決不變**:該輪新增的是同一命令的報告欄位(`pendingResolution`——與既有 venue 寬鬆共鍵的群)與一道扣留,不是新命令、也不是新能力面;`bootstrap-organizations` 同批同形。與 #547 對 `bootstrap-people` 的處置一致。**#554 R6/R7 重新確認裁決不變**:該輪讓報告多一個 `reason` 欄位(`Dropped.reason`——被謂詞拒的 literal 從「零字」變成印出並附理由,與產不出 key 的分得開)並把「已有 venue」的檢查移到謂詞之前(只差一個 Cf 的既有刊名 literal 走 resolve-venues 歸戶、不印成「修來源欄位」);仍是同一命令的報告欄位與扣留順序,不是新能力面(R6 verify 第 11 列抓到沒記)。**#563 重新確認,裁決不變、契約有改**(使用者 2026-10-01 裁決):建檔不再寫 `authorized: [names[0]]`、留空——與 `add-venue` 的 #227「建檔不機械偽造」一致,`displayName` 退到 names 的第一段、顯示不變;指定對外形走 `update-venue --authorize`/`akashic_update_venue.authorize`(兩面都有)。既有的機械值不動(#600 逐本判定)。`doctor` 的 `no authorized name` 只數 person/organization,這一改不多出任何診斷——同一個事實也表示新建 venue 的空 authorized 沒有任何掃描面(`docs/store-format.md` §3.1 照實寫),所以 R1 verify 之後乾跑與 `--apply` 的輸出各多一句:新建的 venue 不指定對外形、指定走 `update-venue --authorize`。另一個回報:倖存者的 authorized 為空時,合併的降級提醒句改說「倖存者沒有這個書寫系統的對外形」,不再說「對外形不變」 **#709(2026-10-05)重新確認,裁決不變、契約有改**:計畫讀到 legacy 拷貝(work)時 `--apply` 整批拒絕、零寫入,乾跑照列並逐份點名——細節在本檔 #709 第四次 verify 段。**#709(2026-10-09)重新確認,裁決不變、契約有改**:無法唯一定位的 work 同樣讓 `--apply` 拒絕(person 不在這個計畫裡);拒絕訊息不帶清單——細節在本檔 #709 b36 verify 段。 | +| `fmt` | 有理由缺席 | 全庫改寫=維運例外 | +| `migrate` | 有理由缺席 | 格式遷移=維運例外 | +| `migrate-provenance` | 有理由缺席 | 同上 | +| `migrate-person-identity`(#227/#241) | 有理由缺席 | 格式遷移=維運例外(同 `migrate`/`migrate-provenance`);且不可逆、要求 store 工作樹乾淨的人工 pre-flight,MCP 的 LLM 消費者不是該角色 | +| `migrate-venues`(#304 venue change) | 有理由缺席 | 格式遷移=維運例外(同 `migrate` 族);per-file trackedness pre-flight+部署鏈(release → migrate → validate → 手動 bump format 11)屬操作者角色 | +| ~~`migrate-work-types`~~(#325,**已退場**) | 有理由缺席 → 退場 | 格式遷移=維運例外(同 `migrate` 族);不可逆、要求檔案受 git 追蹤的人工 pre-flight,MCP 的 LLM 消費者不是該角色。**#325 階段二起命令不存在**——它讀不到舊值(舊值在階段二的 decode 就被拒),留著只會是一個永遠無事可做卻看似可用的命令(`no-compat-fallback` 的「退場即刪」)。列保留但劃掉:刪掉會丟失裁決史,而那正是本檔存在的理由 | +| ~~`migrate-venue-variants`~~(#422,**#567 已退場**) | 有理由缺席 → 退場 | **#567(2026-09-29)命令、實作與測試刪除**(`no-compat-fallback` 第 3 條,形狀同 `migrate-work-types` 列):補集規則在 #554 之後為假、live store 的遷移工作已歸零,而 D41 之後它只剩一個永遠拒絕的 `--apply` 與一個唯讀乾跑——看似可用、永遠無事可做。一併裁掉的:`variant` 的 format 14 寫入閘自此補上(`LibraryStore.assertVenueWritable`——「有沒有 pre-bump 遷移」的答案變成沒有,設閘零成本;format-13 store 零實例);仍在 format 13 的 store 要標異寫,先 bump 再用 `update-venue --add-variant` 逐筆標。列保留但劃掉:刪掉會丟失裁決史。以下是退場前的記錄。**2026-09-15(#554 R14 verify regression 第 9 列)**:只用散文禁止的命令仍可執行、會靜默推翻 D1 的未標——CLI 的 `--apply` 自 R15 起 throw(零成本閘,#567 落地前的中間項)。**2026-09-12(#554 R2 verify 第 3 列)**:它的補集規則「names − authorized → variant」在 #554 之後為假——`--authorize` 刻意把換下來的舊指定留在未標(D1),而那筆記錄恰好滿足它的四個條件;live store 的遷移工作已歸零。依 `no-compat-fallback` 退場即刪(同 `migrate-work-types` 列的形狀),本列在 #567 落地時劃掉。原裁決:格式遷移=維運例外(同 `migrate` 族);不可逆、per-file trackedness pre-flight、且**手動 bump format 是分開的一步**(13 → 14)。**這一列不是從 `migrate-venues` 類推來的**:它多一個該族沒有的性質——它做的是**重新分類**而非搬值(`names` 的名字不動,只是多一個 `variant` 標記),所以誤標的出口是**乾跑逐筆過目**而不是 rollback。實測 35 筆,人工可行 | +| `migrate-identifiers`(#394) | 有理由缺席 | 格式遷移=維運例外(同 `migrate` 族與 `migrate-venues`);不可逆、要求每個將被改寫的檔**自身**受 git 追蹤的人工 pre-flight,MCP 的 LLM 消費者不是該角色。**這一列不是從 `migrate-venues` 類推來的**:它多一個該族沒有的性質——它會**跨記錄搬動資料**(work 的 `issn` 移位到它的 venue),所以一次失敗的部分寫入會讓兩邊都不對,而不只是某些檔沒遷到。乾跑是預設、`--apply` 才寫,且乾跑刻意**不**受 #298 的破壞性閘管制——它不寫東西,且正是用來確認目標的手段 **#641 重新確認,裁決不變**:無法唯一定位的 work(`unlocatableCitekeys` 三類)整筆列進 blockers、不寫——它的 ISSN 照樣進 venue 計畫,號不會消失;先前兩份並存的那一筆在寫入迴圈中途才被 #631 拒,前一筆的 ISSN 已從 work 移除、venue 還沒寫,重跑救不回。 **#669 重新確認,裁決不變**:venue key 有不只一筆記錄、而它是 ISSN 的搬移目標時,乾跑與 apply 都在任何寫入之前拒絕(ISSN 要搬進哪一筆無法判定)。 | +| `repair-venue-names`(#575) | 有理由缺席 | 維運例外:它改寫既有記錄,而被改寫前的位元組只剩 git 那一份——`--apply` 要求那些 venue 檔已 commit、乾淨(`assertRecordsRecoverable`),乾跑逐筆過目「venue/清單[index]:before → after」之後才寫;那是在檔案系統與版控旁的操作者角色,MCP 的 LLM 消費者不是。**這一列不是從 `fmt` 或 `migrate` 族類推來的**,差別有兩個:(1) 它改的是名字的**內容**位元組(NFC、空白收斂),不是序列化或格式——`fmt` 對帶 error 的記錄刻意拒絕(它的語意是 canonical 序列化,不修內容,#554 R6),而這正是 #575 立案的原因;(2) 它有一半輸出是**不動**的:要判斷的違反(不可見或控制字元、無字母數字、近重複、正規化後仍不合法、有 reference 指著舊拼法)只具名與理由,給人讀。也不是候補缺席:它修的是舊 binary 與手改留下的違反,MCP 的寫入工具寫不出非 canonical 的名字(`vetVenueNames` 先 canonical),沒有 LLM 流程會產生需求。**有記錄的面不對稱**:名字不變式的 error 經 `akashic_doctor` 的 `recordIssues` 到得了 MCP 面,那則訊息指向這個 CLI 命令——修復面只有 CLI 一面是這一列的裁決,不是遺漏。乾跑不受 #298 閘管制、`--apply` 受(`DestructiveTargetGate.destructiveCommands` 同批加入) | +| `copy-zotero-attachments`(#606) | 有理由缺席 | 維運例外+批次屬操作者規模:它走訪 Zotero 資料目錄的 `storage/`、一趟把全部 `zotero:` 附件的位元組複製進 `sources/` 並連到 `akashic.sources`(追加)——實跑要求被改寫的 work 檔已在 git 裡 commit、乾淨,且 `sources/` 已被版控排除(第三方版權位元組不得進 remote,`replace-endnote-and-zotero` 的承重閘),乾跑逐筆過目後才 `--apply`;那是在檔案系統與版控旁的操作者角色,MCP 的 LLM 消費者不是。**兩面的能力沒有缺**(判準是「可用性該不該取決於用的是哪一面」):單檔的同一件事 MCP 面本來就有——`akashic_store_source`(收檔案路徑,存位元組與取得記錄)加 `akashic_update_entry` 的 `add_sources`(#614,連到 `akashic.sources`);CLI 這一格是同一條路徑的批次形(同 `create-entry` 陣列形的 #455 裁決:兩面同一條實作,缺的只是「一次送多筆」與「自己走訪 Zotero 的 `storage/`」)。**這一列不是從 `import-zotero` 類推來的**:`akashic_import_zotero` 唯讀 `zotero.sqlite`、只寫記錄;本命令複製第三方檔案的位元組,代價落在 `sources/`(不進 git、換機器要重取)與磁碟。日後若出現 LLM 流程需要一次複製上百個檔,重新裁決的是 MCP 面的位元組預算與阻塞(`--rows` 那格的同一個論證),不是加參數。CLI 面過 #298 的目標確認閘(`WriteGateRulings`:`--apply` 才閘,乾跑不閘);**不改寫 `attachments`**——它是 Zotero 擁有的區塊(§2.5),pull 會還原,且它同時是副本的來源記錄。 **#606 R2 verify 重新確認,裁決不變、契約有改**(CLI 一面):位元組在而 index 沒有條目的已連 digest 另列「已補記」(`recordRestored`,補上取得記錄、不改連結);補存擲錯另列(`restoreFailed`,照樣非零結束),不再記進 work 的 `writeFailed`;只補存時標題不說「已寫入」、結尾不叫人 commit;丟棄的取得記錄逐行說位元組是早已在還是這一次才存。缺席的理由不變——單檔的同一件事 MCP 面仍是 `akashic_store_source`+`akashic_update_entry` 的 `add_sources`。 **#703 重新確認,裁決不變、契約有改**(CLI 一面):超過 256 MiB 的附件逐檔具名略過(印路徑與大小,乾跑就印);digest 與複製逐塊;已在 `sources/` 的同一個 digest(已連過、要補記、新連結遇到既有存檔)逐塊比對內容,不符另列「內容與 Zotero 原檔不符」(`storedBlobMismatch`)——不覆寫、不補記、不新連,`--apply` 遇到時非零結束。**R1 verify 之後**:新連結遇到位址上是目錄、symlink 或讀不到的存檔也具名略過、不連(先前只擋內容不符,照樣連上並報「已複製」);同一筆內內容相同的第二個附件跟著第一個的結果(不再一律算已連過);存檔前置在第一次寫入之前也問暫存檔的路徑。缺席的理由不變:MCP 面的單檔對應 `akashic_store_source` 同批加了同一個上限。 | +| `validate` | 有理由缺席(理由於 #416 換過)| **原理由「讀取檢查由 `akashic_doctor` 覆蓋(功能重疊)」被量測否掉**:`Entry.validate()` 在全樹只有一個呼叫點(CLI),`doctor()` 與 App 面各 0。**落差是 warning 一族**(#416 R1 更正:原本寫「有一條是 error 級」,實測那條到不了——load 的 quarantine 先擋下,而 quarantine 本來就在 `StoreHealth` 裡)。#416 把 per-record 驗證抽進 `StoreHealth.perRecordIssues`,兩個消費面因此都拿得到——**現在**才真的重疊。CLI 面維持獨立命令的理由改為:它**不加面級截斷**、逐則列出,而 MCP 面截斷 20 則(輸出進 LLM context,呼叫端無法在收到後丟棄已付的代價,#236 的既有威脅模型)。這是**呈現粒度**的差異,不是能力缺席。**#554 R24(D66)重新確認,裁決不變、理由收窄**(R23 verify Codex 第 2 列):R14–R26 陸續在 `validate()`/`StoreHealth` **產生訊息時**就套了 per-record 上限(`Entry.perRecordWarningCap`),**只在組合式的六族**:venue 名字內容、venue 的 names/authorized/variant 近重複、**person 近重複**(R26 D72、#576)、重複 venue 邊、confirmed literal、重複判定記錄——各族每筆記錄至多 20 則加一句概括;venue 名字近重複與 person 近重複另各有**整筆 100,000 對**的求值總量上限(R18 D52/R27 D76:venue error 級整筆拒絕、person warning 級只計數並自 R28 起小組先評估,R27 verify 第 25 列指出這一格只列了四層)。那是**求值的安全上限**(讀取路徑對未信任的 store 內容跑,R14 verify security 第 18 列):這六族的則數是每筆記錄的**組合**(配對數/組數),會被一筆記錄撐爆;死 verdict、矛盾 verdict、本機缺存檔、拆分後孤兒 verdict、拆分/移除記錄、記錄檔逼近讀取上限(venue 族與 person/organization 族)、沒有合併管線的歧異記錄、重複的 reference(#582,每組一則)這幾族**沒有上限**(2026-09-27 #582 R1 verify:這份名單先前漏了中間三族,#582 又加一族),它們的則數是每筆 reference/配對/記錄/組各一則——與那筆記錄持有的資料項數**線性**(1,352 筆 verdict 的 holder 全退役就是 1,352 則,不是「每筆至多一則」),上限的理由(組合數遠超資料項數)對它們不成立、上界就是資料項數(R25 D70 → R26 D72;R24 verify 第 12/14/37 列:R24 在 App help 寫成「每筆記錄每族」、對五族以外為假;R25 verify DA 第 2 列 HIGH、第 6/11/15 列:R25 寫「五族」漏掉 person 近重複——它當時是組合式且無上限、真 binary 一筆記錄 19,900 則——又寫「其餘線性、每筆至多一則、撐不爆」,對六族全假。兩輪各換掉一句假話又寫了另一句:這一格的措辭自此逐族點名、不再用「其餘」)。上限所在的三個面共有——所以「逐行、無截斷的完整報表」這句在那之後為假:被截的記錄在 CLI 也只有概括句。~~**不加「完整列舉模式」**:那等於替 CLI 開一條繞過求值上限的路,而上限存在的理由正是讀取路徑不能被 store 內容撐爆;概括句說了「另有 N 個未列出」,沒有東西是沉默的;要全部只能讀 YAML——**那個缺口記成 #581**(R24 verify 第 5/17 列:`replace-endnote-and-zotero` §4 要求能力缺口記 issue,不得靠「讀 YAML」帶過;出口的形狀要在本檔兩面一起裁)。~~ → **#581 裁決(使用者 2026-09-28):出口是單筆完整明細,不是全庫的完整列舉模式**——上一句拒絕的那種(對整份 store 放寬)仍然不做。`validate --owner :` 對**指名的一筆**記錄不套組合式六族的列出上限(`PerRecordListing.full`):上限存在的理由是對整份未信任 store 求值的界限,呼叫端指名一筆之後,輸出的上界是那一筆自己的組合數,不重開那個洞。**不放寬的三樣**(寫出來,免得「完整明細」被讀成「沒有任何上限」):近重複的組內 5,000 對與整筆 100,000 對**求值**上限(觸頂時概括句照樣出現、不再說「每筆記錄最多列 20 組」——那句在完整明細裡是假話);訊息內部的列舉上限(一組列 3 對、一則列 5 個拼法);MCP 面的位元組預算。kind 必填、不猜(跨 kind 同 key 實測 2 個:organization×venue),找不到或同 kind 同 key 兩筆以上都拒絕;範圍是那一筆的 per-record 問題(含掃描族裡 owner 是它的那幾則),跨記錄檢查與 quarantine 不屬於任何一筆、不在範圍內,輸出末行說出這一點。MCP 的對應是 `akashic_doctor` 的 `owner`(見 MCP 表那一列:受位元組預算、`total`/`truncated` 揭露);不帶 `--owner` 的 `validate` 在有記錄被截時多印一行「被截的記錄: N」並指路。`ValidatePerRecordCapCLITests` 走真 binary 釘住這個契約(不帶 `--owner`:25 個配對 → 恰 20 則 +「另有 5 個配對未列出」+指路行;帶 `--owner`:25 則全部具名、沒有概括句)。CLI 與 MCP 的差別自此只剩**面級**截斷有無 | +| `rename` | 有理由缺席 | 高風險身分操作(citekey 遷移含 verdict value 重寫,#232)=維運例外。**#642 R1 verify 重新確認,裁決不變、契約有改**:改名同批遷移 library 的成員規則(第 16 條邊:文件型的文件、規則型的排除清單),registry 檔要 tracked 且 clean、與其他被改寫的記錄同一批預檢;新 citekey 已被規則指涉或被隔離的 registry 檔提到時拒絕;遷移逐條列在輸出裡(CLI 一段、App 回執一行)。App 的改名(第三面)走同一個 `renameEntry`,回執多一行「library 成員規則已遷移」 | +| `rename-person`(#395) | 有理由缺席 | 同 `rename` 的理由,**而且遷移面更大**:除 verdict value(`person:` holder,掛在 person、organization 與 venue 三處——R21 verify 第 35 列:這裡曾寫「兩處」,#463 起 venue 迴圈也在)外,還含全庫 `authors[].key` 與 divergence 的 `candidates`/`judgement.prefers`。**這一列不是從上一列類推來的**——它的參照集合是照 `entity-backlink-completeness` 的封閉列舉逐條窮舉出來的(第 1、9、10、13 條),與 citekey 的那組**不重疊**。維運例外的理由因此更強而非更弱:漏一格的後果是安靜的(檔案照樣載入,只是某些邊指向不存在的 key) | +| `authorize-names` | 有理由缺席 | 批次策展=操作者規模 **#564 重新確認,裁決不變、契約有改(BREAKING)**:`--apply` 必附 `--judgement`(整批一句,早於開 store 的用法錯誤;乾跑不需要),每個寫入的 person、每個被採用或提名的名字各寫一筆 `field: authorized` 的 `指定:理由`(不帶證據),需要 store format ≥ 22;**不再寫 store marker**(升 marker 是使用者的動作)。本面維持 CLI-only:批次策展屬操作者規模。~~名字分類的單筆 person 面不存在(person 的 authorized 經 `authorize-names` 或手改,`update-person` 的 references 明文拒收名字分類記錄)~~ → **#564 R1 verify(b26 F2 第 2/5/11 列)指出這句為假**:`update-person` 的 `fields.names` 全量替換一直寫得進 authorized,只是不附理由、不留記錄。使用者 2026-10-02 裁決(#564 第 1 點)把它補成單筆的名字分類面——見 `akashic_update_person` 那一列;`update-person` 的 references 仍拒收名字分類記錄(只經名字分類面寫)。批次的 `authorize-names` 不設一次 200 個名字的上限(#564 第 4 點):它每個 person 至多寫三筆(每書寫系統一個對外形),上限防的是單筆記錄被同一句理由灌爆,這裡不會發生。 **#641 重新確認,裁決不變**:寫入集合裡有無法唯一定位的 person(`unlocatablePersonKeys`)時 `--apply` 在第一次寫入之前整批拒絕、零寫入——先前逐筆寫到一半才被拒,留下已指定卻沒 bump marker 的 store;乾跑照常出報告。 **#709(2026-10-05)重新確認,裁決不變**:寫入集合裡有任何一筆的 id 還有 legacy 拷貝時同樣整批拒絕、零寫入(`blockedKeys` 一併列出)——先前只擋「legacy 那份本身在寫入集合裡」。**#709(2026-10-09)重新確認,裁決不變**:點名 entities/ 那份的 key 時接上它的 legacy 檔(`blockedCopyFiles`)——validate 以 legacy 那份的 key 列出兩份並存。 | +| `export-tables --view`(#274) | 有理由缺席 | 匯出物是檔案樹,MCP 的回傳形狀未定——#274 註記的正式落位 | +| `file add`/`file remove`(#700 R1 verify 第 9 則) | 有理由缺席 | **註冊與除名 store 是改 registry——部署層的名冊,不是呼叫層的能力**,與橫切選項表 `--config` 那一格同一個理由:讓 LLM 消費者改寫「有哪些 store」這個部署決定。MCP `akashic_files` 收 `list` 與 `use`——`use` 只在名冊**內**切換 session 的 active store、不改名冊(`WriteGateRulings` 把 `file use` 記為只改 registry 的 current);`file add` 寫 registry 並確保佈局(缺的目錄與格式標記才建立——新建一個 store 時是整個佈局;另建化身 id 檔 `incarnation`;既有的 `.gitignore` 沒有 sources 標記區塊時在尾端以位元組附加一段,#66 的排除,原有的位元組不動——b29 V5 LOW 13:先前寫「在佈局不存在時建立」,比行為窄;#700 b31 W5:「既有的行不動」先前只對可讀的 UTF-8 一般檔成立,讀不懂的檔整份被換成只含區塊、symlink 被換成一般檔,現在讀不懂的(讀不到、非 UTF-8 或含 NUL、symlink 而指向的內容沒有區塊、不是一般檔、沒有寫入權限)在建立任何東西之前具名拒絕、不寫 registry),`file remove` 只除名、不刪任何資料。上方 MCP 表 `akashic_files` 那一列的 ✅ 是 `file` 家族的 list/use 功能重疊,**不含** add/remove——#700 的逐腿守衛(`ToolPayloadLegs.commands`)找到這個落差時,它只寫在測試註解與 changelog,這一列補上裁決。**不是維運例外**(沒有不可逆操作:add 只新增一筆名冊、同 key 或同路徑已註冊時具名拒絕、不覆寫(b26 F6:先前這裡與 CLI help 都寫「add 冪等」,不成立——重跑同一個 `file add` 會以「已存在」拒絕,只有佈局的確保是冪等的——標記區塊已在就不再附加);remove 只除名、不刪資料),**也不是候補缺席**:沒有 LLM 流程需要在 session 內註冊 store(store 由伺服器啟動時的環境決定)。**觸發條件**:出現需要在 MCP session 內註冊新 store 的流程時重開——那時要裁的是要不要給 `akashic_files` 加 `add`/`remove` action,並一起重裁 `--config` 那一格 **#700 b33 verify**:不改寫的再加四類(同 `akashic_doctor` 那一格);開檔、寫入時才發現的原因在佈局建好之後拒絕、同時跑的程序加好區塊時照常成功——`file add --help` 寫明。 **#700 b36 verify**:判讀同 `akashic_doctor` 那一格的 b36 補記——`.gitignore` 是 symlink(不論指向的內容有沒有區塊)、規則行帶行首空白或行尾 tab、等鎖逾時,`file add` 一律拒絕(先前 symlink 指向有區塊的檔、規則行帶前後空白時照常成功,而 git 不排除 `sources/`);`file add --help` 寫明。 | +| `create-entry` 的 JSON 陣列與 `library` add/remove 的多 citekey(批次語意,#455) | 有理由缺席 | **批次屬操作者規模**(同三個 `bootstrap-*` 的既有裁決):一次 load、批次內消解 citekey 碰撞、可預期失敗整批擋零寫入、I/O 逐筆收容、一次 rebuild,是目錄匯入(1545 筆量到 O(n²))的使用形;MCP 的 `akashic_create_entry`/`akashic_libraries` 維持單筆——LLM 消費面逐筆顯式指名,且兩面**同一條實作路徑**(單筆是 `createEntries([draft])`/`setMembership(citekeys: [ck])` 的薄包裝),所以缺的只是「一次送多筆」的參數形狀,不是能力。日後若出現 LLM 流程需要一次建千筆,重新裁決的是 MCP 面的**位元組預算**(#388 的同一個論證),不是加參數 | +| `references`(`extract`/`nominate`;#617) | 有理由缺席 | **skill 的內部步驟,不是 store 能力**:輸入是 `akashic-work-references` 經本機工具(`pdftotext`)與 safari-browser 取得後寫下的暫存檔,輸出是下一步(模型逐筆判定)的中間資料;不寫 store(`nominate` 唯讀查候選的 DOI 是否已在庫,那是這次比對的一個欄位,不是對外提供的查詢能力)。開 MCP 面等於要 LLM 呼叫端把 PDF 全文當工具參數送進來——呼叫端為大輸入付代價,與 #388 位元組預算論證同一個方向。不是維運例外(沒有不可逆操作),也不是候補缺席(缺的不是消費流程:唯一的消費流程就是那個 skill,而它在 shell 裡直接呼叫 CLI) | +| `literal-census`(#629,由 literal-census.sh 移植) | 候補缺席 | **唯讀量測,是 `akashic-promote-literals` skill 每輪開場與收尾各跑一次的步驟**(進度數字落一筆 issue comment);目前唯一的消費流程就是那個 skill 在 shell 裡直接呼叫 CLI。MCP 的 `akashic_doctor` 給的是 store 健康計數(含 `unresolvedAuthorLiterals`),口徑不同:普查是**四域**(author/venue/affiliation/org-parents)三個口徑(總邊/literal 邊/distinct),而且要在**讀端拒開的 store 上照樣出數字**(不經 `openStore()`,marker 壞掉或版號太新正是它標「這一輪的數字不能拿去定批次範圍」的情形)——兩者不重疊。**不是維運例外**(沒有不可逆操作)。需求出現(例如有 LLM 端自動跑 campaign 的流程要讀進度)即重啟裁決 | +| `scan-yaml-profile`(#629,由 scan-yaml-profile.py 移植) | 有理由缺席 | **repo 開發者重測 #33(store YAML 輸入 profile)決策的證據工具**,不是使用者能力:對象是**檔案樹**(任何一個 store 目錄,含讀端拒開的),輸出是給人讀的統計表,用途是「corpus 有沒有長出 profile 外的語法」。MCP 的 LLM 消費者不做 corpus 遷移成本評估。不是維運例外,也不是候補缺席——沒有需要它的消費流程。**#629 R1:型別放在 `AkashicCore` 但存取層級是 `package`**(同一個 package 的 target——CLI、MCP、AppKit、測試——看得到,package 外的使用者,例如 Xcode 的 App 專案,看不到;R2 更正了先前「MCP 看不到」的說法),因為它不是使用者能力;根不是資料夾、讀不進來的檔、零個 `.yaml` 都具名失敗,不印全零 | +| `fulltext`(`verify`/`bot-signals`/`jitter`/`fetch`/`take`/`calibrate`/`contract`;#629,由 `akashic-fetch-fulltext/scripts/*` 與 `fetch-fulltext.sh` 移植;#613 刪 `url-rule`、加 `take`;#613 R3 加 `contract`) | 有理由缺席 | **取全文 skill 的內部步驟,不是 store 能力**:輸入是本機 PDF、頁面文字、使用者的 Safari 視窗與 Crossref 回應檔,輸出是下一步(agent 判斷、`store-source`)的中間資料或一個結束碼;**全部不寫 store**(`take` 只寫 `--out` 指定的檔,且拒絕寫進沒有 ignore 它的 git 工作樹——全文是第三方版權內容;#613 起 `fetch` 不寫任何輸出檔,只在 store 之外的每日嘗試帳本記一筆)。開 MCP 面等於要 LLM 呼叫端把 PDF 全文或使用者的瀏覽器 session 當工具參數送進來——呼叫端為大輸入付代價(與 #388 位元組預算論證同一方向),而 `fetch` 要動使用者的 Safari,那是使用者授權給**這個 agent 的這個 skill** 的動作,不是一個能對任何 MCP 客戶端開放的能力(中止條款要求出事時分頁留給使用者看)。不是維運例外(沒有不可逆的 store 操作),也不是候補缺席(缺的不是消費流程:唯一的消費流程就是那個 skill,而它在 shell 裡直接呼叫 CLI)。結束碼是對 agent 的契約,見 `FulltextFetch` 的型別註解。**#629 R1 重新確認,裁決不變、契約有改**:`fetch` 的 `--expect-profile` 命令列必填、`--landing`/`--prime` 只收 https、頁面 DOM 的 PDF 連結 origin 要與頁面相同、輸出目的地不得是目錄/symlink/特殊檔(覆寫為原子替換)、git 閘 fail-closed;`calibrate` 一個檔案都沒量到回 4、缺記錄列完整網址 **#629 R2/R3 重新確認,裁決不變、契約有改**(兩輪都只動 CLI 面,沒有 MCP 面可對):(a) R2:`--landing`/`--prime` 的主機要**長得像**一般網域名稱——不收 localhost、IP 位址、埠號、帳密與幾個已知的私有字尾;這是字面的形狀檢查、不解析 DNS,解析到私有位址的名稱照樣通過(R3 verify 更正「公開主機」的說法);整串不得有引號、反斜線、反引號、`$`、`#`、空白或控制字元(所以規則檔記的「對 `--landing` 加一次性 fragment」要先改這道檢查);頁面 DOM 的 PDF 連結要是頁面裡解析好的同源 `https://` 絕對網址;原子替換的暫存檔名也進 git 閘——只逐字忽略三個最終檔名的工作樹現在被拒;(b) R3:取 PDF 的請求被轉址到別的 origin 時不讀本文、結束碼 1(stderr `redirected off-site`;先前同源檢查只綁第一跳);`--landing` 是 doi.org 網址時的 DOI,scheme 與主機不分大小寫、只認 `https://doi.org/` 與 `https://dx.doi.org/`(`http://doi.org/` 本來就走不到)。**誠實邊界**:git 閘只問 `--out` 的目錄,下載中的暫存目錄在 `$TMPDIR` 底下、不經檢查 **#613 重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對):(a) `fetch` 只在使用者的 Safari 裡導航到頁面自己的 PDF 連結、交給人——不在頁內取檔、不取位元組、不寫輸出檔;`--out`/`--title`/`--pages`/`--doi`/`--prime` 拿掉(舊的呼叫以 64 失敗,不會被讀成「存好了」);結束碼改為 7 交給人、8 等人驗證(`--resume-tab`/`--resume-origin` 在同一個分頁接著走)、9 每站每日上限、6 整批暫停、3、1,沒有 0;(b) 新增 `take`:收人存下來的本機檔,驗證、git 閘、存檔,不碰瀏覽器、不連網——驗證與 git 閘是原本 `fetch` 的那一份,搬過來;(c) `url-rule` 與出版商拼網址規則刪除;(d) `bot-signals` 加 `--kind`(`verify`/`pause`),命中的結束碼仍一律 0。不開 MCP 面的理由不變,而且更強:`fetch` 的每一步都假設旁邊有使用者(交給人、等人驗證)。**帳本不在 store 裡**:`~/Library/Application Support/akashic/fulltext-attempts.jsonl`(`~/.akashic` 是 `main` store 的 root,放進去會變成資料 repo 裡未追蹤的檔)**#613 修正輪(2026-10-02)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;使用者 2026-10-02 兩則裁決與六個程式錯誤):(a) `fetch` 導航之後分頁到了別的主機而沒有任何驗證/封鎖/登入的標記:新主機顯示 PDF → 交給人(7,`pdf-shown`)、其他頁面 → 這一筆交給人(7,新原因 `left-site`)、DOI 解不開 → 7(`doi-not-resolved`),批次繼續;登入、驗證、封鎖頁維持 6;(b) 等人驗證(8)只在文章站本身或已知的驗證服務(`BotSignals.isKnownVerificationService`,封閉清單)上成立,其他主機上的驗證字樣一律 6;HTTP 429 一律 6;(c) 新旗標 `--resume-stage article|followed`(結束碼 8 在導航之後印 `followed`):導航之後的驗證接著走「分頁顯示什麼」的判斷,不再讀連結、不再導航、不再記嘗試,分頁可以在別的主機;(d) 查數與記錄是一步(`FulltextAttemptLedger.reserve`,跨行程的 `flock`)、記錄之前補檔尾換行;(e) `take` 的 `--title` 必填、不得是空的(沒給或空字串是 64,先前跳過驗證仍結束碼 0)、驗證本身跑不起來(截斷的下載、缺 poppler)是 1 而不是 5;(f) `take` 不再印非 PDF 檔的內容(只印位元組數、前 8 個位元組與是否像 HTML);`handover:` 行去掉網址的查詢與片段 **#613 R2 修正輪(2026-10-02)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;R2 verify 與使用者 2026-10-02 兩則裁決):(a) `fetch` 導航之後分頁到了別的主機:**先問 PDF**(`document.contentType`)→ 7 `pdf-shown`,不再先看標題與網址的長相(PDF 分頁的標題常是文章標題);不是 PDF 的頁面要等它載完(最多 60 秒,沒載完是 6 卡住),載完之後標題、網址、頁面文字與 HTTP 狀態一起判訊號(429 一律 6,已知驗證服務上也是),登入/驗證頁的長相(網址的主機字詞與**整段**路徑、標題的**整字**片語,封閉清單)→ 6,其餘 → 7 `left-site`;(b) 「還在 doi.org」看主機不看字串;`doi-not-resolved` 要 doi.org 自己的查無證據(標題或頁面 `DOI Not Found`、或 404),否則 6;doi.org 上的驗證字樣是 6、不印 `resume:`;DOI 落地頁的網址是登入頁的長相(EZproxy、SAML 帶 DOI 的查詢字串)→ 6;(c) `--resume-stage followed` 只接文章站、已知驗證服務、或顯示 PDF 的分頁,其他是 1;(d) 結束碼 8 的 stderr 改成請使用者看頁面、絕不貼上或執行頁面要求的東西、只在使用者說完成之後接著走(文章站本身擋不住假的 CAPTCHA,裁決文章站算);(e) 停止與錯誤訊息、`page:` 一行的網址也去查詢、帳密與 `;jsessionid`;`doi-not-resolved` 不再叫人把存下的檔交給 `take`;(f) 每日帳本的站名在寫入與查數時也正規化(小寫、去空白) **#613 R3(2026-10-04)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b31 W4):(a) 新子命令 `contract`:印 `fulltext-contract `(`FulltextFetch.contractVersion`,這一輪是 4),唯讀、不碰瀏覽器、不連網、不讀不寫任何檔;SKILL 第 0 步以它分辨比 SKILL 舊的 CLI(先前 `take` 的兩條探測分不出 R1、R2)。不開 MCP 面:它只回答「這個 CLI 照哪一版契約走」,消費者是 skill 的 shell 步驟;(b) `fetch` 別的主機上讀不到的分頁(同一個網址連三次讀不到、那個網址上從沒讀到過一般網頁)→ 7 `unverifiable`,不再因標題或網址的字樣得 6;網址在已知驗證服務上、或標題與網址帶著驗證服務自己的標記(封閉的六個標籤)照訊號處理;讀到過「還在載入」的網址等不到是 6 卡住;(c) DOI 落地頁與 `article` 接續:登入頁的長相先於頁面文字判(文字寫著 CAPTCHA 的登入頁也是 6),驗證頁的長相仍在訊號之後;(d) 別的主機上的判斷用同一份快照,讀的當中分頁換了頁就回去等;(e) 等落定以時鐘約 60 秒計(也不超過 30 次輪詢);(f) 訊息、`attempt:` 一行與帳本的站名不帶主機前的帳密;網址另去掉 `;名=值` 的路徑參數與 ASP.NET 的 `(S(…))` 段;`pdf-shown` 的訊息說依據是頁面讀得到的 `document.contentType`;`followed` 接續的分頁空白時不稱它「我們的分頁」 **#613 b34(2026-10-05)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b33 X3 與使用者 2026-10-05 四則裁決):(a) `contract` 印 5;(b) `fetch` 文章站上讀不到的分頁與別的主機同一套(不因標題或路徑的一般字樣得 8 或 6,交給人 `unverifiable`),讀不到的分頁停在登入主機上(只看主機)是 6;文章站上 HTML 頁的判斷與交給人印的網址用同一份快照,讀不到的計數改成同一個網址上連續;(c) 導航之前的接續:主機的登入長相是 6 且不送 JS,分頁顯示 PDF 先交給人(路徑有登入字詞也是),驗證頁的網址而沒有訊號時等它轉走(約 60 秒,只讀網址),沒轉走是 6;(d) 主機的登入字詞只認最左邊的標籤與四個 IdP 服務名(`journals.auth.gr` 不再停),網頁副檔名多 `cfm`、`cfml`、`xhtml`、`jsf`、`jspx`、`faces`、`shtml`、`phtml`,登入字詞拆複合段(`/sso-login`);(e) 頁面連結的 origin 不帶帳密比較與印出(帶帳密的落地頁上同站連結照常跟);訊息另去掉百分比編碼的路徑參數、多群組的 ASP.NET 段、沒有 scheme 的帳密,safari-browser 自己的錯誤訊息裡的網址同樣去憑證;每日帳本的 `landing` 不帶查詢 **#613 b37(2026-10-09)重新確認,裁決不變、契約有改**(只動 CLI 面,沒有 MCP 面可對;b36 Y2 與使用者 2026-10-09 裁決):(a) `contract` 印 6;(b) 複合路徑段只認明確的登入字(`login`、`logon`、`signin`、四個 IdP 服務名,與 `sign-in`、`log-in` 兩種連字號寫法),`cas`、`auth`、`idp`、`sso`、`authenticate` 只在整段比(`hepatitis-c-as-…` 這種 slug 不再在落地頁停);(c) 讀頁面的當中分頁換到同站的登入頁時,那一頁先判登入長相(6,不印 `resume:`)——落地頁、讀連結之前、導航之前的接續;導航之後同站的 HTML 頁仍交給人 `html-page`;(d) safari-browser 失敗時只轉印它 stderr 的第一行(其餘的行可能列出其他分頁與其他 profile 的網址);(e) 印出的網址與站的比對用同一個主機(`URLSplit.hostPort`:反斜線結束 authority、沒有 scheme 的 `//` 先判) | +| `crossref-match`(#629,由 `akashic-bootstrap/scripts/crossref_match.py` 移植) | 有理由缺席 | **skill 的內部步驟**:輸入是 skill 經 safari-browser 取回、存在本機目錄的 Crossref 回應與一份作品清單,輸出是判定 JSON 檔(`confident`/`probable`/`needs_review`/`no_result`);不寫 store、**不連網**(重播式:缺的請求以 JSON 列出、exit 3,由 skill 取回後重跑)。MCP 沒有「取回」這一半,另一半(比對)沒有 store 狀態可讀,開面只會把回應檔的內容當工具參數送進 LLM context。判定的下游是 `akashic-bootstrap` 的人工核對,不是任何 MCP tool | +| `web-read`(`url`/`origin`/`landing`/`check`;#692 R4,由 web-access.md 裡要照抄執行的 check-read.py 移植;`url` 是 b33 verify 加的開分頁前檢查) | 有理由缺席 | **skill 讀頁面時的內部步驟,不是 store 能力**(與 `references`、`fulltext` 同一類):輸入是 `safari-browser documents --json` 的輸出與 skill 寫進暫存目錄的檔(讀回的 JSON、Safari 回報的主機),輸出是一行判定與一個暫存檔(驗過的落地主機、剔除後的頁面文字);**不寫 store、不連網、不碰瀏覽器**。開 MCP 面等於要 LLM 呼叫端把未剔除的第三方頁面文字當工具參數送進來——那正是這個檢查要擋在 context 外的東西(剔除在它之前做才有意義),也與 #388 位元組預算論證同一方向。唯一的消費流程是 web-access.md 的 bash 區塊,它在 shell 裡直接呼叫 CLI。不是維運例外(沒有不可逆操作),也不是候補缺席 **b33 verify**:`landing`/`check` 會刪 `--out`/`--raw` 指的檔——只刪一般檔或 symlink,目錄與其他型態在刪任何東西之前拒絕(先前對目錄整棵遞迴刪除);路徑由呼叫端給、程式不限制位置,「寫的是 skill 的暫存目錄」是 web-access.md 區塊的約定,不是程式強制的(`WriteGateRulings` 那三格同此寫實)。 | +| `abstracts-to-proposals`(#629,由 `akashic-venue-works/scripts/ndjson-abstracts-to-proposals.py` 移植) | 有理由缺席 | **一個 adapter**:輸入是 `akashic-venue-works` 階段 B 的私有產物(NDJSON,形狀不是 store 的契約),輸出是 core 的公開格式 `[Proposal]`(唯一解析器 `AddOnlyEnrichment.decodeProposals`,`akashic enrich --from`/MCP `akashic_enrich` 的 `proposals` 都吃它)。它自己不寫 store(只讀 `sources/` 的存檔、只寫 `--out`);寫入的那一步是 `akashic_enrich`,兩面都有。#516 當時把它放在 skill 目錄、不進 CLI,理由是「把私有形狀焊進 CLI 要在 parity 表加一列」;#629 的 `swift-is-the-implementation-language` 要求 skill 目錄裡不得有 Python,於是它成為子命令並在這裡加一列——**私有形狀只焊進 CLI 這一面、不進 MCP**,換一個來源形狀時是另一個 adapter | + +## 識別碼寫入面的裁決(#394,2026-08-24) + +本 change 新增了五個識別碼欄位(`venue.issn`/`organization.ror`/work 的 `doi`・`pmid`・ +`isbn`),而**兩面都沒有為它們新增參數**。這不是遺漏,是一個要寫下來的裁決——依本檔的 +規則,新增能力時必須裁決兩面,而「兩面都不給」也是一種裁決。 + +| 面 | 現況 | 裁決 | +|---|---|---| +| MCP 寫入面(~~add_venue~~/~~update_venue~~/~~add_organization~~/~~create_entry~~)| ~~不收識別碼參數~~ → **四格全部補齊(#394,2026-08-28)**:`update_venue.add_issn`/`add_venue.issn`/`add_organization.ror`/`create_entry` 的 `doi`・`pmid`・`isbn` | ✅ **這一列已關閉** | +| CLI 寫入面(同名命令)| `update-venue --add-issn`/`add-venue --issn`/`create-entry` 的 JSON draft 收 `doi`・`pmid`・`isbn`;`add-organization` 是 **MCP-only**(本檔既有裁決,不是缺口)| ✅ | +| 既有的 person ORCID 欄位 | **兩面都收**(經 generic `fields` JSON object,#68 的既有形狀)| ✅ 既有,本 change 只改型別不改介面 | + +> **這張表的第一欄刻意不用反引號包 token。** `parity-table-drift.py` 把表格第一欄的 +> 反引號 token 一律當成「宣稱存在的 CLI subcommand」並去 `CLI.swift` 對照——第一版把 +> `person.orcid`(一個**欄位**)放在第一欄,於是守衛報「表列了 `person.orcid` 而它已不在 +> CLI.swift」。守衛沒錯,是我的表格形狀在對它說謊。 + +**缺席的理由**:識別碼的來源目前只有兩條——遷移(`migrate-identifiers`,一次性)與匯入 +(`import-wos`/`import-zotero`,走 `fields` 殘留再由遷移升格)。**沒有第三條路徑產生 +識別碼**,所以現在加寫入參數是替一個還不存在的流程造介面。 + +**但這一列必須標記為弱,理由要寫出來**:遷移之後,若使用者查到一筆 work 的 DOI, +**沒有任何面寫得進結構化欄位**——實測 `entry.doi` / `v.issn` / `org.ror` 在 +`Sources/` 內的 production 寫入路徑**零命中**(`IdentifierMigration` 與 YAML 編解碼除外)。 +唯一的路是手改 YAML。 + +這正是 `replace-endnote-and-zotero` 第 4 條要防的形狀: + +> 使用者說「這個功能我回去用 Zotero 做」一旦變成常態,就是取代失敗的樣子——而且它是 +> **安靜的**失敗,因為每次個別繞過都看起來很合理。 + +**venue 那一格已於 2026-08-28 補齊**(`update-venue --add-issn`/`akashic_update_venue` 的 `add_issn`)。動手的理由不是等到了實例,是相反的:`Venue.issn` 的欄位、型別與正規化都已存在,而**只差一個參數**——把它留著等一個實例,等於讓「查到 ISSN 卻只能手改 YAML」這件事在下次真的發生時才被修,而手改 YAML 的失敗方式是安靜的(2026-08-28 實測差點弄丟一筆 DOI)。 + +**`add_venue` 與 `add_organization` 同日補齊,理由與 venue 那格同**:欄位、型別、正規化都已存在,只差一個參數;而建檔時本來就知道刊物的 ISSN 與機構的 ROR,少了它得「先建再更新」——一次操作變兩次,中間有一個識別碼不在的狀態。 + +**形狀刻意不同**:ISSN 是**清單**(print 與 electronic 是兩個真的號),ROR 是**純量**(一個機構只有一個)。兩者都是「不合法即整個呼叫拒絕、零寫入」——建檔面的拒絕比更新面更強:若守衛只擋識別碼而讓記錄建了出來,結果是一筆「使用者以為帶識別碼、實際沒有」的記錄,比明確失敗更糟。 + +**`create_entry` 同日補齊,這一列因此關閉。** 理由與前三格相同(欄位、型別、正規化都已存在,只差一個參數)。 + +**`.bib` 匯入路徑刻意不帶結構化識別碼**:`.bib` 的 `DOI = {...}` 進 `fields`,走**殘留路徑**由 `migrate-identifiers` 升格。在匯入端順手升格會製造第二條升格路徑,而那條路徑對「解不了的 token」的處置與遷移端不同(遷移把它留在殘留並報出來),兩者會分岔——這正是同日 #424 修掉的那個 bug 的形狀(一個 `return` 綁兩個決定)。 + +**這一列關閉後,`replace-endnote-and-zotero` 第 4 條在識別碼這一格不再有「回去用 Zotero 做」的理由**——查到一個識別碼之後,四個寫入面都收得下。那時要裁的 +不只是加參數,還有**它該長什麼形狀**——`person.orcid` 走 generic `fields` object,而 +`venue`/`entry` 的寫入面目前沒有對應的 generic 通道(`update-venue` 只收 `key`)。 +追蹤:#394 close 前不處理;本列即是那個「已知且具名」的缺口。 diff --git a/docs/rulings/two-kinds-of-edits.md b/docs/rulings/two-kinds-of-edits.md new file mode 100644 index 00000000..b8fd21a1 --- /dev/null +++ b/docs/rulings/two-kinds-of-edits.md @@ -0,0 +1,57 @@ +# two-kinds-of-edits:裁決表 + +從 [`.claude/rules/two-kinds-of-edits.md`](../../.claude/rules/two-kinds-of-edits.md) 搬出(2026-10-15)。規則檔每個 session 自動載入,而搬移前 `.claude/rules/` 底下的檔合計 738,145 位元組,雲端 session 一開場就被它們撐爆 context;所以把只有查過往裁決、或新增一列時才需要讀的段落搬到這裡——`docs/rulings/` 不自動載入。 + +搬移是逐位元組的:下面各段與搬移前規則檔裡的那幾段一字不差,新增的只有本段說明與分隔用的標題。所以讀的時候: + +- 文中的「本檔」指規則檔;「上表」「下表」照搬移前的順序讀(表原本住在規則檔裡)。 +- 文中指向同一個目錄的相對連結是從 `.claude/rules/` 解析的,在這裡會點不開。 + +新增寫入面時在這裡加一列,並在 mcp-cli-parity 的裁決表同時裁決它的兩面。 + +### 裁決史(封閉列舉——列數以下表為準;新的寫入面加一列,不得依性質相似類推) + +| 寫入面 | 種類 | 一句話理由 | +|---|---|---| +| `resolve-people apply`/`reject`/`judge`(#272/#303/#386) | AI | `literal → key` 是身分判定;Jaccard 實測同一人 0.40、不同人 0.50,字串謂詞站在錯的一側。**change `resolution-verdict-states`(#636)**:同一配對的提名層判定(apply/reject)與逐篇判定(judge/refute)**兩筆並存**——判定層級(nominated/judged,封閉二值、由 rule 導出)參與記錄鍵;在此之前逐篇判定被當成重複丟掉,查證理由無處可寫 | +| `resolve-people split-author`(#443) | AI | 「這一格裝了兩個人」是判定;但落地是程式(切出的每一段必然是原文子字串)——持久化見 #450 | +| `resolve-people un-split`(#513) | 程式 | **split 的具名逆操作**——合回去的字串逐字取自 #450 寫下的記錄 value,不做任何判定。同輸入必得同輸出;整批擋零寫入;三種歧義(同 value 多筆記錄、各段出現多處、各段不連續)一律**拒絕不判定**(形狀取自 `enrich` 對 DOI 命中 ≥2 筆的既有處置)。段已升格為 `.key`/`.organization` 時拒絕並指向 `demote`——那一格才是判定的逆轉。**#659 起刪記錄之前要求 work 檔已 commit、乾淨**(比照 #573):被刪的拆分記錄只剩 git 裡的副本 | +| `resolve-people drop-author`(#457) | AI | **「這一格裝的不是作者」是判定**——要知道 `No authorship indicated` 是 PsycInfo 的無署名佔位字串,字串謂詞單獨做不出來。所以理由必填、記錄必留(`Entry.references` 的 `field: authors`、statement `移除:理由`,與作者位改寫同一次寫入,需要 store format ≥ 17)。**沒有具名逆操作**,而那不是「還沒做」:記錄留著被移除的字串逐字,但**刻意不留位置**(同拆分記錄不存索引的既有理由——索引在同一批的前一次操作之後會位移),所以還原無從定位。真出現要還原的需求時是一次顯式裁決。它把作者位的數量改成 N-1(可到 0),與 `split-author`/`un-split` 同族,各自單獨呼叫 | +| `resolve-people attribute-org`(#443) | AI | 團體作者的升格是判定,org key 由呼叫端顯式給 | +| `resolve-venues apply`/`reject`/`repoint`/`demote`(#304/#418) | AI | 同 people;`demote` 是判定的逆轉,原字串從 verdict 逐字取回、取不到寧可拒絕;≥2 個不同 literal 時同樣拒絕不判定(#554 R9,D23)。**翻轉判定時退役相反的那筆**(#554 R9,D20):`repoint`/`demote` 寫 rejected 時把同 holder 上同一配對的 confirmed 移除(反向 repoint 對 rejected 亦然)——verdict 沒有時間戳,「後者為準」只有寫入面知道,留著就是 #486 的矛盾對;歷史留 git(同 un-split 的取捨)。**前提是配對只由一條邊實例化**(#554 R10,D25):verdict 不帶 venue index,同一 work 兩條邊指同一 venue時退役會把另一條邊的證據一起刪——那種 work 具名拒絕零寫入;退役的每筆逐字回報(截 20 筆、總數揭露,R11)。**唯一性對寫入後的邊集合驗,生產端也擋**(#554 R11,D27/D28):R10 的 D25 只看原始 entry 的 from——`repoint` 改指到本 work 已有邊的 venue、`apply` 對兩條同刊名的 literal 邊各 apply 一次,都造得出 D25 宣告不得存在的形;現在 `apply`/`repoint` 對寫入後會出現「同一 work 兩條 key 邊指同一 venue」的整批拒絕零寫入,同一批同一 work 的兩個 move 不得帶同一個 literal(逐 move 的退役會互相覆蓋)。既有的這種 work 由 `Entry.validate()` 報 warning;刪掉多餘邊的面另案 #572——落地前出路只有手改 YAML。**R12(D33):`apply` 改逐筆略過並具名**(`skippedDuplicateVenueEdge`)——整批拒絕讓一筆毒候選殺掉同批無關的候選、既有的重複邊鎖死不相干的歸戶;store 狀態不符是「該筆略過並具名」(`judge` 的先例),語法錯才整批拒。D27 只看被動到的邊、同 literal 的 move 只在 venue 集合相交時拒;否決抑制改比 `matchingKey`(與其餘三處同粒度——代價寫在兩面描述:對一個拼法的 reject/demote 會壓住同 work 同 venue 的其他拼法,R14 verify regression 第 21 列;撤回這樣的否決至今沒有工具面——#559 上線的只有 `update-venue --unauthorize`,與此無關,#712 R1 verify 第 16、26、30 列;被壓住的候選自 #712 起列在列表的 `suppressed`)。**R15(D38):目的 venue 已對該 work 持有另一個正規化後不同的 confirmed literal(沒有對應的邊)時,`apply` 逐筆略過並具名(`skippedConflictingConfirmedLiteral`)、`repoint` 對預測後的 verdict 集合驗、整批拒絕**——R14 verify Codex 第 1 列:生產端的 fail-closed 只在合併有,apply/repoint 寫下第二個 confirmed 就把那條邊鎖進 D23。**R16(D43/D44):閘的相等比位元組、同一 literal 的另一個拼法也略過/拒**(R15 verify 第 3 列 HIGH、第 5 列:R15 以 `matchingKey` 比、放行同鍵異拼法——新拼法沒寫入,之後 demote 還回目的 venue 的舊拼法而不是這條邊的原字串,正是 `confirmedLiteral` 承諾不做的事;D23 比位元組,閘要同一把);apply 的閘移到 D28/D33 的重複邊檢查**之後**(第 2 列 HIGH:重複來源欄位這個最常見情境被分進錯的桶、出路叫人刪掉有邊的 verdict)。**R17(D50)**:閘的訊息兩桶同時非空時兩桶都說、出路分開列(R16 verify DA 第 9 列:只印第一桶、用單數「那筆」,照著刪掉一筆之後 validate 全綠、再跑才撞第二桶) | +| `resolve-people undecided`/`resolve-venues undecided`/`resolve-organizations undecided`(#619;change `resolution-verdict-states`;org 族 #643,change `org-undecided-leg`) | AI | **「查過、判不出來」也是判定工作的產出**:寫它的人查了名字以外的證據而結論是不足以判定——那個結論與它依據的內容(rests-on)正是 `identity-is-judged-not-matched` 要求留下的東西,只是方向是「不下判定」。所以它走 AI 欄的義務:說明必填、記錄必留、可附證據 digest。**它不是判定的逆轉也不動 entry**:未決不抑制提名、不構成矛盾,配對被判定之後保留為查證歷史(不退役)。已判定的配對寫未決=逐筆略過(未決不改變已判定配對的狀態)。落地是程式(整批拒絕、逐筆略過、完全相同=no-op) | +| `resolve-organizations`(#304) | AI | 同上 | +| `resolve-divergence`(#71 一族)/攣生合併(#456/#459/#553) | AI | 兩筆記錄是否同一實體是判定;合併含全庫改寫+刪檔,所以落地那一半是程式且要乾跑過目。**#553 起 venue 走同一個面**(不另開列——列是「寫入面」,而這是同一個面多收一個 shape)。venue 側有一格與 person 側**相反**且必須寫出來:被併者的 `authorized` 在 person 是**拒絕條件**(#81:「哪個名字對外」是判定),在 venue 只是**提醒**——實測 479/479 筆 venue 的 `authorized` 恰好等於 `[names[0]]`,唯一寫入者**當時**是 `VenueBootstrap` 的建檔慣例,所以它不承載判定(#563 起 bootstrap 不再寫它,既有的機械值不動)。拿一個不做判定的操作的副產品當拒絕條件,會讓合併對它要解決的 7 組重複全部無用。**#554 起有面可寫(`update-venue --authorize`),這一格 2026-09-12 重開過一次、裁決不變**——理由換了:那個面不留 judgement(#564),所以「人確認過的 names[0]」與機械值在 store 裡仍然長得一樣,合併端拿不到可以承重的東西。#564 裁「留」且落地後升成拒絕條件(`DivergenceResolve.authorizedDemotedByMerging` 的 doc 記著觸發條件)。**2026-10-01(#564 落地):那個觸發條件成立,這一格升成拒絕**——被併者的 authorized 名字若帶名字分類的判定記錄(`field: authorized` 的指定/確認/撤回,format 22),合併拒絕(`wouldDemoteJudgedAuthorized`,preview 與實跑共用前置;出路是在被併者 `--unauthorize`、或在倖存者 `--authorize`,都要理由);沒有記錄的機械值仍是提醒。判準是「有任何記錄」不是「最後一筆是指定或確認」(最後一筆是撤回卻仍在 authorized 只可能是手改,保守地拒絕)。其餘名字分類記錄在名字於合併後的倖存者上分類與被併者相同時逐位元組搬(`referencesCarried`),不同時以 `wouldLoseFields` 拒絕並說出名字與兩邊分類。**#554 R12 兩條新拒絕(D31/D32,person 與 venue 同)**:倖存者與被併者對同一配對持相反判定時拒——合併不裁決哪一筆對,那是兩個判定的衝突(#486 的矛盾對,處置沒有工具面);venue 合併把同一 work 的兩條 key 邊塌成一條而兩筆正規化後不同的 confirmed literal 都留下時拒(塌完之後 D23 讓那條邊永久不可 demote)。被併者的 verdict 遷移以 `verdictEqualityKey` 去重——R11 之前比位元組,「(field, value) 冪等」只在位元組層為真。**R13**:D31 累積比對(三方合併的 doomed↔doomed)且裝在 holder 遷移那一步(work/person 合併把第三方 holder 的 verdict 改寫成倖存者後不得矛盾——R12 verify DA 真 binary 全工具面重現);D32 守不變式本身(這次新增的 confirmed literal 讓倖存者對某 work ≥2 個正規化後不同的 confirmed 即拒,不看塌不塌邊;倖存者既有的違反不擋);去重丟掉的判定逐筆回報在 `verdictsCollapsed`。**R14(D34/D35)**:三處閘收成同一個 delta 謂詞——合併後與合併前比、只擋這次帶進來的(相反判定三種 shape;≥2 個 confirmed literal 只對 venue 記錄),合併前就有的違反不擋(validate 的 warning 負責——R13 的 holder 側是絕對謂詞、keeper 側漏單一被併者內部的矛盾與雙 literal);holder 遷移那條路兩半都算(R13 只補了相反判定,work 合併純工具面造得出 D23 從此拒的邊);訊息印出處與遷移前的原值;dry-run 與實跑對 `verdictsCollapsed` 同源。**R15(D37/D40)**:差集在**索引層**算——R14 以配對鍵比未改寫的 before 與改寫後的 after,被併鍵上的既有矛盾對與雙 literal 改寫後鍵變了、被判成新(R14 verify logic 第 2 列 HIGH,四席同指);現在 after 的違反是既有的當且僅當構成它的索引裡有一組在合併前就構成同一類違反,含倖存者自己持有的 `person:<被併>` 矛盾對;雙 literal 的拒絕把帶進來的與倖存者既有的分開列;收攏丟列印遷移前的原值並逐段截(rename 同一個生產者)。**R16(D45):「既有」看倖存配對**(R15 verify 第 12/13/20 列:R15 問「有沒有某一個合併前分區已持有全部」——holder 對 doom 持 {A, B}、對 keep 持 {A},doom 那一區 ⊇ 全部就放行,而 (holder, keep) 合併前只有一個 literal、可以 demote,合併後被 D23 鎖住;末句「含住在被併鍵上的不擋」與判準互相矛盾):不擋的只有倖存配對合併前就持有整組、或倖存配對合併前一筆都沒有而整組原樣從單一被併鍵搬來;整組住在被併記錄裡的(R13 的裁決不變)、或讓倖存配對的既有歧義變大的,都擋——矛盾對同一條規則。**R17(D47):收攏的勝者政策**(R16 verify DA 第 1 列 HIGH,真 binary 重現:#468 的「弱血統優先」排在「未改寫者勝」之前,被併記錄弱血統的 `VEE JOURNAL` 贏過倖存者使用者確認的 `Vee Journal`、連位元組一起取代,之後 demote 把邊寫成不是它原本記的字)——碰撞的 literal **位元組不同**時倖存配對自己的(未改寫)那筆勝,同拼法時 #468 三層照舊(留弱只多一句警告、不動任何字串);被丟的那筆連同留下的拼法印在 `verdictsCollapsed`(三條收攏路徑——holder 遷移、keeper 合併、rename——同一個描述函式)。這是對 #468 使用者裁決的邊界收窄,使用者可翻。**R18(D51/D53):倖存配對不是倖存邊**(R17 verify Codex 第 1 列 HIGH:keeper 對某 work 持有的 confirmed 可能**沒有邊**——手改、舊 binary、D38 具名的那種輸入——而 work 的唯一邊指向被併 venue,被併記錄的那筆才是那條邊記錄的字;R17 的 keeper 路徑一律 keeper 勝、三方合併由陣列順序決定、rename 沒有任何政策):三條收攏路徑同一個勝者函式(`collapseWinner`)——拼法位元組不同時先留**活著的邊**那一筆(合併前 work 對該 venue/person 真的有邊),再留與倖存配對自己位元組相同的,再 #468 三層,最後首見;rename 只收攏它動到的鍵,被改寫的那筆對上早已指向新鍵的死 verdict 時留活的,兩筆都沒動到的碰撞留著(validate 照報);**R19(D55)**:以鍵整組算——R18 的單一槽位記帳讓三方碰撞由 YAML 順序決定(R18 verify Codex 第 1 列 HIGH),現在被改寫的全留、早已指向新鍵的全丟。**代價寫出來**(R17 verify regression 第 9 列):D47/D51 留強丟弱時,弱血統那筆的 `rule` 尾註從 store 消失——它的消費端不只合併那一刻的收攏列,還有之後每一次提名(`PersonResolver`/`VenueResolver` 的 confirmed-elsewhere 從 `confirmedByLiteral` 讀 rule 揭露弱出身);#468 保的那個警告在這一格自此只在 `verdictsCollapsed` 出現一次。使用者可翻的是這個取捨。**R20(D58;R19 verify DA 第 1 列 HIGH)**:D55 以含 field 的鍵分組,早已指向新鍵的 rejected(異 field)逃過「全丟」、rename 後復活成 #486 矛盾對——D58 改成不論 field 與拼法一律丟;被改寫的同拼法重複走 `collapseWinner`(#468 那一層在 rename 之前沒跑,R19 留首見);拼法不同的都留——venue×work 以外的六格沒有掃描面,rename 不替它們判定(§3.5 誠實邊界)。**R21(D60;R20 verify 五席)**:D58 的「一律丟」寫在第一段後的早退之後,holder 只持有死 verdict 時整筆記錄原樣通過、rename 後復活(四席同指,真 binary);而 DA 指出即使修好,那也是程式編輯**單方面銷毀判定編輯的產物**——無乾跑、無逆操作、無 git 閘,被丟的可能是人對另一筆仍存在的記錄親自下的判定。現在與 merge 的 D31/D34 同向:目的鍵上已有 verdict(同 holderKind、不論 field 與拼法)即 rename 具名拒絕、零寫入(`assertNoVerdictAlreadyAt`,逐筆列 holder/欄位/value/rests-on,出路是改那一行的 value 或刪掉它)。**R22(D61/D62;R21 verify)**:母體含 quarantined 檔(行級比對,DA 真 binary:R21 讓一筆 validate 本來會報的死 verdict 在 rename 後變成沒有任何面會報的活斷言);折疊只折完全相同的重複(R21 verify DA 第 14 列:同拼法而 judgement/rests-on 不同的曾被折成一筆,digest 永久消失)——「rename 不做任何判定的刪除」自此才是真話。**R23(D63/D64;R22 verify,6 席齊)**:行級比對被 YAML 折行擊穿(needle 含空白,四席同指、live store 2 筆)→ 位元組比對 `:`,與本列 merge 隔離檔閘同一種紀律與同一種誠實措辭;D62 留下的同鍵異 judgement 有了掃描面(`duplicateVerdictRecords`),merge 收攏它時在 `verdictsCollapsed` 回報、`describeDedupedVerdict` 自此也印 rests-on(Codex 第 6 列:R20 只補了 `describeCollapsedVerdict`)。**R25(D69;R24 verify 第 9/26/29 列、DA 第 39 列)**:`fieldsLostByMerging` 的「倖存者身上已經有這筆 reference」改比 `byteExactKey`——canonical `==` 曾把被併記錄的 NFD 拼法判成「已有」、那組位元組隨檔案消失而報告說沒遺失,是 D65 在合併面的同一個缺陷;`verdictsRetired` 的描述自此也印 rests-on(Codex 第 2 列:同一族的第三個生產者)。**R26(D73;R25 verify DA 第 17 列 MEDIUM、requirements 第 21 列)**:venue 的 `fieldsLostByMerging` 自此也比 references——R25 之前那一份**完全不比**、`mergeVerdicts` 只搬 verdict 欄位,被併 venue 的 `paginated` judgement 與 rests-on digest 在純量相同時隨檔案靜默消失,正是本列 R21 D60 為 rename 關掉的「程式編輯銷毀判定編輯的產物」在合併面的同型;三份遺失偵測的訊息自此說出「倖存者有正規化後相等、位元組不同的一筆」(第 14 列)。**#565(2026-09-29):venue 合併的名字併入改成程式編輯該有的樣子——只搬、不替人判定**:被併者的 `names` 段整段搬(時間欄位、source、note 原樣——#553 以 `TemporalValue(value:)` 重建、丟掉沿革段的時間);被併者原本在 `variant` 的才標 variant,其餘(含它的 authorized)未標(#554 D1 同一個論證:「兩筆是同一本刊」不蘊含「它們都是倖存者對外形的異寫」);倖存者已有的名字不改分類、被併者對它說的 variant 只提醒;同名段與倖存者那一段時間不同而不能並存(不是兩段都帶不相交時間的沿革)時具名拒絕、零寫入——哪一段的時間對是判定,合併不做。**#565/#587 R1 verify(2026-09-29)**:分類不取決於被併者的處理順序——倖存者原本沒有的名字,任一被併者列在 variant 就標(未標「不作任何宣稱」,不構成反對);「倖存者已有」只對原本的倖存者判斷,先併入的被併者帶進來的不算;「完全相同的段」比 source/note 的 UTF-8 位元組(Swift `String ==` 會把只差 NFC/NFD 的 note 當成相同而靜默丟掉一份);被併者 `field: issn`/`names` 的 reference 逐位元組搬到倖存者(合併前置與實跑共用同一份計算),同一個 ISSN 兩邊角色不同具名拒絕——這兩格原本讓每個經過 #587 查證流程的被併者都合併不了、或角色靜默消失。 **#709 R3 重新確認(2026-10-02),種類不變**:落地那一半(程式)的前置多一道——會被改指的 entry 自己有 legacy 拷貝(同一種、同一個 id)時,改寫會被 #631 拒絕;這一格先前只在實跑的預檢裡,`--dry-run` 預測成功而實跑被拒,現在住在 preview 與實跑共用的前提段(`assertRewriteTargetsWritable`),兩邊擲同一個錯、零寫入。它是落地前置的完整化,不是新的判定,也不改變上面任何一條拒絕。**#564 R2 verify 重新確認(2026-10-03),種類不變**:搬名字分類記錄(落地那一半)按被併者的順序接、每一筆只與那個名字那個分割此刻的最後一筆比位元組(`appendCollecting`,與寫入面同一個規則)——先前以整份歷史去重,被併者「指定 R → 撤回 S → 指定 R」只搬撤回,倖存者的名字仍是對外形、最後一筆卻是撤回(b29 V1 第 0/2/6 列);合併後最後一筆與分類矛盾、而那是這次帶進來的,preview 與實跑都拒絕(`wouldContradictClassificationTail`);person 記錄的名字要在合併後的 names 裡(只差空白的拼法不併入,它的記錄會成孤兒——先前 dry-run 說搬、實跑在寫入閘失敗,第 3/12 列)。都是落地的完整化,判定(分類一致才搬)不變。**#564 b34 重新確認(2026-10-05,b33 X1),種類不變**:搬名字分類記錄改成被併者依 key 排序、倖存者已有而此刻尾端與分類一致的不重搬(`carryCollecting`——兩筆攣生歷史相同時不把整段再接一遍,第 4/10 列)、接續線性(第 9 列);尾端矛盾的保險以**原始倖存者**為基準、全部接完之後算一次差集(第 1/21 列:先前逐被併者累加,結論隨順序翻轉)。落地的決定論變強了(結果不再取決於被併者的給定順序),判定不變 **#564 b37 重新確認(b36 Y1 第 12 列),種類不變**:被併者自己的名字分類記錄就與分類矛盾時一律拒絕(`doomedTailConflicts`)——先前看全部接完之後的最後一筆,而被併者依 key 排序,同樣兩份歷史只換 key 拼法就從放行翻成拒絕:store 沒有時間資訊說哪一筆較新,依字典序決勝是讓政策預設代替判定(`disambiguate-before-irreversible-writes`)。拒絕多一類(被併者自己不一致),結論從此與 key 拼法無關;這是一處在零實例的形狀上變嚴(live store 名字分類記錄 0 筆),使用者可翻 | +| `update-venue --paginated`(#406) | AI | 「本刊是否使用頁碼」是判定,必附 judgement 與 rests-on | +| `update-venue --remove-issn`/`akashic_update_venue.remove_issn`(#588) | AI | 「這個號不屬於這本刊」是判定——號本身合法(過得了檢查碼),錯的是它掛在哪裡,字串謂詞做不出來。理由必填(`=理由`)。**理由不進 store、只進報告**(使用者 2026-09-27 裁決「只進報告與 git 歷史」;git 保存的是移除前的檔——被移除的號與它的 provenance——理由要留在 git 裡,得由操作者寫進後續的 commit message,工具不代寫。#588 R1 verify 指出先前寫成「理由進 git 歷史」是過度宣稱),所以「留 verdict」這一格在 store 內是**有記錄地不兌現**:兌現它的一半是報告(全文、不截斷)、一半是 git 的移除前副本——移除前要求那筆 venue 檔已 commit、乾淨(`assertRecordsRecoverable`,#573 的閘一般化),否則整批拒絕零寫入。逆操作是 `--add-issn` 加回來——只加回號;同一次移除一併刪掉的 `field: issn` provenance(R1 起,報告逐號列筆數)要從 git 的移除前副本取回,沒有工具面。落地是程式(不合法的號、理由空白或過長、同號重複、venue 沒有這個號、同一個號同時加與移除,都整批拒絕零寫入) **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘——會刪判定記錄的移除腿一律過閘;MCP 不閘 | +| `update-venue --references`/`akashic_update_venue.references`(#587) | AI | 「這個號(這個名字)出自這份來源」是查證的產出——寫它的人查了名字以外的證據,而識別碼終結指涉:沒有來源的號錯了沒有跡象(`identity-is-judged-not-matched`)。每一筆就是那份記錄本身(retrieval 帶 url/日期/HTTP 狀態/digest,judgement 帶理由與 rests-on),所以「留 verdict」由記錄自己兌現。**具名逆操作是 `--remove-reference`**(#673,見下一列;在它之前 venue 的 reference 沒有移除面,唯一會刪它的是 `--remove-issn` 對被移除號的連帶刪除,寫錯的只能手改 YAML)。落地是程式:形狀走 `ProvenanceReference` 平面 init、附著走載入的同一個驗證、append-only 以位元組去重、任一筆不合整批拒絕零寫入。verdict 欄位與 `paginated` 判定不收——它們各有自己的面,通用面寫進去會讓判定與記錄的值分岔。**#587 R1 verify 起通用面只收 `issn` 與 `names`**:`authorized` 與 `note` 也拒收——當時沒有移除面,每多收一格就多一個死角(`authorized` 的 reference 會鎖住 `authorize` 換對外形,只能手改 YAML 解開;#673 起有移除面,要不要收回通用寫入面待使用者裁決);合併(`resolve-divergence`)自此把被併者這兩格的 reference 逐位元組搬到倖存者,所以來源記錄不會讓合併卡住 | +| `update-person` 的 references/`akashic_update_person.references`(#68;#674 起與 venue 同一個解析函式) | AI | 理由同上一列:每一筆就是那份查證記錄本身(retrieval 帶 url/日期/HTTP 狀態,judgement 帶理由與 rests-on),「留 verdict」由記錄自己兌現。append-only、以位元組去重;**沒有具名逆操作**,person 的 reference 沒有移除面、寫錯的只能手改 YAML(有記錄的缺口)。verdict 欄位對拒收,只經 resolve 流程。落地是程式(同一個 `parseReferenceObjects`,任一筆不合整個呼叫拒絕零寫入)。**這一列只裁 references**:同一個命令的其他欄位(`orcid`、`profile`)是另一件事,沒有在這裡歸類;`names` 動到 authorized 的那一格見下一列(#564 修正輪)。名字分類的判定記錄(`field: authorized` 的「指定/確認/撤回:理由」)不收——只經名字分類面寫 | +| `update-person` 的 `fields.names` 動到 authorized/`akashic_update_person` 的 `names`+`judgement`(#564 修正輪,使用者 2026-10-02 裁決第 1 點) | AI | 「這個名字是他的對外形」是判定——與 `authorize-names` 同一句話的單筆形(R1 verify b26 F2 第 0/2/5/20 列:先前這條全量替換能不附理由、不留記錄地改 authorized,而 mcp-cli-parity 寫「person 的單筆名字分類面不存在」)。替換讓名字**進**或**出** authorized 時理由必填(`--judgement`/MCP `judgement`,同 venue 的理由檢查;證據可空),比照 venue 寫 `field: authorized` 的記錄:進的「指定」、~~仍在的「確認」(附了理由時)~~ → 仍在的不寫(b34)、出的「撤回」;只動 variant 的替換不必附理由(person 的 variant 是「其他名字」,不是判定的分割,所以沒有 `field: variant` 的記錄)。與那個名字最後一筆記錄位元組相同的不重寫。落地是程式:替換拿掉一個有記錄的名字具名拒絕並指出口(先移到 variant 寫撤回、再 `--remove-name`);寫入閘對 format < 22 具名拒絕;有 `--dry-run`(`judgementsToRecord`)。**#564 R2 verify 重新確認(2026-10-03),種類不變**:~~讓**沒有記錄**的對外形整個離開 names 改為放行(附理由;b29 V1 第 4 列——整份替換改正對外形拼寫的既有用法被修正輪改成三步,而那個名字沒有判定可推翻、沒有記錄會成孤兒),它沒有地方錨定記錄、列在報告 `authorizedRemovedWithoutRecord`——理由只寫在進入的名字上~~ → b34 退場(見本格末);沒附理由的拒絕在 format < 22 時說出寫入閘與升級路徑;一次上限只數 authorized(variant 不寫記錄)。~~「只動 variant 的不必附理由」是實作對裁決第 1 點(「動到 authorized/variant」)的讀法,待使用者確認;附理由時仍在 authorized 的每個名字都寫確認(呼叫端沒辦法只確認幾個),也待使用者看過。~~ → 使用者 2026-10-05 裁決:前者確認,後者改為不寫(見本格末 b34)。**#564 b34 重新確認(使用者 2026-10-05 裁決第 1–3 點),種類不變**:上面 R2 的放行退場——對外形在替換裡整個離開 names 一律拒絕(移出要寫「撤回:理由」、記錄錨定 names,所以留在 variant;R2 那條路的理由既不入庫也不在回應,b33 X1);只替進出的名字寫記錄,仍在 authorized 的不寫確認,附理由而沒有名字進出的拒絕(理由不靜默丟掉);只動 variant 不必理由(使用者確認了讀法)。「留 verdict」因此對每一次 authorized 的進出都兌現 **#564 b37 重新確認(b36 Y1 第 2/14 列),種類不變**:`fields` 路徑(含乾跑)對兩筆不同的 person(id 不同)共用同一個 key 整批拒絕——判定記錄寫進先載入的那一筆是猜,落地那一半的前置補齊(檔案處境那一類由寫入當下的 #631 前置擋,單檔寫入沒有撕裂)。**給了理由而沒有名字進出 authorized 就拒絕**這一格比 2026-10-05 的裁決嚴(裁決說只替有變動的寫、只動 variant 不必理由,沒說附了理由要拒絕),b36 Y1 第 4 列指出,交給使用者裁 | +| `update-person --remove-name`/`update-organization --remove-name`(MCP `remove_names`)(#564 修正輪,使用者 2026-10-02 裁決第 2 點) | AI | 「這個名字不該在(打錯字、錯拼法)」是判定。**只收最後一筆名字分類記錄是「撤回」的名字**——人已經說過它不是對外形,刪它不推翻仍成立的判定;名字(organization 是它的每一段)連同它的名字分類記錄一起刪。套用移除面一族的裁決:理由必填、只進報告、不寫進 store,「留 verdict」在 store 內**有記錄地不兌現**——兌現的是報告與 git 的刪除前副本(刪之前要求那個檔已 commit、乾淨)。沒有記錄、最後一筆不是撤回(出口:先撤回,再刪)、還是對外名稱、被 `field: names` 的 reference 指著(兩種實體的 reference 都沒有移除面)、organization 刪完沒有名字,都整批拒絕零寫入。organization 沒有一般的名字移除面(待裁,#557)——這一條只是打錯字的出路。venue 的同一件事走既有的 `--edit-name-segment` 的 remove(下方那一列)。單獨呼叫;一次至多 200 個。**#564 R2 verify 重新確認(2026-10-03),種類不變**:拒絕依名字的處境給出口(b29 V1 第 6/7/23 列:沒有記錄的——person 指向 `fields.names` 整份替換,organization 說出「先讓它有一筆撤回」的兩步與代價;不在分割裡、最後一筆卻是指定的——先指定回去再撤回);person 刪完沒有名字也拒(第 18 列,與 organization、venue 同);organization 的報告逐段回時間欄位、source、note(第 20 列);刪記錄改成線性(第 16 列)。**#564 b34 重新確認(使用者 2026-10-05 裁決第 4、5 點),種類不變**:CLI 的 `--remove-name` 過 #298 的目標 store 確認閘(會刪判定記錄的面過閘;person 的 `--dry-run` 不擋,organization 沒有乾跑),MCP 不閘;organization 的 `remove_names` 保留。出口的文字補上代價與寫入閘(b33 X1 第 6/17/20/26/28/29/30/31 列) | +| `update-venue --remove-reference`/`akashic_update_venue.remove_reference`(#673) | AI | 「這筆來源記錄不成立」是判定——寫它的人當初讀過來源(#587 的 `--references`),收回它要重新讀過才知道錯在哪,字串謂詞做不出來(例:兩本姊妹刊的 ISSN Portal 頁面網址只差一碼,`url` 貼到另一本)。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的一半是報告(`referencesRemoved`:**理由全文不截**;reference 本身在報告裡是截過的形——`value` 200、`url`/`statement` 300 字元、`rests_on` 只列前 5 個——**足以認出是哪一筆,不足以無損重建**;MCP 面另只有前 20 筆帶 reference 的內容、其後只回 `field`/`value`/`reason`)、一半是 git 的移除前副本——實跑要求那筆 venue 檔已 commit、乾淨(`assertRecordsRecoverable`),**無損還原以 git 為準**。逆操作是 `--references` 把同一筆加回去(append-only、位元組相同的略過)——要有那筆的**逐字**內容,所以以 git 的移除前副本為準(報告的截過的形對超過上限的 value/url/statement 與多於 5 個的 `rests_on` 不夠),且限 `issn`/`names`(通用寫入面不收 `authorized`/`note`,那兩格只能從 git 還原);移除本身不動 reference 指的號或名字。落地是程式:定位=`field`+`value` 的位元組相等(可再以與 `--references` 同名的鍵縮小),定位不到、定位到多筆、兩個定位指到同一筆、verdict 與 `paginated` 欄位(各有自己的面,`paginated` 是判定且要留史)、理由空白或過長、一次超過 200 筆都整批拒絕零寫入;**單獨呼叫**,不與 `update-venue` 的其他參數組合 **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘——會刪判定記錄的移除腿一律過閘;MCP 不閘 | +| `update-venue --edit-name-segment`/`akashic_update_venue.edit_name_segment`(#675) | AI | 「這一段的時間是錯的」「這一段不該存在」是判定——`names` 的一段(`TemporalValue`)帶時間欄位、`source`、`note`,哪一格對要讀來源才知道,字串謂詞做不出來(例:兩本刊合併時同名的兩段只差 `source`,哪一段的來源才對)。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的一半是報告(`nameSegments[].reason`:**理由全文不截**;`before`/`after` 各欄位截在 300 字元、時間欄位 40,超過的形只用來認得,MCP 面只有前 20 項帶它們),另一半是 git 的改寫前副本(改寫前要求該 venue 檔已 commit、乾淨;每一項都沒有變動時不寫檔也不過閘)。逆操作是再呼叫一次把值改回去——`before` 在報告裡,所以改寫是**有具名逆操作的**;刪掉的一段用 `--add-name` 補名字、再以本面的 `set` 補回時間、`source`、`note`(`before` 在報告裡),或從 git 取回。落地是程式:形狀走封閉的鍵集合、定位是 canonical 名字加給了的鍵的位元組相等(`null` 是要求缺席)、改完的時間欄位過 `Venue.nameSegmentRangeIssue`、這次造出的 `validate()` error 在寫之前歸因到這次呼叫、任何一項不成立整批拒絕零寫入。**不與其他腿組合**(單獨呼叫,同 `--remove-reference`):其餘腿增減同一份 `names`,混在一次呼叫裡「定位的是哪一段」交錯。**程式不替人動同一筆記錄的其他判定**:移除一個名字的最後一段時,它若還在 `authorized`/`variant`/`field: names` 的 reference 裡就具名拒絕並指路(`--authorize`/`--remove-reference`)——`authorize` 遇到被 reference 指著的舊指定同一條紀律(D60 同向)。它若有名字分類的判定記錄(#564):最後一筆是「撤回」的,記錄隨名字一起刪(使用者 2026-10-02 裁決 #564 第 2 點;報告 `judgementRecordsRemoved`,內容在 git 的上一版);不是撤回的具名拒絕(先撤回,再刪)。合併(`resolve-divergence`)對同名而不能並存的段拒絕、出路指向這個面——合併是程式落地、這一面是人/AI 的判定,兩者的分工與 `identity-is-judged-not-matched` 的「提名(程式)→ 判定(AI)→ 落地(程式)」一致 **#564 b34(使用者 2026-10-05 裁決第 4 點),種類不變**:CLI 過目標 store 確認閘(只算帶 `remove` 的呼叫)——會刪判定記錄的移除腿一律過閘;MCP 不閘 | +| `resolve-venues --drop-venue`/`akashic_resolve_venues.drop_venue`(#572) | AI | 「這條邊不該在」是判定——兩條同刊名的邊哪條多餘、publisher 推出來的邊是不是載體,字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決,同上一列 `--remove-issn`),所以「留 verdict」在 store 內**有記錄地不兌現**:兌現它的是報告與 git 的移除前副本(移除前要求 work 檔已 commit、乾淨)。**key 邊的前提把判定的範圍收窄**:只刪「同一 venue 還有另一條 key 邊」的那種,verdict 因此不受影響;唯一的 key 邊要先走 `demote`(那一格處理 verdict)。落地是程式(index 位移、整批拒絕零寫入) | +| `update-venue --authorize`/`akashic_update_venue.authorize`(#554) | AI | 「這個名字是本刊的對外形」是判定——與 `authorize-names`(#81)同一句話換到 venue。語意是同 `WritingSystem` 原子替換(不是 append:「每書寫系統至多一個」對 470 筆機械值必擋 append);被換下來的舊指定**移出 authorized、留在 names、不標 variant**——程式不替呼叫端多說「它是異寫」(R1 verify D1)。~~**不留 judgement**(與同列的 `authorize-names`、#471 `add_variant` 一致)——這是**有記錄的裁決不是遺漏**:三個名字分類面要不要留、留什麼形狀一次裁(#564);**#564 已於 2026-10-01 裁決要留**(名字分類面全部,含確認既有值與 #559 的撤回、#557 的 organization),另案落地(需要 store format bump);在落地之前這一格對「留 verdict」的義務是**未兌現的**,而它的代價住在上面 #553 那列(合併端分不出判定與機械值)。~~ → **#564 落地(2026-10-01):留。** 理由必填(`--judgement`/MCP `judgement`,至多 4,096 位元組)、證據 digest 可空(`--rests-on`,至多 20 個),每次指定寫一筆 `field: authorized` 的 `指定:理由`、對已是對外形的名字寫一筆 `確認:理由`(先前是無聲的 no-op);同書寫系統被換下的舊指定、被抬出 variant 的名字各寫一筆 `撤回:`(理由由程式在前面說出原因)。一次呼叫的理由套用到該次每一筆記錄;位元組完全相同的不重寫(報告 `judgementsRecorded`);缺理由整批拒絕零寫入,需要 store format ≥ 22。記錄錨定在 names 不在分割,所以撤回之後判定史仍在、`authorize`/`unauthorize` 的 pinned 檢查只看名字分類記錄**以外**的 `field: authorized` reference。**這一格對「留 verdict」的義務自此兌現**,合併端因此分得出人判定過的與機械值(上面 `resolve-divergence` 列)。與 `paginated` 不同一次呼叫(兩個判定各要自己的理由)。落地是程式(入口的輸入驗證整批拒絕零寫入;名字內容的不變式住在 `Venue.validate()`——五條的規範文字只在 `docs/store-format.md` §5.7(這裡不複述:R6 verify 第 10 列抓到本列是七份副本裡第一個分岔的)——所有寫入者存 canonical,D8;分割互斥同樣由 `Venue.validate()` 擋、不重造) | +| `update-venue --add-variant`/`akashic_update_venue.add_variant`(#471) | AI | 「這個名字是那個名字的異寫」是判定,不是「不在對外清單裡」的推論(#471 的論證:補集規則是一個不做判定的操作充當判定寫入者,已由 #567 退場刪除)。先前這一格沒有列——#471 寫入面新增時漏記;本列補上。**#564 落地(2026-10-01):留記錄**:每個被標的名字寫一筆 `field: variant` 的 `指定:理由`(已是異寫的寫 `確認:理由`),理由必填(`--judgement`)、證據可空,需要 store format ≥ 22。不在 names 的一併 append 進 names(兩個分割都是對 names 的標記);被 `--authorize` 抬出 variant 時連帶寫一筆 `field: variant` 的 `撤回:`。**沒有撤回 variant 的獨立面**(有記錄的缺口:venue 的 variant 只有被抬出時連帶撤回;要不要補面是另一個裁決)。記錄只由名字分類面寫:`--remove-reference` 明文拒收 `variant`。落地是程式(名字的 vetting 整批拒絕零寫入、互斥由 `Venue.validate()` 擋) | +| `update-venue --unauthorize`/`akashic_update_venue.unauthorize`(#559) | AI | 「這個名字不是(或不再是)本刊的對外形」是判定——撤回回到 `venue-entity` spec 的未標(「不作任何宣稱」),與 `clear_paginated`(#500)、`resolve-venues --demote`(#418)同一格:替換只能換成另一個名字,只有一個名字的 venue 在此之前 authorized 一旦在就永遠在(使用者 2026-10-01 裁決)。逐名、必須是現有成員(相等看 `NameIdentity.canonical`,同 `--authorize`),移出後留在 names、不標 variant——程式不替呼叫端多說「它是異寫」(D1 同一個立場);與 `--authorize` 共用 `AuthorizedDesignation` 一份(#557 起 organization 的 `--authorize` 也用它;organization 沒有撤回面)。**`--authorize` 不是精確逆操作**(R1 verify):名字與分類(未標)回得來,但它對不在 authorized 裡的名字接在尾端,而預設顯示名取 `authorized.first`——撤回再指定,多書寫系統 venue 的預設顯示名可能換書寫系統,遺失的只有 authorized 內的順序,所以報告逐名帶原 `index`、預設顯示名因此改變時多 `displayNameChanged`。**不設 git 閘**(使用者沒有裁決要閘;名字與分類都留在 names)。~~判定記錄:#564 已於 2026-10-01 裁決名字分類面全部要留(含撤回),另案落地(需要 store format bump);在落地之前不寫記錄,報告的 `authorizedWithdrawn`(含 `index`)是這個判定唯一的痕跡。~~ → **#564 落地(2026-10-01)**:每個被撤回的名字寫一筆 `field: authorized` 的 `撤回:理由`,理由必填(`--judgement`),先前的記錄留著(判定史);需要 store format ≥ 22;報告的 `authorizedWithdrawn`(`name`/`index`)與記錄說的是同一批名字(store 拼法)。落地是程式(不是現有成員、同一個名字又在 `--authorize`、被 `field: authorized` 的一般 reference 指著(名字分類的判定記錄不算——它們錨定 names),都整批拒絕零寫入;整項空白回報在 `unauthorizeDropped`;分割互斥與子集仍由 `Venue.validate()` 擋) | +| `update-organization --authorize`/`akashic_update_organization`(#557) | AI | 「這個名字是本機構的對外名稱」是判定——與 `update-venue --authorize`(#554)同一句話換到 organization,**同一份邏輯**(`AuthorizedDesignation`;`AuthorizedNames` 的 doc:venue 與 organization 的「哪個名字對外」是同一個問題,不該有兩套答案)。在此之前 `Organization.authorized` 零寫入面(使用者 2026-10-01 裁決形狀 2:新增部分更新面,先提供 `--authorize`)。同書寫系統原子替換,被換下的移出 authorized、留在 names(organization 沒有 variant)。**沒有撤回面**:首輪實作順手加了 `--unauthorize`,超出裁決、且在 organization 上不是「回到未判定」(names 只增不減——當時;#564 修正輪起 `--remove-name` 只刪已撤回的名字,仍沒有一般的名字移除面——撤回後剛加進 names 的名字成為 fallback 顯示名),R1 verify 之後拿掉,待使用者裁決。~~判定記錄:#564 已於 2026-10-01 裁決名字分類面全部要留(列舉的五個面裡 organization 只有 `--authorize`),另案落地(需要 store format bump);在落地之前不寫記錄。~~ → **#564 落地(2026-10-01)**:organization 新增 `judgement`/`rests_on`(CLI `--judgement`/`--rests-on`),`authorize` 的名字必附理由、證據可空;每次指定、確認各寫一筆 `field: authorized` 的記錄(被換下的舊指定也寫一筆撤回),需要 store format ≥ 22;organization 沒有 variant,不會有 `field: variant`。organization 沒有撤回腿(`--unauthorize` 於 #557 R1 verify 之後拿掉、待使用者裁決),所以 #564 的五個面裡 organization 只有 `--authorize`。落地是程式(入口的輸入驗證整批拒絕零寫入;子集與每書寫系統至多一個由 `Organization.validate()` 在寫入閘擋,不重造;key 重複整批拒絕,#669/#670;都已是對外名稱而同一句理由已記過時不寫檔,第一次說「確認」會寫一筆記錄) | +| `update-entry --remove-field`/`akashic_update_entry.remove_fields`(#544) | AI | 「這個值不是來源給這個欄位的資料」是判定——#544 的 21 筆 `abstract` 是 Crossref 的錯誤頁、兩筆的 `journaltitle`/`publisher` 是補助計畫名稱與總統令字號,要讀內容才知道,字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決),store 內「留 verdict」有記錄地不兌現——兌現的是報告與 git 的移除前副本(實跑要求 work 檔已 commit、乾淨)。**這個不兌現是 Claude 代裁、待使用者確認**:2026-09-27 的裁決是為 #588/#572/#586 下的,延伸到 `fields` 的值沒有 #544 自己的裁決紀錄;issue 要的 #450 形記錄(work 側 `references` 的退役記錄)沒有做。逆操作:**一般字串鍵**(`abstract`、`journaltitle`、`publisher` 之類)的值可由 `enrich` 補回(鍵已缺席,add-only 收得下);其餘——`doi`/`pmid`/`isbn` 的殘留(`enrich` 把它們導向結構化欄位,不是殘留的 `fields[…]`)、`enrich` 拒收的鍵(`issn`、`type`、`title`、`venues`)——以 git 還原那個 work 檔。同一次移除一併刪掉的 `fields.<鍵>` reference 要從 git 的移除前副本取回,沒有工具面。**反面:同一個值會被 `import-wos` 回填、`enrich`、Zotero pull 補回來**——store 不記得它被判定過不屬於這裡(報告的 `reintroductionNote`)。落地是程式(鍵不存在、同鍵兩次、理由空白或過長都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫 | +| `update-entry --add-source`/`akashic_update_entry.add_sources`(#614) | 程式 | 把已存的內容寫進 `akashic.sources`——**落地不是判定**:「這份 PDF 是這篇」的判定在上游(`akashic-fetch-fulltext` 的驗證步驟:頁數、首頁標題、DOI),證據隨 `store-source` 的 note 記在 `sources/index.jsonl`;本面收的是已判定的 (citekey, digest),同輸入必得同輸出。add-only、冪等(已在的不重寫、集合語意),可預期失敗(digest 不合法、本機沒有存檔、孤兒 blob、index 壞到判不出來、同一次重複)整批擋零寫入。**具名逆操作是 `--remove-source`**(#677,見下一列):在它之前沒有移除腿,連錯了只能以 git 還原那個 work 檔。預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--remove-zotero-source`/`--remove-source`)組合(兩種編輯不混在一個呼叫;四條腿兩兩互斥) | +| `copy-zotero-attachments`(#606) | 程式 | 把 Zotero 附件的位元組複製進 `sources/` 並追加連到 `akashic.sources`——**搬運與落地,不是判定**:「這個附件屬於這一筆」的關係是 Zotero 的 child item 早就記下的(`attachments` 的 `zotero:` 記錄,由 pull 產出),本面不重新判斷它、也不動它。同輸入必得同輸出(digest 由位元組決定);冪等(digest 已連過、位元組在本機且 index 有條目的不重複——連結在而本機缺位元組的只補存、缺取得記錄的只補記,都不改連結;已存的 blob 不重寫、index 不重複 append);可預期失敗(`sources/` 沒被版控排除、index 有壞行、寫入閘、被改寫的 work 檔未 commit)整批擋零寫入,逐檔的問題(路徑不合、檔案不在、不是普通檔、0 byte、計畫之後內容變了)具名略過而其餘照跑;代價量測(乾跑列出每個檔的大小與 digest)。#703 起「位元組在本機」要**內容相符**(逐塊比對既有 blob),不符具名列出、不覆寫;超過 256 MiB 的檔具名略過——兩者都是決定論式的比較,種類不變(R1 起位址上是目錄、symlink 或讀不到而判不出來的,新連結也具名略過——判不出來不是判定,是不做)。**具名逆操作是判定型的另一格**:`update-entry --remove-source =理由`(#677,理由必填、只進報告)收回連結,blob 與取得記錄留在 `sources/`。預設乾跑、`--apply` 才寫;不與判定型的腿混在一次呼叫裡(它只做這一件事)。 | +| `update-entry --remove-zotero-source`/`akashic_update_entry.remove_zotero_sources`(#680) | AI | 「這個 Zotero 來源不屬於這一筆」是判定——來源是主來源還是附加來源、是記錯了還是真的屬於這篇,要讀那個條目與這筆記錄的內容才知道(#610 的多筆宣稱、#679 的沒記 `library_id` 的附加來源,處置都是這個判定),字串謂詞做不出來。理由必填、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內有記錄地不兌現——兌現的是報告與 git 的移除前副本(實跑要求 work 檔已 commit、乾淨)。**沒有具名逆操作**:被移除的來源只在 git 的移除前副本與報告裡;重新匯入不會把它接回這一筆(那個條目若在 Zotero 端仍在,是否另建一筆新 entry 依移除之後還有沒有別的 entry 宣稱這個來源而定——`reimportNote`/`reimportEffect` 逐來源說),要接回只能手改 YAML 或走攣生合併。主來源移除時附加來源**不升格**(同 App 的「與 Zotero 脫鉤」),所以本面不替呼叫端決定哪個來源接管欄位的改寫權。落地是程式(來源鍵不在這筆、同來源兩次、形狀錯、理由空白或過長都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--add-source`/`--remove-source`)組合 | +| `update-entry --remove-source`/`akashic_update_entry.remove_sources`(#677) | AI | 「這份內容不是這篇的副本」是判定——`--add-source` 收的是上游已判定的連結(`akashic-fetch-fulltext` 的驗證:頁數、首頁標題、DOI),而**收回**要重新讀內容才知道連錯了,字串謂詞做不出來。理由必填(`=理由`)、**只進報告**(移除面一族的裁決,使用者 2026-09-27),所以「留 verdict」在 store 內是**有記錄地不兌現**:兌現它的一半是報告(`sourcesRemoved`,全文)、一半是 git 的移除前副本——實跑要求那筆 work 檔已 commit、乾淨(`assertRecordsRecoverable`)。逆操作是 `--add-source` 再連回去——digest 沒有變、blob 與取得記錄本來就沒動(本面只收回宣告,不刪 `sources/`),所以還原是無損的(只要本機仍有那份存檔,`--add-source` 的存在性閘才過)。落地是程式(digest 形狀不對、不在清單上、理由空白或過長、同一個 digest 兩次都整批拒絕零寫入);預設乾跑、`--apply`(MCP `dry_run:false`)才寫;不與另外三條腿(`--remove-field`/`--add-source`/`--remove-zotero-source`)組合 | +| `dismiss-divergence`/`akashic_dismiss_divergence`(#586) | AI | 「這個問題不成立/不再延後判定」是判定——與 `record-divergence` 的「當場記錄而非當場判斷」相對:那一格記下判定尚未做出,這一格判定它不必再做。理由必填、**只進報告**(移除面一族的裁決),store 內「留 verdict」有記錄地不兌現——兌現的是報告與 git 的刪除前副本(要求記錄檔已 commit、乾淨)。只刪記錄、候選實體與參照都不動,所以沒有判定被連帶銷毀 | +| App 裁決台 Orphans:移到垃圾桶/與 Zotero 脫鉤(#605)/拿掉已刪除的附加來源(#609) | AI | 「上游刪了,要不要放棄等待」是判定——Zotero 端的刪除可能是暫時的(同步途中、誤刪後救回),程式不替人決定放棄;三個動作都在動作當下重新讀盤確認形狀(`Entry.zoteroLinkState`,TOCTOU)後才寫。**拿掉已刪除的附加來源**套用移除面一族的使用者裁決(2026-09-27):理由必填、只進回傳的結果(不寫進 store),移除前要求記錄檔已 commit、乾淨——所以「留 verdict」在 store 內**有記錄地不兌現**,兌現的是結果與 git 的移除前副本。**#683 起 git 閘的外層與理由上限與 service 的移除面是同一份**(StoreIO 的 `LibraryStore.recordRecoverability`/`LibraryStore.maxStatementBytes`):先前 App 自己寫了一份(自己的路徑解析、自己的措辭、自己的 `4_096`),改一邊另一邊不會跟著變。**「與 Zotero 脫鉤」早於那一族裁決**(#605 R1 使用者裁決),不帶理由與 git 閘——同一個裁決台上兩個移除動作的閘不一致,是有記錄的缺口,要不要補是另一個裁決;垃圾桶走系統 Trash,可救回。三者都只在 App(CLI/MCP 沒有 orphan 裁決面)。**#609 R1 verify 補記**:(1) 三個動作共用同一條定位守衛(citekey 重複或與另一筆共用 id 時拒絕、零寫入);(2) 拿掉已刪除的附加來源拿掉的只能是使用者看到並確認的那一組(呼叫端傳入看到的來源鍵,動作當下的那一組不同就拒絕),git 閘通過後記錄若又被改動也拒絕;(3) **#609 把「與 Zotero 脫鉤」的無理由、無 git 閘延伸到「沒有主來源、附加來源全部已刪除」這個新形狀**(先前脫鉤只作用在主來源 orphan 的形狀)——這是 #605 R1 使用者裁決的延伸,與 2026-09-27 移除面一族裁決(新增的移除路徑都要理由與 git 閘)不一致,要不要補閘待使用者決定 | +| `record-divergence`(#77) | AI | 「當場記錄而非當場判斷」——記下判定尚未做出,本身也是判定型工作的一部分。**#611 起記錄不只由這個面寫**:`import-zotero` 以程式寫 DOI 提名(見 `import-wos`/`import-zotero` 那一列,種類是程式),那種記錄沒有判斷;補上判斷走本面對同一組候選的重錄(同組候選=同一筆記錄,「無→有」本來就允許) | +| `rename`/`rename-person`(#35/#232/#395) | 程式 | 改名不改身分;參照集合由封閉列舉逐條窮舉、可機械檢查。**#554 R21/R22/R23(D60/D63/D62/D64)**:目的鍵上已有 verdict(三種 holder 與 quarantined 檔都掃——quarantined 檔以**位元組**比對 `:`,不含空白、YAML 折行打不斷;R22 的行級比對被折行擊穿,R22 verify 四席同指)即具名拒絕、零寫入——程式編輯不得銷毀判定編輯的產物;唯一的收攏是完全相同(含 judgement 與 rests-on)的重複,零資訊損失(R21 verify 第 18 列:D60 曾只記在 `resolve-divergence` 列,本列才是它的家)——**「完全相同」是位元組層的話**(R24 D65,`ProvenanceReference.byteExactKey`;R23 verify Codex 第 1 列:R23 以合成 `Hashable` 當鍵,Swift `String` 的相等是 canonical 的,NFC/NFD 兩種拼法被折成一筆——R16 D42 修過同一個缺陷的另一處);留下的同鍵異 judgement 由 `StoreHealth.duplicateVerdictRecords` 報(D64)——rename 那一步零損失,下一次合併會收攏、回報在合併的 `verdictsCollapsed`。**#642 R1 verify:library 的成員規則(第 16 條邊)也在同批遷移裡**——換掉的只是規則裡的 citekey,不判定;registry 檔要 tracked 且 clean(同 #573 一族的可回溯閘),與其他被改寫的記錄同一批預檢 | +| `migrate-*` 一族/`fmt`(#227/#304/#325/#394/#422) | 程式 | 格式遷移;不可逆但決定論,前置是 git 追蹤,乾跑逐筆過目 | +| `repair-venue-names`(#575) | 程式 | venue 名字不變式(§5.7)的機械修復,**只改確定性的那一類**:字串只違反 canonical 形(`NameIdentity.canonical`=NFC+空白收斂——收錄條件是「空白不是名字的一部分、NFC 不改 Swift 的相等」),正規化後通過名字不變式,整筆記錄改完通過 `Venue.validate()`(沒造出近重複、authorized ⊆ names 與分割互斥仍成立),且沒有 reference 以位元組指著舊拼法。同輸入必得同輸出,改完再跑一次是空計畫(冪等);逆操作是 git——`--apply` 要求那些 venue 檔已 commit、乾淨,任一筆過不了寫入閘整批零寫入。**判斷一律不做**:刪哪個不可見字元、近重複留哪一筆、指著舊拼法的 reference 要不要跟著改——只具名與理由(D8 的「不猜、不靜默修」);同一筆記錄還有任何判斷項時,它的確定性改寫**全部延後**(記錄還有 error 就寫不進去)。**不併進上一列**:那一列是格式遷移與序列化對齊,本列改的是名字的內容位元組,而 `fmt` 對帶 error 的記錄刻意拒絕。**誠實邊界**:NFC 的單一碼位替換(CJK 相容表意文字 U+FA10 → U+585A)是位元組層有損的——它仍是 canonical 形的定義、仍算確定性,乾跑逐碼位說出來 | +| `import-wos`/`import-zotero`(#206) | 程式 | 對映歸對映、收集歸收集;conflict 交人,不猜。**#611 起 `import-zotero` 另寫歧異提名,種類不變**(使用者 2026-10-01 裁決「照建,並自動記一筆歧異提名」):新建的 work 與另一筆 work(既有的,或同一趟也新建的)共用 DOI 時照建,並以 `recordDivergence` 寫一筆 work 形狀、**沒有判斷**的歧異記錄,候選是那一對。它是**提名**(recall)不是判定:DOI 相等在攣生上不裁決(`identity-is-judged-not-matched`「識別碼終結指涉,不終結描述」——勘誤與原文共用 DOI、一筆作品可有多個 DOI),而「哪兩筆共用 DOI」是同輸入必得同輸出的比對。judgement/prefers 一律不填;已有涵蓋這一對的記錄不重寫(重寫會換掉 question、可能碰到人補上的判斷);無法唯一定位的候選不點名、只報告。**提名要有資訊量才記**(R1 verify):共用同一個 DOI 的 work 超過 10 筆時一對都不記、改報一列——那多半是書或資料集的概念 DOI,逐對提名是 C(k,2) 筆沒有資訊量的 recall(DA 實測 k=80 → 3,160 筆);歧異記錄的 id 只由候選 key 決定、不含形狀,所以同 id 的另一種形狀記錄既不算涵蓋、也不覆寫(報 failed)。判定與合併仍是 `resolve-divergence` 那一列(AI)——提名(程式)→ 判定(AI)→ 落地(程式)的第一段 | +| `create-entry`/`createEntries`(#206/#455) | 程式 | citekey 由規則生成;批次內碰撞由 `existing` 累積消解;可預期失敗整批擋 | +| `enrich-from-zotero`/generic enrich(#340/#458) | 程式 | 只補不存在的鍵;來源給什麼收什麼,不判定 | +| `bootstrap-people`/`-organizations`/`-venues`(#367;#547/#548/#563 補註) | 程式 | 門檻建檔是提名不是判定——建出來的實體仍待 `resolve-*` 判定歸戶。**#547 起 `bootstrap-people` 多扣住一類**:彼此寬鬆共鍵而兩邊都還沒有記錄的群(`pendingMutual`),與既有的「與既有 person 共鍵」對稱。**種類不變,仍是程式編輯**——它用 `LooseNameKey` 做決定論式**提名**(同輸入必得同輸出),判定留給人/AI;`identity()` 一行不動,因為放寬它就會變成程式做身分判定(`identity-is-judged-not-matched`)。這一列**不另開新列**:新增的是既有寫入面的一個報告欄位與一道扣留,不是新的寫入面,而本表的列是「寫入面」。**#548 對 venue/org 做同一件事,種類同樣不變**——但判準是另一套(`LooseTitleKey`:標點/`&`/前導冠詞),不是把 `LooseNameKey` 平移過去。理由是人名與刊名的變異形狀不同:人名會被索引系統重排(`Hsu, Yung-Fong`)、刊名不會,而 token 集合相等對刊名是誤判來源。那是一次裁決,寫在 `LooseTitleKey` 的檔頭。**#563 起(2026-10-01,使用者裁決)`bootstrap-venues` 建檔不寫 `authorized`**:先前每筆寫 `[names[0]]`(字母序第一個寫法),那是判定形狀的寫入由不做判定的操作寫出——#471 對 `variant` 修掉的同一形狀,#554 補了判定面卻沒動這個寫入者。現在與 `add-venue`(#227)一致留空,`displayName` 退到 names 的第一段、顯示不變;三個 bootstrap 自此都不寫 authorized(`bootstrap-people` 本來就不寫,指定走 `authorize-names`)。既有的 470 筆機械值不動,#600 裁決不跑全量 campaign、按需判定——它們仍分不出是不是人確認過的,下面 `resolve-divergence` 列的「提醒不擋」因此不變(**#564 落地後**:機械值沒有名字分類記錄、人判定過的有,合併端分得出兩者——機械值仍是提醒、有記錄的拒絕;見該列)。**#709 第三次 verify(2026-10-04)重新確認,種類不變**:literal 來源看完整的 load(R3 曾改用以 entities/ 為準的視圖,那是寫入候選面、超出使用者 2026-10-01 的裁決範圍,確認之前收回);計畫含同一筆記錄的 legacy 拷貝時開頭說出幾份、計數多算一次(`legacyCopiesInPlanNote`,2026-10-09 起改名 `planBlockersNote`)——提名的輸入照實,不安靜略過拷貝裡的 literal。**#709 第四次 verify(2026-10-05,使用者裁決)重新確認,種類不變**:literal 來源維持完整的 load(裁決 1:寫入候選面一律看完整資料);計畫讀到拷貝時 `--apply` 整批拒絕、零寫入(裁決 2)——拷貝讓一對相同的 literal 算兩次、可能越過 `--min-occurrences`,那是可預期的失敗,程式編輯欄的義務是整批擋,不是附註之後照寫。只數流進該命令 literal 來源的種類(`BootstrapPlanSource`)。**#709 b36 verify(2026-10-09,使用者裁決)重新確認,種類不變**:拒絕條件改成「計畫讀到的種類裡 load 標出的拷貝 ∪ 無法唯一定位的記錄」(`LibraryLoad.planBlockers`)——`bootstrap-people` 的計畫也讀 person 拷貝(裁決 1:它讀 person 的 key、名字與否決/確認史,拷貝可以讓候選變多、讓 literal 安靜消失、讓新 key 多後綴,「只會讓候選少一個」是假的),works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`(裁決 2)。理由同上一句:計數的輸入有兩份、store 說不出哪一份才算,是可預期的失敗 | +| `library add`/`remove`、`tag`、`link`、`set-status`(#219/#258/#455) | 程式 | 集合語意,冪等。**#642 起 `library add` 對規則型/文件型 library 逐筆比對成員規則、不符的不寫並具名,種類不變**:比對是決定論式的(venue key 邊、type 集合、排除清單、文件的 `cites`——同一份 store 對同一筆必得同一個答案),不是判定;判定(這筆作品屬於哪本刊、這篇文件引了什麼)早已由 `resolve-venues` 與 `cites` 的寫入者做完並留下記錄,add 只是讀它。venue 只有未歸戶 literal 的不寫(`literal-first-then-key`:不猜),指路 `resolve-venues`;未標性質的 library 拒絕 add(查不到依據) | +| `library set-kind`/`akashic_libraries(set-kind)`(#642) | 程式 | 宣告 library 收什麼是使用者的決定——`source-of-truth-over-consent` 表「library 的性質與成員規則」一列(#642 同批加表),使用者的話就是依據;寫入本身是整值替換、冪等,規則指涉的 venue/文件要在庫。**它不判定現有成員**:不符新規則的成員列出來、不自動移除(移除是另一個寫入,`library remove`)。**#642 R1 verify:整值替換回顯先前的值,替換既有的性質要求 registry 檔已 commit**——程式編輯的義務是「冪等或有具名逆操作、代價要量測」,而舊規則(排除清單是人的裁決)在沒有 commit 時沒有逆操作;所以與 #573 一族同一支閘。**標成 topic 等於關掉檢查**,工具不替呼叫端擋(那是使用者的話),但規則與 skill 明寫不得為了讓 add 通過而替使用者標 topic | +| order-insensitive collapse(#461)、verdict holder 遷移(#463) | 程式 | 對已判定結果的機械搬移;只收本次觸及、不碰未觸及 | +| `authorize-names`(#81) | AI | 「哪個名字對外」是人的判斷,建檔不得機械偽造(#227)。**#564 落地(2026-10-01)**:`--apply` 必附 `--judgement`(整批一句、不帶證據:一個批次共用同一組 digest 等於宣稱每個人的名字都依據同一份文件),每個寫入的 person、每個被採用或提名的名字各一筆 `field: authorized` 的 `指定:理由`;本面只對 authorized 為空的人寫入,所以不寫「確認」;乾跑不需要理由;需要 store format ≥ 22。**不再寫 store marker**(先前 `--apply` 結尾無條件把 marker 寫成 supported,在 format 22 下會安靜地替使用者升 marker——升 marker 是使用者的動作)。先前不留記錄。**#709 第三次 verify(2026-10-04)重新確認,種類不變**:計畫與 `--apply` 的拒絕都看完整的 load(R2 曾讓計畫取視圖,改名留下拷貝的 person 因此從整批拒絕變成寫入);乾跑算出 `--apply` 會擋的那幾筆並說出來(`blockedKeys`),乾跑與實跑對同一個 store 說同一件事。**#709 第四次 verify(2026-10-05)補記,種類不變**:寫入集合裡有任何一筆的 id 還有 legacy 拷貝時也整批拒絕——先前只擋「legacy 那份本身在寫入集合裡」,改名一對而 legacy 那份已有 authorized 時只寫 entities/ 那份,兩份各帶不同的 authorized 狀態;與三個 bootstrap 對同類狀態的處置一致(使用者 2026-10-05 裁決 2) | +| `enrich`/`akashic_enrich`(#458) | 程式 | generic add-only:只補不存在的鍵、來源給什麼收什麼、不判定——同輸入必得同輸出。DOI → citekey 由程式做是**識別碼例外**(`identity-is-judged-not-matched`),命中 ≥2 筆時**拒絕不判定**(`ambiguous`)——判定屬 #459 的攣生管線。`enrich-from-zotero` 自此是它的 adapter,種類不變。**#517 起它寫來源 reference,而 kind 必然是 `retrieval` 不是 `judgement`**——那不只是形狀偏好:judgement 是 AI 編輯欄的產出物(「留 verdict、必附理由」),一個決定論式的補值面**不該發出判定**。「這個值取自那份存檔」是一次取得的記錄,不是推理。**#655 重新確認,種類不變**:補進去的 `date` 也寫 retrieval(新格 `field: date`,store format ≥ 20),理由同上;補進去的 `authors` **不寫**——`field: authors` 那一格只收 judgement(作者位記錄的拆分/移除,value 是已退役的 literal),一個決定論式的補值面寫進去,要嘛發出判定、要嘛讓同一個 field 承載兩種相反的語意,兩者都不可接受;不寫的理由進報告(`provenanceOmitted`) | diff --git a/docs/rulings/zero-instance-guards.md b/docs/rulings/zero-instance-guards.md new file mode 100644 index 00000000..ebbb8443 --- /dev/null +++ b/docs/rulings/zero-instance-guards.md @@ -0,0 +1,275 @@ +# zero-instance-guards:裁決表 + +從 [`.claude/rules/zero-instance-guards.md`](../../.claude/rules/zero-instance-guards.md) 搬出(2026-10-15)。規則檔每個 session 自動載入,而搬移前 `.claude/rules/` 底下的檔合計 738,145 位元組,雲端 session 一開場就被它們撐爆 context;所以把只有查過往裁決、或新增一列時才需要讀的段落搬到這裡——`docs/rulings/` 不自動載入。 + +搬移是逐位元組的:下面各段與搬移前規則檔裡的那幾段一字不差,新增的只有本段說明與分隔用的標題。所以讀的時候: + +- 文中的「本檔」指規則檔;「上表」「下表」照搬移前的順序讀(表原本住在規則檔裡)。 +- 文中指向同一個目錄的相對連結是從 `.claude/rules/` 解析的,在這裡會點不開。 + +`zero-instance-rows-audit` 從這份檔讀裁決表與「各列共通的東西」、也掃這份檔裡的量測;`measured-numbers-audit` 同樣掃它。各列的量測與歷輪補記在 [`docs/zero-instance-measurements.md`](../zero-instance-measurements.md)。 + +## 裁決史(封閉列舉——現有 102 列,一列不多一列不少) + +| # | 情形 | 裁決 | 理由 | +|---|---|---|---| +| 1 | **零實例、成本一行、前件精確**(#254:`duplicate-rationale` 擴及同命題內兩條 relation。全 corpus 被 >1 條共用的 rationale 種類數 = 0)| ✅ **寫** | 成本是一行前件;而不寫的話,那個形狀第一次出現時**不會有任何跡象**——它會通過 validate、通過測試,只在有人逐條讀 corpus 時才看得出來 | +| 2 | **零實例、成本一行、但前件寫寬了會誤傷**(#253:issue 的 Expected 寫「對所有 status 檢查」。若照字面實作成「任一 relation」會 flag 上百條)| ✅ **寫,但先實測界定前件** | 判準不是「要不要寫」而是「前件多寬」。實測「rationale 引用了 issue」恰好命中 issue 具名的三筆、零附帶損害;「任一 relation」則會 flag 上百條。**沒有那次量測就不該動手** | +| 3 | **未涵蓋不得冒充通過**(#326:`BibValidator` 的必要欄位表只涵蓋 7 個 entry type,store 另有 47 筆不在表內)| ✅ **寫「未涵蓋」的出口** | 這一列不是「要不要寫守衛」而是「守衛沉默時要不要說話」。若只回 issues,「沒被檢查」與「檢查過且乾淨」在輸出上完全一樣——`lossless-intake` 執行細節 3 的「靜默是最糟的形式」 | +| 4 | **猜錯不會有跡象,而猜的代價是整組欄位需求**(#367:`bootstrap-venues` 的型別衝突守衛。同一個刊名來自不同種類的來源欄位時不建檔、交人裁。實測 441 個 literal 全部只對應一個欄位種類 → 0 實例)| ✅ **寫** | 前三列的代價都落在「看不見」;這一列的代價是**看得見但看起來是對的**。`VenueType` 決定「哪些欄位存在」(#324 的 §11 判準),所以取錯 type 不是標籤錯而是**整組欄位需求錯**——而那筆記錄會通過所有檢查,因為它確實滿足了(錯的)那一組。守衛的成本是一個 `guard`,不寫的代價是一筆看起來健康的壞資料 | +| 5 | **重複看起來像多一份覆蓋**(#407 R67c:負控 harness 裡兩個 case 若輸出逐字相同即互相不可區分。實測 35 個 case → 未具名的重複 0 組)| ✅ **寫** | 前四列的代價都落在「被守衛的東西」上;這一列落在**守衛自己身上**。一個重複的 case 會讓計數從 35 變 36、多印一個 `✓`,維護者讀成「又多檢查了一件事」——而那是同一件事查了兩次。它不是看不見(第 1 列)、不是假訊號(第 4 列)、不是沉默的歧義(第 3 列),是**對測試自己的覆蓋率說謊**。成本是零:既有迴圈本來就跑完全部 case,這裡只是分組 | +| 6 | **假紅會讓所有紅燈失效**(#407 R67e:ROBUST 負控拿「守衛在未注入 copy 上的輸出」當 oracle、要求逐字相同。那個前提是輸出決定性——實測 2 支守衛各跑兩次皆逐字相同、無 temp 路徑洩漏 → 0 實例)| ✅ **寫** | 前五列的失敗都**只傷到自己那一格**;這一列的失敗會外溢。守衛若哪天開始印時間/耗時/temp 路徑/走訪順序,逐字比對就偶發變紅——而**偶發的紅**比漏報更貴:維護者學會「這支有時候會紅,重跑就好」,那個習慣會套用到**所有**守衛身上。所以本 harness 唯一一個「失敗會傷到其他守衛」的機制,自己要被檢查。成本是每支 ROBUST 守衛多跑一次未注入(實測 2 支、約 2 秒) | +| 7 | **守衛兌現的是一句散文,而它只兌現得了一部分**(#414:`booktitleCarrierTypes` 的成員資格判準只存在於 doc comment,「不得依性質相似類推第三個」沒有可執行的程序。守衛改成「本表成員不得帶 `editor`」——實測 54 筆成員記錄觸發 0)| ✅ **寫,但明寫它只是必要條件** | 前六列的守衛,通過時至少**指向**它們要證的那個性質;這一列連指都沒指到——它查的是一個**嚴格更弱的命題**。這一列不是——一個不帶 `editor` 的新型別**仍未必**該進表,那一步永遠是人工裁決。所以本列的裁決有兩半:寫(它讓一句空頭承諾變成部分可兌現),**以及在守衛旁邊明寫它兌現不了的那一半**。只做前半會把一個空頭承諾換成另一個——讀者會以為「守衛綠了=成員資格對了」,而那正是 #414 指出的原病 | +| 8 | **守衛防的是一個當下不可達的狀態**(#416 R1:`errorsFirst` 讓 error 排在 warning 之前,因為 MCP 面截斷 20 則。實測**五族的 error 級 per-record 檢查全部是 key 合法性檢查,而 load 對每族做同樣檢查並 quarantine 整個檔**——那些 error 分支對載入後的記錄結構上不可達,0 實例)| ✅ **寫,但把「為什麼是零」也釘住** | 前七列的零實例都是「那個形狀還沒出現」;這一列是「那個形狀**目前走不到**」——不可達性由**別處的**驗證(load 的 quarantine)造成,而那是我沒有控制、也沒有守衛盯著的東西。所以本列的裁決有兩半:寫那個排序(成本一行,且它一旦可達就是**看不見的漏報**),**以及寫一條測試釘住「現在為什麼是零」**。**前提的失效日期**:「per-record 的 error 級檢查全是 key 檢查」對 venue/organization **自 #227(authorized ⊆ names)/#422/#473 起就不成立**——那些內容檢查在寫入期、decode 不驗,載入後可達;本列的 pin test `testNoPerRecordErrorIsReachableFromALoadedStore` 只對 entry/person/divergence 改壞 key,證的是「key 錯會 quarantine」而不是這句前提,是弱測試。排序守衛因此早已是承重結構,不是裝飾品;本列的裁決(寫)不變,只是理由的第二半(釘零)從那時起就沒釘住。(歷輪補記見量測文件第 8 列) | +| 9 | **一個零實例的欄位,而缺席會被讀成「不可能」**(#394:`Organization.ror`。實測 8 筆 organization、**0 筆帶 ror**;`Venue.issn` 有 39、`Entry.doi` 有 664,只有 organization 這一格是空的) | ✅ **寫** | **前八列講的都是守衛,這一列講的是欄位**——它不查任何東西,所以「失敗」的意思不同。不寫的代價不是漏報,是**模型會說一句它沒打算說的話**:一個讀者問「Akashic 有沒有模型化機構的識別碼」,會看到 person 有 `orcid`、venue 有 `issn`、organization 什麼都沒有,於是合理推論「機構沒有識別碼可記」。而那是假的——ROR 存在、`identity-is-judged-not-matched` 的封閉例外節具名列了它,我們只是還沒有資料。**缺席在這裡不是中性的,它是一個關於世界的斷言**,而那個斷言是錯的。成本是一個 `ROR?` 欄位(additive,舊 binary tolerant-preserve)| +| 10 | **零實例、成本高,而形狀取決於一個還不存在的用途**(#365:`Author.correspondingAuthor`。APA7 的參考文獻**不印**通訊作者,所以它服務的是 CV 產生或作者查詢——而那兩個功能都不存在。實測 55 筆 work 的作者含 `che-cheng`,而「其中幾筆是通訊作者」在 store 內**不是可求值的命題**) | ❌ **不寫** | **本表第一個「不寫」,而它是本檔自己預測過的**(見下方「還沒出現過的情形」——那段寫著「零實例且成本高的守衛……那一列的理由**很可能是『不寫』**」)。理由不是成本高本身,是**形狀取決於用途而用途不存在**:`Bool` 還是 `Set` 只有那個場景能決定,現在選一個等於用猜的固定一個介面。與第 9 列(`Organization.ror`)的關鍵差別:那一列的**形狀是確定的**(ROR 是純量),缺的只是資料。**觸發條件**(逐字取自 2026-08-28 裁決 §1):出現第一個需要它的場景(CV 產生器、或一個「我是哪幾篇的通訊作者」的查詢)——那時形狀才能被決定。這一格沒有機械量測,需要人指認 | +| 11 | **零實例、成本高,而既有欄位已經承載它**(#365:`Author.role`。實測 `fields` 的 role 一族恰 **2 筆**(`editor` ×2),其餘八個相關欄位全零;而那 2 筆已由 `fields["editor"]` 承載、#350 已讓 `EDITOR` 在編著作品上滿足 APA7 下限) | ❌ **不寫** | 與第 10 列同為「不寫」而**理由完全不同**:這一列的形狀是確定的,缺的是**理由**——為 2 筆把 `Author` 從 `enum` 改成 `struct`(非 additive、bump store format、**`Sources/` 54 行/26 檔,含 `Tests/` 153 行/64 檔**——量法與重跑指令見量測文件;先前寫「46 個呼叫點」而三個來源給出三個數,見 #481)不成比例。**觸發條件可檢查**:`fields` 的 role 一族 ≥ 20 筆,**或**出現一個 role 是 `fields` 承載不了的(同一人在同一篇既是作者又是譯者——那時 `fields["translator"]` 與 `authors` 會各自說一半)(量測見量測文件第 11 列) | +| 12 | **零實例,而它屬於另一個型別**(#365:`Author.pseudonym`。筆名是「這個人也用那個名字發表」——關於**人**的事實,`PersonNames` 已有可承載它的 `variant` 分區;authorship 側只在「同一人在**不同作品**用不同筆名、且兩個名字都要進各自的參考文獻」時才需要 per-occurrence 的名字選擇,且與 #386 的 per-work judged authorship 是同一個位置。) | ❌ **不寫** | 與第 10、11 列同為「不寫」而理由是**第三種:位置錯了**。第 10 列缺用途、第 11 列缺理由,這一列連問題都問錯了型別——把筆名放進 `Author` 會讓「人的名字」這個事實有兩個家(`Person.names` 與每一筆 authorship)。APA7 對筆名的處置(用出版時的名字、必要時方括號補真名)是**呈現層**規則,讀的是 `Person.names`——它已經有兩個分區可用。觸發條件見情形欄;**這一格沒有機械量測,需要人指認**(store 沒有筆名的表示法,連查詢都寫不出來——與第 11 列不同,那列至少有一個可數的析取項)。落點在 #386 那一族,不是 `Author.pseudonym`(歷輪補記與量測見量測文件第 12 列) | +| 13 | **零實例,但同族的結構缺口已經出現三次、stale 實際累積過一次——抓到它的三個機制全是場外的,沒有一個是守衛**(#464:死 verdict 掃描。resolution verdict 的 value 指向一個沒有載入的 holder。2026-09-03 實測 live store 2,700 條 verdict(confirmed 與 rejected 合計),死引用 **0**。) | ✅ **寫** | 前面各列的零實例都是「這個形狀還沒發生過」;這一列的零是**清理過之後的零**——形狀在 #460 真的發生過(205 條),只是被場外機制(人肉、verify lens、一次性腳本)掃乾淨了,store 裡因此看不到。所以本列的理由不是第 1 列的「不寫就沒有跡象」(跡象有,在 issue 史裡),而是**跡象住在錯的地方**:每一次都要一個人記得去看,而下一條 holder 退役路徑不會有人記得。掃描放進 `StoreHealth` 是把跡象搬到工具會自己看的地方。**severity 是 warning**:升 error 要與第 8 列一起重開——死 verdict 若是 error,就是第一個既非 key 檢查、又對載入後記錄可達的 per-record error,第 8 列釘住的零會翻掉,所以本列繼承第 8 列的釘零義務;今天仍沒有修復路徑,裁決未重開。warning 級的 `validate` exit 仍為 0——掃得到、不叫醒。**觸發條件可檢查**:「死 verdict」數應恆為 0;非零時先看 quarantined 清單——被 quarantine 的 holder 是「讀不到」不是「退役」,其餘才指向一條漏了遷移的退役路徑。(歷輪補記與量測見量測文件第 13 列) | +| 14 | **零實例、成本一個掃描,而判準取決於一個還不存在的相等定義**(#464 的第二個掃描項——同一 owner 對同一配對同時持有 `resolution-confirmed` 與 `resolution-rejected`。2026-09-03 實測 live store 以 (檔, holder) 與 (檔, 完整 value) 兩種收攏法各算一次,並存數皆 **0**;追蹤 #486,其 `### Blocking` 記 #470) | ✅ **寫**(2026-09-09 翻轉——觸發條件成立) | 與第 10 列同形——判準取決於一個還不存在的東西——而對象不同:那一列是**形狀**取決於用途,這一列形狀確定(一個並存掃描),是它要比較的**相等**取決於 #470:寫入面位元組精確、讀取面 `NameNormalization.matchingKey` 正規化,兩面今天對同一筆 verdict 答案不同。現在寫任一個,都是在 #470 之前偷偷定案第三份相等定義,裁決後必然分岔(`no-compat-fallback` 記過的形狀)。**觸發條件已成立**:#470 於 2026-09-09 選定相等定義(正規化,且只住在鍵裡),本列依它自己寫下的條件改裁「寫」,在 #486 實作——`StoreHealth.contradictoryVerdictIssues`,warning 級,2026-09-09 實測 live store 仍 **0**。**翻轉的方式本身值得留著**:這一列不是被人想起來才改的,是它自己寫下了一個**機械可檢查**的觸發條件(另一張 issue 的 state),而那張 issue 一 close 就到期。(歷輪補記見量測文件第 14 列) | +| 15 | **零實例,而零是本機的——同一份 store 在別台機器上全是實例**(#453:本機缺承重存檔的掃描。記錄的 provenance 指向一個本機 `sources/` 沒有的 digest。) | ✅ **寫** | 前面各列的零,成立與否**不取決於在哪台機器上量**;這一列的零**只在這台機器上為真**——`sources/` 不進 git(`replace-endnote-and-zotero` 的承重閘:第三方版權 PDF 住在那裡),所以每一台新 clone 上這個數字都是「全部」。與第 13 列(清理過之後的零)最像而不同:那一列的零是**時間上**的(曾經非零、掃乾淨了),這一列的零是**空間上**的(換一台機器就非零)——而任何只在本機量的守衛都會對它報綠。**severity 是 warning**:記錄合法可載入、缺的是位元組,而其他 clone 上「全部 dangling」是常態,error 會讓 `hasErrors` 翻紅擋住 export 類流程。**觸發條件可檢查**:本機數字應恆等於「不是合法 digest 的引用數」;多出來的那些指向沒同步的 `sources/` 或真的捏造——先同步,同步後仍在的才是後者(歷輪補記與量測見量測文件第 15 列) | +| 16 | **零實例,而它守的是一個已裁決「現在不改形狀」的 O(n) 增長**(#499:venue 的記錄檔逼近讀取上限的 warning——2026-09-26 起量記錄檔的位元組、門檻是讀取上限 8 MiB 的一半;原本以 verdict 數換算 decode 預算的節點,量錯了軸,更正見量測文件第 16 列。沒有一本刊接近門檻 → 0 實例。使用者裁決(2026-09-04):候選 3——不改序列化位置、半預算處出聲、達門檻重開裁決;候選 2(sidecar ledger)是那時的形狀。) | ✅ **寫** | 前面各列的守衛守的是「某個形狀出現」;這一列守的是**一條已知會漲、且裁決了暫不改形狀的曲線**——它的零不是「還沒發生」,是「還沒漲到」。不寫的代價與第 1 列同形(撞上硬預算時整檔 quarantine、venue 消失,而在那之前沒有任何跡象),但理由多一層:**裁決本身依賴這個守衛**。候選 3 之所以可接受,是因為「達門檻時重開」被承諾為一個工具會自己看的門檻,而不是散文觸發條件(`blocked-issues-must-be-scannable` 的誠實邊界:散文命題沒有機制會叫醒任何人)。拿掉守衛,裁決就退化成「等它壞」。**觸發條件可檢查**:任一 venue 的 warning 出現即重開第 13 條邊的規模化裁決,不要只放寬預算。(歷輪補記與量測見量測文件第 16 列) | +| 17 | **零實例,而零的來源是「寫入面剛長出來」**(#450:拆分後錨失效的兩種 warning——某 person/organization 持有的 resolution verdict 其 literal 已被 work 的拆分記錄退役(孤兒 verdict)、拆分記錄各段全不在作者位。) | ✅ **寫** | 前面各列的零各有來源——還沒發生(第 1 列)、走不到(第 8 列)、掃乾淨了(第 13 列)、在這台機器上(第 15 列)、還沒漲到(第 16 列);這一列的零是**形狀已經發生過 4 次、只是沒被記下**——寫入面在本 change 才長出來,實例從第 5 次拆分起才可能出現。不寫的代價與第 1 列同形而更具體:下一次拆分後,一條指向已退役作者位的 `resolution-rejected` 會讓 `resolve-people` 永遠不再提名那一段(否決抑制以 (citekey, literal) 配對),而沒有任何跡象——那正是 `literal-first-then-key` 說的「誤不可逆」在 verdict 側的形。severity 是 warning(處置是人的重新消歧,不是修檔;`validate` exit 仍 0——「掃得到」不「叫醒」,#464 的同一條界線)。零由三支測試釘住(`OrphanedSplitVerdictScanTests`:乾淨為零、形狀出現即報、鍵是 (citekey, literal) 不是 literal)。**觸發條件可檢查**:live store 第一次跑 `--split-author` 之後兩個計數才可能非零;非零時先看孤兒 verdict——那是要人動手的那種,各段全不在只是提醒記錄留著供 un-split(歷輪補記與量測見量測文件第 17 列) | +| 18 | **零實例,而零只存在於「已經進來的資料」上——守衛守的是入口**(#519:`AddOnlyEnrichment` 對單一字串的 65,536 位元組上限。離上限還有 **15.5 倍**,今日零實例。**但那個形狀已經被造出來過**:#516 verify 用一份 5 MB 摘要走**同一條鏈**實測 RSS 46.6 MB、`proposals.json` 5.0 MB 原樣流進 store 欄位——是上限的 **80 倍**。) | ✅ **寫** | 前十七列的零都是關於**已經在庫裡的東西**——還沒發生(第 1 列)、走不到(第 8 列)、掃乾淨了(第 13 列)、在這台機器上(第 15 列)、還沒漲到(第 16 列)、寫入面剛長出來(第 17 列)。這一列的零**量的是出口,而守衛裝在入口**:store 的最長值是 4,220 bytes,是因為進得來的都小;而這道檢查管的是 adapter **這一次遞過來**的字串,那個長度不受庫內歷史約束。與第 16 列(已知會漲的曲線)最像而不同:**這裡沒有曲線**——實例不會由目錄成長慢慢逼近,它會由一次外部呼叫**一步跨過**,而 #516 verify 已經一步跨過 80 倍。**裁決的第二半是語意**:超限**整批拒絕、零寫入、具名,不截斷**——截斷會讓一個不是來源給的值進 store 且不出聲。(歷輪補記與量測見量測文件第 18 列) | +| 19 | **零實例,而被守的是「辯護」不是事實**(#479:本表每一列都要在「各列共通的東西」有一條 bullet 講它自己的理由。) | ✅ **寫** | 前十八列守的都是**可判定的性質**——欄位在不在、數字對不對、檔案存不存在、記錄形狀合不合法。這一列守的是**這張表存在的理由本身**:表是規格,理由欄是它的辯護,而共通段是那個辯護的第二層(它說明每一列的理由**彼此不同**,那正是本檔不寫總括判準的依據)。**少一條 bullet 不會讓任何檢查變錯**——沒有任何數字會偏、沒有任何斷言會紅——它只會讓下一個人拿判準去類推,而那是本檔開宗明義禁止的動作。所以這一列的失敗不是「漏報」也不是「假訊號」,是**規格的辯護少了一角而規格本身看起來完好**。**觸發條件可檢查**:未被引用的列數應恆為 0(歷輪補記與量測見量測文件第 19 列) | +| 20 | **零實例,而作者對同一形狀的窮舉自己漏了一格**(#526:workflow 的 `run:` 跑一支不存在的腳本。#521 的四個實例裡**有兩個**是這個形狀——`census-parity.yml` 與 `ci.yml` 各跑一支在 `989ac64`(#433「Python 歸零」)刪掉的 `.py`——而**實例 4 是 verify 席找到的**:當時的封閉列舉漏了它。) | ✅ **寫** | 這一列的零是**剛剛被人工修完的**,而修它的那一輪自己證明了人工窮舉不可靠:四個實例裡第四個是**別人**找到的,且它與前三個形狀完全相同。所以理由不是第 1 列的「不寫就沒有跡象」(跡象有,就在同一張 issue 裡),也不是第 13 列的「跡象住在錯的地方」(它就在眼前)——是**看過了、看的是對的地方,仍然漏了一個**。守衛換掉的不是人的注意力,是「這件事需要注意力」這個前提。後果不對稱使它值得:一個掛掉的 step 會擋住其後**全部**步驟,`ci.yml:94` 那次擋掉的包含 AkashicApp(`swift test` 涵蓋不到的那塊,#101 的立案理由)。**觸發條件可檢查**:「不存在」應恆為 0;非零時那個 step 已經是壞的,修路徑或刪 step,不要放寬檢查(歷輪補記與量測見量測文件第 20 列) | +| 21 | **零實例,而既有守衛的謂詞對這一類成員結構上不可能為真**(#522:受保護清單少了成員。`trigger-coverage` 的 `missing` 問的是「清單裡列的路徑還在不在磁碟上」——而 **glob 產生的成員永遠不會「列了卻不存在」**,它只會停止被列出。) | ✅ **寫** | 這一列不同:**守衛在,而且天天跑,只是它的謂詞對 70 個成員裡的 **48** 個永遠不可能為真(glob 與 `swiftGuards()` 衍生的那些;顯式字面 22 個,2026-09-27 重量,量法見量測文件——立案時是 58/37/21;#629 移植後 2026-09-29 重量是 61 個成員、43 個衍生、18 個顯式,比例同形)**。它不是漏看,是問了一個那一類成員答不錯的問題。所以理由既不是「缺跡象」(第 1 列)也不是「跡象住在錯的地方」(第 13 列)——**跡象根本無法產生**。**觸發條件可檢查**:差異應恆為 0;非零時先讀是「少了」還是「多了」——前者確認刪檔是有意的,後者確認新成員真的有讀者(往已在 CI paths 的路徑根加零讀者的檔可以零成本灌水)(歷輪補記與量測見量測文件第 21 列) | +| 22 | **零實例,而它是一條 spec Requirement 不是程式**(#474:venue 的 `names` 時間軸——刊名沿革。#422 把它保留而收窄(`variant` 不得帶時間),於是沿革成了一個「保留位置」的形狀。2026-09-09 實測 live store:venue **406** 筆、`names` 帶任一時間欄位(`start`/`end`/`ended`/`attested`)的 **0** 筆。) | ✅ **保留** | 這一列講的是 **spec 裡的一條 Requirement**,而它的失敗方式是第三種:守衛失敗是**漏報**、欄位缺席是**模型說了一句它沒打算說的話**,而一條零實例的 Requirement 的失敗是——**有人把它當死重刪掉**(「405 筆沒有一筆用到,留著幹嘛」),然後那個形狀到達時沒有位置可落。**保留而不刪的理由是那個現象是真的**:JRSS Series B/C 的分裂、`bulletin-of-the-institute-of-mathematics-academia-sinica` 的新舊系列都是 store 今天**表達不了**的實例(`entity-backlink-completeness` 的「分裂/繼承」那一節記著同一組例子)。**零實例的成因也具名**:異寫法佔著它的位置——#422 之前 `names` 時間軸同時裝沿革與異寫,收窄之後異寫搬到 `variant`,而沿革還沒有人填。**觸發條件可檢查**:帶時間欄位的 venue 數 > 0 即代表沿革開始被用——那時第 16 列(venue verdict 預算)與 `which-side-does-a-relation-live-on` 的「分裂/繼承」觸發條件 ② 也一起到期,三處要一起讀(歷輪補記與量測見量測文件第 22 列) | +| 23 | **零實例,而製造它的那條路徑是本輪自己開的**(#457:移除記錄與作者位互相矛盾——某 work 帶一筆「移除:理由」說某個 literal 已退役,而它現在又在作者位上。2026-09-09 實測 live store:移除記錄 **0** 筆——寫入面(`--drop-author`)在本 change 才存在,所以矛盾今天必為零。) | ✅ **寫** | 第 17 列的零也是「寫入面剛長出來」,而這一列多一件事:**矛盾的可達路徑是本 change 自己造出來的**。移除之後 `authors` 變成**空的**,而 `enrich --include-absent-authors` 的既有契約正好是「只在 authors 完全為空時補」——於是同一個字串補得回去,那時 store 同時斷言「它已退役」與「它是作者」。所以這不是「還沒發生的形狀」,是**新面把一個原本不可達的狀態變成可達**,而讓它可達的那一步與守衛必須在同一個 change 裡(`entity-backlink-completeness` 引 3.325 的立場:矛盾寫不出來最好,寫得出來就要出聲)。severity 是 warning(記錄合法,失效的是證據錨——同 `staleSplitRecords` 的既有分級;`validate` exit 仍 0)。**觸發條件可檢查**:計數應恆為 0;非零時先確認是不是補值面把它加回來的——要嘛再移除一次,要嘛刪掉那筆記錄,不要兩者並存(歷輪補記與量測見量測文件第 23 列) | +| 24 | **零實例,而它是一條「記得起來、解不掉」的半吊子管線——且零是雙重的**(#555:organization 的攣生合併。`recordDivergence` 的 `byShape` 收 org、`resolveDivergence` 對它擲 `unsupportedShape`。2026-09-11 實測 live store:organization **13** 筆、寬鬆共鍵的重複群 **0** 組、含 org 候選的 divergence 記錄 **0** 筆——沒有重複可合、也沒有人記過。) | ⚠ **暫不做——既不實作也不拿掉** | 前二十三列裡只有第 22 列同樣是「不動既有的東西」——它的對象是一條 spec Requirement、失敗方式是被當死重刪掉;本列的對象是一條**程式**的半吊子管線,而不動它可以接受的理由是**它已經誠實了**:#553 讓 `unsupportedShape` 的訊息從一份與分支對帳的清單生成——「organization 的合併管線尚未實作——支援的是 person/work/venue」——使用者撞到時看到的是真話。動它的兩個方向代價都更高:**實作**要替 `parents` 時間軸設計合併形狀,零實例時做等於猜(同第 10 列「形狀取決於一個還不存在的用途」);**拿掉**(`byShape` 移除 org)是關掉記錄面——一組真的 org 攣生在合併面落地之前只能留在人的腦裡,正是 #71「判斷留不下來」的病;venue 在 #553 之前就是這個姿態,而 #553 把記錄面與合併面綁在同一個 change,因為兩者分開時記錄面先開會撞牆(本列)、合併面先開則沒有輸入。**觸發條件**:org 重複群 > 0 **或** 出現第一筆含 org 候選的 divergence 記錄——任一成立即重開,那時要選的是實作或拿掉,不再是暫不做。後者自 R2 起由工具出聲(`StoreHealth.unmergeableDivergences`,warning——第 16 列的紀律:散文觸發條件沒有機制會叫醒任何人,R1 verify 第 13 列)。重開時 `entity-backlink-completeness` 第 9 條邊要一起改。**誠實邊界**:重複群看不到兩種形狀——一筆把幾個機構黏在一格的 org、同一機構的單/複數誤植(`LooseTitleKey` 刻意不摺詞形)——所以「重複群 0 組」承載的比它讀起來弱。本列與第 8 列同型(零的來源在別處),繼承它的釘零義務(`OrgBootstrapResolveTests.testLooseKeyCollisionWithExistingOrgRoutesToPendingResolution`)。**若實作**,`DivergenceResolve.swift` 裡帶 `org-merge-slot` 標記的每一格要同批補。(歷輪補記與量測見量測文件第 24 列)| +| 25 | **零實例,而實例全部是在 verify 裡被造出來的——守衛裝在 store 邊界,零量的是五個寫入者的出口**(#554 R5,使用者裁決 D8:venue 名字內容的不變式——canonical 形、無控制/格式/不可見字元、至少一個字母或數字、三張清單各無近重複對(names 的例外:兩段都帶不相交時間的沿革改回舊名,R6)——住在 `Venue.validate()`,error 級;謂詞一份在 `NameIdentity.wellFormednessIssue`、與輸出閘 `UnsafeToEmitScalar` 共用危險 scalar 的定義,「不可見」用 Unicode 的 `Default_Ignorable_Code_Point`,ZWJ/ZWNJ 只在兩個脈絡合法——(a) 掛在同一文字**字母**上的 virama 之後(virama 與中間的標記都是基底那個文字的;右鄰居若在要是同一文字的字母/數字,空白視同沒有)、(b) 左鄰居是 join-control 文字的字母/標記/數字、右鄰居是**同一文字**的字母/數字或同一 Indic 文字的 virama,U+2800 顯式列入不可見。) | ✅ **寫(error 級、在 store 邊界)** | 第 18 列的理由是「零量的是出口而守衛裝在入口」;這一列多一件事:**入口有五個**(`updateVenue` 的三個名字迴圈、`addVenue`、`VenueBootstrap`)。所以本列的裁決有兩半:**寫**(零實例但實例已被造出四次),**以及寫在 store 邊界而不是任何一個寫入面**——寫入面(`vetVenueNames`)留作入口的好訊息,不再是防線。**severity 是 error** 而非 warning,理由是 live store 0 筆違反(提級不拒絕任何既有記錄——但別的 clone 若持有手改的記錄,`akashic validate` 對它會 exit 1)且違反的後果是 displayName 直接壞掉(`displayName` 讀 `authorized`,#475)。量測文件第 25 列的 Python 對照腳本鏡射 `NameIdentity.wellFormednessIssue` 與 `joinerIsLegal`——改一邊要同批改另一邊。**觸發條件可檢查**:`akashic validate` 對 venue 的名字內容 error 應恆為 0;非零時那筆是手改或舊 binary 寫的,修法是人改 YAML(同 dated-variant 的立場,不猜、不靜默修;訊息自 R6 起逐條說改什麼)——**第 1 條(canonical 形)的確定性那一類自 #575 起另有機械修復面** `akashic repair-venue-names`(歷輪補記與量測見量測文件第 25 列) | +| 26 | **零實例,而它守的是三個閘共同的前提——閘擋工具面、守衛掃非工具面**(#554 R10 verify 第 2/5/10 列,Claude 代裁 D28:同一 work 兩條 key 邊指同一 venue。D25(repoint/demote 拒)、D27(repoint 後不得出現)、D28(apply 不得造出)三個閘守的都是「配對只由一條邊實例化」,而 store 另有手改與舊 binary 兩個寫入者不經任何閘;R10 verify 用真 binary 三次造出這個形(`apply` 兩條同刊名的 literal 邊、`repoint` 改指到本 work 已有邊的 venue),`validate`/`doctor`/App 全綠,使用者直到想 demote 才知道。) | ✅ **寫(warning 級、在 `Entry.validate()`)** | 第 25 列的理由是「入口有五個」而守衛裝在 store 邊界;這一列的入口在 R11 之後只剩**不經工具的兩個**(手改、舊 binary)——venue 合併**不是**入口,而那兩個正是閘擋不到的。**守衛與閘是同一條不變式的兩半**:閘讓工具面造不出這個形,守衛讓已經在庫裡的看得見——少了守衛,D25 的拒絕就是使用者第一次知道自己的 store 走進這一格的時刻,而那時他要做的操作已經被擋。**severity 是 warning**,理由與第 13 列同型:記錄合法可載入(兩條邊各自指向存在的 venue),失效的是判定逆轉的前提;升 error 會讓一筆只能手改才修得好的 work 擋住自己所有的寫入,而處置面當時還不存在。**觸發條件可檢查**:計數應恆為 0;非零時先看那筆是不是 R11 之前的 binary 寫的(`apply` 的兩次歸戶)——修法是刪掉多餘的邊,以 `resolve-venues --drop-venue` 刪(歷輪補記與量測見量測文件第 26 列) | +| 27 | **零實例,而它是同一句不變式的另一半——第一半的守衛結構上照不到它**(#554 R13 verify DA 第 16 列,Claude 代裁 D36:同一 (venue, work) 上 ≥2 個正規化後不同的 confirmed literal。§3.5 把配對唯一性寫成一句 normative 的兩半,第 26 列守的是第一半(work 的邊),第二半住在 venue 的 references 上——`Venue.validate()` 不掃 verdict 配對、`StoreHealth` 沒有對應項,全樹「個不同的 confirmed literal」只出現在 `confirmedLiteral` 的**拒絕訊息**,零 validate/doctor 呼叫端。) | ✅ **寫(warning 級、在 `Venue.validate()`)** | 第 26 列的理由是「閘與守衛是同一條不變式的兩半」——閘擋工具面、守衛掃非工具面;這一列是**同一條不變式的另一半要有自己的守衛**:兩半在偵測面上互斥(造出第二半的路徑不點亮第一半的燈),所以第 26 列的量測「應恆為 0」只兌現了一半,而本檔自己的紀律是「寫出條件而不是只寫數字」。裁決有兩半:寫(零實例但實例已在 verify 裡被造出、且 R14 之前純工具面造得出),**以及不改成只修散文**——DA 給的另一個出口是把 §3.5 那句改成「第二半今天沒有掃描面」,那等於把一個已知可達的狀態留在「只有 demote/repoint 撞上時才知道」。**severity 是 warning**,理由與第 26 列同型:記錄合法可載入,失效的是判定逆轉的前提(D23 從此拒那筆 work 的 demote/repoint);處置是手改 YAML 留一筆——#572 的 `--drop-venue` 刪的是邊不是 verdict,這一格至今沒有工具面,升 error 會讓一筆只能手改才修得好的 venue 擋住自己所有的寫入。**鍵不是一把是兩把,掃描兩類都掃**:以位元組相異的 confirmed 分組,正規化後不同=不變式的違反、只差位元組=重複的判定記錄(工具面以 `verdictEqualityKey` 去重、寫不出它),兩類同一家族前綴(`Venue.confirmedLiteralAmbiguityPrefix`)、措辭分開;`StoreHealth.confirmedLiteralAmbiguities` 有計數;每筆 venue 最多列 20 筆 work、其餘一句概括(第 16/18 列:讀取路徑上對未信任內容跑,鄰居剛為此加了上限)。**觸發條件可檢查**:兩個計數應恆為 0;非零時那筆是 R14 之前的 work 合併、手改或舊 binary 寫的——修法是刪掉不屬於那條邊的 confirmed verdict(歷輪補記與量測見量測文件第 27 列) | +| 28 | **零實例,而它守的是另一條裁決刻意留下的狀態——「零資訊損失」把一筆判定從當場刪改成留著,而留著的那一格沒有任何面看得見**(#554 R22 verify security 第 14 列,Claude 代裁 D64:同一筆記錄對同一配對持有 ≥2 筆同 field 的判定記錄(`verdictEqualityKey` 相同)。2026-09-16 實測 live store:`重複的判定記錄` **0**。) | ✅ **寫(warning 級、在 `StoreHealth`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列多一件事:**可達性是本 change 的另一條裁決刻意造出來的**——D62 為了不刪判定而留下這個狀態,代價正是它在中間那一格看不見。守衛不是在替 D62 擦拭,是 D62 的另一半:留著而不出聲,等於把「rename 當場刪、有回報」換成「合併時刪、回報在另一個命令的輸出裡」,後者未必更好。severity 是 warning,理由與第 13/26 列同型:記錄合法可載入,兩筆各自都是一個人的判定,error 會讓一筆只能手改才修得好的記錄擋住自己所有的寫入。**觸發條件可檢查**:計數應恆為 0;非零時先看是不是剛 rename 過——那兩筆在舊鍵上就已經並存,處置是留一筆或改其中一筆的 value。(歷輪補記與量測見量測文件第 28 列) | +| 29 | **零實例,而它守的是另一個守衛的前提——一個常數必須大於等於全樹每個 sink 的輸出上限**(#554 R31/R32:`ErrorDisplay.inputScalarCeiling`=4,096。D83 讓逃脫只讀每行前 ceiling 個輸入 scalar,等價性的前提是「任一 sink 的輸出上限 ≤ ceiling」——每個輸入 scalar 至少產生一個輸出 scalar,所以輸出上限之內的字串不受截短影響;前提一破,被 ceiling 截短的行會冒充完整的行、且每個 sink 都綠。) | ✅ **寫(四族各自有地板;第四族可為空)** | 第 16 列的理由是「裁決依賴守衛」(門檻到了才重開);這一列更緊:**另一個守衛的正確性依賴它**——`testInputCeilingDoesNotChangeClippedOutput` 證的是「有界=無界」,而那個等式只在 sink 上限 ≤ ceiling 時成立,本列守的就是那個前提。零的來源與前面各列都不同:不是還沒發生、不是掃乾淨了、不是在這台機器上,是**兩個數字今天恰好對齊**(最大 sink 4,096=ceiling 4,096),而改任一邊都是一行 diff、不會有任何測試因此變錯(每個 sink 自己的測試只看自己的數)。**地板要分族**:R31 的單一地板由 57 個 `displaySafeError` 站點撐著,`maxLineLength:` 那一半通過的方式是「什麼都沒找到」——與第 3 列「未涵蓋不得冒充通過」、R29 NC4「空掃描不是通過」同一個形狀,在同一支守衛裡再犯一次。**觸發條件可檢查**:超過 ceiling 的站點數應恆為 0;非零時要一起抬 ceiling(並重量線性),不是放寬那一個 sink(歷輪補記與量測見量測文件第 29 列) | +| 30 | **零實例,而上限守的記錄設計上只增不減**(#619 R1 verify security:未決腿的三個上限——一次 200 個 id、20 個 digest、單句說明 4,096 位元組。) | ✅ **寫(整批拒絕、零寫入、不截斷)** | 第 18 列的上限守的是一次遞進來的字串,並夾在兩個量測錨點之間;這一列多一件事:**被守的記錄設計上不退役**——未決是查證歷史,只收位元組相同的重複,一個會迴圈的呼叫端每跑一次就多留一筆。說明與 digest 的上限各有量測錨點(687 位元組的約 6 倍、3 個的約 7 倍);**id 數 200 沒有可量的母體**——它限的是一次呼叫的批次大小、不是記錄內容,取 CLI 單批 triage 的量級,這一格是推估不是量測。**誠實邊界**:上限只約束一次呼叫、不約束累積——累積由預算 warning 出聲:venue 側是第 16 列,person 與 organization 側是第 31 列(#645;organization 側由 #643 的未決腿加入)。**觸發條件可檢查**:任一筆 reference 的說明或 rests-on 逼近上限時重開。(歷輪補記與量測見量測文件第 30 列) | +| 31 | **零實例,而它是一條已有守衛的曲線換了持有者**(#645:person/organization 的記錄檔逼近讀取上限。第 16 列守 venue 側;person 側的增長來源是一個人的著作數,#619 起多了不退役的 `resolution-undecided`(一筆說明可寫到 4,096 位元組),#643 起 organization 同。) | ✅ **寫(warning 級、在 `StoreHealth`,門檻與量法同第 16 列)** | 第 16 列的理由是「裁決依賴守衛」——暫不改形狀之所以可接受,是因為漲到門檻時工具會出聲;這一列是**同一條曲線的另一個持有者一直沒有那盞燈**,而讓它開始漲的是一個新面(未決腿)。所以理由不是第 1 列的「不寫就沒有跡象」,是**第 30 列自己寫下的誠實邊界**(上限只擋單次呼叫、不擋累積)要有工具面兌現,否則那句邊界就是一句沒有後續的散文。**門檻不另立常數**(`recordFileWarningBytes` 兩族共用——同一個預算的第二份描述會分岔)。warning 仍只在讀取面計算:它管漸進的增長,單步的跳躍由寫入閘擋。**觸發條件可檢查**:計數應恆為 0;非零時先看那筆記錄的 `resolution-undecided` 筆數(歷輪補記與量測見量測文件第 31 列) | +| 32 | **零實例,而謂詞寫寬的方向是 Unicode——前件看起來是「數字」,實際是「任何書寫系統的數字」**(#589:ISSN/ISBN/ORCID 共用的 `idCompact` 以 `CharacterSet.alphanumerics` 過濾、以 `wholeNumberValue` 取值,兩者都認 Unicode 數字;全形與阿拉伯-印度數字寫成的號過得了 mod-11、原樣成為 `normalized`。) | ✅ **寫(在 `idCompact`,非 ASCII 的英數字元換成必定失敗的 `?`;已入庫的會在載入時被隔離)** | 第 2 列的理由是「前件寫寬了會誤傷」;這一列相反——前件寫寬了會**放行**,而放行的東西看起來是對的(第 4 列的偽裝性):一個全形寫成的 ISSN 過得了 check digit,而 `identity-is-judged-not-matched` 讓識別碼相等**單獨**就能做身分判定,所以它是一個看起來像決定性證據的假號。修在 `idCompact` 而不是三個呼叫端,因為三個呼叫端共用它,修一處就全部關掉;**換成 `?` 而不是濾掉**,因為濾掉會讓夾在 ASCII 號裡的非 ASCII 字元被靜默吞掉、號照樣通過。**已入庫的處置是隔離而不是 warning**:這一族在 decode 時本來就以 `ISSN(v) != nil` 驗證、不合法整檔隔離(`IdentifierYAML.make`),收緊謂詞之後非 ASCII 號自然落進同一條路,與其他不合法的號同級;零實例所以不需要遷移(`no-compat-fallback`)。**觸發條件可檢查**:非 ASCII 數應恆為 0;非零時那筆在新 binary 下會被隔離,修法是把號改寫成 ASCII(歷輪補記與量測見量測文件第 32 列) | +| 33 | **一半零實例、一半兩筆,而同一個檢查只寫了三分之一**(#579、#652:work 的三種參照邊——`.key` 作者、`.organization` 作者、`venues[].key`——只有第一種有懸空檢查(`crossRecordIssues` 的「作者 key 沒有對應的 people 檔」)。2026-09-26 新 binary 對 live store:懸空的 venue 邊 **0** 筆;懸空的團體作者 **2** 筆(`anon1954technical` 的 `American Psychological Association`、`anon2014pisa` 的 `OECD`——2026-08-20 #340 批次手寫,payload 是名稱不是 key,而且不是合法 StoreKey)。) | ✅ **寫(warning,在 `crossRecordIssues`;key 不是合法 StoreKey 時訊息另說「不可能對應任何記錄」)** | 第 13 列的理由是「跡象住在錯的地方」;這一列更早一步——**跡象根本不存在**,而同形的檢查就在隔壁:person 那一半有、另外兩半沒有,是檢查寫的時候只有 person 那一種邊。所以它不是新守衛的裁決,是把既有守衛的前件補完(第 2 列的「前件多寬」,方向是寫窄了)。**severity 是 warning**,理由與既有的懸空作者相同:懸空參照讓畫面少東西、不毀資料,而解析中途本來就會有;#579 另提的「error、與 person/organization 的 key 檢查對齊」不採——那些是**記錄自身**的 key,load 就 quarantine;這裡是**參照**,quarantine 整筆 work 會讓一條壞邊把整篇作品藏起來。非法 key 另附說明,因為記錄的 key 在 load 時就驗過,一條非法 key 的邊不是「目標還沒建」而是「永遠建不出來」。**觸發條件可檢查**:venue 邊的數應恆為 0;團體作者那兩筆的處置是人的判斷——建 org 記錄再改 key,或改回 `literal:` 交給 `resolve-organizations`(目前沒有把 `.organization` 退回 literal 的工具面,只能手改 YAML)(歷輪補記與量測見量測文件第 33 列) | +| 34 | **零實例,而第 33 列補完 work 側之後,同一種懸空在 person 與 organization 側仍然沒有檢查**(#660:第 7 條邊 `Person.profile.affiliations` 與第 8 條邊 `Organization.parents` 的 `.key` 指向不存在的 organization。) | ✅ **寫(warning,在 `crossRecordIssues`;key 不是合法 StoreKey 時同第 33 列另說「不可能對應任何記錄」)** | 第 33 列的理由是「同一個檢查只寫了三分之一」;這一列是**那一次補完只補了 work 那一側**——封閉列舉裡指向 organization 的邊有五條——第 1 條的 `.organization` 作者、第 7 條、第 8 條,以及第 9 條(divergence 候選的 organization shape)與第 13 條(verdict value 的 `organization:` holder);後兩條早有檢查(懸空候選的 warning、死 verdict 掃描)。**severity 是 warning**,理由同第 33 列:懸空參照讓畫面少東西、不毀資料,quarantine 整筆 person 會讓一條壞隸屬把整個人藏起來。**literal 不報**:未歸戶的隸屬是誠實狀態(`literal-first-then-key`)。**觸發條件可檢查**:兩個數應恆為 0;非零時處置是人的判斷——建那筆 organization,或把 key 改回 `literal:` 交給 `resolve-organizations`(歷輪補記與量測見量測文件第 34 列) | +| 35 | **零實例,而可達路徑是一次相等定義的替換打開的**(#582:同一筆記錄裡 ≥2 筆**非判定** reference 彼此 canonical 相等——只差 NFC/NFD,或位元組完全相同。) | ✅ **寫(warning,`StoreHealth.duplicateReferences`;不設 per-record 上限)** | 第 28 列的理由是「可達性是本 change 的另一條裁決刻意造出來的」;這一列同形而**對象不同**:那一列是 rename 為了不刪判定而留下的判定記錄重複,這一列是位元組相等的去重為了不丟位元組而收下的非判定 reference 變體——同一個取捨(零資訊損失)在兩類欄位上各開一格,第 28 列只照判定那一格。issue 給的另一個出口是「裁決變體是合法並存、不報」:不採,因為並存的兩筆對同一件事說了兩次,合併時 `fieldsLostByMerging` 以位元組比對,倖存者只有其中一種拼法時合併會被擋下(issue 記載的路徑),那時才第一次出聲、而且出在錯的操作上。**位元組完全相同的也報**、措辭分開,一組裡兩種都有時兩件事都說。**不設上限**:一組一則,則數至多是該記錄非判定 reference 數的一半——線性家族(與死 verdict 同類),不是第 28 列那種組合式家族。**severity 是 warning**:兩筆都合法,處置是人決定留哪一筆。**觸發條件可檢查**:計數應恆為 0;非零時先看是不是 NFC/NFD 變體——那是位元組相等的去重收下的,確認兩筆說的是同一件事後留一筆。(歷輪補記與量測見量測文件第 35 列) | +| 36 | **零實例,而製造它的通道是新開的,且它的錯看起來是對的**(#588:同一個 ISSN 掛在 2 個以上 venue。ISSN 的 mod-11 檢查碼擋得住亂碼,擋不住**合法但屬於姊妹刊的號**——JRSS-A/B/C 在模糊搜尋下都會命中,而 #556 起 `akashic-verify-venue` 把查到的 ISSN 寫進 venue。) | ✅ **寫(warning,在 `crossRecordIssues`,以正規形比對)** | 第 4 列的理由是「錯誤的偽裝性」——猜錯的結果看起來是對的;這一列同形而更尖:`identity-is-judged-not-matched` 讓識別碼相等**單獨**就能做身分判定,所以一個掛錯刊的號不只是一筆錯的欄位,是一個看起來像決定性證據的假身分宣稱,之後以它為據的攣生判定都建在上面。**severity 是 warning**:兩本刊共用一個號可能是其中一本記錯,也可能兩筆是同一本刊(那時要走合併),那是判斷不是修檔;訊息把兩個出口都說出來。**移除面與守衛同批**:只有守衛而沒有移除面,warning 出現時唯一的出路是手改 YAML(`replace-endnote-and-zotero` 第 4 條)。**觸發條件可檢查**:計數應恆為 0;非零時先查兩本刊是不是同一本,不是就移除記錯的那個號(歷輪補記與量測見量測文件第 36 列) | +| 37 | **零實例,而舊稽核的謂詞對新長出來的寫入面結構上不可能為真——而正確的謂詞不存在**(#658:CLI 的每一個葉命令、`resolve-people`/`resolve-venues`/`resolve-organizations` 的每一條腿,都要在 `Sources/akashic/WriteGateRulings.swift` 的裁決表有一格——過閘、不閘並寫出這一格自己的理由、或不寫 store。舊稽核(`DestructiveTargetGateTests`)只認布林的 `--apply`/`--reject` 宣告:預設就寫的命令與逐 id 的寫入腿長出來時它不會紅。) | ✅ **寫(測試:執行期命令樹雙向比對、不閘的格理由非空、過閘的集合由表現算且與閘的呼叫點一致、逐腿以真 binary 驗)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」——那一列換一個謂詞(棘輪)就解決了;這一列換不了,因為**「該不該閘」是裁決不是性質**:#653 的「只閘不可逆的」沒有一句可機械判定的「可逆」(`resolve-people` 的逐篇判定沒有把作者位退回 literal 的工具面,而它不閘)。所以守衛不判斷對錯,只要求**每一格都有裁決、不閘的寫出自己的理由**,並釘住兩件可機械核對的事:表說過閘的命令原始碼真的呼叫閘、反之亦然;逐腿的裁決對真 binary 成立(過閘的腿未指名目標時被擋、不閘的腿的樣本走得到參數檢查之後)。這是第 7 列的形:它兌現的是一個嚴格更弱的命題——通過不代表理由對,理由仍是人寫的。**觸發條件可檢查**:兩個差集應恆為空;非空時在表加一格——過閘、不閘(寫理由)或不寫 store,不從鄰居類推(歷輪補記與量測見量測文件第 37 列) | +| 38 | **零實例,而它守的是讀取面對「哪一筆」的回答——兩筆同 kind 同 key 的記錄會讓一份「單筆明細」變成兩筆的聯集**(#581:`validate --owner`/MCP `akashic_doctor` 的 `owner` 以 (kind, key) 定位一筆記錄,per-record 問題以 (族名, key) 篩出——同 kind 同 key 兩筆以上時篩出的是它們的聯集,而輸出的標題說「這一筆」。) | ✅ **寫(拒絕、不猜——訊息列出各筆的檔名 UUID;CLI exit 1、MCP 回錯誤)** | 第 4 列的理由是「錯誤的偽裝性」——猜錯的結果看起來是對的;這一列同形而位置不同:前面各列守的是 store 的內容,這一列守的是**讀取面對「哪一筆」的回答**。聯集裡的每一則都是真的,錯在標題——而使用者正是為了看「那一筆」才指名的;在全庫輸出裡兩筆同 key 至少還有 citekey/person key 的跨記錄 error 旁證,在單筆明細裡什麼都沒有。**不選「兩筆都列、各自標記」**:`OwnedIssue` 以 (族名, key) 為身分,兩筆同 key 的記錄在那個結構裡本來就分不開,要分得開得讓每一則帶 UUID——那是換 `OwnedIssue` 的形狀、動到三個面,為一個零實例的形狀付出不成比例的代價。**觸發條件可檢查**:同 kind 重複 key 應恆為 0;非零時先修重複(citekey 與 person key 由 `validate` 的跨記錄 error 指路,venue 與 organization 要看檔),再用 owner(歷輪補記與量測見量測文件第 38 列) | +| 39 | **零實例,而它守的是單步的跳躍——一次寫入把一筆讀得回來的記錄推過讀取上限,漸進的預警看不到**(#648,使用者 2026-09-28 裁決兩件都做:(a) 寫出的位元組上限是讀取上限 `AliasEventBudget.maxBytes`(8 MiB),`writePathMultiplier` 的 2 倍只給**不增長**的改寫——寫出的位元組 ≤ 目的檔目前的位元組(`writeByteLimit(replacing:)`);(b) resolve-people 的 judge/refute 理由上限 4,096 位元組,與未決腿同一個常數(`maxStatementBytes`)。) | ✅ **寫(encode 層具名拒絕、零寫入;寬限只給不增長的改寫;多檔寫入面在任何檔落盤之前逐筆 preflight;judge/refute 理由整批拒絕、不截斷)** | 第 31 列的理由是「同一條曲線換了持有者」,它自己寫下的誠實邊界是「warning 只在讀取面、一次夠大的寫入從門檻之下直接跳過讀取上限」——這一列兌現那句邊界的另一半:**漸進的增長由第 16/31 列出聲,單步的跳躍要在寫入端擋**。與第 18 列(入口的字串上限)同形而對象不同:那一列限一個字串,這一列限寫出的整筆記錄,而兩種上限各自都不夠——字串上限擋不住很多個字串(未決腿一次 200 × 4,096 位元組,控制字元經 YAML 跳脫成 4 倍,約 3.3 MB),記錄上限擋不住多檔面的撕裂。所以裁決有三半:寫入閘在 encode(`AliasEventBudget.checkWriteSize`,六個 encoder 共用,拒絕具名記錄與兩個位元組數)、多檔寫入面逐筆 `preflightWrite`(writeX 在寫入當下跑的每一道,含 #631 目的檔與 encode)、judge/refute 的理由在輸入端先擋。**觸發條件可檢查**:超過讀取上限的檔數應恆為 0;最大檔逼近上限時第 16/31 列的 warning 先響,那時同時查是否有單一呼叫帶進大量內容(歷輪補記與量測見量測文件第 39 列) | +| 40 | **零實例,而拒絕一直都在——只是問在錯的時間點**(#641:legacy 殘留(`entries/.yaml`/`people/.yaml`)加上三者之一——未受 git 追蹤或有未 commit 的修改、目的檔 `entities/.yaml` 被隔離或是另一種記錄、同一個 id 兩份並存——的 work/person,寫入當下一定被 #631 的前置拒絕;那個拒絕發生在多檔寫入者的中途時,前面幾筆已經落盤。) | ✅ **寫(load 時以同一組前置判斷標註,併進 `unlocatableCitekeys` 與新增的 `unlocatablePersonKeys`;validate 報 warning)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」;這一列的謂詞沒有問題——#631 的前置判斷對這些記錄答得完全正確,錯在**問的時間**:寫入當下才問,「拒絕」到達時同一個操作的前幾筆已經落盤。所以修法不是換謂詞,是**把同一組判斷(`assertEntitiesDestination`、`legacyMoveCandidate`、`filesNotSafelyRecoverable`)搬到 load 再問一次**,不另寫一份(`no-compat-fallback` §同一件事只能有一份描述);答案記在記錄旁(`FileSituation`:不參與相等、不序列化),併進「無法唯一定位」的定義,#627/#628 已接好的各面閘因此在第一次寫入之前就拒絕或具名略過——那些面一行不改就拿到第 3 類;person 側的寫入者(judge/refute/apply/reject/undecided、org 以 person 為 holder 的各腿、App 的 accept、authorize-names)另接新的 `unlocatablePersonKeys`。**severity 是 warning**:記錄讀得到、內容完好,擋的是寫入;升 error 會讓 `assertNoCrossRecordErrors` 擋下不相干的改名與合併。**觸發條件可檢查**:legacy 殘留數應恆為 0;非零時 `akashic validate` 逐筆說哪一筆寫入時會被拒、為什麼,處置是人的——把 legacy 檔 commit(之後寫入會搬移它)、刪掉兩份裡的一份、或修好被隔離的目的檔(歷輪補記與量測見量測文件第 40 列) | +| 41 | **零實例,而同一個值在兩層有相反的身分——作為位址合法、作為引用不合法**(#654:空內容的 digest,即 0 byte 的 SHA-256 `sha256:e3b0c442…`。) | ✅ **寫(error 級、在 `ProvenanceReference.isValidDigest`;位址層另用只看形狀的 `isWellFormedDigest`)** | 第 32 列的理由是「前件寫寬的方向是放行」;這一列同形而放行的不是假號,是一個真的、形狀完全合法的值——它的錯不在形狀,在於它是常數、對所有空輸入都相同,不指認任何一份存檔,所以只看形狀的檢查全部報綠。**閘放在謂詞本身而不是 `enrich` 入口**(使用者 2026-09-28 裁決):引用端的寫入面不只一個、再加上載入,逐面補會重演第 25 列「入口有五個」的形;修在共用謂詞一處全部關掉,同第 32 列。**裁決的第二半是把謂詞拆成兩個**:`sources/` 的路徑解析與 `index.jsonl` 的文法、以及 `AuthorListFingerprint` 的持久形只看形狀。live store 的 index 有一列指向空 blob,若 index 文法也排除它,那一列會被判成無法解析,而 index 有無法解析的行時 `store-source` 對**所有**新內容拒寫——一次為了守零實例而加的收緊會把一個能用的 store 關掉。**已入庫的處置是隔離**:digest 在 decode 時本來就驗、不合法整檔隔離,收緊謂詞之後空內容 digest 自然落進同一條路(同第 32 列),零實例所以不需要遷移(`no-compat-fallback`);訊息說「0 byte」而不是「形狀必須是 …」——對一個形狀合法的值說形狀錯是假話。**觸發條件可檢查**:指向空內容 digest 的值應恆為 0;非零時那筆在新 binary 下會被隔離,修法是重新取得內容、`store-source` 存檔後改引用新的 digest(歷輪補記與量測見量測文件第 41 列) | +| 42 | **零實例,而重複 key 在兩個形狀有檢查、在另兩個形狀沒有——沒有檢查的那兩個,讀取時不是少報,是崩潰**(#669:兩筆 venue 或兩筆 organization 持同一個 key。2026-09-28 實測 live store:venue 485、organization 13,同 kind 重複 key 0 組。) | ✅ **寫(error 級、在 `crossRecordIssues`;讀取端不 trap;以 key 定位寫入的兩個面拒絕;源碼掃描守衛)** | 第 33 列的理由是「同一個檢查只寫了三分之一」;這一列同形而後果更重:沒有檢查的那一格讓讀取端**崩潰**——`uniqueKeysWithValues` 對重複鍵是 precondition failure,不是可捕捉的錯誤,一筆手改的 YAML 就讓 `export-bib` 與 MCP server 整個停掉。**severity 是 error**,與 citekey、person key 同級:key 是 `.key(…)` 參照、verdict holder 與每一個寫入面的定位依據,重複時改名與合併(`assertNoCrossRecordErrors`)要先停;第 38 列的定址拒絕原本是這兩個 kind 的重複**唯一**出聲的地方,它的訊息自本列起指路 validate。**寫入面拒絕**:attribute-org(把 verdict 寫進那個 organization)與 migrate-identifiers(把 ISSN 搬進那個 venue)在重複時整批拒絕、零寫入——同 #627 對 citekey。**觸發條件可檢查**:重複 key 應恆為 0;非零時先在 YAML 改掉其中一筆的 key——這個 error 在時改名與合併都先停,所以合併不是第一步;若兩筆其實是同一本刊,改完之後再走 record-divergence/resolve-divergence(歷輪補記與量測見量測文件第 42 列) | +| 43 | **零實例,而它守的是合併把沿革安靜地拿掉**(#565:venue 合併時,被併者的名字段與倖存者(或先併入的被併者)同名,時間、source、note 不完全相同,又不能並存。先前 `mergedVenueKeeper` 以 `TemporalValue(value:)` 重建被併者的名字,時間、source、note 一律丟掉,同名段被 canonical 濾除而不出聲。) | ✅ **寫(不能並存的同名段具名拒絕、零寫入;其餘名字整段搬)** | 第 22 列替沿革在 spec 裡留了位置;這一列守那個位置的**另一端**:合併正是把落進位置的沿革拿掉的那一步,而結果是一筆看起來健康的 venue(第 4 列的偽裝性)。哪一段的時間才對是判斷,合併不猜,所以不能並存的同名段具名拒絕;能並存的整段搬(時間、source、note 都保留),被併者原本在 variant 的才標 variant,其餘併入後未標(#554 D1 的論證用在合併端)。**觸發條件可檢查**:帶時間、source 或 note 的名字段數 > 0——那時第 22 列的觸發條件也一起到期(歷輪補記與量測見量測文件第 43 列) | +| 44 | **零實例,而設閘的判準早就寫在程式裡,只是答案剛變**(#567:format < 14 的 store 寫出帶 `variant` 的 venue。`migrate-venue-variants` 退場刪除之後,「有沒有 pre-bump 遷移」的答案從「有」變成「沒有」;`LibraryStore` 的註解本來就寫著「#567 退場時一併裁」。2026-09-29:live store format 18,format-13 的 store 零實例) | ✅ **寫(同 `paginated` 的閘:format < 14 且 variant 非空即拒)** | 與第 1 列同形——不寫的話,format-13 的 binary 會把異寫當一般名字顯示,而沒有任何跡象;差別在於這道閘不是新的判準,是既有判準在遷移退場後的答案。**誠實邊界**:`fmt` 不經 `assertVenueWritable`,不受這道閘管。**觸發條件可檢查**:`VenueStoreTests.testVariantWriteRefusedBelowFormat14` 釘住閘;live store 的 format 見量測文件(量測見量測文件第 44 列) | +| 45 | **零實例,而上限守的是一個新開的通用寫入面**(#587:`update-venue --references`/MCP `references` 一次 200 筆、statement 4,096 位元組、rests-on 20 個、其餘字串 65,536 位元組;`add_issn` 對認不出的角色與角色衝突拒收。) | ✅ **寫(整批拒絕、零寫入、不截斷)** | 與第 30 列同形——上限只約束單次呼叫,累積由第 16 列的預算 warning 出聲;錨點取既有常數(`maxStatementBytes`、`maxRestsOnPerCall`、`AddOnlyEnrichment.maxValueBytes`),不另立數字。一次 200 筆同第 30 列的 id 數:限的是批次大小,沒有可量的母體,是推估。角色的拒收比遷移嚴:遷移面對既有資料,對認不出的寫法保留原值、報 warning;寫入面不寫一個 `validate` 會報的值。(歷輪補記與量測見量測文件第 45 列) | +| 46 | **零實例,而路由用的字典安靜地選一筆**(#610:同一個 `(library_id, zotero_key)` 被兩筆以上 entry 宣稱,主來源與附加來源都算。三張路由索引都是後寫覆蓋先寫的字典,匯入只更新其中一筆,沒有任何地方說出來。) | ✅ **寫(warning 在 `crossRecordIssues`;匯入端不更新、不新建,報 `ambiguousSourceClaims`)** | 與第 33/42 列同形:隔壁的重複(DOI、標題)有檢查,宣稱者的重複沒有;後果是把 Zotero 的書目欄位寫進別的記錄。**severity 是 warning**:升 error 會讓 `assertNoCrossRecordErrors` 擋下改名與合併,而合併正是修復的出路。可達路徑:手改、舊 binary、#607 修掉的 legacy 認領。**觸發條件可檢查**:宣稱者 ≥2 的來源數應恆為 0(歷輪補記與量測見量測文件第 46 列) | +| 47 | **零實例,而同一個形狀已經真實發生過一次——零來自規則還不存在,不是來自清理**(#642:規則型/文件型 library 的既有成員不符規則、規則指向不在庫的 venue 或文件,以及未標性質的 library。) | ✅ **寫(warning;未標性質是 per-record,不符規則與懸空規則是跨記錄,每個 library 一則、點名前 5 筆)** | 第 13 列的零是「清理過之後的零」;這一列的形狀發生過一次,而今天的零來自 live store 的 4 個 library **都還沒標性質**——規則根本不存在,當然沒有東西不符。一旦有人 `set-kind`,零就取決於兩件事:`library add` 的逐筆比對(寫入端的閘,擋工具面),以及這盞燈。燈照的是閘擋不到的來源:手改 YAML、`set-kind` 當下就已經不符的既有成員(set-kind 只列出、不移除),以及規則建好之後依據才出問題的形狀(venue key 變成重複、文件被刪、排除的 citekey 被刪,#642 R1 verify 補)。這是第 26 列「閘與守衛是同一條不變式的兩半」的形。**severity 是 warning**:成員關係錯了不毀資料,error 會讓 `assertNoCrossRecordErrors` 擋下不相干的改名與合併。**未標性質也報**:未標就沒有依據,`library add` 一律拒絕;不報的話使用者只會看到 add 被拒,看不到原因住在哪裡。**觸發條件可檢查**:不符數應恆為 0;非零時用 `library check ` 看是哪幾筆、為什麼,處置是人的——remove,或規則寫錯了就 `set-kind` 改規則(歷輪補記與量測見量測文件第 47 列) | +| 48 | **零實例,而角色是新開的寫入通道每次查證都會寫的東西**(#587 R1 verify:venue 合併不比同一個 ISSN 的角色——被併者有角色而倖存者沒有、或兩邊不同(含遷移留下認不出的寫法),合併之後角色安靜消失。#587 起 `add_issn` 收角色、`akashic-verify-venue` 查證時會寫;同一輪 `field: issn`/`names` 的 reference 開放寫入,而合併原本對它們一律拒絕。) | ✅ **寫(角色遺失具名拒絕、零寫入、出路逐格;`field: issn`/`names` 的 reference 隨合併逐位元組搬)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形——角色掉了之後記錄仍是一筆看起來健康的 venue,`validate` 不會報——而可達性是 #587 的寫入面造成的(第 23 列的形),所以裁決與新面同批。角色比 `qualifierRaw` 的位元組(認不出的寫法也算,否則遷移留下的 `Online` 會被安靜覆寫);被併者有而倖存者沒有、或兩邊不同,都具名拒絕,出路各寫(`--add-issn "號 (角色)"`;角色不同時先 `--remove-issn` 再 `--add-issn`,並說明會連帶刪那個號的 reference;認不出的寫法手改 YAML)。reference 那一半不是拒絕而是搬:`field: issn`/`names` 的值在倖存者上存在就逐位元組搬(`byteExactKey` 去重),乾跑與實跑共用 `venueReferenceCarry` 一份;`paginated`、手改的 `authorized`、`note` 仍拒絕——那幾格沒有工具面能搬。**觸發條件可檢查**:含 venue 候選的 divergence 數 > 0 時這兩道才會被走到;守衛測試是 `VenueMergeReferencesAndISSNRolesTests` 的角色四支與 reference 四支(歷輪補記與量測見量測文件第 48 列) | +| 49 | **零實例,而形狀是一次裁決之後才合法、先前哪一面都看不見**(#609:附加來源已刪除而主連結仍在,以及沒有主來源、附加來源全部已刪除——#605 R1 裁決之後兩者都是合法狀態,但 `doctor`、index 的 `orphaned` 欄、App 裁決台都只看主來源。) | ✅ **寫(列表面:`StoreHealth.orphanedCitekeys`/`orphanedAdditionalSourceCitekeys`;判準只有 `Entry.zoteroLinkState` 一份;處置在 App 裁決台)** | 第 46 列的理由是「路由字典安靜地選一筆」;這一列與它成對而守的東西不同:**合法狀態的可見性**。前面各列的零是壞形狀還沒發生,這一列的形狀是合法的,缺的是能看見它的判準——`.orphaned` 的母體在 #605 R1 之後擴大,每個 orphan 介面的判準卻還停在只看主來源,於是一筆作品已經從每一個連結的 library 消失,卻在每個介面都看不見。不寫的代價與第 1 列同形(第一次發生時沒有跡象);成本是一個三值判準。它不是守衛(沒有 severity、不進 `validate`),所以這一列記的是「為什麼列表算數」。**觸發條件可檢查**(腳本見量測文件):兩個計數任一 > 0 即出現在 `akashic doctor`(`orphaned:`/`orphaned additional sources:`)、MCP `akashic_doctor`(`orphaned`/`orphanedAdditionalSources`)與 App 側欄(歷輪補記與量測見量測文件第 49 列) | +| 50 | **零實例,而檢查只問「在不在」,沒問「是不是一份檔」**(#614 b11c verify R1:`update-entry --add-source`/MCP `add_sources` 要求 digest 的內容已存在本機 `sources/`,而先前用 `fileExists(atPath:)` 判斷——它對同名的**目錄**也回 true;blob 被換成目錄或 symlink、index 那一列還在時,add-source 會宣告一份讀不到的副本而不出聲。) | ✅ **寫(`SourcePresence.notRegularFile`,以 lstat 的類型判定;目錄、symlink、特殊檔案具名拒絕,乾跑與實跑一致、零寫入)** | 第 1 列的理由是「失敗的不可見性」;這一列同形——不寫的話,那個形狀第一次出現時 add-source 照常回報成功,而宣告的那份副本打不開。與第 15 列(本機缺承重存檔)相鄰而不同:那一列問的是位元組在不在這台機器上,這一列問的是在那個位置上的東西是不是一份檔。前件精確(lstat 的類型不是 regular),成本是一個屬性檢查。**symlink 也拒**:`sources/` 由 `store-source` 寫成普通檔,store 沒有任何一條路徑會寫出 symlink;若日後有意用 symlink 共用 blob,要回來改這一列。**誠實邊界**:只看類型、不重新雜湊內容(位元組與 digest 是否相符不在這道檢查裡)。**觸發條件可檢查**(腳本見量測文件):非普通檔數應恆為 0;非零時先查是誰放的,再依 add-source 的拒絕訊息處置(歷輪補記與量測見量測文件第 50 列) | +| 51 | **零實例,而「沒有東西可檢查」與「檢查過且乾淨」在輸出上分不開**(#629 移植 `rule-coverage` 成 `akashic-guards rule-coverage`:某個 plugin 根的 `rules/` 底下一條 `.md` 都沒有時,舊 shell 版因 glob 不命中而**意外**變紅,Swift 版的空迴圈會印綠燈。2026-09-29 量測:兩個 plugin 根(`plugin`、`plugins/akashic-discovery`)各 2 條規則,空的 `rules/` 0 個) | ✅ **寫(`RuleCoverage.swift` 對零條規則顯式 rc=1 並具名)** | 第 3 列的理由是「未涵蓋不得冒充通過」;這一列同形,差別在它是**移植時才可能長出來的沉默**:舊實作的紅是 glob 的副作用,不是裁決,換一個語言就換掉了。所以移植要把那個副作用寫成顯式的判定。成本一個分支。**觸發條件可檢查**:`ls /rules/*.md | wc -l` 應恆 ≥ 1;等於 0 時守衛紅,處置是補規則檔或把那個根移出 `plugin-roots`(歷輪補記與量測見量測文件第 51 列) | +| 52 | **零實例,而少算一個檔的普查與「查完歸零」無法區分**(#629 移植 `literal-census.sh` 成 `akashic literal-census`:記錄檔讀不進來或不是一般檔時,Python 版 traceback、shell 版少算,Swift 版具名 exit 3 拒絕輸出計數(`LiteralCensus.Failure.unreadableRecord`)。) | ✅ **寫(exit 3、具名那個檔,不輸出任何計數)** | 第 3 列的理由是「守衛沉默時要不要說話」;這一列同形而後果更具體:普查的輸出是 `literal-first-then-key` 終局的**進度量測**(literal 邊數的趨勢),少算一個檔會讓數字往「歸零」那一側偏,而那正是量測要回答的問題。所以不是略過並警告,是整次拒絕輸出。**觸發條件可檢查**(腳本見量測文件):不可讀或非一般檔數應恆為 0;非零時先修那個檔的權限或類型,再跑普查(歷輪補記與量測見量測文件第 52 列) | +| 53 | **零實例,而同一個不明確的依據設定時被拒、使用時被放行**(#642 b11d verify R1:規則型 library 的 venue key 之後變成重複、或排除清單的 citekey 打錯或已不在庫——`create`/`set-kind` 本來就拒絕重複的 venue key,`library add` 卻只比 key 字串;排除清單則從不查存在,打錯一個字排除就無聲失效。) | ✅ **寫(寫入閘與 warning 同批:`LibraryMembershipCheck` 的 `ruleVenueAmbiguous`,三面都不寫、`library check` 揭露;`create`/`set-kind` 對不在庫的排除 citekey 具名拒絕;validate 報兩種 warning)** | 第 47 列的理由是「零來自規則還不存在」,燈照的是寫入閘擋不到的來源;這一列補兩個它沒照到的:依據本身出了問題(venue key 重複)與排除清單懸空。前者是第 26 列「閘與守衛是同一條不變式的兩半」的形——同一個依據在設定時被閘擋下、在使用時卻被放行,等於那條不變式只有一半;後者讓同一份程式對「排除清單是不是參照」給出相反的答案(改名與合併的守衛把它當參照,建立時卻不查)。**severity 是 warning**:成員關係錯了不毀資料,與第 47 列同級。**觸發條件可檢查**(腳本見量測文件):兩個數應恆為 0;非零時用 `library check ` 看是哪一條規則、哪個 citekey(歷輪補記見量測文件第 53 列) | +| 54 | **零實例,而先前的守衛是一條走不通的出路**(#642 b11d verify R1:改名碰到被 library 規則指涉的 citekey——文件型的 `document`、規則型的 `excluded`。#642 首版一律拒絕並指路「先 set-kind 改規則」,而文件型的 `set-kind` 要求新 citekey 已在庫、`rename` 要求它不在庫,出路是死路。) | ✅ **寫(`renameEntry` 同批遷移規則裡的 citekey,registry 檔進同一批預檢與寫入,逐條列在 `RenameReport.libraryRulesRewritten`;三個前置拒絕、零寫入:新 citekey 已被規則指涉、被隔離的 registry 檔提到舊或新 citekey、要遷移的 registry 檔不可回溯)** | 改名不改身分,而 library 規則是關係邊第 16 條——改名本來就該遷移指向舊 key 的邊(第 13 條邊的 verdict value 是先例)。三個前置拒絕各擋一種遷移會造成的安靜錯誤:新 citekey 已被規則指涉時,改名會讓一條懸空的規則「復活」成指向另一筆;被隔離的 registry 檔看不到,遷移會漏掉它;registry 檔不可回溯時,被改寫的規則沒有舊值可取回。work 與 venue 合併維持拒絕,訊息改成可照做的 `set-kind` 命令、不經過 topic。**觸發條件可檢查**:規則型或文件型 library 出現之後,改名一筆被指涉的 citekey 時報告會多一段「library 成員規則已遷移」;守衛測試是 `LibraryMembershipStoreTests` 的改名六支(歷輪補記見量測文件第 54 列) | +| 55 | **零實例,而整值替換會丟掉人的裁決,舊值只剩 git 那一份**(#642 b11d verify R1:`set-kind` 替換一個既有的成員性質時,排除清單這類逐筆寫下的裁決整份被換掉,先前不回顯舊值、也不要求舊值取得回來。2026-09-29 唯讀量測 live store:4 個 library 全未標性質,從未標標成任何一種不經這道閘,所以零實例) | ✅ **寫(替換既有性質時要求 `libraries/.yaml` 已 commit 且乾淨;兩面回顯先前的性質;可回溯閘抽成共用核心 `LibraryStore.recoverabilityRefusal`,與 #573 那一族同一支)** | 第 44 列的理由是「既有判準的答案變了」;這一列同形而對象不同:#573 那一族的閘原本只看 `entities/` 的記錄檔,registry 檔在 `libraries/`,於是移除面一族的判準(舊值要能從 git 取回,使用者 2026-09-27 裁決)在這裡沒有套上。**Claude 代裁**:閘也涵蓋把 topic 換成別的性質(topic 沒有參數可失去,比必要的嚴)。**沒做的**:把規則放寬(降成 topic、清掉排除清單、換 venue)時要不要另外要求明確確認——待使用者決定;目前的防線只有回顯與 git。**觸發條件可檢查**:有 library 標了性質之後,替換它而 registry 檔未 commit 時 `set-kind` 拒絕;測試是 `LibraryMembershipServiceTests.testReplacingAnExistingMembershipRequiresACommittedRegistryFile`(回顯由 `testSetKindEchoesThePreviousMembership` 釘住)(歷輪補記見量測文件第 55 列) | +| 56 | **零實例,而 decode 接受它、唯一守著它的合併閘只擋一條進入路徑**(#679:附加 Zotero 來源沒記 `library_id`——匯入端與 `ZoteroSourceClaims.claims(of:)` 都對不回它,沒有別的 entry 宣稱那個條目時再匯入會另建一筆 twin;先前註解宣稱「由合併閘保證不會出現」,而合併閘只擋合併新收,管不到手改與舊檔。) | ✅ **寫(warning、在 `Entry.validate()`,每筆 entry 一則、至多列 5 個來源鍵;訊息說出後果與兩條出路:補真正的 `library_id`,或以 `update-entry --remove-zotero-source` 移除)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形——那筆記錄看起來連著 Zotero,而下一次匯入會安靜地另建一筆,結果是一對看起來各自健康的攣生。與第 33 列也同形:同一族的檢查(#610 的多筆宣稱)只照到有 `library_id` 的那一半。**severity 是 warning**:記錄合法可載入;decode 拒收會讓既有的檔整個被隔離(`writeEntry` 不對 `Entry.validate()` 的 error 設閘,所以升 error 也擋不住寫入,那不是理由)。**觸發條件可檢查**(腳本見量測文件):`LC_ALL=C grep -a -q '附加 Zotero 來源沒記 library_id' "$(command -v akashic)" && "$(command -v akashic)" validate 2>&1 | grep -c '附加 Zotero 來源沒記 library_id'` 應恆為 0;非零時照訊息給的定位鍵處置(歷輪補記與量測見量測文件第 56 列) | +| 57 | **零實例,而錯的回應看起來是對的**(#629 第二塊:`akashic crossref-match` 以重播協定讀 skill 經 safari-browser 取回的 Crossref 回應,回應的 DOI 與請求的 DOI 不同時拒絕;DOI 插進網址前先驗形狀(`^10\.[0-9]{4,9}/` 後接不含空白、引號、反斜線、`$`、反引號、`#`、`?`、`%` 的字元,且沒有 `.`/`..` 路徑段)。指令從未對真回應跑過。) | ✅ **寫(身分不符具名拒絕、形狀不過的 DOI 不組成請求)** | 第 4 列的理由是「錯誤的偽裝性」;這一列同形:把另一個 DOI 的回應讀成這一個,比對照樣成功、結束碼 0,而結果是一筆看起來查證過的錯資料。這個形狀在頁內 fetch 已經發生過一次(PsychQuant/safari-browser#190:一批讀到上一批的結果、結束碼全為 0),所以讀回後核對身分是 `web-access-via-safari-browser` 的既有紀律,這裡把它落實成程式。形狀檢查與 `web-access.md` 的形狀表同一條。**觸發條件可檢查**:形狀不過的 DOI 數應恆為 0;身分檢查由 `CrossrefMatchTests.testReverseVerifyRejectsAResponseAboutAnotherDOI` 與 `testAnUnsafeCandidateDOIIsNeverTurnedIntoARequest` 釘住(歷輪補記與量測見量測文件第 57 列) | +| 58 | **零實例,而半套的數字讀起來像完整的**(#629 第二塊:`akashic fulltext calibrate` 讀本機的 Crossref 回應目錄計分;缺任何一筆記錄時列出缺的網址並以結束碼 3 結束、不出數字,`--partial` 才照已有的量。校準從未在本機跑過——2026-09-24 那批語料本機找不到,重取要連網) | ✅ **寫(缺記錄預設不出數字)** | 第 3 列的理由是「未涵蓋不得冒充通過」;這一列同形:`22/28`、`0/808` 這類數字若只涵蓋有記錄的那一部分,讀的人會當成全部。拒絕輸出是預設,放行要顯式帶 `--partial`,而那時輸出本身就說明它是部分的。**誠實邊界**:移植時舊校準數字(自己 22/28、別篇 0/808)沒有重跑,替代證據是新舊判定規則的差分——它證明新舊判得一樣,不證明那組數字在新語料上仍成立。**觸發條件可檢查**:`SkillToolsCLITests.testCalibrateListsMissingCrossrefRecordsAndMeasuresTheRest` 釘住結束碼 3 與缺記錄清單(歷輪補記與量測見量測文件第 58 列) | +| 59 | **零實例,而「移哪一筆」對兩筆完全相同的記錄沒有判定內容可依**(#673:`update-venue --remove-reference` 以 field+value+位元組鍵定位;兩筆 reference 的 `byteExactKey` 完全相同時具名拒絕、不挑一筆移。2026-09-29 唯讀量測 live store:venue 485、帶通用 reference 的 venue 0、位元組相同的重複組 0。) | ❌ **不寫(不給完全相同的重複一條移除路徑)** | 與第 10 列同形:形狀取決於一個還沒出現的用途——要嘛加 ordinal 指定第幾筆,要嘛一條「重複全收成一筆」的腿,兩者的語意不同,而今天沒有實例可以告訴我們哪一種才是使用者要的。先具名拒絕是可逆的;選錯一個形狀則是固定了一個介面。這種重複本來就由 `StoreHealth.duplicateReferences`(第 35 列)報出。**觸發條件可檢查**(腳本見量測文件):位元組相同的重複組 > 0,或 `akashic validate` 對 venue 報「重複的 reference:」——那時重開,由使用者選形狀(歷輪補記見量測文件第 59 列) | +| 60 | **零實例,而上限沒有量測依據、也不是位元組界線**(#673:venue 讀取面(CLI `venue`、MCP `akashic_venue`)顯示非判定的 references,至多 `venueReferencesCap`=25 筆、每筆 `rests_on` 至多 5 個,以 `referencesTotal`/`referencesTruncated`/`rests_on_total` 揭露。2026-09-29 唯讀量測 live store:帶通用 reference 的 venue 0,單筆 venue 最多 0 筆) | ✅ **寫(第 1 類,守衛:上限 25 筆、`rests_on` 前 5 個,截斷一律揭露)** | 第 18 列的理由是「零量的是出口而守衛裝在入口」;這一列同形,差別在錨點:第 18 列夾在兩個量測之間,這一列**一個量測都沒有**——25 是**選的數字**(live store 沒有任何一筆通用 reference 可量),量級考量是一本刊的 ISSN 與名字各有幾筆來源記錄、再留一個數量級的餘裕。**它實際做的事**:不讓 `references` 這一段跟著一本異常刊無限長(筆數上界);單筆的字串各有字元上限。不設上限的代價是讀取面對未信任的 store 內容線性長大(第 16 列那一族)。**誠實邊界**:要限位元組得另設預算(`candidateByteBudget` 的形),此刻沒有實例;讀取面對 value 截在 200、url/statement 截在 300,逐字定位要對照 YAML。**觸發條件可檢查**:任一 venue 的輸出帶 `referencesTruncated: true`——那時才有實際分布可量,重量上限(並一併決定要不要設位元組預算)(歷輪補記見量測文件第 60 列) | +| 61 | **零實例,而上限守的是「理由只在報告裡」的那份輸出**(#680、#677、#673 的 b13f verify R1:移除面一族的報告輸出有上限——`update-entry --remove-zotero-source` 的 `zoteroSourcesRemaining` 兩面都截 20(`zoteroSourcesRemainingCap`,附總數);`remove_sources` 的取得記錄(`sourcesAddedCap`)與 `remove_reference` 的 reference 內容(`removalDetailCap`)只在 MCP 面截 20,其後仍逐筆列定位鍵與理由,以 `detailsTruncated`/`detailsListed` 揭露。) | ✅ **寫(輸出上限,截斷一律揭露;理由逐筆不截)** | 第 60 列的理由是「上限只有算術可依」;這一列同形(20 是選的數字、限筆數不限位元組),差別在這一族每筆都帶理由,而理由依 2026-09-27 的裁決只在報告裡有一份——所以不截整筆、只截第三方的內容,與 `add_sources` 截整筆的先例刻意不同:截掉第 21 筆以後的理由就無處可記。**誠實邊界**:省略的內容只在 git 的移除前副本與 CLI 輸出裡;上限不保證輸出位元組。**觸發條件可檢查**(腳本見量測文件):出現 `detailsTruncated: true`,或 `zoteroSourcesRemainingTotal` > 20 時重量(歷輪補記與量測見量測文件第 61 列) | +| 62 | **零實例,而失敗是刪掉使用者已有的資料、而且發生在「下載成功」之後**(#629 R1 verify:`akashic fulltext fetch` 的輸出步驟對已存在的目的地「先刪再搬」——`--out` 是非空目錄時遞迴刪光,是普通檔時搬失敗前原檔已毀。2026-09-29 唯讀量測:`~/Downloads` 兩層內名為 `*.pdf` 的目錄或 symlink **0**。) | ✅ **寫(三個目的地先 lstat、同目錄暫存檔+rename)** | 第 1 列的理由是不寫就沒有跡象;這一列多一層:**跡象指向錯的方向**——命令回報下載成功,被刪的是使用者原本就有的東西。成本是一次 lstat。釘住 `FulltextFetchHardeningTests.testANonEmptyDirectoryAtOutIsRefusedAndKept` 與 `testAWriteFailureAfterTheTempFileWasCreatedKeepsTheOriginalFile`(後者以 `RLIMIT_FSIZE` 讓寫入在暫存檔建立後失敗)。#613 起這道閘在 `akashic fulltext take`(`fetch` 只導航、不寫任何檔),兩支測試名不變、改對 `take` 跑;目的地剩 `--out` 與 `*.unverified.pdf` 兩個(量測見量測文件第 62 列) | +| 63 | **零實例,而「git 答不出來」與「不在 repo 裡」在輸出上分不開,閘又是隱私閘**(#629 R1 verify:fetch 的 git 閘寫成「git 回 0 才算在 repo 裡」,git 起不來、rev-parse 非零、dubious ownership 全被讀成「不在工作樹」而放行——PDF 可能寫進一個會推上 remote 的 repo。2026-09-29 唯讀量測:`$TMPDIR`、`~`、`~/Downloads`、`/tmp` 逐層往上都沒有 `.git`) | ✅ **寫(祖先沒有 `.git` 由檔案系統事實判;有才問 git,答不出來一律拒絕)** | 第 3 列同形——沒被檢查冒充成檢查過且乾淨——而後果更重:外流進 remote 不可逆(git 隱私邊界)。git 呼叫改走 `LibraryStore.hardenedGit`(#585 那一份),不另寫第三份。釘住 `FulltextFetchHardeningTests` 的 git 一族七個測試(#613 起這道閘在 `akashic fulltext take`,測試改對 `take` 跑,名稱不變)(量測見量測文件第 63 列) | +| 64 | **零實例,而兩個 JSON 解析器對同一份輸入安靜地給出不同答案**(#629 R1 verify:`JSONSerialization` 對重複鍵取第一個、吃掉字串開頭的 U+FEFF、接受尾隨逗號、拒 NaN,Python `json.loads` 四處都相反;從 Python 移植過來的 `abstracts-to-proposals`、`crossref-match` 因此可能對同一份檔讀出不同的欄位值。) | ✅ **寫(`PyJSONParser`,照 `json.loads` 的文法)** | 第 4 列的偽裝性:解析成功、欄位有值,只是那個值不是來源的意思;`lossless-intake` 的「來源給什麼收什麼」在解析這一層就可能失真。釘住 `PyJSONParserTests`(38 個 Python 案例)與 `AbstractProposalsPythonJSONTests`。**誠實邊界**:仍有三處與 Python 不同(深度上限 512、孤立代理對拒絕、超出 `Int` 的整數退成 `Double`),寫在型別 doc(歷輪補記與量測見量測文件第 64 列) | +| 65 | **零實例,而同一個推導有四份、各自讀起來都通順**(#663:CLI、MCP、App 三個讀取面與匯出端各自推導「這個人的隸屬現況」,只被觀測到的段在三個面說成「曾隸屬」、匯出端說 `undetermined`,要拿兩者對照才看得出。) | ✅ **寫(源碼掃描守衛)** | 第 13 列同形(清理過之後的零):四份併成 `TimelineOf.standing` 一份之後,第五個讀取面若又自己寫 `current ?? latestPastSegment`,不會有任何跡象。`entity-backlink-completeness` 執行細節 2 的「一個讀取面只能有一條實作路徑」在這裡由源碼掃描兌現。守衛 `TimelineStandingTests.testNoReadSurfaceCallsLatestPastSegmentDirectly`,負控 `testTheScanRecognisesAViolationAndAllowsTheCoreFiles`(歷輪補記與量測見量測文件第 65 列) | +| 66 | **零實例,而帳密一旦寫進 git 追蹤的 YAML 就收不回來**(#674:person 與 venue 的 references 寫入面對 retrieval 的 `url` 只收 http/https 且不含帳密、`retrieved` 是 ISO 8601、`status` 在 100–599。) | ✅ **寫(入口拒絕,整批零寫入)** | 第 18 列同形:零量的是出口,守衛裝在入口。多一層:url 裡的帳密是秘密(`lossless-intake` 不收的第 1 類),寫進去之後 git 歷史裡永遠在。**誠實邊界**:只擋 userinfo,query 或路徑裡的 token 擋不到;~~`enrich` 的來源欄位沒有跟著收緊,是另一份契約——而 live store 僅有的 33 筆 retrieval url 正是經它寫入的,追蹤於 #695~~ → **#695(2026-09-30)起不成立**:守衛搬到 Core 的 `RetrievalWriteShape`,`enrich` 的 `sourceURL`/`sourceRetrieved`/`sourceStatus` 呼叫同一個函式(整批拒絕);那 33 筆全數通過(2026-09-30 重量)(歷輪補記與量測見量測文件第 66 列) | +| 67 | **零實例,而欄位的語意改了、卻沒有任何讀者**(#684:index 的 `entries.orphaned` 在 #609 改由 `Entry.zoteroLinkState == .orphaned` 判定,`schemaVersion` 仍是 5。2026-09-29 量測:`Sources/` 讀這一欄的地方 0 處、建表恰 1 處) | ❌ **暫不做(不 bump)** | 第 10 列同形——要不要做取決於一個還不存在的東西,這裡是讀者。index 是衍生物,store 一寫就因 mtime 過期重建,舊 binary 建的 index 到那時自癒;沒有讀者就沒有東西受漂移影響,bump 只會讓每個使用者白重建一次。**觸發條件**:出現第一個讀者時,`schemaVersion` 與 `PRAGMA user_version = 5` 兩處同步升 6(後者是寫死的重複)(量測見量測文件第 67 列) | +| 68 | **零實例,而清單會由一次匯入一步跨過**(#684:MCP `akashic_import_zotero` 的 `ambiguousSourceClaims` 至多 20 個來源、每個來源 20 個宣稱者,揭露 `ambiguousSourceClaimsTotal`/`ambiguousSourceClaimsTruncated`;CLI 全列。2026-09-29 唯讀量測:work 2,572、Zotero 來源 535、被 ≥2 筆宣稱 0、最大宣稱者數 1。) | ✅ **寫** | 與第 18、30 列同形:沒有曲線,實例會由一次匯入一步跨過;上限保護的是 LLM context,不是 store。**觸發條件**:任一次匯入回 `ambiguousSourceClaimsTruncated: true` 就重開(量測見量測文件第 68 列) | +| 69 | **零實例,而它是沿革位置的第一個寫入面**(#675:`update-venue --edit-name-segment`/MCP `edit_name_segment` 改名字段的時間欄位、`source`、`note` 或刪段。上限沿用既有常數——一次 200 項、理由 4,096 位元組、`source`/`note` 65,536 位元組;`attested` 200 點是推估、沒有母體(R1 起每個點也有 65,536 位元組上限);ISO 8601 前綴與區間有效性只在這個入口驗。) | ✅ **寫(整批拒絕、零寫入、不截斷;時間欄位入口驗)** | 與第 45 列同形:上限只擋單次,累積由第 16/31 列出聲。差別在對象:第 22 列替沿革保留位置、第 43 列守合併時不把它拿掉,而在此之前**沒有任何面寫得進去**——這是第一個。ISO 只在入口驗、不升成 `validate()` 的 error:載入端對 venue 日期不驗是既有裁決(#85),升級會讓手改的值從「載入得了」變成「所有寫入被拒」。**誠實邊界**:手改的非 ISO 值仍載入得了,沿革豁免不認它。**觸發條件**:帶時間、source 或 note 的名字段數 > 0——那時第 22、43 列同時到期(歷輪補記與量測見量測文件第 69 列) | +| 70 | **零實例,而它是清單清空之後留著的機制**(#690:`trigger-coverage` 的「宣告範圍裡不在受保護集合的檔」檢查,與它的已知缺口清單 `.githooks/acknowledged-ci-gaps.txt`。2026-09-30 量測:宣告範圍的缺口 **0**、清單條目 **0**。) | ✅ **寫檢查、保留清單(清單是資料檔,每條分支各有負控;#711 起清單記缺口大小,變大即紅)** | 第 13 列的零是「清理過之後的零」;這一列同形,多一件事:**清空它的是一次計費裁決**。`paths` 若因成本再收窄,同一個缺口立刻回來,那時要的是「列管並追蹤」,而不是只有紅燈或什麼都沒有。留著的代價是一條可能變成豁免的路,所以四道閘:每一條必附 `#`、缺口消失而條目還在是缺口(過期)、格式不對是缺口、**缺口變大是缺口**(#711,見下)。**第四道閘是 #710 R1 verify 留下的另一半**:清單原本不記缺口多大,issue 接受的是「那一刻的那些檔」,而範圍長了新檔時 rc 仍是 0——接受從一個具體的缺口悄悄變成任意大小的缺口。**觸發條件可檢查**(指令見量測文件):清單有條目時,逐條確認它的 issue 還開著;issue 關了而條目還在,把條目拿掉——缺口若也還在,守衛照常紅(歷輪補記與量測見量測文件第 70 列) | +| 71 | **零實例,而它守的是另一支守衛的輸入讀不讀得懂**(#689:`migrated-guard-control` 的兩個出口——一支 harness 以命名慣例或 `// negative-control-for:` 宣告了某支守衛、source 裡卻沒有執行它的寫法(「宣告是空的」);`run-guards.sh` 裡一處 `.build/debug/akashic-guards` 不落在任何被抽取認得的呼叫裡(「抽取認不出」)。) | ✅ **寫(兩個出口都計入 rc,各有負控)** | 第 21 列的理由是「既有守衛的謂詞對那一類成員結構上不可能為真」,換一個謂詞就好;這一列的兩個出口補的是**另一支守衛(負控的抽取)自己的輸入**,而補丁有一個第 21 列沒有的失效方式:**它的失敗條件若與它要補的那個抽取相同,抽取漏掉的寫法它也漏掉**,零實例量到的就只是「看不到」而不是「沒有」。**觸發條件可檢查**(指令見量測文件):兩個數應恆為 0;非零時先看是不是 runner 或 harness 換了寫法——改成獨立一行、或讓宣告的守衛真的被執行,不要放寬偵測。**2026-10-01(#707)補記:兩個出口隨文字判準一起退場,本列的裁決(寫)不變、出口換了**——`migrated-guard-control` 改讀 `run-guards.sh` 這一次執行留下的紀錄(格式與寫入點在 `Sources/akashic-guards/GuardRunLog.swift`)。(歷輪補記與量測見量測文件第 71 列) | +| 72 | **零實例,而上限是一次裁決給的數字,守的是一步就跨得過的入口**(#703:`sources/` 單份內容的上限 `LibraryStore.maxSourceBytes`=268,435,456 bytes(256 MiB;裁決原話「256 MB」),`store-source`(CLI/MCP `akashic_store_source`)、`SourceStore.storeSource`、`ZoteroStorageFile` 的定位與開檔、`copy-zotero-attachments`(含乾跑)都讀這一個常數。) | ✅ **寫(超過的不截斷、不存、具名大小;單檔入口整個拒絕,批次入口逐檔略過)** | 第 18 列是入口的單一字串上限,值夾在兩個量測錨點之間;這一列同形而對象是整份內容,而且**只有一個錨點**:上限是使用者 2026-09-30 的裁決,約為最大單檔的 48.8 倍(以 MB 粗算 256/5.5 ≈ 46),不是兩個量測夾出來的。與第 18 列不同的是拒絕的形狀:`copy-zotero-attachments` 的其他「這個檔不能收」(檔案不在、不是普通檔、0 byte)本來就逐檔具名略過,超過上限是同一類,整批拒絕會讓一個大附件擋住其餘全部——所以 `lossless-intake` 有界拒絕的第 1 條(整批)在這個入口換成逐檔、具名、附大小,該檔同批寫明為什麼仍不是靜默丟棄;單檔的 `store-source` 就是一整批,照第 1 條整個拒絕。**觸發條件可檢查**(腳本見量測文件):最大單檔逼近上限時回 #703 重開——那時先問那份檔是不是一份文獻的全文。(歷輪補記與量測見量測文件第 72 列) | +| 73 | **零實例,而殘留與進行中長得一樣**(#703 R1 verify 第 13、18、22、23 則:`writeBlob` 的暫存檔 `.<62 hex>.incoming-` 只靠 `defer` 清理,行程在複製途中被殺掉(SIGKILL、斷電、逾時)時留在分片目錄裡,一份可以到上限那麼大;`auditSourceIndex` 只認 62 hex 的檔名,doctor 看不到、也沒有清理面。) | ✅ **寫(warning 級:doctor 的 sources 一致性逐行列路徑與大小,MCP 與 App 讀同一份 audit;只報不刪)** | 第 15 列的零是本機的、第 23 列的可達路徑是本 change 自己開的;這一列兩者都有一點而理由不同:暫存檔是 #703 自己開始管的(先前的 `Data.write(.atomic)` 由 Foundation 管暫存,窗口只有一次寫入那麼長;現在是逐塊複製兩遍 I/O 的時間),它**必然**會在被殺的行程之後留下,而它與一個正在進行的存檔在磁碟上完全相同——所以守衛只能報、不能刪。以年齡判斷「一定是殘留」是猜(一份 256 MiB 的檔在慢磁碟上要寫多久沒有上界),猜錯的代價是刪掉別人正在寫的暫存、讓那一次存檔失敗;報而不刪的代價是人看一眼。**severity 是 warning**:記錄與 blob 都沒有壞,佔的是磁碟;它仍計入 `hasFindings`(要人看一眼,與孤兒 blob 同級)。**觸發條件可檢查**(指令見量測文件):數目應恆為 0;非零時先確認沒有 `store-source`/`copy-zotero-attachments` 在跑,再刪掉那幾個檔。(歷輪補記與量測見量測文件第 73 列) | +| 74 | **零實例,而寫入點分散、沒有單一出口可以一次包住**(#708:App 的每一個 `writeEntry(`/`writePerson(`/`renameEntry(`/`renamePerson(` 呼叫要在 `AppState.recordingLegacyCopies` 的範圍裡——在範圍外,寫進 `entities/` 而 legacy 拷貝刪不掉的那一筆擲 `legacyCopyNotRemoved`,畫面上是「操作失敗」,與 CLI/MCP 把它記在成功那一側(#705)是兩種說法。) | ✅ **寫(源碼掃描守衛)** | 第 65 列同形(清理過之後的零,由源碼掃描守):新的寫入點忘了開範圍,同一件事在 App 回到「操作失敗」而沒有任何跡象——它只在 legacy 拷貝刪不掉時才發生,平常的測試碰不到。判準是**字面包含**:寫入呼叫要在某個 `recordingLegacyCopies {` 的大括號裡。(歷輪補記與量測見量測文件第 74 列) | +| 75 | **零實例,而數不出來等於沒有上限**(#613:`akashic fulltext fetch` 每次準備導航到 PDF 連結(或把頁面的下載按鈕交給人)就在 store 之外的帳本記一筆,同一站每個 Asia/Taipei 日曆日最多 10 次(使用者 2026-10-01)。帳本讀不懂——不是普通檔、有一行不是 `{"at": 帶偏移的 ISO 8601, "site": 主機}`、`at` 沒有明確的時區——時在開任何分頁之前拒絕、說出第幾行。) | ✅ **寫(fail-closed:整個拒絕,不略過壞行)** | 第 52 列的形狀(少算的普查與「查完歸零」分不開)落在一個上限上:略過一行讀不懂的就是少算一次嘗試,而少算正是上限要擋的方向——漏停的代價是出版商封鎖使用者機構的 session,誤停的代價是使用者修一行帳本。沒有時區的時間不知道是哪一天,同樣拒收(全域 CLAUDE.md〈時區〉:寫進檔案的時間一律帶 offset),寫入端一律寫 `+08:00`。**誠實邊界**:`--ledger` 與 `HOME` 仍能換一個帳本——上限對一個被引導的 agent 是自願遵守的;CLI 對非預設的 `--ledger` 在 stderr 警告、SKILL 明寫禁止,但程式不擋(測試要用它)。**觸發條件可檢查**:`ls -la "$HOME/Library/Application Support/akashic/"`;`fetch` 以結束碼 1 報帳本讀不懂時,照訊息指的那一行修或刪,不要刪整個帳本來繞過上限(歷輪補記見量測文件第 75 列) | +| 76 | **零實例,而收進來的檔是別人給的路徑**(#613:`akashic fulltext take --from` 收使用者存下來的本機檔。`--from` 必須是普通檔——`lstat` 拒 symlink、目錄、FIFO,`O_NOFOLLOW`+`O_NONBLOCK` 開(lstat 與 open 之間被換成 FIFO 時不卡住)、`fstat` 再確認——大小不超過 `LibraryStore.maxSourceBytes`,讀取至多讀「上限+1」位元組(讀取當下還在增長的檔不撐爆記憶體);只讀一次,同一份位元組先驗證、再存檔。) | ✅ **寫(整個拒絕、什麼都不寫)** | 第 62 列守的是輸出目的地,這一列守輸入:跟隨一個 symlink 會把別的路徑上的內容收成這一篇,而驗證與存檔若各讀一次,兩次之間被換掉的檔會以驗證過的名字存下別的位元組——所以只讀一次,驗的就是存的。上限不另立數字:超過 `store-source` 上限的檔收進來也存不進 store(第 72 列的常數,同一份)。**誠實邊界**:讀取上限的「讀至多上限+1」沒有測試能量到記憶體用量(只量得到拒絕);`take` 沒有算 digest,驗證與入庫之間只靠 `--out` 不被改(歷輪補記見量測文件第 76 列) | +| 77 | **零實例,而製造它的是本張自己的回歸——位址被佔住時工具說成功**(#703 R2 verify 第 4、5、6、15 則:`sources/` 的位址上是目錄、symlink(含懸空的)、特殊檔案,或普通檔的大小與 `index.jsonl` 那一列的 `bytes` 不同。2026-10-01 唯讀量測 live store:普通檔 blob 103、位址上不是普通檔 0、index 列 103、大小與 index 不符 0。) | ✅ **寫(warning 級:doctor 三個面的 sources 一致性列出位址問題,只報不刪;寫入端——`store-source`、`--add-source`——具名拒絕、不寫 index)** | 第 26 列的理由是「閘與守衛是同一條不變式的兩半」;這一列同形而**閘與守衛是同一批裡一起長出來的**:位址上的東西只有一個分類(`sourceOccupant`),寫入端以它拒絕、audit 以它報告——兩處各自判,正是 R2 之前四個讀者(`writeBlob`、`checkStoredBlob`、`sourcePresence`、audit)對同一個狀態給出三種答案的原因。**severity 是 warning、計入 `hasFindings`**(與孤兒 blob 同級:記錄沒有壞,位址上的內容不是它說的那一份)。**觸發條件可檢查**(腳本見量測文件):兩個數應恆為 0;非零時照訊息把那個位置移走,再對原檔重跑 `store-source`(或 `copy-zotero-attachments`);大小不符時錯的也可能是 index 那一列的 `bytes`——重存不會改既有的列(append-only),要手改 `index.jsonl` 那一行(歷輪補記與量測見量測文件第 77 列) | +| 78 | **零實例,而製造它的寫入面是本輪自己開的,且拒絕的依據是另一條裁決刻意留下的提醒**(#564:名字分類的判定記錄。venue 合併對「被併者的 authorized 會被降級、而它帶名字分類的判定記錄」改為拒絕;寫入名字分類記錄要求 store format ≥ 22。) | ✅ **寫(拒絕:`DivergenceResolveError.wouldDemoteJudgedAuthorized`,住在 `validateVenuePreconditions`,preview 與實跑共用;寫入閘:`LibraryStore.assertNameClassificationRecordsWritable`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列同形而多一層:**讓這個狀態可達的寫入面(五個名字分類面)與拒絕必須同批落地**,而拒絕的位置早就被預告過——`authorizedDemotedByMerging` 的 doc 明寫「#564 裁「留」且落地後,對有 judgement 的 authorized 升成拒絕條件、對機械值維持提醒」。**寫入閘是另一半**:format-21 binary 對這種記錄整檔 quarantine(三個成因各自足夠,`StoreVersion` 的 22 那一段),所以在 marker 升到 22 之前寫入面一律具名拒絕——零寫入而不是寫出舊 binary 讀不懂的檔。**機械值仍是提醒**:沒有記錄的降級照舊合併並出 warning(`NameClassificationMergeTests.testMechanicalDemotionStillMergesWithAWarning` 釘住)——不得把拒絕放寬成「一律拒」,那會讓合併對 470 筆機械值全部無用。**觸發條件可檢查**(腳本見量測文件):名字分類記錄數 > 0 即代表合併拒絕與寫入閘從零實例變成有實例——那時重跑 `NameClassificationMergeTests`,並確認第 13 列的死 verdict 掃描與第 35 列的重複 reference 掃描對這種記錄沒有誤報(歷輪補記與量測見量測文件第 78 列) | +| 79 | **零實例,而降級把一道拒絕從共用段挪到只有一邊走得到的地方**(#709:同一筆記錄的 legacy 拷貝——同一種、同一個 id,一份在 `entities/`——造成的 UUID 共用與 citekey/key 重複,自使用者 2026-10-01 的裁決起降為 warning,不再讓共用的 `assertNoCrossRecordErrors` 擋下改名與合併。原本由那個 error 順帶擋住的兩格:(a) `resolve-divergence` 的**候選**有 legacy 拷貝(被併者只刪 `entities/.yaml`、legacy 那份在下一次 load 復活成唯一的一份);(b) **會被改指的 entry** 自己有 leftover(改寫它會被 #631 拒絕)。) | ✅ **寫(`DivergenceResolve.assertRewriteTargetsWritable`,住在 preview 與實跑共用的 `validateResolvePreconditions`;測試把 preview 與實跑逐一配對:work/person/venue 三種合併、改名留下的拷貝、對照組)** | 第 40 列的理由是「拒絕問在錯的時間點」,第 8 列是「零的來源在別處」;這一列的拒絕沒有錯時間、零也不由別處造成,是**降級改變了它在哪裡**:共用段的 error 閘一旦變窄,順帶被它擋住的格子就各自找新家,而只裝在實跑那一邊的新家讓 dry-run 說謊——正是 `validateResolvePreconditions` 的 doc 記過的 #139 F1。**位置是裁決的一半**:寫在共用段而不是 `previewResolveDivergence`,兩邊才不可能再分岔;`commitResolution` 裡那一道留著當最後一道防線(它的名單由實跑迴圈算出,兩份名單一致只由測試釘住)。負載下的絕對值不可比,只當量級;**觸發條件可檢查**(指令見第 40 列):legacy 殘留數 > 0 時 `resolve-divergence --dry-run` 與實跑對每筆合併要擲同一個錯——`ResolveDivergencePreviewLegacyCopyTests` 釘住(歷輪補記見量測文件第 79 列) | +| 80 | **零實例,而上限是一個沒人量過的裁量值,且它讓一條使用者裁決在尾端不成立**(#611:`DOINomination.maxGroupSize`=10。Zotero 匯入新建的 work 與另一筆 work 共用 DOI 時,使用者 2026-10-01 的裁決是「照建,並自動記一筆歧異提名」;一個 DOI 被超過 10 筆 work 共用時(這一趟新建的加上其餘的)一對都不記、只回一列 `groupTooLarge`——那一格**偏離**該裁決的字面,而使用者沒有裁決過這個數字:10 是 #611 第一輪修正裁量的,不是量出來的。) | ✅ **寫(超過的整個 DOI 一對都不記、回一列 `groupTooLarge`,匯入照建;沒記下來的列讓 CLI 以非零結束)** | 第 18 列的上限夾在兩個量測錨點之間、第 72 列有一個使用者給的數字;這一列**沒有任何錨點**——門檻不是量出來的、也不是使用者給的,誠實的說法是它是裁量。不寫的代價是 C(k,2) 筆記錄:k=80 → 3,160 筆、約 10 秒(#611 R1 verify DA 實測),而一個 DOI 被這麼多筆共用幾乎必是書或資料集的概念 DOI,逐對提名沒有資訊量。**誰決定的**:Claude 在第一輪修正裁量,**使用者尚未確認**;使用者的原話只涵蓋「照建並記一筆」,沒有說超過某個大小時怎麼辦——所以這一列的狀態是「待使用者確認」,不是「已定案」。**重開觸發(可檢查,腳本見量測文件)**:(1) live store 出現共用同一個 DOI 的 work ≥ 10 筆的組——門檻開始碰到實資料,那時要用實例重新決定這個數字;(2) 使用者對「超過門檻時怎麼辦」給出裁決,或裁決 `groupTooLarge` 不該讓 CLI 非零(歷輪補記與量測見量測文件第 80 列) | +| 81 | **零實例,而製造它的是本張自己改的覆寫語意——同一個 id 不得換成另一組候選**(#611:`recordDivergence` 在磁碟上同一個 id 的歧異記錄、候選 (shape, key) 與這次不同時整個拒絕(`StoreIOError.divergenceIdHeldByOtherCandidates`),先前互相覆寫、不出聲。) | ✅ **寫(拒絕、不猜、原記錄逐位元不動;訊息點名現有那一筆的候選、說原因是 key 還是形狀、出路是 `dismiss-divergence` 或 `resolve-divergence`)** | 第 23 列的理由是「可達性是本 change 自己造出來的」;這一列同形而方向相反:先前**不拒絕**——兩者互相覆寫、不出聲(#611 R1 verify 第 1/21 列立案的缺陷)——所以這道拒絕是把一個先前靜默發生的覆寫變成可見的失敗。零實例是因為 live store 只有一筆歧異記錄而且它的 id 沒過期,**不是因為這個形狀不可達**:citekey 改名之後 `renameEntry` 把歧異記錄的候選 key 就地改成新名、id 不換(真實路徑),之後 Zotero 匯入再新建一筆取回舊 citekey,記一對就撞上同一個 id。**重開觸發(可檢查,腳本見量測文件)**:量測腳本的「id 已過期」數 > 0——這個形狀已經在 store 裡,那時要決定的是 `rename` 改寫候選時要不要像合併一樣重算 id 並改名舊檔(需要遷移)(歷輪補記與量測見量測文件第 81 列) | +| 82 | **零實例,而上限、理由形狀與 person 的理由要求守的是同一批名字分類面的入口——它們的失敗都會變成一筆刪不掉的記錄**(#564 修正輪,使用者 2026-10-02 裁決第 1、4 點與 R1 verify 的理由形狀:名字分類的寫入面一次至多 200 個名字(venue 三條腿合計、organization `authorize`、person `fields.names` 的 authorized——R2 起 variant 不寫記錄、不計、`remove_names`)、單一名字至多 65,536 位元組;理由開頭不得是組合符號、格式或不可見字元、要有字母或數字;person 的 `fields.names` 讓名字進出 authorized 時理由必填。) | ✅ **寫(入口整批拒絕、零寫入、不截斷;三個數字都不另立:200=`maxSpecsPerCall`、65,536=`AddOnlyEnrichment.maxValueBytes`、理由的 L/N 類與名字不變式同一個謂詞)** | 第 30/45 列的上限只擋單次呼叫、累積由預算 warning 出聲;這一列同形而多一層:**被守的記錄只追加、移除面不刪**——名字分類記錄是判定史,唯一刪得掉的路是修正輪的「最後一筆是撤回的名字連同記錄一起刪」(第 2 點),而那條路要先撤回;一次寫進一千五百筆記錄的錯誤要逐名撤回再刪。所以入口要先擋,不是事後清。理由形狀的那一半不是上限、是文法:以 Grapheme_Extend 字元開頭的理由會與動作前綴的全形冒號併成同一個字,只看 Character 的讀者把它讀成一般 reference(`parse` 已改比 scalar 前綴,入口另外拒收,兩道一起);只有不可見字元的理由則是「必填」被字面滿足、什麼都沒說。**觸發條件可檢查**(腳本見量測文件):某筆記錄的名字分類記錄數逼近讀取上限的預算 warning(第 16/31 列)時,回來看是不是有呼叫端繞過了入口(歷輪補記與量測見量測文件第 82 列) | +| 83 | **零實例,而零是 live store 在 APFS 上的零——同一份程式在別種磁碟區上,每一次存檔都是實例**(#703 b26 F6,使用者 2026-10-02 裁決:`sources/` 所在的磁碟區做不到不覆寫的原子放置——`renamex_np(RENAME_EXCL)` 與 `link(2)` 都不支援,exFAT、FAT32 即此類。2026-10-02 唯讀量測 live store:`sources/` 所在的磁碟區是 APFS,所以今天這道閘不會觸發。) | ✅ **寫(拒絕:每一次存檔在建立任何檔案之前具名拒絕、零寫入;`LibraryStore.assertSourcesVolumeCanPlace`,`storeSource` 與 `preflightStoreSource` 共用;旗標讀不到時 `writeBlob` 在任何分支之前以 `assertDirectoryCanPlace` 實際放一次——b29 V5)**;**不寫替代放置路** | 第 15 列的零是「資料在別台機器上缺席」;這一列的零是**這台機器的磁碟區種類**——同一份程式換一種檔案系統行為就變。裁決有兩半。**寫拒絕**:R1 把存檔改成 `RENAME_EXCL` 之後 exFAT 上每一個新 blob 都失敗,先前存得進去——那是回歸,由使用者裁決「不支援」而不是設法支援。替代路為一種檔案系統造出一組「只在那裡才會走到」的程式,而那組程式的不變式恰好是那種檔案系統打破的。**觸發條件可檢查**:使用者要把 store 放在不支援的磁碟區上時重開——那時要裁的是「磁碟區上的替代放置法」,不是回頭補第三條路(歷輪補記與量測見量測文件第 83 列) | +| 84 | **零實例,而它們收回的是一則裁決被實作放寬過頭的部分**(#613 R2 verify,使用者 2026-10-02 裁決 1、2 與主 session 對 MEDIUM 的裁定:`akashic fulltext fetch` 把裁決 1 的「別的主機上沒有標記 → 交給人、批次繼續」實作得比裁決寬——還在載入的頁面第一眼就被說成「沒有標記」、網址只是提到 doi.org 的登入頁被說成「DOI 解不開」、離線時 Safari 的錯誤頁也是「DOI 解不開」、`--resume-stage followed` 對位置漂移到的任何分頁跑讀頁 JS。) | ✅ **寫(往整批暫停或拒絕收;別的主機上交給人只留給四種:PDF、載完而沒有命中清單的頁面、讀不到而可能是 PDF 檢視器的分頁、doi.org 自己的查無頁)** | 第 75 列守的是一個上限、第 76 列守的是收進來的路徑;這一列守的是**一則放寬中止條款的裁決的邊界**:裁決 1 把「別的主機」從整批暫停改成交給人,而實作在三個地方把「交給人」給了裁決沒有點名的頁面(沒載完的、登入頁、Safari 的錯誤頁)。交給人時批次繼續,所以每多一個不該交給人的頁面,就是下一篇照常對同一個站發請求——中止條款當初要防的事。判準因此是裁決的字面:裁決點名的交給人,其餘往停的那一邊倒。**觸發條件要人觀察**(b31 W4 第 11 則:先前寫「可檢查」,而帳本只記嘗試、不記交給人的原因,`ls` 只說得出 `fetch` 跑過沒有):交給人的 `left-site`/`unverifiable`/`doi-not-resolved` 經使用者看過其實是登入頁、驗證頁或錯誤頁,那一格就是這一列漏掉的形狀,回來加閘。`ls -la "$HOME/Library/Application Support/akashic/"` 是前提:帳本出現之前不可能有實例(歷輪補記與量測見量測文件第 84 列) | +| 85 | **零實例,而它守的是一道分類規則與它造出的記錄之間的一致——合併接上去的記錄若與名字的分類矛盾,之後按位置讀記錄的兩道閘會得出相反的結論**(#564 R2 verify:b29 V1 第 0/2/6 列。保險:合併後倖存者上若有名字的最後一筆與分類矛盾——撤回而仍在分割裡,或指定/確認而不在——而那是這次合併帶進來的,preview 與實跑都拒絕。) | ✅ **寫(拒絕:`DivergenceResolveError.wouldContradictClassificationTail`,住在 `validatePersonPreconditions`/`validateVenuePreconditions`,與實跑同一個 `mergedPersonKeeper`/`mergedVenueKeeper` 算;delta——倖存者原本就有的不擋)** | 第 78 列守的是「合併不替人改判定」(降級帶記錄的對外形);這一列守的是**合併接上去的記錄本身說的話**:刪名字閘(`latestAction`:最後一筆是撤回才可刪)與寫入面的去重(只比最後一筆)都按位置讀,尾端一旦與分類矛盾,前者會放行刪一個仍是對外形的名字、或叫人去「撤回」一個不在分割裡的名字。主修在搬法(按位置接);保險擋的只剩來源本身就不一致的被併者——手改,或修正輪之前的 binary(那時寫入面的去重比整份歷史,「指定 R → 撤回 → 再以 R 指定」的第三筆會被丟),零實例而且不靠人記得。**觸發條件可檢查**(腳本見量測文件):矛盾的(分割, 名字)應恆為 0;非零時那筆是手改或舊 binary 寫的,出口是在那筆記錄上重下一次與分類一致的判定。(歷輪補記與量測見量測文件第 85 列) | +| 86 | **零實例,而三種實體裡只有它刪得到沒有名字——兩個鄰居早就擋了**(#564 R2 verify:b29 V1 第 18 列。`update-person --remove-name`/MCP `remove_names` 刪完 person 沒有任何名字時整批拒絕、零寫入,乾跑同樣拒絕;organization 的 `remove_names` 與 venue 的 `edit_name_segment`(`.noNamesLeft`)在修正輪就擋了。) | ✅ **寫(入口拒絕;不改 `Person.validate()`)** | 第 25 列把名字的不變式裝在 store 邊界,因為入口有五個;這一列的形~~只有一個入口造得出~~有兩個入口——`remove_names` 與 `fields.names` 的空替換,所以擋在入口、與兩個鄰居同一句話。**不改 validate** 的理由:沒有名字的 person 讀得進來、不毀資料(`doctor` 已經報「no authorized name」),升成 error 會讓手改出來的那一筆擋住它自己所有的寫入;這一格要的只是工具面不再造出它。**觸發條件可檢查**(腳本見量測文件):沒有名字的 person 數應恆為 0;非零時那筆是手改或 R2 verify 之前的 binary 寫的,`update-person --fields` 的 names 補一個名字即可(歷輪補記與量測見量測文件第 86 列) | +| 87 | **零實例,而它關掉的是一個把「讀不懂」折成「空的」的回退——接在後面的是整份替換**(#700 b31 W5 MEDIUM 0、1:`ensureLayout` 對 store 根目錄 `.gitignore` 的處置先前是 `(try? String(contentsOf:encoding:.utf8)) ?? ""`,讀不出來(非 UTF-8、UTF-16、權限 000)就當成空檔,再以只含 sources 區塊的內容原子替換——使用者原有的規則(擋 raw 逐字稿的 `*.srt` 之類)全部消失、結束碼 0、沒有任何訊息;symlink 被換成一般檔。) | ✅ **寫(讀不懂的不改寫:`file add` 在建立任何東西之前具名拒絕、`doctor` 與兩個匯入照常建佈局並印 warning(匯入是使用者 2026-10-05 的裁決:匯入不寫 `sources/`,防線在 `store-source`);要加區塊時只附加位元組;`LibraryStore.inspectSourcesIgnore`/`applySourcesIgnore`,`SourcesIgnorePolicy`。b36 verify 起「區塊已在」要與 git 的判讀一致:規則行照 git 的切法、symlink 不論指向的內容都不算已在——git 不讀 symlink 的 `.gitignore`)** | 第 3 列是「沒被檢查」與「檢查過且乾淨」在輸出上分不開;這一列是同一種折疊落在**輸入**上:「讀不懂」與「空的」在程式裡是同一個值,而接在後面的動作是整份替換,所以一個錯的折疊把一次附加變成一次刪除。零實例不降低它的份量——失敗是安靜的(結束碼 0),被刪的是隱私邊界的規則,`git add -A` 之後外流不可逆。裁決有三半:**標記以位元組比對**(已有區塊時不必讀懂整個檔,尾端有非 UTF-8 位元組也不動);**加區塊只附加位元組**(原有位元組不經解碼、不重寫);**讀不懂就不動**——`file add` 拒絕、診斷面與兩個匯入改報(診斷工具最該說話的時候不是最該掛掉的時候,`doctor` 不因此失敗)。**觸發條件可檢查**(腳本見量測文件):註冊的 store 裡 `.gitignore` 不是 UTF-8 一般檔、又沒有區塊的數應恆為 0;非零時那個 store 的 `file add` 會拒絕、`doctor` 與兩個匯入會報 warning,處置是人把它轉成 UTF-8 或自己加那段(歷輪補記與量測見量測文件第 87 列) | +| 88 | **零實例,而守衛只對它認得出的單位求值——認不出的寫法讓量測與它的閘一起消失、守衛照樣綠**(#711 R3 verify:b31 W6 第 0、2–7、20 列。`zero-instance-rows-audit` 的自證閘自 R2 起只收一種寫法,但「哪些文字是一個單位、哪些單位是量測」沒有寫出來:R2 的單位是 fence 內一個實體行、fence 外同一實體行裡的一段 inline code,binary 與計數拆在兩行(`\`、行尾的豎線或 `&&`、跨行的 inline code)、寫在 fence 與 inline code 以外(縮排區塊、`
`、引用區塊裡的 fence)、或計數的寫法沒被認出(`grep '-c'`、樣式含豎線而 `-c` 在後、`uniq -c`、預設值展開),一條量測就不成單位、不被計入;把 34 條裡的 33 條改寫成預設值展開,閘全部消失而守衛 rc=0。) | ✅ **寫(辨識補寬+條數地板+退場區塊個數;認不出的三類照實列出、由地板兜底)**;R4:認不出時偏向「是量測」與「同一個單位」、地板依閘去重、退場標記拿掉、認不出的寫法改寫成開放清單 | 第 21 列的謂詞對那一類成員結構上不可能為真,換一個謂詞就好;第 37 列連正確的謂詞都不存在。這一列在兩者之間:謂詞(模板)是對的,錯在它只對**認得出的單位**求值,而「哪些文字是一個單位」是一個沒寫出來的前提——R2 寫「封閉的模板沒有旁路可找」,對模板為真、對辨識為假。所以裁決有兩半:把那個前提寫出來並補寬(邏輯行、引用區塊、CommonMark 配對、fence 外的文字、不切註解、引號與位置變化的計數選項),**以及**承認辨識不會完整——以變數或檔案當來源、`awk` 自己加總、binary 名與計數不在同一個邏輯行,這三類仍然認不出——所以條數要有一個不靠辨識的地板(同第 16 列:裁決依賴守衛;地板過期只擋得住整批、擋不住少量,新增量測後由人調高)。**R4 更正兩處**:認不出的不只三類(開放的,見 [`measurement-commands-self-prove`](measurement-commands-self-prove.md));退場區塊的個數釘得住、內容釘不住(加一行、換一行都綠),所以 R4 拿掉退場標記、照掃那個區塊。**觸發條件可檢查**(指令見量測文件):棘輪的訊息數應恆為 0;標題列的「合模板」比下限多出許多時調高下限(歷輪補記與量測見量測文件第 88 列) |
+| 89 | **零實例,而防線先前住在一份抄本裡——跑的是模型照文件抄寫的程式,測試釘的是文件**(#692 R4 verify:讀頁面的五道閘——(a) Safari 回報的網址在解析器會分岔的形狀(反斜線、帳密、百分比編碼、IPv6 字面值、空白與控制字元)一律認不出主機;(b) `LAND` 預設 `-`(不比對落地主機)時主機仍要形狀合格;(c) 頁面自報的 `rawLength` 限 0–1,000,000,000;~~(d) 主機不合而換到的是已知的驗證服務時整批暫停(結束碼 2),不當成不可達~~ → (d) 分頁在已知的驗證服務上——不論換到或直接落在——不當內容讀,以頁面文字分等人驗證 3/整批暫停 2(b35,使用者 2026-10-05);(e) 輸出檔與落地主機檔在第一個可能失敗的瀏覽器指令之前刪,讀回的 JSON 由 `trap … EXIT` 刪。) | ✅ **寫(在 `akashic web-read`,剔除集合是 `UnsafeToEmitScalar`(b33 verify 起加 noncharacter,與 `escapesInLLMDocument` 同一個加項);清理順序在區塊本身)** | 第 84 列守的是一則裁決的邊界、第 83 列的零是磁碟區種類的零;這一列的零是**取得路徑從沒跑過**的零,而它的特別之處在防線的**位置**:R3 把這類閘寫成文件裡要照抄執行的程式,跑的是抄本、測試釘的是文件,兩者之間沒有任何東西核對;剔除集合也是 `UnsafeToEmitScalar` 的第二份描述,在 macOS 的系統 Python(Unicode 13)上安靜地少了九個碼位。所以裁決有兩半:寫這五道閘,**以及把它們放進 CLI**——放在抄本裡的閘,通過時證明的是文件,不是實際跑的那一份。**觸發條件可檢查**:verify-venue SKILL 的「沒有實跑過」一段改寫的那一天(對出版商頁實跑過 safari-browser),回來看哪一道閘在真 Safari 上有實例、哪一道的前提(例如 WebKit 回報正規化網址)量過。(歷輪補記與量測見量測文件第 89 列) |
+| 90 | **零實例,而擋它的寫法看起來已經擋住了——`${V:?…}` 只停在管線的一段,計數照印 0**(#711 R4 verify:b33 X6 第 1、3、5、14、15 列。R3 把第 18、46、49、73、77 列的 `--library "$STORE"` 改成 `--library "${STORE:?…}"`,散文寫「沒設時 shell 停下」。) | ✅ **寫(模板加前置條件;fence 與 inline code 裡任何單位把 `${V:?…}` 寫在管線裡即紅)** | 第 13 列的理由是「沒被檢查」與「檢查過且乾淨」在輸出上分不開,自證閘擋的成因是舊 binary;這一列是同一個分不開換一個成因——binary 根本沒跑。它比第 13 列多一層:擋它的寫法**看起來已經擋住了**(`${V:?…}` 的字面意思是「沒設就停」),文字照著寫,三輪 verify 才量出它只停管線的一段。所以不能只改散文——散文改對了,下一條量測照樣可以寫成同一個形狀。裁決是把前置條件寫進模板(`: "${V:?…}"` 在 `&&` 串最前面,整條不跑;`<參數>` 用到的變數都要列在那裡),`--library "$V"` 另要 `test -f "$V/store.yaml"`(打錯路徑時 `doctor` 會建 store),並在 fence 與 inline code 的**任何**單位把管線裡的 `${V:?…}` 判成錯誤——不只量測:第 73 列的 `find …` 接 `wc -l` 不提 binary。**觸發條件可檢查**(指令見量測文件):兩種訊息數應恆為 0(歷輪補記與量測見量測文件第 90 列) |
+| 91 | **零實例,而揭露與寫入在同一次執行裡——附註的補救到達時寫入已經發生**(#709 第四次 verify MEDIUM 5、LOW 6/13/15、LOW 22,使用者 2026-10-05 裁決 2:三個 bootstrap 的計畫讀到同一筆記錄的 legacy 拷貝時,拷貝裡的 literal 也算一次出現——一篇 work 加它的拷貝讓只被用一次的名字以 ×2 越過 `--min-occurrences`。先前開頭印一行附註「先刪掉拷貝再跑,計數才準」,`--apply` 照寫;真 binary 重現建出 `doe-a`、`journal-of-alpha-things`。) | ✅ **寫(`LibraryLoad.refuseApplyWithPlanBlockers`:計畫讀到拷貝時 `--apply` 整批拒絕、零寫入,拒絕說筆數與處置、清單在乾跑;只數計畫讀到的種類——`BootstrapPlanSource`,2026-10-09 起 `bootstrap-people` 也讀 person 拷貝;`authorize-names` 的寫入集合裡有任何一筆的 id 還有 legacy 拷貝時同樣整批拒絕)** | 第 3 列的理由是「守衛沒說話」;這一列的守衛說了話、卻沒有擋——揭露只有在它到達與動作之間還有一個人時才有用,而 `--apply` 把兩者放在同一次執行裡。所以裁決不是把附註寫得更清楚,是在寫入之前擋(`two-kinds-of-edits` 程式編輯欄的「可預期失敗整批擋零寫入」);乾跑照常列出並說明,那是人還來得及讀的地方。**誠實邊界**:拒絕比計數嚴——拷貝與 entities/ 那份內容相同、計數只是乘二而不越過門檻時也擋;那是裁決本身的取捨(「有拷貝就拒絕」,不是「計數越過門檻才拒絕」)。**觸發條件可檢查**:計畫讀到的種類裡有被 load 標成拷貝的記錄時(`akashic validate` 的「同一筆記錄的 legacy 拷貝還在」警告;只有 legacy 檔、沒有 entities/ 那份的不算拷貝——那一類歸第 102 列),該命令的 `--apply` 以結束碼 1 拒絕——`ShadowedLegacyCopyReadersCLITests`/`ShadowedPairCensusCLITests`/`BootstrapPlanBlockersCLITests` 釘住,對照組是只有 person 拷貝時 venues 照寫。~~legacy 殘留數 > 0 時三個 bootstrap 都拒絕~~(b36 verify LOW 11、18:殘留數 > 0 不蘊含拷貝,也不蘊含每個 bootstrap 都讀到它)(歷輪補記見量測文件第 91 列) |
+| 92 | **零實例,而防線移植時刪除的範圍被放大了**(#692 b33 verify X2 第 0/1 列:`akashic web-read landing`/`check` 刪 `--out`/`--raw` 指的路徑用 `FileManager.removeItem`——對目錄**整棵遞迴刪除**,而且在讀任何輸入之前就刪;被移植的 Python 用 `os.remove`,遇到目錄失敗、不刪。) | ✅ **寫(`WebRead.removalProblem`/`removeFile`:參數先驗,不存在、一般檔、symlink(刪連結本身)才刪,目錄與其他型態以 1 具名拒絕、什麼都不刪;只呼叫 `unlink`;刪不掉除 ENOENT 外具名回報,讀回的 JSON 刪不掉時 READ-OK 改成 READ-FAIL)** | 第 89 列的理由是防線住在抄本裡;這一列是**搬進 CLI 的那一步自己改了語意**:同一個「刪掉上一輪的檔」,Python 的 `os.remove` 對目錄是失敗、Foundation 的 `removeItem` 是遞迴刪除,而兩者在一般檔上長得一樣,所以移植時看不出差異。裁決回到移植前的語意(只刪檔),並把「先驗後刪」寫成順序:任何一個參數不合就什麼都不刪。**觸發條件可檢查**:`WebReadRemovalCLITests`(真 binary:三種參數各指向含 sentinel 的目錄、`--out .`、symlink)全綠即零;實例要看的是使用者回報(刪除沒有留下任何紀錄)(歷輪補記見量測文件第 92 列) |
+| 93 | **零實例,而同一個主機形狀的判斷有兩份,開分頁前的那一份比較寬、也比較早**(#692 b33 verify X2 第 16/39 列:開分頁之前的「完整網址」檢查是 web-access.md 裡的一條正則,由模型照著看;落地之後的 `WebRead.hostIsAcceptable` 是程式。) | ✅ **寫(`akashic web-read url --file`:整串形狀同那條正則,主機另過 `hostIsAcceptable`;`privateSuffixes` 加五個特殊用途名稱;文件那一格改成呼叫它,〈開始前〉第 4 點多一條探測。b36 verify 起整串形狀的頂層名稱也收 `xn--` 形(先前只收字母,比 `hostIsAcceptable` 嚴)、網址檔不修剪(檢查的就是 `$(cat …)` 開出去的那一份),開分頁、導航、取 API 的區塊自己跑它)** | 第 37 列與第 21 列講的是謂詞答不錯或不存在;這一列的謂詞存在、也答得對,**錯在有第二份**——寫在散文裡的那一份由模型執行,比程式那一份寬,而且排在它之前、在請求送出之前。所以裁決不是修正則,是把它換成呼叫同一個函式(`swift-is-the-implementation-language` 放置表第 3 列);特殊用途名稱一併補進那一份(只有一份了,補一次就兩邊都有)。**觸發條件可檢查**:`WebReadTests.testUrlModeSharesTheHostCheckWithLanding` 釘住兩邊同一個判斷;web-access.md 的「完整網址」一格裡不再有正則(`grep -c '\^https://(\[A-Za-z0-9-\]+' plugin/skills/akashic-bootstrap/references/web-access.md` 應為 0)(歷輪補記見量測文件第 93 列) |
+| 94 | **零實例,而它守的是「先看、後寫」之間的窗口——比對 inode 與大小只縮小它**(#700 b33 verify X5 第 0 列 MEDIUM:`ensureLayout` 先 `inspectSourcesIgnore`、建完佈局才 `applySourcesIgnore`;附加前以 device/inode/大小比對代替鎖。live registry 的 store 已有區塊——不會再走到寫入,0 實例) | ✅ **寫(開檔後 `flock(LOCK_EX)`、確認路徑仍指著這個檔、鎖內重讀內容再決定;先看在讀內容之前取 `flock(LOCK_SH)`、等持鎖的寫入者寫完(先看與鎖內重看是同一個判斷,b34 修正輪補);`EEXIST` 與換檔重看(至多 4 次),重看之後區塊已在就算成功;回滾只截這一次寫的、`ftruncate` 的結果要看。同輪補四個不改寫的形狀:有其他硬連結、大到 git 不讀、有標記沒有排除規則、symlink 打不開)** | 第 87 列關掉的是「讀不懂折成空的」;這一列是同一段程式**看與寫之間**的事:一個只縮小窗口的比對被文件寫成保證,而失敗的樣子是兩個程序都以為自己是唯一的寫入者。所以裁決要能排序,不是更精細的比對——鎖讓同時跑的 akashic 排隊,鎖內重讀讓排在後面的那一個看到前一個的結果;`EEXIST` 從「被換掉」改成「重看」,因為它的意思通常正是「別人剛做了你要做的事」。**觸發條件可檢查**(量測文件第 87 列的腳本):註冊的 store 裡會被拒的形狀數應恆為 0;並行的回歸由 `GitignoreConcurrencyCLITests` 釘住(含「持鎖、寫到一半時先看要等」那一支)(歷輪補記見量測文件第 94 列) |
+| 95 | **零實例,而它守的是一句理由會不會落地——沒有記錄要寫時,寫入閘與報告都看不到它**(#564 b34,使用者 2026-10-05 裁決第 1、2 點;b33 X1 第 0/2/3/8/11/13/18/23 列。person `fields.names` 的替換兩道拒絕:(a) 對外形在替換裡整個離開 names——有沒有記錄都一樣,移出要寫「撤回:理由」、記錄錨定 names,所以要留在 variant;(b) 附了理由而沒有名字進出 authorized——只替進出的名字寫,留下的不寫「確認」,理由沒有地方放。) | ✅ **寫(入口整批拒絕、零寫入;拒絕說出放在 variant 的出口,format < 22 時另說寫入閘)** | 第 82 列守的是入口的上限與理由形狀;這一列守的是**理由有沒有地方落**:「判定必附理由」只在理由寫進記錄時兌現,一條不寫記錄的路照樣收理由,就是讓必填變成字面。兩道拒絕各封一條這樣的路——(a) 名字離開 names 時記錄沒有地方錨定,(b) 沒有名字進出時沒有記錄要寫。拒絕而不是自動把名字留在 variant:整份替換的契約是「提及的欄位整個換」,程式替呼叫端改它送的清單會讓結果與請求不同(這一句是實作對裁決第 1 點的讀法,~~交給使用者~~ → 使用者 2026-10-05 05:18Z 裁:維持現狀)。**觸發條件可檢查**(腳本見量測文件):這兩道拒絕沒有 store 上的實例可數;量的是前提——名字分類記錄數與有對外形的 person 數;記錄數開始長時(format 升到 22 之後),回來看有沒有對外形在沒有撤回記錄的情況下從 names 消失(歷輪補記與量測見量測文件第 95 列) |
+| 96 | **零實例,而回應的體積由 store 內容決定、沒有上限**(#564 b34;b33 X1 第 22/27/34 列。organization 的 `remove_names` 報告每個刪掉的名字回 `segments`——R2 加的,段數與每段的 attested 項數都不封頂,一個名字 3,000 段時一次回 1,968,449 位元組,MCP 面整份進 LLM context;沒有時間、source、note 的段回 `{}`、不帶任何資訊。b36 Y1 第 0/1/13 列:b34 只截了段數,每段的 attested 照樣整列——一段兩萬個觀測點一次回 460,506 位元組。) | ✅ **寫(報告截在 20 段,總數照報;不新增回應鍵。b37 起每段的 attested 也截在 20 個,最後一項說出省略數與總數——venue `edit_name_segment` 的 before/after 同一個上限,讀取面全列)** | 第 30/45 列的上限擋的是單次呼叫送進來的量;這一列擋的是**單次呼叫送出去的量**——它由 store 內容決定,呼叫端無法在收到之後丟掉已付的代價(#236 的威脅模型),而同一族的 venue `edit_name_segment` 早在 20 項截斷。截在服務層、CLI 與 MCP 同一份(不分面:刪掉的段的完整內容在 git 的上一版,報告只是索引)。**觸發條件可檢查**(腳本見量測文件):同一個名字的段數逼近 20 時,回來看報告的截斷是否還夠用(歷輪補記與量測見量測文件第 96 列) |
+| 97 | **零實例,而一次搬家讓只該有一份的東西各多了一個可以住的地方**(#711 b35 M1,使用者 2026-10-05 裁決:規則檔拆成規則檔+量測文件,量測寫法抽成規則 `measurement-commands-self-prove`。那份規則引用的模板是守衛 `selfProofTemplate` 的第二份描述;棘輪標記原本與量測同檔,現在兩份檔都放得下它。2026-10-05 量測:規則與守衛的模板一致、規則檔裡的棘輪標記 0 個。)| ✅ **寫(規則裡的模板與 `selfProofTemplate` 逐字相同,規則不見也紅;棘輪標記只住量測文件,規則檔裡出現即紅;量測文件不在即中止)** | 第 13 列的自證與第 88 列的棘輪都假定模板與地板各只有一處;這一列是那個假定被一次搬家打破:同一份模板有了第二份描述(`no-compat-fallback`〈同一件事只能有一份描述〉——兩份說的是同一件事,而規則要給寫量測的人讀,不能只寫一句引用),同一個地板有了第二個可以寫的檔(兩份各一個時下限有兩個數,哪個算數沒有答案)。不寫的代價是安靜的分岔:照規則寫的量測被守衛判紅、或規則放行的寫法守衛不收,兩邊各自看起來都對。量測文件不在時中止、不當成沒有量測,是第 3 列。**觸發條件可檢查**:守衛這三則訊息的數應恆為 0(量測見量測文件第 97 列) |
+| 98 | **零實例,而合併的結論取決於被併者的 key 拼法**(#564 b36 Y1 第 12 列:合併搬名字分類記錄時被併者依 key 排序,第 85 列的保險只看全部接完之後的最後一筆——一筆被併者自己就不一致(以撤回結尾而名字仍在分割裡,或以指定/確認結尾而不在:手改,或修正輪之前的工具),另一筆的一致記錄排在它後面就蓋住矛盾、放行;把不一致那一筆的 key 改成排在後面,同一份內容被拒。2026-10-09 唯讀量測 live store:名字分類記錄 0 筆、store format 18。) | ✅ **寫(被併者帶來的某個(分割, 名字)在它自己的記錄裡最後一筆就與分類矛盾時,preview 與實跑一律拒絕並說出那筆被併者,不論後面接上的記錄會不會蓋住它;倖存者原本就有的矛盾照舊不擋)** | 第 85 列的保險假定「全部接完之後的最後一筆」說得出合併有沒有帶進矛盾;這一列是那個最後一筆**由誰排在最後決定**,而排序的依據是 key——一個不承載任何意義的字串。store 沒有時間資訊說兩筆被併者的記錄哪一筆較新,依字典序決勝就是讓政策預設代替判定(`disambiguate-before-irreversible-writes`),而合併不可逆。拒絕而不是改成「誰都不蓋住誰」:被併者自己不一致的那一格只有人能判斷哪一筆對,出口在拒絕訊息裡(在那筆被併者上補一筆與分類一致的記錄)。這一格在零實例的形狀上變嚴:b34 的測試曾把「一致的記錄排在後面就放行」寫成期望(`testTheDecisionDoesNotDependOnTheOrderTheDoomedAreGiven` 等三支),同批改寫。**觸發條件可檢查**(腳本見量測文件第 98 列):名字分類記錄開始長時(format 升到 22 之後),回來看有沒有被併者自己不一致的記錄 |
+| 99 | **零實例,而被掃的檔 PR 改得到——判讀的成本本身就是要守的東西**(#711 b37,b36 verify 第 1、2、19 列:`zero-instance-rows-audit` 在一長串空白行與計數選項的回溯上是二次方,verify 量到四萬個空行 41 秒;b37 再找到三條同類的路徑——行中一長串空白、相鄰 inline code 的接續、表的編號逐列掃 Sources。`census-parity.yml` 以 `pull_request` 觸發、跑在 macOS runner,`audit-guards-mutations` 再在副本上跑這支幾十次。2026-10-09 量測:最大的檔 256,003 位元組、最大的 fence 152 行、依閘去重 41 條,離上限都在十倍以上,repo 裡超過上限的 0 個。) | ✅ **寫(判讀改成線性;檔 4 MiB、fence 與段落各 1,500 行與 128 KiB、依閘去重 500 條各一道上限,超過即具名紅、不判讀那一塊;`audit-guards-mutations` 以 10 倍輸入的後設檢查與限時的格子釘住)** | 前面各列的失敗都是**答案錯**——漏報、假訊號、說了一句沒打算說的話;這一列的失敗是**答案對、但要很久才給**,而那個「很久」由別人寫的檔決定。不寫的代價落在 runner 的帳單與一個沒有逾時的 job 上。上限超過時**紅而不截斷**:截斷了再判讀會把截掉的量測當成不存在——第 3 列「未涵蓋不得冒充通過」。線性與上限兩者都要:只有上限,上限之下的二次方仍在(幾千個各 1,500 行的 fence);只有線性,常數乘上一個上百 MB 的檔仍是幾分鐘。**觸發條件可檢查**:守衛的上限訊息數應恆為 0(量測見量測文件第 99 列);接近上限時先問那份檔是不是該拆,再談提高上限 |
+| 100 | **零實例,而前置條件比的是名字、不是那個路徑**(#711 b37,b36 verify 第 0、8、9、15、20 列:R4 的前置條件只認 `--library` 後接一個空白或 `=`、只比變數名,`--library"$V"`、`--library "$V/sub"` 配 `test -f "$V/store.yaml"`、`$1` 都過關;`doctor` 不帶 `--library` 也過關,而第 73 列的散文要它帶;辨識認不出 `"$AKASHIC_BIN"`、`<(…)`、寫檔再讀、`<<<`。2026-10-09 量測:量測文件 40 條合模板的量測全部滿足新條件,用 `doctor`、`enrich`、`add-venue` 的都帶 `--library` 與對應的 `test -f`。) | ✅ **寫(`--library` 的分隔與 shell 相同、`test -f` 逐字是它的引數加 `/store.yaml`、`$` 後面不是名字即紅、`akashic` 的子命令不是 `WriteGateRulings` 的 `.readOnly` 時要 `--library`;變數名以 akashic 開頭的展開、`<(`、`<<<`、寫檔再讀都算 binary 的輸出送進一個命令)** | 第 90 列把前置條件寫進模板;這一列是那個前置條件**比對的對象錯了**:它比變數名,而 shell 執行的是整個引數。辨識那一半同形:R4 的「進任何命令都算」只看管線,而 binary 的輸出還有三條路進一個命令。唯讀子命令的清單**不另列**:讀 `WriteGateRulings` 的 `.readOnly` 格(`no-compat-fallback`:同一件事一份描述),讀不到時每一條 `akashic` 的量測都要 `--library`——偏向紅(使用者 2026-10-05 裁決)。**觸發條件可檢查**:守衛對前置條件的訊息數應恆為 0(量測見量測文件第 100 列) |
+| 101 | **零實例,而背書的範圍寫寬了——小節標題的編號說的是哪一列,不是何時量的**(#711 b37,b36 verify 第 17 列:`measured-numbers-audit` 把「標題到該行之間有 `#NNN`」當成時間錨;量測文件的歷輪補記小節標題都是「### 第 N 列(#NNN)」,那些小節下的數字都被標題自動背書,b35 補的負控又插在沒有編號的標題之下。2026-10-09 量測:標題的 `#NNN` 不算之後,四個來源裡新出現的裸數字 0 個。) | ✅ **寫(標題那一行的 `#NNN` 不算時間錨,標題裡的日期照算;負控插在真的小節標題之下)** | 第 2 列的理由是「前件寫寬了會誤傷」;這一列方向相反——寫寬了會**放行**,而放行時守衛照樣數到那些數字、照樣印「全部都有時間錨」。負控插錯位置是同一件事的另一面:它在一個不具代表性的地方證明守衛會紅,對實際結構下守衛看不到的那一大塊沒有暴露任何東西——第 5 列「守衛對自己的覆蓋率說謊」。**觸發條件可檢查**:守衛報的裸數字應恆為 0(量測見量測文件第 101 列) |
+| 102 | **零實例,而拒絕只認一種重複——判準照著一個標記寫,另外兩種讓計數加倍的狀態不帶那個標記**(#709 b36 verify MEDIUM 4,使用者 2026-10-09 裁決 2。第 91 列的拒絕只問 load 的拷貝標記,而標記要求 entities/ 有同一種、同一個 id 的那份;兩個 legacy 檔共用 id、或 entities/ 與 legacy 同 citekey(同 key)而 id 不同,validate 都報 error、計數同樣加倍,先前乾跑沒有附註,`--apply` 寫入 person 之後才以 index 的 UNIQUE 失敗收場。2026-10-09 唯讀量測 live store:legacy 殘留 `entries/` 0、`people/` 0,entities/ 裡重複的 citekey 0、重複的 person key 0。) | ✅ **寫(`LibraryLoad.planBlockers`:拒絕與附註的條件是「load 標出的拷貝 ∪ 計畫來源種類中無法唯一定位的記錄」——works 問 `unlocatableCitekeys`、people 問 `unlocatablePersonKeys`,與 `authorize-names` 一致;乾跑逐筆點名,同一對拷貝的兩份不重複列)** | 第 91 列的理由是揭露與寫入在同一次執行裡;這一列是那道拒絕的**判準照著一個標記寫**:標記是為 index 的「以 entities/ 為準」設計的,只認同一筆記錄的兩份,而讓 bootstrap 計數失準的是「store 說不出哪一份才算」——那正是「無法唯一定位」的定義,各寫入面早已問它。所以不另寫一份重複的判準,問同一個集合(`no-compat-fallback`〈同一件事只能有一份描述〉)。**誠實邊界**:「無法唯一定位」的第 3 類(load 判定檔案寫入時會被拒——legacy 單份不受 git 追蹤、有未提交修改、目的檔被隔離)不讓計數加倍,也照樣擋——那是裁決的字面(與 `authorize-names` 一致),代價是 store 不在 git 裡時,計畫讀到的那一種只要有一份 legacy 殘留,`--apply` 就拒絕,直到它被搬移或刪掉(2026-10-09 真 binary 實測,`BootstrapPlanBlockersCLITests` 釘住);venue/organization 的重複 key(#669)不在條件裡,裁決沒有列它們。**觸發條件可檢查**(腳本見量測文件):重複的 citekey、person key 與 legacy 殘留數應恆為 0;非零時 `akashic validate` 逐筆說出原因,三個 bootstrap 的乾跑點名受影響的那幾筆 |
+
+## 各列共通的東西(觀察,不是判準)
+
+第 1–9 列與第 13、14 列的裁決都是「寫」(第 14 列是 2026-09-09 從「不寫」翻過來的)、第 10–12 列(皆出自 #365)是「不寫」,但**理由各不相同**,這正是不寫總括判準的原因:
+
+- 第 1 列的理由是**失敗的不可見性**(不寫 → 第一次發生時沒跡象)
+- 第 2 列的理由是**前件的精確度**(要不要寫沒有疑問,寬度才是問題)
+- 第 3 列的理由是**沉默的歧義**(守衛本身沒問題,它的「沒說話」有問題)
+
+- 第 4 列的理由是**錯誤的偽裝性**(守衛沒問題、沉默也沒問題,問題是猜錯的結果**看起來
+  是對的**)
+- 第 6 列的理由是**訊號可信度的外溢**——前五列的失敗都關在自己那一格裡,這一列的
+  失敗會讓人開始不相信別格的紅燈
+- 第 5 列的理由是**覆蓋率的自我謊報**——前四列講的都是「被守衛的東西」出了什麼事,
+  這一列講的是**守衛自己**:它多印了一個 ✓,而那個 ✓ 沒有對應到任何新的事實
+- 第 7 列的理由是**兌現範圍的誠實**——它的守衛**通過了也不代表對**,所以它的裁決
+  包含「必須在它旁邊寫出它管不到什麼」
+- 第 9 列的理由是**缺席本身在說話**——前八列問「這個守衛該不該存在」,這一列問「這個
+  欄位不存在時,讀者會推論出什麼」。答案是「機構沒有識別碼可記」,而那是假的
+- 第 8 列的理由是**零的來源在別處**——前七列的「還沒發生」是關於世界;這一列的
+  「走不到」是關於**另一段程式**(load 的 quarantine),而那段程式可以改而不會有人
+  想到這裡。所以它的裁決包含「釘住為什麼是零」,不只是「寫這個守衛」
+- 第 10、11、12 列的理由是三個**不同的**「不寫」——缺用途(形狀由用途決定)、缺理由(既有欄位承載、
+  不成比例)、**位置錯了**(筆名是 Person 側的事實)。三列同出自 #365 而不可互換,正是這張表
+  不寫總括判準的理由再一次實例。區辨全在**觸發後會發生什麼**:第 11 列越過門檻後欄位**進** `Author`、
+  第 10 列用途出現後進 authorship 側(`Author` 上的 `Bool`、或 `Entry` 上的 `Set`——由那個場景決定)、第 12 列**永遠不進** `Author`(落點在 #386 那一族)
+- 第 13 列的理由是**跡象住在錯的地方**——它與第 1 列(缺跡象)、第 8 列(零的來源在別處)最像而都不同:跡象存在(三張 issue 的家族史),零也是真的零(#460 清理過之後的零),問題是每次都靠人記得去看。守衛各列(第 1–8 列)問「守衛該不該存在」,這一列問「已經發生過的形狀,為什麼還是零實例」——答案是場外機制,而場外機制不會自己跑
+- 第 16 列的理由是**裁決依賴守衛**——前十五列的守衛是裁決的結果,這一列的守衛是裁決的**前提**:「暫不改形狀」只有在「漲到門檻時工具會出聲」為真時才站得住。它的零是「還沒漲到」,與第 1 列(還沒發生)、第 13 列(掃乾淨之後)、第 15 列(在這台機器上)都不同
+- 第 15 列的理由是**零是本機的**——前十四列的零不取決於在哪台機器上量,這一列換一台機器就非零;它與第 13 列(時間上的零)成對:一個是「掃乾淨之後」,一個是「在這台機器上」。任何只在本機量的守衛都會對它報綠,所以掃描要住在每台機器都會跑的 `StoreHealth` 裡
+- 第 19 列的理由是**被守的是辯護不是事實**——前十八列的失敗都會讓某個可判定的性質變錯(數字偏了、檔案不在、記錄形狀壞了),這一列的失敗**不讓任何東西變錯**,只讓「每列理由彼此不同」這個說明缺一角。而那個說明正是本檔不寫總括判準的依據,所以它缺一角就等於判準悄悄回來了
+- 第 18 列的理由是**零量的是出口而守衛裝在入口**——前十七列的零都是關於已經在庫裡的東西,這一列的零(庫內最長 4,220 bytes)與它要防的東西(adapter 這一次遞過來的字串)不是同一個母體。與第 16 列(已知會漲的曲線)最像而不同:沒有曲線,實例會由一次外部呼叫一步跨過,而 #516 verify 已經一步跨過 80 倍
+- 第 17 列的理由是**寫入面剛長出來**——與第 13 列(掃乾淨之後)最像而相反:那一列是形狀發生過、被場外機制清掉;這一列是形狀發生過 4 次、從未被記下,因為記錄它的值域在本 change 才存在。零從第 5 次拆分起才可能被打破,而守衛必須在那之前就在
+- 第 22 列的理由是**它不是程式**——前二十一列講守衛與欄位,這一列講 spec 裡的一條 Requirement,而它的失敗是「被當死重刪掉」,不是漏報也不是錯誤的缺席斷言
+- 第 21 列的理由是**既有守衛的謂詞對那一類成員結構上不可能為真**——它不是漏看,是問了一個 glob 成員答不錯的問題(「列了卻不存在」對只會變少的成員永遠為假)。前二十列的零都是關於「還沒有這個守衛」,這一列的守衛在、而且天天跑
+- 第 20 列的理由是**窮舉自己漏了一格**——與第 1 列(缺跡象)、第 13 列(跡象住在錯的地方)都不同:跡象就在同一張 issue 裡、就在眼前,而窮舉它們的人仍然漏了第四個,是**別人**找到的。守衛換掉的不是注意力,是「這件事需要注意力」這個前提
+- 第 23 列的理由是**可達性是本 change 自己造出來的**——第 17 列的零同樣是「寫入面剛長出來」,但那一列的形狀早已在庫外發生過 4 次;這一列的**矛盾狀態**在本 change 之前根本不可達(沒有面能把作者位清空),是新面把它變成可達的。讓它可達的那一步與守衛因此必須同批落地
+- 第 14 列的理由是**判準的輸入未定**——與第 10 列(缺用途)同形而不同:那一列缺的是形狀由什麼決定,這一列形狀確定、缺的是它要比較的「相等」的定義,而定義在 #470 手上。寫了就是替 #470 預先裁決。**2026-09-09 #470 裁決後本列翻成「寫」**,理由隨之變成第二個、也是本表目前唯一的一個:**觸發條件是機械可檢查的,所以它自己到期**——不像第 10、12 列要人指認
+- 第 25 列的理由是**入口有五個**——第 18 列的「零量的是出口而守衛裝在入口」對它也成立,但那一列只有一個入口;這一列三輪 verify 各修一個入口,第四、第五個仍開著。守衛的位置本身是裁決的一半:寫在 store 邊界,不寫在任何一個面
+- 第 27 列的理由是**同一條不變式的另一半要有自己的守衛**——第 26 列的閘與守衛守的是「一條邊」那一半;「一個 literal」那一半的違反由另一條路徑造出、第一半的燈不亮。兩半各自要一盞燈,否則「應恆為 0」只量了一半
+- 第 26 列的理由是**閘與守衛是同一條不變式的兩半**——第 25 列把守衛裝在 store 邊界,因為入口有五個;這一列的三個閘(D25/D27/D28)已把工具面全關,剩下的入口是閘擋不到的手改與舊 binary,守衛守的正是那兩個。與第 23 列成鏡像:那一列是新面讓不可達變可達,這一列是新閘讓可達變不可達(對工具面)
+- 第 29 列的理由是**另一個守衛的正確性依賴它**——第 16 列是裁決依賴守衛(門檻到了才重開),這一列是等價性證明的前提(sink 上限 ≤ ceiling)由它守;零是兩個數字今天恰好對齊,改任一邊都是一行 diff 而每個 sink 自己的測試都不會紅。地板要分族,否則一族的零命中被另一族撐成通過(R31 verify 第 6 列)
+- 第 28 列的理由是**可達性是本 change 的另一條裁決刻意造出來的**——第 23 列是新面讓不可達變可達;這一列是為了不刪判定(D62)而選擇留下一個狀態,守衛是那個選擇的另一半:留著而看不見,等於把刪除延後到合併、把回報搬到另一個命令
+- 第 30 列的理由是**被守的記錄不退役**——第 18 列守的是一次遞進來的字串,這一列守的記錄只增不減,所以上限只擋得住單次呼叫,累積要另一盞燈(venue 側是第 16 列、person 與 organization 側是第 31 列)
+- 第 32 列的理由是**前件寫寬的方向是放行**——第 2 列怕寬了誤傷,這一列寬了會放行一個看起來是決定性證據的假號;修在共用的謂詞,而且換成必定失敗的字元而不是濾掉
+- 第 33 列的理由是**同一個檢查只寫了三分之一**——跡象不是住錯地方(第 13 列),是根本不存在,而同形的檢查就在隔壁;補的是既有守衛的前件,不是新守衛
+- 第 34 列的理由是**補完只補了一側**——第 33 列照 work 的欄位窮舉,指向同一個目標(organization)而住在別的形狀上的兩條邊沒被看到;窮舉的軸要是「指向誰」,不只是「住在誰身上」
+- 第 35 列的理由是**同一個取捨在另一類欄位上開的格**——第 28 列照判定記錄那一格,位元組相等的去重在非判定 reference 上收下的變體沒有燈
+- 第 36 列的理由是**假號的來源是合法的號掛錯地方**——第 4 列是猜錯的結果看起來對、第 32 列是謂詞寬到放行假號;這一列的號本身合法,錯在它屬於哪本刊,只有跨記錄比對看得到,且守衛要與移除面同批,否則 warning 沒有工具面的出口
+- 第 37 列的理由是**舊謂詞對新形狀結構上不可能為真,而正確的謂詞不存在**——第 21 列換一個謂詞就好;這一列換不了,因為「該不該閘」是裁決不是性質,所以守衛只要求每一格都有裁決並寫出自己的理由(第 7 列的形:通過不代表理由對)
+- 第 38 列的理由是**錯在標題不在內容**——第 4 列是猜錯的結果看起來對;這一列聯集裡的每一則都是真的,錯的是「這一筆」這個標題,而讀的人正是為了那個標題才指名。守的是讀取面的定址,不是 store 的內容
+- 第 40 列的理由是**拒絕問在錯的時間點**——第 21 列是謂詞問了那一類成員答不錯的問題、第 13 列是跡象住在錯的地方;這一列的謂詞答得對、跡象也在,錯在答案到達時寫入已經進行到一半。修法是同一組判斷換一個時間點再問一次,不是寫第二份判斷
+- 第 41 列的理由是**同一個值在兩層有相反的身分**——第 32 列放行的是形狀看起來對的假號;這一列的值形狀本來就對,錯在它不指認任何東西。閘在共用謂詞,而且要把謂詞拆成位址與引用兩個——否則為了守零實例而加的收緊會把 live store 的 `store-source` 關掉
+- 第 42 列的理由是**沒有檢查的那一格讓讀取端崩潰**——第 33 列的缺口是少報,這一列的缺口是 trap;重複 key 的檢查補在 `crossRecordIssues`,查找表改成不 trap,以 key 定位寫入的面拒絕
+- 第 43 列的理由是**合併是拿掉沿革的那一步**——第 22 列替沿革留位置,這一列守位置的另一端:落進去的東西不得在合併時安靜消失,而哪一段的時間對是判斷,合併不猜
+- 第 44 列的理由是**既有判準的答案變了**——閘不是新的裁決,是遷移退場之後「有沒有 pre-bump 遷移」從有變成沒有
+- 第 45 列的理由是**上限只擋單次**——同第 30 列,錨點取既有常數;累積由第 16 列出聲
+- 第 46 列的理由是**路由字典安靜地選一筆**——隔壁的重複有檢查、宣稱者的重複沒有,匯入時把欄位寫進別的記錄
+- 第 47 列的理由是**零來自規則還不存在**——第 13 列的零是清理過之後的零;這一列的形狀發生過一次(52 筆),今天的零是因為 live store 的 library 都還沒標性質。燈照的是寫入閘擋不到的三個來源(舊 binary、手改、set-kind 當下就不符的既有成員),同第 26 列閘與守衛的分工
+- 第 48 列的理由是**新開的通道讓偽裝可達**——同第 4 列,角色掉了之後記錄看起來健康;可達性是 #587 的寫入面造成的(第 23 列的形),所以閘與新面同批,而 reference 那一半改成搬而不是拒絕
+- 第 49 列的理由是**合法狀態的可見性**——前面各列的零是壞形狀還沒發生;這一列的形狀是合法的,缺的是能看見它的判準,而判準只有一份
+- 第 50 列的理由是**「在不在」不等於「是不是一份檔」**——同第 1 列不寫就沒有跡象;與第 15 列相鄰:那一列問位元組在不在這台機器上,這一列問那個位置上是不是一份檔
+- 第 51 列的理由是**移植會換掉意外的紅燈**——同第 3 列未涵蓋不得冒充通過;舊實作的紅是 glob 的副作用,換語言時要寫成顯式判定
+- 第 52 列的理由是**進度量測不得往終局偏**——普查的數字就是 literal 歸零的進度,少算一個檔等於假進步,所以整次拒絕輸出
+- 第 53 列的理由是**同一個依據兩個時間點給相反的答案**——設定時擋、使用時放,是第 26 列那條不變式只剩一半;排除清單懸空則讓同一份程式對「是不是參照」答兩種
+- 第 54 列的理由是**守衛的出路要走得通**——一律拒絕而出路是死路的守衛等於沒有出路;改名本來就該遷移指向舊 key 的邊,三個前置拒絕擋住遷移本身的安靜錯誤
+- 第 55 列的理由是**判準已經有了,只是沒套到這種檔**——同第 44 列既有判準的答案變了;registry 檔不在 `entities/`,移除面一族的可回溯要求原本照不到它
+- 第 56 列的理由是**守著它的閘只擋一條路**——同第 4 列,對不回的來源讓再匯入安靜地造出攣生;合併閘管不到手改與舊檔
+- 第 57 列的理由是**讀回後核對身分**——同第 4 列,別筆的回應看起來是對的;那個形狀在頁內 fetch 已經發生過,這裡把紀律落實成程式
+- 第 58 列的理由是**半套的數字不得冒充全部**——同第 3 列,缺記錄時預設不出數字
+- 第 59 列的理由是**形狀取決於還沒出現的用途**——同第 10 列,ordinal 與「全收成一筆」語意不同,先具名拒絕是可逆的
+- 第 60 列的理由是**上限沒有量測依據、也不是位元組界線**——同第 18 列的出口與入口,差別在錨點:25 是選的數字;首版拿各字串的字元上限相加當位元組的算術並不成立(b13f R1 verify)
+- 第 61 列的理由是**理由只有一份,所以只截第三方內容**——同第 60 列上限是選的數字;差別在逐筆帶理由,截整筆就等於丟理由
+- 第 62 列的理由是**失敗看起來像成功**——第 1 列的不可見性再加一層:命令回報成功,被刪的是使用者原本就有的資料
+- 第 63 列的理由是**答不出來被讀成否**——第 3 列同形,而閘是隱私閘,外流進 remote 不可逆
+- 第 64 列的理由是**解析成功而值不是來源的意思**——第 4 列的偽裝性落在解析層,兩個實作對同一份輸入安靜地分岔
+- 第 65 列的理由是**第五份推導不會有跡象**——第 13 列同形,四份併成一份之後由源碼掃描守單一路徑
+- 第 66 列的理由是**秘密一進 git 就收不回來**——第 18 列的入口守衛,對象是 url 裡的帳密
+- 第 67 列的理由是**沒有讀者就沒有漂移的受害者**——第 10 列同形的「不做」,觸發條件是第一個讀者
+- 第 68 列的理由是**清單由一次匯入一步跨過**——同第 18、30 列,上限保護的是 LLM context
+- 第 69 列的理由是**第一個寫得進沿革位置的面**——同第 45 列的單次上限;第 22 列保留位置、第 43 列守合併,這一列守寫入
+- 第 70 列的理由是**清空它的是一次計費裁決**——第 13 列同形的「清理過之後的零」;機制留著是因為成本決定可以翻,而它留著的那條路要自己有負控(清單因此是資料檔);#711 起清單還記缺口多大,因為「接受」是對一個具體大小說的
+- 第 71 列的理由是**偵測不得與它要補的抽取共用失敗條件**——第 21 列換一個謂詞就好;這一列補的是另一支守衛的輸入,補丁若與原抽取共用 regex,漏掉的寫法兩邊一起漏,零實例量到的只是「看不到」
+- 第 72 列的理由是**上限是一次裁決給的數字**——第 18 列的上限夾在兩個量測錨點之間,這一列只有一個錨點(最大單檔的 48.8 倍);批次入口逐檔略過而不整批拒絕,因為那個入口的其他「這個檔不能收」本來就逐檔具名
+- 第 73 列的理由是**殘留與進行中長得一樣**——第 15 列的零是本機的、第 23 列的可達路徑是本 change 自己開的;這一列的東西是工具自己的失敗路徑必然留下的,而它與正在進行的存檔在磁碟上分不開,所以守衛只報不刪
+- 第 74 列的理由是**沒有單一出口**——第 65 列同形,由源碼掃描守;CLI/MCP 的範圍開在進入點與分派層,新命令不必記得,App 沒有那一層,每個寫入點都得自己開,而忘了開的後果只在 legacy 拷貝刪不掉時才看得到
+- 第 75 列的理由是**少算正是上限要擋的方向**——第 52 列同形(略過讀不懂的等於少算),落在一個保護使用者機構 session 的上限上,所以讀不懂就拒絕、不略過
+- 第 76 列的理由是**驗的與存的必須是同一份位元組**——第 62 列守輸出,這一列守輸入:別人給的路徑可能是 symlink,也可能在兩次讀之間被換掉
+- 第 77 列的理由是**閘與守衛同批長出、共用一個分類**——第 26 列的閘與守衛各自成形;這一列兩者讀同一個 `sourceOccupant`,因為 R2 之前四個讀者各自判,對同一個佔用給出三種答案。與第 73 列成對:那一列報自己的失敗留下的,這一列報別人留在位址上的
+- 第 78 列的理由是**讓狀態可達的面與拒絕同批落地,而拒絕的位置早被預告**——第 23 列的矛盾狀態由新面造出、第 26 列的閘與守衛是同一條不變式的兩半;這一列兩者兼有:名字分類面讓「人判定過的對外形」寫得進 store,合併拒絕讓它不被安靜降級,寫入閘讓舊 binary 不被餵讀不懂的記錄。與第 8 列成鏡像:那一列的零由別處的程式造成、可能被改掉而沒人知道;這一列的零由 store marker 還在 18 造成,marker 一升就變成有實例,所以拒絕的測試與「機械值仍是提醒」的釘零要在 marker 升級之前就存在
+- 第 79 列的理由是**降級把拒絕從共用段挪到只有一邊走得到的地方**——第 40 列的拒絕問在錯的時間點、第 8 列的零由別處的程式造成;這一列兩者都不是,是降級讓共用的閘變窄、順帶被擋住的格子各自找新家,而只裝在實跑那一邊的讓 dry-run 說謊(#139 F1 的同一個形狀)
+- 第 80 列的理由是**上限是一個沒人量過的裁量值,而且它讓一條使用者裁決在尾端不成立**——第 72 列的上限至少有一個使用者給的數字、第 18 列夾在兩個量測錨點之間;這一列兩者都沒有,所以誠實寫成「Claude 裁量、使用者尚未確認」,並把它的代價(沒記下來的列讓 CLI 非零)與可檢查的重開觸發寫在同一列
+- 第 81 列的理由是**先前靜默的覆寫變成具名的拒絕,而零是因為資料少、不是因為形狀不可達**——第 23 列的矛盾狀態由新面造出,這一列相反:它擋的是舊行為(互相覆寫);零實例來自 live store 只有一筆歧異記錄,rename 之後的過期 id 是真實路徑。第一版借用泛用的 #631 拒絕、把原因說成形狀、出路指向循環——訊息與守衛同批要對,否則守衛擋對了、使用者卻被引到錯的處置
+- 第 82 列的理由是**被守的記錄只追加、移除面不刪**——第 30/45 列的上限只擋單次、累積由預算出聲;這一列的記錄是判定史,唯一的刪除路要先撤回,所以入口先擋;理由形狀的那一半是文法不是上限(開頭的組合字元讓記錄讀不出來)
+- 第 83 列的理由是**零是磁碟區種類的零,且不補替代路**——第 15 列的零是本機的(資料換一台機器就缺席),這一列是同一份程式換一種檔案系統就變;與第 13 列對照:那一列清理過之後的零靠人記得,這一列靠一道在建立任何檔案之前問磁碟區的閘。替代路被實測否掉過一次(inode 漂移讓清理全部失效),所以裁決是拒絕而不是再補一條
+- 第 84 列的理由是**收回一則放寬裁決被實作放寬過頭的部分**——第 75、76 列守的是一個上限與一條路徑;這一列守的是裁決的邊界:裁決 1 把別的主機從整批暫停改成交給人,實作把交給人給了裁決沒有點名的頁面(沒載完的、登入頁、Safari 的錯誤頁),而交給人時批次繼續。判準是裁決的字面,不是「看起來沒事」
+- 第 85 列的理由是**被守的是一道規則與它造出的記錄之間的一致**——第 78 列守合併不替人改判定,這一列守合併接上去的記錄本身說的話;尾端矛盾時,按位置讀記錄的刪名字閘與去重會得出與分類相反的結論。主修在搬法,保險只剩來源本身就不一致的那一格
+- 第 86 列的理由是**三種實體裡只有它刪得到沒有名字**——第 25 列因為入口有五個而把不變式放在 store 邊界;這一列的形只有一個入口造得出,所以擋在入口、與兩個鄰居同一句話,不升成 validate 的 error(讀得進來、不毀資料)
+- 第 87 列的理由是**「讀不懂」被折成「空的」,而接在後面的是整份替換**——第 3 列的折疊在輸出(沒被檢查與乾淨長得一樣),這一列在輸入(讀不懂與空的長得一樣);後果不是少報,是一次附加變成一次刪除。所以裁決不是加一道檢查,是讓「讀不懂」有自己的狀態:~~寫入類命令拒絕~~ `file add` 拒絕、診斷面與兩個匯入改報(匯入自使用者 2026-10-05 的裁決起)、能附加時只附加位元組
+- 第 88 列的理由是**辨識是守衛沒寫出來的前提**——第 21 列換一個謂詞就好,第 37 列連正確的謂詞都不存在;這一列的謂詞(模板)是對的,錯在它只對認得出的單位求值。補寬辨識之外另加一個不靠辨識的地板,因為認不出的寫法仍在,而地板是承認這件事的那一半;R4 再承認一次:辨識寫成列舉會在下一輪漏,所以反過來寫(認不出時偏向是量測),地板依閘去重,退場標記拿掉
+- 第 89 列的理由是**防線住在抄本裡**——第 84 列守的是一則裁決的邊界,這一列守的閘本身不新奇(形狀、上限、清理順序),新的是它們先前的位置:文件裡要模型照抄執行的程式,通過時證明的是文件、不是實際跑的那一份;剔除集合也是另寫的一份。裁決因此含「搬進 CLI」這一半,不只是「寫」
+- 第 90 列的理由是**擋它的寫法看起來已經擋住了**——第 13 列的「沒被檢查與乾淨分不開」換一個成因(binary 根本沒跑),而 `${V:?…}` 的字面意思就是「沒設就停」,文字照著寫、三輪 verify 才量出它只停管線的一段;所以把前置條件寫進模板、在任何單位裡判管線中的 `${V:?…}`,不只改散文
+- 第 91 列的理由是**揭露與寫入在同一次執行裡**——第 3 列的守衛沒說話;這一列的守衛說了話卻沒有擋,而 `--apply` 讓揭露與動作之間沒有人。附註留給乾跑,寫入之前擋
+- 第 92 列的理由是**移植改了刪除的語意**——第 89 列把閘從抄本搬進 CLI,這一列是搬的那一步自己把「刪不掉目錄」換成「遞迴刪除」,在一般檔上兩者長得一樣,所以看不出來;裁決回到原本的語意,並把「先驗後刪」寫成順序
+- 第 93 列的理由是**同一個判斷有第二份,而且排在前面**——謂詞本身沒錯,錯在散文裡另有一份比較寬、由模型執行、在請求送出之前的版本;裁決是換成呼叫同一個函式,不是把正則修到一樣
+- 第 94 列的理由是**看與寫之間的比對被寫成保證**——第 87 列管「讀不懂」,這一列管同一段程式在並行時的窗口:比對只縮小它,鎖加鎖內重看才關得掉;`EEXIST` 的意思通常是「別人剛做完」,所以重看而不是拒絕
+- 第 95 列的理由是**理由有沒有地方落**——第 82 列守入口的上限與理由形狀;這一列守的是收了理由之後它寫不寫得進記錄:一條不寫記錄的路照樣收理由,必填就成了字面,而寫入閘因為沒有記錄要寫也看不到它
+- 第 96 列的理由是**送出去的量由 store 內容決定**——第 30/45 列擋送進來的量;這一列擋回應的體積,呼叫端收到之後才付的代價丟不掉
+- 第 31 列的理由是**同一條曲線換了持有者**——第 16 列的燈只照 venue;新面(未決腿)讓 person 與 organization 也開始累積,第 30 列寫下的誠實邊界要有工具面兌現,否則就是一句沒有後續的散文
+- 第 39 列的理由是**單步的跳躍預警看不到**——第 16/31 列的燈在讀取面、照的是漸進的增長;一次寫入從門檻之下直接越過讀取上限時,燈來不及響,擋它的只能是寫入端。而那道閘要擋的不只是位元組:多檔寫入面在它觸發之前已有檔落盤,所以閘與零寫入的 preflight 同批
+- 第 24 列的理由是**半吊子已經誠實**——前二十三列裡只有第 22 列同樣是「不動既有的東西」(比的是裁決的**動作**:那一列的對象是 spec 文字、失敗是被當死重刪掉;本列的對象是程式的半吊子管線、失敗是使用者撞牆或被當成待修殘留而被人動手)。留著的代價不是零(記了就刪不掉——#586 在 2026-09-28 補了移除面,代價從「刪不掉」降為「要人判定放棄」),而是今天未兌現、且一出現就會出聲;動它的兩個方向(實作/拿掉)代價都更高。與第 10 列(缺用途)最像的是理由的**形**:實作那一半同樣是「形狀取決於還不存在的用途」;但第 10 列的對象根本不存在,這一列的對象已經在、且已經誠實
+- 第 97 列的理由是**一次搬家讓只該有一份的東西有了第二個家**——第 13、88 列假定模板與地板各只有一處;拆成兩份檔之後,規則裡的模板與量測文件裡的棘輪標記都可能與另一處分岔,而分岔的兩邊各自看起來都對
+- 第 98 列的理由是**結論取決於一個不承載意義的東西**——第 85 列的保險看全部接完之後的最後一筆,而接上去的順序依 key 排序:被併者的 key 拼法決定了誰的記錄蓋住誰的矛盾;store 沒有時間資訊說哪一筆較新,依字典序決勝是讓政策預設代替判定,所以被併者自己不一致就拒絕
+- 第 99 列的理由是**答案對、但要很久才給**——前面各列的失敗都是答案錯;這一列的成本由別人寫的檔決定,所以判讀要線性、三道上限讓常數也有界,超過即紅而不截斷(第 3 列)
+- 第 100 列的理由是**前置條件比對的對象錯了**——第 90 列把前置條件寫進模板,這一列是它比變數名、而 shell 執行的是整個引數;辨識那一半同形:binary 的輸出除了管線還有三條路進一個命令
+- 第 101 列的理由是**背書的範圍寫寬了**——第 2 列怕寬了誤傷,這一列寬了會放行,而且放行時守衛照樣數到那些數字;負控插在不具代表性的位置,是同一件事的另一面
+- 第 102 列的理由是**判準照著一個標記寫**——第 91 列的拒絕只認 load 的拷貝標記,而標記只認同一筆記錄的兩份;另外兩種讓計數加倍的重複不帶它。改問各寫入面早已共用的「無法唯一定位」,不另寫一份
+
+**「完備」在第 1–7 列裡有三種強度,不是兩種**(#414 R1 自審更正——這一段原本寫
+「前六列在前件內都完備」,而那對第 5、6 兩列為假):
+
+| 強度 | 哪幾列 | 通過代表什麼 |
+|---|---|---|
+| 檢查**就是**那個性質 | 1–4 | 前件成立即違規,通過即乾淨 |
+| 檢查是那個性質,但由**有限抽樣**建立 | 5、6 | 強證據不是證明——第 6 列跑兩次逐字相同,證不到「輸出是決定性的」這個全稱命題;第 5 列找不到逐字重複,也不排除「兩個 case 印不同的字卻查同一件事」 |
+| 檢查是一個**嚴格更弱的命題** | 7 | 通過只排除一個已知會錯的形狀(帶 `editor`),對真正的問題(「這個型別該不該在表裡」)**不提供任何證據** |
+
+第 7 列與第 5、6 列的差別因此是**種類**而非程度:後者的缺口是知識論的(樣本數),
+前者的缺口是結構的(問了另一個問題)。
+
+一句「成本低就寫」會把這些理由壓成同一件事,然後在下一個還沒出現過的情形上給出沒人同意的答案。
+
+(這句與下方同型的那句**刻意不寫序數**。它們寫過序數,而序數在本檔過期了兩次:「第十個情形」在第 10、11 列進表(2026-08-28)後一直留到 2026-09-02 的 diff 仍未改;「第 12 個」被改成「第 13 個」的同一天,#464 已排定在表尾加第 13 列——把正在修的缺陷複製到下一列。表頭的列數有 `measured-numbers-audit` 盯著,散文裡的「第 N 個」沒有守衛認得,所以不寫。#365 verify DA,2026-09-03。)
+
+**第 4 列與第 1 列的差別值得看一眼**(兩者最像):第 1 列不寫的話,那個形狀第一次出現時
+**不會有任何跡象**;第 4 列不寫的話,會有跡象——一筆完全正常的 venue 記錄——只是那個跡象
+指向錯的方向。前者是缺訊號,後者是**假訊號**。
+
+~~**還沒出現過的情形**:零實例且**成本高**的守衛。真出現時要加一列,而那一列的理由
+**很可能是「不寫」**~~ → **2026-08-28 出現了,而且一次三個**(第 10、11、12 列,皆出自 #365;第 12 列於 2026-09-02 補進表——2026-08-28 的裁決 comment 三項都裁了,只有前兩列當時被寫進來)。
+
+**那個預測對了三分之一**:三列的裁決確實都是「不寫」,但它預測的理由(成本高)**只涵蓋第 11
+列**。第 10 列的理由是**形狀取決於一個還不存在的用途**——成本高只是背景,真正擋住它的是
+「現在選一個形狀等於用猜的固定一個介面」;第 12 列的理由是**位置錯了**——成本根本不是問題,
+問題是它不該進這個型別。
+
+**三列的理由不可互換,這是本表存在的理由的再一次實例**:一句「零實例且成本高就不寫」會把
+它們壓成同一件事,然後在下一個還沒出現過的情形上給出沒人同意的答案(序數刻意不寫,理由見上)。
+
+**現在還沒出現過的**:零實例、成本**低**、而裁決是「不寫」的情形。**「成本與裁決兩軸完全相關」這句話
+曾寫在這裡,被本表自己否掉了**(#365 verify DA,2026-09-03):第 3、7 列根本沒有陳述成本,第 12 列的成本
+在上一段被明文否認為理由——12 個資料點裡 2 個沒有成本值、1 個的成本值不相干,剩下的「相關」是讀情形欄
+的樣板前綴得來的,而拿樣板前綴當性質正是本檔禁止的「依性質相似類推」。誠實的版本是:**陳述了成本的
+那幾列**裡,低成本的都「寫」、高成本的都「不寫」。結論不依賴那個統計,且在相關被削弱後更強——連相關都
+不完整,就更不該拿它當判準。真出現時要加一列,不要從「成本低就寫」推導。
+
+→ **2026-09-03 出現了**:第 14 列成本低、裁決「不寫」——理由是判準的輸入(相等定義)取決於 #470,與成本無關。預測對的是「會出現」;它的理由又是一個新的,這正是不寫總括判準的理由再一次實例。
+
+→ **而它在 2026-09-09 又消失了**:#470 一 close,第 14 列依自己寫下的觸發條件翻成「寫」。所以「零實例、成本低、裁決不寫」這一格**現在又是空的**——那不是預測失效,是那一列的觸發條件真的到期了。留著這兩句是因為只寫最新狀態會讓下一個人以為這格從未有過成員。
diff --git a/docs/zero-instance-measurements.md b/docs/zero-instance-measurements.md
index 08766c52..b8d05665 100644
--- a/docs/zero-instance-measurements.md
+++ b/docs/zero-instance-measurements.md
@@ -6,6 +6,8 @@
 
 **守衛讀這份文件**:`zero-instance-rows-audit` 掃這裡與規則檔的量測指令(自證閘、合模板的條數與棘輪標記——棘輪標記住在這裡),`measured-numbers-audit` 掃這裡的「實測 N」。寫法見 [`measurement-commands-self-prove`](../.claude/rules/measurement-commands-self-prove.md)。
 
+**2026-10-15 補記**:規則檔的裁決表與「各列共通的東西」搬到 [`docs/rulings/zero-instance-guards.md`](rulings/zero-instance-guards.md)(同樣不自動載入;`.claude/rules/` 底下的檔合計 738,145 位元組,雲端 session 一開場就被撐爆 context)。上面兩段說的「規則檔的裁決表」「每一列的核心句仍在規則檔」,自此指那份檔;`zero-instance-rows-audit` 與 `measured-numbers-audit` 也掃它。
+
 ## 各列的歷輪補記
 
 只列有補記被搬過來的列。
@@ -1032,7 +1034,7 @@ EOF
 ```bash
 python3 - <<'EOF'
 import io, re
-t = io.open('.claude/rules/zero-instance-guards.md', encoding='utf8').read()
+t = io.open('docs/rulings/zero-instance-guards.md', encoding='utf8').read()  # 2026-10-15 起裁決表住在 docs/rulings/
 rows = [int(n) for n in re.findall(r'^\| (\d+) \|', t, re.M)]
 i = t.index('## 各列共通的東西'); sec = t[i:]
 cited = set()
@@ -2693,7 +2695,7 @@ def sizes(path):
             if standalone: flush('para', block); block = []
     flush('para', block)
     return len(t.encode()), out['fence'], out['para']
-for p in ['.claude/rules/zero-instance-guards.md', 'docs/zero-instance-measurements.md']:
+for p in ['.claude/rules/zero-instance-guards.md', 'docs/rulings/zero-instance-guards.md', 'docs/zero-instance-measurements.md']:
     b, f, g = sizes(p)
     print(f'{p}:{b} 位元組|最大的 fence {f[0]} 行、{f[1]} 位元組|最大的段落 {g[0]} 行、{g[1]} 位元組')
 PY