Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[P2] objectui: a spec-named symbol must be an import, not a declaration — make "derived vs hand-written" a CI rule, not a review convention #4043

Closed

Description

@os-zhuang

Migrated from objectstack-ai/objectstack#4115 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-07-30T10:22:01Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.


Systematizes what objectstack-ai/objectstack#4074 / objectui#3003 / objectui#3009 did by hand for the third time. Filed so the rule outlives the sessions that keep rediscovering it.

The failure class

objectui declares local types/consts under the same names as @objectstack/spec exports, with doc comments claiming spec canonicity. Every audited instance had either drifted or was one spec release away from it:

symbol comment claimed code did found
ActionType "canonical definition from @objectstack/spec" hand union, missing form objectstack-ai/objectstack#2231/#2901
ChartType (sibling schema re-exported under spec's name) 7 of 19 members objectui#2944
ActionLocation + 2 "single source of truth… re-export here" re-declared union + tuple + z.enum objectstack-ai/objectstack#4074
ActionParamFieldType "aligned with the field types available in spec" 16 of 49 members objectstack-ai/objectstack#4074

The mechanism that makes this class expensive is specific to agent-driven development: an agent reads the comment, takes it as ground truth, and builds on it. objectstack-ai/objectstack#2901 was literally filed with a backwards premise because the fork was re-exported under the spec's own symbol name. A wrong canonical-claim is not stale documentation — it is a planted premise for the next session.

And the absence of derivation is what made objectstack-ai/objectstack#3856 silent: spec 17 removed action.execute and objectui produced zero compile errors, because nothing was bound to the spec's z.infer.

The rule

Any exported type or const in objectui whose name matches a @objectstack/spec export must be an import/re-export of it — or carry an allowlist entry with a reason.

Enforced by a CI script (~100 lines), not prose:

  1. Enumerate spec's export names per subpath (@objectstack/spec/ui, /data, …) at runtime.
  2. Grep objectui packages/*/src for export (type|const|interface) <SameName> declarations not backed by an import from spec.
  3. Fail with the symbol, the file, and the fix ("import it, or add an allowlist entry with a reason").

Value exports additionally pin by reference identity (expect(X).toBe(SpecX)), because a faithful copy passes every value comparison — identity is the only check that distinguishes a re-export from a fork. This insight is already written down in objectui's spec-subschema-parity.test.ts and already applied to ACTION_LOCATIONS / ActionLocationSchema / ACTION_PARAM_FIELD_TYPES (objectui#3003); this issue is about applying it uniformly instead of one symbol per incident.

The allowlist (what keeps this from being a nuisance)

Same governance as check-type-check-coverage.mjs's DEBT map — declared, reasoned, shrink-only. Known-legitimate first entries:

  • @object-ui/types ActionSchema — a deliberate objectui-side view over the spec's action (carries renderer-only fields like openIn); documented as such.
  • ObjectUiLocalActionType / ObjectUiLocalParamFieldType — named local dialects, each with a tripwire test asserting the spec does not own the name (so the day spec adopts it, CI names the alias to retire). This is the sanctioned escape hatch for "implementation accepts more than spec": declare the dialect next to the vocabulary it extends, never hide it in a Record<string, string> in another package.
  • The legacy ActionSchema in crud.ts and similar explicitly-legacy shapes, if any collide.

A same-name symbol that is narrower or wider on purpose belongs in the allowlist with that purpose written down — at which point the comment is load-bearing instead of decorative.

One meta-rule

This rule must land as the check, not as an AGENTS.md paragraph. A prose-only rule is precisely the "declared ≠ enforced" landmine this whole thread is about (objectstack-ai/objectstack#4074, objectui#3009 — where a guard file's own header falsely claimed its checks were "the real enforcement" for the entire interval nothing compiled it).

Refs objectstack-ai/objectstack#4074, objectstack-ai/objectstack#3856, objectui#3003, objectui#3009, objectui#2944, objectstack-ai/objectstack#2901, objectstack-ai/objectstack#2231.

Activity

  1. added
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Aug 23, 2026
  2. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: domain:spec (lane only — tracking and priority:p2 untouched). The rule this card tracks — a spec-named symbol must be an import, not a declaration — is about which repo owns a contract symbol. That is the contract stream, and it is the umbrella over several cards already routed there this round (#5494, #2231, #5213).

    ⚠️ As a tracking card its children are the dispatchable units; the lane label is for inventory visibility.

    Routed via the maintainer direct-dispatch channel, this session, verbatim: 「然后 批 4–5」. PM session session_0124Qg8rLvpXnQDwCmpKUmaJ. objectui three-stream split (maintainer 2026-08-21); not a Routine triage fire — the triage seat may re-grade.


    Generated by Claude Code

  3. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    Closing as completed — the rule this card asked for exists as the check, not as prose (spec@objectui seat, session session_01V3hPr7riucnMfhcHY86Msd, 2026-09-02; verified on origin/main 2e32ed4).

    • scripts/check-spec-symbol-derivation.mjs implements the card's rule ("a spec-named symbol must be an import, not a declaration"): its header quotes this card's failure-class table verbatim (ActionType / ChartType / ActionLocation / ActionParamFieldType) and its "what counts as derived" section accepts exactly the two sanctioned forms — export type { X } from '@objectstack/spec/…' and export type X = z.infer<typeof SpecX> — with a declared-dialect allowlist (the "named local dialect" escape hatch the card specified).
    • Wired as check:spec-symbols (package.json:40) and run in CI (.github/workflows/ci.yml:326, inside the Type Check job). Current reading on a lane PR today: "spec symbol derivation: 1333 files scanned against 4959 spec export names; 16 declared dialects, 0 untriaged collisions in 0 packages" (PR feat(types)!: ObjectFormSection drops the inert className/gridClassName members (objectstack#13626 remainder) #7278 gates).
    • Residuals are their own cards, not this one: the gate's export-filter narrowing is Split the derivation gate's export-filter change: rule 2 one-liner now, rule 1 scoped (narrowings → DEBT block → 4 ALLOW entries) #6291 (devx lane, in flight); the "unbacked alignment claims" ledger is reported by the same script and ratchets separately.

    Provenance for closing a tracking card: its deliverable (the CI check) landed; a tracking card with nothing left to track is closed, not held. No pm:* state label was present. Reopen if a spec-named hand-written symbol lands without the gate catching it.


    Generated by Claude Code

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

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2tracking

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions