Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports by claude[bot] · Pull Request #6883 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/3162-types-ledger-batch8-verdicts.md
Original file line numberDiff line numberDiff line change
@@ -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.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/navigation-spec-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, unknown> | null {
Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand DownExpand Up@@ -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<T> = [unknown] extends [T] ? ([T] extends [unknown] ? true : false) : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand All@@ -676,7 +694,7 @@ const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny<SpecJoinedRepor
void _specJoinedReportBlockIsStillUntyped;
void _specJoinedReportBlockIsNotEvenAny;

describe('JoinedReportBlock stays in the ledger (objectstack#4171)', () => {
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.
Expand Down
46 changes: 46 additions & 0 deletions packages/types/src/__tests__/spec-derived-unions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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).
Expand DownExpand Up@@ -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<NavigationItem, NavigationItemInput>`; this holds that
// measurement, and fires if the annotation ever regresses to `z.ZodType<any>`.
//
// 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<z.infer<typeof SpecNavigationItemSchema>>;

// ── 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 —
Expand DownExpand Up@@ -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;
Expand Down
22 changes: 19 additions & 3 deletions packages/types/src/zod/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,9 +126,25 @@ export const NavigationItemSchema: z.ZodType<any> = 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<any>` (objectstack#4171 / objectui#3162) and would validate
* nothing.
* - `navigation` — objectui's `NavigationItemSchema`. ⚠️ The reason is NO LONGER
* "the spec's is `z.ZodType<any>` 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<NavigationItem,
* NavigationItemInput>` — 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` /
Expand Down
Loading
Loading