Skip to content

[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

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 / ObjectUiLocalParamFieldTypenamed 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions