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 #4115

Description

@os-zhuang

Systematizes what #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 #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 #4074
ActionParamFieldType "aligned with the field types available in spec" 16 of 49 members #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. #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 #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 (#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 #4074, #3856, objectui#3003, objectui#3009, objectui#2944, #2901, #2231.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions