fix(types): chain the spec's objectNavTargetExclusivity in NavigationItemSchema (objectui#8563) - #9060
Conversation
…ItemSchema (objectui#8563) `NavigationItemSchema` is hand-written rather than derived from a spec `.shape`, so nothing carried the spec's own checks across it: an object nav entry declaring both `filters` and `recordId` — or both `runAction` and `recordId` — parsed clean at this door and was refused by `@objectstack/spec` at publish. The rule is CHAINED, not restated. A local copy passes every case the day it is written and drifts the day the spec's own rule moves, which is this same defect one layer down. The pin compares this door's issues byte for byte against the exported function driven directly, and parses the mirror's source to refuse a local re-declaration of the name. The two asymmetries the spec keeps on purpose are preserved and pinned by six negative controls: `recordId` + `viewName` stays tolerated, and `runAction` composes with `viewName` or `filters` — it is refused with `recordId` only. Two `.describe()` strings and the `NavigationItem` interface doc taught a `Precedence: recordId -> filters -> viewName` that resolves nothing, since the combination is refused; they now match the spec's own describe. The `@objectstack/spec` floor moves `^17.3.0` -> `^17.4.0`: 17.4.0 is the first published version that EXPORTS the rule, bisected across the published 17.x line against each version's own tarball rather than workspace resolution. `imported-defaults-8317.test.ts` gains a third declared exception kind — a chained spec refinement, which is a function with no Zod graph to strip and no accept set to differentiate — with a guard that each entry really is a function. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewReviewed head: Charter read at objectstack Tier: Inputs: the card and every comment on it (body, ① Derived judgments — accept-set and public-surface changes, one by one
② Semver grading against the changesetChangeset declares ③ Dev flags,
|
Provenance — carriers cleared, landingClause-② contract review: Reviewer ran at Independence — NOT self-review: Sixth reviewer today to reach this from the charter text, and it stated openly in the record that both subagents came from the same dispatching session, resting independence on the context-isolation clause ( Landing pre-checks — all measured just now
⭐ The floor bisection, and the reviewer confirming my error preciselyI warned in the
That is exactly the rule I failed to apply. My control Two non-binding observations carried forward, ⛔ not fixed here
Also corrected by measurement: the dev reported 9 open PRs holding files; the reviewer measured 14. The conclusion (the out-of-surface edit is held by none of them) is unchanged. Both carriers stripped in this same act; PR flipped ready and enqueued. ⛔ Base is not hand-merged — the queue rebuilds on current Generated by Claude Code |
Fixes #8563
NavigationItemSchemais hand-written rather than derived from a spec.shape, so nothing carried the spec's own checks across it. An object nav entry declaring bothfiltersandrecordId— or bothrunActionandrecordId— parsed clean at this door and was refused by@objectstack/specat publish. This chains the spec's exportedobjectNavTargetExclusivityon thetype: 'object'arm, so the two doors agree.The floor was bisected, not assumed
The dispatch flagged that
17.4.0was not established as the first version carrying the export: a17.3.0probe had returnedundefinedfor the symbol and for its positive controlcheckListViewPageMount, which reads as a broken instrument rather than a negative.Re-derived here against each published
17.xversion's own tarball, never workspace resolution. The instrument was rebuilt first, because the original one had two faults: it grepped the CJSrequirebranch while importing the ESM one, and a plain grep measures PRESENCE, not EXPORTEDNESS.objectNavTargetExclusivityin ESM namespacecheckListViewPageMountObjectNavItemSchema(control)zzzNotAThing(control)The dispatch's
17.3.0reading was a true negative, not a broken instrument.checkListViewPageMountis simply another symbol that first shipped as an export in 17.4.0, so it fails for the same reason as the subject — it cannot serve as a control for versions below it.ObjectNavItemSchemais a control that fires at every version in the range, and with it in place all four readings below 17.4.0 are interpretable negatives.Also worth recording, because it is what made the naive instrument lie:
objectNavTargetExclusivityis present in the bundle from 17.0.0 (4 textual occurrences — it was defined and chained internally all along). 16862 added theexportkeyword. A grep for the name is green four versions before the symbol is importable.Independent third instrument, from a different direction —
check:spec-floorsfetches the declared floor from the registry and reads itsdist/ui/index.d.mtsexport surface. Reverting the floor to^17.3.0produces:The gate names
^17.4.0itself. Floor moved^17.3.0->^17.4.0; the lockfile was regenerated bypnpm installand the diff is exactly one generated specifier line.The two asymmetries are preserved, re-derived from the installed rule
Read out of the installed
17.4.0implementation body, not from the card's prose or the dispatch's:So
recordId+viewNameis tolerated (no clause pairs them), andrunActionis refused withrecordIdonly — it composes withviewNameorfilters. Six negative controls pin exactly that, plus a vacuity guard asserting the rule itself raises nothing for any of them, so the comparison below cannot pass as empty-equals-empty.Chained, not copied — and that is asserted, not just intended
A local copy of the rule body passes every case the day it is written and drifts the day the spec's own rule moves, which is this same defect one layer down. Two assertions are about identity rather than behaviour:
Ablation (committed first, restored to a byte-identical tree, both legs verified by blob hash). Deleting the chained call: 6 failed / 9 passed — the three refusal cases, the nested-
childrencase, the byte-identity comparison and the AST check all go red, while the six negative controls and the vacuity guard stay green. Restored, then re-run: 15/15 green.The precedence sentence had no referent
Precedence: recordId -> filters -> viewNamewas copied from a spec docblock the spec itself corrected. No precedence resolves these combinations — they are refused, so an author following the sentence got a rejection. Corrected in thefilters.describe()(the card's item 3) and in two adjacent sites the same change falsifies:runAction.describe()in the same schema object, which said the combination was ignored when it is now refused;NavigationIteminterface doc inpackages/types/src/app.ts, both thefiltersandrunActionmembers.packages/types/src/app.tsis outside the file surface the dispatch named, so it was taken as a bounded in-place fix and is called out here rather than done quietly: same defect class, comment-only, no gate surface added, and verified held by none of the 9 open PRs.One gate had to be extended, honestly
imported-defaults-8317.test.tsenforces that every@objectstack/specvalue read in a mirror is wrapped instripImportedDefaults, and it is a source census that "can see an import written TOMORROW". A chained refinement is a third kind of read its two declared exceptions did not cover, and it failed on exactly the two assertions predicted before the run.It is extended rather than worked around. Routing the import through a local re-export would have made the census blind, which is the opposite of the point. The new
REFINEMENT_EXCEPTIONSentry is keyed by binding name, holds the binding itself, and a new assertion requires each entry to be a real function carrying no Zod internals — so a schema cannot reach the list merely by being listed in it. The test's own docblock invited this ("adding a fourth is an edit to this list").navigation-spec-parity.test.ts's docblock said the spec'ssuperRefineexclusivity rule was deliberately not modelled. This change falsifies half of that sentence, so it is amended: the union convergence is still not modelled, the object arm's exclusivity rule now is.Verification
pnpm --filter @object-ui/types test— 171 files, 3379 tests, all passedpnpm --filter @object-ui/types type-check— rc 0 (tsc --noEmit+ examples + test projects)eslint .inpackages/types, counts read from--format json: 241 files selected by eslint itself, 0 errors, 277 warnings. The one warning onapp.zod.tsis pre-existing — the sameno-explicit-anyon the samez.ZodType<any>declaration, line-shifted; confirmed by linting the base revision of the file, which reports the identical single warning. No type-aware linting is configured (projectappears 0 times ineslint.config.js), so this diff cannot move the verdict of any file it does not touch.studio-design/navSurface,StudioDesignSurface.navItemInspector, consolepreview-samples-spec-validandregistry-inputs-spec-parity,plugin-gantt/readme-navigation-example, reactoffline-nav-performance-spec-parity,metadata-admin/inspectors/nav-target): 7 files, 296 tests, all passed.resolveHrefand the nav-target inspector takeNavigationItemas a TYPE and never parse, so the narrowing does not reach them.check:spec-symbols,check:control-bytes,check:phantom-deps,check:unused-deps,check:lockfile-integrity(VERDICT clean),check:new-line-citations,check:entry-guard,check:bash32-floor,check:esm-specifiers,check:pre-install-import-graph,changeset:checkandcheck-changeset-presence.check-governed-queue-guard --testover all 8 changed paths: NOT GOVERNED, so no maintainer brief is owed on this PR.check:spec-floorsreports 17no-artifactfindings because onlypackages/typesis built locally —@object-ui/typesitself was judged and clean, and its workflow builds the whole workspace first (it is nightly + release-path, not a per-PR gate).check:readme-exportsself-declares a COLLAPSED population ("35 unbuilt"); its workflow builds./packages/*first, and this diff adds no export and no README.Changeset is
minor, from this repo's own precedent read by content: AGENTS.md §版本号策略 andcheck-changeset-no-major.mjs's header both state that objectui's own breaking changes ship asminorwith the break spelled out in the body, because the 39-package fixed group tracks@objectstack's major.packages/types/CHANGELOG.md17.5.0 carries the matching precedent — an accept-set narrowing (formatsno longer admitting'pdf') shipped as a minor on the same reasoning.Held out of this PR
skills/objectui/guides/app-composition.mdanddocs/adr/0055-parameterized-bare-data-surface.mdstill teach the refused precedence. Reported as objectui#9059 rather than edited:skills/**is a published skill package that pulls in three more gates plus line-budget accounting, and amending an ADR is a maintainer's call, not a rider on a schema PR.Verification notes
Both shared checkouts were left untouched and clean.
/home/user/objectuiHEADd4733f27eand/home/user/objectstackHEADeabdd66f4, bothgit status --porcelainempty at start and at finish. All work was done in a fresh clone.Generated by Claude Code