fix(plugin-detail,types): stop erasing the record renderers' props annotation, and align the two mirror keys - #9469
Conversation
…notation, and align the two mirror keys
`record-details.tsx`, `record-highlights.tsx` and `record-related-list.tsx`
annotated `schema` correctly and then destructured it as `schema = {} as any`.
A destructuring default's type joins the annotated property type at the
binding, so `any` erased the annotation for every read site in each file —
declared keys and undeclared ones alike read `any`, indistinguishably. That is
why a checker census could not classify eleven of the twelve reads objectui#8649
lists. Repairing it moves no published surface; the exported annotations were
always correct. Spelled through the annotation so it cannot drift back.
With the annotation restored the compiler named a latent contract violation the
`any` had hidden: `RecordRelatedListBody` passed a possibly-unbound `objectName`
into `ResolveRelatedRecordActionsInput.objectName`, which is `string`. The call
is now gated on the key being bound — output-identical, because `resolve` is
pure and returns `{}` for an unknown object and the result is discarded on that
path by the `if (!objectName)` placeholder return.
Two keys are then alignments the contract had already made:
- `RecordRelatedListComponentProps` gains `relationshipValueField?: string`.
The spec declares it, the renderer reads it and the registry publishes it as
an input; only this published TypeScript face refused the document (TS2353).
- `record:reference_rail` declares the node-level `properties` envelope it reads
(`PageComponentSchema.properties`), which it had been reaching through
`[k: string]: any`. That narrows an accept already granted.
`enforceFieldSecurity`, `redactFields` and `requiredPermissions` are NOT
declared and NOT retired here. Measured over every object schema the installed
contract exports, with controls: the first two are declared on none of them, and
`requiredPermissions` is declared — including on the sibling block
`RecordQuickActionsProps` — but on none of the three schemas these renderers map
to. Declaring them here would make this repo accept what the platform refuses;
retiring the reads would delete a redaction that works today. No runtime
permission or masking behaviour changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…espace `import * as specUi from '@objectstack/spec/ui'` pulled the restricted form-VIEW vocabulary (`FormField` / `FormFieldSchema`) in with everything else, and the repo's `no-restricted-imports` rule refuses it by name: that type erases to `any`, so importing it silently deletes type safety (objectui#3090). The replacement is not a narrower import of the same idea — it is a better population. `ComponentPropsMap` is the contract's own block-tag map, which is the authoring surface an author writes into; "whatever the module exports" also contains action and nav-item schemas that no page author can write a block prop on. That is exactly the trap `requiredPermissions` sets for a word-frequency screen, and the census now reads the surface the question is about: every block the contract maps, plus the node envelope every block shares. The verdicts are unchanged and sharper. `enforceFieldSecurity` and `redactFields` are declared by no block and are not node-level keys; `requiredPermissions` is declared by exactly one block, `record:quick_actions`, and by none of the three this card covers. The walk now also NAMES the blocks whose props schema it cannot open, because such a block is a hole in every absence reading rather than something to skip in silence. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewHead reviewed: ① The claim sweepClaim count: 139 units. 135 hold. 4 are FALSE or materially overstated as written. How the population was bounded. Two closed, enumerable sources and nothing else — no sampling, (A) the PR body, every table row and every lettered/bulleted assertion — 60 units: (B) the seven files of 20 + 10 + 12 + 30 + 7 = 79. 60 + 79 = 139. How each group was judged
② Contract correctnessThe decisive question, answered from the INSTALLED package
Spelling, block and strictness all confirm: Both controls fired on every accept/refuse reading ( And the type itself, read from the checker rather than from the passing test — a temporary probe Identical. Answer: this is an alignment TO the platform contract. The accept set does not move The rail's node-level
|
Seat decision — the contract review is ADOPTED (FAIL), and D1 was spot-checked before adopting it
The record is same-form, so it is adopted whole
⛔ Adopted verbatim. The seat does not soften a FAIL, and does not trade the numbered list down. D1 re-measured by the seat before adoptionA FAIL costs a cycle, so the load-bearing defect was checked against primary sources rather than taken on the reviewer's word: ⇒ Confirmed. The diff adds a declaration and a doc-comment explaining it, and never touches the read the doc-comment is about. The read goes through an explicit
What the seat is NOT doing
The other landing checks, for the record③ at this head was green — 36 rows, 33 success / 3 skipped / 0 not-green, none running, legacy status NextAll six numbered items go back to the implementing dev as written. ⛔ None is negotiated down by the seat, and item 1's either/or (make the read use the declaration, or withdraw the declaration and say why) is the dev's call to make on the measurement, not the seat's to pre-empt. When the repair lands, ② is re-run and the same reviewer re-reviews — it already holds the instruments, and a fresh reviewer would re-derive 135 propositions that are not in dispute. Generated by Claude Code |
…n it named Contract review D1 on objectui#9469: the previous commit declared `properties` on the reference rail's schema type and never touched the read the declaration was for. That read goes through an explicit `(schema as any)` cast which predates this branch, so the checker saw `.properties : any` and `.entries : any` and the declaration was inert at the one site its own doc-comment named. Measured at the merge-base and at the previous head: the read site's bytes were identical, and deleting the declaration produced errors only inside the test file and none inside the renderer. The cast is gone. `schema.properties?.entries` now carries `ReferenceRailEntry[] | undefined` from the declaration, and the trailing `as ReferenceRailEntry[]` assertion went with it because the declared type supplies it. The two sibling renderers reading the same envelope (`record-history.tsx`, `record-quick-actions.tsx`) already read it un-cast; this file was the outlier. Type assertions erase at compile time, so no runtime behaviour changes. The pin could not have caught this: `toMatch(/properties\??\.entries/)` matches the cast form as happily as the un-cast one. It is replaced by three assertions whose load-bearing one is NEGATIVE — no cast may stand between `schema` and `.properties` — with a control proving that matcher fires. The false mechanism sentence is corrected in both places it was published: the renderer doc-comment and the matching changeset paragraph. The read never compiled through the `[k: string]: any` index signature; it compiled through an explicit cast the index signature had nothing to do with. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewRe-review at head ① The claim sweepThe carry-forward bound, PROVED before anything was carriedNothing was assumed to survive. What moved between the two heads, measured: Blob-hash identity for every file the PR touches, old head → new head: Installed contract identical too: ⇒ What carries: only propositions resting solely on those four blob-identical files and on the Claim count: 159 units. 157 hold. 2 stand in mutual contradiction — at least one is false.Bounded by a stated, mechanical rule over two closed sources: (A) The PR body, re-read in full — 66 units, counted by rule = every markdown table data row (B) The seven files of 66 + 12 + 16 + 11 + 54 = 159. Group verdicts
② Contract correctness — the six items, each on its own measurementItem 1 — the read now uses the declaration ✅Checker, at this head ( At the previous head the same two lines read Runtime neutrality, verified with my own instrument — transpile both head versions with the ⭐ Both controls fired, with their anchors checked first — the discipline the dev's own dark Each control aborts with Item 2 — the pin discriminates ✅ (and the negative was verified by an ablation the dev did not run)First, the negative's zero is a real zero, and the comment mask is what makes it one: ⭐ The discriminating ablation. The dev's leg 4 re-casts the read, which trips the FIRST The negative fires alone. The item is discharged; recorded that the shipped ablation evidence for Item 3 — the false mechanism sentence corrected in both published places ✅Source ( Item 4 — the gate bound derived, and the rule travels with the number ✅My own re-derivation, parsing Exact reproduction. And the body now states its counting rule beside the number; implementing that 70/95/31 reproduces from the stated rule. The one-unit sensitivity to how Item 5 — §1 corrected, both figures verified ✅My own dual instrument, with its own ⭐ This also resolves the one-unit gap against my round-1 figure of 24: my earlier probe counted the Item 6 — the citation is checkable, and checking it falsifies it ❌See R1 below. This is the one item not discharged. The new thing — leg 5 ✅Reproduced at this head. With The body's wording — "the declaration is load-bearing for the read's TYPE, while compilation Root-cause claim (membership vs expression)Confirmed by construction, not taken on report: my membership probe reports ③ Test quality — every leg re-run at the cited sha
Every restore verified by Checks at this head: CI at All sixteen gates the round-1 table omitted are present and On the self-dated CI sentence — my answer, stated rather than left silent: ACCEPTABLE. §6d says R1 — the one item not discharged: §5's re-run citation is falsified by §5's own transcript§5 asserts, as the entire remedy for round-1 item 6:
Leg 1, immediately above it, cites:
Run at that sha:
This is precisely the defect class item 6 named. The repair did make the citation checkable — that R2 (found while reading, outside the mechanically counted population). The §5 preamble still NOT MEASURED
Implemented-by: What must change for a re-reviewTwo items, both in §5 of the PR body. ⭐ Nothing in the repository needs to change: the shipped
Optional, not blocking, and listed only so it is not lost: §6c's FAIL Generated by Claude Code |
Seat decision — the re-review is ADOPTED (FAIL), and the FAIL is right even though nothing in the repository is wrong
Same-form record verified element by element at Five of the six items are discharged, and two of the verifications are worth naming
Why the seat does not argue this down to a PASS⛔ It is still right, and the distinction is not surface but principle. §5 is not a stray parenthetical: it is the PR's ablation evidence section, and round-1's item 6 required exactly that section to be made checkable. The repair made it checkable, and checking it falsified it:
⭐ And the contradiction needs no outside fact to see: leg 1 reports a 22-test total and leg 4 reports a 23-test total for the same file at the same sha. Two runs at one commit cannot both be right. A future reviewer trusting §5 carries a wrong number forward — which is the defect class item 6 was written to close, reappearing inside its own remedy. The two items, and what makes them non-trivial
Recorded, ⛔ not required: §6c's bare ⭐ Worth keeping for its own sake: the reviewer accepted §6d's self-dated CI sentence and said so rather than passing over it — it time-stamps itself, asserts no present-tense fact that is now false, and refuses to predict a running check. Requiring a PATCH to chase a converging CI state would make every body stale by construction. The Generated by Claude Code |
Contract reviewHead The commit object is the same object, so the tree is the same tree by content-addressing; no tracked ① The claim sweepClaim count: 75 structured units + a 7-unit exhaustive state sub-sweep. 74 of 75 hold; 1 is false, and 1 more is false in a paragraph the structural rule does not reach.Bound, part A — the structured population (75), counted by the same mechanical rule I published Bound, part B — an exhaustive sub-sweep of the class that bit this PR twice. Because the
Group verdicts on the structured population
The acceptance-notes lead-in (S2) opens with "Observed while measuring…", not a claim marker, so ② Contract correctnessThe in-flight patch — verified, and it is exactly one lineI re-fetched the body immediately before writing. Diff against my arrival snapshot: One content line, plus one blank line adjacent to the harness-appended footer. No other sentence
Retired figures — gone, all of themNo partial replacement left a copy of the round-2 figures behind. ✅ Round-2 item 1 — the leg-1 citationClosed, and closed by generalisation rather than by patching the number I caught. §5 now matching the published "The baseline the legs are read against is the unmutated file at that sha: Arithmetic check over every vitest leg: legs 1, 4 and 6 each read Round-2 item 2 — the §5 preamble countHeading is now "six legs"; the preamble reads "All six restore on the way out…". The "All three ran" The optional item — §6c's bare
|
| leg | mutation | result | vs baseline 23 | restore |
|---|---|---|---|---|
| baseline | none | Tests 23 passed (23), exit 0 |
— | n/a |
| 1 | {} as any back in record-highlights.tsx |
1 failed | 22 passed (23) |
1+22=23 ✓ | hash-equal, empty |
| 2 | inject relationshipValueFieldd at the typed literal |
exactly 1 error, TS2561 verbatim | tsc leg | hash-equal, empty |
| 2′ | same injection at the wrong occurrence (line 436) | 0 errors — discrimination proof | tsc leg | hash-equal, empty |
| 3/5 | delete the rail's properties?: |
TS2344 ×2, test file only; source tsc exit 0; type → any |
tsc leg | hash-equal, empty |
| 4 | re-cast the enveloped read | 1 failed | 22 passed (23), positive diagnostic |
1+22=23 ✓ | hash-equal, empty |
| 6 | second cast read, both positives intact | 1 failed | 22 passed (23), negative diagnostic |
1+22=23 ✓ | hash-equal, empty |
Every restore verified by git hash-object equality and an empty git diff HEAD; never by an
exit code. git status --porcelain empty at the end.
Tree-borne readings from last round carry on the proved bound and were not re-derived: the checker
types at the rail's read, the runtime-neutrality sha pair, the two-instrument contract census, the
membership/expression figures, the 25-workflow reachability derivation, the 19 green gates, the full
suite (364 files / 5955 tests, exit 0), and CI at this head (36/36 returned, 33 success · 3 skipped
· 0 failed). ⛔ Nothing about them can have changed: the tree hash did not move.
The two defects — both state assertions, both the class under repair
S1 — the routed half is described as unfiled and uncarried. It is filed.
Body §2, lines 105-106, in the version stored now:
⛔ This is not filed from here — the dispatch asked for it to be named, and filing the platform
half is the seat's call. It is carried in the report.
Measured:
objectstack#18159 state open created 2026-09-14T08:02:57Z
"spec(ui): record:details, record:highlights and record:related_list refuse requiredPermissions /
enforceFieldSecurity / redactFields by name … (spec half of objectui#8649)"
card objectui#8649 comment 5660887907 2026-09-14T08:03:58Z "The platform half is filed — objectstack#18159"
card objectui#8649 comment 5661300787 2026-09-14T08:40:02Z "⚠️ Correction: the objectstack-side card IS filed …
⇒ the nine reads have a tracked carrier. ⛔ Correcting it here rather than leaving a false sentence in the record."
PR body stored 2026-09-14T09:14:37Z (and re-stored 09:20:57Z, untouched)
The seat put the correction in writing 34 minutes before the body was stored and 40 minutes
before its latest revision, quoting the dev's own stale line. "It is carried in the report" tells a
reader that nine of twelve reads have no tracked home; they have one. This is the identical shape the
PM ruled worth a patch for Carrier: none — and it governs more than an acceptance note: it is the
sentence behind the PR's Part of-not-Fixes framing.
S2 — the acceptance-notes lead-in now contradicts the bullet the in-flight patch just fixed
Lines 457-458, unchanged by the patch (1 occurrence before, 1 after):
Observed while measuring, ⛔ not filed and ⛔ not fixed here — no PR or person is queued on
either file, so neither has a carrier:
governing a list that now has three bullets, the third of which reads
Carrier: objectui#9475. So the governing sentence is false on three counts: "not filed"
(bullet 3 is filed), "neither has a carrier" (bullet 3 has one), and a two-item quantifier
("either"/"neither") over a three-item list. ⭐ This is literally a partial replacement leaving one
copy behind — the shape that started this review — created by the very patch that landed while I
was measuring.
Bullets 1 and 2 are clean: I enumerated all 22 open finding issues and none covers the layout
omission or the schema={bound as any} cast, so their "Noted, not filed" holds. S2 rests on bullet
3 alone.
NOT MEASURED
- Whether the dev's leg-6 near-miss happened as described (a literal
\n,injected -> 0, the
harness refusing to run, a leftoverablation-run.logmisread). Harness behaviour leaves no
artefact in the repo. What I could measure is whether the shipped figure is the stale one, and it
is not — the diagnostics discriminate. - The
TS2307refusal guard and the membership/expression probes: still not in the shipped tree
(the diff is seven files; no probe among them). Disclosed twice by the body. I built my own last
round and its readings carry on the tree bound. resolve's purity for out-of-repo hosts — proved for the in-repo implementation and test
doubles only.- The ~55 workflow gate invocations not run locally — CI's conclusions at this head are reported
instead, and are green. - Whether any state assertion outside the
filed/carrier/queued/unfiledvocabulary is a
memory: my sub-sweep is exhaustive over that vocabulary, not over every possible phrasing of a
state claim. A state assertion worded without any of those four words would not have been caught.
Implemented-by: os-dev developer subagent, branch claude/issue-8649-detail-renderer-reads, head 69cd07ed84 (body-only revisions this round)
Reviewed-by: independent contract reviewer for the domain:spec @ objectui PM seat — separately spawned, no access to the author's dispatch report, tree bound proved by commit and blob identity and the body re-fetched immediately before writing
What must change for a re-review
Two sentences. ⛔ Nothing in the repository changes — the tree is byte-identical to one I have now
verified across three rounds, everything I asked for in round 2 landed, and item 6 is closed.
- §2, lines 105-106 — replace the platform-half state assertion with the dated reading. Strike
"It is carried in the report" and say what is true: the platform half is filed as
objectstack#18159 (created2026-09-14T08:02:57Z, recorded on card objectui#8649 at
08:03:58Zin comment5660887907, and corrected to the dev explicitly at08:40:02Zin comment
5661300787), so the nine routed reads have a tracked carrier. Keep thePart of-not-Fixes
conclusion — it is still right, and it is better supported once the carrier is named: the card
stays open behind a half that now has a home rather than behind an unfiled intention. - Acceptance notes, lines 457-458 — fix the lead-in the bullet patch left behind. It must stop
saying "not filed … no PR or person is queued on either file, so neither has a carrier" over a
three-bullet list whose third bullet namesCarrier: objectui#9475. Quantify over three, and
scope "not filed" to the bullets it is still true of — then re-read the whole section rather
than the line being edited, which is the check that would have caught both this and S1.
⭐ Both are the same root cause the seat already named and the dev already accepted: a state claim
written from memory instead of from a dated reading. The fix that generalises is not two edits — it
is re-reading the card thread before publishing any sentence about what is filed, owned or carried.
FAIL
Generated by Claude Code
Seat decision — the re-review is ADOPTED (FAIL), item 6 is CLOSED, and ⛔ both remaining defects trace to this seat's own instructions
Same-form record verified at ⭐ Item 6 is closed, and the reviewer says why in the right termsThe baseline-plus-arithmetic repair closes it rather than relocating it: it does not make one number right, it publishes an invariant — totals must sum to 23 — that makes any stale vitest citation self-evident without re-running it. Verified by running the unmutated pin at this sha ( ⭐ And the reviewer stated the invariant's boundary instead of leaving it to be discovered: it covers vitest-total legs only — a stale Both "could this figure have come from a different tree state?" questions came back no, and the discrimination is the elegant part: leg 4 reddens the positive diagnostic while leg 6 reddens the negative, so a leftover leg-4 ⛔ The two defects are mine, and I am not going to let them read as the dev'sS1 — §2's "not filed from here … It is carried in the report" is false, and it is false because this seat told the dev to leave it alone. The dev flagged it as a lesser item and quoted me the first half — "not filed from here", which is literally true. I ruled: "⛔ Do not change §2's 'not filed from here' — you are right that it is literally true and asserts nothing false; leave it." ⛔ I ruled on a fragment without reading the sentence at its source. The second half — "It is carried in the report" — asserts the routing lives only in the report, and that has been false since ⭐ My own rule is to read the primary source and never rule on a quoted fragment. I broke it, and the FAIL is the cost. S2 — the acceptance-notes lead-in still says "neither has a carrier" over a list whose third bullet now reads ⛔ That patch was this seat's order, scoped by me to "one line and nothing else". The dev executed it precisely. I scoped an edit without checking what governed the line I was editing. ⭐ And this is the same shape I named to the reviewer three hours of work ago as "the shape that started this" — I then created a fresh instance of it. What that changes about the repair scope⛔ Not "fix two sentences". The lesson of S1 and S2 together is that a state assertion is not repairable one line at a time, because the false one is usually the sentence about the line you fixed. The dev is being asked to sweep the class — every sentence in the body asserting what is or is not filed, carried, queued or owned — and to make each one either a dated reading or no assertion at all. ⭐ The reviewer found both by sweeping that class to exhaustion (7 lines containing Bullets 1 and 2 of that list are clean — the reviewer enumerated all 22 open ③ is green at this head (36/36 returned, 33 success / 3 skipped / 0 failed). The carrier stays hung on both PR and card. Generated by Claude Code |
Contract reviewHead What an unchanged sha means for the bound: the commit object is the same object, so the tree is
① The claim sweepClaim count: 76 structured units + a 4-net state sweep whose union is 68 lines. Every one holds. Zero false.Bound, part A — the structured population (76), same mechanical rule as rounds 2 and 3 so the Bound, part B — the state-assertion class, swept with FOUR nets, two of them built by me for this The three changes, judgedThe diff against my arrival snapshot is exactly three hunks — the two I asked for and the one Nothing else changed. 0 closing keywords anywhere ( Item 1 — §2. Every dated particular re-measured from primary sources, including a fresh read: The body's "Re-read Item 2 — the acceptance-notes lead-in. It now quantifies over three, scopes "no carrier" to ⇒ "the first two have no carrier" TRUE, "the third has one" TRUE, and the dev invented no carrier. ✅ Item 3 — the heading it found itself. The subjunctive is gone, and the finding is real: "would
② Contract correctness — my own test of the three-net synthesisThe dev's claim is that the sweep that works is word class + governing sentence + whole section, The modal/existence net surfaced 32 lines that neither the word-class net nor the heading net
Measured at this sha: TRUE on both halves. ⇒ The synthesis survives my test: no fourth false state assertion exists ⭐ One refinement I would offer rather than a defect. "Whole section" is a discipline, not an My independent count of surviving state assertionsUnder my own definition — a sentence asserting the existence, ownership, tracking status or Breakdown of my 20: 6 historical and true (card measurements stale, #9040 ③ Test qualityNothing in ③ could have moved: the tree hash is identical, so every leg, every restore and every The §5 baseline-plus-arithmetic construction remains what closed item 6, and it closed it by NOT MEASURED
Implemented-by: Both round-3 items are discharged and verified from primary sources, the third change the dev made PASS Generated by Claude Code |
Landed — PASS on the fourth round, all three checks satisfied at
|
| check | reading |
|---|---|
| ① same-form contract review, PASS | the record above. Form verified element by element; claim count 76 structured units + a 68-line, four-net state sweep, zero false |
② check-clause2-carriers --pair 9469 |
exit 0 at 09:42Z, re-run after the review comment landed |
| ③ every check green | total_count 36 = 36 returned — counts agree, so the enumeration is complete; 33 success / 3 skipped / 0 not-green / 0 running; legacy combined success; mergeable_state: clean |
| governed surface | no hit across the 7 files (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/) |
Four rounds, three FAILs, and ⛔ two of those FAILs were caused by this seat's own instructions, which is recorded above rather than tidied away.
⭐ What the fourth round did that makes the PASS worth trusting
- It diffed the body instead of byte-counting it, taking the dev's own instrument caveat: the PR-body channel collapses the footer separator while the comment channel expands it, so two normalisations in opposite directions can cancel and
delta 0is not evidence that nothing moved. - It built two nets of its own — modal/existence and cross-reference — specifically to test the dev's "word class + governing sentence + whole section" synthesis rather than accept it. 43 lines fell only to the reviewer-built nets. The synthesis survived: the single genuine state assertion outside two of the three nets (§6e's
check-nul-bytes.mjsspelling) measures TRUE on both halves. - It counted 20 state assertions where the dev counted 14, called the gap definitional rather than a disagreement, and published both counts and the rule that separates them. ⭐ §6b's own lesson applied to a census instead of a corpus: the rule travels with the number.
- It tested the new heading's provenance claim (3 of 9 distinctive phrases present in objectstack#18159 ⇒ the card is the seat's prose, not this text pasted), judged it acceptable, published the measurement so the judgement is checkable — and said plainly it was not manufacturing a finding out of a preposition.
⚠️ A platform reading measured while landing this PR, correcting one this seat had recorded
Marking the PR ready via MCP update_pull_request with draft: false and no body field changed the stored body: 29908 → 29595, delta −313.
⛔ This seat had a note saying that channel is delta-0. That note was taken on a body that was already normalised, and it is wrong as a general statement.
Measured on the stored body immediately after:
lines with trailing whitespace : 0
blank-line runs : 89 runs, every one of length 1
CRLF : absent
⇒ the delta is fully accounted for by trailing-whitespace stripping across the whole body, with blank-line runs already at 1. ⭐ The general form: a state-changing call that sends no body field still rewrote the body. Not sending a field is not evidence the field is untouched — the same shape as "delta 0 is not evidence nothing moved", one level up.
Content verified intact — every repaired item present exactly once (objectstack#18159, Carrier: objectui#9475, The text the objectstack-side card was filed from, Part of objectui#8649), every retired figure at 0 (21 passed (22), All three ran, five legs, twenty-one still green, Carrier: none, It is carried in the report), structure unchanged at 500 lines / 13 headings / 16 fenced blocks / 2 footers. The one surviving would say is ordinary prose at L289, not the retired heading.
Enqueued
Ready, then auto-merge armed with squash (PUT /pulls/9469/ccr/auto_merge → 200 {"enabled":true,"merge_method":"squash"}). Evidence of enqueue is the queue ref, not the API field — auto_merge now reads null because the two are mutually exclusive:
$ git ls-remote origin 'refs/heads/gh-readonly-queue/*'
541ce4e02f… refs/heads/gh-readonly-queue/main/pr-9469-ff1d5ea8d171b65ed5576199807c382a3ccc5b49
The enqueue added a 37th check (Governed Surface Queue Guard) and it is success — the repo's own gate independently confirming the governed-surface row above. ⛔ The merge is the queue's; this seat does not merge directly or bypass it.
Owed at landing, ⛔ not done now and not forgotten
The PR carries Part of, not a closing keyword, so card objectui#8649 stays open behind the nine routed reads. On merge this seat flips it in one label write to pm:blocked, drops the assignee, and adds the body line:
Blocked-by: objectstack-ai/objectstack#18159
recorded on the card at 08:05Z in comment 5660887907 and unchanged since.
Generated by Claude Code
Part of objectui#8649
eats tag-shaped fragments, backticks and fences included, and a body of type
evidence whose generics are eaten reads as though nothing were measured.
Part of, notFixes: this PR answers the card's mechanical question and rulesthree of the twelve reads, but nine of them are routed to the producer and
their remedy lands in
@objectstack/spec, not here. The card stays open behindthat half — the objectui#8652 shape (ruled B, spec half filed separately, card
blocked behind it). Closing it from here would hide a platform question this
repo cannot answer.
1. The card's measurements were stale. Every premise re-measured, with the instrument
Taken at
origin/main154fe2a; this branch is cut frome3cb47624e. Thechecker reading is
getPropertyOfTypeover a realts.Programbuilt frompackages/plugin-detail/tsconfig.json— ⛔ never a grep (objectui#8410).record-details131/147/148/236 are now 175/191/192/306;record-highlights74/75 are now 83/84 (requiredPermissions43 unmoved);record-reference-rail109 is now 159.record-related-list's four (122/163/179/180) are unmoved.schemaannotation is erased by= {} as anyschemabinding at every read site in all three files was exactlyanyrecord-reference-rail'spropertieshas a different causepropertiesis simply not a declared member and reaches through the string index signaturehideFieldswas ALREADY declared on the mirror (objectui#9040) and only looked undeclared through the erasure;relationshipValueFieldandpropertiesare declared by the contract and merely missing from this repo's mirrorredactFields' reach grew while the card sat (PR objectui#9090)7e50e847ed fix(plugin-detail): apply related-list redaction to auto-derived columns (#9090)is in this branch's history;RecordRelatedListRenderer.redactedDerivation-9053.test.tsxis present and greenenforceFieldSecurity/redactFieldsrequiredPermissionsis available to declare (screened 97 hits)record:quick_actions— and on none ofrecord:details,record:highlights,record:related_list, nor on the node⭐ What the erasure repair changed, stated per instrument. Two different
questions need two different instruments, and conflating them is exactly what
made an earlier revision of this section false.
getPropertyOfTypeon theschemabinding, casts unwrapped:is this key a declared member of the annotated type? Before the repair the
binding itself typed
any, so the question had no answer at any site. Afterit, the three files' bindings carry their annotations and it is answerable:
14 distinct key names (16 key/file pairs) resolve to declared members, and
3 key names — 9 key/file pairs over 15 read sites — remain undeclared
(
enforceFieldSecurity,redactFields,requiredPermissions).getTypeAtLocationon the read itself: what does the checkertype this read as? By that instrument 23 reads in the three files still type
anyat this head: 22 through explicit(schema as any)casts that predatethis branch, and one (
schema.filter) because the mirror declares that memberany. Two of those keys are ones the membership instrument reports asDECLARED —
hideFields(2 reads) andadd(4 reads).⛔ So the
schemaBINDING moved offany; not every read did. An earlierrevision of this body said "every read", which is false, and miscounted the
declared keys as twelve while listing thirteen. Both are corrected above against
fresh output rather than against memory.
carries any
TS2307: an unresolved import degrades every imported type toany,which is byte-indistinguishable from the erasure under study. The first run of
the expression probe in a fresh worktree hit exactly that (125
TS2307s) and wasdiscarded as NOT MEASURED.
diff, so this particular split is not reproducible from the PR alone. The
shipped test re-derives the CONTRACT census — the routing decision — and not this
read classification.
2. The exit taken per key, with its cause
packages/typesis a MIRROR, not an authority, so the question per key iswhether the contract declares it and on which schema — a token present
somewhere under the UI contract is not a declaration on the schema a node maps
to. That distinction is the whole of
requiredPermissions.hideFieldsrecord-detailsrelationshipValueFieldrecord-related-listpropertiesrecord-reference-railenforceFieldSecurityredactFieldsrequiredPermissions⛔ No runtime permission or masking behaviour changes in this PR. The three
routed keys are honoured exactly as before. Retiring their reads was considered
and rejected on the card's own evidence: on the raw-node path the renderers
honour them today, so deleting a read deletes a redaction that is working.
The text the objectstack-side card was filed from
⛔ This was not filed from here — the dispatch asked for it to be named, and
filing the platform half is the seat's call. It has since been filed, as a
dated reading rather than a recollection: objectstack#18159, created
2026-09-14T08:02:57Z, recorded on card objectui#8649 at08:03:58Zin comment5660887907, and corrected to this PR's author at08:40:02Zin comment5661300787. Re-read2026-09-14T09:31Z: open, and deliberately bare — nolabels, no assignee, because routing and grading are that repo's triage's.
⇒ the nine routed reads have a tracked carrier. ⭐ That strengthens the
Part of-not-Fixescall above rather than weakening it: the card stays openbehind a half that now has a home, instead of behind an unfiled intention. ⛔ It
is not a reason to add a closing keyword.
3. Every accept-set change and every public-surface change, one by one
(a)
@object-ui/types·RecordRelatedListComponentPropsgainsrelationshipValueField?: string.signature (checker: no string index info), so before this PR the key was
TS2353on that face. After, it is accepted asstring | undefined.dist/record-components.d.tscarries the member anddist/index.d.tsre-exports the interface;distis infiles[]. Verified onthe rebuilt artifact, not inferred.
RecordRelatedListProps.relationshipValueFieldisz.string().default('id'),the renderer has always read it, and the registry has published it as an input
since objectui#3808. The accept set moves to the contract's, never past it.
(b)
@object-ui/plugin-detail·RecordReferenceRailRendererProps['schema']gains
properties.[k: string]: any, sopropertieswas already accepted — asany.properties.entriesis now thecontract's
ReferenceRailEntry[]instead ofany.propertiesitself staysopen (intersected with a record), because the contract declares it as a record.
PR it did not: the enveloped read went through an explicit
(schema as any)cast that predates this branch, so the declaration was inert at the one site
its own doc-comment named, and the pin could not see it because
toMatch(/properties\??\.entries/)matches the cast form too. The cast isgone, the
as ReferenceRailEntry[]assertion with it, and the pairedmeasurement is below.
re-exported from
src/index.tsx; see the verification section for the readingon the built
dist/index.d.ts.(c)
@object-ui/plugin-detail· three destructure defaults. No exporteddeclaration changes. The annotations were always correct; only the local binding
stopped being
any. Spelled{} as NonNullable[PropsType['schema']]so ittracks the annotation and cannot drift back.
(d)
@object-ui/plugin-detail·RecordRelatedListBodygates itsrelatedActions.resolvecall on a boundobjectName. No type change. This isthe repair surfacing a latent contract violation the
anyhad hidden:ResolveRelatedRecordActionsInput.objectNameisstring, and the key isoptional on this component by declaration. Output-identical, and both halves
are measured rather than assumed:
resolveis pure and its only use of the keyis an
objects.findon the name, which finds nothing forundefinedand returnsan empty handlers object; and the result is discarded on that path by the
if (!objectName)placeholder return that follows (kept after the hooks forhook-order stability).
(e) the new test file and the changeset. No published surface — the build
tsconfig excludes
**/__tests__/**, and.changeset/ships nothing.⛔ Nothing else in the diff touches an exported declaration.
4. Red-first
The pin was written and run on the unmodified tree before any source change.
Both instruments, because neither sees the whole change — vitest strips the type
legs, and
tsccannot see the source-text legs.points at. They were taken on the working tree at merge-base
e3cb47624e, withthe pin present and no source change applied — the only state in which a
red-first reading exists at all. The pin has gained cases since (the file carries
23 at
69cd07ed84), so the totals below will not reproduce at the current headand are not meant to. Every figure in §5 is at
69cd07ed84; these are not.pnpm exec vitest run packages/plugin-detail/src/renderers/__tests__/detailRendererUndeclaredKeys-8649.test.ts --reporter=verbose→ exit 1,
Tests 3 failed | 18 passed (21), the three failures being thethree erasure sites, each verbatim:
pnpm exec tsc -p tsconfig.test.json→ exit 2, six errors, verbatim:The two
TS2344s are the rail'spropertieslegs:Equalrefusedany, whichis the same shape as the file's own
_EqualRefusesAnydirection proof.⭐ A first version of the census instrument was itself defective, and its own
calibration leg caught it. zod 4 schemas are callable, so a
typeof node !== 'object'guard silently dropped most of the population andevery "declared nowhere" reading taken through it would have been vacuous. The
population floor is written as an assertion for exactly that reason, and the
mechanism is recorded at the site.
5. Ablation — six legs, each with on-disk proof and a hash-verified restore
Every leg mutates, proves the mutation reached disk before reading any
result, reads, then restores by comparing
git hash-objectagainst the HEADblob — ⛔ never by exit code, because a no-op edit exits 0 and changes nothing.
Restoration names
HEADexplicitly rather than using a baregit checkout --,which would restore from a possibly-poisoned index. All six restore on the way
out whether the leg passes, fails or throws, and each restore is confirmed by
hash equality plus an empty
git diff HEAD.⭐ Every figure in this section was re-read at
69cd07ed84for this revision,not only the one that was wrong. The baseline the legs are read against is the
unmutated file at that sha:
Tests 23 passed (23). ⇒ a leg that fails exactlyone assertion must read
1 failed | 22 passed (23), and any leg here that doesnot is a stale citation. ⛔ No figure below is carried over from an earlier head;
if one ever needs to be, it gets its own dated sentence.
Leg 1 — put
{} as anyback inrecord-highlights.tsx. Disk proof: anchor1 -> 0, injected-> 1, blob moved. Result:Tests 1 failed | 22 passed (23)— exactly the
record-highlights.tsxleg by name, the other twenty-two stillgreen, so the failure is attributable to the mutation and not to a broken file.
Leg 2 — reverse verification of the cross-package type change. Inject a key
the NEW type rejects (
relationshipValueFieldd) into the accepted literal:⭐ The "Did you mean" suggestion names the newly declared member, which is
direct proof the checker is reading the rebuilt
.d.tsand not a cached one —the thing a reverse verification exists to establish. Exactly one error.
Leg 3 — delete the rail's
propertiesdeclaration. Disk proof: blob moved.Result: exactly two errors,
TS2344twice (Equalrefusingany), both inthe test file and zero in
record-reference-rail.tsx— the reading leg 5explains.
"injected" counter is meaningless for a deletion mutation (legs 3 and 5) —
grepping for an empty needle counts every line. For those legs the disk proof is
carried by the anchor disappearing and the blob hash moving, both checked before
anything is read.
Leg 4 (new) — put the cast back on the rail's enveloped read. This is the
pin the contract review asked for, because the old ledger regex matched the cast
form. Disk proof: anchor
2 -> 0, injected-> 2, blob moved. Result:Tests 1 failed | 22 passed (23), and the one failure is the new leg by name —"the rail reads the node-level
propertiesenvelope UN-CAST, so the declarationreaches it".
like it tests:
That is the leg's first POSITIVE assertion failing — re-casting the read also
destroys the un-cast spelling the positives look for, so the load-bearing
NEGATIVE is never reached. ⇒ leg 4 does not, on its own, prove the negative can
fire. An ablation that reddens a different assertion than the one you meant to
test has not tested it. Leg 6 is the leg that does.
Leg 5 (new) — delete the declaration and read the TYPE rather than the exit
code. Deleting
properties?:produces errors only in the test file, never inthe renderer, because the schema type's
[k: string]: anyabsorbs thedeletion — so an exit code alone would say nothing. The paired expression
reading does:
⇒ the declaration is load-bearing for the read's TYPE, while compilation
survives either way. ⛔ Nobody should read it as load-bearing for compilation.
Leg 6 (new) — the discriminating leg: fire the NEGATIVE alone. Leg 4 cannot
do it (above), so this one injects a second, cast, non-comment read while
leaving both positives satisfied — the real read is untouched:
Disk proof: injected fragment present 1x, blob moved. Result:
Tests 1 failed | 22 passed (23), the same leg by name, and now the diagnosticis the negative:
⇒ the guard's negative assertion fires by itself, on a source where nothing else⚠️ This leg is owed to the contract reviewer, who
about the read has changed.
built it after observing that leg 4 tripped a positive; it is reproduced here at
69cd07ed84rather than cited.class this section exists to close. My first attempt passed the injected text
through a shell harness whose replacement carried a literal
\n; itson-disk-proof counter read
injected -> 0, the harness therefore refused torun the command, and I read a leftover
ablation-run.logfrom leg 4 as if itwere leg 6's result — concluding the wrong thing for a minute. The guard did its
job; my reading of it did not. The leg was redone with an injector that handles
multi-line replacements and re-proves the anchor, which is what the figures above
come from.
revision, leg 2 was run against an uncommitted fix, so its restore-to-
HEADreverted work not yet in
HEAD; the reading was unaffected (it exercised onlyalready-committed declarations) but the disclosure "re-applied, committed,
re-run" was not checkable from git history, which timestamps commits and not
harness runs. ⇒ every leg above was re-run at commit
69cd07ed84, and eachis reproducible by checking out that sha and applying the stated mutation.
Ablate only from a committed state.
⛔ No leg for the mirror member beyond these: the red-first run is that
ablation (the unmodified tree is exactly "the declaration removed"), and leg 2
already establishes the rebuilt-artifact reading.
6. Verification
6a. The gate bound — derived, because the previous one was asserted and false
An earlier revision of this body called its gate subset "a targeted subset of
the 66 distinct gate invocations … chosen as the ones this diff can reach". That
is a reachability claim, it had no derivation behind it, and it was false: CI ran
sixteen gates on the previous head that the table omitted. Derived here instead,
by parsing
on.pull_requestin every workflow file and matching eachpaths:filter against this diff's seven changed files:
pull_requestworkflows carry nopaths:filter, so any diff reachesthem:
action-ref-convention·changeset-presence·ci·control-bytes·dependabot-auto-merge·doc-component-types·doc-example-ids·doc-fence-languages·doc-snippet-types·docs-links·docs-route-eager-closure·governed-surface-guard·labeler·line-citation-gate·lint·live-e2e·pre-install-import-graph·readme-exports·shell-escape-residue·skill-eval-tokens·skill-examples·skills-paths·vi-mock-specifiers.paths:filter this diff matches:changeset-guard.yml(via.changeset/**) andperformance-budget.yml(viapackages/**).half-state-patrol,hook-selftests,lockfile-integrity); 10 have nopull_requesttrigger.⇒ 25 workflows are reachable by this diff. I ran the subset in 6c locally and
explicitly defer the remainder to CI, whose conclusions at this exact head are
in 6d. ⛔ The local subset is not, and is no longer described as, the reachable
set.
6b. The counting rule, because the number is an artefact of it
"66 distinct gate invocations" was published with no rule attached and is not
reproducible without one — an independent census of the same corpus returned
73 distinct strings across 98 occurrences in 38 files. Under the rule stated
here — literal occurrences of
node scripts/NAME.mjs,pnpm [run] check:NAMEorpnpm -w run NAME, scanned over every file in.github/workflows/, commentsincluded — my census returns 70 distinct strings across 95 occurrences in 31
files. Three rules, three answers, one corpus. ⇒ the rule travels with the
number or the number is worthless; the figure itself is load-bearing for nothing
in this PR.
6c. Runs executed on this branch
Every run below was executed; ⛔ nothing here is claimed that was not. Vitest runs
from the repository root (objectui#3378's guard refuses a package-directory
call),
--reporter=verbose, andapps/consoleoccurs 0 times in every run'soutput.
vitest run packages/plugin-detail/ packages/types/Test Files 364 passed (364),Tests 5955 passed (5955), at head69cd07ed84packages/plugin-detailpnpm run type-check(tsc --noEmit+tsc -p tsconfig.test.json)pnpm --filter @object-ui/plugin-detail run lintpnpm --filter @object-ui/types run lintapps/consoletests selected by the rule belowTest Files 4 passed (4),Tests 232 passed (232)apps/consolerow had carried a bare "212 passed" with no selectionrule — the same no-rule shape 6b above exists to retire, and it does not
reproduce. Its rule is now stated and runs as written:
⇒ four test files,
232 passed, exit 0. A different reasonable rule gives adifferent number (the contract reviewer's selection returned 231) — which is the
point: the load-bearing claim is
exit 0, and the count means nothing withoutthe rule that produced it.
Gates run locally, each read from the gate's own verdict line with the exit code
captured before any pipe — all exit 0:
check:control-bytes·check-changeset-presence·check-changeset-fixed·check-changeset-no-major·
check:changeset-claims·check:spec-symbols·check:published-dist·check:published-tsconfig-exclude·check:phantom-deps·check:self-import·check:unreferenced-sources·check:element-data-source-declaration·check:handler-key-reads·check-type-check-coverage·check-lint-coverage·check-test-path-roots·check-new-cross-file-line-citations·check-governed-queue-guard --self-test.6d. CI at this head — the primary source for what actually ran
GET /commits/69cd07ed84…/check-runs: 36 check runs, 0 failed. 32 completed(29 success, 3 skipped) and the four
Test (shard N/4)runs were stillin_progressat the moment this body was written — recorded as such rather thanpredicted.
Lint,Type Check,Build & E2E,Build Docs,Bundle Analysisand
Governed Surface Queue Guardare allsuccess.The sixteen gates the previous revision's table omitted —
Action Ref Convention·
Bundle Analysis·Changeset Overwrite Report·Doc Component Type Check·Doc Example Id Check·Doc Fence Language Check·Doc Snippet Type Check·Docs Route Eager Closure Check·Inert vi.mock Specifier Check·Internal Docs Link Check·Pre-Install Import Graph Check·README Export Check·Shell Escape Residue Scan·Skill Eval Token Check·Skill Example Check·Skill Guide Path Check— all ran on this diff and all aresuccess. They arenamed here rather than left implicit.
6e. NOT MEASURED, and why
⛔ Recorded rather than counted as green or red:
TS2307unresolved-module diagnostics, which degrade every imported type to
anyandare byte-indistinguishable from the erasure under study. Discarded, the
dependency closure built, and the probe given a guard that now refuses to
report at all when any
TS2307is present.check-nul-bytes.mjsdoes not exist in this repo (it is spelledcheck-control-bytes), and three gates were first invoked underpnpm runnames that do not exist. Both classes exited non-zero with
ERR_PNPM_NO_SCRIPT/ module-not-found before reaching any gate body, whichis a not-measured reading and ⛔ never a red. All were re-run under their real
spellings; those exits are the ones tabulated in 6c.
in §1 is not reproducible from the PR alone. Stated there, too.
resolve's purity for out-of-repo hosts. Proved for the single in-repoimplementation and the test doubles only;
resolveis a host-implementedinterface.
Acceptance notes
Three observations made while measuring, ⛔ none of them fixed here. Their
carrier state is a dated reading, not an assumption — taken
2026-09-14T09:31Zby enumerating this repo's 22 open
findingissues: the first two have nocarrier (no open issue covers either, and no PR or person is queued on the
file), so the acceptance-notes route applies to them; the third has one —
objectui#9475, created
2026-09-14T08:40:46Z— so it is tracked, ⛔ is notorphaned, and must not be re-filed.
RecordDetailsComponentPropsomitslayout, whichRecordDetailsPropsstillcarries. That is objectui#9040's Direction 2 acting deliberately (the contract
refuses the value set this face used to offer), ⛔ not drift — recorded so the
next reader does not re-open it. Noted, not filed.
record-related-list.tsxhands its bound schema to the body asschema={bound as any}throughElementDataSourceGate. That cast is at theARGUMENT, so it does not erase the binding's type inside the body and is not
this card's defect; it is a second
anyin the same file and a reader maymistake the two. Noted, not filed.
record-related-list.tsxreads(schema as any).relationshipValueField— thesame cast-defeats-declaration shape as the rail's, for the very key this PR
declares.
external author's document COMPILES against the published type, and that is
measured and unaffected by the renderer's own read. Fixing it is a behaviour-
free one-token change, but it is outside what the review's six items asked for
and this PR does not widen to take it. Surfaced by the expression instrument in
§1. Carrier: objectui#9475, which the seat filed for exactly this read —
it records that the cast defeats the mirror declaration this PR adds, names
the expression instrument that would settle the cost argument, and leaves
the grading to triage. ⛔ Not orphaned: do not re-file it.
Generated by Claude Code
Generated by Claude Code