Repository navigation
Audit which string-or-object union sites lack a pin on their AUTHOR-VISIBLE message — the coverage gap #14722 was re-filed out of #15423
Description
Activity
Claim: session_01MkQhmuuJAVDjmeWNixwDDH — branch
claude/issue-15423-union-message-pinsClause-②: no
Adding pins to existing behaviour moves no accept set and adds no authorable key. ⛔ The round re-derives this from what it ships and says so if it measures otherwise — in particular, if closing the gap requires changing a rendered message rather than pinning it, that is a different card and a STOP.
⛔ Changeset: NOT asserted. Measure whether one is owed and apply the measured answer. Test-only diffs under
packages/spec/src/**/*.test.tsare a plausibleskip-changesetcase, butpackages/spec's publishedfiles[]is the instrument — ⛔ measure, do not assume either way.Dispatched by the
domain:specexecution seat at 2026-09-10T22:43Z. The round inherits this claim and assignee: ⛔ no secondClaim:, ⛔ never writes the assignee, ⛔ never yields the card.Premise pre-measured by the seat BEFORE claiming —
origin/mainad715aca57selectUnionBranches packages/spec/src/api/zod-issues-to-fields.ts:213 (the renderer the card names) referenced at manifest-unknown-keys.test.ts:170 (PR #14975's devPlugins pin — it landed) population is real z.union across 66 files in packages/spec/src `z.union([z.string()` spread over ai/, api/, automation/, data/ …⇒ The card's shape holds: one site (
devPlugins) is pinned, the rest of the family is not.⚠️ Falsify on your own merge base anyway.⭐ Why this card exists is the part worth reading
#14722 asserted a
devPlugins[]refusal reaches the author keyless. Measured, it does not — the manifest branch renders verbatim. But nothing pinned that, so the behaviour was re-reported as a defect, and it cost a grading, a dispatch and a PR to rediscover that the code was already correct.⇒ The deliverable is coverage, not repair. If you find a site whose author-visible message is genuinely wrong, ⛔ do not fix it here — report it, because that is a different card with a different clause-② answer.
The deliverable
- Enumerate the string-or-object union sites — mechanically, from the tree, ⛔ not from a hand-list. State the population and how you derived it.
- Pin the AUTHOR-VISIBLE message where a pin is missing, in the shape PR test(spec): pin the author-visible
devPlugins[]refusal, and record why the union reshape must not land #14975 used fordevPlugins: the rendered message names key/surface/rename; branch selection is structural across multi-issue fixtures; the accept side is unmoved. - Report the sites you deliberately did not pin and why — a population with declared exclusions is a measurement; a silent subset is not.
⚠️ Every count needs a lit control in the same command. A zero without one is not an absence.Decision frame: the canonical four in
pm-dispatch〈升级与决策〉 — 实际业务需求 / 项目长远合理性 / 防 AI 写元数据 app 犯错 / 创业阶段不扩散.
Generated by Claude Code
os-dev-report
{ "issue": 15423, "status": "done", "branch": "claude/issue-15423-union-message-pins", "pr": "https://github.com/objectstack-ai/objectstack/pull/17576", "premise_still_valid": true, "summary": "Enumerated the string-or-object z.union population mechanically from the tree with the TypeScript compiler API (166 z.union array sites in packages/spec/src; 31 string-or-object in production, 7 more in test fixtures), then split it by what the OBJECT arm does with an undeclared key: A curated strictObject() arm 13, B bare .strict() arm 1, C open arm 17. Only A and B have an author-visible unknown-key refusal to lose; 2 of those 14 were already covered (devPlugins by PR 14975, ActionRef by state-machine.test.ts), so 12 are pinned here in one new file, packages/spec/src/shared/union-author-message-pins.test.ts, with PR 14975's three pins per site (rendered message names key/surface/rename; branch selection structural on a multi-issue object branch; accept side unmoved). The 17 class-C sites are declared excluded with a measured reason, not quietly narrowed. COVERAGE, not repair: every message was measured before it was asserted and every one was already correct, so no behaviour changed and no *.zod.ts was touched. The card's predicted split recurred twice: RecordHighlightsField and GuardRef each had a raw-shape pin and no rendered one.", "tests": "All heavy runs via scripts/pm/os-verify-lock.sh, verdicts quoted from its VERDICT command-exit line, exit codes captured before any pipe. (1) pnpm --filter @objectstack/spec test + typecheck -- VERDICT command-exit 0, 473 files / 13406 tests passed. (2) new file + 3 neighbouring pin files at final head eb64917a4d -- VERDICT command-exit 0, 4 files / 363 tests passed (38 new). (3) spec dependency-closure build + spec build -- VERDICT command-exit 0. (4) Typecheck coverage proven, not assumed: tsc -p tsconfig.test.json --listFiles reports 1 hit for the new file, lit control error-map.zod.ts 1, negative control 0; it contributes 0 of the 259 ledgered test-layer errors and check:test-typecheck exits 0. ABLATION -- direction predicted BEFORE running: forcing formatZodIssue's union descent empty should redden pin 1 (12) + pin 2's rendered half (12) + the CONTROL (1) = 25 red / 13 green, and leave the pre-existing RAW-SHAPE pins green. Observed exactly that: 'Tests 25 failed | 13 passed (38)'. Discrimination proof in the same ablated run: src/ui/component.test.ts 1 passed file / 0 failures -- its RecordHighlightsField raw-shape pins sat green through the mutation -- while src/kernel/manifest-unknown-keys.test.ts went 6 failed / 21 passed, every failure a rendered-message pin, its devPlugins raw-shape pin still green. No pin stayed green under a mutation that should break it. Mutation proven ON DISK by occurrence counts (anchor 1 to 0, injected marker 0 to 1) plus non-empty git diff --stat, never from an editor exit code; the script aborts unless both counts land. Restore via git checkout HEAD -- ABSOLUTE_PATH (never a bare git checkout --) under an EXIT/INT/TERM trap seeded from git rev-parse --show-toplevel, proven by blob equality against HEAD (8410fd41adca09eacb2f9d6cd88e5d8a4309b203) plus empty git diff HEAD; empty hash treated as failure. Tree clean, nothing from the ablation on the branch. No rebuild needed: the test imports ./error-map.zod relatively, resolving to source, not through dist. GATES -- derived on the actual diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-built path list), reconciled with --ran recording COMMAND :: exit CODE per family: 76 derived, 74 run green, 2 NOT-MEASURED (both DERIVED from a recorded exit 3). The 2 are check:dual-build-cjs-loads and check:type-check-debt; both print their own PREREQUISITE NOT MET and need the whole workspace built (87 packages / 31 closures) -- judged by substance, not only by the code. Four gates initially refused this way; the two with cheap prerequisites (check:doc-formula-expressions, check:lean-entry-closure) were unblocked by building formula/lint/objectql and then ran GREEN. PR CI at report time: Check Changeset SUCCESS (the specific evidence the dispatch asked for -- the Clause-2 line begins its own line, not a heading, so the declaration limb was read), Governed Surface Queue Guard success, 'Part-of PR must not also close its card' success, 'No other open PR may claim the same issue' success; Lint & Repo Gates / Test Core / Build Core / Type Check / Dogfood / Temporal still in_progress.", "mcp_calls": "0 -- the whole run went through repo-scoped REST plus git; the REST probe returned HTTP 200 at the outset, so no MCP GitHub call was needed, including for the duplicate search.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: packages/spec/src/ui/component.zod.ts:1179 claims 'component-props-union-arm.test.ts pins it from this side' -- that filename occurs exactly ONCE in the repository (in that comment) and exists nowhere on disk. Lit controls: state-machine.test.ts and view-union-diagnostics.test.ts are referenced the same way from a .zod.ts and both exist. A comment asserting a guard that was never there. NOT fixed here because component.zod.ts is a PUBLISHED file (files[] admits src/**/*.zod.ts), so editing it would flip this diff from 'publishes nothing' to 'publishes' and cost the clean skip-changeset measurement. The guard it describes now genuinely exists, as the RecordHighlightsField row of the new file. Carrier: the next PR touching component.zod.ts. Not a defect, contract violation or metadata trap, so not card-worthy.", "noted, not filed: StateNodeSchema.on and StateMachineSchema.on are two textually duplicated z.union([z.string(), TransitionSchema, z.array(TransitionSchema)]) expressions at two doors. Both are population members and both are pinned; whether the duplication should become one shared constant is a shape question this coverage PR deliberately does not open. Carrier: whoever next edits state-machine.zod.ts.", "noted, not filed: a STANDING guard that re-derives the population from the tree and fails when a new string-or-object union with a closed object arm appears unpinned would stop this gap reopening -- this PR pins today's population, which is a snapshot. Real machinery and a different card; declined here under the 'do not proliferate' axis. Carrier: none today -- it needs a card of its own if the maintainer wants it.", "premise correction (not a finding against the tree): the seat's pre-measurement labels packages/spec/src/api/zod-issues-to-fields.ts:213 'the renderer'. It is not -- that is the ADR-0114 D3 WIRE mapper. The prose renderer an author reads is formatZodIssue in packages/spec/src/shared/error-map.zod.ts:221, and selectUnionBranches itself is DEFINED in packages/spec/src/shared/union-branch-policy.ts:191; both files call it. The card's shape is unaffected: the mechanism, the population and the gap are all as described, and the ablation targeted the renderer half deliberately." ] }
Generated by Claude Code
Round collected — PR #17576. ⛔ Not landing yet: the gate the round itself hung is being discharged properly.
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T00:00Z, headeb64917a4d.⛔ First: my pre-measurement was wrong, and the round said so
The claim comment above states "
selectUnionBrancheslive atpackages/spec/src/api/zod-issues-to-fields.ts:213" and calls that file the renderer. Measured onorigin/mainad715aca57:DEFINED packages/spec/src/shared/union-branch-policy.ts:191 selectUnionBranches CALLED packages/spec/src/api/zod-issues-to-fields.ts:213 ← what my claim named "live at" CALLED packages/spec/src/shared/error-map.zod.ts:221 RENDERER packages/spec/src/shared/error-map.zod.ts:254 export function formatZodIssue(…)⇒
zod-issues-to-fields.tsis the ADR-0114 D3 wire mapper, not the prose renderer; the message an author reads comes out oferror-map.zod.ts. ⭐ The correction matters beyond bookkeeping: had the round taken my file at its word, its ablation would have mutated the wire mapper and the rendered-message pins would have sat green through it — the exact "a pin that goes green forever" failure this card exists to end. It targeted the renderer instead, deliberately, and said why.⚠️ One line cite in the correction is itself off by a call site: it namesformatZodIssueaterror-map.zod.ts:221, but:221is aselectUnionBranchescall inside that file and the function is declared at:254. The substance — the renderer lives inerror-map.zod.ts, not inzod-issues-to-fields.ts— is right, and that is the half that changed what the round did.The gate, and why the seat is not clearing it on its own reading
The round hung
needs:contract-reviewon both carriers, and the PR declaresClause-②: no. Those pull in opposite directions:nomeans the change need not pass the in-seat contract review, while an open carrier is by rule genuinely pending — 「开着的载体恒 = 真实待审」.⇒ Two rules in tension, and the standing instruction for that is 「两条细则冲突 ⇒ 按更严的一条行动」. In this seat the in-seat clause-② review is the at-tier one (「达档只在 spec 席」), so the gate is discharged by commissioning an isolated at-tier reviewer, ⛔ not by the seat reading its own round's work and declaring itself satisfied. It is disproportionate for a one-file test-only PR and it is still the right side to err on.
The commission is context-isolated: card, PR and tree only, ⛔ no dispatch order and ⛔ no seat conclusions. Its brief names five attacks, first among them re-deriving the population independently — because a classification that silently drops a site would let this PR certify closed a gap that is still open, which is precisely the shape of the original defect.
Pre-checks already read (⛔ ④ outstanding, ① pending the commission)
pre-check reading, 2026-09-11T00:00Z ② carriers check-clause2-carriers --pair 17576→ exit 0, both carriers agree, no widening tell③ governed check-governed-merges --test packages/spec/src/shared/union-author-message-pins.test.ts→ exit 0, NOT governed (0 of 1 path on the register)④ checks head eb64917a4d: 31 check NAMES, 0 non-green, 16 still in progressskip-changesetfiles[]=dist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json— a*.test.tsmatches none of themWhat the round did that the seat wants on the record
⭐ It lit its own control before clearing it. The scan's unresolved-arm count — sites with an arm it could not classify, each of which could hide a match — went 46 → 0 as the resolver learned
lazySchema,strictObjectand function declarations. A zero from a scan that never resolved anything is not a measured absence; this one was demonstrably lit first. That is the difference between an audit and a shrug.⭐ It refused to pin the 17 class-C sites, and measured the refusal. Their object arms strip undeclared keys, so there is no author-visible message to regress — a pin there would assert the absence of prose and go green forever. Verified behaviourally on
GroupByNodeSchemaandBookNodeSchemaagainst a class-A lit control (ChartGroupBySchema) on the same probe. ⛔ Declared exclusions with a reason, not a quiet narrowing.⭐ The card's predicted split recurred twice —
RecordHighlightsFieldandGuardRefeach carried a careful raw-shape pin and no rendered one, the exact statedevPluginswas in when its correct behaviour was re-reported as a defect at the cost of a grading, a dispatch and a PR.⚠️ Coverage, not repair, held: every message was measured before it was asserted, every one was already correct, and no*.zod.tswas touched.
Generated by Claude Code
Contract review returned FAIL. Gate cleared on both carriers in the same breath; the owed work rides this comment, ⛔ not a label.
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T00:22Z.- Record: the verdict, adopted verbatim — test(spec): pin the author-visible message at every string-or-object union site that has one #17576 (comment)
- Head judged:
eb64917a4d066f5701867df76a7a01bf15e1e56c - Implemented-by: branch
claude/issue-15423-union-message-pins· Reviewed-by: isolated subagent atCONTRACT_REVIEW_TIER, adopted by this session ⇒⚠️ in-seat at-tier, ⛔ not cross-seat independent - Tier: verified from the transcript — 113 harness-stamped
modelfields, allclaude-fable-5-1; lit control 82 assistant messages. ⛔ Not self-report. - Carriers:
needs:contract-reviewremoved from card Audit which string-or-object union sites lack a pin on their AUTHOR-VISIBLE message — the coverage gap #14722 was re-filed out of #15423 and PR test(spec): pin the author-visible message at every string-or-object union site that has one #17576 at00:12:37Z, one stroke each, seconds apart (⚠️ a lone stroke is what H35 fires on). Per the standing rule a FAIL clears them exactly as a PASS does: the label means a review is pending, and one has happened. ⛔ Card state and assignee are untouched; ⛔ the PR is not ready, not enqueued, no auto-merge.
⭐ Why this round was worth the disproportionate gate
The PR declares
Clause-②: no, touches one*.test.ts, moves no schema and ships nothing — by the letter it owed no at-tier review. It got one because the round had hung the gate and the seat took the stricter of two conflicting rules rather than clear a gate on its own round's work.⇒ An independent re-derivation by a different method — a brace-matching scanner rather than the TypeScript compiler API — found one site the round dropped:
ui/view.zod.ts:2895,FormSectionSchema.fields, whose object arm closes with.strict()behind alazySchemaand which renders a curated unknown-key refusal (surface and the ADR-0089 rename) through exactly the union descent this card is about. Settled by behaviour, not by either scanner's syntax classification. Not pinned, not declared excluded — andexpect(SITES).toHaveLength(12)certifies the population complete without it.⚠️ That is this card's own failure mode, reproduced inside the fix for it. #15423 exists because a site's author-visible message went unpinned and its correct behaviour was later re-reported as a defect — at the cost of a grading, a dispatch and a whole PR. A coverage PR that certifies a population it has not fully enumerated re-arms that trap and puts a passing test over it.Owed before this can land — three, all in
packages/spec/src/shared/union-author-message-pins.test.ts- Add the
ui/view.zod.ts:2895row and change:391to 13; update the header counts at:38,:48-62,:140-146.⚠️ The rename renders as an ADR-0089 bullet, not as`k` → `k'`, so pin 1's:326template must accommodate it or the row declaresrename: nulland pins the bullet separately. - Correct
:63-71. The class-C arm raises no unknown-key refusal — but a wrong-typed or bad-enum declared key at a class-C site is refused and is rendered through the same descent. The text as written ("no refusal is raised at all … no author-visible message at them to regress") is false; the true statement is "no curated unknown-key prose to pin", with the generic coverage inerror-map.test.tsnamed. - List the 17 class-C coordinates in the header, so the exclusion set is auditable from the file rather than only by re-deriving it — which the dispatch asked for and the round answered with a count and a class.
⛔ Not owed here (cards or nits, per the verdict): per-site rendered pins for class-C non-unknown-key refusals; the class-B fixture ordering that would make pin 2's whole-branch claim discriminate; the
:144-146wording that reads as an enforced rule; the standing population guard.⭐ What the review confirmed rather than overturned, so the rework does not re-litigate it: the 166-site total, the class-A coordinates (identical), the class-C count, the
46 → 0control,skip-changeset(both the source-sidenpm packhalf and thedistgraph half), and the ablation numbers — 25 red / 13 green reproduced exactly, with two further mutations the reviewer added.
Generated by Claude Code
os-dev-report
{ "issue": 15423, "status": "done", "revision": "2 — after the CONTRACT_REVIEW_TIER FAIL; all three must-fixes addressed", "branch": "claude/issue-15423-union-message-pins", "head_sha": "f1640984bec7c728c8c79ac3c5a08697bde5768c", "pr": "https://github.com/objectstack-ai/objectstack/pull/17576", "premise_still_valid": true, "corrected_population": "32 string-or-object production sites (was 31) = 14 class A + 1 class B + 17 class C; 15 have an unknown-key refusal to lose, 2 covered elsewhere, 13 pinned in the table (was 12).", "summary": "MUST-FIX 1 (the dropped site): found the root cause rather than just adding the row. My scanner resolved lazySchema(() => ...) only when the arrow body was an EXPRESSION; FormFieldBaseSchema's arrow has a BLOCK body, so ui/view.zod.ts:2895 fell out as unclassifiable and never entered the population. A second bug: .strict() applied to the ARM expression was read only when the root was a literal z.object, so an identifier root recursed into the declaration and lost it. Both fixed; re-derived population is 32 and the old-vs-new scan diff is EXACTLY one site -- there is no second site of that shape. Classified by BEHAVIOUR, as the review insisted: it is class A, not class B. Curation and closure are independent -- FormFieldBaseSchema carries a strictObjectError({surface:'this form field'}) map WITHOUT closing the shape (#6619) and FormFieldSchema closes it one level up with a plain .strict(). Reproduced the reviewer's measurement through FormSectionSchema and through the real author door FormViewSchema at sections.0.fields.0. strictObjectError( has exactly ONE call site in the tree, so this pair has exactly one member today; the scanner is now curation-aware and its syntax verdict agrees with the measured behaviour. Also fixed the latent trap the review flagged: pin 1 built the rename assertion from a `key -> target` TEMPLATE, and this site prescribes its rename as an ADR-0089 BULLET with no arrow. Rather than declare rename:null and drop a real prescription, every row now supplies the exact rendered substring (renameInMessage). A template would have stayed green while measuring less than it claimed. MUST-FIX 2 (false class-C reason): corrected. Unknown keys are stripped, but a wrong-typed or bad-enum DECLARED key at a class-C site IS refused and IS rendered through the same descent -- measured on GroupByNodeSchema {field:123} and {dateGranularity:'fortnight'} and ExpressionInputSchema {dialect:'cel',source:5}. The file now claims only 'no curated unknown-key prose to pin' and names shared/error-map.test.ts (:226, :307-329) as the generic coverage of the rendered half there. MUST-FIX 3 (auditable exclusions): all 17 class-C coordinates are listed in the file header. Also corrected the header line the review called out as reading like an enforced rule: it now says plainly that nothing mechanically holds the table equal to the tree, that toHaveLength pins the snapshot not the derivation, and that the standing guard is a separate card rather than something implied to exist.", "tests": "All heavy runs via scripts/pm/os-verify-lock.sh; verdicts from its VERDICT command-exit line, exit codes captured right after a single redirected command, never through a pipe. (1) new file + FOUR neighbours at f1640984be -- VERDICT command-exit 0, 5 files / 738 tests passed (41 new, up from 38). (2) pnpm --filter @objectstack/spec typecheck -- VERDICT command-exit 0; test-layer debt unchanged at 54 files / 259 errors, so the new row adds none. (3) ABLATION re-run with the added row. Direction predicted BEFORE running: pin 1 (13) + pin 2 rendered half (13) + CONTROL (1) = 27 red / 14 green. Observed exactly: 'Tests 27 failed | 14 passed (41)'. THE NEW ROW REDDENS on both pin 1 and pin 2, and ZERO pin-1/pin-2 rows stayed green across all 13 sites -- so no added row is incapable of failing. Discrimination proof at the newly found site specifically: ui/view.test.ts + ui/component.test.ts ran 652/652 GREEN under the same mutation, and ui/view.test.ts:3785-3803 is that site's pre-existing RAW-SHAPE pin. The devPlugins split, demonstrated at the very site the review found. Mutation proven on disk (anchor 1->0, marker 0->1, git diff --stat non-empty); restored via git checkout HEAD -- ABSOLUTE_PATH under an EXIT/INT/TERM trap, blob equality 8410fd41adca09eacb2f9d6cd88e5d8a4309b203 == HEAD, git diff HEAD empty, tree clean. (4) GATES: family set re-derived and byte-identical to revision 1 (same single changed path); all 76 re-run at the new head -- 74 green, 2 NOT-MEASURED (check:dual-build-cjs-loads, check:type-check-debt, both exit 3 PREREQUISITE NOT MET needing the whole workspace built). Reconciled: '76 derived, 74 run, 2 NOT-MEASURED, 0 UNRUN'.", "mcp_calls": "0 for this revision as well — repo-scoped REST plus git throughout.", "open_questions": [], "out_of_scope_findings": [ "NEW PLATFORM READING, worth recording where the measured cells live (pm-dispatch references/platform-readings.md) — I did not edit that file. A PR body CREATE via raw REST stores the body byte-identical (measured revision 1: 14481 bytes sent, stored identical, one session-URL footer). A PR body EDIT (PATCH) via raw REST APPENDS the platform block — exactly '\\n\\n\\n---\\n_Generated by [Claude Code](https://claude.ai/code)_', 58 bytes — leaving the session-URL footer I sent plus a bare one. It is NOT cumulative: a second PATCH of my own (append-free) body left the count at 1+1, not 1+2. Per AGENTS.md the tail bare footer is the platform's, not my form downgraded, so I left it rather than 'repairing' it, and I re-sent MY body rather than the stored one.", "Deliberately NOT acted on, per the coordinator's explicit list — reported, not decided: per-site rendered pins for class-C non-unknown-key refusals; re-ordering the class-B fixture so pin 2's whole-branch claim discriminates (the reviewer's MUT2 found that row green because its unrecognized_keys issue happens to be the branch's first issue; it does redden under MUT1, so it measures the rendered half, just not the whole-branch half); the standing population guard. I agree none of the three is a must-fix and I am not treating them as such.", "carried from revision 1, unchanged: packages/spec/src/ui/component.zod.ts:1179 names component-props-union-arm.test.ts, which exists nowhere in the tree. Carrier: the next PR touching component.zod.ts.", "carried from revision 1, unchanged: StateNodeSchema.on / StateMachineSchema.on are textually duplicated union expressions at two doors; a shape question, not this PR's.", "METHOD LESSON the reviewer is owed credit for, now written into the file header: a cleared unresolved-arm control proves the scan resolved every arm, NOT that it resolved them correctly — ui/view.zod.ts:2895's arm resolved to something, just the wrong something, so the control read zero throughout. One scanner agreeing with itself across three merge bases is not independent evidence. The site was found by changing METHOD, not by looking harder." ] }
Generated by Claude Code
Rework collected — new head
f1640984be, corrected population 32. Narrow at-tier re-read commissioned.domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T00:55Z.All three must-fixes are pushed. The re-read is scoped to (a) those three, (b) ⭐ whether the corrected population is now complete — which is the whole reason the review was a FAIL — and (c) whether anything moved outside that scope. ⛔ It is told not to re-litigate what already passed. The gate label stays off both carriers: that is the FAIL bookkeeping, ⛔ not permission to land, and the PR is still draft with no auto-merge.
⭐ The round found the root cause, and the root cause is an epistemics bug, not a parsing bug
Two scanner defects, not one:
lazySchema(() => …)was resolved only when the arrow body was an expression, andFormFieldBaseSchema's is a block; and.strict()on the arm expression was read only when the root was a literalz.object. Either one alone hides the site.⚠️ And the unresolved-arm control read zero the whole time — because that arm did resolve, just to the wrong thing. In the round's own words:A cleared control proves every arm resolved, never that it resolved correctly.
⭐ That is the sharpest sentence to come out of this card, and it generalises well past this file. This seat has spent the night insisting that a zero is only a reading when a control was demonstrably lit first — and here is the failure mode on the other side of that rule: a control that lights, clears, and is still blind to the error class that matters. The round wrote it into the file header rather than just fixing the number, which is the right disposal.
⇒ The re-read is told to test exactly that: whether the corrected scanner now has a control that would catch that class of error, or whether the header claims a guarantee the code still does not give.
Two more things the round got right by refusing an easy exit
It classified the new site by behaviour and came out disagreeing with the review's own guess — class A, not B:
FormFieldBaseSchemacarries the curatedstrictObjectErrormap without closing the shape, andFormFieldSchemacloses it a level up with plain.strict(). ⇒ Curation and closure are independent, which is precisely what no arm-syntax classifier gets right, and it is the same lesson that produced the miss.⭐ It refused the
rename: nullescape the must-fix itself offered. Pin 1 built its rename assertion from a`key` → `target`template, and this site prescribes its rename as an ADR-0089 bullet with no arrow — so taking the offered escape would have left the row green while dropping a real prescription out of the pin. Every row now supplies the exact rendered substring instead.⚠️ That changes the existing 12 rows' assertions, so the re-read is told to verify none of them was weakened.Ablation: 27 red / 14 green predicted before running, observed exactly; the new row reddens on both pins; zero pin-1/pin-2 rows stayed green. The discrimination proof landed at the newly found site itself —
ui/view.test.ts+component.test.ts652/652 green under the same mutation, withview.test.ts:3785-3803as that site's pre-existing raw-shape pin. ⇒ ThedevPluginssplit, demonstrated where the review found it.⚠️ Operationally worth carrying: four spec gates exited 1 on a fresh worktree purely becausepackages/spec/distdid not exist — prerequisite refusals judged by substance, green after building. ⛔ A refusal does not always arrive as exit 3.
Generated by Claude Code
os-dev-report
{ "issue": 15423, "status": "done", "revision": "3 — after the narrow re-read's PASS WITH FINDINGS; the must-fix and the seat-authorised increment are both in.", "branch": "claude/issue-15423-union-message-pins", "head_sha": "ba6e361e6fb6880ecb4e3d19c9c9c6cb9612a677", "pr": "https://github.com/objectstack-ai/objectstack/pull/17576", "premise_still_valid": true, "corrected_population": "Unchanged and now confirmed by a third, independent method (the reviewer's runtime zod-graph walk): 32 string-or-object production sites = 14 class A + 1 class B + 17 class C; 13 rows pinned here, 2 covered elsewhere. No closed arm exists outside the 15.", "summary": "MUST-FIX (the self-contradiction): revision 2 corrected the class-C reason in the header but left the CONTROL test 400 lines below still titled 'a class-C (open) arm raises no refusal at all' and still saying 'there is no author-visible message at that site to pin or to regress' — the sentence the header now calls untrue. Both now carry the header's narrow claim (no UNKNOWN-KEY refusal, no curated prose to pin; a wrong-typed or bad-enum DECLARED key IS refused and IS rendered). Swept the whole file: the only surviving occurrence of that phrasing is the narrow one. DECLARED INCREMENT (pin 2's vacuous >1): the clause counted (union.errors ?? []).flat().length > 1 — every branch summed. At a string-or-object site the string arm always contributes exactly one invalid_type, so the bound held however few issues the object arm raised: a pin that could not fail, inside the file written to stop exactly that. MEASURED ACROSS ALL 13 ROWS rather than fixing only the probed one, and the answer is NARROWER than 'vacuous at every two-arm site': counting the branch selectUnionBranches really picks, 11 of 13 rows ALREADY satisfied the claim; exactly two did not — ui/view.zod.ts:2895 (the row added in revision 2) and data/object.zod.ts:855 (class B). So the CLAUSE was vacuous everywhere but the FIXTURES were honest at 11 of 13. The fix is stronger than the bound it replaces: each row now DECLARES selectedBranchIssues and the count is asserted EXACTLY against the selected branch, which an unrelated arm cannot satisfy and which fails in either direction when a fixture stops measuring what its row claims; pin 2 also asserts the selected branch is the object arm (carries unrecognized_keys). ui/view.zod.ts:2895's fixture now omits the required `field`, giving its object branch a genuine second issue. data/object.zod.ts:855 declares 1 with the reason at the row — its $in arm wins the fewest-issues tiebreak with a lone unrecognized_keys, so it cannot discriminate 'the WHOLE selected branch is rendered'. Re-ordering that fixture is the open card that is NOT mine, so the count is declared to make the gap visible instead of silent, and a table-level assertion pins that exactly one row may carry the exemption and names which — it cannot spread by copy-paste. The two header claims the bug made false are corrected.", "tests": "All heavy runs via scripts/pm/os-verify-lock.sh; verdicts from its VERDICT command-exit line; exit codes captured right after a single redirected command, never through a pipe; edits proven by state on the pushed ref. (1) pins file + ui/view.test.ts + ui/component.test.ts -- VERDICT command-exit 0, 693/693, matching the reviewer's own baseline. (2) those plus manifest-unknown-keys and state-machine at ba6e361e6f -- VERDICT command-exit 0, 5 files / 738 passed. (3) typecheck green; test-layer debt unchanged at 54 files / 259 errors. (4) Gate families re-derived and BYTE-IDENTICAL to revisions 1 and 2; all 76 re-run -- 74 green, same 2 NOT-MEASURED at exit 3 (dual-build-cjs-loads, type-check-debt; both need the whole workspace built). check:doc-formula-expressions again refused at exit 3 for an unbuilt @objectstack/formula and ran GREEN after building it. ABLATION, two mutations, both predicted BEFORE running. MUT1 (renderer descent forced empty): predicted UNCHANGED at 27 red / 14 green -- because pin 2's new clauses call selectUnionBranches DIRECTLY from the policy module, which MUT1 does not touch. Observed exactly: 27 failed | 14 passed (41). MUT4 (NEW, targets what MUT1 cannot reach -- the POLICY stops ranking: isKindMismatchOnly filtering removed): predicted 13 red / 28 green with pin 1 staying green. ⚠️ OBSERVED 24 failed | 17 passed (41) -- MY PREDICTION WAS WRONG, and wrong in the informative direction. Pin 1 reddened too (12 rows). Mechanism: with kind-mismatch branches no longer dropped, the string arm (one issue, no unknown keys) ranks BEST on fewest-issues, so the object branch is never tied at the top and is never selected at all -- removing that filter does not add noise beside the prescription, it REPLACES the prescription with noise. And 12 of 13, not 13: the survivor is data/object.zod.ts:855, the row that declares selectedBranchIssues: 1, whose $in arm still wins the unknown-key tiebreak unranked. The exemption that row declares is therefore DEMONSTRATED, not merely asserted -- the one row that says it cannot discriminate is precisely the one that does not move. Mutations proven on disk (MUT1 anchor 1->0 marker 0->1; MUT4 anchor 1->0 marker 0->1; git diff --stat non-empty for each); both files restored under one EXIT/INT/TERM trap with absolute paths, blob equality 8410fd41... (error-map.zod.ts) and 53fc1a11... (union-branch-policy.ts) against HEAD, git diff HEAD empty for both, porcelain clean.", "mcp_calls": "0 for this revision as well — repo-scoped REST plus git throughout.", "open_questions": [], "out_of_scope_findings": [ "⛔ NOT taken, exactly as instructed, and named so the boundary stays visible: the standing population guard; per-site rendered pins for class-C non-unknown-key refusals; the class-B fixture ordering. The class-B row is TOUCHED only to the extent of declaring its measured count (1) and citing the open card — its fixture is unchanged, so the card remains exactly as filed.", "MEASUREMENT that refines the verdict rather than merely complying with it: the reviewer inferred the >1 check was vacuous 'at every two-arm site'. The clause was indeed vacuous at every row, but measured per row only TWO fixtures actually failed the real claim (ui/view.zod.ts:2895 and data/object.zod.ts:855); the other 11 carry a genuinely multi-issue selected branch. Reporting the distinction because 'the clause cannot fail' and 'the fixture does not satisfy it' are different defects with different fixes.", "carried unchanged from earlier revisions: packages/spec/src/ui/component.zod.ts:1179 names component-props-union-arm.test.ts, which exists nowhere in the tree — carrier is the next PR touching component.zod.ts; and StateNodeSchema.on / StateMachineSchema.on are textually duplicated union expressions at two doors.", "platform reading, still holding at revision 3 and worth recording where the measured cells live (pm-dispatch references/platform-readings.md — I did not edit that file): a PR body CREATE via raw REST stores byte-identical; a PR body EDIT (PATCH) APPENDS the platform block (blank lines, rule, bare footer, 58 bytes). It is NOT cumulative — a third PATCH of my own append-free body left the count at 1 session-URL + 1 bare, not 1+2." ] }
Generated by Claude Code
Landing record — PR #17576 merged
2026-09-11T02:48:31Zas3ef96b4712. Verified by content onorigin/main, read 02:56Z.the file is present git ls-tree -r --name-only | grep -c 'union-author-message-pins.test.ts' : 1 ⚠️ a PATH listing, ⛔ not a content grep — the instrument that answers the question asked the population :554 expect(SITES).toHaveLength(13); LIT CONTROL 'selectedBranchIssues' in that file : 17 the added row :51 / :72 — `ui/view.zod.ts:2895` named in the header's own account⇒ Thirteen sites pinned on their rendered author-visible message, the class-C exclusions declared with their coordinates, and the population reconciled at 32. Closing.
⭐ Three reviews, two FAILs' worth of real findings, and the card's own failure mode reproduced inside its fix
Review 1 → FAIL. An independent re-derivation by a different method (a brace-matching scanner, not the TypeScript compiler API) found one site the round had dropped:
ui/view.zod.ts:2895, whose object arm closes with.strict()behind alazySchema— settled by behaviour, not by either scanner's syntax classification.⚠️ That is this card's own failure mode reproduced inside the fix for it: #15423 exists because a site's author-visible message went unpinned, andexpect(SITES).toHaveLength(12)would have certified an unfinished population with a passing test over it.The root cause was an epistemics bug, not a parsing bug. Two scanner defects (a block-bodied
lazySchemaarrow, and.strict()read only on a literalz.objectroot) — and the unresolved-arm control read zero throughout, because the missed arm did resolve, just to the wrong thing. In the round's own words, now written into the file header:A cleared control proves every arm resolved, never that it resolved correctly.
⇒ This seat spent the night insisting a zero is only a reading when a control was demonstrably lit first. That sentence is the failure mode on the other side of the same rule.
Review 2 → PASS WITH FINDINGS, with the population settled by a third method: a runtime walk of the built zod graph — 65 modules, 11,729 nodes, 178 distinct defs, every object arm probed with a bogus key — giving 39 string-or-object defs, exactly 15 closed arms (14 curated + 1 uncurated), one-to-one with the header, and all 17 class-C sites confirmed by behaviour. It also stated its own blind spot: a closed arm built only inside a never-invoked factory would be invisible to that walk and to both syntax scanners.
Review 3 → PASS, and the reviewer corrected itself twice more: its
src/**/*.tspathspec had silently excluded top-levelsrc/stack.zod.ts—devPlugins, the site this card started from — so it redid every count; and it retracted its own "object branch carries 2 issues", which was a flattened count on a fixture it had prescribed.⭐ And the round pushed back on a must-fix, correctly. Told the
>1check was vacuous "at every two-arm site", it measured all 13 instead of fixing the probed one, found 11 of 13 fixtures already satisfied the real claim, and separated "the clause cannot fail" (universal — the reviewer later confirmed it holds at all 13, wider than its own must-fix said) from "the fixture does not satisfy it" (two rows). ⇒ "Separating them is what let the round change one fixture rather than thirteen."⭐ It also predicted an ablation wrong and reported the observation. MUT4: predicted 13 red / 28 green, observed 24 red / 17 green — "I was wrong" — with the mechanism: with kind-mismatch branches no longer dropped, the string arm ranks best on fewest-issues, so removing the filter replaces the prescription rather than adding noise beside it. And the sole survivor is the one row that declares it cannot discriminate ⇒ the exemption is demonstrated, not asserted.
⛔ Every card the three verdicts named was declined and recorded with 承接者:无 — all of it comment wording inside a
.test.ts, which ships in nofiles[]and is not a declared contract. The reviewer itself said "every executable claim in the file is accurate" and that it was deliberately not escalating them.
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Filed by the triage seat (R+145) out of PR #14975's body, which identified it as the half worth a card and deliberately did not perform it. #14722 is closed; this is its surviving remainder.
Why this exists
#14722 asserted that a
devPlugins[]refusal reaches the author keyless. Measured, it does not —selectUnionBranchesrenders the manifest branch verbatim. But nothing pinned that, so the rendered half could have regressed, or been re-reported as a defect, without a single test moving.⭐ It was re-reported as a defect. That is the evidence this gap is real rather than theoretical, and it cost a grading, a dispatch and a PR to discover that the behaviour was already correct.
PR #14975 closed the gap for one site (
devPlugins, three pins: the rendered message names key/surface/rename; branch selection is structural across two multi-issue fixtures; the accept side is unmoved). Every other string-or-object union site is still in the statedevPluginswas in.The deliverable
A measured population, then pins where they are missing:
packages/spec/src/**— the arm pair where one branch isz.string()and the other an object schema. ⛔ Do not work from the three the strictness ledger happens to name (ActionRef/GuardRef,submitBehavior,devPlugins); derive the population.devPlugins[]refusal, and record why the union reshape must not land #14975 landed, and say in the PR how many sites were found and how many already had coverage.⛔ What this is not
⛔ Not a schema-reshaping campaign. PR #14975 settled that:
union-branch-policy.tsalready serves every union site generically, a string-or-object arm pair cannot be discriminated without moving the accept set, and every alternative reshape costs either the accept set or the published JSON Schema. Reshaping sites one at a time would add site-local machinery duplicating the general solution.⛔ Not a change to
union-branch-policy.ts,ManifestSchemaorformatZodError.⛔ Not a re-opening of #14722.
Notes for whoever takes it
⭐ The distinction to hold onto: a pin on the raw issue shape and a pin on the rendered message measure different halves, and only one of them is what an author experiences.
devPluginshad the first and not the second, which is exactly why it read as broken. Expect the same split elsewhere.⭐ Reuse PR #14975's ablation as the discrimination proof: force
formatZodIssue's union descent empty and confirm the new pins redden while raw-shape pins stay green. A pin that does not redden under that mutation is not measuring the rendered half.priority:p3— no defect is known behind this gap; the cost it has demonstrably imposed is one wasted grading-and-dispatch cycle, which is real but bounded.Refs: #14722 (closed) · PR #14975 (the one-site precedent and the measurement) ·
packages/spec/src/shared/union-branch-policy.ts.