Skip to content

Commit 1c52a5e

Browse files
fix(spec): the strict blueprint nav item's label describe states that null inherits the target's current label (#21309)
Fixes #21248 Clause-②: no ## What changed `StrictNavItem.label` in `packages/spec/src/ai/solution-blueprint.zod.ts` is the describe the AI design step reads. `propose_blueprint`'s structured output is generated against `SolutionBlueprintStrictSchema`, and strict mode makes `label` a required decision on every nav entry. Until now it read `'Nav entry label, or null'`. It now reads: > Nav entry label, or null. null ⇒ the entry inherits the CURRENT label of what it opens at render time (a renamed target shows its new name); a string ⇒ rendered verbatim, so never copy the target's label in as a default. Write a label ONLY when the entry must read differently from what it opens; otherwise null. - The meaning of each arm is the lenient twin's, byte for byte. The empty arm (`absent` there, `null` here) and the written arm (`present` there, `a string` here) say the same thing on both sides. `BlueprintNavItemSchema.label` is unchanged. The strict side adds the closing steer the triage direction names. - Shape unchanged: still `z.string().nullable()`, so the schema accepts and refuses the same blueprints. - A lockstep comment sits above the line, mirroring the one on `viewName`. - One pin in `packages/spec/src/ai/solution-blueprint.test.ts`, placed in the `strict mirror ↔ lenient schema — key parity` block right after the `viewName` pin (`states ONE label rule on both sides …`). It reads both nav `label` describes the same way the `viewName` pin reads both nav shapes. It extracts what each side's empty spelling and written spelling mean, and asserts the two sides say the same thing. It pins sameness, not wording, so both sides can be reworded together. - `.changeset/21248-strict-nav-label-describe.md`: `@objectstack/spec` `patch`, `Clause-②: no`. ⛔ No applier-side text matching. ⛔ No new gate. ## Premise checks (on `origin/main` `4e6dc2338a`) 1. **Describe locations hold.** Strict `:372` read `'Nav entry label, or null'`; the lenient twin is at `:189`. 2. **Render-time inheritance holds at objectui's `.objectui-sha` pin `31971ff1e28f`.** In `packages/layout/src/NavigationRenderer.tsx`, `resolveNavItemLabel` returns `inheritedNavItemLabel(item, targetLabel)` when `item.label === undefined`. The fallback order is: the view's label (when the entry names a labelled view), then the object's or dashboard's label, then the machine name. The label is asked of the host's metadata on every render, so "a renamed target shows its new name on the next render". A present string renders verbatim. The new describe states only that. The strict `null` reaches the renderer as an absent label through the null strip that this file's own strict-mirror header documents ("the blueprint tools strip those nulls"). 3. **The `viewName` lockstep pin is a KEY-parity pin, not a describe pin.** It consists of `carries viewName …` and `the NAV ITEM schemas carry exactly the same keys`. No describe-text pin existed before. The new pin follows its style and sits beside it. ## Generated artefacts After `pnpm --filter @objectstack/spec build`, `pnpm --filter @objectstack/spec check:generated` reported all 15 artefacts up to date. No tracked artefact carries this describe. The reference page renders `SolutionBlueprintStrict.app` only one level deep (`nav` shows as `object[]`), so the strict nav item's describe was never on `content/docs/references/ai/solution-blueprint.mdx`, before or after this change. The only copy is the gitignored `packages/spec/json-schema/ai/SolutionBlueprintStrict.json`, which carries the new text after the build. ## Verification (final head `f7fee5be0e`) - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ai/solution-blueprint.test.ts`: 44 passed (43 before, plus the new pin). - `pnpm --filter @objectstack/spec test`: exit 0. Test Files 597 passed (597); Tests 17475 passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck`: exit 0. `check:test-typecheck: OK`, 52 test files compiled. - **Ablation (two legs, run after the fix was committed).** Both used `scripts/ablation-replace.mjs` in wrap mode, which verifies the write on disk and restores against HEAD. The test imports `./solution-blueprint.zod` by relative path, so no `dist/` sits on the resolution path and no rebuild was needed. - A1 reverted the strict describe to `'Nav entry label, or null'`. The pin went red with `expected undefined to be defined` (1 failed, 43 passed). The file was restored: blob `9fad23cd1314` equals HEAD and `git diff HEAD` is empty. - A2 drifted the lenient clause from `absent ⇒ the entry inherits the CURRENT label` to `absent ⇒ the entry copies the target label`. The pin went red with `expected { …(2) } to deeply equal { …(2) }`. The file was restored the same way. - Expected direction: red. Observed direction: red on both legs. - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) derived 83 commands at `f7fee5be0e`. All 83 ran and exited 0. `--ran` reconciliation: 83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN. - On the first pass, three gates exited 3 with PREREQUISITE NOT MET: `check:doc-formula-expressions`, `check:lean-entry-closure` and `check:dual-build-cjs-loads`. Each went green once its prerequisites were built (formula and lint; the objectql closure; the workspace build). The final pass ran with those builds in place. - The derivation printed STALE TREE: `origin/main` moved 15 commits after the branch point. Of the files it changed that the derivation reads, `ci.yml` only adds `env:` to the Build Core build step. `scripts/pm/issue-transfer.mjs` and `scripts/docs-audit/handwritten-docs.json` touch none of this diff's paths. - **Lint, narrowed (a measurement, not a full run).** `pnpm exec eslint --no-inline-config --format json` over the two touched `.ts` files: 2 files, 0 errors, 0 warnings. ESLint's own `isPathIgnored` returns false for both. `calculateConfigForFile` shows neither `parserOptions.project` nor `projectService`, so type-aware linting is off and this diff cannot change any verdict on an untouched file. The full `pnpm lint` is CI's. - **`test:repo` (the spec `repo` vitest project), narrowed.** I ran the 4 of its 48 files that read the blueprint source or describes. `scripts/solution-blueprint-header-row.test.ts`: 4 passed. `scripts/escape-mdx.test.ts`, `scripts/query-pointer-row.test.ts` and `src/api/rest-api-config-dead-keys-retirement.test.ts`: 39 passed. - NOT MEASURED: `scripts/build-schemas-check-mode.test.ts` and the full `test:repo` project. Reason: that file spawns `build-schemas.ts` repeatedly, and neither run finished inside a 480 s bound on the shared box. Left to CI. - Not run locally, left to CI: Build Core, Test Core, Dogfood, Temporal Conformance and the workspace type-check lanes. ## Acceptance notes - Observation, not filed: `StrictNavItem.viewName`'s describe, which this PR leaves unchanged, illustrates its rule with entries named by their labels ("a 「工单列表」 entry with viewName null plus a 「工单看板」 entry …"). This is a labelled example on the same nav item the model fills, and the source report counts labelled examples among the inputs that steer the model toward writing labels. No wrong answer has been measured from it. carrier: the cloud seat's golden-journey measurement, after cloud's pin carries this change. - Downstream, and not part of this PR: cloud's half measures how many nav entries a golden-journey build leaves null. Generated artefacts touched: none tracked (see above). Diff: 3 files, describe text, one test and one changeset. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7526058 commit 1c52a5e

3 files changed

Lines changed: 51 additions & 1 deletion

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the strict blueprint nav item's `label` describe says `null` inherits the target's current label
6+
7+
Clause-②: no
8+
9+
`SolutionBlueprintStrictSchema` is the output contract the AI design step generates against, and
10+
strict mode makes every nav entry's `label` a required decision. Its describe read only "Nav entry
11+
label, or null", so nothing the model reads said which of the two choices follows a rename of the
12+
target, and the model was steered toward writing one. The describe now states the lenient
13+
`BlueprintNavItemSchema.label` rule in the strict spelling: `null` ⇒ the entry inherits the CURRENT
14+
label of what it opens at render time (a renamed target shows its new name); a string ⇒ rendered
15+
verbatim, never a copy of the target's label. Write a label only when the entry must read
16+
differently from what it opens.
17+
18+
Describe text only: the key stays `z.string().nullable()`, so the schema accepts and refuses the
19+
same blueprints. A pin holds the lenient and strict `label` describes to one rule.

‎packages/spec/src/ai/solution-blueprint.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,26 @@ describe('strict mirror ↔ lenient schema — key parity', () => {
593593
expect(strictNavKeys).toContain('viewName');
594594
});
595595

596+
it('states ONE `label` rule on both sides: the empty spelling inherits, a written label is verbatim', () => {
597+
// cloud#2021. The lenient describe said what an absent label means; the
598+
// strict mirror — the describe the design model actually reads — said only
599+
// "or null", so nothing on the generating side told the model that null is
600+
// the choice that follows a rename of the target. Each side spells "empty"
601+
// its own way (`absent` / `null`) and "written" its own way (`present` /
602+
// `a string`); what each spelling MEANS must be the same text on both.
603+
const navLabelDescribe = (schema: any): string =>
604+
schema.shape.app.unwrap().shape.nav.unwrap().element.shape.label.description;
605+
const rule = (describe: string) => ({
606+
empty: describe.match(/\b(?:absent|null) ⇒ ([^;]+);/)?.[1],
607+
written: describe.match(/\b(?:present|a string) ⇒ ([^.]+)\./)?.[1],
608+
});
609+
const lenient = rule(navLabelDescribe(SolutionBlueprintSchema));
610+
const strict = rule(navLabelDescribe(SolutionBlueprintStrictSchema));
611+
expect(strict.empty).toBeDefined();
612+
expect(strict.written).toBeDefined();
613+
expect(strict).toEqual(lenient);
614+
});
615+
596616
it('round-trips a list + board pair on ONE object through the lenient schema', () => {
597617
const parsed = SolutionBlueprintSchema.parse({
598618
summary: 's',

‎packages/spec/src/ai/solution-blueprint.zod.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,18 @@ const StrictDashboard = z.object({
369369
const StrictNavItem = z.object({
370370
type: z.enum(['object', 'dashboard']).describe('What this nav entry opens'),
371371
target: strictIdent('Object or dashboard machine name to surface (snake_case)'),
372-
label: z.string().nullable().describe('Nav entry label, or null'),
372+
// ⛔ Must state the SAME rule as the lenient `BlueprintNavItemSchema.label`
373+
// (cloud#2021). THIS describe is the one the design model reads:
374+
// `propose_blueprint`'s structured output is generated against this mirror,
375+
// and strict mode makes `label` required, so the model decides on every
376+
// entry. The blueprint tools strip its `null` to an absent label, which the
377+
// renderer resolves to the target's CURRENT label on every render; a written
378+
// string is rendered verbatim and never follows a rename. A describe that
379+
// does not say which choice inherits steers the model to write a label on
380+
// every entry. The nav `label` rule pin in `solution-blueprint.test.ts`
381+
// (beside the `viewName` key-parity pin) fails if the two drift.
382+
label: z.string().nullable()
383+
.describe('Nav entry label, or null. null ⇒ the entry inherits the CURRENT label of what it opens at render time (a renamed target shows its new name); a string ⇒ rendered verbatim, so never copy the target\'s label in as a default. Write a label ONLY when the entry must read differently from what it opens; otherwise null.'),
373384
icon: z.string().nullable().describe('Lucide icon name, or null'),
374385
// ⛔ Must stay in lockstep with the lenient `BlueprintNavItemSchema.viewName`
375386
// (cloud#2150). THIS side is the one that decides whether the design step can

0 commit comments

Comments
 (0)