Skip to content

fix(spec): the strict blueprint nav item's label describe states that null inherits the target's current label - #21309

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21248-strict-nav-label-describe
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21248-strict-nav-label-describe

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

claude added 3 commits October 2, 2026 02:19
…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 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 393ae878d3d52fe843c56b4621c004b934dcf853 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 31cd87cc57285fa055a94535065385890e8d7b09 — the merge of head f7fee5be0ec20b5124a5261fb1260d154a3e65c6 into base 393ae878d3d52fe843c56b4621c004b934dcf853, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 31cd87cc57285fa055a94535065385890e8d7b09 && git checkout 31cd87cc57285fa055a94535065385890e8d7b09
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 393ae878d3d52fe843c56b4621c004b934dcf853 f7fee5be0ec20b5124a5261fb1260d154a3e65c6 && git checkout -B drift-repro 393ae878d3d52fe843c56b4621c004b934dcf853 && git merge --no-ff f7fee5be0ec20b5124a5261fb1260d154a3e65c6

node scripts/docs-audit/affected-docs.mjs --json 393ae878d3d52fe843c56b4621c004b934dcf853

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f7fee5be0ec20b5124a5261fb1260d154a3e65c6
Local-runs: none

Isolated reviewer for card #21248 / PR #21309. Inputs: the card body and its three comments (triage 5941498298, claim 5944357787, dev report 5945274529), the PR body and file list, the net diff against origin/main at this head (3 files, +51/-1), the check-runs on this head, and objectui's NavigationRenderer.tsx at the .objectui-sha pin 31971ff1e28f. Read-only: nothing built, run or re-run.

① Derived judgments

