Skip to content

test(tooling): fail when an exported schema name has a second authority - #6348

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-6273-one-authority-per-exported-name
Aug 25, 2026
Merged

test(tooling): fail when an exported schema name has a second authority#6348
os-warren merged 1 commit into
mainfrom
claude/issue-6273-one-authority-per-exported-name

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#6273

The recurrence guard the 2026-08-25 family ruling ordered (decision 甲/A1 on #6172, verbatim acceptance 「同意」): every exported schema name has exactly one authority. The cleanup cards fix instances; this re-derives the collision set from source on every test run so the #6172 / #6155 / #6169 / #6170 / #6086 class cannot silently regrow after they land.

One file: scripts/__tests__/one-authority-per-exported-name-6273.test.ts.

Where it lives, and why

A root-suite test, not a check: script. Both were open per the card. The test inherits the vitest run that already exists and needs no workflow wiring — a check: script would owe a new job and its own path in that workflow's paths: filter (#6321), which is new surface for no extra signal. #6337 (card #6186) just demonstrated the whole shape end to end in the same directory, so this is the freshest house precedent rather than a new convention.

scripts/check-spec-symbol-derivation.mjs (#5899, another identity) is untouched. Its subject is a different claim — "a local declaration sharing a name with an @objectstack/spec export must be derived from the spec". This one is about two authorities inside objectui. Different population, different verdict; no reason to serialise behind it.

What counts as an authority

ShapeCounted?
export interface X / export type X / export enum X (and declare / const enum)✅ a declaration
export type { A as X } from './a' — an alias publishing a name✅ a second meaning
export { X } from './x', export type { X } from './x', export * from './x', export * as ns from './x'⛔ one declaration, many export sites
export { X as X } from './x'⛔ a plain re-export spelled long
export const / export function / export class⛔ out of the stated bound
Two export interface X in one file⛔ TypeScript declaration merging — one file's business

Two stated bounds, because a claim has to be bounded or derivable (2026-08-25 close-out, decision B1):

  1. Type-level names only. The ruling's subject is the schema/type vocabulary. Including the value namespace would bring 66 exported classes and every exported helper in as noise the same run. Narrowing the claim and narrowing the population is one change.
  2. Alias sites count only where the alias is type-marked. This is lossless rather than a heuristic: tsconfig.base.json sets "isolatedModules": true repo-wide, so a type re-export must carry the marker to compile. It also stops a value alias (export { helper as Grid }) reddening against an unrelated interface Grid.

The two defects #6337 warned about, and how each is answered

(1) A leg that silently never fires. The positive control exercises the alias leg on its own cases, not just the declaration leg — and the alias leg has a live instance on the real tree, so it is demonstrably not vacuous: packages/plugin-calendar/src/index.tsx publishes CalendarViewEvent as CalendarEvent, a name @object-ui/types already declares.

(2) The gate reddening on its own source. Solved with a real discriminator, ⛔ not a filename exemption. Unlike #6337's gate this one needs one mask, not two, and the reason is a property of the subject rather than a copied pattern: a declaration and an export clause are code in every spelling they have, so nothing here needs string literals to survive the mask. That single mask is also what lets the scan read packages/create-plugin/src/templates.ts:417 — which writes export interface ${…}Schema extends BaseSchema { inside a template literal — without reddening on it. It is proved directly rather than argued: the matcher is run over this test file's own bytes and must find nothing, independently of the population bound.

Negative controls — every one a spelling this repository really writes

Cited where it lives, because an assertion that reds on legitimate code gets deleted by the first person who hits it:

  • export type { StatusFieldSource } from './record-semantics.js';packages/types/src/index.ts:71
  • export * from './registry/Registry.js';packages/core/src/index.ts:10
  • export { formatTitleTemplate as formatRecordTitle }packages/app-shell/src/utils/index.ts:9 (a value alias)
  • export { default as en }packages/i18n/src/locales/index.ts:5
  • KanbanConditionalFormattingRule / KanbanNativeConditionalFormattingRule next to the contested ConditionalFormattingRulepackages/types/src/objectql.ts:2202 / :2181 (derived names are not collisions)
  • expect(typesFile).toContain('export interface HeatmapSchema extends BaseSchema {');packages/create-plugin/src/__tests__/templates.test.ts:449
  • expect(source).toContain('export type { ChatMessage }');packages/plugin-chatbot/src/__tests__/chat-message-contract.test.ts:448
  • expect(typesSrc).toContain('export interface CalendarEvent {');packages/plugin-calendar/src/__tests__/name-collision-5044.test.ts:173
  • the MenuItem as AppMenuItem cure pattern — packages/types/src/index.ts:59 — stays green, or the remedy itself reds

The population refuses to collapse

Three floors, because three things can collapse independently — the package derivation, the file walk, and the matcher itself. Measured on 79ebf30d1: 38 published packages (derived from private !== true, not listed), 1,364 files, 2,158 authority sites. Floors are 30 / 1000 / 1500.

Baseline — re-measured today, not taken from the card

⚠️ The card cites #6172 (FormField, MarkdownSchema, KanbanSchema) and #6155 (KanbanCard ×4). This baseline is today's measurement on 79ebf30d1, and it differs:

  • FormField is already clean — one declaration, packages/types/src/form.ts:911. It is not in the table.
  • MarkdownSchema, KanbanSchema and KanbanCard ×4 are still live, and KanbanCard's four files match finding(types): KanbanCard / KanbanColumn are declared four times in this tree and the published copies disagree (cards vs items, badges vs labels) #6155's count exactly — which is also the evidence that this population is co-extensive with the one the family cards counted (two of the four are KanbanEnhanced.tsx / KanbanImpl.tsx, neither a barrel).
  • 46 colliding names in total, so most of the table was named by nobody. KanbanColumn collides across the same four files as KanbanCard and no family card mentions it; ActionSchema collides inside packages/types itself (crud.ts vs ui-action.ts); DataSource, ComponentMeta, ComponentConfig, SchemaNode, MenuItem, ValidationFunction and ThemeProviderProps are all cross-package. Reported up as new information.

KNOWN_COLLISIONS is ⛔ SHRINK-ONLY and reconciles in both directions: a new name or a new site is fresh debt, and a listed site that no longer collides is a stale entry that must come down in the same PR that cleaned it. Without that second direction the table degrades into a skip-list nobody dares touch — the shape check-skills-paths.mjs and check-doc-fence-languages.mjs already refuse. The failure message prints a copy-paste-ready baseline line so nobody has to hand-transcribe one, and the remedy text points at the MenuItem as AppMenuItem worked example.

Evidence — red before, green after, on the real tree

Fixture controls prove the matcher discriminates; this proves the whole pipeline (population walk → matcher → reconcile) fires on a real second declaration in real source. Injected a second export interface MarkdownRendererProps into packages/plugin-markdown/src/types.ts, with an EXIT-trapped restore and absolute paths:

HEAD blob : e71cadc1540187bd20551a257e5a2b409e286058
injected marker : 1 (want 1)
mutated blob : 3a90fa20731a7c3d70c5b237feab0ebca7f3bff4 <- the mutation is ON DISK
=== MUTATED LEG === MUTATED_EXIT=1
MarkdownRendererProps — a NEW colliding name
baseline line, if this is being accepted as debt: ['MarkdownRendererProps', ['packages/plugin-markdown/src/index.tsx', 'packages/plugin-markdown/src/types.ts']],
Test Files 1 failed (1) | Tests 1 failed | 10 passed (11)
restored blob : e71cadc1540187bd20551a257e5a2b409e286058 <- byte-identical to HEAD
marker still : 0 (want 0) git diff HEAD : 0 lines (want 0)
=== RESTORED LEG === RESTORED_EXIT=0
Test Files 1 passed (1) | Tests 11 passed (11)

No build/dist leg exists to invalidate: the gate reads tracked source text, so neither leg needs a rebuild — stated rather than assumed.

Gates run — union taken at 78a9140c9, the final commit

Exit codes captured by redirect before any pipe. Each gate's own verdict line:

GateExitIts own verdict
pnpm exec vitest run --project unit scripts/__tests__0Test Files 78 passed (78) / Tests 2273 passed (2273)
pnpm type-check:scripts (tsc -p tsconfig.scripts.json)0clean; --listFiles shows 1 hit for this file, so it is really compiled, not excluded
pnpm lint:root0✖ 28 problems (0 errors, 28 warnings) — all pre-existing no-explicit-any warnings elsewhere; eslint --format json on this file alone: errors: 0, warnings: 0
node scripts/check-control-bytes.mjs0✅ check-control-bytes: OK (scanned 5225 tracked text file(s); skipped 85 binary).
node scripts/check-lint-coverage.mjs0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
node scripts/check-type-check-coverage.mjs0✅ type-check coverage: 45/46 via type-check … ✅ test type-check coverage: 41/41
node scripts/check-changeset-presence.mjs0✅ No source of a released package changed in this range, so no changeset is owed.

No changeset, on that gate's own verdict — one test file, no published source. ⛔ No skip-changeset label (#4912); that mechanism does not exist in this repo.

Heavy commands were serialised through the shared verify lock. Repo-wide pnpm lint (turbo, all packages) is left to CI, which runs the farm exactly once regardless.


Generated by Claude Code

Adds the recurrence guard the 2026-08-25 family ruling (objectui#6172,
decision A1) ordered: a root-suite gate that re-derives, from source, every
exported type-level name in the published packages that carries more than one
authority -- a second declaration, or a type-marked aliasing re-export
publishing a name something else already declares.
Deliberate re-exports are one declaration with many export sites and are not
counted; near-spellings and derived names are anchored out. Today's 46
collisions land as a named, shrink-only baseline that fails in both
directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CZgmFFzqA9cX8tBMhvpFe
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gate: forbid a second declaration of an exported schema name — the recurrence guard the 2026-08-25 family ruling ordered

2 participants

@os-warren@claude