fix(deps): triage the 14 rc.6 spec-symbol collisions and refresh the console parity exemptions (#4167) - #4189
fix(deps): triage the 14 rc.6 spec-symbol collisions and refresh the console parity exemptions (#4167)#4189yinlianghui wants to merge 3 commits into
Conversation
…pec bump (objectstack#7100) `global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered — the console's ⌘K palette reads no action metadata at all — and the Studio designer previewed a mock command-palette frame for it anyway. @objectstack/spec 17.0.0-rc.6 retired the member (objectstack#6888, maintainer ruling 2026-08-09 direction 2); this removes the designer surfaces that outlived it, plus the bump that makes them stop compiling. - ActionPreview.tsx: the mock ⌘K placement frame is gone. The metadata strip still echoes the declared token, deliberately — reporting what a draft says is honest; the frame CLAIMED the platform renders it. - ActionDefaultInspector.tsx: the `LOCATION_LABELS` entry is gone. The map is `Record<ActionLocation, string>`, so the retirement arrived as a compile error (objectui#3017's mechanism firing as designed). - block-config.ts + both locale tables: the palette option and its now-orphaned i18n key. - components' action:bar doc comment aligned (its enum is `[...ACTION_LOCATIONS]` and followed on its own). - ActionEngine.getActionsForLocation is UNCHANGED: narrowing it would be a second rejection point beside the schema's. The type and `ActionLocationSchema` are the enforcement, both pinned. The bump (all 37 `@objectstack/*` declarations, rc.5 -> rc.6) rides along because the two cannot compile apart, and its fallout is repaired here: the `…Input` alias retirement (types/theme, types/spec-report, core/ActionRunner), the five dead i18n re-exports, the `I18nLabel` widening at 8 render sites, `.extend` -> `.safeExtend` on a now-refined `GlobalFilterSchema`, and two parity lists re-pointed. NOT green: the bump's remaining surface is out of this card's scope and filed — objectui#4165 (contract conflict, blocks), #4167 (adaptation remainder), #4163 (I18nLabel consumer story), #4164 (name reclaim available). Part of objectstack-ai/objectstack#7100 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
… one conflict
Second pass over the @objectstack/spec 17.0.0-rc.6 bump. The first commit fixed
every COMPILE error; a full `vitest run` (14615 tests) then surfaced the runtime
half. All mechanical, each pinned; the one non-mechanical case is left as a
tripwire rather than decided.
- types/zod/complex.zod.ts: `GlobalFilterSchema` composes by spreading the spec's
`.shape`. rc.6 closed BOTH extension doors on a now-refined schema — `.extend()`
throws at module load, `.safeExtend()` types the three overridden keys as
`never` (TS2322, which only `apps/site`'s build caught). The spread preserves
rc.5 behaviour exactly and decides nothing: the refinement-vs-stored-`{preset}`
conflict is objectui#4165, pinned by a new tripwire that reds from either side.
The pin isolates the refinement by widening `defaultValue` first — the bare spec
schema rejects `{preset}` on the OLD field type and the refinement never runs,
so the obvious shorter assertion would have pinned the wrong thing.
- core/actionKeys.ts: `description` joins SPEC_ACTION_KEYS (rc.6 added it to
`ActionSchema`). Inventory only — `ActionDef` is untouched.
- metadata-admin flow tests x2: `FlowNodeSchema` became pipe-wrapped, so `.shape`
read `undefined`. Access-path repair (walk the wrapper); the blocks themselves
are unchanged upstream, and a non-vacuity guard now says so.
- clientValidation.optOuts: INVERTED — rc.6 makes `SharingRuleSchema` declare the
seven ADR-0010 envelope keys, so the create door now ACCEPTS a stamped
`_packageId`. Upstream convergence, pinned as accept, with a bogus-key case
proving the door is still a door.
- clientValidation.viewDiagnostics: fixture supplies a `label`; rc.6's widened
`I18nLabel` made every label a LEAF union, which #3606's root-union expansion
does not reach. The resulting bare `Invalid input` is recorded as its own pin
(objectui#4163), not papered over.
- components/record-picker parity: fixture drops `displayField` — an ADR-0087 D2
tombstone refused by name, which was failing all three `filter` assertions.
- console registry parity: `SPEC_SHAPE_EMPTY_ON_THE_PIN` deleted exactly as its
own self-clearing test instructed (rc.6 lands `PageContainerProps`), with a
tombstone assertion so the deletion cannot be silently undone.
- i18n residue ratchet: matches `const { t, … } = useObjectTranslation(` by
pattern. Both sidebars legitimately also take `language` now, to resolve the
spec's inline per-locale labels.
Still red, filed, NOT fixed here: 2 console-parity exemption-list entries and
`check:spec-symbols`' 13 name collisions (objectui#4167) — each is per-symbol
triage, not a mechanical edit — and objectui#4165, which blocks ready.
Part of objectstack-ai/objectstack#7100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…console parity exemptions (#4167) objectui#4167, objectstack#4115. `check:spec-symbols` reported 13 collisions on the @objectstack/spec 17.0.0-rc.6 bump and a 14th (`GlobalFilterSchema`) appeared during the bump itself; each is triaged on its own rather than blanket-renamed. Renamed (3) — the spec's same-named export means something else: MetadataState -> MetadataCacheState (react, app-shell) resolveI18nLabel -> resolveKeyedI18nLabel (react, app-shell, + apps/console) DateRangePreset -> FilterBuilderDateRangePreset (types) Bound or derived (10): DATE_RANGE_PRESETS (+ a `satisfies` completeness pin on the local bounds table), NavigationMode (+ a both-directions type pin), AddressValue, BreakpointColumnMap, BreakpointOrderMap, KanbanConfig, CalendarConfig, GanttConfig. Four copies were losing information: GanttConfig declared 6 of the spec's 17 keys while plugin-gantt reads all of them, AddressValue 5 of 7 parts, and DATE_RANGE_PRESETS was the third copy objectstack#4614 extracted the vocabulary to collapse. ALLOW (1): GlobalFilterSchema, the same `.shape`-spread dialect as SelectOptionSchema beside it — it collided only because rc.6's refinement forced `.extend()` to be respelled, moving a derivation the guard could see into an object literal it deliberately does not descend into. Nothing about #4165 is encoded. Console registry-inputs-spec-parity: all 12 off-spec exemptions deleted (rc.6 landed objectstack#6776 and objectstack#5775, verified per key against the resolved shape), and `element:record_picker` publishes `sort` / `limit` / `emptyText` — read by the renderer all along, and the red demanding them was predicted in writing by the exemption it replaces. 49 tests/2 failing -> 51/0. Also repairs 3 pre-existing type-check errors of this card's own rc.6 family, found only by running `type-check` (which the bump PR did not): the retired `ThemeInput` alias re-pointed BY SIDE, and `DroppedFieldsEvent.reason`'s new `primary_key` arm. The consumer gap that arm exposes is objectui#3935. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Train note (objectui seat PM, session This PR's content lands via PR #4208 instead — that branch is stacked on this one and already contains every commit here; the train-finisher agent is adding the rc.6 residual fixes on top, retargeting #4208 to Generated by Claude Code |
Fixes #4167.
⛔ Stacked on #4169 (
claude/issue-7100-global-nav-studio-retire), not onmain. This card's premise — the@objectstack/specfamily resolved at17.0.0-rc.6— exists only on that branch, so CI here runs against that base and inherits its state. It retargets automatically if #4169's base evolves.Both of the card's items are complete and their gates are green:
check:spec-symbolsname collisions, per-symbol triagecheck:spec-symbolsgreenregistry-inputs-spec-paritygreenThere is also a third finding that changes how this PR's CI must be read, in §3. It is not mine to fix and it is measured, not assumed.
1. The collisions — 14, and each triaged on its own
The card named 13. The guard reports 14:
@object-ui/types:GlobalFilterSchemaappeared during the rc.6 bump and is an artifact of it, so it belongs here rather than to a future card. Details in its row.Verdicts follow the objectstack#4115 method — bind, derive, rename, or ALLOW with a written reason — and the answer genuinely differs per symbol. Nothing was blanket-renamed.
app-shellMetadataState'draft' | 'active' | 'deprecated' | 'archived'MetadataCacheStateapp-shellresolveI18nLabelresolveKeyedI18nLabelcoreDATE_RANGE_PRESETSsatisfiescompleteness pin)fieldsAddressValuez.input< typeof AddressValueSchema >— seven partslayoutBreakpointColumnMap$strictsix-key maplayoutBreakpointOrderMap$strictsix-key mapreactMetadataStateMetadataCacheStatereactNavigationModez.input< typeof NavigationModeSchema >— the same 7-member unionreactresolveI18nLabelresolveKeyedI18nLabeltypesDateRangePresetFilterBuilderDateRangePresettypesKanbanConfig$strict3-key configtypesCalendarConfig$strict4-key configtypesGanttConfigtypesGlobalFilterSchemaThe three renames, and why each is a rename rather than an ALLOW
The
AuthProvider/AuthProviderConfigsplit from batch 5 is the test applied: a JSX element cannot be mistaken for a zod enum, so it was ALLOWed;AuthProviderConfignames the spec's OAuth registration shape in the same domain and the same words, so it was renamed. All three below fail that test the same way.MetadataState— an object of five arrays vs a string union, mutually unassignable, so nothing would have caught the misreading at the point it was made.Cachebecause that is what it is:MetadataProvideris a TTL cache and these are its materialized contents. Six references repo-wide.resolveI18nLabel— the one where the collision had already started costing something, and the only one that is dangerous rather than untidy. rc.6 widenedI18nLabelfromstringtostring | Record< string, string >, so the same authored value now reaches either resolver, and each answers wrongly and silently for the other's input: the keyed one returnsundefinedfor{ en: 'Owner' }(nokey, nodefaultValue); the spec's readskey/defaultValue/paramsas locale tags and picks one as display text. retire(studio): drop the global_nav designer surfaces with the rc.6 spec bump (objectstack#7100) #4169 met this and aliased the spec's import asresolveInlineI18nLabelin five files, with hand-written comments at two of them — a review convention, which is exactly what objectstack#4115 exists to replace with a rule.Keyedis now the counterpart of thatInline. A fourth copy inapps/console/src/utils.tsis renamed with them; it is outside the guard's scan surface (packages/*/srconly) but plants the identical premise.DateRangePreset— two vocabularies that are not even nested by accident. The spec's 13 are historical windows and itssuperRefineonGlobalFilterSchemarejects everything else by name; this one adds 8 future windows (tomorrow,next_week, …) for theFilterBuilderConfigsurface, where a forward-looking range is the point. So a reader who took the spec's 13 off this name would have been told 8 windows exist that the dashboard schema rejects, and vice versa.FilterBuilderCondition(batch 5) named its sibling the same way. Zero internal consumers outsideDateRangeFilterin the same file, so no call-site churn.No tripwire tests were added for the new names, deliberately.
check:spec-symbolsalready is that tripwire: the day the spec publishesMetadataCacheState,resolveKeyedI18nLabelorFilterBuilderDateRangePreset, the guard reds naming it. TheObjectUiLocal…-plus-tripwire form in #4115 exists for symbols that stay in the spec's own vocabulary; these three left it.Four copies were losing information, not just duplicating it
GanttConfigdeclared six keys and called itself canonical; rc.6 declares seventeen. The eleven it never mentioned —parentField,typeField,baselineStartField,baselineEndField,groupByField,resourceView,assigneeField,effortField,capacity,quickFilters,autoZoomToFilter— are all read today byplugin-gantt/src/ObjectGantt.tsx, through a localGanttConfigExintersection that existed only because this type did not carry them.timeSegments(shift segmentation) is the one genuinely local key and stays declared as objectui's;GanttConfigSchemais$looseupstream, so it is legal metadata rather than a second dialect — the spec's decision for this vocabulary, not a widening taken here.GanttConfig.tooltipFieldscarried the comment "not part of the upstream GanttConfigSchema". It is, as of rc.6.AddressValuedeclared five of seven parts (countryCode,formattedmissing) under a comment already claiming to be "the part names ofAddressSchema". The widget still renders five inputs —formattedis derived andcountryCodeis not a part a human types — but the type no longer asserts the platform cannot store them, and the{ ...address }write-through now says so.DATE_RANGE_PRESETSwasObject.keys(PRESET_RANGES). The spec's own doc comment names this module as one of the three copies objectstack#4614 extracted the vocabulary to collapse. It is now the spec's array by reference (toBe, nottoEqual— a faithful copy passes every value comparison, which is why test(plugin-charts): serve ObjectChart's option-color probe from a double, not the network #4115 makes reference identity the test), and the local date-macro bounds table is pinned complete against it withsatisfies.That
satisfiesis load-bearing, not decoration. With the list coming from the spec and the bounds staying local, the two can drift in exactly the direction the spec's comment names as the failure mode — a preset the schema knows and the bounds table lacks "validates clean and then resolves to nothing". The annotation formconst PRESET_RANGES: Record< DateRangePreset, … >would not catch it: a string index signature satisfies every literal key, so the check passes vacuously.GlobalFilterSchema— the 14th, and why it is an ALLOWIt collided because of #4169's own forced rewrite. rc.6 put a refinement on the spec's schema, which makes
.extend()throw at module load and types every override asneverunder.safeExtend(); the only remaining spelling is a.shapespread. That moved a derivation the guard could see (a call target) into an object literal it deliberately does not descend into — the guard's header says why: a spread insidez.object({…})is indistinguishable, to an AST pass, from a hand-typed shape.So the dialect did not change; its spelling did. The precedent is exact and one entry above it in the same list:
SelectOptionSchemacomposes the identical way (...SpecSelectOptionSchema.shape) and is ALLOWed for the identical reason. Nothing about #4165 is encoded here — the refinement is still not carried, the standstill is still pinned, and the entry says which issue owns the decision.2. The parity exemptions
registry-inputs-spec-parity: 49 tests, 2 failing → 51 tests, 0 failing.2a. All twelve off-spec exemptions deleted — the list is now empty
Not eleven or thirteen: the
carries no stale exemptiontest named all twelve at once, and each was verified against the resolvedComponentPropsMap[type].shapeat this pin rather than from the issues' wording.page:headernow declaresrecordChrome/showStar/showCopyId;page:accordiondeclaresvariant;page:tabsdeclarestabStyle. Five keys the renderers had read all along are contract now.element:record_pickerdeclareslabelField/valueField/label;page:carddeclareschildren(replacing the retiredbody);page:section/page:footer/page:sidebarcarry the sharedPageContainerProps. These seven are the objectui 跟进(#5775):page:card 的 registry inputs 仍把已退役的body发布为设计器槽;page:section/footer/sidebar 三个注册项没有 inputs,设计器无法授权 children #4027 stale-pin set, which predicted its own deletion ("Delete this entry when the pin moves") and got it.page:tabsis the interesting one: the spec declares bothtabStyleandtype, so the carrier collision the deleted entry wrote up was resolved upstream by declaring the alias rather than renaming. That is whypage:tabs.typestays inUNPUBLISHED_EXEMPTIONS(spec-declared, unpublishable in a flat carrier) whiletabStyleneeds no cover at all.The map is kept declared-and-empty rather than removed, with a tombstone test — otherwise "empty because everything resolved" and "deleted by accident along with the divergences it covered" look identical and every forward assertion passes either way. That is the misdiagnosis #4027 records for the
SPEC_SHAPE_EMPTY_ON_THE_PINcarve-out, which also left an assertion behind rather than just a comment.Note that the
UNPUBLISHED_EXEMPTIONSentries did not go stale, exactly as their own reasons predicted: ADR-0087 D2 retirement replaces an entry withz.never().optional()rather than deleting it, sodisplayField/searchFields/multiple/page:card.bodyare still "declared" to this gate. They resolve via objectui#3809, not via the pin.2b. Three record-picker inputs declared
sort,limitandemptyTextwere already read by the renderer and declared by the contract, but absent frominputs— sorenderers/layout/page.tsx, which builds the JSX-page compiler's prop whitelist fromgetKnownTypes()plus theseinputs, drew anunknown-propwarning on keys the renderer then honoured. objectui#3407's shape, and the same repair #3808 and #3830 made before it. The red demanding them was predicted in writing by the exemption covering the retired trio, which called it "correct and wanted".Each description documents renderer behaviour rather than restating the schema, per the #3830 / #4041 shape — because that is the half an author cannot read off the contract:
sort—'array'is the spec's shape, verified bysafeParse:[{ field, order }]parses and the terse"name asc"does not. Overridden outright by a node-leveldataSource(dataSource.sort ?? sort), not merged.limit— default 50 comes from the renderer (record-picker.tsx:107), not the schema; a record outside the limit cannot be picked at all, with nothing in the control saying more exist.emptyText— published asstringagainst a contract ofstring | Record< string, string >. rc.6 widened it toI18nLabel, and this renderer passes the value straight into a text node with no locale resolution, so only the plain-string form renders. The description says so rather than advertising a shape the renderer drops — the narrowed-type treatment objectui#3832 describes, with the render-site gap belonging to §3.Both sets are pinned by name as DECLARED, not merely "not failing", because the derived assertions would go green just as readily if the keys had been exempted instead — which is the move #3808 exists to rule out.
3.⚠️ How to read this PR's CI — 13 type errors that are not mine
First, what actually runs here: almost nothing, and that is structural. Every gate workflow in
.github/workflows/filterspull_requesttobranches: [main, develop]—ci.yml,lint.yml,changeset-guard.yml,changeset-presence.yml,control-bytes.yml,skills-paths.yml,docs-links.yml,performance-budget.yml,live-e2e.yml, all of them. This PR's base is a feature branch, so none of them trigger. Its checks converge at two, bothsuccess(Auto Label PRs,Vercel Preview Comments), with nothing queued and nothing red. A green tick here is therefore evidence of very little, which is why every gate below was run locally instead and its real output quoted.Second, what will run when the chain reaches
main.check:spec-symbolsis green, but theTypeScript Type Checkjob it lives in will be red on the merged chain, and this card cannot make it green. The issue's premise that this gate is "currently the main red" of that job is accurate aboutcheck:spec-symbolsand incomplete about the job.turbo run type-check --continuereports 13 errors in 5 packages, and every one is the same fact: rc.6 widenedI18nLabelfromstringtostring | Record< string, string >, and these sites take it into aReactNode, aninputvalue, or objectui's keyed resolver.#4169 repaired eight such render sites; these are the ones its pass did not reach, and
turbo run buildcannot see them — these packages build through vite/rolldown, so onlytype-checkcompiles them. That is why a bump reporting a green 44/44 build and 14,609 passing tests still has them.This is objectui#4163 part 1 verbatim ("the I18nLabel audit"), which my scope guard fences off, and it is not mechanical:
@object-ui/layoutdeliberately has no i18n dependency ("enables convention-based i18n auto-resolution without coupling the layout package to i18n"), so choosing where its locale comes from is a design call in that package, not a find-and-replace. Left alone, with the inventory posted to #4163 so it is a named worklist rather than an open-ended audit.Measured, not assumed
A second worktree at the untouched base (
db9dad961) was type-checked with the identical command:db9dad961The 13 are byte-identical to 13 of the base's 16. This branch introduces none and removes three.
The three it removes
All pre-existing, all rc.6 adaptation in this card's own family (not #4163's), and all found only by running
type-check— which #4169 did not. Two are one-line re-points; the third is a member-list update. They are included because without the first,turbostops at@object-ui/typesand never reaches a single package this PR touches, so verifying my own work required them.types/…/page-nav-misc-spec-parity.test.ts—ThemeInputno longer exists (the…Inputalias retirement, item 1 of the card's own rc.6 inventory). Re-pointed by side, not by name:ThemeInput→ThemeandTheme→ThemeParsed, because rc.6 moved the bare name onto the input side. Following the names would have made the second pin compare the authoring side against itself and go green on nothing — the same swap retire(studio): drop the global_nav designer surfaces with the rc.6 spec bump (objectstack#7100) #4169 avoided inspec-report.ts.data-objectstack/spec-symbol-batch6.test.ts— rc.6 added a thirdDroppedFieldsEvent.reasonarm,primary_key. This pin fired exactly as designed, and its firing is the point: the hand copy it replaced had widenedreasonto barestring, and under that the arm would have arrived silently. The member list is updated; the consumer gap is objectui#3935, which was filed before the arm shipped and named this precise moment as its activation condition.emitWriteWarningstill branches binary, so aprimary_keydrop is currently narrated as "Read-only" — false, and the same mis-wording ADR-0056 的 Consequences 自相矛盾:「Capabilities 在 Setup 里作者化」与 P2/P4「在 Studio 设计、Setup 里没有 facet 可编辑」冲突 #3794 fixed forreadonly_when. Not fixed here: it needs an exhaustive map plus a third wording in both locale tables, and writeWarningToast branches onDroppedFieldsEvent.reasonwith a binary ternary, so every future reason is labelled "Read-only" #3935 already prescribes the shape.3b. And one inherited test failure, in a package this PR does not touch
pnpm exec vitest runis 14,618 / 14,620 with one failure:plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts→ "the spec really takes OBJECT sections — the id-list spelling is gone, not unioned in".Proved to be a pure spec fact, with a probe that runs no objectui code at all:
rc.6 turned
record:details.layoutinto a D2 tombstone, and the fixture carrieslayout: 'custom'. The test's own premise is untouched — thesectionsobject form still parses, member keysname/label/columns/fields.The interesting half is that the sibling assertion in the same test has gone VACUOUS. It reads
expect(idList.success).toBe(false)to prove the retired id-list spelling ofsectionsis rejected — and that is now green because oflayout, not because ofsections. A rejection assertion passing for the wrong reason is worse than a failing one, so this needs the fixture repaired and the rejection re-pointed at the key it names.Not fixed here: objectui#3818 already owns
record:details.layout(it is the card whose upstream resolution objectstack#6946's tombstone is), the renderer's ownlayoutread point needs the same audit, and none of it is in this card. Posted to #3818 rather than filed as a twin.Reverse verification
Three, each predicted red before running and each red for the named reason. The second and third are the ones that matter: in both, the repo's own derived assertion and the pin this PR adds fire together, which is what shows the new pins are not vacuous.
1. Un-fix one collision —
git checkout origin/claude/issue-7100-global-nav-studio-retire -- packages/layout/src/ResponsiveGrid.tsx:2. Restore one deleted exemption (
page:header.recordChrome):3. Withdraw one newly-declared input (
emptyText):Case 3 is the one the pin exists for: the derived assertion alone would have gone green again if
emptyTexthad been added toUNPUBLISHED_EXEMPTIONSinstead of declared. The named pin refuses that, which is the move objectui#3808 exists to rule out.Verification
Run locally, because §3 explains that this PR's own CI runs no gate workflow — its two checks are
Auto Label PRsandVercel Preview Comments, both green, and neither judges any of this.node scripts/check-spec-symbol-derivation.mjs— ✅1224 files scanned against 4834 spec export names; 14 declared dialects, 3 untriaged collisions in 1 packages(the 3 are the pre-existing@object-ui/typesDEBT ledger, untouched)pnpm exec turbo run build --concurrency=2— 44/44 successfulpnpm exec turbo run type-check --concurrency=2 --continue— 73/78, the 5 failures being §3's inheritedI18nLabelsetpnpm exec vitest run(whole repo) — 14,618 passed / 14,620, 1 skipped, 1 failed:plugin-detail/…/recordDetailsInputs.spec-parity.test.ts, inherited and unrelated — see §3bapps/console/…/registry-inputs-spec-parity.test.ts— 51 passed (was 49 with 2 failing)check-changeset-fixed✅ ·check-changeset-no-major✅ ·check-changeset-presence✅ (3 changesets) ·check-control-bytes✅turbo run lintover the 10 touched packages — 10/10 successful, 0 errors (warnings pre-existing)Changesets:
minorfor the seven packages with changed public exports (majoris banned bycheck-changeset-no-major— objectui's own breaking changes ship as minor with the break spelled out, since its major is pinned to@objectstack's), pluspatchforplugin-designer, which only follows the rename at its call sites.Generated by Claude Code