Skip to content

feat(spec/ui): export navItemVisibleDataNeedsDataSource, so a mirror of NavigationItemSchema attaches it by identifier - #24628

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24534-export-nav-visible-data-check
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24534-export-nav-visible-data-check

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24534

Clause-②: yes (widening)

What this does

@objectstack/spec/ui now exports navItemVisibleDataNeedsDataSource. That is the refinement NavigationItemSchema already runs: it refuses a navigation entry with no dataSource whose visible reads data. It is exported the same way as objectNavTargetExclusivity, checkPageRegionPlacement and checkPageMainRegionWidth, so a hand-written mirror of the navigation item can chain the spec's own function instead of restating the rule.

  • packages/spec/src/ui/app.zod.ts: the declaration gains export, and its docblock gains the sibling paragraph (what it judges, the issue it raises, who attaches it) After the seat's answer A (comment 6273673166), the function also reads an authored bare-string visible. The string goes through EvaluatedExpressionInputSchema, the slot's own schema, via a module-private authoredVisibleEnvelope, so the shorthand normalisation is reused and never restated. The docblock says the function accepts both spellings. The .superRefine(navItemVisibleDataNeedsDataSource) on the union inside NavigationItemSchema is unchanged.
  • packages/spec/src/ui/app-nav-visible-data-needs-data-source-export.test.ts: the export pin (5 legs, 36 tests, below).
  • packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json: regenerated by gen:api-surface (it reported touched: ui.json) and gen:export-origins (1 shard(s) rewritten). Each gains one row. No other shard moved.
  • .changeset/24534-export-nav-visible-data-check.md: minor on @objectstack/spec, Clause-②: yes (widening). A widening carries no ADR-0087 disposition marker, and check-adr-0087-registration is green without one.

Nothing an author can write is accepted or refused differently. The issue is the same: custom at visible, with the same message. The existing rule pins in app-nav-visible-data-needs-data-source.test.ts are untouched and green.

Premises

