From 60b13d7cd978dfe269c7da98083d3a9156483459 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 02:19:16 +0000 Subject: [PATCH 1/3] fix(spec): the strict blueprint nav item's label describe states the null-inherits rule StrictNavItem.label read only "Nav entry label, or null", so the design model that generates against this mirror was never told that null is the choice that inherits the target's CURRENT label and follows renames. It now states the lenient twin's rule in the strict spelling, and a pin beside the viewName lockstep pin holds the two describes to one rule. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- .../spec/src/ai/solution-blueprint.test.ts | 20 +++++++++++++++++++ .../spec/src/ai/solution-blueprint.zod.ts | 12 ++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/ai/solution-blueprint.test.ts b/packages/spec/src/ai/solution-blueprint.test.ts index 2be0ee655bb..e63bde02f5f 100644 --- a/packages/spec/src/ai/solution-blueprint.test.ts +++ b/packages/spec/src/ai/solution-blueprint.test.ts @@ -593,6 +593,26 @@ describe('strict mirror ↔ lenient schema — key parity', () => { expect(strictNavKeys).toContain('viewName'); }); + it('states ONE `label` rule on both sides: the empty spelling inherits, a written label is verbatim', () => { + // cloud#2021. The lenient describe said what an absent label means; the + // strict mirror — the describe the design model actually reads — said only + // "or null", so nothing on the generating side told the model that null is + // the choice that follows a rename of the target. Each side spells "empty" + // its own way (`absent` / `null`) and "written" its own way (`present` / + // `a string`); what each spelling MEANS must be the same text on both. + const navLabelDescribe = (schema: any): string => + schema.shape.app.unwrap().shape.nav.unwrap().element.shape.label.description; + const rule = (describe: string) => ({ + empty: describe.match(/\b(?:absent|null) ⇒ ([^;]+);/)?.[1], + written: describe.match(/\b(?:present|a string) ⇒ ([^.]+)\./)?.[1], + }); + const lenient = rule(navLabelDescribe(SolutionBlueprintSchema)); + const strict = rule(navLabelDescribe(SolutionBlueprintStrictSchema)); + expect(strict.empty).toBeDefined(); + expect(strict.written).toBeDefined(); + expect(strict).toEqual(lenient); + }); + it('round-trips a list + board pair on ONE object through the lenient schema', () => { const parsed = SolutionBlueprintSchema.parse({ summary: 's', diff --git a/packages/spec/src/ai/solution-blueprint.zod.ts b/packages/spec/src/ai/solution-blueprint.zod.ts index fa064e5cc7c..9fad23cd131 100644 --- a/packages/spec/src/ai/solution-blueprint.zod.ts +++ b/packages/spec/src/ai/solution-blueprint.zod.ts @@ -369,7 +369,17 @@ const StrictDashboard = z.object({ const StrictNavItem = z.object({ type: z.enum(['object', 'dashboard']).describe('What this nav entry opens'), target: strictIdent('Object or dashboard machine name to surface (snake_case)'), - label: z.string().nullable().describe('Nav entry label, or null'), + // ⛔ Must state the SAME rule as the lenient `BlueprintNavItemSchema.label` + // (cloud#2021). THIS describe is the one the design model reads: + // `propose_blueprint`'s structured output is generated against this mirror, + // and strict mode makes `label` required, so the model decides on every + // entry. The blueprint tools strip its `null` to an absent label, which the + // renderer resolves to the target's CURRENT label on every render; a written + // string is rendered verbatim and never follows a rename. A describe that + // does not say which choice inherits steers the model to write a label on + // every entry. The nav label-rule pin below fails if the two drift. + label: z.string().nullable() + .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.'), icon: z.string().nullable().describe('Lucide icon name, or null'), // ⛔ Must stay in lockstep with the lenient `BlueprintNavItemSchema.viewName` // (cloud#2150). THIS side is the one that decides whether the design step can From 5ba57d0349763584342f4618e2096ff5d5769f56 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 02:29:42 +0000 Subject: [PATCH 2/3] chore(changeset): spec patch for the strict nav label describe Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- .changeset/21248-strict-nav-label-describe.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/21248-strict-nav-label-describe.md diff --git a/.changeset/21248-strict-nav-label-describe.md b/.changeset/21248-strict-nav-label-describe.md new file mode 100644 index 00000000000..8e3e384cd6a --- /dev/null +++ b/.changeset/21248-strict-nav-label-describe.md @@ -0,0 +1,19 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): the strict blueprint nav item's `label` describe says `null` inherits the target's current label + +Clause-②: no + +`SolutionBlueprintStrictSchema` is the output contract the AI design step generates against, and +strict mode makes every nav entry's `label` a required decision. Its describe read only "Nav entry +label, or null", so nothing the model reads said which of the two choices follows a rename of the +target, and the model was steered toward writing one. The describe now states the lenient +`BlueprintNavItemSchema.label` rule in the strict spelling: `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, never a copy of the target's label. Write a label only when the entry must read +differently from what it opens. + +Describe text only: the key stays `z.string().nullable()`, so the schema accepts and refuses the +same blueprints. A pin holds the lenient and strict `label` describes to one rule. From f7fee5be0ec20b5124a5261fb1260d154a3e65c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 03:19:01 +0000 Subject: [PATCH 3/3] docs(spec): point the strict nav label lockstep comment at the pin's real file Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- packages/spec/src/ai/solution-blueprint.zod.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/ai/solution-blueprint.zod.ts b/packages/spec/src/ai/solution-blueprint.zod.ts index 9fad23cd131..6ec5a0076a6 100644 --- a/packages/spec/src/ai/solution-blueprint.zod.ts +++ b/packages/spec/src/ai/solution-blueprint.zod.ts @@ -377,7 +377,8 @@ const StrictNavItem = z.object({ // renderer resolves to the target's CURRENT label on every render; a written // string is rendered verbatim and never follows a rename. A describe that // does not say which choice inherits steers the model to write a label on - // every entry. The nav label-rule pin below fails if the two drift. + // every entry. The nav `label` rule pin in `solution-blueprint.test.ts` + // (beside the `viewName` key-parity pin) fails if the two drift. label: z.string().nullable() .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.'), icon: z.string().nullable().describe('Lucide icon name, or null'),