Repository navigation
spec: record:reference_rail has no ComponentPropsMap row — an entry filter parses, typechecks, validates, ships, and silently does nothing #8691
Description
Activity
Triage: lands in
packages/spec(ComponentPropsMap) ⇒domain:spec— the semantic seat, notspec-surface. Type Bug, queued.Rationale for the seat split: adding a strict row changes the accept/reject behaviour — an entry
filterthat parses today becomes a publish-time refusal. Metadata that was legal before is not legal after, so the surface/semantic test lands ondomain:spec. The card's own framing agrees ("tightens an existing shape rather than expanding the authorization surface").⛔ Dispatch constraint, non-negotiable: this card changes contract accept/reject behaviour, so it is on the fable-mandatory tier — dispatch at
model: claude-fable-5. The only降档 exit is measured fable unavailability, recorded in the claim comment'sContainer & modelline.Not escalated, and the boundary is worth stating because it is close: declaring the row the renderer already reads is enforcement of an existing shape. Granting a rail
filterwould be capability expansion and is not in scope — if the dev concludes the row cannot be written without deciding whetherfilteris supported, that is the fork, and it goes back to the box rather than being resolved in the PR.Scope is the first gap only. The two console-side items the card names (rail title cannot reach
pluralLabel; title is an untranslatable literal) are out of scope here — both arerepo:objectuiand both are capability expansion pending a business-pull ruling. Do not fold them in; if the maintainer rules there is pull, they become objectui cards.Consumer-package test requirement (spec contract change): run the consuming packages' fixture suites, not just
packages/spec— a strict row can flip a downstream fixture into asserting the opposite. Name the real per-package readings in the report.Size/model suggestion: S–M,
claude-fable-5(mandatory, see above).
Generated by Claude Code
Claim: PM loop round 4
Session:session_01E5tUwGM3LQoqErTfkvRW7W
Branch:claude/issue-8691-reference-rail-props-row
Worktree:objectstack-issue-8691
Domain:domain:spec
File surface:packages/spec/src/ui/**(theComponentPropsMaprow + its entry type), its test file, changeset, and the ADR-0087 accompaniment if the gate demands one (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-fable-5(mandatory clause: the row turns silently-accepted keys into publish-time refusals — acceptance-surface change)
Serial constraints cleared: none — no in-flight claim or open PR touchespackages/spec/src/ui/**; #8318's union-branch work (shared/+api/) is MERGED and disjointScope is the card's own: declare the shape the renderer actually reads, so an undeclared key is refused loudly instead of shipping as a no-op. ⛔ The two console-side items the card explicitly excludes (rail title vs
pluralLabel; untranslatable literal title) are NOT in scope — they are capability expansion pending a maintainer pull ruling, and belong to the console seat.
Generated by Claude Code
{ "issue": 8691, "status": "done", "branch": "claude/issue-8691-reference-rail-props-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/8745", "premise_still_valid": true, "summary": "Added the strict record:reference_rail row to ComponentPropsMap (37 -> 38 keys): ReferenceRailEntrySchema + RecordReferenceRailProps, key set measured from the renderer's read points at the .objectui-sha pin (665661ab), which the local objectui checkout matched exactly. One PM mechanism assumption falsified: `icon` is declared in the renderer's TS interface and emitted by the synthesizer but read by NO render path, so it is a guidance refusal, not a declared key; `title` is z.string() (raw React child), not I18nLabelSchema. Planted-filter pinned in both directions (spec parse + lint dispatch), dist-level ablation reverse-verified in the predicted direction (row removed -> 37 keys, lint finding 1 -> 0, silence restored; restored build hash matched the fix build). Protocol-18 semantic entry + regenerated registry + minor/BREAKING/adr-0087 changeset per the #8666/#8695 precedent; ADR-0122 isomorphic pins Iso845/Iso846. Out-of-scope sibling gap (record:alert / record:quick_actions / record:history, same mechanism) searched and filed as #8744; the card's two console-side exclusions were not touched and not filed (downstream card carries them).", "tests": "All at final HEAD e347bbe0f (post-merge of origin/main ff4ba6a06). @objectstack/spec: 401 files / 10603 passed, typecheck green. @objectstack/lint (direct downstream consumer of the map): 72 files / 2026 passed, tsc green after building the lint dependency closure (first run showed the unbuilt-deps false red: 21 files failed on missing @objectstack/formula dist, all green after `pnpm --filter '@objectstack/lint^...' build`). check:generated 13/13. Derived battery via dispatch-gates.mjs over actual changed paths (additions beyond the prompt's named families: check:spec-parsed-alias, check:query-options-erasure, check:type-check-coverage, check:type-check-debt): all green incl. check:nul-bytes, changeset gates (healthy signature), adr-0087 registration, type-check-debt --re-measure (none above recorded ceilings). Ablation legs each rebuilt spec dist and proved the mutation reached the artifact (Object.keys of the built map: false/37 ablated, true/38 restored). check:react-declaration-parity not runnable here (needs objectui sdui.manifest; pin-bump-triggered by settled ruling). CI gate status at report time: in_progress.", "open_questions": [], "out_of_scope_findings": [ "filed as #8744: record:alert / record:quick_actions / record:history have no ComponentPropsMap row — same silent no-op mechanism, key sets to be measured from renderer read points; also notes the record:discussion/record:chatter palette-vs-row asymmetry" ] }
Generated by Claude Code
Generated by Claude Code
ACCEPT (spec seat, session
session_01E5tUwGM3LQoqErTfkvRW7W) — PR #8745.Verified against GitHub, not the report's self-description: 14 files, all inside the claimed surface;
Fixes #8691correct (the row fully discharges the card); the two console-side items the card excludes are untouched and unfiled, as instructed.The review's highlight is a third dispatch-premise falsification today, and the sharpest one: my prompt relayed the card's key list including
icon. The dev measured the renderer at the repo's actual.objectui-shapin and foundiconis declared in the TS interface and emitted by the synthesizer but read by no render path — so declaring it would have minted a fresh declared-but-unenforced key on the card whose whole purpose is to abolish that class. It is a guidance refusal instead. Same discipline ontitle:z.string(), notI18nLabelSchema, because the renderer paints it as a raw React child — and refusing the locale-map spelling makes no pull judgement, which keeps the excluded console question genuinely open.limit/hideEmptyleft default-free so the renderer's fallbacks stay the renderer's facts.Reverse verification is the standard I want cited in future dispatches: the ablation removed only the map row, rebuilt spec and proved the mutation reached
dist(Object.keys→ 37, the card's own measured pre-fix count), then showed the lint dispatch pin drop 1 → 0 findings — the pre-fix silence restored, direction as predicted — and the restored build's input hash matched the fix build. That is an ablation that actually exercised the shipped artifact rather than the source.Suites green (spec 401/10603; lint 2026 — with the unbuilt-deps false red correctly diagnosed as a worktree state rather than a defect);
check:generated13/13; post-cut changeset class + protocol-18 entry per today's precedent; ADR-0122 isomorphic pins.Out-of-scope finding #8744 verified filed (same mechanism on
record:alert/record:quick_actions/record:history, plus therecord:discussion/record:chatterasymmetry) — I have labelled itfindingso it enters the pool rather than reading as an untriaged bare card. It is the natural next slice of this surface once triage grades it.Landing: ready + auto-merge armed; heavy jobs still converging, the queue gates on their conclusions.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Sep 29, 2026
Measured on
@objectstack/spec17.0.0 GA and@objectstack/console17.0.0 GA, statically and on a rendered page in a browser. Downstream at objectstack-ai/hotcrm#986 (GA close-out probe objectstack-ai/hotcrm#1154).Scope note up front: the downstream card records three gaps in
record:reference_rail. This mirror carries only the first — the missing spec declaration. The other two (rail title cannot reachpluralLabel; title is an untranslatable literal) are console renderer behaviour and genuine capability expansion, so they belong to the console seat and to a maintainer ruling on business pull; they are named at the bottom, not folded in here.The gap
grep reference_rail dist/ui/index.d.ts→ 0 hits; there is noReferenceRailEntrytype either. With no row in the map,PageComponent.propertiesstays an open bag (z.record(z.string(), z.unknown())) and nothing on any path parses a rail entry.The rail actually consumes only
objectName/relationshipField/limit/title(plusicon,displayFieldfor row rendering), issuing one query per entry:Why it matters: the authored key is accepted everywhere and honoured nowhere
Reverse verification on a real app, with the expected direction fixed before running. Planted on a rail entry whose object has 3 related rows, 2 of them not
completed:tsc --noEmitobjectstack validatereference_railappears 0 times in the outputobjectstack buildreference_railappears 0 timesdist/objectstack.jsoncompletedrow still listedThe contrast is the finding: the same build run emits loud
component-props-unknown-key/component-props-invalidwarnings forrecord:related_list,record:activityandpage:accordionin the very same file — because those components have rows inComponentPropsMap. The rail is silent purely because it is undeclared.Control confirming the mechanism rather than assuming it:
ComponentPropsMap['record:related_list'].safeParse({ … bogus key … })rejects.So an author — most importantly an AI author — can add
filterto a rail entry, watch it pass typecheck,validate, build and publish, and ship source that claims to filter while the badge keeps counting everything. This is the failure shape #4001 was closed to eliminate, still open on this one component.Suggested fix
Add a strict
record:reference_railrow toComponentPropsMapdescribing the shape the renderer actually reads (objectName,relationshipField,limit,title,icon,displayField, and the component-levelhideEmpty). This tightens an existing shape rather than expanding the authorization surface: it needs no judgement about whether a rail filter has business pull, and it converts today's silent no-op into a loud publish-time rejection. If a railfilteris later granted, the row is where it gets declared and enforced.Explicitly not in this card
objects.*.label, neverpluralLabel, on a card that carries a total-count badge (cosmetic);titleis rendered as a raw React child, so unlikerecord:alertit cannot take an inline translation map — the only options are "omit" or "a literal that overrides every locale".Both are console-side and both are capability expansion; the downstream card notes the only consumer found by a repo-wide grep is a single rail on one detail page, so the pull question is genuinely open. Recommend splitting them to the console seat only if the maintainer rules there is pull.
Downstream: objectstack-ai/hotcrm#986 (see also hotcrm#972, hotcrm#733).