P1 holds. On origin/main 5a871e4e41:

  • packages/spec/src/ui/app.zod.ts:1041 declares function navItemVisibleDataNeedsDataSource(item: unknown, ctx: z.RefinementCtx): void with no export.
  • :1107 attaches it with .superRefine(navItemVisibleDataNeedsDataSource) on the discriminated union that NavigationItemSchema's z.lazy returns.
  • The siblings are export function checkPageRegionPlacement( at page.zod.ts:1766 and export function checkPageMainRegionWidth( at :1846.

They reach @objectstack/spec/ui this way:

  • the barrel src/ui/index.ts has export * from './app.zod' (:21) and export * from './page.zod' (:54);
  • package.json exports["./ui"] maps to dist/ui/index.{mjs,js,d.mts,d.ts};
  • each export has one row in api-surface/ui.json and one in export-origins/ui.json.

So adding export is the whole route.

Measured after the build, from packages/core (a consumer that resolves the workspace package through its exports):

  • ESM: import('@objectstack/spec/ui') gives typeof navItemVisibleDataNeedsDataSource === 'function', length 2, resolved to packages/spec/dist/ui/index.mjs;
  • CJS: require('@objectstack/spec/ui') gives function, from dist/ui/index.js;
  • the emitted declaration is declare function navItemVisibleDataNeedsDataSource(item: unknown, ctx: z.RefinementCtx): void;.

P2 holds for the signature, but not for what a mirror gets when it chains the function as it is.

  • Signature. (item: unknown, ctx: z.RefinementCtx) => void is what a superRefine callback takes. The ctx type is the same one objectNavTargetExclusivity(item, ctx) takes, and objectui's NavigationItemSchema already chains that function. I read objectui origin/main 50ba1bb0a6, packages/types/src/zod/app.zod.ts. That code follows objectui#7715's rule: re-attach the spec's object-level checks by identifier. The function needs no state a mirror would have to supply. Its CEL reader (celReadsRootData / lexCel) is closed over in the module.

  • Behaviour. The function judges the entry after the spec's parse, where visible is the { dialect, source } envelope that EvaluatedExpressionInputSchema produces. Measured on the card's entry, { id: 'nav_envs', type: 'object', objectName: 'sys_environment', visible: 'data.total == 1' }:

    • the door (NavigationItemSchema.safeParse) refuses it with one custom issue at visible;
    • the export called on the same entry with visible parsed ({ dialect: 'cel', source: 'data.total == 1' }) raises the identical issue: code, path and message;
    • the export called on the entry as written (bare-string visible) raises nothing.
  • objectui's mirror. It declares visible: z.union([z.boolean(), z.string()]).optional(), so its parsed visible is still the authored string. Chaining this export there as it is would refuse nothing on the card's entry.

  • What this PR does about it. The first head (50c3ba8a4a) wrote only the export and raised the difference as an open question. The seat answered A (comment 6273673166 on this PR): the function also reads the authored bare string, normalised through EvaluatedExpressionInputSchema (the slot's own schema), so the spelling a mirror hands it is judged exactly as the spec's parse would judge it. Measured on e058ce508e:

    • the card's entry as written now draws the door's identical issue (code, path and message) from a direct call;
    • the same bare string with a dataSource draws nothing;
    • a boolean or absent visible judges nothing.

    NavigationItemSchema accepts and refuses exactly what it did, because its own check only ever sees the parsed envelope. The schema pins in app-nav-visible-data-needs-data-source.test.ts are unchanged and green (23 passed).

The pin (app-nav-visible-data-needs-data-source-export.test.ts, 36 tests)

  1. Parity. Over 8 fixtures (4 refusing branches: object, dashboard, url, group; 4 accepting paths: the card's entry with a dataSource, an ambient-root predicate, a member named data, and no visible), the direct call and NavigationItemSchema.safeParse agree issue for issue. These fixtures write visible as the envelope, the one spelling whose parsed value equals what was written.
  2. Mount. The union the lazy schema resolves to carries exactly one custom check, and its issue vector over the matrix equals the export's.
  3. Attachment by identifier. The source declares export function navItemVisibleDataNeedsDataSource( once and chains .superRefine(navItemVisibleDataNeedsDataSource) once, on a code line.
  4. Barrel identity. ./index exports the same function object, with arity 2.
  5. Both spellings.
    • The card's bare-string entry is refused by the door.
    • The export raises the door's identical issue on the entry's parsed visible and on the entry as written.
    • Over the leg-1 matrix rewritten with bare strings (7 rows: 4 refusing, 3 accepting), the direct call and the door agree issue for issue.
    • The control (the same bare string with a dataSource) draws nothing from the door or the export.
    • A boolean or absent visible judges nothing.

Ablation (reverse verification), at 42945c2f43 with the implementation committed. The mutation was export function navItemVisibleDataNeedsDataSource( → /* ablated: export removed */ function navItemVisibleDataNeedsDataSource(, applied through node scripts/ablation-replace.mjs:

  • landed on disk: anchor 1 → 0, blob 8755bfe0b8eb → 07a6fe68bd65;
  • result: 22 failed / 5 passed (27), red as predicted;
  • restored: blob == HEAD 8755bfe0b8eb, and git diff HEAD is empty.

The test imports ./app.zod and ./index relatively (src, not dist), so the ablation needed no rebuild. The first attempt was a no-op: the replacement text was a substring of the anchor, and the tool refused before it ran anything. It was re-run with a distinct marker. (That reading is at the first head, where the pin had 27 tests.)

Ablation of the bare-string read, at e058ce508e with the patch committed. The mutation was const visible = (typeof entry.visible === 'string' → const visible = (/* ablated: bare-string read removed */ false && typeof entry.visible === 'string', through ablation-replace.mjs:

  • landed on disk: anchor 1 → 0, blob 74d3de955a29 → c459f2d5767d;
  • result, running this file with app-nav-visible-data-needs-data-source.test.ts: 5 failed / 54 passed (59);
  • exactly the new legs went red: the as-written leg and the 4 refusing rows of the bare-string matrix;
  • the accepting rows, the controls and the whole schema pin file stayed green;
  • restored: blob == HEAD 74d3de955a29, and git diff HEAD is empty.

Tests and gates (final head e058ce508e)

origin/main c3954057fe was merged at 50c3ba8a4a through scripts/pm/os-regen-merge.sh. Main's later commits touch no file this PR touches, so it was not merged again.

  • pnpm --filter @objectstack/spec test: 702 files passed, 21348 tests passed, 1 todo.
  • pnpm --filter @objectstack/spec typecheck: tsc --noEmit, check:scripts-typecheck and check:test-typecheck all pass. The test-typecheck debt is unchanged at 52 files / 242 errors / 132 pinned signatures, so the new test adds no errors.
  • The new pin file alone: 36 passed. The schema pin app-nav-visible-data-needs-data-source.test.ts is unchanged since 50c3ba8a4a and passes 23.
  • pnpm --filter @objectstack/spec check:generated: All 14 generated artifacts are up to date, against the dist this head built. check:api-surface reports public API surface + factory signatures unchanged, and check:export-origins reports current.
  • The derived gate list (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, 89 commands, identical before and after the merge) was run on this head with each exit code captured before any pipe. All 89 exited 0. dispatch-gates --ran answers: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED (a DERIVED zero — all 89 recorded an exit code and none of them is 3). This includes check:api-surface, check:export-origins, check:entry-nameability, check:browser-reachable-entries, check:dual-source-exports, check:exported-any, check:docs, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:published-files, check:nul-bytes, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt. On both heads, three of them (check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure) first exited 3 (PREREQUISITE NOT MET, nothing measured) in a freshly created worktree. They exited 0 once their packages were built, and --ran records those exit-0 re-runs.
  • Local-only: check:dev-prereqs exit 0 and check:skill-frame-freshness exit 0.
  • CI check-runs on e058ce508e (40 runs), read when the patch report was written: every required context except Test Core (6/6) is green (Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Test Core (1/6) to (5/6)). Test Core (6/6) then went red on a runtime share-link case that timed out at vitest's 5000 ms default. That failure is not this PR's (seat CI note 6274396318) and is filed as Flaky on Test Core (6/6): packages/runtime share-links-refusal-order.test.ts › dispatcher twin › "[#23372] email audience: the refusal is the one an unknown token and a password-less email link answer" times out at vitest's 5000 ms default #24633. Its one re-run is in flight. The patch report on spec(ui): export navItemVisibleDataNeedsDataSource, so mirrors of NavigationItemSchema can attach it by identifier — objectui's NavigationItemSchema accepts a nav entry whose visible reads data with no dataSource, which the spec refuses #24534 cites the check-runs.

Acceptance notes

  • The open question, answered A by the seat (comment 6273673166). The function now reads an authored bare-string visible as well as the parsed envelope, so objectui's mirror can attach the export as it is, by identifier. B (objectui restates the shorthand) and C (objectui takes EvaluatedExpressionInputSchema by reference) are not taken here. C stays objectui's own call for its boolean and envelope divergences.
  • Read-only observation, not filed. objectui's visible arm differs from the spec's in two more ways. It accepts visible: true / false, which the spec's EvaluatedExpressionInputSchema refuses. It refuses the { dialect, source } envelope, which the spec accepts. I read this from the source and did not measure it through objectui validate. It belongs to the objectui follow-up this card names (Done-when 2).

Generated by Claude Code

…ttaches it by identifier

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…igationItemSchema attaches

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
… add the changeset

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 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 973917c5795f7a440d46a15bc5cf8961af9d8703 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 973917c5795f7a440d46a15bc5cf8961af9d8703

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24628 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T17:54Z

REWORK (patch round 1, same claim 6271491852, same os-dev subagent) at head 50c3ba8a4abde423f365d3ced95300dbe7361e05, on the dev's open_questions[0] (report 6273629677 on #24534). The contract review runs on the patched head, not this one.

item reading
Shape Draft, base main, assignee zhuangjianguo. Line 1 is Fixes #24534. Clause-②: yes (widening). 5 files, +308 / −1. Not governed. The changeset is minor on @objectstack/spec.
What holds navItemVisibleDataNeedsDataSource is exported the way its siblings are (src/ui/index.ts export *, exports["./ui"], the api-surface and export-origins ui shards). The schema keeps attaching the same function, and its issue code, path and message are unchanged. The spec suite and all 89 derived gates are green.
The gap the dev measured (P2) The function reads the spec's PARSED entry (visible as { dialect, source }). objectui's mirror keeps visible as the authored string, so attaching the export there as it stands refuses nothing on the card's own entry. The export alone does not meet the card's purpose, which is that the mirror closes the door by identifier.
Seat answer: A The function also reads a bare-string visible as CEL source, the spelling the spec's own shorthand normalises to the envelope. The four axes all point to A. Long term: one rule, in spec, attached by identifier (objectui#7715), with no restated normalisation in the mirror. AI error: attaching the export is enough by itself, with no silent no-op. Business need: the bare string is the dominant authored spelling (16 to 1 in examples/**, the dev's count). Scope: S, with no gate added. The accept set does not move: the schema's own refinement only ever sees the envelope, and the function is new in this PR, with no caller outside the schema. B restates the shorthand in objectui. C is objectui's own call for its boolean and envelope divergences, and stays out of this PR. Not escalated: it shapes how a new helper reads its input, inside the ruling (no change to what the schema accepts or refuses).
The patch One commit on this branch. (1) The bare-string branch beside the envelope read, normalised exactly as the spec's shorthand is (cite the normaliser). A boolean visible and an absent visible judge nothing, as today. (2) Pin leg 5 flips: the card's entry as written (bare-string visible, no dataSource) now draws the door's identical issue from a direct call. Add a control: the same bare string with a dataSource draws none. (3) The docblock and the changeset say the function accepts the authored and the parsed spelling. (4) The PR body records the answer. Fixes #24534 stays.
Accepted deviations (1) The ablation re-run with a distinct marker; that reading is the one cited. (2) The three gates that were PREREQUISITE NOT MET before the merge exit 0 after the builds. (3) The os-regen-merge.sh merge carries the script's own message.
Finding objectui's visible arm accepts visible: true and refuses the envelope; spec does the reverse. It is carried by the objectui follow-up this seat files when this lands (Done-when 2), as context for that card. Not filed separately.
Landing After the patch, a CONTRACT_REVIEW_TIER review on the new head (schema face, widening), then landing on a green head.

Generated by Claude Code

…re-string visible too

The export now normalises a bare-string visible through
EvaluatedExpressionInputSchema, the slot's own schema, so a mirror whose
visible stays the authored string chains it as it is. NavigationItemSchema
accepts and refuses exactly what it did: its own check only ever sees the
parsed envelope.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note (seat domain:spec#2, session session_019oFY6Hi35J7atvgVFnmoaK).

Test Core (6/6) is red on e058ce508e (run 39298888283, job 118219765573). One case failed:

Why this is not this PR's failure:

  • The diff adds export to one spec function and adds its pin, docs and generated rows. It changes nothing that @objectstack/runtime or @objectstack/plugin-sharing runs. The share-link resolve path never calls navItemVisibleDataNeedsDataSource.
  • The case did not fail an assertion. It ran out of time. In one test body it makes 18 sequential resolve calls over better-sqlite3: 2 references, plus 2 callers × 3 emails × 3 passwords, minus the 2 served rows.
  • The shard was heavily contended. The duration was 1081s, with 2733s of cumulative import time.
  • CI on main is green on the PR's base 65a8eeb7d4 and on 973917c579.
  • No open issue tracks this case.

Fix: none exists yet. The case's budget is vitest's default and it is not sized to its loop. That is the oversubscription class #19828 describes. The seat is filing the flake as its own card in the runtime lane, so this PR does not widen to packages/runtime. One re-run of the failed job follows.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e058ce508e78c913754deabacd401acb6d2e20c5
Local-runs: none
Scope: packages/spec/src/ui/app.zod.ts + packages/spec/src/ui/app-nav-visible-data-needs-data-source-export.test.ts, packages/spec/src/ui/app-nav-visible-data-needs-data-source.test.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, .changeset/24534-export-nav-visible-data-check.md, packages/spec/src/shared/expression.zod.ts, packages/spec/src/shared/refinement-projection.ts, packages/spec/src/ui/index.ts, packages/spec/src/ui/page.zod.ts, packages/spec/src/migrations/entries/semantic/18.nav-item-visible-data-needs-data-source.ts, objectui packages/types/src/zod/app.zod.ts, objectui packages/types/src/tests/navigation-spec-parity.test.ts

Read: card #24534 (body and all six comments, 6267689111 through 6274643516), PR #24628 (body, file list, and the net diff of five files against the merge base c3954057fe), objectui at the brief's pin 5e15928b46, at the committed .objectui-sha 4016370af3 and at objectui origin/main 51269f0fdb, and the check-runs on this head. Nothing built, run or re-run. The seat records on this PR were read as seat records; every judgment below is from the diff and the code.

① Derived judgments

  1. Public surface: one new function export, navItemVisibleDataNeedsDataSource(item: unknown, ctx: z.RefinementCtx): void on @objectstack/spec/ui. Right. app.zod.ts:1077 gains export; the barrel src/ui/index.ts:21 is export * from './app.zod'; package.json exports["./ui"] maps to dist/ui/index; the root src/index.ts re-exports only defineApp and the NavigationItemInput type from app.zod, so no root-shard row is owed. The same route as objectNavTargetExclusivity (app.zod.ts:607), checkPageRegionPlacement (page.zod.ts:1766) and checkPageMainRegionWidth (page.zod.ts:1846).
  2. No spec accept set moves. Proven from the code. git grep at this head finds the function in exactly three places in app.zod.ts: the {@link} at :369, the declaration at :1077 and the one mount at :1145, .superRefine(navItemVisibleDataNeedsDataSource) on the discriminated union inside NavigationItemSchema's z.lazy. A union-level check runs on the union's parsed payload, and the visible slot of every branch is EvaluatedExpressionInputSchema.optional() (app.zod.ts:371), whose string arm transforms to { dialect: 'cel', source } (expression.zod.ts:269-274). So the door hands the function undefined or an envelope, never a bare string; the new typeof entry.visible === 'string' branch (:1081-1083) is reachable only from a direct call. No other spec door attaches it, and no file outside app.zod.ts, its two pins, the two ui shards and the changeset names it. The existing rule pin app-nav-visible-data-needs-data-source.test.ts is byte-identical to the merge base and to origin/main (23 cases, bare-string and envelope spellings through every navigation door).
  3. Parity by construction. Holds. Both paths end in celReadsRootData(visible.source) on a source produced by the same schema object. EvaluatedExpressionInputSchema has one string arm (refine NON_BLANK_STRING, which is source.trim().length above zero, refinement-projection.ts:263-266, then the wrap to dialect: 'cel') and one envelope arm (EvaluatedExpressionSchema, any enum dialect with a non-blank source); no arm yields another dialect, trims, or transforms beyond the wrap. The case table: a non-blank string, whitespace-padded or non-CEL, wraps identically on both paths and is judged by the same lexer. A blank or whitespace-only string is refused at the slot as an aborting invalid_union on the door (expression.zod.ts:198-216, measured there), so the union-level check never runs; authoredVisibleEnvelope returns undefined on the direct call; nothing is judged on either path, and the docblock's "a blank string it refuses is judged by nothing here either" (app.zod.ts:1069-1070) matches the schema. An envelope with a blank source is a non-aborting custom at source on the door, the check runs, and celReadsRootData of a blank is false; the direct call reads the same envelope and answers the same. An ast-only envelope and a boolean are slot refusals on the door, and typeof visible.source !== 'string' or visible.dialect !== 'cel' returns on the direct call. A non-cel dialect envelope passes the slot and the function returns early on both paths.
  4. The export's direct-call behaviour on a bare-string visible: refused where the door refuses. Right, and inside the siblings' posture. checkPageRegionPlacement reads page.kind ?? 'full', the default the parse would apply (page.zod.ts:1770), so reading an authored spelling as the parse would is the established posture for an exported check. The normaliser is reused by reference (authoredVisibleEnvelope, app.zod.ts:1026-1029, EvaluatedExpressionInputSchema.safeParse), not restated, so this is not the lenient-consumer shape Prime Directive Add comprehensive test suite for Zod schema validation #12 forbids: the bare string is the spec's own authored shorthand, judged by the spec's own slot schema.
  5. Docblock, changeset and PR body. True, one precision note. Every clause was checked against the code: what is read (both spellings, cel only), what is judged (no dataSource and a root data read), what is raised (one custom at visible, the message at :1089-1093, unchanged from the merge base), the mount count, the D3 entry (migrations/entries/semantic/18.nav-item-visible-data-needs-data-source.ts:12), the sibling route, and "a mirror attaches it as it is". Precision note, not a defect: the changeset's closing "refuses what the spec refuses" is true of this rule; the spec's slot-level refusals of a boolean, a blank string and an ast-only envelope are not this function's, and a mirror whose slot is boolean | string does not inherit them (③ 3).
  6. The pin (36 cases). Legs 1 to 5 test what they claim. Leg 1: eight shape-valid fixtures (four refusing branches, four accepting paths) with visible as the envelope, direct call versus NavigationItemSchema.safeParse, issue for issue; runParse throws on any non-custom issue, so agreement cannot be vacuous. Leg 2: the lazy union carries exactly one custom check and its issue vector equals the export's, with an all-empty vector excluded. Leg 3: declarations() anchors on a line-start (export )?function name and attachments() drops lines opening with * or //, so both are code-line counts; at this head the attachment is the code line :1145 and the declaration :1077, once each. Leg 4: ./index reference identity and arity 2. Leg 5: the card's entry as written draws the door's issue from a direct call; the seven-row bare-string rewrite of the leg-1 matrix agrees with the door issue for issue; the dataSource control draws nothing; a boolean or absent visible draws nothing. Ablations, read not run: removing export leaves the named import undefined under vitest's module transform, so every leg that calls the function throws, and the five survivors are leg 1's vacuity case, leg 2's count, leg 3's attachment count and leg 5's two door-only cases, consistent with the dev's 22 failed / 5 passed at the 27-case head. Removing the bare-string read makes the direct call see the raw string and return at visible.dialect !== 'cel', so exactly the as-written case and the four refusing bare-string rows go red: the dev's 5 failed / 54 passed over both files (36 + 23 = 59). A hand-rolled { dialect: 'cel', source } in place of the schema call is NOT distinguishable by this pin, nor by any output-based test today: the two differ only on a blank string, and a blank string reads no data either way. The reuse is a source-level fact (:1027), not a pinned one (③ 4).
  7. Generated shards. Right. api-surface/ui.json:543 gains "navItemVisibleDataNeedsDataSource (function)" and export-origins/ui.json:528 gains "navItemVisibleDataNeedsDataSource": "src/ui/app.zod.ts#navItemVisibleDataNeedsDataSource (function)", each in alphabetical position between misplacedDefaultPageRefusal and normalizeFilterOperator, matching the exported name, kind and origin file. git diff --name-only over packages/spec/api-surface and packages/spec/export-origins against the merge base lists exactly those two files. TypeScript Type Check, the job that hosts check:api-surface and check:generated, is green on this head.

② Semver level

minor on @objectstack/spec. Right: a new public function export widens the published surface, no schema accepts or refuses anything new (① 2), nothing is removed or renamed, and the function had no public contract before this PR, so its direct-call reading of a bare string is part of what this PR publishes, not a change to anything a consumer held. The Clause-②: line reads yes (widening) on both carriers, the changeset (line 7) and the PR body (line 3), and that is the right arm: yes takes at least minor, and (widening) is the arm a surface addition takes. A widening carries no ADR-0087 disposition marker: check-adr-0087-registration.mjs judges only a changeset that declares a breaking change (its rule section), and this one is minor. Check Changeset is green on this head on both of its runs.

③ Boundary flags

  1. The dispatch's "no change to what the refinement accepts or refuses" against the patch. The seat's answer A (6273673166) changed what the function refuses on a direct call, not what any schema refuses. Judged in scope: the card's Done-when 1 is "the same way it exports checkPageRegionPlacement and checkPageMainRegionWidth", those exports already read the authored spelling as the parse would (① 4), and the spec's accept set is unmoved (① 2). Answered.
  2. Card closure. Fixes #24534 is right. Done-when 1 is met in full by this diff (① 1, ① 7). Done-when 2 is, in the card's own words, an objectui follow-up "filed there when this lands", an act that follows the landing, which the seat's record 6273673166 takes on. Flag for the seat: post the follow-up's number on the closed card when it is filed, so Done-when 2 has a receipt on the card itself. The guard "The card this PR closes must claim this branch" is green.
  3. The out-of-scope finding (objectui's visible arm). Confirmed, and it needs a card of its own in objectui. At the brief's pin 5e15928b46, at the committed .objectui-sha 4016370af3 (the mirror file is byte-identical between those two objectui commits) and at objectui origin/main 51269f0fdb, packages/types/src/zod/app.zod.ts declares visible: z.union([z.boolean(), z.string()]).optional() on the flat NavigationItemObject (line 255 at both pins, 256 at main) and chains objectNavTargetExclusivity inside that object's superRefine under if (item.type === 'object') (line 327 at the pins, 335 at main). Against the spec's EvaluatedExpressionInputSchema.optional() (app.zod.ts:371): objectui accepts visible: true, visible: false and a blank string, all three invalid_union at visible on the spec door; objectui refuses the { dialect, source } envelope, which the spec accepts. The boolean half is a recorded, deliberately kept objectui divergence, pinned at packages/types/src/__tests__/navigation-spec-parity.test.ts:175 at both the pin and main ("menuItemToNavigationItem MANUFACTURES one when it inverts AppMenuItem.hidden") and acknowledged in the mirror's own docblock (main :425-439); the envelope half is in no pinned list, so it is a new, unpinned divergence, and objectui's two arms already disagree on a boolean (the doc arm is judged by the spec's arm and refuses it, :298). A reading, not measured through objectui validate. Where it belongs: the Done-when 2 card is an attach-by-identifier at the next pin bump (S); closing these divergences is the dev's option C, a narrowing of objectui's published validator with its own Clause-② ruling there (M). Carrying it only as context inside the attach card is the burying Prime Directive chore: version packages #10 names: file it as its own objectui card, cross-linked from the attach card. No effect on this PR's verdict.
  4. Pin gap, accepted. The reuse of EvaluatedExpressionInputSchema in the bare-string branch is not pinned (① 6). Leg 5's bare-string parity would catch a future normaliser change only on a fixture that exercises it, and none today does. Acceptable because what the legs pin is the behaviour contract (parity with the door), and it holds on every fixture; a one-line source-text leg on EvaluatedExpressionInputSchema.safeParse( inside the function would close it, at the seat's option, not a blocker.
  5. Dev deviations (6273629677, 6274643516). The ablation re-run with a distinct marker; the three PREREQUISITE NOT MET gates re-run after builds; the os-regen-merge.sh merge commit 50c3ba8a4a carrying the script's own message (the sanctioned form for that commit); the worktree re-created at the remote head without a second merge (origin/main moved c3954057fe to 973917c579 with no change to any file this PR touches or reads, verified by git diff --name-only over those paths); and the cosmetic leg-5 labels. All read, none bears on the contract. Accepted as the seat accepted them.
  6. Check-runs on this head, read at 2026-10-19T18:59Z. Required contexts: Lint & Repo Gates success, TypeScript Type Check success, Build Core success, Governed Surface Queue Guard success, Test Core (1/6) to (5/6) success. Test Core (6/6), job 118235895669: attempt 1 red on packages/runtime/src/domains/share-links-refusal-order.test.ts:356 at vitest's 5000 ms default (seat CI note 6274396318; filed as Flaky on Test Core (6/6): packages/runtime share-links-refusal-order.test.ts › dispatcher twin › "[#23372] email audience: the refusal is the one an unknown token and a password-less email link answer" times out at vitest's 5000 ms default #24633, open, domain:cli, a test-budget flake outside this diff, which touches nothing packages/runtime runs); attempt 2 in_progress with no conclusion at this read. Its conclusion is the gate's verdict and is not pre-empted here: the landing waits for it. Non-required: Check Changeset success (both runs), Spec property liveness success, Dogfood Regression Gate success, Flag docs affected by code changes success, Check Documentation Links success, Check PR Size success, Auto Label success, the three claim and closure guards success, every Lint · and Type Check · lane success; Console Pin Gate, Build Docs, Temporal Conformance, Dogfood Verify CLI, the dogfood matrix and the packed-tarball smoke skipped.
  7. Read-only shape held. No worktree, checkout, build, test, gate or ablation was run; the ablation readings above are the dev's, cross-checked by reading the pin's control flow.

Implemented-by: claude/issue-24534-export-nav-visible-data-check
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T19:01Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 19:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 19:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 35450f0 Oct 19, 2026
62 of 64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24534-export-nav-visible-data-check branch October 19, 2026 20:00
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:ui size/m tests tooling

Projects

None yet

2 participants