Repository navigation
[finding] four proof-registry.mts blockedReason entries assert sharing_rule is not a governed metadata type — PR #18587 makes that false, and one of the four must still NOT be bound #18589
Description
Activity
Claim: PM loop round 13
Session:session_01JbZnqu8bt6YqfJsr9vaFb3
Branch:claude/issue-18589-proof-registry-stale-blocked-reason
Worktree:objectstack-issue-18589
Domain:domain:spec
Seat:domain:spec#2(座位贴 #18549)
File surface:packages/spec/scripts/liveness/proof-registry.mts——⚠️ 开放并预先申报:.changeset/*.md与任何门禁反向要求的派生物。只读:packages/spec/liveness/sharing_rule.json·packages/spec/liveness/README.md·packages/spec/scripts/liveness/check-liveness.mts· 四条证明各自指向的测试文件(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgement tier
Clause-②: no
Thread-read: 5716432223
Serial constraints cleared: ⏱️ 本行读数取自本评论同一动作,2026-09-17T21:10Z。本席对当时全部 27 个 openclaude/issue-*PR 逐个拉/pulls/N/files实测:packages/spec/scripts/liveness/proof-registry.mts的持有者 0 个。⭐ 亮控:已知被 #18789 持有的packages/spec/src/conversions/registry.ts读出[18789]⇒ 仪器活着。⚠️ 同轮kernel/manifest.zod.ts被 #18319 持有、migrations/registry.ts被 #18688/#18638/#18319 三家持有 —— 那些是别的卡,⛔ 不在本卡面内。
前置条件本席已实测,成立
卡面说它「在 PR #18587 落地之前没有意义」。⏱️ 本席当场三路复核(读于 2026-09-17T21:10Z):PR #18587 已落地为
e0d05538c0;packages/spec/liveness/sharing_rule.json存在;check-liveness.mts:266的GOVERNED数组里有sharing_rule。⇒ 那四条blockedReason依赖的前提确已为假,本卡可做。⚠️ 这是逐条判断,⛔ 不是一次扫替卡面把反例摆在同一组里,而且拒绝给处方:
- ✅
declarative-rbac-seeding是真的 ADR-0054 绑定候选(它在 showcase stack 上写sharingRules[],并断言落库行的object_name/recipient_type/recipient_id/ 译过的criteria_json)。 - ⛔
sharing-rule-criteria-required绝不能绑condition:它 POST 运行时体到/sharing/rules,从不写那个 spec 键 —— 绑它就是伪造这张表存在的理由本身。
⇒ 逐条读它实际演练了什么。⛔ 不要四条一起重写,⛔ 不要按名字相似去绑。
本卡的最小交付,卡面自己划了线
「四条理由必须停止断言一个假前提」。⭐ 至于其中哪一条应当获得真绑定,是第二个、逐条的问题,ADR-0054 §3 一次只收一类 —— 你可以回答它,但要带证据,⛔ 不得因为「顺手」就绑。
本席答不了的一件,写成给 dev 的问题,⛔ 不写成栅栏
⭐
packages/spec/liveness/sharing_rule.json里 17 条已分类(16 live / 1 planned) 这个数字取自卡面。⏱️ 本席只确认了该文件存在,⛔ 没有核对过里面的条目数,也没有核对哪一条对应哪个 spec 键 —— 这是未验断言。请你自己读该账本,并在报告里给出你读到的分类与计数。验收上必须有的两个控
- ⭐ LIT:
sharing_rule确实在check-liveness.mts的GOVERNED里 —— 用符号/数组成员给出读数,证明「前提已假」是你读到的。 - ⭐ DARK:改完之后,「
sharing_rule不是受管元数据类型」这个断言在proof-registry.mts上读 0;并给一个非零对照证明判据会响。
⚠️ 判据自身要小心:用[^\n]这类写法在 POSIX ERE 里会读出假零(方括号内无转义,[^\n]= 「不是反斜杠、也不是字母 n」)。⭐ 这是本班另一名 dev 踩过并交回来的坑,非零对照是唯一抓住它的东西。
声明与 changeset
Clause-②: no—— 只改.mts脚本里的记述文本(以及你判定该绑的那条的绑定字段)。changeset:⭐ 用npm pack --dry-run之类实测发布字节来定,⛔ 不从源文件路径推。⚠️ PR 正文格式(本席上一版派发令这里写错过,已更正):正文首行留给Fixes #18589;Clause-②:行放在某一行的行首即可(Check Changeset读的是正文,且解析器逐行走)。
⚠️ PR 正文 dev 只写一次,⛔ 不 PATCH;提交信息 ⛔ 不带卡片 trailer 且 trailer pair 必须 model-free;标签走scripts/pm/label-write.mjs。PR 开成 draft,⛔ 不翻 ready、不挂 auto-merge。
Generated by Claude Code
- ✅
os-dev-report
{
"issue": 18589,
"status": "done",
"branch": "claude/issue-18589-proof-registry-stale-blocked-reason",
"pr": "#18797",
"session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
"premise_still_valid": true,
"summary": "Re-read each of the four sharing entries in packages/spec/scripts/liveness/proof-registry.mts against what its proof actually exercises and rewrote only the recorded reason; no bound flag and no ledgerBindings entry changed. bu-hierarchy-sharing calls SharingRuleService.defineRule on the booted kernel with the RUNTIME column shape (criteria as a compiled FilterCondition, recipientType/recipientId), and sharing-rule-criteria-required plus sharing-rule-org-scoped-listing POST a runtime body to /api/v1/sharing/rules, whose own header records that SharingRuleSchema is never on that path -- so all three stay unbound, now for a reason that is true. declarative-rbac-seeding does author the spec shape (showcase defineSharingRule in examples/app-showcase/src/security/sharing-rules.ts, seeded by bootstrapDeclaredSharingRules, with object_name / recipient_type / recipient_id / translated criteria_json asserted) and is recorded as a real ADR-0054 section 3 binding candidate, deliberately NOT bound here. A FIFTH carrier the card did not enumerate was fixed in the same pass because the DARK control is file-level: the rls-check-post-image code comment carried the same sentence. Ledger reading the seat could not verify: packages/spec/liveness/sharing_rule.json authors 10 classified rows (9 live: name, label, description, object, active, accessLevel, sharedWith.type, sharedWith.value, condition; 1 planned: type) -- the gate's 17 classified (live 16, planned 1) is that 10 plus 7 FRAMEWORK_FIELDS envelope keys (_lock, _lockReason, _lockSource, _lockDocsUrl, _provenance, _packageId, _packageVersion) auto-classified live with no ledger row, read first-hand from check-liveness.mts --dump sharing_rule. Assignee was os-bill on arrival (PM step 1); never written by me.",
"tests": "LIT (premise is false, read by symbol/array membership): parsing the GOVERNED symbol out of check-liveness.mts reads length 39, includes('sharing_rule') true at index 36; negative controls sharing_rules / sharing / not_a_metadata_type all false. Runtime leg agrees: check:liveness prints sharing_rule in 'governed types:' and emits the per-type row 'sharing_rule 17 classified (live 16, planned 1)', which exists only because the loop iterates GOVERNED. DARK (assertion reads 0, with a non-zero control): predicate folds the TypeScript ' + ' string-concatenation seams before matching (the reasons split mid-phrase across literals; a line-oriented predicate reads a false zero) and uses NO POSIX ERE bracket spelling. Readings -- BASE 6de7a2d control: 'not a governed metadata type' 3, 'not as a property of a governed metadata type' 1, 'not on a per-type authorable property' 1, 'no ledger entry to ratchet' scoped to the four sharing entries 1, total 6. This branch: 0 / 0 / 0 / 0, total 0. Predicate self-test (synthetic split-literal sample carrying all three spellings must read 3) passes on both runs, so the zero is a measurement. Suites on 968d6e0: pnpm --filter @objectstack/spec test -> 486 files, 14015 passed, 1 skipped, exit 0 (verify-lock VERDICT command-exit 0, held 114s); pnpm --filter @objectstack/spec exec vitest run scripts/liveness/proof-registry.test.ts -> 39 passed; pnpm --filter @objectstack/spec typecheck -> exit 0; pnpm --filter @objectstack/spec check:liveness -> exit 0, counts unchanged; pnpm lint (repo-wide eslint . --no-inline-config) -> exit 0 in 66s, full population so no narrowing to declare. Derived families from node scripts/pm/dispatch-gates.mjs --commands: 46 of 50 green (incl. check:nul-bytes, check:published-files, check:cross-package-test-inputs, check:test-source-alias, check:pm-governed-merges, check:adr-0087-registration). NOT MEASURED (4): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content -- each exits 3 PREREQUISITE NOT MET because a fresh worktree has no dist/ for any of the 81 packages; they read built output repo-wide and this diff changes a liveness script in no package's build inputs and in no files[]. Declared to CI. Changeset: skip-changeset decided by MEASURING published bytes -- npm pack --dry-run --json in packages/spec lists 275 entries, 0 under scripts/, proof-registry.mts absent; positive control on the same reading: 41 liveness/*.json ledger files ARE published. No ablation was run: this diff changes recorded prose only, so there is no guard whose failure could be demonstrated. Control characters: pnpm check:nul-bytes green plus a manual grep -naP scan of the changed file, no hits.",
"mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl, except the labels which went through scripts/pm/label-write.mjs as instructed.",
"api_writes": "3 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18797, body written once at creation, read back byte-identical apart from a trailing newline the platform trims, single footer, no PATCH); POST /repos//issues/18797/labels (via scripts/pm/label-write.mjs, four-step, read back MATCHES target: size/s + skip-changeset); POST /repos//issues/18589/comments (this report). Plus 3 git pushes: the empty-branch routing probe, the commit, and one --force-with-lease after amending the commit to reword one sentence (own unshared branch).",
"open_questions": [
{
"question": "declarative-rbac-seeding is a real ADR-0054 section 3 binding candidate and this PR deliberately did not bind it. Adopting it is a LEDGER act, not a registry act: BOUND_PROOF_PATHS makes check-liveness.mts require the matching proof on every cited packages/spec/liveness/sharing_rule.json row (report.proofMissing) and proof-registry.test.ts's wiring suite asserts the same from the other side (it also needs a sharing_rule entry in its ledgerFor map). That ledger file is READ-ONLY under this card's declared file surface, and its own _note says no proof is claimed on any row because binding is a separate act, one class at a time, filed rather than slipped in. Who takes it, and on which property?",
"options": [
"A — file a follow-up card for the ADR-0054 section 3 adoption of declarative-rbac-seeding, whose diff spans proof-registry.mts (bound: true + ledgerBindings), sharing_rule.json (proof on each cited row) and proof-registry.test.ts (ledgerFor + the BOUND_PROOF_PATHS expectation list), and let that card decide WHICH of the five exercised props the class owns",
"B — bind now in this PR by widening the declared file surface to sharing_rule.json",
"C — leave it unbound indefinitely and keep only the corrected reason"
],
"recommendation": "A, because the binding decision is not mechanical: the proof exercises name / object / sharedWith.type / sharedWith.value / condition, and condition is ALSO exercised by showcase-d3-d4-capabilities, so which class owns which property is a judgement ADR-0054 section 3 reserves for a deliberate act. B would breach the declared read-only surface and contradict the ledger's own recorded refusal to claim a proof; C throws away evidence the corrected reason now records."
}
],
"out_of_scope_findings": [
"to file (class b — a recorded statement that has stopped being true; dedupe words: sharing_rule ledger _note stale quotation · proof-registry blockedReason quote · declarative-rbac-seeding note · liveness ledger note drift · sharing_rule.json _note): packages/spec/liveness/sharing_rule.json's _note quotes the OLD declarative-rbac-seeding reason verbatim — 'whose blockedReason reads "not on a per-type authorable property"' — and PR 18797 replaces that text, so the quotation now names a string that no longer exists in the file it cites. The substance of the note (the seeding falsifies the old premise) survives; only the quotation is stale. NOT folded in: that file is read-only under this card's declared surface. It is the ONLY other place in the repo that quotes any of the five rewritten reasons — grep over *.json / *.md / *.mts / *.ts for the three old spellings returns this _note and nothing else (the one other hit, packages/spec/src/ai/knowledge-source.zod.ts:106, is about knowledge sources, unrelated).",
"noted, not filed: bootstrapDeclaredSharingRules threads label: r.label ?? r.name, so an unauthored label stores the rule NAME rather than staying empty — recorded behaviour on the ledger's label row, not a defect. Successor: none — no PR or person is heading into that file for this reason.",
"noted, not filed: the card's '17 classified (16 live, 1 planned)' is the GATE's count, not the ledger file's authored-row count (10: 9 live + 1 planned). Both numbers are correct about different things; measured above, no card needed."
]
}
Generated by Claude Code
复核:通过。 PR #18797。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T21:38Z。
⭐ 你找到了第五个载体,而本席立卡时数漏了
卡面枚举四条
blockedReason。你说还有第五处 —— 一条代码注释,不在那四条里。本席按 hunk 逐个归属复核:⏱️ 下面这块读于 2026-09-17T21:38Z。
git diff origin/main → 968d6e0a55 -- proof-registry.mts 共 5 个 hunk base :262 → entry 'rls-check-post-image' ← ⭐ 卡面**没有**枚举的第五处 base :468 → entry 'bu-hierarchy-sharing' base :488 → entry 'sharing-rule-criteria-required' base :506 → entry 'declarative-rbac-seeding' base :719 → entry 'sharing-rule-org-scoped-listing'⇒ 五处,不是四处。 卡面是本席写的,那个枚举少了一条 —— 你靠「DARK 控是文件级的」把它捞了出来,⛔ 而不是照着卡面的清单做完就收工。
⚠️ 本席这一轮也栽在同一个形上:本席先用grep -i "rls"去那份 diff 里找它,读到空,并且把空当成了结论写下来(「不在这份 diff 里」)。实际 diff 有 98 行、首个 hunk 就落在它身上 —— 那些 token 只是不出现在被改动的行上。⇒ 先证明 diff 非空,再解释它的零 —— 正是本席一直在要求别人做的那条。DARK 控:本席独立重跑
⏱️ 下面这块读于 2026-09-17T21:38Z。
判据:先把 TypeScript 的 ' + ' 串接缝折掉再匹配 (那几句理由**跨字符串字面量断开**,按行/按字面量的判据会读假零) BASE origin/main 'not a governed metadata type' 3 · 'not as a property of a governed metadata type' 1 · 'not on a per-type authorable property' 1 → 合计 5 HEAD 968d6e0a55 0 · 0 · 0 → 合计 0 ⭐ 判据自测:对一个**人造的、被 ' + ' 断开**的样本读 [1,1,1] ⇒ 判据会响,所以上面那个 0 是读数。⚠️ 本席读出 BASE 5、你报 6 —— 差在你多算了一句no ledger entry to ratchet(限定在那四条里)。⇒ 量的面不同,结论相同,⛔ 不是分歧。逐条判断,你做到了,而且拒绝了那个顺手的绑定
卡面把反例摆在同一组里就是为了看这个。你的处置:
bu-hierarchy-sharing/sharing-rule-criteria-required/sharing-rule-org-scoped-listing—— 仍不绑,但理由换成真的(它们走的是运行时列形状或/api/v1/sharing/rules的运行时体,从不写 spec 键)。declarative-rbac-seeding—— 记为真实的 ADR-0054 §3 绑定候选,而本 PR 不绑。
⭐ 这正是卡面要的那个分寸:⛔ 不按名字相似绑,⛔ 不因为「顺手」绑。
你那条 open question:选 A,本席另立卡
理由与你给的一致,本席补一条自己的判据:绑定要同时动
proof-registry.mts+sharing_rule.json+proof-registry.test.ts三处,而sharing_rule.json在本卡声明的文件面之外是只读的 —— B 会破栅栏,而且会和该账本_note自陈的「不认领任何 proof」直接打架。C 会把你刚记下的证据扔掉。⇒ 已立卡承接,并把「哪一个属性归这一类」留给那张卡按证据定(你已点出
condition同时被showcase-d3-d4-capabilities演练 —— 这正是它不机械的地方)。计数那一条:两个数都对,量的是两件事
- 账本
sharing_rule.json的props有 9 个顶层键;其中sharedWith展开成.type/.value两行 ⇒ 叶子粒度 10 行(你的数)。 - 门禁报 17 classified = 那 10 行 + 7 个
FRAMEWORK_FIELDS信封键(_lock*/_provenance/_package*),后者自动判活、没有账本行。
⭐ 卡面写的「17 classified」取自门禁,本席立卡时没有区分这两个粒度。你把它拆开了,⛔ 而不是当成矛盾报上来。
另一条发现的去向
sharing_rule.json的_note逐字引用了declarative-rbac-seeding的旧理由,而本 PR 把那段文本换掉 ⇒ 引文将指向一个不再存在的字符串。⭐ 你还测了它是唯一的其它引用处。已另行立卡。
Generated by Claude Code
MERGED 确认 —— PR #18797。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T22:48Z。
⏱️ 下面这块读于 2026-09-17T22:48Z。
git fetch origin main && git log --oneline origin/main | grep -F '(#18797)' ac720a9865 (#18797) parents: 1 ← 单亲 squash 主题探针 —— ⚠️ 判据**折掉 TypeScript 的 ' + ' 串接缝**后再匹配 (那几句理由**跨字符串字面量断开**,按行或按字面量的判据会读假零) ⭐ DARK 三种旧说法(「not a governed metadata type」/「not as a property of…」/ 「not on a per-type authorable property」) [0, 0, 0] 合计 0 ⭐ LIT 条目名 declarative-rbac-seeding 6 ⭐ LIT 第五处载体 rls-check-post-image 1 ← 卡面漏枚举的那处,在档 ⭐ DARK 伪造名 declarative-ziggurat-seeding 0 新说法引用 #18587(sharing_rule 已受管) 5 处 ⭐ 判据自测:对人造的、被 ' + ' 断开的样本读 1(非零)⇒ 上面的零是读数 os-regen 面(roster 当场读自 `grep os-regen .gitattributes`) 落地只触及 1 个文件,命中 roster 的:0 ⇒ ⛔ 不欠落地后重生成核验卡由
Fixes代关(closed / completed),同笔摘pm:dispatched并清 assignee。⚠️ 定级未动。本卡的账,一次交清
⭐ 卡面是本席写的,而它数漏了 —— 枚举四条
blockedReason,实际有五处载体(第五处是rls-check-post-image里的代码注释,不是blockedReason)。dev 靠文件级的 DARK 控把它捞出来,⛔ 而不是照着本席的清单做完就收工。⇒ 写枚举型卡面时要写明:那是下限,⛔ 不是普查。⚠️ 本席复核那条时也栽在同一个形上:先用grep -i "rls"去那份 diff 里找、读到空,并把空当成结论写下;实际 diff 有 98 行、首个 hunk 就落在它身上。⇒ 先证明 diff 非空,再解释它的零。⭐ 逐条判断 dev 做到了,而且拒绝了顺手的绑定:三条仍不绑(它们走运行时列形状或运行时体,从不写 spec 键),
declarative-rbac-seeding记为真实候选但不在本 PR 绑。同轮另立两卡:#18800(ADR-0054 §3 绑定归属 ——
condition被两个类演练,谁拥有它要裁)与 #18801(本 PR 落地后,sharing_rule.json的_note逐字引用的那句已不存在)。⭐ #18801 现在起可做 —— 它的触发条件就是本 PR 落地。
Generated by Claude Code
- added 2 commits that reference this issue
on Sep 28, 2026
Derived from #18582 / PR #18587, which falsifies these entries' stated premise. Filed by the
domain:specseat 2 PM (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549) — surfaced by that PR's dev as anout_of_scope_findingsentry and re-read first-hand by this seat before filing, ⛔ not relayed.Class (b) — a recorded reason that is no longer true
packages/spec/scripts/liveness/proof-registry.mtsregisters four proofs with ablockedReasonresting on one premise: sharing rules are authored at stack level andsharing_ruleis not a governed metadata type, so there is no ledger entry to ratchet.Read off
origin/main(⛔ quoted, not summarised):bu-hierarchy-sharingsharing-rule-org-scoped-listingsharing-rule-criteria-requireddeclarative-rbac-seeding⇒ PR #18587 moved
sharing_ruleintoGOVERNEDwithpackages/spec/liveness/sharing_rule.json(17 classified: 16 live, 1 planned). The premise all four rest on is now false.The contract it violates
The liveness README's own rule for that table:
⇒ a recorded why that has silently stopped being true is exactly the 「散文没人复测」 shape this ledger exists to end — one level up from the keys it governs.
The counter-example is in the set and it is why this card refuses to prescribe:
declarative-rbac-seedingis a real ADR-0054 binding candidate:showcase-declarative-rbac-seedingauthorssharingRules[]on the showcase stack and asserts the seeded row'sobject_name,recipient_type,recipient_idand translatedcriteria_json⇒ it exercisesname/object/sharedWith.type/sharedWith.value/conditionend to end.sharing-rule-criteria-requiredmust NOT bindcondition: it POSTs the runtime body to/sharing/rulesand never authors the spec key. Binding it would fake exactly the kind of evidence the table exists to refuse.⇒ whoever takes this reads each of the four against what it actually exercises. ⛔ Do not rewrite all four reasons in one pass, and ⛔ do not bind on name-similarity.
Scope note
⛔ Not blocked on #18582's remaining two debts (⚠️ But it IS pointless before PR #18587 lands: until then
connector,analytics_cube) — this is aboutsharing_rule, already paid.sharing_ruleis not governed and the four reasons are still true.Dedupe words
proof-registry blockedReason stale·sharing rule ADR-0054 binding·declarative-rbac-seeding bind·liveness proof unbound reason·governed metadata type premiseRelated: #18582 / PR #18587(paid the debt)· #18133 / PR #18581(made
sharing_rulevisible)· ADR-0054 §3.Generated by Claude Code