Skip to content

spec: record:reference_rail has no ComponentPropsMap row — an entry filter parses, typechecks, validates, ships, and silently does nothing #8691

Description

@hotlong

Measured on @objectstack/spec 17.0.0 GA and @objectstack/console 17.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 reach pluralLabel; 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

ComponentPropsMap (@objectstack/spec/ui, 17.0.0 GA)
  total component keys = 37
  record:* keys = ["record:details","record:related_list","record:highlights",
                   "record:activity","record:chatter","record:path"]
  has record:reference_rail = false

grep reference_rail dist/ui/index.d.ts → 0 hits; there is no ReferenceRailEntry type either. With no row in the map, PageComponent.properties stays 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 (plus icon, displayField for row rendering), issuing one query per entry:

find(objectName, { $filter: { [relationshipField]: parentId }, $top: limit ?? 3, $count: true })

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:

filter: [{ field: 'status', op: 'neq', value: 'completed' }]
stage result
tsc --noEmit exit 0
objectstack validate passed — reference_rail appears 0 times in the output
objectstack build exit 0 — reference_rail appears 0 times
shipped artifact filter present verbatim in dist/objectstack.json
rendered rail badge unchanged at 3; the completed row still listed

The contrast is the finding: the same build run emits loud component-props-unknown-key / component-props-invalid warnings for record:related_list, record:activity and page:accordion in the very same file — because those components have rows in ComponentPropsMap. 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 filter to 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_rail row to ComponentPropsMap describing the shape the renderer actually reads (objectName, relationshipField, limit, title, icon, displayField, and the component-level hideEmpty). 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 rail filter is later granted, the row is where it gets declared and enforced.

Explicitly not in this card

  • rail title resolves objects.*.label, never pluralLabel, on a card that carries a total-count badge (cosmetic);
  • rail title is rendered as a raw React child, so unlike record:alert it 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).

Activity

  1. added theissue type on Aug 14, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    ContributorAuthor

    Triage: lands in packages/spec (ComponentPropsMap) ⇒ domain:spec — the semantic seat, not spec-surface. Type Bug, queued.

    Rationale for the seat split: adding a strict row changes the accept/reject behaviour — an entry filter that parses today becomes a publish-time refusal. Metadata that was legal before is not legal after, so the surface/semantic test lands on domain: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's Container & model line.

    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 filter would be capability expansion and is not in scope — if the dev concludes the row cannot be written without deciding whether filter is 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 are repo:objectui and 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

  3. self-assigned this
    on Aug 14, 2026
  4. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    Contributor

    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/** (the ComponentPropsMap row + 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 touches packages/spec/src/ui/**; #8318's union-branch work (shared/ + api/) is MERGED and disjoint

    Scope 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

  5. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    Contributor
    {
      "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

  6. os-zhuang commented on Aug 14, 2026

    @os-zhuang
    Contributor

    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 #8691 correct (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-sha pin and found icon is 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 on title: z.string(), not I18nLabelSchema, 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/hideEmpty left 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:generated 13/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 the record:discussion/record:chatter asymmetry) — I have labelled it finding so 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

  7. removed their assignment
    on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions