refactor(types): burn down seven spec-symbol ledger entries, with the triage that decides the rest - #3042
Merged
Conversation
… triage that decides the rest
objectstack#4115's ledger opened at 156 untriaged collisions. This is the first
tranche: 149 remain. Every symbol removed was proved equivalent to the spec's
before being bound, not assumed equivalent because its doc comment said so —
four of the seven carried exactly such a comment.
Re-exported: BreakpointName, ExportJobStatus, ImportJobStatus, ImportWriteMode,
ValidationError. Derived with z.infer: JoinStrategy, WindowFunction, which the
spec exports as zod enums rather than as types. All seven are structurally
unchanged; full repo type-check passes 76/76.
Mutual assignability is the obvious test for "safe re-export" and it lies three
ways, all present in this repo:
- The SPEC's export resolves to `any` (NavigationItem, JoinNode, FormField).
Binding these replaces a precise local interface with `any`. A naive probe
calls them identical and recommends exactly the wrong edit.
- The LOCAL declaration resolves to `any` — recursive schemas annotated
z.ZodType<any> (FilterConditionSchema, NavigationItemSchema).
- The local declaration carries `[key: string]: any` — the objectstack#4075
mechanism (FormField, AppSchema, PageSchema, ThemeSchema, +12).
And a zod schema needs one question more than a type: FormFieldSchema has an
identical _output and a divergent _input, so re-exporting it would have silently
changed what authoring input parses. All of this is now in the ledger's
burn-down instructions with the detection probe for each case.
spec-derived-unions.test.ts gains an inverted pin asserting the three spec-side
`any` cases are STILL `any`. The day the spec types one properly the assertion
stops compiling, and the failure is the instruction to burn that symbol down.
Guard fix: referencesSpec walked the declaration's own name node, so a symbol
whose name was also bound to a spec import counted as derived from itself.
TypeScript rejects that pair as a duplicate identifier so it was unreachable in
compiling code, but a guard that depends on the compiler having run first has a
hole in it. Clean-tree result unchanged, confirming it masked nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvv6qysks2dRLaGfpTdgEy
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
The three symbols the ledger cannot burn down — NavigationItem, JoinNode, FormField — are blocked on the spec exporting them as `any`. Filed as objectstack#4171 (4 of the spec's 2240 exported types), so the inverted pin and the burn-down instructions name where the fix lives instead of just saying "upstream". Comments only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rvv6qysks2dRLaGfpTdgEy
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
objectstack#4115's ledger opened at 156 untriaged collisions. This is the first tranche: 149 remain.
The interesting part is not the seven. It is that the obvious way to pick them is wrong, in three distinct ways, all of which occur in this repo — so the durable output here is the triage, written into the ledger's burn-down instructions.
Why "are these two types the same?" is the wrong question
The natural test for "is this collision a safe re-export" is mutual assignability:
Run over
@object-ui/types' 80 collisions, that reports 13 identical. Six of the thirteen are wrong, and acting on any of them would have made the codebase worse:1. The spec's export resolves to
any—NavigationItem,JoinNode,FormField. The spec annotates the recursive schemas behind them asz.ZodType<any>.anyanswers everyextendsquestion affirmatively, so the probe calls them identical and recommends binding to the spec — which would replace a precise, 118-line documented interface withany. A type-safety regression wearing a burn-down's clothes.I had already converted
NavigationItemandJoinNodebefore catching this, and reverted them. They stay in the ledger; the fix belongs upstream in the spec.2. The local declaration resolves to
any—FilterConditionSchema,NavigationItemSchema, both annotatedz.ZodType<any>because they are recursive. Same mechanism, other side.3. The local declaration carries
[key: string]: any—FormField,AppSchema,PageSchema,ThemeSchema, and 12 more. This is the objectstack#4075 mechanism showing up in a new place: an index signature absorbs any extra member, so two types compare equal while accepting wildly different objects. LocalFormFieldhas one; the spec's does not.And a zod schema needs one question more than a type does.
FormFieldSchemahas an identical_outputand a divergent_input— the two schemas agree on what they produce and disagree on what they accept. Re-exporting it would have silently changed what authoring input parses, with no type error anywhere.Detection probes for all four cases are now in the ledger comment, so the next tranche starts from the answer rather than rediscovering it.
The seven
Each was proved equivalent, not assumed equivalent because its doc comment said so — and four carried exactly such a comment:
"Mirrors the server's ImportWriteMode (@objectstack/spec)","(ObjectStack Spec v2.0.1)". That is the claim objectstack#4115 exists to make true.BreakpointNamespec/uiExportJobStatus,ImportJobStatus,ImportWriteModespec/apiValidationErrorspec/kernelJoinStrategy,WindowFunctionspec/dataz.infer— the spec exports these as zod enums, not types, so a re-export does not compileAll structurally unchanged, so no consumer changes.
The classification the rest of the burn-down starts from
For
@object-ui/types' 80 collisions, after excluding the artifacts:IDENTICALLOCAL⊂SPECLOCAL⊃SPECDIVERGEDLOCAL-INDEXSPEC-ANY/LOCAL-ANYSPEC-ANYis not fixable here at allThe
LOCAL⊂SPECfour are worth naming, because that bucket is where drift has already cost something.FeedItemTypecarries 7 of the spec's 13 members, andplugin-detail'sRecordActivityTimelinebuilds two exhaustiveRecord<FeedItemType, …>maps from it — so the activity timeline is structurally unable to renderfile,sharing,note,record_create,record_deleteorapproval.PermissionActionis missingexecute,manage,configure.Three of the
DIVERGEDare not forks at all but genuine same-name-different-concept collisions, and belong inALLOWrather than being "fixed":CacheStrategy(objectui: HTTP/SW fetch strategy; spec: cache eviction policy),FilterOperator(objectui: verbose snake_case UI operators; spec: mongo-style query operators),GestureType(objectui: kebab-case with directional swipes; spec: snake_case with a singleswipeplusdrag).Inverted pin
spec-derived-unions.test.tsasserts the three spec-sideanycases are stillany. The day the spec types any of them properly,true satisfies IsAny<…>stops compiling and the failure is the instruction to re-run the triage and burn that symbol down. Same shape as theActionDefindex-signature pin from #3032.Guard fix
referencesSpecwalked the declaration's own name node, so a symbol whose name was also bound to a spec import counted as derived from itself. TypeScript rejects that particular pair as a duplicate identifier, so it was unreachable in compiling code — but a guard that depends on the compiler having run first is a guard with a hole in it. Found by writing the discrimination proof and watching it pass. The clean-tree result is unchanged (149 before and after), confirming it was masking nothing.Discrimination proof — three ways this fails
1. Re-fork
BreakpointNamealongside the spec import (the hole just closed — passed before the fix):2. The realistic regression — revert the binding entirely:
3. The inverted pin, simulating the spec gaining a real type:
Verification
pnpm type-check— 76/76 tasks successful (5m43s)packages/typestests — 106 passed (14 files)node scripts/check-spec-symbol-derivation.mjs— green, 156 → 149node scripts/check-type-check-coverage.mjs— green, both halvesnode scripts/check-changeset-fixed.mjs— greeneslinton all changed files — 0 errors (72 pre-existingno-explicit-anywarnings indata.ts, none on touched lines)Changeset:
@object-ui/typespatch — the seven are structurally unchanged, so no consumer sees a different type.Scope note: objectstack#4115 is filed on
objectstack-ai/objectstack; every change here is inobjectui.🤖 Generated with Claude Code