diff --git a/.changeset/3162-types-ledger-batch8-verdicts.md b/.changeset/3162-types-ledger-batch8-verdicts.md new file mode 100644 index 000000000..d578606c3 --- /dev/null +++ b/.changeset/3162-types-ledger-batch8-verdicts.md @@ -0,0 +1,33 @@ +--- +--- + +Closes the last batch of the `@objectstack/spec` any-erasure ledger (objectstack#4115 batch +8/8) with zero re-exports, because measurement says none of the four symbols is burnable. + +The batch — `JoinNode`, `NavigationItem`, `NavigationItemSchema`, `JoinedReportBlock` — was +filed as "blocked on objectstack#4171; burn down when the upstream `any` erasure lifts". +That issue closed completed on 2026-07-30, which is the card's own start signal. Re-measuring +each symbol in the BUILT dist at `@objectstack/spec` 17.2.0 (source and issue state are both +insufficient — dist erasure is invisible from either) found three different reasons why +binding is still wrong, none of them the reason the ledger recorded: + +- `JoinNode` — the spec no longer exports the symbol at all; spec 17.0.0 retired the + `query.joins` cluster. Nothing upstream to bind to. +- `NavigationItem` — upstream is precise now, so "no longer `any`" is settled; the three + semantic blockers that actually prevent binding are untouched by it. +- `NavigationItemSchema` — upstream is precise now too, and that is exactly what makes the + burn-down dangerous: the live blocker is runtime shape, invisible to every type-level + probe. Referencing the spec's schema would make the published `objectui validate` reject + navigation metadata this renderer accepts today. +- `JoinedReportBlock` — still erased, to `unknown`, by a cause objectstack#4171 never + covered (a bare `z.ZodTypeAny`, not recursion). + +So the remaining debt is converted from a card pointing at an upstream issue into pins that +each name their own release condition, and the guard's machine-readable ledger +(`check:spec-symbols`) goes from "3 untriaged collisions" to zero, with the three symbols +recorded as declared dialects carrying their measured reasons. + +Two stale justifications were corrected in passing, both the same defect class this ledger +exists to catch — a comment asserting an upstream type "erases to `any`" when it no longer +does. Left alone, the next triage checks the claim, finds it false, and lands the regression. +No published behaviour changes; no runtime code was touched. diff --git a/packages/types/src/__tests__/navigation-spec-parity.test.ts b/packages/types/src/__tests__/navigation-spec-parity.test.ts index e8f344d7e..421d29020 100644 --- a/packages/types/src/__tests__/navigation-spec-parity.test.ts +++ b/packages/types/src/__tests__/navigation-spec-parity.test.ts @@ -38,6 +38,7 @@ import { describe, it, expect } from 'vitest'; import { NavigationItemSchema, NavigationAreaSchema } from '../zod/app.zod.js'; +import { NavigationItemSchema as SpecNavigationItemSchema } from '@objectstack/spec/ui'; /** Parse and return the surviving object, so "accepted" cannot hide a strip. */ function keep(input: unknown): Record | null { @@ -114,3 +115,54 @@ describe('NavigationAreaSchema keeps the spec fields it used to drop', () => { expect(r.success && r.data.description).toBe('Pipeline and accounts'); }); }); + + +// ───────────────────────────────────────────────────────────────────────────── +// Why this schema is NOT the spec's, stated as a measurement (objectui#3162) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * The objectstack#4115 ledger filed `NavigationItemSchema` as "erased to `any` + * upstream — flow the spec's shape in by reference once objectstack#4171 lands". + * #4171 landed, and the spec's schema is precise now (pinned in + * `spec-derived-unions.test.ts`). That makes the ledger's stated reason spent — + * and it is exactly the moment the burn-down becomes DANGEROUS, because the + * remaining reason lives at runtime where no type-level probe can see it. + * + * The header above says it in prose: objectui keeps one flat, all-optional shape + * and the spec is a discriminated union of `.strict()` variants. Prose does not + * fail. This does: every case below is metadata `objectui validate` accepts today + * and the spec's schema rejects, so referencing the spec's schema would silently + * narrow the published CLI's accepted surface. + * + * The day the two are reconciled these flip and this block is the instruction to + * re-run the triage — not to delete the assertions until the divergence is + * actually gone. + */ +describe('referencing the spec NavigationItemSchema would reject metadata objectui accepts (objectui#3162)', () => { + /** Guards every rejection below from passing because the spec schema rejects everything. */ + it('accepts a fully spec-shaped item and a bare separator (positive controls)', () => { + expect(SpecNavigationItemSchema.safeParse({ + id: 'apps', type: 'object', label: 'Apps', objectName: 'sys_app', + }).success).toBe(true); + expect(SpecNavigationItemSchema.safeParse({ type: 'separator' }).success).toBe(true); + }); + + it.each([ + ['pinned', { id: 'apps', type: 'object', label: 'Apps', objectName: 'sys_app', pinned: true }, + 'backs useNavPins + FavoritesProvider'], + ['defaultOpen', { id: 'grp', type: 'group', label: 'G', children: [], defaultOpen: true }, + 'the legacy spelling this file keeps accepting for published metadata'], + ['visible: boolean', { id: 'ai', type: 'url', label: 'AI', url: '/ai', visible: true }, + 'menuItemToNavigationItem MANUFACTURES one when it inverts MenuItem.hidden'], + ['separator label', { type: 'separator', label: 'Section' }, + 'menuItemToNavigationItem emits one; the spec separator declares only id/order'], + ['single-character id', { id: 'a', type: 'url', label: 'A', url: '/a' }, + 'objectui requires only a non-empty id; the spec requires two characters'], + ])('the spec rejects %s (%s)', (_name, input, _why) => { + // objectui accepts it... + expect(NavigationItemSchema.safeParse(input).success).toBe(true); + // ...and the spec does not, which is the whole reason for the local schema. + expect(SpecNavigationItemSchema.safeParse(input).success).toBe(false); + }); +}); diff --git a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts index d232d0735..3710dce9d 100644 --- a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts +++ b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts @@ -641,6 +641,15 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 ['DriverQueryConfig', 'high-level query config the SQL AST builder consumes'], ['SqlDriverInterface', "objectui's SQL-oriented client driver abstraction"], ['DatasourceRegistration', 'in-memory datasource registration record'], + // objectui#3162 batch 8. `JoinNode` was ledgered as "erased to `any` upstream, + // re-export once objectstack#4171 lands". #4171 landed — and the symbol it was + // waiting on no longer exists: spec 17.0.0 retired `query.joins` and the whole + // JoinNode cluster (framework#4286), so there is nothing upstream to bind to + // and `data-protocol.ts` owns the name outright. Measured against spec 17.2.0: + // importing it from `@objectstack/spec/ui` OR `/data` is TS2305. This row is + // what tells us if the spec ever mints the name again, which is the only way + // the collision could come back. + ['JoinNode', "objectui's local query-AST join node; the spec retired its own (framework#4286)"], ])('the spec does not own `%s` (%s)', (name) => { expect(names).not.toContain(name); }); @@ -668,6 +677,15 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 * types the schema properly, `IsUnknown<…>` flips to `false`, `true satisfies * false` stops compiling, and the failure is the instruction: re-run the triage * and burn it down. + * + * ⚠️ objectstack#4171 is CLOSED (completed 2026-07-30) and this symbol is STILL + * erased — do NOT read that closure as this pin's release. #4171 typed the + * RECURSIVE schemas (`NavigationItem`, `FormField`); it never touched + * `JoinedReportBlockSchema`, whose erasure has a different cause — a bare + * `z.ZodTypeAny` annotation, not recursion. Re-measured in the built dist at + * spec 17.2.0 (objectui#3162 batch 8): `IsUnknown` is still `true`. The pin + * below, not the state of any upstream issue, is what says when this is + * burnable. */ type IsUnknown = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false; type IsAny = 0 extends 1 & T ? true : false; @@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny { +describe('JoinedReportBlock stays in the ledger — still untyped upstream (objectui#3162)', () => { it('documents why: the spec ships the schema untyped', () => { // The compile-time pins above are the real guard; this keeps the reason // visible in the test report rather than only in a comment. diff --git a/packages/types/src/__tests__/spec-derived-unions.test.ts b/packages/types/src/__tests__/spec-derived-unions.test.ts index 262b7ff6a..f728fa98e 100644 --- a/packages/types/src/__tests__/spec-derived-unions.test.ts +++ b/packages/types/src/__tests__/spec-derived-unions.test.ts @@ -57,7 +57,9 @@ import { ACTION_LOCATIONS as SpecACTION_LOCATIONS, ActionLocationSchema as SpecActionLocationSchema, ActionSchema as SpecActionSchema, + NavigationItemSchema as SpecNavigationItemSchema, } from '@objectstack/spec/ui'; +import type { z } from 'zod'; import { FieldType as SpecFieldType, } from '@objectstack/spec/data'; @@ -147,6 +149,38 @@ const _validationErrorShape: ValidationError = { field: 'name', message: 'requir // gone with the imports. `packages/types/src/data-protocol.ts` now restates the // members locally, verbatim from the last spec that published them, as the // objectui query-AST vocabulary they have become. +// +// ── objectstack#4115 batch 8/8 (objectui#3162): CLOSED WITH ZERO RE-EXPORTS ─── +// +// The ledger's last batch — `JoinNode`, `NavigationItem`, `NavigationItemSchema`, +// `JoinedReportBlock` — was filed as "blocked on objectstack#4171; burn down when +// the upstream `any` erasure lifts". #4171 closed completed on 2026-07-30, and the +// re-measurement it licensed found that NONE of the four is burnable, for three +// DIFFERENT reasons — none of which is the one the ledger recorded. Measured in the +// BUILT dist at spec 17.2.0 (source and issue state were both insufficient: the +// whole point of this ledger is that dist erasure is invisible from either): +// +// - `JoinNode` — the spec no longer exports the symbol AT ALL +// (TS2305 from `/ui` and `/data`); spec 17.0.0 retired +// the cluster. Nothing upstream to bind to. Absence +// pinned in `report-chart-query-spec-parity.test.ts`. +// - `NavigationItem` — upstream IS precise now (`IsAny` and `IsUnknown` both +// `false`), so #4171 really did land. Binding is still +// wrong: the three semantic blockers pinned below are +// unaffected by it. `any` was never the only blocker — +// #3177 established that, and it still holds. +// - `NavigationItemSchema`— upstream IS precise now; pinned below. The live +// blocker is SHAPE, and it is a RUNTIME one: see +// `navigation-spec-parity.test.ts`. +// - `JoinedReportBlock` — STILL erased, to `unknown`, by a cause #4171 never +// covered. Pinned in +// `report-chart-query-spec-parity.test.ts`. +// +// So the batch burns down to zero re-exports and the ledger's remaining debt is +// now carried as state pins that name their own release condition, rather than as +// a card pointing at an upstream issue that has since closed without settling it. +// ⛔ A closed upstream issue is not a licence to re-export: prove the dist type is +// precise, then prove binding it does not narrow what this package already accepts. /** * Admission probes for `NavigationItem` and `FormField` (#3177). @@ -238,6 +272,17 @@ const _specSeparatorStillHasNoLabel = false satisfies 'label' extends keyof Extr // that drops a member (the objectstack#4115 failure class) fails here. const _navTypeCoversSpec = null as unknown as SpecNavigationItem['type'] satisfies NavigationItemType; +// `NavigationItemSchema` (objectui#3162 batch 8): upstream precision is SETTLED, +// so no future triage may re-derive the ledger's stale reason from prose. The spec +// now ships `z.ZodType`; this holds that +// measurement, and fires if the annotation ever regresses to `z.ZodType`. +// +// This pin deliberately does NOT say the schema is bindable — it says the OLD +// reason for not binding is spent. The live reason is runtime shape, and only +// `navigation-spec-parity.test.ts` can pin that, because it is about which +// metadata parses, not which types assign. +const _specNavSchemaIsNoLongerAny = false satisfies IsAny>; + // ── FormField: not one concept in two dialects, but two concepts on two layers ─ // // `select-option-spec-parity.test.ts` states the distinction in its own header — @@ -296,6 +341,7 @@ void _validationErrorShape; void _localNavIsNotYetTheSpecUnion; void _specNavVisibleStillRejectsBoolean; void _specNavStillHasNoPinned; void _specNavStillHasNoDefaultOpen; void _specSeparatorStillHasNoLabel; void _navTypeCoversSpec; +void _specNavSchemaIsNoLongerAny; void _specFormFieldIsNoLongerAny; void _specFormFieldStillHasNoName; void _specFormFieldInputStillHasNoName; void _specFieldSlotIsStillAName; void _localFieldSlotIsStillAnObject; void _specDependsOnStillTakesNoArray; diff --git a/packages/types/src/zod/app.zod.ts b/packages/types/src/zod/app.zod.ts index d458e70df..51bfedfc5 100644 --- a/packages/types/src/zod/app.zod.ts +++ b/packages/types/src/zod/app.zod.ts @@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType = z.lazy(() => z.object({ * is what let that happen. * * One key is pinned locally, matching the TS twin in `app.ts`: - * - `navigation` — objectui's `NavigationItemSchema`, because the spec's is - * `z.ZodType` (objectstack#4171 / objectui#3162) and would validate - * nothing. + * - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER + * "the spec's is `z.ZodType` and would validate nothing". That was true + * when this was written and is false now: objectstack#4171 landed, and spec + * 17.2.0 declares `NavigationItemSchema: z.ZodType` — measured precise in the BUILT dist, not just in + * source (objectui#3162 batch 8). + * + * What blocks the burn-down now is SHAPE, not precision. The spec models + * navigation as a discriminated union of `.strict()` variants; objectui keeps + * one flat, all-optional object that deliberately accepts more. Measured + * against spec 17.2.0, referencing the spec's schema would make `objectui + * validate` REJECT metadata this renderer accepts today: `pinned`, + * `defaultOpen` and a separator carrying `label` all fail `unrecognized_keys`, + * `visible: boolean` fails `invalid_union`, and a one-character `id` fails + * `too_small`. Pinned by `__tests__/navigation-spec-parity.test.ts`. + * + * Converging on the union is a breaking change for every consumer that reads + * fields off `NavigationItem` without narrowing — tracked separately, and + * deliberately not smuggled into a ledger burn-down. * * `order`, `visible` and `requiredPermissions` were AREA-level keys until * `@objectstack/spec` 17.0.0 retired them (`AREA_VISIBLE_RETIRED` / diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index 0ed34f286..30d0b3495 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -255,8 +255,10 @@ const ALLOW = { "Two-LAYER vocabulary, not two dialects of one concept (objectui#3090): the spec's " + "FormField is the authored form-VIEW shape (`field` = object-field reference, presentation " + "deltas only) while this is the runtime widget config (`name` = form data path, " + - "self-contained). Not derivable — and the spec's FormField type erases to `any` in its dist " + - "(objectstack#4171), so a re-export would delete typing outright. The layers meet only in " + + "self-contained). Not derivable. ⚠️ It is NOT the `any` erasure that blocks this any more: " + + "objectstack#4171 landed and the spec's FormField is typed — `spec-derived-unions.test.ts` " + + "pins `_specFormFieldIsNoLongerAny`. The blocker is the two-LAYER split above, which " + + "precision does not touch (guard header case 2c). The layers meet only in " + "`normalizeSectionField` (@object-ui/plugin-form), gated by " + "sectionFields.spec-parity.test.ts (per-key behavioral coverage of the spec key set, both " + "directions). Misimport of the spec names is banned by the no-restricted-imports entry in " + @@ -339,6 +341,57 @@ const ALLOW = { "if the spec's `AuthProvider` ever stops being an enum of provider ids.", issue: 4115, }, + // ── objectstack#4115 batch 8/8 (objectui#3162) ─────────────────────────────── + // These three were the last DEBT entries. The ledger filed them as "blocked on + // objectstack#4171, burn down when the upstream `any` erasure lifts"; #4171 + // closed completed 2026-07-30, and the re-measurement it licensed found none of + // them burnable — for reasons that are NOT the one the ledger recorded. They are + // deliberate dialects, so they belong here with the reason written down, rather + // than in a debt bucket implying work that measurement says must not happen. + // Every verdict below was measured in the BUILT dist at spec 17.2.0. + "@object-ui/types:NavigationItem": { + reason: + "Precise upstream, still not bindable — guard header case 2c. objectstack#4171 landed and " + + "the spec's NavigationItem is neither `any` nor `unknown` any more, so `no longer any` is " + + "settled and is NOT a licence to bind. The spec models navigation as a nine-variant " + + "discriminated union; objectui keeps one flat shape carrying `visible: boolean` (the spec " + + "takes a CEL string / Expression envelope, and `menuItemToNavigationItem` MANUFACTURES a " + + "boolean when it inverts legacy `MenuItem.hidden`), plus `pinned` (`useNavPins` + " + + "`FavoritesProvider`) and `defaultOpen`, neither of which the spec declares at either tier, " + + "plus a separator carrying `label`. Each blocker is pinned one-per-line in " + + "packages/types/src/__tests__/spec-derived-unions.test.ts, written to stop compiling the day " + + "that specific blocker lifts. What IS derivable is already derived (`NavigationItemType` and " + + "the per-branch keys come off the spec).", + issue: 4115, + }, + "@object-ui/types:NavigationItemSchema": { + reason: + "Zod twin of the NavigationItem dialect, and the case where a precise upstream is most " + + "dangerous. The spec now ships `NavigationItemSchema: z.ZodType` — pinned by `_specNavSchemaIsNoLongerAny` — so the ledger's stated " + + "reason (`the spec's is z.ZodType and would validate nothing`) is spent. The live " + + "blocker is RUNTIME shape, invisible to every type-level probe: this schema has a published " + + "consumer in `objectui validate`, and referencing the spec's would make it REJECT metadata " + + "objectui accepts today — `pinned`, `defaultOpen` and a separator `label` all fail " + + "`unrecognized_keys` against the spec's `.strict()` branches, `visible: boolean` fails " + + "`invalid_union`, and a one-character `id` fails `too_small`. All five are pinned, behind two " + + "positive controls, in packages/types/src/__tests__/navigation-spec-parity.test.ts. " + + "Converging on the union is a breaking change tracked separately.", + issue: 4115, + }, + "@object-ui/types:JoinedReportBlock": { + reason: + "Guard header case 2b, and still live: the spec declares `JoinedReportBlockSchema` as a bare " + + "`z.ZodTypeAny`, so its exported type resolves to `unknown`. Re-exporting would replace this " + + "package's precise block interface (`name`/`columns`/`groupingsDown`/`groupingsAcross`/" + + "`filter`/`chart`) with nothing at all. ⚠️ objectstack#4171 is CLOSED and this is STILL " + + "erased — that issue typed the RECURSIVE schemas and never touched this one, whose erasure " + + "has a different cause. Re-measured at spec 17.2.0: `IsUnknown` is still `true`. Pinned in " + + "packages/types/src/__tests__/report-chart-query-spec-parity.test.ts, which fails the day the " + + "spec types the schema — that pin, not the state of any upstream issue, is the release " + + "condition.", + issue: 4115, + }, "@object-ui/types:SelectOption": { reason: "TS twin of the SelectOptionSchema dialect (objectui#3090): carries every spec key " + @@ -495,13 +548,12 @@ const ALLOW = { // divergent `_input` — so re-exporting it would silently change what parses. // Compare `_input` too before touching a schema const. const DEBT_ISSUE = 4115; -const DEBT = { - "@object-ui/types": [ - "JoinedReportBlock", - "NavigationItem", - "NavigationItemSchema", - ], -}; +// EMPTY since objectui#3162 (objectstack#4115 batch 8/8). The last three entries +// were not burned down — they were MEASURED not-burnable and moved to ALLOW as +// declared dialects, each carrying its own release condition as a pin. Re-adding +// a name here means "collides, not yet triaged"; a name whose triage concluded +// "deliberate divergence" belongs in ALLOW instead. +const DEBT = {}; // Files under these paths are not objectui's own authored surface. // - `ui/` is the Shadcn no-touch zone (AGENTS.md #7): upstream 3rd-party files