Repository navigation
[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
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/specexport must be animport/re-exportof it — or carry an allowlist entry with a reason.
Enforced by a CI script (~100 lines), not prose:
- Enumerate spec's export names per subpath (
@objectstack/spec/ui,/data, …) at runtime. - Grep objectui
packages/*/srcforexport (type|const|interface) <SameName>declarations not backed by an import from spec. - 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/typesActionSchema— a deliberate objectui-side view over the spec's action (carries renderer-only fields likeopenIn); 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 aRecord<string, string>in another package.- The
legacy ActionSchemaincrud.tsand 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
fix(plugins,app-shell): resolve the six spec-named symbol collisions …
os-zhuang commentedon Aug 23, 2026
Triage:
domain:spec(lane only —trackingandpriority:p2untouched). 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).trackingcard 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
yinlianghui commentedon Sep 2, 2026
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 onorigin/main2e32ed4).scripts/check-spec-symbol-derivation.mjsimplements 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/…'andexport type X = z.infer<typeof SpecX>— with a declared-dialect allowlist (the "named local dialect" escape hatch the card specified).check:spec-symbols(package.json:40) and run in CI (.github/workflows/ci.yml:326, inside theType Checkjob). 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).Provenance for closing a
trackingcard: its deliverable (the CI check) landed; a tracking card with nothing left to track is closed, not held. Nopm:*state label was present. Reopen if a spec-named hand-written symbol lands without the gate catching it.Generated by Claude Code