Skip to content

feat(spec): a blueprint nav entry can name the VIEW it opens (cloud#2150) - #16841

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-2150-blueprint-nav-viewname
Sep 8, 2026
Merged

hotlong merged 2 commits into
mainfrom
claude/issue-2150-blueprint-nav-viewname

Conversation

@hotlong

@hotlong hotlong commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

What

BlueprintNavItemSchema gets an optional viewName, and the OpenAI-strict mirror's StrictNavItem gets it in lockstep (required-but-nullable, per the strict convention).

Why

Until now the blueprint nav shape could only say which object an entry opens. A model that had just designed a kanban and wanted it in the app menu therefore had exactly one move left: emit a second entry at the same target, with the intent carried by label/icon alone.

Measured downstream (objectstack-ai/cloud#2150, a real tenant DB):

[0] {"id": "nav_frez_ticket", "label": "工单列表", "icon": "list",   "type": "object", "objectName": "frez_ticket"}
[1] {"id": "nav_frez_ticket", "label": "工单看板", "icon": "kanban", "type": "object", "objectName": "frez_ticket"}

Two identical ids, both opening the object's default view. The kanban view (frez_ticket.ticket_status_board) really was built — the menu just could not reach it. And nothing complained: the target object exists, so the dangling-target lint had nothing to say.

The runtime could always express this — ObjectNavItemSchema.viewName is 'Default list view to open. Defaults to "all". …' — so the gap was the blueprint's alone, and the duplicate entry was the reasonable move under the expressiveness the model was given.

Both halves, deliberately

A key on the lenient side only is a fix on no path the product runs: propose_blueprint validates its structured output against the mirror, so the design step could never author a board entry while apply_blueprint accepted a key nothing emits. That is the seedData divergence (cloud#2118) with a different key name.

Ablation — reverting only the StrictNavItem half and re-running packages/spec/src/ai/solution-blueprint.test.ts:

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/ai/solution-blueprint.test.ts > SolutionBlueprintStrictSchema (OpenAI strict mirror) > carries viewName on a strict nav item — nullable, and REQUIRED to be present (OpenAI strict)
 FAIL  src/ai/solution-blueprint.test.ts > SolutionBlueprintStrictSchema (OpenAI strict mirror) > accepts the QUALIFIED <object>.<view> spelling too — the applier normalizes, the mirror must not refuse
 FAIL  src/ai/solution-blueprint.test.ts > strict mirror ↔ lenient schema — key parity > the NAV ITEM schemas carry exactly the same keys
 FAIL  src/ai/solution-blueprint.test.ts > strict mirror ↔ lenient schema — key parity > carries `viewName`, so a menu entry can say WHICH view it opens
      Tests  4 failed | 39 passed (43)

Restored: 43 passed (43).

Why no SNAKE_CASE regex

viewName is deliberately un-regexed on both sides, unlike target. A view answers to two interchangeable spellings — the bare key a blueprint's views[].name carries, and the qualified <object>.<key> a staged view record's name carries — and the applier normalizes between them. Constraining the leaf would make one spelling legal to GENERATE and illegal to APPLY: the cloud#1967 shape, where an already-approved blueprint was refused wholesale at apply time. VALUE parity holds because neither side constrains it.

Parity pin widened

Third time the same gap was found one level further out: fields → objects → nav items. The nav-level key-parity test now fails on the next divergence in either direction.

Verification

  • packages/spec full suite: 13328 passed | 23 skipped (13351)
  • tsc --noEmit -p packages/spec: clean
  • check:authorable-surface, check:docs: in sync (both regenerated in this commit — authorable-surface/ai.json gains one key, the blueprint reference gains one row)

Companion

The cloud half — passing viewName through to the runtime nav entry, and the nav-id derivation change that lets one object legitimately carry two entries — is objectstack-ai/cloud#2156 (draft, and it pins this branch head until this one merges). Part of objectstack-ai/cloud#2150.

🤖 Generated with Claude Code

…150)

`BlueprintNavItemSchema` gets an optional `viewName`, and the OpenAI-strict
mirror's `StrictNavItem` gets it in lockstep (required-but-nullable, per the
strict convention).

Until now the shape could only say which OBJECT a menu entry opens. A model
that had just designed a kanban and wanted it in the app menu therefore had
exactly one move left: emit a SECOND entry at the same `target` and carry the
intent in `label`/`icon` alone. Both entries then opened the object's default
view, and the cloud applier derives a nav id from the target, so the two
collided — the user clicked 「工单看板」 and got the list. Nothing anywhere
complained: the target object really does exist, so the dangling-target lint
had nothing to say. The runtime nav item could always express this
(`ObjectNavItemSchema.viewName`, "Default list view to open"), so the gap was
the blueprint's alone and the duplicate entry was the reasonable move under
the expressiveness the model was given.

Both halves, deliberately. A key on the lenient side only is a fix on no path
the product runs: `propose_blueprint` validates its structured output against
the mirror, so the design step could never author a board entry while
`apply_blueprint` accepted a key nothing emits — the `seedData` divergence
(cloud#2118) with a different key name.

`viewName` is deliberately un-regexed on BOTH sides, unlike `target`: a view
answers to a bare key (`views[].name`) and to the qualified `<object>.<key>` a
staged view record carries, and the applier normalizes between them.
Constraining the leaf would make one spelling legal to generate and illegal to
apply — the cloud#1967 shape, where an already-approved blueprint was refused
wholesale.

The strict↔lenient key-parity pin is widened one level further out (fields →
objects → NAV ITEMS); ablating the mirror half fails four tests. Generated
authorable-surface and the blueprint reference are regenerated.

Part of objectstack-ai/cloud#2150.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ai tests tooling labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/build-with-claude-code.mdx (via viewName (symbol, a field of const object StrictNavItem))
  • content/docs/ui/apps.mdx (via viewName (symbol, a field of const object StrictNavItem))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via viewName (symbol, a field of const object StrictNavItem))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 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 c5ea982d971cb2550d35a845694a6cbc2575ea52 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bef7a30604c2beb4432db92bdcc5d9031e735170 — the merge of head 597696d1bb1ec6913c532e762a733bdaa9952c64 into base c5ea982d971cb2550d35a845694a6cbc2575ea52, 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 bef7a30604c2beb4432db92bdcc5d9031e735170 && git checkout bef7a30604c2beb4432db92bdcc5d9031e735170
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5ea982d971cb2550d35a845694a6cbc2575ea52 597696d1bb1ec6913c532e762a733bdaa9952c64 && git checkout -B drift-repro c5ea982d971cb2550d35a845694a6cbc2575ea52 && git merge --no-ff 597696d1bb1ec6913c532e762a733bdaa9952c64

node scripts/docs-audit/affected-docs.mjs --json c5ea982d971cb2550d35a845694a6cbc2575ea52

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5ea982d971cb2550d35a845694a6cbc2575ea52 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ope` (cloud#2150)

The empty-state gate refuses a permissive empty state nobody signed off on, and
the strict mirror's describe states one out loud ('or null for the object's
default list'). It is presentation breadth, not authorization: the entry's
`target` already fixes WHICH OBJECT is reachable, record- and field-level
security are enforced downstream, and the empty state is the behaviour every
existing app already has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hotlong
hotlong marked this pull request as ready for review September 8, 2026 11:27
@hotlong
hotlong enabled auto-merge September 8, 2026 11:27
@hotlong
hotlong added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 8333a6c Sep 8, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-2150-blueprint-nav-viewname branch September 8, 2026 11:52
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant