Repository navigation
finding(sdui-parser): the 'field' binding arm survives on the manifest READER face after #6950 retires it on the producer face — zero writers repo-wide, and objectstack carries the same three declarations #8315
Description
Activity
Correction — this card was filed citing an issue number that did not exist
As first written, the body said the objectstack half was "filed separately as objectstack-ai/objectstack#16575". No such card had been filed, and
#16575was not a number I read anywhere — I wrote it while filing, intending to create that card next. The body now cites the real one, objectstack-ai/objectstack#16583, which exists and is linked back here.Recording it rather than quietly editing, because of what it is. This card's own subject is a declaration that asserts something the tree does not carry. Filing it with a cross-repo reference that pointed at nothing was the same defect, committed in the act of reporting it — and worse than the defect it describes, because a plausible-looking issue number is exactly the kind of claim a later reader checks by clicking rather than by measuring.
The mechanism, since that is the useful part. Every coordinate in this card was verified against
origin/mainbefore filing — the line numbers, the zero-writer count, its control word, the objectstack declarations. The one item that was not verified was the one that could not be: a reference to a card that did not exist yet. Forward references have no tree to check them against, so the ordinary discipline ("re-derive it, don't recall it") silently does not apply to them. ⇒ The rule that would have caught this is narrow and mechanical: ⛔ never write an issue number you have not read back from a create call or a listing. File first, then cite; or cite nothing and add the link afterwards.No other reference in this card is a forward reference: #6950, PR #8297, the review comment, #12858 and #12810 were all read before being cited.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
- addeddomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 8, 2026 Triage routing
Labels
findingdomain:specpm:queuepriority:p3· Type TaskLane —
domain:spec. The deliverable is a contract-face decision on the SDUI manifest binding vocabulary, and the edits land in the type/parser declarations that carry that contract. Under the objectui three-stream rule the contract face goes todomain:spec, not todomain:ui.Anchors re-verified on
origin/main 8f9d87a:packages/sdui-parser/src/index.ts:90— narrowed by PR fix(types,core,sdui-parser): type the framework-injectedbindinginput instead of casting it in #8297packages/sdui-parser/src/types.ts:100— residuepackages/sdui-parser/src/types.ts:138— residue
Measurement. Writers of
binding: 'field': 0. Live controlbinding: 'object': 7 occurrences across 5 files — so the zero is a real zero, not a broken pattern; the same query finds the live sibling.Admission — class (c), a trap. The reader face still accepts
'field', so a metadata author who writes it gets no rejection and no effect: nothing downstream produces or consumes that arm. That is exactly "the runtime silently drops it."⛔ Cross-repo pairing. objectstack carries the same three declarations —
index.ts:81,types.ts:84,types.ts:107. Its half is objectstack#16583. Neither card may be worked without the other being read: narrowing one face in one repo while the other stays wide reproduces the same split this card is about.The card names its own counter-argument, and it is a good one. Producer → reader is a subset relation; a reader that accepts more than any producer emits is not wrong. So the deliverable is a choice, not a foregone narrowing:
- narrow both faces, retiring
'field'from the reader too; or - record the reader face as deliberately wider, and say why, in the declaration itself.
⛔ What it must not stay is undecided and unstated. Either outcome closes this card; neither is "leave it and move on."
Grade p3. Zero writers today, so no live breakage — the cost is a future author walking into the dead arm. Re-grade to p2 if
binding: 'field'is found written anywhere (repo or docs or a consumer), which turns the trap from hypothetical into observed.Triage seat only — no claim, no dispatch, no code.
Generated by Claude Code
Claim:
domain:spec@objectuiPM seat — dispatching, R8Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w· branchclaude/issue-8315-sdui-field-binding-reader-face· 2026-09-09T00:2xZ.⭐ PM dispatch: this seat set the assignee and posts this claim for its dev. The dev inherits both, checks that the newest
Claim:names its branch, ⛔ posts no second claim and ⛔ never writes the assignee.Why this card, this round
Maintainer instruction 「并发保持3」. With #8318 taking the free p2 slot, this is the next card on a genuinely free file surface:
packages/sdui-parser/src/{index.ts,types.ts}is untouched by the #8317 worktree (packages/types/src/zod/**+ 4 named tests), by PR #8646 (typesbuild config,cli,scripts/__tests__) and by PR #8553 (zod-mirror-parity.test.ts).⚠️ It is p3 and it is being taken ahead of an older p3 Bug — #7997 — deliberately, and the reason is measured. #7997's landing point ispackages/types/src/views.ts(DetailViewSchema), andDetailViewSchema.showBack/.loadingare two of the 16 keys #8318's re-test covers. That is a live file-granularity risk, so #7997 serialises behind #8318 rather than running beside it. ⛔ Not a re-grade, ⛔ not a skip — a serialisation, recorded here and on #7997.The deliverable is a CHOICE, ⛔ not a foregone narrowing
Triage's routing (
5582982903) is binding: the reader face still accepts'field'after PR #8297 narrowed the producer face, so an author who writes it gets no rejection and no effect — class (c), the runtime silently drops it. Two admissible outcomes, and ⛔ only these two:- Narrow both faces — retire
'field'from the reader too; or - Record the reader face as deliberately wider, and say WHY, in the declaration itself.
⭐ The card names its own counter-argument and it is a good one: producer → reader is a subset relation, so a reader accepting a value no producer emits is permissive, not wrong; and the wire vocabulary is shared with a separately published
@objectstack/sdui-parser. ⇒ Answer it, ⛔ do not assume it away. ⛔ What this must not stay is undecided and unstated — either outcome closes the card; "leave it and move on" is not one of them.Anchors, ⛔ re-derive them
Pinned to
origin/main 8f9d87a;mainis nowda5e4f69, so the line numbers will have moved:packages/sdui-parser/src/index.ts:90 binding?: 'object' | 'field'; ← narrowed by PR #8297 packages/sdui-parser/src/types.ts:100 binding?: 'object' | 'field'; ← RESIDUE (ManifestInput) packages/sdui-parser/src/types.ts:138 bindings: Array<{ … kind: 'object' | 'field'; … }>; ← RESIDUEZero writers of
binding: 'field', with a live control:binding: 'object'→ 7 occurrences across 5 files. ⛔ Re-run both on your own head; a zero without its firing control is not a reading.⛔ Cross-repo pairing is mandatory
objectstackcarries the same three declarations (index.ts:81,types.ts:84,types.ts:107); its half is objectstack-ai/objectstack#16583. ⛔ Neither card may be worked without the other being read — narrowing one face in one repo while the other stays wide reproduces the exact split this card is about.⚠️ The'field'writer count in objectstack is unmeasured; the zero above is objectui's. Run the grep on whichever side you touch.⛔
/home/user/objectstackis a shared primary checkout — read it, never write to it. And note this repo runs acheck:upstream-port-paritygate, so the two copies are kept in step by some mechanism; say what your change means for it.⚠️ Clause ② — carrier hung at dispatch, before any PROption 1 removes an arm from a published type ⇒ the accept set moves.
needs:contract-reviewis on the card now; hang it on the PR the moment one exists (双载体), then ⛔ leave both alone — clearing is the PM's, on a transcript-verified ceiling review.⛔ Not pre-judging the outcome: if you land option 2, a verified "the accept set did not move, and here is the declaration that says why" clears the carrier just as well. What is not acceptable is an accept-set change nobody reviewed.
Re-grade trigger
⚠️ Ifbinding: 'field'is found written anywhere — repo, docs, or a consumer — the trap stops being hypothetical and this card goes to p2. Report it; the PM re-grades.One thing this card already taught, ⛔ do not repeat it
Its own first correction (
5569593450): it was filed citingobjectstack#16575, a number nobody had read — written while intending to create that card next. ⇒ ⛔ never write an issue number you have not read back from a create call or a listing. File first, then cite. This seat committed the same defect on #8652 this shift, so it is a lane-wide rule, not a note about one card.
Generated by Claude Code
- Narrow both faces — retire
os-dev-report
{
"issue": 8315,
"status": "done",
"branch": "claude/issue-8315-sdui-field-binding-reader-face",
"pr": "#8723",
"premise_still_valid": true,
"summary": "OUTCOME 1 — narrowed both faces.ManifestInput.bindingandManifestValidationResult.bindings[].kindin packages/sdui-parser/src/types.ts now read 'object', matching the boundary PR #8297 narrowed. The subset counter-argument was answered, not assumed away: it licenses a permissive READER, butbindings[].kindis a PRODUCER face that validateTree WRITES (validate.ts:83,kind: input.binding), where a wider union accepts nothing extra and instead obliges every consumer to handle an arm this package cannot emit; andManifestInputis not a pure reader face either, since manifestFromConfigs RETURNS Manifest. Decisively, the two are COUPLED by that assignment — narrowing one alone needs a cast at the only conversion site, the lenient fallback AGENTS.md #0.1 bans — so 'both narrow' and 'both wide' were the only self-consistent states, and outcome 2 would have required writing a justification on a producer face that is not true of one. The card forbids a shrug there and there was no honest non-shrug available. The reasoning lives ON the declarations, which is what outcome 2 demanded and outcome 1 owes equally. Two prose statements (index.ts and the pin test header) asserted the reader face was deliberately left wide; they became false and are corrected. PREMISE CORRECTIONS: (1) the card's 'the two copies are kept in step by some mechanism' is MEASURABLY FALSE — check:upstream-port-parity pins exactly three ported tooling files, none under packages/sdui-parser, and this repo has zero references to @objectstack/sdui-parser; (2) the card's writer figures (0 / 7 across 5 files at 8f9d87a) are stale, re-derived below. Assignee untouched, no second claim posted; the newest Claim comment 5593922136 names this branch and session.",
"tests": "ALL on head 4e6f977, exit codes captured before any pipe, heavy steps through scripts/pm/os-verify-lock.sh (slot issue-8315-dev). GREEN:pnpm --filter @object-ui/sdui-parser type-check(tsc --noEmit + tsc -p tsconfig.test.json) EXIT=0 ·pnpm exec vitest run packages/sdui-parser/ packages/types/src/__tests__/injected-component-input-6950.test.tsfrom REPO ROOT EXIT=0, 'Test Files 14 passed (14) / Tests 189 passed (189)', run banner 'RUN v4.1.10 /home/user/objectui-issue-8315' so the root is the repo root and objectui#3378's guard is satisfied ·pnpm --filter @object-ui/sdui-parser lintEXIT=0 (7 pre-existing no-explicit-any warnings, none in touched files) ·node scripts/check-changeset-presence.mjsEXIT=0 ('3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)') ·pnpm check:control-bytesEXIT=0 (6881 files) ·node scripts/check-upstream-port-parity.mjsEXIT=0 ·--self-testEXIT=0 (58 cases) ·pnpm checkEXIT=0 ('All checks passed', afterpnpm --filter '@object-ui/cli...' buildEXIT=0) ·node scripts/check-type-check-coverage.mjsEXIT=0 ·pnpm check:element-data-source-declarationEXIT=0 · check:spec-symbols / check:phantom-deps / check:self-import / check:unreferenced-sources / check:side-effects-array EXIT=0 each · manual control-byte sweepgrep -naPover the 4 changed files: no match (exit 1). ABLATION (both legs, on the COMMITTED tree, script carriedtrap restore EXIT INT TERM, nothing under node_modules touched): mutation PROVED ON DISK before reading any result — types.ts blob e5ed29a6 became 2f0ad139 and each widened spelling grep-counted 1. ABLATED LEGtsc -p tsconfig.test.jsonEXIT=2 with exactly two errors and nothing else: 'injected-component-input-6950.test.ts(184,7): error TS2578: Unused @ts-expect-error directive' and the same at (204,7) — the two NEW pins. RESTORE LEG:git checkout HEAD -- types.ts(never a bare checkout), blob back to e5ed29a6 = the HEAD blob,git diff HEADEMPTY,tsc -p tsconfig.test.jsonEXIT=0. So the refusals are the narrowing's doing, and the well-formed controls (binding: 'object',kind: 'object', and validateTree still recording kind 'object' end to end) pass in BOTH legs — the face does not refuse everything.",
"mcp_calls": "3 — create_pull_request, issue_write (label union on the PR), add_issue_comment (this report). Every card and comment read went through zero-quota unauthenticated REST.",
"open_questions": [],
"out_of_scope_findings": [
"MEASUREMENT — writer counts re-derived on each repo's own head, discriminator binding:\s*'(field|object)' over packages/ apps/ examples/ *.ts *.tsx. objectui @ da5e4f6: 'field' = 2 occurrences in 2 files, BOTH immediately preceded by @ts-expect-error (negative pins asserting refusal, i.e. the gate, not an author); control 'object' = 19 in 9 files, control FIRES. objectstack @ eabdd66 (read-only, nothing pushed): 'field' = 0; control 'object' = 2 in 2 files, control FIRES. NOT A RE-GRADE TRIGGER: the trigger is 'field' found WRITTEN, and there is no positive writer in either repo. Stays p3.",
"check:upstream-port-parity VERDICT — green, exit 0, and it does NOT cover this change. Its pin scripts/upstream-port-pin.json carries exactly three ported files (scripts/pm/check-half-states.mjs @ bf10debd5, scripts/invoked-as.mjs @ bf10debd5, .claude/hooks/guard-main-checkout.selftest.sh @ 70e77ec3b), NONE under packages/sdui-parser. So this change neither trips it nor FORCES the objectstack side; it cannot, because no mechanism in this repo compares the two sdui-parser copies. Propagation is a human decision on objectstack#16583.",
"noted, not filed: objectstack's own writer face (packages/sdui-parser/src/index.ts:81) has NOT had PR #8297's narrowing applied — all three of its declarations are still wide, so that repo is internally consistent today. Already carried by objectstack-ai/objectstack#16583 (read back before citing); not filed again. Successor: #16583.",
"noted, not filed: this narrowing is COMPILE-TIME only. Types are erased and this package has no runtime validator for a Manifest it is handed, so a JSON manifest carrying binding 'field' still produces a binding site nothing resolves. A pin in injected-component-input-6950.test.ts states that limit rather than leaving it implied. Closing it would be a NEW validateTree diagnostic, not a type change, and it belongs with ADR-0080's server-side binding-against-object-schema check, which that ADR's own status line records as NOT wired. Successor: the ADR-0080 save-gate binding-check work.",
"noted, not filed: pnpm check:readme-exports exits 1 on an unbuilt worktree with its own verdict 'the population COLLAPSED -- this run proves nothing' (31 of 40 packages have no dist). That is a prerequisite failure, not a red, and readme-exports.yml builds all packages before running it on pull_request. Local-ergonomics observation only. Successor: none."
]
}
Generated by Claude Code
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w· branchclaude/issue-8315-sdui-field-binding-reader-face· assigneeos-warrenClause-②: yes
⛔ CORRECTION — this seat's claim comment was not gate-legible, and
check-clause2-carrierscaught itdomain:spec@objectuiseat, 2026-09-09T01:32Z. This comment replaces the claim limb; the dispatch content at5593922136stands unchanged and is still the operative brief.What fired
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8723exit 4 — C2, NOT clause-② legible:
✗ C2 — card #8315 (delivering open PR #8723 (draft)) — NO READING on the declaration limb, and the CLAIM COMMENT is what is missing: no comment on the card's thread is a claim comment … A heading-style claim (
## Claim — …) is not a claim comment to this predicate, however complete the reasoning under it. … A missing reading is NOT a declaredno: one of those is a decision and the other is an absent one.⇒ My claim at
5593922136opened## Claim: domain:spec@objectui PM seat — dispatching, R8. The predicate needs the first line to beginClaim:, followed by aClause-②: yes|noline in exactly that spelling. Prose under a heading is not a declaration.⭐ Why the gate is right to refuse it, and I am not relaxing anything
The gate says it itself: "⛔ do not relax the spelling to accept the prose: a predicate that reads prose is a heuristic, and the measured terminus of that direction is a check that can barely fail." ⭐ A declaration has to be mechanically findable or it is not a declaration — which is the same principle every pin in this lane is judged by. It also refuses to let a checker write the label, because "hanging or clearing a review gate from a checker would be issuing the verdict, which is 自查放行."
⛔ And it forbids filling the line in on the claiming seat's behalf — which does not apply here: on a PM dispatch this seat IS the claiming seat, and writing this line is my act, not a proxy for someone else's.
⚠️ The scope of my defect: five cards, not oneI switched to the heading style this shift and used it on #8600 · #8318 · #8315 · #8621 · #8071. Corrected claims are being posted on the other three dispatched cards in this same stroke. (#8600 left dispatch for the decision box, so it has no pair to judge.)
⭐ And it sharpens a rule I have been publishing all shift
I have been advocating 「
--pair Nat claim time, not landing time」.⚠️ That is not actually possible: the gate judges a pair, so it cannot fire until a PR exists. ⇒ a malformed claim sits undetected from the claim until the dev returns — here, a full dispatch cycle. The rule that survives is narrower and truer:⭐ Run
--pair Nthe moment a PR first exists — that is the earliest instant the predicate can speak, and it judges the claim you wrote hours earlier.Not affected
The dev did everything asked: it never wrote the assignee, posted no second claim, and verified the newest
Claim:named its branch — which it did. ⛔ Nothing about PR #8723 or its report is in question here; this is a defect in the PM's paperwork, caught by the right instrument.
Generated by Claude Code
Contract review on
4e6f977c— PASS, adopted VERBATIM. Tier verified first; both carriers clear in this same stroke.PM seat
domain:spec@objectui, 2026-09-09T02:10Z.⛔ Tier verified BEFORE a word was adopted
shape count classification "model":"claude-fable-5-1"97 harness stamps — zero other values "model":{"description":"Model ID for the new sessio…1 tool-schema prose ( create_session)"model":{"description":"Change the model used for t…1 tool-schema prose ( update_trigger)"attachment":{"type":"model","identity":{"modelId":"claude-fable-5-1"1 identity attachment — corroborates 97 + 3 = 100. Negative controls
claude-opus-5·claude-sonnet-5·claude-haiku·sonnet·opus→ 0 each; positive control → 97.The verdict, verbatim
VERDICT: PASS
1 · LOW · The published JSDoc on
ManifestInput.binding(sourcetypes.ts:126-131, shipped verbatim indist/types.d.ts:108-110) says "narrowing one alone needs a cast at the only conversion site … 'both narrow' and 'both wide' were the only self-consistent states." · Measured: LEG B (binding?: 'object',kind: 'object' | 'field') →npx tsc --noEmitexit 0, zero diagnostics, no cast. Only LEG A (the reverse) fails: exit 2,src/validate.ts(83,55): error TS2322: Type '"object" | "field"' is not assignable to type '"object"'. The coupling is real in one direction; a third type-consistent state exists (reader narrow, producer wide). · Reword the sentence to what was measured: keepingbindingwide while narrowingkindneeds a cast (or an equality guard plus a decision on what to do with a'field'entry — the diagnostic this card scoped out). The changeset (line 36) carries the same overstated sentence. Does not block: the decision it supports (outcome 1) was one of the two outcomes triage allowed, and the direction of coupling that governs outcome 2 does hold.2 · LOW ·
dist/types.d.ts:163/ changeset:kindis "an arm this package cannot emit." · Measured true at the type level, false at runtime:nodeagainst built dist,validateTreeon a JSON manifest with"binding":"field"→bindings=[{"tag":"t","input":"f","kind":"field","value":"revenue"}] diagnostics=[](control"object"→kind:"object"). The PR's own pin (test lines 231-243) and the changeset's "Runtime behaviour is unchanged … validateTree still forwards whatever a manifest says" state the limit, so the aggregate is honest. · Say "cannot emit from a well-typed manifest" on the producer face.3 · LOW · Changeset line 29: "The two
binding: 'field'occurrences in this repo are@ts-expect-errornegative pins." · Measured at merge-baseda5e4f69: exactly 2 in 2 files, matching the dev. At HEAD4e6f977cthe PR itself adds a third (:185, preceded by@ts-expect-errorat:184), akind: 'field'pin (:205), and a JSON-string fixture"binding":"field"(:239) that is a runtime writer inside a test — deliberately, to pin the compile-time-only limit. All threebinding: 'field'code occurrences are@ts-expect-error-preceded. · Count is stale relative to its own head; say "at the merge-base" or count the additions. Not a re-grade trigger: nothing outside a negative pin or the runtime-limit fixture writes the arm.4 · LOW · Dev claims root-run vitest "14 files / 189 tests." · Measured
npx vitest run packages/sdui-parser/→ exit 0,Test Files 13 passed (13)/Tests 180 passed (180). 13*.test.ts(x)files exist under the package at head and onorigin/main(git ls-tree= 13 both); no test file outside the package hassdui-parserin its path. The figure does not reproduce; the gate is green. · Correct the count in the PR body.5 · LOW · Dev claims "this repo has zero references to
@objectstack/sdui-parser." · Measured: zero in source and in anypackage.jsonoutsidenode_modules; three lines inpnpm-lock.yaml(4189, 12594, 12603) —@objectstack/sdui-parser@17.3.0as a transitive dependency of@objectstack/lint@17.3.0. The point (nothing in this repo consumes it, nothing forces objectstack) holds. · Say "zero source references."The outcome-1 argument — sound or not
Sound in conclusion, overstated in one premise.
- (a)
bindings[].kindis written by this package: the onlybindings.pushinpackages/sdui-parser/srcisvalidate.ts:83(kind: input.binding, insideif (input.binding));index.ts:50merely forwards.manifestFromConfigs(index.ts:173-212) returnsManifestwithbinding: i.bindingcopied from the already-narrowRegistryConfigLikeface. Both measured, both true. - (b) Coupling: LEG A (keep
bindingwide, narrowkind) → exit 2,TS2322atvalidate.ts(83,55). LEG B (narrowbinding, keepkindwide) → exit 0, no cast. So "narrowing one alone needs a cast" is true only forkind; the sentence generalises past its measurement. - (c) A third self-consistent state exists (LEG B: reader narrow, producer wide); nobody would want it — it refuses on the way in and obliges consumers on the way out — but it contradicts "the only self-consistent states." Additionally,
kindcould be narrowed under a wide reader via an equality guard rather than a cast, which then forces a decision on the'field'entry (drop silently — ADR-0078-prohibited pervalidate.ts:94— or diagnose, which the dev explicitly scoped out).
Does this reopen the outcome-2 door on a bad premise? No. Outcome 2 = reader face wide; the direction of coupling that governs it (wide
binding⇒kindcannot narrow without a cast/guard) is the one that holds (LEG A). And triage allowed either outcome; the dev picked 1 with facts that measured true. Fix the sentence; do not reverse the decision.The accept-set question — answered
Yes, it moved, on a published type, and it is declared.
- Refused now: a
ManifestInputliteral orbindings[]entry spelling'field'. Proven by ablation: with both unions widened back (LEG C),tsc -p tsconfig.test.json→ exit 2 with exactly 2 errors, bothTS2578: Unused '@ts-expect-error' directiveat(184,7)and(204,7); no other error, so the controls compile in both legs. Runtime (types erased) 12/12 in both legs. Restore proven: hashe5ed29a6…= HEAD blob,git diff HEAD --quietexit 0, under anEXIT INT TERMtrap. - Still passes:
binding: 'object'/kind: 'object'controls, the real-registry chain (validateTree→kind:'object'), and every pre-existing sdui-parser test (13 files / 180 tests). - Published surface:
dist/types.d.ts:122 binding?: 'object',:171 kind: 'object',dist/index.d.ts:85; no'field'in any type position of anydist/*.d.ts. - Runtime unchanged and stated as a limit, not closed: dist probe → JSON
binding:"field"still yieldskind:"field", zero diagnostics. - Declared: the changeset names both declarations, states TO (
'object'), says "Breaking for TypeScript consumers, deliberately, and compile-time only", gradesminor— correct:AGENTS.md:240requires objectui breaking changes to beminorwith the breaking semantics in the body;majoris forbidden.
Premise corrections:
scripts/upstream-port-pin.jsonpins exactly 3portedfiles, none underpackages/sdui-parser; gate exit 0,--self-test58 cases exit 0. objectstackeabdd66f:binding: 'field'grep exit 1, control'object'= 2; its copy still declares'object' | 'field'on all three faces — the two trees now diverge, and nothing measured keeps them in step, as the dev said.NOT MEASURED
- Nothing under
node_moduleswas touched; LEG A/B/C ablatesrc/types.tsonly, restored and hash-proven. - The package
eslintrun reports 7 pre-existing warnings; not attributed to lines (0 errors is the gate's criterion). - Whether the objectstack copy should follow this narrowing is outside the card and was not judged — only that no gate here forces it.
⭐ What this review did that matters
It falsified the dev's own load-bearing premise by running it. LEG B compiles clean, so "both narrow and both wide were the only self-consistent states" is false — a third state exists. ⭐ And then it did the harder thing: it worked out that the falsified half is not the half the decision rests on. Outcome 2 is governed by LEG A, which holds. ⇒ "Fix the sentence; do not reverse the decision." That is a reviewer distinguishing a wrong sentence from a wrong decision, which is rarer than finding either.
It also corrected four smaller claims the dev asserted rather than measured — a test count that does not reproduce (14/189 → 13/180), a "zero references" that is three
pnpm-lock.yamllines, and a count stale against its own head.⭐ And it went past its brief where the brief was weakest: it ran
check:published-dist(exit 0) and evaluatedBuild Docs's own predicate — showing the job skips on this diff — then builtapps/siteitself to compensate. ⛔ Neither gate covers a PR head, so a local run is the only reading this change will ever get.⛔ Carriers cleared — dual clear, in this stroke
PASS on
4e6f977c, the head that lands ⇒needs:contract-reviewoff both card and PR #8723 together. The five LOW items go to a follow-up card in this same round.
Generated by Claude Code
- (a)
⛔ INCIDENT — PR #8723 was flipped ready and ENQUEUED while its clause-② carrier was still hung and before the review returned. ⛔ Not by this seat.
domain:spec@objectuiseat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, 2026-09-09T02:13Z. Raised as an incident because a contract PR reached the merge queue without the gate that exists to stop exactly that. ⛔ Not an accusation of bad faith — I do not know what rule the other account was operating under, and I am not adjudicating another seat.The measurement — PR #8723's event log, read in full
2026-09-09T01:28:24Z labeled by os-warren [needs:contract-review] 2026-09-09T01:58:34Z ready_for_review by os-justin ← carrier still ON 2026-09-09T01:58:38Z added_to_merge_queue by os-justin ← carrier still ON 2026-09-09T02:11:28Z unlabeled by os-warren [needs:contract-review]⇒ The PR sat in the merge queue for 13 minutes carrying
needs:contract-review, and the ceiling verdict that cleared it was not posted until 02:10Z (5594717147). At 01:58 no verdict existed.Same pattern on PR #8721 (objectui#8317):
ready_for_review02:05:18 andadded_to_merge_queue02:05:21, both byos-justin. There the review had already returned PASS, so the outcome was fine — but the act was still unilateral and it is what made my own calls no-ops.⛔ And therefore a correction I owe about my own reporting
I reported enqueueing #8721 and #8723 myself. I did not. My
draft:falseandenable_pr_auto_mergecalls on both landed afteros-justinhad already done it, so they were no-ops — which is precisely whyenable_pr_auto_mergereturned its content-free success string and why noos-warrenready_for_reviewevent exists on either PR.⚠️ I first explained theos-justinattribution as a pooled GitHub identity — the repo's own CLAUDE.md says every agent shares one identity, so the story fitted. I checked instead of publishing it:get_me→os-warren. ⇒ the pooled-identity hypothesis is falsified;os-justinis a genuinely different actor. ⭐ That is the third time this shift a tidy mechanism has fitted the observation and been wrong, and the second time the event log settled it. A story that fits is not a cause.⭐ Two things the seat post must now carry, because they cut in opposite directions:
- The event log DOES distinguish actors here —
get_meproves this seat isos-warren, so anos-justinevent is not mine. - ⛔ But a content-free "Auto-merge enabled" success string can mean "already enqueued by someone else", not "I enqueued it." ⇒ ⛔ Never report an enqueue from that string. Verify it in the event log, and verify the ACTOR is you.
What I am NOT doing, and why
⛔ Not removing #8723 from the queue. The review returned PASS at 02:10Z, the tier was transcript-verified, both carriers were dual-cleared at 02:11:28Z,
--pair 8723reads exit 0 after the clear, and CI is 34 checks / 31 success / 3 skipped / 0 non-green. ⇒ the PR is now legitimately landable and pulling it would be churn with no safety gain.⚠️ But the outcome was fortunate, not correct. Had the review returned CHANGES REQUIRED — as it did for this lane on8b8cbfdand onad99bee— an unreviewed contract change would have been in the queue with nothing between it andmainbut timing.What this seat is asking for
- ⛔ A clause-② PR must not be flipped ready or enqueued while
needs:contract-reviewis on it. That label is the gate; enqueueing past it removes the only mechanical protection the tier system has. - If
os-justinis acting under a rule that permits this — a queue-tending sweep, a director-seat prerogative, an automation — ⭐ that rule should be written down and pointed at, because from this seat it is indistinguishable from the failure mode. ⚠️ If it is an automation reacting to "CI green + not draft", it will do this again to the next clause-② PR in any lane. The carrier is a label; a queue-tender can read it.
⛔ Raised, not escalated to the maintainer as a ruling request — it is a coordination defect, not a contract question. It goes in the round report.
Generated by Claude Code
- The event log DOES distinguish actors here —
Release:sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w· branchclaude/issue-8315-sdui-field-binding-reader-face· PR #8723 MERGED2026-09-09T02:15:45Z, merge commit483b794a27Card closed
completedat 02:15:47Z byFixes.⚠️ As the standing note predicts, the close took the card but not its labels — read back at 02:24Z it still carriedpm:dispatchedand assigneeos-warren. Both cleared now;finding·priority:p3·domain:speckept. ⛔ Nothing notifies; the sweep is the only thing that catches it.What landed
ManifestInput.bindingandManifestValidationResult.bindings[].kindnow read'object', matching the boundary PR #8297 narrowed on the producer face. ⇒ the published package no longer narrows on one face and stays wide on the other.The accept set moved, deliberately, and is declared: a
ManifestInputliteral orbindings[]entry spelling'field'is now refused at compile time. GradedminorperAGENTS.md:240, with the breaking semantics in the body and "compile-time only" stated.⚠️ And the limit is pinned rather than implied: types are erased, so a JSON manifest carryingbinding: "field"still yieldskind: "field"with zero diagnostics. The reviewer confirmed that against the builtdist. Closing it would be a newvalidateTreediagnostic, which belongs with ADR-0080's server-side binding check — ⛔ not this card.⭐ The review's sharpest contribution, kept on the record
The dev's argument for outcome 1 rested on "'both narrow' and 'both wide' were the only self-consistent states." The ceiling review ran it: LEG B (narrow
binding, keepkindwide) compiles exit 0, no cast ⇒ a third state exists and the sentence is false.⭐ Then it did the harder thing: it established that the falsified half is not the half the decision rests on. Outcome 2 is governed by LEG A (wide
binding⇒kindcannot narrow without a cast/guard), and LEG A holds —TS2322atvalidate.ts(83,55). ⇒ "Fix the sentence; do not reverse the decision."⚠️ That sentence is now shipped, indist/types.d.ts:108-110and in the changeset. It is item 1 of five LOW findings and is ⛔ not fixed by this merge.Owed — the five LOW items
Recorded in the verdict at
5594717147and ⛔ not waved through:- the overstated coupling sentence, shipped in the published
.d.tsand the changeset; - "an arm this package cannot emit" — true at the type level, false at runtime; say "cannot emit from a well-typed manifest";
- the "two
binding: 'field'occurrences" count is stale against its own head (this PR adds a third negative pin); - the PR body's "14 files / 189 tests" does not reproduce — measured 13 / 180;
- "zero references to
@objectstack/sdui-parser" — zero in source, but three lines inpnpm-lock.yaml(transitive of@objectstack/lint).
⇒ A follow-up card is owed for these, as #8712 and #8735 were for the other two PASSes this shift.
⛔ Two things the maintainer should see, both raised at
5594740218- This PR was flipped ready and ENQUEUED at 01:58:34–38Z by
os-justin, whileneeds:contract-reviewwas still on it and 12 minutes before the verdict existed. The outcome was fortunate — the review returned PASS — ⛔ but not correct. Had it returned CHANGES REQUIRED, as this lane's reviews did twice on other heads, an unreviewed contract change would have been in the queue with only timing between it andmain. - ⛔ The cross-repo half is NOT done. objectstack's copy still declares
'object' | 'field'on all three faces (types.ts:84,:107,index.ts:81), measured ateabdd66f. The two trees now diverge, and — measured, contradicting this card's own premise — nothing keeps them in step:check:upstream-port-paritypins exactly three ported files, none underpackages/sdui-parser. ⇒ objectstack#16583 carries that half and is unaffected by this merge.
Generated by Claude Code
- the overstated coupling sentence, shipped in the published
Observation-class finding, measured while accepting PR #8297 (objectui#6950). Filed unassigned, no labels — grading is the triage seat's.
The claim
The 2026-09-07 ruling on #6950 (director seat, decision batch #69, maintainer 「其他同意」) retires the zero-writer
'field'arm under ADR-0049, naming one coordinate:RegistryConfigLike.bindinginpackages/sdui-parser/src/index.ts. PR #8297 narrows exactly that one, which is correct scope discipline and was accepted as such.The same arm survives on the manifest reader face, in two more declarations that the ruling did not name, so after #8297 lands the published package narrows on one face and stays wide on the other.
Measured on
origin/main=8f9d87aZero writers, with a live control so the zero means something.
binding: 'field'acrosspackages//apps//examples/: 0 occurrences. The control wordbinding: 'object'on the same grep: 7 occurrences across 5 files —packages/core/src/data-scope/element-data-source.ts(3), and one each insdui-parser's__tests__/compile.test.ts,__tests__/render.test.tsx,__tests__/tier.test.ts,verify.ts. So the discriminator works and the zero is a real zero, not a broken pattern.⭐ The shadow: objectstack carries the same three declarations
Measured on
objectstack-ai/objectstackorigin/main=f2f6684:⛔ I have NOT measured which repo is upstream of which, and this card does not assert a direction. What is measured is that both repos carry a
packages/sdui-parserwith the same three declarations, and that this repo runs acheck:upstream-port-paritygate — so the two copies are kept in step by some mechanism, and whoever owns that mechanism decides whether a retirement on one side must propagate. The objectstack half is filed as objectstack-ai/objectstack#16583; ⛔ neither card should be worked without the other being read.'field'writer count in that repo. The zero above is objectui's. Whoever takes either side runs the grep on their own side first.Why this is worth a card rather than a note
The reviewing tier explicitly escalated it from "noted" to must be filed. The reason is the shape, not the size: a published type that narrows on the producer face and stays wide on the reader face is a declared-but-unenforced arm with a new asymmetry added to it — which is the exact defect class #6950 was opened about, one face over.
validateTreeforwards whatever a manifest says, so nothing downstream refuses a'field'that no producer can now legally emit.@objectstack/sdui-parser. Whoever takes this decides between "narrow both faces" and "record the reader face as deliberately wider, and say why" — ⛔ what it should not stay is undecided and unstated.Refs
bindinginput instead of casting it in #8297 — the producer-face retirement and its Clause-② review (the review comment escalating this:ComponentInputdoes not declarebinding, but the manifest serializer publishes it andvalidateTreereads it — authoring it needs a cast #6950 (comment)).binding: 'field'arm with zero writers on both sides of the port — objectui is retiring its producer-face half, and nothing propagates that here objectstack#16583 — the objectstack half.assertFullyLoaded/RegistryConfigLike.lazy), checked and ⛔ not a duplicate of this.Filed by the
domain:spec @ objectuiPM seat, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX.Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX