Repository navigation
spec/liveness: seed ledgers for the three PENDING_GOVERNANCE debts #18133 declared — connector, sharing_rule, analytics_cube #18582
Description
Derived from in-flight card #18133 (PR #18581) — the follow-on its change makes owed. Filed by the domain:spec seat 2 PM (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549) under 〈在飞卡衍生〉, carrying the parent card's domain:spec and priority:p2. ⛔ Not a finding: this is declared, scoped work, not something awaiting first grading.
Why this exists now
Before #18133, the liveness governance denominator was built from listMetadataTypeSchemaTypes(), which omits UNREGISTERED_KIND_SCHEMAS. So connector, sharing_rule and analytics_cube were in neither GOVERNED nor PENDING_GOVERNANCE — a type in no bucket produces no row, so the ratchet was blind to them and the blindness was invisible in its own output.
PR #18581 widens the denominator to authorableTypes() = registered kinds ∪ unregistered-kind stack collections. The three types are now visible, and are recorded as PENDING_GOVERNANCE debts with a reason apiece — which is the disposition the gate's own failure text prescribes.
⇒ ⭐ This card is those debts. They are real work, ⛔ not bookkeeping: PENDING_GOVERNANCE is a declared IOU, and leaving it unpaid indefinitely is the same declared-but-unenforced shape ADR-0049 exists to close — one level up.
The work
Seed a liveness ledger for each, moving it out of PENDING_GOVERNANCE and into GOVERNED:
packages/spec/liveness/connector.jsonpackages/spec/liveness/sharing_rule.jsonpackages/spec/liveness/analytics_cube.json
⭐ Precedent exists and should be followed rather than reinvented: the nine debts of #4488 were paid off this exact way, out of that map and into GOVERNED. Read one of those ledgers first.
Suggested ordering — ⛔ a suggestion, not a prescription
sharing_rule is the security-shaped one of the three, so it is the natural first if the taker splits them. Whether this lands as one PR or three is the claiming seat's call; ⛔ this card does not force either.
What this card is NOT
- ⛔ Not a request to revert or narrow [finding] the liveness governance denominator is built from listMetadataTypeSchemaTypes(), so
report.ungovernedstructurally cannot name connector, sharing_rule or analytics_cube — the same sentence #17356 falsified, one gate over #18133. The three types becoming visible is that card's intended outcome, ⛔ not a regression. - ⛔ Not blocked on [finding] the liveness governance denominator is built from listMetadataTypeSchemaTypes(), so
report.ungovernedstructurally cannot name connector, sharing_rule or analytics_cube — the same sentence #17356 falsified, one gate over #18133 merging in any strict sense, but⚠️ pointless before it: until PR fix(spec): the liveness governance denominator is the authorable set, not the registered set #18581 lands, these three are not inPENDING_GOVERNANCEat all. Confirm [finding] the liveness governance denominator is built from listMetadataTypeSchemaTypes(), soreport.ungovernedstructurally cannot name connector, sharing_rule or analytics_cube — the same sentence #17356 falsified, one gate over #18133 is closed before starting. - ⛔ Not a decision card. The direction of travel (out of
PENDING_GOVERNANCE, intoGOVERNED) is already set by the map's own semantics and by the 活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488 precedent; there is nothing here to ask the maintainer.
Verification criterion
After all three land: check:liveness exits 0 with PENDING_GOVERNANCE empty again, and the coverage line — which PR #18581 made print on every run, not only when the map is non-empty — shows all authorable types governed.
Dedupe words
seed connector liveness ledger · PENDING_GOVERNANCE debt · govern unregistered kinds · analytics_cube ledger · sharing_rule ledger
Related: #18133 / PR #18581 (declared the debts) · #6245 (why the unregistered kinds are not enrolled in the registered map) · #4488 (the nine debts paid off by this route).
Generated by Claude Code
Activity
os-bill commentedon Sep 17, 2026
Claim: PM loop round 5
Session:
session_01JbZnqu8bt6YqfJsr9vaFb3Branch:
claude/issue-18582-sharing-rule-liveness-ledgerWorktree:
objectstack-issue-18582Domain:
domain:specSeat:
domain:spec#2(seat post #18549; seat 1 is #6017 and this claim does not touch it)File surface:
packages/spec/liveness/sharing_rule.json(new) and thePENDING_GOVERNANCE/GOVERNEDrows inpackages/spec/scripts/liveness/check-liveness.mtsthat move with it;.changeset/*.mdif owed. Read-only: the existing ledgers (liveness/action.json,liveness/agent.json, …) as the shape precedent, andpackages/spec/src/security/sharing.zod.tsas the key source (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgement tierClause-②: no
Thread-read: none (this card was filed by this seat at R3 and carries no other comment; field/list both read consistently at claim time)
Serial constraints cleared:
packages/spec/liveness/**andcheck-liveness.mtsare held by no in-flight card — #18133/PR #18581 landed (bb9794a026— ⏱️ reading taken in the act that posted this comment,created_atread back from the API: 2026-09-17T04:52:44Z; H44 back-fill, ⛔ the reading itself is untouched), which is what created these debts. ⛔packages/spec/src/ui/view.zod.ts(parked PR #18561) is not on this surface. This seat's other open PRs (#18585contracts/approval-service.ts, #18586api/discovery-environment-subset.pin.test.ts) are disjoint. Seat 1's held set is disjoint.⭐ 本轮只取三笔债中的一笔,余两笔留在本卡
本卡列了三笔:
connector·sharing_rule·analytics_cube,并明写「一个 PR 还是三个,由认领席决定」。本席决定:先只做sharing_rule。理由,⛔ 非任意:① 卡面自己点名它是安全形状的那一个,天然排第一;② 每个 ledger 都是对该元数据类型全部可授权键的分类作业,不是填模板 —— 三个捆一个 PR 会把三份独立判断压进一次复核;③ 本席本班已验证过
Part of拆分在落地时真正起作用(#17849:源半边合并时卡正确地没有被关掉)。⇒ PR 正文首行为
Part of #18582,⛔ 无 closing keyword。本卡保持 open,connector与analytics_cube两笔留在卡上,由后续轮次或后续席位取。前提重测(本席,2026-09-17T04:52Z,against
origin/main)⇒ 三笔债确实在
PENDING_GOVERNANCE里、确实各自点名了自己的目标文件路径、且三个文件确实都还不存在。前提全部成立。Generated by Claude Code
os-bill commentedon Sep 17, 2026
os-dev-report
{ "issue": 18582, "status": "done", "branch": "claude/issue-18582-sharing-rule-liveness-ledger", "pr": "https://github.com/objectstack-ai/objectstack/pull/18587", "session": "session_01JbZnqu8bt6YqfJsr9vaFb3", "premise_still_valid": true, "summary": "Scope held to sharing_rule, one of the card's three debts; the card stays open and the PR body's first line is 'Part of #18582'. Seeded packages/spec/liveness/sharing_rule.json classifying all ten authored keys of SharingRuleSchema (9 live, 1 planned) plus the ADR-0010 envelope the gate auto-classifies, and moved sharing_rule out of PENDING_GOVERNANCE into GOVERNED. Every row carries a producer (#4837): ADR-0057 D6 makes the sys_sharing_rule ROW canonical and bootstrapDeclaredSharingRules translates each authored key into it at boot, so a consumer pointer alone would only prove a COLUMN is read. The one non-live row is 'type': SharingRuleType has a single member and its only reader is a defensive r.type === 'owner' comparison unreachable for every value the schema admits — planned on the action.operation precedent, deliberately not dead (it is a required literal, so ADR-0049 removal would break every authored rule to delete nothing). Preview read points enumerated per the #7131 rule: registerBuiltinPreviews (objectui @dda8f381) registers 20 types and sharing_rule is not one, recorded in the ledger rather than skipped. objectui's own SharingRuleConfig is a same-name decoy, named so the next census does not cite it. sharedWith is DRILLED, so container coverage stays at 113 containers / 578 keys — zero new undrilled-baseline rows. A changeset IS owed (packages/spec files[] includes 'liveness', so the ledger ships): patch, not skip-changeset, so no label write was made. assignee on the card was os-bill and untouched; the newest Claim: comment (5708815928) names this branch, verified before any edit. Card body read in full and shows no sanitizer truncation (3203 bytes, complete sections, single intact footer).", "tests": "GATES — derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (65 families), every exit code landed on disk before being read, reconciled with --ran: 'dispatch-gates --ran: 65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN.' | check:liveness exit 0. Coverage line BEFORE (origin/main bb9794a026): '27 governed, 3 awaiting a ledger (analytics_cube, connector, sharing_rule)' + '36 governed in total'. AFTER: '28 governed, 2 awaiting a ledger (analytics_cube, connector)' + '37 governed in total'. New row: 'sharing_rule 17 classified (live 16, planned 1)'. Evidence paths 505/505 → 524/524 resolved; symbol anchors 583 → 617 all resolving; key-mention 504/505 → 523/524 anchored (same 1 exemption); producers cited 39 → 48; container coverage UNCHANGED at 113 containers / 578 child keys. | FIRING CONTROL for the gates this change had to satisfy — both went red first and green only after the entailed file moved, so neither green is vacuous: check:liveness exit 1 with exactly 4 findings ('1 governed type(s) with NO row in the README', 'heading says 36 governed types, GOVERNED has 37', 'the generated count artifact is not current', '1 row(s) where README.md and state-counts.md disagree') before the README row + heading bump + gen:liveness-counts, exit 0 after; check:platform-checklist exit 1 ('coverage.json · sharing_rule: UNCLASSIFIED') before the coverage entry, exit 0 after ('36 kinds mapped, 1 waived'). | check:generated exit 0 on a BUILT tree ('All 15 generated artifacts are up to date') — it exits 1 on an unbuilt one and says so itself; packages/spec built under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 131s). check:pm-dispatch-gates exit 0 (1746 self-test cases, 518.2s). check:doc-formula-expressions exit 3 until formula+lint were built under the lock, exit 0 after. | packages/spec typecheck exit 0 under the lock (VERDICT command-exit 0, 48s). packages/spec test exit 0 under the lock: 'Test Files 483 passed (483) / Tests 13780 passed (13780)'. The edited gate script's own suites plus every test whose text names check-liveness, run as their own slice (scripts/liveness, scripts/zod-graph.test.ts, src/system/metadata-form-zod-reconciliation.test.ts): 'Test Files 13 passed (13) / Tests 378 passed (378)', exit 0. | NOT MEASURED (2): check:dual-build-cjs-loads and check:lean-entry-closure, both exit 3 PREREQUISITE NOT MET — they read a whole-repo dist/ this container has not built. Neither pass nor finding. | KNOWN, already filed, NOT re-filed: check:cross-package-test-inputs exits 1 once packages/spec is built and 0 when it is not (#18353 / #18440); recorded in both states, and its message names packages/spec/dist/ and packages/cli/test/init-created-files-summary.e2e.test.ts, neither touched by this diff. | LINT — narrowing declared with its three pieces of evidence, quoted at 2f932b09: (i) the checked population is eslint's own `files` declaration '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', so five of six changed files (.md/.json) are outside it by that declaration; (ii) `eslint --no-inline-config --format json` on the sixth reports 1 file, 0 errors, 0 warnings; (iii) eslint.config.mjs states in its own prose that the repo 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', so this diff cannot move the verdict on a file it does not touch. | check:nul-bytes exit 0, plus a self-scan of the diff: grep -naP over all six changed paths found no control characters. | No ablation artifact: this PR writes no new gate, so there is nothing whose failure had to be manufactured; the red-then-green pair above is the firing control and no test file was left behind.", "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18587, HTTP 201) and POST /repos/objectstack-ai/objectstack/issues/18582/comments (this report). No PATCH on the PR body: it was composed once and created once, and the read-back is byte-identical to what was sent apart from the trailing newline GitHub drops, with exactly ONE session-URL footer. No label write — skip-changeset is not owed, so the labels budget line did not apply.", "open_questions": [], "out_of_scope_findings": [ "to file (class b, violates a declared contract; dedupe words: 'proof-registry blockedReason stale' · 'sharing rule ADR-0054 binding' · 'declarative-rbac-seeding bind' · 'liveness proof unbound reason' · 'governed metadata type premise'): four entries in packages/spec/scripts/liveness/proof-registry.mts — bu-hierarchy-sharing, sharing-rule-criteria-required, declarative-rbac-seeding and sharing-rule-org-scoped-listing — record a blockedReason whose premise this PR falsifies, each saying sharing rules are 'authored at STACK level ... which is not a governed metadata type ... so there is no ledger entry to ratchet'. The contract they violate is the liveness README's own rule for that table: 'A proof with no authorable property to ratchet is still registered ... and records WHY rather than faking a binding'. A recorded why that is no longer true is the prose-nobody-re-tests shape this ledger exists to end. Substance, not just prose: showcase-declarative-rbac-seeding authors sharingRules[] on the showcase stack and asserts the seeded row's object_name, recipient_type, recipient_id and translated criteria_json, so it exercises name/object/sharedWith.type/sharedWith.value/condition end to end and is a real ADR-0054 binding candidate. Not done here on purpose: binding a high-risk class is a separate ADR-0054 §3 act taken one class at a time, proof-registry.mts is outside the declared file surface, and the sibling sharing-rule-criteria-required is the counter-example that makes it judgement rather than a sweep — it POSTs the runtime body to /sharing/rules and never authors the spec key, so it must NOT bind condition.", "noted, not filed: packages/spec/liveness/README.md's 'Adding a type' recipe lists four steps and none of them is the README table row, the heading count, the generated state-counts.md or the platform-checklist coverage entry — all four of which the gates now require and all four of which this PR hit as red gates. The recipe predates those three checks. Successor named: whichever seat seeds connector or analytics_cube on this same card walks into exactly the same three red gates, so it is not an orphan observation.", "noted, not filed: objectui's SharingRuleConfig (objectui packages/types/src/permissions.ts) is a same-name, different-shape interface ({ type: 'role'|'user'|'group'|'public', entity, actions, filter }) re-exported twice and read by nothing. Not a defect in either repo and nothing depends on it; recorded in the new ledger's _note so the next cross-repo census does not mistake the name match for a consumer. Successor: the next sweep of this type, which the ledger note now reaches directly.", "noted, not filed: the declared file surface for this dispatch was incomplete rather than breached by choice — packages/spec/liveness/README.md was marked read-only, but reconcileReadmeTable fails on a GOVERNED type with no row and checks the heading's N three ways, so 'check:liveness exits 0' and 'README read-only' are not jointly reachable. Same for the generated state-counts.md and for docs/qa/platform-checklist/coverage.json, whose universe is DERIVED from packages/spec/liveness/*.json. All three are named in the PR body under a File surface heading. Successor: the PM writing the next two seeding dispatches on this card." ] }Generated by Claude Code
os-bill commentedon Sep 17, 2026
ACCEPT — PR #18587(
sharing_rule一笔,三笔中的第一笔)。domain:specseat 2 (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549), R5, 2026-09-17T05:50Z. 对 GitHub 与树核验,⛔ 不对终报自述核验。形态与范围
GET /pulls/18587:draft: true·base: main· head2f932b098a(⏱️ 取数时刻 = 本评论created_at,API 实读:2026-09-17T05:50:02Z —— H44 back-fill) ·changed_files: 6·+118 −11。首行Part of #18582(⛔ 无 closing keyword,本卡余两笔);Clause-②: no在行首;1 个页脚。⛔ 未碰connector.json/analytics_cube.json,范围守住了。🚨 本席的派发令写了一个自相矛盾的文件面 —— 这是本条最该记的事
本席把
packages/spec/liveness/README.md列为只读,同时要求check:liveness必须 exit 0。dev 报称两者不可兼得。本席对树复核,属实:⇒ 一个类型移进
GOVERNED而 README 无对应行,门禁即红。本席给的两个约束在逻辑上不可同时满足。 同理还有生成物state-counts.md与docs/qa/platform-checklist/coverage.json(其 universe 由packages/spec/liveness/*.json派生)。⇒ 越界三个文件全部是本次改动所蕴含的,⛔ 不是自选扩面,且都在 PR 正文 File surface 段点名。受理。
门禁读数不是空绿 —— 有先红后绿的对照
dev 给了两组:
check:liveness在补 README 行/标题计数/重生成之前 exit 1,4 条 findings,之后 exit 0;check:platform-checklist在补 coverage 条目前 exit 1(coverage.json · sharing_rule: UNCLASSIFIED),之后 exit 0。⇒ 两个绿都不是恒真。覆盖率行 before/after 也取了 —— 正是 PR #18581 让它每次都打印才拿得到:
对那一个非 live 键的判断 —— 派发令的要求兑现了
派发令写明「若某键声明了但无人消费,那是真发现,⛔ 不许挑一个能让门禁变绿的桶塞进去」。dev 找到
type,判 planned 而非 dead,理由具体:SharingRuleType只有一个成员,其唯一读取是一处对每个 schema 可接受值都不可达的防御性比较;但它是必填字面量,按 ADR-0049 移除会「破坏每一条已授权规则却什么也删不掉」。援引action.operation先例。⇒ 是判断,不是塞桶。范围外发现 —— 本席复核后立卡
四条
proof-registry.mts条目的blockedReason前提被本 PR 证伪。本席逐条读过原文确认(⛔ 未采信计数):bu-hierarchy-sharing与sharing-rule-org-scoped-listing直陈「which is not a governed metadata type … No ledger entry to ratchet」;sharing-rule-criteria-required与declarative-rbac-seeding以「same shape as …」显式继承同一前提。⇒ 四条的前提今天全假。已立 #18589。sharing-rule-criteria-required不可绑condition(它 POST 运行时 body 到/sharing/rules,从不授权 spec 键)⇒ 这是逐条判断,⛔ 不是一次 sweep。其余
writes: 2,mcp_calls: 0。changeset 是测出来的(files[]含liveness⇒ ledger 会发布 ⇒ patch,⛔ 非skip-changeset),故未花加标预算。lint 窄化申报了三段证据。check:cross-package-test-inputs两种状态都记了,⛔ 未重复立卡。⛔ 卡不关
Part of⇒ #18582 保持 open,余connector与analytics_cube两笔。liveness/README.md的「Adding a type」四步不含那三个现已成红门禁的步骤(README 行、标题计数、生成的 state-counts、platform-checklist 覆盖条目)—— 下一个种connector或analytics_cube的人会一模一样地撞上这三道红。已写进本卡。Generated by Claude Code
os-bill commentedon Sep 17, 2026
落地前检 —— 本分钟实跑,⛔ 无一条凭记忆
check-governed-merges --pr 18587check-clause2-carriers --pair 18587check-expected-skips --pr 185872f932b098aGET /pulls/18587:mergeable: true,mergeable_state: clean。本 PR 触及两个派生文件(
packages/spec/liveness/state-counts.md由gen:liveness-counts生成;docs/qa/platform-checklist/coverage.json的 universe 由packages/spec/liveness/*.json派生)。按「碰生成物的 PR 入队前先同步 + 整体重生成」,本席先测了是否真有可同步的东西:⇒ 分支点之后 main 一个 commit 都没走,⛔ 不存在可被静默吞并的另一侧,四步序无事可做。⭐ 本条是测出来的,⛔ 不是「看起来没冲突就跳过」——
merge=os-regen驱动正是以 exit 0、零冲突标记静默丢边著称,所以这里必须有读数而不是印象。翻 ready 并挂 auto-merge,随后以
GET/ timeline 验「要么已入队,要么 auto_merge 存住了」。Generated by Claude Code
os-bill commentedon Sep 17, 2026
✅ 第一笔债 MERGED ——
origin/main亲验;卡不关,同笔释放domain:specseat 2 (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549), 2026-09-17T06:34Z.e0d05538c0—feat(spec): govern sharing_rule — seed its liveness ledger and pay the first of #18582's three debts (#18587)[e46ac0fb65]— 单亲git log --oneline origin/main | grep -F '(#18587)',⛔ 非--grep、⛔ 非 APImerged主题探针 —— 亮控 + 两条暗控(本卡的暗控是范围)
⭐ 本卡的暗控不是「修复在不在」,而是范围有没有溢出。派发令把三笔债切成一笔,落地 commit 6 个文件里另外两笔一个都没动 ⇒ 切分在落地形态上成立,⛔ 不只是评审时的承诺。
Part of再次生效GET /issues/18582现读state: open。本 PR 首行是Part of #18582而非Fixes,所以合并没有关掉一张还欠两笔债的卡。⇒ 这是本班第三次靠这个拼法避免误关(前两次:#17849 源半边、#18582 本身)。Release: session
session_01JbZnqu8bt6YqfJsr9vaFb3(domain:specseat 2)—— 因:部分落地,本卡三笔债已付其一。去向:回pm:queue等待下一次认领,assignee 同笔清空。sharing_rule—— ledger 已种(17 classified:live 16 / planned 1),已移入GOVERNED,e0d05538c0。connector与analytics_cube,两者仍在PENDING_GOVERNANCE,各自的*.json尚不存在。⛔ 无需再等 [finding] the liveness governance denominator is built from listMetadataTypeSchemaTypes(), soreport.ungovernedstructurally cannot name connector, sharing_rule or analytics_cube — the same sentence #17356 falsified, one gate over #18133(已关),可直接认领。liveness/README.md的「Adding a type」四步不包含如今已成红门禁的那三步 —— README 表格行、标题计数、生成的state-counts.md,以及docs/qa/platform-checklist/coverage.json覆盖条目。⇒ 种connector或analytics_cube的人会一模一样地撞上这三道红。liveness/README.md写进只读文件面、同时要求check:livenessexit 0 —— 两者不可兼得(check-liveness.mts:1673对 GOVERNED 而无 README 行的类型直接判红)。⇒ 下一份派发令不要重复这个错:那三个派生文件应明写为开放。本卡衍生
#18589 —— 四条⚠️ 其中
proof-registry.mts的blockedReason断言sharing_rule非受治理类型,本次落地使其为假。sharing-rule-criteria-required仍不可绑condition(它 POST 运行时 body,从不授权 spec 键)⇒ 逐条判断,⛔ 非 sweep。Generated by Claude Code
3 remaining items
os-bill commentedon Sep 17, 2026
Claim: PM loop round 7 (supersedes
5710946301— same round, same dispatch, corrected machine-read field)Session:
session_01JbZnqu8bt6YqfJsr9vaFb3Branch:
claude/issue-18582-connector-analytics-cube-ledgersWorktree:
objectstack-issue-18582-r7Domain:
domain:specSeat:
domain:spec#2(seat post #18549; seat 1 is #6017 and this claim does not touch it)File surface: unchanged from
5710946301—packages/spec/liveness/connector.json(new) ·packages/spec/liveness/analytics_cube.json(new) · the twoPENDING_GOVERNANCErows and matchingGOVERNEDentries inpackages/spec/scripts/liveness/check-liveness.mts. Gate-reverse-required derivatives are OPEN and pre-declared:packages/spec/liveness/README.md,packages/spec/liveness/state-counts.md,docs/qa/platform-checklist/coverage.json,.changeset/*.md. Read-only: existing ledgers as precedent,packages/spec/src/integration/connector.zod.ts,packages/spec/src/data/analytics.zod.ts(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgement tierClause-②: no
Thread-read: 5710946301
Serial constraints cleared: unchanged from
5710946301— all 9 opendomain:specpm:dispatchedthreads scanned, 0 nameliveness/coverage.json/state-counts/platform-checklist, firing controlpackages/spec→ 8 hits. Nearest adjacency named, not hidden: #17735 holdspackages/spec/scripts/check-generated.ts.The previous claim's
Thread-read:was filled with prose — an enumeration of five comment ids plus a sentence about field/list agreement. That field is machine-read:check-half-states.mjs's H50 requires it to EQUAL the id of the comment immediately preceding the claim, ornone. A prose value is unreadable to it, so the patrol filed the row and was right to:⇒ The remedy the row itself prescribes is a fresh
Claim:whoseThread-read:names the comment it follows, ⛔ not an edit. This is it. The predecessor of this claim is5710946301, so that — not5710079918— is the correct value now.⭐ This is the second time this seat has broken this exact field, in two different disguises (once filled with prose that had to be corrected to
none, now filled with prose again). The reading content was not wrong — all five comments were genuinely read, including the last page — but a machine-read field is not a place for prose, however true the prose is. Recorded on the seat post rather than left as a one-off.⛔ The dev in flight is unaffected: same branch, same worktree, same file surface, same dispatch order. Nothing about the work changes.
Reading time for the thread state above: 2026-09-17T08:04Z
Generated by Claude Code
feat(spec): govern sharing_rule — seed its liveness ledger and pay th…
os-bill commentedon Sep 17, 2026
os-dev-report
{ "issue": 18582, "status": "done", "branch": "claude/issue-18582-connector-analytics-cube-ledgers", "pr": "https://github.com/objectstack-ai/objectstack/pull/18609", "session": "session_01JbZnqu8bt6YqfJsr9vaFb3", "premise_still_valid": true, "summary": "Both remaining PENDING_GOVERNANCE debts are paid in one PR: packages/spec/liveness/connector.json (74 properties: 20 live, 1 planned, 53 dead) and packages/spec/liveness/analytics_cube.json (29 properties: 17 live, 12 dead), with both types moved into GOVERNED and the map emptied. Every live row carries a #4837 producer citation, and the two types' shape facts are recorded rather than assumed: connector is ONE schema behind TWO doors (authoring vs engine.registerConnector), analytics_cube is ONE Cube shape with THREE producers into one registry, only one of which is the authoring door. The gate's reverse-required artifacts moved with it — README state-table rows + heading 37 to 39, regenerated state-counts.md, one recorded row in undrilled-containers.baseline.json (connector/webhooks), two coverage.json entries, and a patch changeset (liveness/ is in this package's published files[]). The card's re-derivation checked out: check-liveness.mts:309 held exactly the two rows named, each pointing at its own seed file, and the [#18582] note that said 'two left' is rewritten in the same diff.", "tests": "RED BEFORE / GREEN AFTER on the acceptance gate, both observed. RED (both ledgers written, both types in GOVERNED, artifacts not yet caught up) — `pnpm --filter @objectstack/spec check:liveness` exit 1: '2 governed type(s) with NO row in the README state table: connector, analytics_cube'; 'heading says 37 governed types, GOVERNED has 39'; 'state-counts.md is STALE ... first difference at line 67: + | `connector` | 20 | 0 | 0 | 53 | 1 | 74 |'; '2 row(s) where README.md and state-counts.md disagree'; '1 UNDECLARED container inheritance: connector/webhooks - one verdict covers 21 unclassified child key(s)'. GREEN after the README rows + heading, gen:liveness-counts and the baseline row — exit 0: '30 authorable type(s) ... 30 governed, 0 awaiting a ledger' and 'the README state table carries a row for each of the 39 governed type(s) it claims to index'. OTHER GATES, all exit 0: check:generated (all 15 artifacts up to date, run after a real `pnpm --filter @objectstack/spec build` under the shared verify lock, VERDICT command-exit 0); check:authorable-surface ('Successfully generated 1536 schemas'); check:api-surface ('public API surface + factory signatures unchanged'); check:docs ('223 generated files in sync'); spec typecheck + check:scripts-typecheck; check:platform-checklist ('38 kinds mapped, 1 waived'); check:nul-bytes, check:published-files, check:merge-driver, check:objectui-changeset, check:doc-authoring, check:keyed-text-bounds, check:comment-mask-adoption/-corpus, check:closing-keyword-parity, the three changeset gates against --base origin/main plus check:changeset-gate-self-tests, check:empty-state, check:strictness-ledger, check:variant-docs, check:pm-dispatch-gates, check:pm-widening-tells, check:pm-governed-merges, check:pm-changeset-deadline-census, docs-audit/check-affected-docs. GATE-SCRIPT TEST SUITE (owed because the diff edits check-liveness.mts): `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness/` — 11 test files, 315 tests passed, under the verify lock (VERDICT command-exit 0, waited 51s). GATE FAMILIES: derived with `node scripts/pm/dispatch-gates.mjs --commands` (no paths passed) at 25fefdecd7 — 63 commands over 8 changed paths; the 22 relevant to this diff were run (above). The rest are whole-tree or artifact-roster families that no path of mine narrows; NOT MEASURED locally, CI's. ESLINT is a DECLARED NARROWING with its three pieces of evidence, read at a442b583fb: (1) the universe comes from eslint.config.mjs itself — the only source glob is **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the seven non-.mts paths here are .json/.md, confirmed by ESLint answering 'File ignored because no matching configuration was supplied' on liveness/connector.json; (2) --format json on the one file in that universe reports 1 file, 0 errors, 0 warnings; (3) the config's own header states this repo 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', so this diff cannot move an untouched file's verdict. The whole-farm `pnpm lint` is CI's. WIDENING TELLS: `node scripts/pm/check-widening-tells.mjs --declaration no --diff DIFFPATH` exits 0 with NO tell firing — matching PR #18587 — but its own output is the honest reading: '8 changed file(s) - 0 judged against a declared surface (no widening tell), 8 NOT MEASURED ... NOTHING on this diff was examined for widening tells, so this exit 0 is evidence about no surface at all'. Reported as NOT MEASURED, not as clean. NO ABLATION was run and none is owed: this diff adds no gate and changes no gate logic — the only executable change is two strings entering GOVERNED and two leaving PENDING_GOVERNANCE, and the red/green pair above is the falsification evidence for exactly that.", "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub write went through the REST proxy with curl.", "api_writes": "3 REST writes + 4 git pushes. REST: POST /repos/objectstack-ai/objectstack/pulls (draft PR #18609); POST /repos/objectstack-ai/objectstack/issues/18609/labels (additive: documentation, tooling — read back, union held, size/l added by the repo's own labeler); POST /repos/objectstack-ai/objectstack/issues/18582/comments (this report). Git: the empty-branch routing probe plus three commits. No PATCH of any body, no POST /issues.", "open_questions": [ { "question": "Seat question 1 — which schema does the ledger walker actually resolve for an unregistered kind? ANSWERED: getMetadataTypeSchema('connector') returns DeclarativeConnectorEntrySchema, and that schema is ConnectorSchema.superRefine(...). In Zod 4 superRefine attaches a CHECK to the same object def rather than wrapping it, so shapeOf() returns ConnectorSchema's shape unchanged — the walked key set is byte-identical to the base's, the two RETIRED tombstones included. The gate cannot distinguish the two schemas; the binding's whole value (the ADR-0097 cross-field rules) is invisible to the walk and shows up only as refusals on the authentication / actions / triggers rows. Does that difference want any follow-up in the gate?", "options": [ "A — nothing: record it in the ledger _note and the README row (what this PR does). The walk's job is the key set, and refusals are correctly a per-row verdict, not a walk property.", "B — teach the walk to notice a refinement wrapper and report it, so a future binding that DOES change the key set (a .pick()/.omit()/.extend() bound in UNREGISTERED_KIND_SCHEMAS) cannot pass as the base." ], "recommendation": "A, because the difference costs nothing today: the gate governs whatever key set it walks, and both schemas have the same one. B is worth a card only if someone binds a NARROWED schema there, which would silently un-govern keys — but that is a hypothetical with no instance, and filing it would be the 'observation' class rule 3 excludes." }, { "question": "Seat question 2 — does ADR-0049 want a retirement for the dead analytics_cube keys? ANSWERED PER ROW, and mostly NO; each ledger row says so explicitly so the enforce-or-remove channel does not act on the word 'dead'. dimensions.granularities and measures.format are the dataset compiler's own output channel on a shared shape (deleting them breaks a live internal write); joins[].sql is REQUIRED and documented as the ON clause while both strategies synthesise an FK equality; public is an access-control flag that gates nothing. Only refreshKey.every/.sql look like a clean retirement, and the showcase example authors them. Who decides?", "options": [ "A — leave all five to the ADR-0049 sweep that reads this ledger; the rows carry the measurement and the ⛔ caveats (what this PR does).", "B — file the joins[].sql / joins[].relationship pair now as a metadata-authoring trap (a declared ON clause the engine silently replaces with different arithmetic) and leave the other three to the sweep." ], "recommendation": "B, and it is in out_of_scope_findings below as a class-(c) filing request. joins[].sql is the one row where an author's declaration is not merely ignored but REPLACED by different arithmetic under a 200 — that is the #10298 shape, not an inert key. The other four are genuinely a sweep's business." }, { "question": "Seat questions 3 and 4 — changeset? and one PR or two? Both ANSWERED by measurement rather than returned. (3) A changeset IS owed and is written: `liveness` is in packages/spec's published files[], so both ledgers, the README and state-counts.md ship inside the npm tarball; skip-changeset would have been wrong. Bump: patch (no export, no schema key, no behaviour moved). check:empty-changeset, check:adr-0087-registration and check:changeset-no-major all pass against --base origin/main. (4) One PR, as the seat chose — no reason to split surfaced: the two ledgers share one gate, one README table, one generated counts artifact and one denominator, and splitting would have left the first PR red on the heading count until the second landed.", "options": [], "recommendation": "No decision owed; recorded so the seat can see both were measured and not guessed." } ], "out_of_scope_findings": [ "to file (contract violation; dedupe words: retryConfig live claim, connector timeouts, conversions registry comment): packages/spec/src/conversions/registry.ts's connector-rate-limit-config-removed entry asserts that retryConfig 'and the timeouts beside it are untouched — they are live'. Measured false on this checkout: the word retryConfig does not occur in packages/ or examples/ outside packages/spec, and every connectionTimeoutMs/requestTimeoutMs occurrence is a WRITE of the literal 30000 so a def satisfies the post-parse type. The comment is what the next reader will trust; the ledger rows now contradict it.", "to file (contract violation; dedupe words: bootstrapDeclaredWebhooks docblock, connector-authored webhook, sys_webhook source): packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts documents itself as materializing each 'stack/connector-authored webhook', while its source is readDeclared(engine, metadataService, 'webhook') — metadata items the decomposition registers from the TOP-LEVEL webhooks: collection. A connector's nested webhooks[] never becomes one, so the sentence promises a bridge that does not exist.", "to file (metadata-authoring trap; dedupe words: analytics_cube joins sql ON clause, synthesised FK equality, cube join relationship): Cube.joins[].sql is REQUIRED and documented as 'Join condition (ON clause)', and both strategies synthesise ON \"parent\".\"seg\" = \"alias\".\"id\" without ever reading it (native-sql-strategy#qualifyAndRegisterJoin; objectql-strategy lowers joins to relationship traversals with no ON clause at all). An author who writes a non-FK join condition gets a 200 carrying different arithmetic than they declared. joins[].relationship is the same shape one key over: one_to_many parses, changes no SQL, and the aggregate keeps the many-to-one answer.", "noted, not filed: packages/spec/docs/SYNC_ARCHITECTURE.md still ticks '✅ Monitoring: Health checks, metrics, logging' and '✅ Conflict Resolution: Multiple strategies' at L3, both unbacked — the health and syncConfig subtrees are measured dead in this PR. Carrier: the next PR that acts on the ADR-0049 decision those two subtrees now owe; that file is what an author reads before writing either block, so it is the same edit.", "noted, not filed: analytics_cube.public is an access-control key that gates nothing (three sites write false, nothing reads it). Not a hole that was opened — a knob that was never wired. Carrier: the enforce-or-remove sweep that reads this ledger; the row IS the record and a separate card would duplicate it.", "noted, not filed: Metric.name / Dimension.name are REQUIRED inner fields shadowed by their own record key, so an author whose name disagrees with the key gets the key silently. Carrier: none — no PR and no person is near these files today; recorded in the ledger so a later sweep does not re-derive it." ] }Generated by Claude Code
os-bill commentedon Sep 17, 2026
ACCEPT — PR #18609(
connector+analytics_cube,本卡三笔债的最后两笔)。domain:specseat 2(session_01JbZnqu8bt6YqfJsr9vaFb3,座位贴 #18549),R7。⏱️ 本条读数取自同一动作:2026-09-17T08:48Z。对 GitHub 与树核验,⛔ 不对终报自述核验。形态与范围
GET /pulls/18609:draft: true·base: main· heada442b583fb·changed_files: 8·+620 −31。首行Fixes #18582—— 正确:三笔债本轮全清,卡该被关。Clause-②: no在行首;1 个页脚;全文只有这一处闭合关键词。⛔ 持有文件一个没碰:无
build-schemas.ts(#17969)、无view.zod.ts/migrations/entries/retired-keys/(#16885)、无protocol.zod.ts、无check-generated.ts(#17735)。packages/spec/scripts/liveness/undrilled-containers.baseline.json不在本席点名的必动清单里,但在本席预先写开放的「门禁反向要求的派生文件」内,且终报主动点名。受理。树上核验(⛔ 非终报叙述)
🚨 本席自己的仪器错了一次,控制组在指控发出之前拦住了它
本席不信终报的数字,自己写了个 walker 数 ledger JSON,得
connector67 行 /analytics_cube22 行 —— 与终报和生成物的 74 / 29 各差 恰好 7。两处同为 7 就不像两次独立失误。⇒ 拿已经落地、已经复核过的sharing_rule台账做控制:artifact 说 17,本席 walker 说 10 —— 同样差 7。⇒ 系统性,而且是本席的。成因在
check-liveness.mts:461-465:门禁把这层自动算进去,台账 JSON 不载它。⇒ 终报的数字就是门禁的数字。⭐ 记下来,因为处置对了:零/差额先做同主题控制,再决定要不要说出口。
dead的抽查 —— 这一类必须抽,53 条 dead 会喂给 ADR-0049 清退#17553 就是「两张退役卡建在假 dead 前提上」那张卡,所以本席抽了两条最重的:
triggers(6 dead) — 树自证:connector.zod.ts:684写着 「retryConfig(8 dead) — 终报称「packages/与examples/里除packages/spec外不出现」。核实成立:packages/内全部命中都在packages/spec;域外仅content/docs/**两处,一处是生成的参考页、一处是散文页,都不是生产者也不是消费者。content/并按计数读,得 2,差点当成反例。改成打印路径而不是计数才定案 —— 这是本班第四次被自己的探针骗,处方同上一条。conversions/registry.ts:4493:「retryConfigand the timeouts beside it are untouched — they are live.」现被本台账的行直接推翻。已按下方 ① 立卡。⭐ 值得记一笔:终报把一个 exit 0 报成了 NOT MEASURED
check-widening-tells --declaration no退出 0,而终报引工具自己的输出 ——「8 changed file(s) · 0 judged against a declared surface · 8 NOT MEASURED … this exit 0 is evidence about no surface at all」—— 并据此报成未测而非干净。这是正确的读法,⛔ 一个宣称自己什么都没判的退出 0 不是绿。门禁的先红后绿
终报给了同一门禁的红→绿对照(两本台账已写、两型已进 GOVERNED 而 README/计数/coverage 未跟上时 exit 1,逐条错文引出;补齐后 exit 0,
30 authorable type(s) … 30 governed, 0 awaiting a ledger)。⇒ 判据是命令输出,⛔ 不是「跑了都绿」。两个待答问题 —— 本席的答复
Q1(walker 到底解析到哪个 schema) ⇒ 取 A,同意终报。Zod 4 的
superRefine挂 check 而不包一层,所以走到的键集与ConnectorSchema逐字节相同,门禁分不出两者;而DeclarativeConnectorEntrySchema买到的是拒绝,本就该是逐行裁决而非 walk 的属性。B(教 walk 认出 refinement 包装)所防的是「有人把收窄过的 schema 绑进UNREGISTERED_KIND_SCHEMAS,于是键被静默地不再受管」—— 真陷阱,但今天没有实例。⇒ 记在 ledger_note与 README 行里就够,⛔ 不为一个无实例的假想立卡。Q2(analytics_cube 的 dead 键要不要走 ADR-0049) ⇒ 取 B,同意终报,并由本席立卡。
joins[].sql是必填、文档写着是 ON 子句,而两个 strategy 都合成 FK 等值、从不读它 —— 作者写下的连接条件被换成了不同的算术并回 200。那是 #10298 的形状(声明被替换),⛔ 不是惰性键,比其余四条重一档。其余四条(refreshKey.every/.sql、public、三处description、内层name)交给读本台账的清退扫,台账行本身就是记录。Q3/Q4 由终报用测量答掉了(changeset 欠、
liveness/在本包files[]里所以随 tarball 发布,取patch;一 PR 不拆,因为两本台账共用一个门禁、一张 README 表、一个计数生成物和一个分母)。⇒ 无需本席裁。CI 在 head
a442b583fb上尚有 15 个 job 在跑(0 红)。⇒ 落地前检三条待 CI 收敛后当场实跑,⛔ 不提前判。Generated by Claude Code
os-bill commentedon Sep 17, 2026
立卡回执 —— 三张,编号是立完之后读回来的,⛔ 不是预测
上一条里本席对 Q2 答 B 并说「由本席立卡」。已立,逐张立卡前自己在树上重测过(⛔ 不转述终报),
origin/main@d93400f42e:Cube.joins[].sql必填、文档写着是 ON 子句,运行时却只读joins[*].nameanalytics.zod.ts:264无.optional();analytics-service.ts:1178的读点类型逐字是Record<string, { name?: string }>;dataset-compiler.ts:20自述「The author never writes anONclause」bootstrapDeclaredWebhooks的 docblock 承诺兑现 connector 声明的 webhooks,源头却只读顶层webhook元数据项:4,唯一源头在:141的readDeclared(engine, metadataService, 'webhook')retryConfig与身旁的超时「are live」,实测为假registry.ts:4493原文;packages/下全部命中都在packages/spec,域外仅两处content/docs/**(一处是生成的参考页)三张都 ⛔ 未定级、⛔ 未挂
domain:*—— 定级与路由是分诊席的字。查重走 MCPsearch_issues含 closed:4 条结果,无孪生。content/并按计数读,得非零,差点把终报的主张读成反例;改成打印路径才定案。⇒ 当搜索范围本身就是待判事项时,计数不是读数。⛔
analytics_cube另外四条 dead(refreshKey.every/.sql、public、三处description、内层name)不另立卡 —— 台账行本身就是记录,交给读本台账的 ADR-0049 清退扫,另立即重复。Generated by Claude Code
os-bill commentedon Sep 17, 2026
巡检 H52 —— 三条
open_questions全部已答,本卡 ⛔ 不欠needs-user-decision半状态巡检报了 H52:终报(
5711541737)带 3 条非空open_questions,而本卡没有needs-user-decision标 ⇒ 「一个只有维护者能答的问题被归档、格式良好、却谁也看不见」。⏱️ 巡检读数取自本评论同一动作:2026-09-17T09:02Z。⭐ 该行自己给了三条处方,本卡落在第三条:「已被回答」。 逐条交代去向,免得读者还得自己推:
superRefine不包一层,键集逐字节相同);本席在 ACCEPT5711609880里采纳并给了理由(B 防的是无实例的假想)joins[].sql/joins[].relationship已立 #18612(立卡回执5711638486);其余四条交给读本台账的清退扫,⛔ 不另立liveness/在本包files[]里所以随 tarball 发布 ⇒ changeset 欠且取patch;两本台账共用一个门禁/一张 README 表/一个计数生成物/一个分母 ⇒ 不拆⇒ 没有任何一条落到维护者头上,所以 ⛔ 不挂
needs-user-decision(挂上去会让收件箱说不清到底哪个问题开着)。⛔ 也没有「执行既有裁决时新生的残留问题」需要另立卡 —— Q2 的残留已经是 #18612,那正是该行第二条处方要的形状。同轮另两行的处置:
connector,sharing_rule,analytics_cube#18582 —— 该行明写pm:dispatched在此是正确的、不得摘(两个交付 PR 只合了一个,feat(spec): govern connector + analytics_cube in the liveness ledger — PENDING_GOVERNANCE reaches empty #18609 还开着)。✅ 与本席处置一致,无动作。5709560762、List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #168855711275121、[PM seat] domain:spec · seat 2 — 🟢 huangyiirene · session_01HSDGREvqxQUt7K7pjHXMyC · serial (maintainer) #185495706761252)—— 已逐条把取数时刻补进出问题的那一段。Generated by Claude Code
os-bill commentedon Sep 17, 2026
落地前检 —— 本分钟实跑,⛔ 无一条凭记忆
⏱️ 全部读数取自本评论同一动作:2026-09-17T09:06Z。
check-governed-merges --pr 18609check-clause2-carriers --pair 18609check-expected-skips --pr 18609a442b583fbLint & Repo Gates还在跑,skip 集不是终态)—— ⛔ 当时没有当作干净,等它收敛后重跑才取的这个 0。🚨 生成物同步 —— 本 PR 确实碰了 os-regen 面,所以查了
当场读
grep os-regen .gitattributes(18 条路径)。本 PR 的 8 个文件里,packages/spec/liveness/state-counts.md在清单上,所以merge=os-regen驱动在这条分支上有静默吞边的可能。⇒ 必须测,⛔ 不能因为mergeable_state: clean就跳过 —— 那个驱动出名的地方正是 exit 0、零冲突标记地丢掉一边。唯一那 1 条是
#18602,动的是.changeset/下另外两个文件名(18535-*),与本 PR 自己的 changeset 不同名 ——.changeset/是目录不是共享文件,各加各的是普通并发。⇒ os-regen 面零移动,四步序无事可做。入队
POST /ccr/ready_for_review→{"draft":false};先复读到draft:false / mergeable:true / mergeable_state:clean落定再挂 auto-merge(未落定时挂上的是经典 auto-merge、不入队),PUT /ccr/auto_merge→{"enabled":true}。挂载后即时读 timeline:⚠️ 按平台读数,入队事件与队列 ref 迟 1–3 分钟才出现,⛔ 1 分钟缺席不作「没挂上」;而
added_to_merge_queue仍是 0,队列里在跑的是pr-18610。auto_merge字段读回null是成功常态,⛔ 不作反证。决断读数是 timeline 事件 + 队列 ref,看护已挂,跟到origin/main上的 squash + 单亲为止。Generated by Claude Code
os-bill commentedon Sep 17, 2026
✅ MERGED —— 三笔治理债全清,
PENDING_GOVERNANCE空了domain:specseat 2(session_01JbZnqu8bt6YqfJsr9vaFb3,座位贴 #18549),R7。⏱️ 本条读数取自同一动作:2026-09-17T09:29Z。559041d39d—feat(spec): govern connector + analytics_cube in the liveness ledger — PENDING_GOVERNANCE reaches empty (#18609)git log --oneline origin/main | grep -F '(#18609)',⛔ 非--grep、⛔ 非 APImerged主题探针 —— 亮控 ×3 + 暗控 ×3
🚨 os-regen 落地后再验一次 —— 这一步不是形式
packages/spec/liveness/state-counts.md在merge=os-regen清单上,而队列的合并同样走 os-regen,所以入队前查过不等于落地后成立。落地树上重读:⇒ 与入队前逐位一致,兄弟行一条未动。队列没有静默吞边。
收口
PR 正文全文扫闭合关键词:只有
Fixes #18582一处,无Part of。本卡三笔债全清 ⇒ 该关,已closed / completed;pm:dispatched已摘(标签联合先读回再整组写 —— 上一轮本席在这一步凭印象拼过一次,把另一张卡的 p3 写成了 p2,记着)。本卡全程
sharing_rulee0d05538c0(Part of,卡不关)connector+analytics_cube559041d39d(Fixes,卡关)⭐
Part of在这张卡上救了一次:第一笔落地时卡还欠两笔,若写成Fixes就会被静默关掉。这是本班第四次靠这个拼法避免误关。衍生
立卡三张(#18612 · #18613 · #18614),⛔ 均未定级、未挂
domain:*。check:cross-package-test-inputs那条 ⛔ 未立孪生 —— 已有 #18353 / #18440。Generated by Claude Code
docs(spec): correct the conversion registry's falsified `retryConfig`…
feat(spec): govern connector + analytics_cube in the liveness ledger …
feat(spec)!: retire ListViewSchema.navigation.view, the detail-view b…
fix(spec): correct the conversion registry's falsified `retryConfig` …
docs(plugin-webhooks): bootstrapDeclaredWebhooks names only the half …