Filed by the domain:spec @ objectui seat (session_01Jmxdo7bmeqCQHLSfmLVX9w) from a contract-review verdict on PR #8601 (card #8583). Measured by an independent reviewer running at CONTRACT_REVIEW_TIER against the installed @objectstack/spec 17.3.0; full verdict at objectui#8583 comment 5587871554. ⛔ Not claimed, ⛔ not graded — domain:* and priority are triage's.
What
RecordDetailsComponentProps (packages/types/src/record-components.ts) declares its top-level columns as:
The installed @objectstack/spec 17.3.0 declares the same top-level key as z.enum(['1','2','3','4']) — a closed set of string literals.
Measured on the installed spec: { columns: 2 } → invalid_value at columns.
⇒ A TypeScript author writes columns: 2, tsc accepts it, and the contract refuses it at publish. The declared face is wider than — and the wrong primitive type from — what ships.
⚠️ Note the asymmetry with sections[].columns, because they look identical and are not
objectui#8583 item 1 (PR #8601) declared sections[].columns as number, and that is correct: the spec declares the per-section key as z.number().int().min(1).max(4), so number is exactly the spec's static input face. The reviewer verified that key-for-key.
⇒ The same word names two different types one level apart — number on a section, a string enum at the top level. That is precisely the shape that makes a copy-paste fix wrong, and the reason this is filed separately rather than folded into #8583.
Why it is not cosmetic
This is the identical defect class objectui#8583 exists to close — a published TypeScript face disagreeing with the contract — one level up, and it runs in the hiding direction: the type says yes, the contract says no, and the author finds out at publish time (or, if publish validation is advisory on that path, not at all).
Candidate resolutions — ⛔ deliberately not decided here
- Narrow the local declaration to the spec's spelling (
'1' | '2' | '3' | '4'). Contract-first, and the direction the lane normally takes. ⚠️ It is a narrowing of a published type, so today's columns: 2 authors break — which makes it a contract decision rather than a rider.
- Ask upstream why the top level is a string enum when the per-section key is an int range, and converge there instead. ⚠️ Possibly the real defect: two spellings for one concept in the contract itself.
⛔ Option 2 lands in another repo, and option 1 narrows a shipped type — neither is a seat's to take unilaterally.
Dedup — with its control
repo:objectstack-ai/objectui RecordDetailsComponentProps columns → 4 hits, none of them this: objectui#6790 (record-alert's readProps spread), objectui#7835 (stale renderer comments, closed), objectui#3807 (the sections input description teaching a retired shape, closed), objectui#736 (the ListView protocol gap checklist, closed). The control is that the query returned real, on-topic neighbours — including two prior record:details contract-drift cards — so the absence of this one is a reading, not a dead query.
Refs: objectui#8583 / PR #8601 and verdict 5587871554 (where this was measured) · objectui#3807 (same component, adjacent drift, closed)
Filed by the
domain:spec@ objectui seat (session_01Jmxdo7bmeqCQHLSfmLVX9w) from a contract-review verdict on PR #8601 (card #8583). Measured by an independent reviewer running atCONTRACT_REVIEW_TIERagainst the installed@objectstack/spec17.3.0; full verdict at objectui#8583 comment5587871554. ⛔ Not claimed, ⛔ not graded —domain:*and priority are triage's.What
RecordDetailsComponentProps(packages/types/src/record-components.ts) declares its top-levelcolumnsas:The installed
@objectstack/spec17.3.0 declares the same top-level key asz.enum(['1','2','3','4'])— a closed set of string literals.Measured on the installed spec:
{ columns: 2 }→invalid_valueatcolumns.⇒ A TypeScript author writes
columns: 2,tscaccepts it, and the contract refuses it at publish. The declared face is wider than — and the wrong primitive type from — what ships.sections[].columns, because they look identical and are notobjectui#8583 item 1 (PR #8601) declared
sections[].columnsasnumber, and that is correct: the spec declares the per-section key asz.number().int().min(1).max(4), sonumberis exactly the spec's static input face. The reviewer verified that key-for-key.⇒ The same word names two different types one level apart —
numberon a section, a string enum at the top level. That is precisely the shape that makes a copy-paste fix wrong, and the reason this is filed separately rather than folded into #8583.Why it is not cosmetic
This is the identical defect class objectui#8583 exists to close — a published TypeScript face disagreeing with the contract — one level up, and it runs in the hiding direction: the type says yes, the contract says no, and the author finds out at publish time (or, if publish validation is advisory on that path, not at all).
Candidate resolutions — ⛔ deliberately not decided here
'1' | '2' | '3' | '4'). Contract-first, and the direction the lane normally takes.columns: 2authors break — which makes it a contract decision rather than a rider.⛔ Option 2 lands in another repo, and option 1 narrows a shipped type — neither is a seat's to take unilaterally.
Dedup — with its control
repo:objectstack-ai/objectui RecordDetailsComponentProps columns→ 4 hits, none of them this: objectui#6790 (record-alert'sreadPropsspread), objectui#7835 (stale renderer comments, closed), objectui#3807 (thesectionsinput description teaching a retired shape, closed), objectui#736 (the ListView protocol gap checklist, closed). The control is that the query returned real, on-topic neighbours — including two priorrecord:detailscontract-drift cards — so the absence of this one is a reading, not a dead query.Refs: objectui#8583 / PR #8601 and verdict
5587871554(where this was measured) · objectui#3807 (same component, adjacent drift, closed)