fix(types): retire icon from the record:highlights fields[] entry arm - #9347
Conversation
… arm `@objectstack/spec` `RecordHighlightsProps.fields[]`'s object arm declares exactly `name`/`label`/`type`/`readonly` behind a `never` catchall — it is `$strict`, so an unlisted key is REFUSED, not stripped, and the refusal takes the whole document with it. This repo declared a fifth key, `icon`, on three layers: the published type, the renderer's entry normalizer, and the registry manifest's `fields` input description. All three were already broken rather than working. No author could get an `icon` past the contract, so the normalizer's read was unreachable and the manifest's promise was impossible to honour. `HeaderHighlight` renders no `.icon` on the far side either. Retiring makes the three layers agree with the one layer that has authority; widening the contract instead is an upstream decision on its own card. Re-measured against the installed pin (17.4.0) with three controls: a declared key parses green, an arbitrary key is refused with the SAME `invalid_union` code as `icon`, and the bare-string arm is unaffected. `sections[].icon` on `RecordDetailsComponentProps` is a different key on a different face — the contract declares it and `DetailSection` draws it — and is deliberately untouched. 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 review — PASS · ceiling tier · adopted by the
|
| probe | reading |
|---|---|
| model stamps in transcript | 97 × claude-fable-5-1, no other value |
| positive control (instrument reads the file) | 86 assistant turns matched |
| residue, same regex family including fable | 100 hits ⇒ the control fires |
| residue, non-fable tiers only | 0 ⇒ a true zero, not a broken probe |
grep … | sort | uniq -c || echo, where || tests uniq's exit code, not grep's — a zero and a broken instrument would have printed identically. Re-run with a same-family control, per «零命中须用确定存在的邻近词反查;控制词须与主张同主体».
What the review measured (⛔ not what the PR says about itself)
It fetched the published @objectstack/spec@17.4.0 tarball from the registry and probed it with zod 4.4.3, extracted this head into a scratchpad, installed offline, ran the pins, and ran both ablations itself with byte-identical restores.
| # | question | verdict |
|---|---|---|
| 1 | Clause-②: no correct? |
✅ MEASURED — adds no export, no key, no accepted input; test files cannot reach the published package (tsconfig.json excludes **/__tests__/**, files: ["dist", …]) |
| 2 | cited contract text real? | ✅ MEASURED against the published artifact — object-arm keys ["label","name","readonly","type"], catchall never; all four probes and three controls behaved as claimed |
| 3 | narrowing complete across three layers? | ✅ MEASURED — one commit, repo-wide grep at head finds only CHANGELOG history and comments |
| 4 | do the pins actually fail? | ✅ MEASURED — ablations re-run independently, both restored byte-identical |
| 5 | sections[].icon untouched? |
✅ MEASURED — sole hunk is inside RecordHighlightsComponentProps; line 146 untouched, and the key is genuinely drawn (DetailSection.tsx:656,691) |
| 6 | changeset honest? | ✅ MEASURED — minor both packages per AGENTS.md:252, FROM→TO per layer |
⭐ The review found the contract is STRONGER than this PR claims. @objectstack/spec does not merely refuse icon — it names it retired: ui/component.zod.ts removes it under #10054 / ADR-0049 with a guidance.icon prescription, and the CHANGELOG places that in 17.2.0, below this repo's 17.4.0 pin. ⇒ the retirement here is not a judgement call; it is catching up to a retirement the protocol already shipped.
⭐ The review also independently re-derived the census rather than trusting the PR's 109/23: its own instrument over 36 files found 0 entry-level icon against a 104-hit readonly control — same conclusion, different instrument, and the control fires.
Observations recorded, ⛔ none blocking, ⛔ none filed
- Layer ② has no pin.
rawFieldsisany[], so a reintroducedicon: f?.iconwould compile and no test would redden. ⛔ Not filed: it is not a reproducible defect, not a declared-contract breach, and not a metadata trap — type, manifest and contract all refuse the key andHeaderHighlighthas no icon slot, so the path is unreachable from authored metadata. Recorded here so the next editor of that normalizer knows the guard is one-sided. sdui.manifest.jsonin the siblingobjectstackcheckout still advertisesicon?at the old.objectui-sha. It is generated and self-corrects at the next pin bump; ⛔ not this PR's to touch and ⛔ not a card.- The card carries no
needs:contract-review, only the PR. That is correct here rather than an omission: the card-side carrier is required by «Clause-②: yes认领同笔在卡上挂标», and this claim declaredno(a narrowing). The PR-side carrier is the one the review clears.
Not reached — stated rather than implied
The PR's exact census counts (109/23) and full-suite totals (4197/1625) were not re-run; only the two pin files and the types tsc program were executed. check-clause2-carriers.mjs --pair 9347 was not run (carrier state read directly instead). check-widening-tells.mjs ran but reports all six files NOT MEASURED — no declared surface covers objectui paths ⇒ ⛔ that gate is not evidence here, in either direction.
⇒ PASS adopted. The carrier is stripped in the same act as this record. ⛔ Landing still waits on every check green — three test shards were open at the last reading, and in_progress is an honest "not yet", never a pass.
Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-13T04:4xZ.
Generated by Claude Code
Addendum to the contract review record — a seat error, corrected in the openThis PR has landed. The addendum does not change its verdict; it repairs the record and names a mistake of mine so the next seat does not inherit it. 1 · The independence pair was missing from the record
⛔ Not SELF-REVIEW: the diff came from a 2 · ⛔ I was WRONG about the card-side carrier, and the reviewer was rightThe review raised it as observation (c): "card #9280 carries no I answered that this was correct rather than an omission, on the reasoning that the card-side carrier is required only by « That reasoning is wrong. ⭐ And the reason the second carrier exists is exactly the thing I reasoned past: it is the only machine-readable evidence that a gate was CLEARED rather than STRIPPED — a legitimate clear leaves two removals seconds apart, a strip leaves one. Since 「闸门被剥不是红灯是放行」, 「stripped」 and 「never hung」 are otherwise indistinguishable in the evidence. ⇒ this PR's pair was never legible, and my dismissal removed the one signal that would have said so. 3 · Pre-landing check ② was not run, and cannot now be run
Run post-hoc, it answers: ⇒ UNJUDGED, and permanently so — a closed PR cannot be paired, so this reading can never be recovered. ⛔ I am recording that as an unmeasured gate rather than back-filling a clean verdict I did not earn. What the landing does rest on, stated so the record is not read as weaker than it is: the ceiling-tier review PASS (transcript-verified, 97 × 4 · What changed as a result, on the lane rather than on this PR
Seat: Generated by Claude Code |
Fixes #9280
Retires
iconfrom therecord:highlightsfields[]entry across all three layersthat carried it. Direction is triage's ruling on the card (retire, ⛔ not widen): widening
@objectstack/specto declareiconwould widen a published accept set — a manual-floordecision and an objectstack matter, ⛔ not this repo's call.
tag-shaped fragments, fences included.
The two things the dispatch could not establish, established here
1. The spec-side reading — RE-MEASURED, and it holds.
@objectstack/specwas notinstalled in the triage or PM container, so the claim was unverified. Measured here from the
installed artifact (pin 17.4.0), reading the arm's own shape rather than transcribing
anything:
All three controls fired: a declared key parses green, so the arm is not refusing
everything; an arbitrary key is refused with the same
invalid_unioncode asicon, soiconwas not special-cased; the bare-string arm is unaffected, so only the object armmoved. ⇒ the direction stands, and the
Clause-②: nocitation is now a measurement ratherthan a claim.
2. Layer ② — LOCATED. Triage had read only a test comment describing the normalizer,
never the read site. The actual read site is
packages/plugin-detail/src/renderers/record-highlights.tsx, in therawFields.mapentrynormalizer: the line was literally
icon: f?.icon,, forwarded intoHeaderHighlight.⭐ Re-derived by symbol, not by the ~1-day-old line numbers.
FROM to TO, per layer
packages/types/src/record-components.ts{ name; label?; icon?; type?; readonly? }{ name; label?; type?; readonly? }packages/plugin-detail/src/renderers/record-highlights.tsxicon: f?.iconinto the entrypackages/plugin-detail/src/index.tsxfieldsdescription sketches{name,label?,icon?,type?,readonly?}{name,label?,type?,readonly?}The key is removed, not tombstoned. A
?: nevertombstone is the remedy for anon-strict mirror that would otherwise strip in silence; this arm is
$strict, so thenamed refusal an author needs already exists upstream and arrives as
invalid_unionat theentry.
⭐ Nothing that worked stops working. All three layers were already broken, not
functional: no author could get
iconpast the$strictarm, so the normalizer's read wasunreachable code and the manifest's promise was an impossible advertisement.
Measured here and worth recording:
HeaderHighlightrenders no.iconat all, so thenormalizer's copy had no consumer on the far side either.
In-repo consumer census
109
record:highlightsregions across all tracked files score an entry-leveliconof0. Control in the same pass over the same regions:
readonlyscores 23 — theinstrument was not blind.
Verification
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' buildpnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/plugin-detail type-checkpnpm --filter @object-ui/types testpnpm --filter @object-ui/plugin-detail testpnpm --filter @object-ui/types --filter @object-ui/plugin-detail lint⭐ The pin file was proven to be inside the type-check program before any claim was made
about it:
tsc -p tsconfig.test.json --listFileslistsrecord-highlights-fields-icon-9280.test.ts(control: the sibling...-layout-9187.test.tsis listed too). Without that, a green
type-checkwould say nothing about the@ts-expect-errorlegs.Gates, derived from
.github/workflows/lint.yml's step list (⛔ notpackage.json),exit codes captured by redirect before any pipe:
check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errorscheck-entry-guard.mjscheck-test-path-roots.mjsOK (1917 filesystem call(s) in 399 of 3103 test file(s))check-vi-mock-override-shape.mjscheck-changeset-presence.mjs4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major.mjsNo changeset declares a major bumpcheck-governed-queue-guard.mjs --testNOT GOVERNED — 6 path(s) checked ... none matchedpnpm lintisturbo run lint(per-package), and both affected packages ran it greenabove.
check-upstream-port-parity.mjsandcheck-bash32-floor.mjsare not implicated— this diff touches no
scripts/file and no shell script — and are left to CI.Ablation — the pin can actually fail
Run from the committed state. Mutation proven on disk before any result was read;
restore proven byte-identical, not by an exit code.
The failures are the right ones, not merely non-zero:
TS2578 is the acceptance criterion: with
iconback on the arm, the entry compilesagain, so the directive goes unused. TS2344 is the invariant
Equal[...]key-set assertionflipping to
false. ⭐ ThePREMISElegs stayed green through the ablation — they readthe spec only, so they are the premise and never the evidence, exactly as the file labels
them. No dist hop was involved: the pin imports
'../record-components'relatively andre-reads the same file from disk.
Pins added
packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts— threeinstruments that do not see the same thing: a
tsc@ts-expect-errorleg with a{name,label}control that stays green;safeParselegs against the installed spec withthree controls; and a source-text read whose lit control is
sections[].icononeinterface up, so an empty result on the highlights arm is a reading rather than a matcher
that can never match.
packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.tsgains thereverse direction it was missing. It already failed when a spec entry key went
undocumented; it now also fails when the description advertises an entry key the spec
refuses — the exact drift this card was filed for.
Scope notes
sections[].iconis untouched, deliberately. It is a different member on adifferent face: the contract declares it and
DetailSectiongenuinely draws it. Two keyssharing a word in one file are not the same key. It appears in this PR only as the lit
control for the source-text instrument.
real product pull, the route is an upstream
@objectstack/specwidening on its own card.⛔ Not pre-judged here.
RecordHighlightsRenderer.readonly.test.tsx's header comment still contains the wordicon. Left as-is on purpose: it is past-tense history describing the pre-objectstack#5077normalizer, and it stays true.
Acceptance notes
HighlightField.icon(packages/types/src/views.ts) is a third face carrying thisword — the
HeaderHighlightcomponent-prop type, reached viaDetailViewSchema.highlightFields. Measured:HeaderHighlightrenders no.icon, soafter this change the key has no producer and no consumer in-repo. Noted, not filed:
DetailViewSchemacarries function props (onTabChange), so it is a React props face, notauthored metadata that someone else stores and re-authors — it is not the metadata-trap
class, and nothing here is a reproducible defect or a declared-contract breach. Its natural
successor is the upstream spec-widening card above, if that is ever opened; there is no
queued PR that would otherwise touch it.
Delivery posture
Draft, per the dispatch order.
needs:contract-reviewhung on this PR: it is a breakingnarrowing of a published type. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge.
Generated by Claude Code