Accept set and public surface: unchanged. Judged right.

  • StrictNavItem.label stays z.string().nullable(); the diff on packages/spec/src/ai/solution-blueprint.zod.ts changes the .describe() text and adds a lockstep comment, nothing else. BlueprintNavItemSchema.label (z.string().optional()) is untouched. The JSON Schema the design model generates against differs only in that one description; no key, value domain or export moves. api-surface/data.json is rightly not touched: a describe is not a type.
  • No tracked artefact lifts the describe: git grep "Nav entry label, or null" origin/main hits the zod source only; packages/spec/json-schema/ is gitignored (.gitignore:63); content/docs/references/ai/solution-blueprint.mdx renders the strict app.nav inline as object[] | null (:367, :406), one level deep, so the strict nav item's describe was never on that page. The dev's "15 artefacts up to date" is consistent with this and was not re-run here.
  • The test file is not published surface; the changeset is release input. No limb of clause ② is hit, so the review is owed only because the diff sits on a published schema in packages/spec/src/**.

The describe is true at the renderer. Judged right.

  • Trace of a model null. The strict mirror's own header on origin/main (solution-blueprint.zod.ts, the "Strict structured-output mirror" block) states that the model emits null for empty fields and the blueprint tools strip those nulls before the lenient SolutionBlueprintSchema parses. On this side of the contract the strip is forced, not optional: the lenient label is z.string().optional() and refuses null, and the stored BaseNavItemSchema.label (packages/spec/src/ui/app.zod.ts) is I18nLabelSchema.optional() inside a strict object, which refuses null as well. A model null therefore has exactly two fates: stripped to an absent key, or refused wholesale with nothing stored. Neither is a present string and neither is an empty label. The cloud applier's source is not among this review's inputs; the card body records cloud#2563 as staging no label the author did not give.
  • At the pin, resolveNavItemLabel (packages/layout/src/NavigationRenderer.tsx:370-381) branches on item.label === undefined alone. Absent goes to inheritedNavItemLabel, which asks the host's targetLabel resolver on every render (the view's label when the entry names a view, else the object's or dashboard's label, else the machine name) and stores nothing, so a renamed target shows its new name on the next render. A present string goes to presentNavItemLabel, whose first line returns a string as-is. No text is matched against a name (objectui#11201). Both arms of the new describe hold.
  • One nuance, not a defect and not new: rung 1 of the stored schema's order, the id-keyed translateApp bundle entry applied at the /meta boundary, can replace a present label by its id. It never matches text, a first AI build has no bundle, and the lenient twin uses the same word "verbatim"; for a model-facing describe it is the right steer.

The two describes say one rule, and the pin holds sameness. Judged right.

  • Lenient: absent ⇒ inherits the CURRENT label of what it opens at render time (a renamed target shows its new name); present ⇒ rendered verbatim, so never copy the target's label in as a default. Strict: null ⇒ the same text; a string ⇒ the same text; plus the closing steer "Write a label ONLY when the entry must read differently from what it opens; otherwise null." The arm texts are byte-equal; only the spelling of empty/written differs, by design. Nothing in the strict text contradicts the lenient twin.
  • The pin (solution-blueprint.test.ts:596-614) extracts each side's two arms with one regex per arm (absent|null ⇒ … ; and present|a string ⇒ … .), asserts the strict arms are defined, and deep-equals strict to lenient. Read against the dev's two ablations as described: A1 (strict back to the old text) leaves the strict empty arm undefined, red on toBeDefined; A2 (lenient arm reworded alone) is red on the deep-equal. A harmless rewording of both arms together, or of any framing text outside the arms, stays green; a lenient-only loss of its arms is red through the deep-equal, so the pin cannot pass vacuously. What it freezes is the arm grammar (the ⇒ glyph, the ; between arms, the . after the written arm): a side that drops that grammar reads red in the fail-closed direction, and the test's comment states what each spelling means. That is a pin on the rule, not on wording. Its extraction path through the lazy proxies works: lazySchema's get trap forwards shape, unwrap and element to the real instance.
  • The lockstep comment above the line names cloud#2021, states the strip and the render-time rule, and points at the pin's real file (corrected in the head commit).

Steering. Judged right.

  • Read as a prompt, the text names which choice inherits, when a label is warranted, and ends with an explicit default ("otherwise null"), which is what should raise the null rate. It closes the two wrong doors the card names: "never copy the target's label in as a default" (a copied label) and "otherwise null" (an empty string is a string, and nothing invites one). z.string() accepting '' is pre-existing on both twins and outside this card. No phrasing contradicts the lenient twin. The describe carries no tracker number (check:doc-authoring).

No applier-side text matching, no new gate. Verified on the diff: no applier lives in this repo and the diff touches no script or workflow; the pin is a unit test in the package's own suite, which the triage direction itself asked for.

② Semver level

@objectstack/spec patch, Clause-②: no, no arm. Judged right.

  • The act is a describe text change on a published schema: the accept set is neither widened nor narrowed and no export moves, so clause ② (「本卡放宽接受集或扩大公开面吗」) is no, and with no narrowing there is no arm. clause2-line.mjs reads exactly this spelling; check-changeset-no-major's level axis reads no as not-declared and stands down; check-adr-0087-registration has no breaking arm to register. The Check Changeset check-run on this head concluded success.
  • patch, not skip-changeset: the string ships in the npm tarball (the compiled schema the design call is generated against), and the "WHICH LEVEL" prose in pr-automation.yml says a fix( that changes no public surface stays patch.
  • The changeset prose states what ships truthfully: the new rule in the strict spelling, "the key stays z.string().nullable()", the pin, and why the strict describe is the one the model reads (the file's own header: this mirror is used only as the structured-output contract). Its premise that the old text steered the model toward writing a label is the card's recorded source measurement (cloud dev report 5941123027), not a new claim. The body carries no tracker number.

③ Boundary flags

Dev deviations, each answered:

  1. "The viewName lockstep pin is a key-parity pin, not a describe pin." Right. On origin/main the two viewName pins (solution-blueprint.test.ts:442, :581) assert key presence on both shapes; no describe-text pin existed. The new pin sits in the same key-parity block right after them, so the claim's placement holds, and the extraction style is the right new tool.
  2. Narrowed test:repo: scripts/build-schemas-check-mode.test.ts and the full spec repo project NOT MEASURED locally, declared to CI. In CI the spec repo project runs under Test Core (ci.yml:755, pnpm turbo run test test:repo across the six shards) and the full pnpm lint under Lint & Repo Gates (lint.yml:425). Their conclusions on this head are in the check-run line at the end of this record; a running gate is not reported as passed.
  3. No merge of origin/main, 15 commits past the branch point. Verified against the trees: git diff --name-only 4e6dc2338a origin/main lists 568 paths; outside the .changeset/* files that chore: version packages consumed, the packages/spec paths touched are CHANGELOG.md, api-surface/data.json, export-origins/data.json, package.json and sources under src/data, src/identity, src/migrations, src/security and src/ui. None under packages/spec/src/ai/, none under content/docs/references/ai/, and this PR's changeset path is absent on main. The PR is mergeable and CI tests the merge ref. No overlap.
  4. Commit trailers use the model-free pair. The repo's rule (no model identifier in a GitHub artefact) outranks the harness reminder; not a contract matter, nothing owed.
  5. Labels: labeler-added only, no skip-changeset. Right, since a changeset exists.

open_questions: none declared, none found.

out_of_scope_findings (1): StrictNavItem.viewName's describe illustrates its rule with label-named entries; carrier "the cloud seat's golden-journey measurement". Accepted as an acceptance note; no card owed now. Prime Directive #10's filing gate takes a reproducible defect, a contract violation or an authoring trap, each with its evidence. Here there is no measured wrong answer, the example is an illustration rather than a rule, the same example sits on the lenient twin (:192) under the cloud#2150 lockstep, and the card's own Acceptance already assigns the golden-journey measurement to cloud's half. Escalation to the measuring seat: if that measurement attributes written labels to this example, it files then, with the measurement as the evidence and the dev's dedupe words. Nothing is filed from this review.

Independence: the diff was produced by the dev subagent on the branch below; this record is rendered by an isolated reviewer and adopted by the dispatching session named below.

Implemented-by: claude/issue-21248-strict-nav-label-describe
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

Check-runs on this head at the final read (2026-10-02T04:13Z): 35 check-runs, all concluded; 32 success, 3 skipped by path (Build Docs, Console Pin Gate, Packed-tarball smoke), none failed, none running. The two families ③.2 left NOT MEASURED locally concluded success in CI: Lint & Repo Gates (the full pnpm lint) and Test Core shards 1/6 through 6/6 plus the Test Core aggregate (the spec repo vitest project, build-schemas-check-mode.test.ts included). Also success: Build Core, Dogfood Regression Gate 1/3 to 3/3 and aggregate, Dogfood Verify CLI, Temporal Conformance, the four Type Check lanes and their aggregate, Check Changeset, Governed Surface Queue Guard, Spec property liveness, and the PR-automation guards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/s tests tooling

Projects

None yet

2 participants