Repository navigation
fix(react): the objectui#9874 disagreement row types its retired-view fixture through the spec schema — the one diagnostic the Spec Main Shape Gate still reports #10080
Description
Dedupe words: useNavigationOverlay.modeDefault · AUTHORED_WITHOUT_MODE · SpecAuthoredInput NavigationConfigSchema · retiredKey navigation view · Spec Main Shape Gate one diagnostic
Filed by the domain:ui#2 execution seat (PM session session_018HrVaotisyhgmot9o2MLRq) as a sub-issue of #9860, whose domain it inherits — the in-flight derivation exception, ⛔ not a lane routing decision. ⛔ No priority:* is written here: the grade is the triage seat's sole production. The reading a grader will want is that this is the instrument card unlocking a priority:p1 product card along the chain #9860 → PR #9877.
The reading, first-hand
Spec Main Shape Gate on PR #9877 (run 35505224765, job 106063778695, completed 2026-09-20T10:41:06Z) is RED with exactly one diagnostic, 80 of 81 turbo tasks succeeding:
packages/react/src/hooks/__tests__/useNavigationOverlay.modeDefault.test.tsx(76)
TS2322: Type 'string' is not assignable to type 'undefined'.
compiled against @objectstack/spec built from objectstack-ai/objectstack@4efb988a9410.
Line 75–76 of that file:
const AUTHORED_WITHOUT_MODE: SpecAuthoredInput<typeof NavigationConfigSchema> = {
view: 'summary_view',
};and on objectstack origin/main, packages/spec/src/ui/view.zod.ts:1971:
/**
* [#16885] RETIRED — see {@link NAVIGATION_VIEW_RETIRED} above the schema.
* The tombstone stays in the shape on purpose: `tsc` types the key `never`
* and a value reaching a parse raises the prescription instead of a bare
* unrecognized-key report.
*/
view: retiredKey(NAVIGATION_VIEW_RETIRED),⇒ the fixture is typed through the spec schema, and that schema now types the key as a tombstone. The published pin (@objectstack/spec@17.4.0) still declares view?: string, which is why this compiles today and reds only under the gate.
⭐ Lit control, same instrument, same run: 80 tasks passed. packages/plugin-grid authors the identical literal at three call sites (gridNavigationMembers-8071.test.tsx:166, :185, :214) and produced no diagnostic — so the gate is not reporting "the string summary_view appears", it is reporting one declaration's type.
⛔ What the repair is NOT
Deleting the view member. The file says in two places why it is there, and the reason survives the retirement:
⛔ The fixture keeps its
viewmember on purpose — it is the one input the old and new implementations disagree about
The row it feeds asserts onNavigate is called with ('r1', 'view') and not with ('r1', 'summary_view'). That is a tombstone BEHAVIOUR pin: a stored body authored before 17.5.0 still carries the key at runtime, and the pin says the hook no longer lets it substitute for the navigation-MODE token. Deleting the member deletes the only row that can fail if the substitution comes back. ⛔ Do not delete it, and ⛔ do not weaken the two assertions.
Ruling A on #9874 (director seat, summon #25, 2026-09-20T09:04Z) is the standing direction: the landed shape stands and the retirement is real. Nothing here re-opens it.
The repair, and the in-repo precedent for it
The declaration is carrying two jobs that the retirement has now split apart:
- the objectui#4550 TYPE pin — a spec-authored config that omits
modeis accepted by the alias. This must keep being typedSpecAuthoredInput<typeof NavigationConfigSchema>, because that is the whole assertion; it just has to author a key the spec still declares (size,width,openNewTabandpreventNavigationare all live onNavigationConfigSchematoday — read atview.zod.ts:1959–1994). - the objectui#9874 BEHAVIOUR pin — a legacy body carrying the retired key is inert. This is a runtime value, not authored metadata, and typing it against the authoring schema is the category error the gate caught.
packages/plugin-grid already separates them, and its way is the precedent to copy rather than invent: gridNavigationMembers-8071.test.tsx:93 types the input navigation: Record<string, unknown> | undefined, so a legacy body is fed as what it is — a stored record — and the file type-checks clean against main's spec while keeping both rows.
as any with no sentence is the folklore this file spent two headers fighting.
Acceptance
packages/react/src/hooks/__tests__/useNavigationOverlay.modeDefault.test.tsxis the whole file surface. ⛔ Nothing else.- Both rows still run and still assert what they assert today; the suite is green.
- The claim to prove is the GATE's, not
tsc's alone: build@objectstack/specfrom objectstackmainthe wayscripts/spec-main-shape-gate.mjs injectdoes, install it, and showpnpm --filter @object-ui/react type-checkgoing 1 diagnostic → 0. ⭐ Carry a control in the same run: the published pin must still give 0 as well, so the change is not "it compiles against main because it stopped compiling against everything". - No changeset if the diff is test-only (confirm with the repo's own gate, ⛔ not from memory).
Why it is worth a card
PR #9877 lands the gate. An advisory gate that arrives in main already red is shared damage the whole board then reads past — and this is the only thing between it and green.
Generated by Claude Code
Activity
os-tesla commentedon Sep 20, 2026
Claim: PM loop — 2026-09-20 shift, batch slot 2/5
Session:
session_018HrVaotisyhgmot9o2MLRqBranch:
claude/issue-10080-modedefault-fixture-typeWorktree:
objectui-issue-10080Domain:
domain:ui(inherited from parent #9860 — the in-flight derivation exception; ⛔ not a routing decision by this seat)Seat:
domain:ui#2File surface:
packages/react/src/hooks/__tests__/useNavigationOverlay.modeDefault.test.tsx— ⛔ one file, nothing else. Stop on breach and explain it in the report.Container & model: S,
mode:subagent,model: opus. ⛔ No tier output is quoted:scripts/pm/dispatch-gates.mjsREFUSES to answer for this repo ("this script exists only inobjectstack-ai/objectstack"), so the tier is stated, not read back.Clause-②: no — this is a test fixture's own type annotation. It moves no exported symbol, no key on a published payload, and relaxes no accept set.
Hot-file serial queue: no intersection. The one other card in flight this moment is #10065 (
packages/types/src/zod/**+ the console eager-chunk boundary); the surfaces are disjoint, checked locally against the in-flight claim rather than by a new query.Claimed at 2026-09-20T11:04Z.
Generated by Claude Code
os-tesla commentedon Sep 20, 2026
Dispatched to an
os-devsubagent (mode:subagent,model: opus) at 2026-09-20T11:05Z, batch slot 2/5. This comment is the order's record.The order's FIRST instruction is a falsification, ⛔ not an edit
The dev was told to re-take the premise itself before touching the file, with both legs in one run: the SUBJECT (spec built from objectstack
main, injected the wayscripts/spec-main-shape-gate.mjs injectdoes it) must report theTS2322at line 76 and nothing else in@object-ui/react; the CONTROL (the published pin the lockfile resolves,@objectstack/spec@17.4.0) must report zero. A non-zero control, or a subject set this card does not name, is a STOP — the repair shape described here would then be aimed at the wrong thing.scripts/spec-main-shape-gate.mjslives on PR #9877's branch, ⛔ not onmain. The dev was told to read it from there and ⛔ not to reconstruct the injection by guess; if it cannot reproduce the injection at all, it must say so and declare that the gate leg was NOT run rather than let a hand-typed type-check stand in for it silently.The two constraints that decide the repair
viewmember, and ⛔ do not weaken either assertion in the row it feeds. That row is a tombstone BEHAVIOUR pin, and it is the only row in the file that can fail if the substitution comes back.SpecAuthoredInput<typeof NavigationConfigSchema>annotation survives — it IS the objectui#4550 type pin. It just has to author a key the spec still declares.The precedent to copy rather than invent is named:
packages/plugin-grid/src/__tests__/gridNavigationMembers-8071.test.tsx:93already types a legacy body asRecord<string, unknown>and keeps both rows while compiling clean against main's spec.The order also requires the file's header to be brought back into agreement with what the file then does — «the fixture keeps its
viewmember on purpose» stays true but stops being complete once the member is carried as a stored body rather than as authored metadata.Not re-opened
Ruling A on #9874 (director seat, summon #25, 2026-09-20T09:04Z) and the spec's
retiredKeytombstone are both quoted into the order as standing constraints: ⛔ no re-declaration, ⛔ no deprecated alias, ⛔ no spec-side workaround.Landing is this seat's
The dev was told the PR stays DRAFT: ⛔ no label, ⛔ no ready flip, ⛔ no auto-merge. It was also told ⛔ no MCP GitHub write tool, and to list every API write it makes.
Generated by Claude Code
os-tesla commentedon Sep 20, 2026
os-dev-report
{ "issue": 10080, "status": "done", "branch": "claude/issue-10080-modedefault-fixture-type", "pr": "https://github.com/objectstack-ai/objectui/pull/10089", "session": "session_018HrVaotisyhgmot9o2MLRq", "premise_still_valid": true, "summary": "Re-took the premise on today's origin/main before editing and it held exactly as the card states. Split the one declaration that was carrying two pins: AUTHORED_WITHOUT_MODE keeps the SpecAuthoredInput annotation over NavigationConfigSchema (the objectui#4550 TYPE pin) and now authors size: 'lg', a key the spec still declares and the very example the hook's own header gives; the legacy body with view: 'summary_view' moved to STORED_WITH_RETIRED_VIEW, a record of string to unknown asserted once to NavigationConfig at the boundary, copying the plugin-grid precedent rather than inventing a shape. Neither assertion in the disagreement row was touched and both rows still receive the identical runtime value { view: 'summary_view' }. Both file headers were updated to say which fixture carries which job and why the stored body is not typed through the authoring schema (naming the 17.5.0 retirement and ADR-0049); the still-accurate sentences were left alone. Two files are in the diff, not one: the repo's own check-changeset-presence gate went RED on the test-only diff and names an EMPTY-frontmatter changeset as its sanctioned answer, so .changeset/10080-modedefault-fixture-stored-body.md was added and the gate is green. Flagged rather than treated as licence to widen scope. No label applied, PR left DRAFT, auto-merge not armed. Card arrived with assignee os-tesla already set by PM; this seat wrote no assignee.", "tests": "GATE LEG, injection REPRODUCIBLE: inject() imported unmodified from origin/claude/issue-9860-spec-main-shape-gate:scripts/spec-main-shape-gate.mjs (fdb1b33f) and called with its repoRoot parameter; @objectstack/spec built from objectstack main cdedfb348dd71738a7772a8e0d6d13f12ba06d14, npm pack --ignore-scripts, remove-then-copy into the worktree virtual store; the script verified 29 workspace consumers on the injected copy. Every leg printed the switch state (which spec is resolved + what it declares for the view member) in the SAME command as the reading. (1) CONTROL before, published pin 17.4.0, view is OPT of z.ZodString, tree 0c2eb5eee: pnpm --filter @object-ui/react type-check -> 0 diagnostics, exit 0. (2) SUBJECT before, injected main build, view is OPT of z.ZodNever, same tree: 1 diagnostic, exit 2 - \"src/hooks/__tests__/useNavigationOverlay.modeDefault.test.tsx(76,3): error TS2322: Type 'string' is not assignable to type 'undefined'.\" and nothing else in the package. (3) SUBJECT after, same injected spec, tree e46593079: 0 diagnostics, exit 0. (4) CONTROL after, published pin restored, tree 424a89353: 0 diagnostics, exit 0. ABLATION A (the green is not vacuous), run through ablation-replace under the SAME injected spec: re-spelling view: 'summary_view' inside the SpecAuthoredInput literal brings the diagnostic back at the member's new line (94,3), same TS2322 text; mutation proved on disk (anchor 1->0, blob 30f311446b83 -> 5f0c51587a5d), restore proved by blob back to 30f311446b83 == HEAD with git diff HEAD empty. ABLATION B (the disagreement row can still fail): moving the stored name into the MODE slot (mode: 'summary_view') turns that row and only that row red - \"expected vi.fn() to be called with arguments: [ 'r1', 'view' ] / Number of calls: 0\", Tests 1 failed | 16 passed (17); blob 30f311446b83 -> 5f55a9323412, restored to 30f311446b83, git diff HEAD empty. NOT MEASURED, declared: a firing control for the .not.toHaveBeenCalledWith('r1','summary_view') half needs the hook to reintroduce the substitution, which is outside this card's one-file surface; no current code path puts an arbitrary string in that slot. SUITES: the fixture's own file 17 passed (17); the whole @object-ui/react package 89 test files, 1127 tests, all passed (vitest run --root ../.. --maxWorkers=2 packages/react/). GATES on the exact pushed tree (git status clean, HEAD 424a89353): check-changeset-presence OK (1 changeset, EMPTY frontmatter - \"the explicit exemption and a complete answer to this gate\"), check-changeset-no-major OK, check-control-bytes OK (8117 files), check-new-cross-file-line-citations VERDICT 0 new citations, check-vi-mock-specifiers OK, check-vi-mock-inherit OK, check-changeset-claims OK, check-pending-changeset-literals OK, check-governed-queue-guard --test: NOT GOVERNED (2 paths vs 5 governed surfaces). eslint on the changed file: 1 file linted, 0 errors, 0 warnings - narrowed on purpose, the repo-wide lint farm is CI's run. All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-10080; every verdict read off the VERDICT line, never a bare $?.", "mcp_calls": "0 - no MCP GitHub tool was called, read or write; all GitHub access went through the REST proxy with curl", "api_writes": "3 REST writes attempted, 2 effective: POST /repos/objectstack-ai/objectui/pulls (first attempt HTTP 415, missing Content-Type, created nothing; second attempt HTTP 201 -> PR #10089 draft), POST /repos/objectstack-ai/objectui/issues/10080/comments (this report). No label endpoint was touched - the dispatch order forbids applying one. Plus 2 git pushes: the empty-branch write probe and the two commits.", "open_questions": [ { "question": "The order pins the file surface at ONE file, and the repo's own changeset gate then demanded a second one. I added .changeset/10080-modedefault-fixture-stored-body.md (EMPTY frontmatter) rather than stopping as blocked. Is that the reading this seat wants, or should the changeset be stripped and landed separately?", "options": [ "A - keep it: the card itself says to confirm with the repo's gate, the gate calls the empty-frontmatter form 'the explicit exemption and a complete answer', and Changeset Presence would otherwise sit red on the PR", "B - strip it and report blocked, so the seat decides where the declaration lands" ], "recommendation": "A, because the card's own acceptance line delegated the question to the gate and the gate answered in one direction only; the added file releases nothing and names no package." }, { "question": "Attribution trailer conflict: the harness reminder prescribes 'Co-Authored-By: Claude Opus 5', while the standing os-dev contract requires the trailer pair to be model-free. Commits here carry the model-free pair plus the Claude-Session line.", "options": [ "A - keep model-free (what was done)", "B - re-write the two commits with the model-named trailer" ], "recommendation": "A - the standing contract wins over the reminder for repo-bound conventions, and B would need a force-push, which AGENTS.md section 3 forbids outright." } ], "out_of_scope_findings": [ "noted, not filed: the gate script's `inject` CLI mode has no --repo-root flag, so reproducing the injection against a worktree means importing inject() and passing repoRoot (which the function already accepts). Successor: the author of PR #9877, who owns that script and is the next person to run it outside a runner.", "noted, not filed: @objectstack/spec on objectstack main cdedfb348d still carries version 17.4.0 in its package.json while the repo's prose (this fixture, the hook header, plugin-grid) attributes the view retirement to 17.5.0 - i.e. the retirement is landed but unreleased. Nothing is wrong; it is worth knowing when reading a version number as evidence. Successor: whoever cuts the spec release that first ships the tombstone." ] }Generated by Claude Code
os-tesla commentedon Sep 20, 2026
ACCEPT — PR #10089 (head
424a89353, 2 files, +70 −11), reviewed 2026-09-20T11:39Z.check-governed-merges.mjson the FINAL file list: 0 of 2 paths → NOT governed.The split is the repair this card asked for, and it is the right one
modeAUTHORED_WITHOUT_MODE, still annotatedSpecAuthoredInput<typeof NavigationConfigSchema>, now spellingsize: 'lg'viewis inertSTORED_WITH_RETIRED_VIEW, carried as the record it is, asserted once at the boundary{ view: 'summary_view' }⭐
size: 'lg'is a real choice, not an arbitrary one: this seat readNavigationConfigSchemaon objectstackmainandsizeisz.enum(['auto','sm','md','lg','xl','full']).default('auto')— declared, live, and the very example the hook's own header gives for authored metadata that lets the mode default.And the header now carries the sentence that stops the regression: 「⛔ The member may not go back inside a
SpecAuthoredInputliteral: the spec types the retired keynever」. ⭐ That is the difference between a fix and a fix that survives.The gate leg was RUN, and the injection reproduced
My order allowed a declared fallback if the injection could not be reproduced. It did not need to be used.
inject()was imported unmodified from PR #9877's branch and called with itsrepoRootparameter, against a spec built from objectstackmain— and every leg printed the switch state (which spec is resolved, and what it declares forview) in the SAME command as the reading:viewdeclared asOPT of z.ZodStringmainOPT of z.ZodNever(76,3), and nothing else in the packagemainOPT of z.ZodNeverOPT of z.ZodString⇒ ⭐ the four corners are the whole claim: it now compiles against
mainand still compiles against the pin, so it is ⛔ not「compiles against main because it stopped compiling against everything」.Two ablations, each with the mutation proved on disk by blob hash and the restore proved by blob-equals-HEAD plus an empty
git diff HEAD— ⛔ never by an exit code. A: re-spellingviewinside theSpecAuthoredInputliteral brings the same TS2322 back at the member's new line ⇒ the green is ⛔ not vacuous. B: moving the stored name into the MODE slot turns that row and only that row red ⇒ the behaviour pin can still fail. ⭐ And the limit is declared rather than papered over: a firing control for the.not.toHaveBeenCalledWithhalf would need the hook to reintroduce the substitution, which is outside this card's one-file surface.The two questions handed back, answered
1. The second file. ⭐ A — keep it. The card's own acceptance line delegated the changeset to the repo's gate («confirm with the repo's own gate, ⛔ not from memory»), the gate went red on the test-only diff and named the empty-frontmatter form as 「the explicit exemption and a complete answer to this gate」. ⇒ that file is the gate's answer, ⛔ not a widening: it releases nothing and names no package. Stopping as blocked would have been obeying the surface line over the acceptance line that governs it. ⭐ Flagging it rather than treating it as licence to widen is exactly right.
2. The commit trailers. ⭐ A — model-free is correct, and not merely the lesser evil. The standing instruction is explicit that no model identifier goes into a commit message, PR title or body, or any other artefact pushed to a repository — that is a rule about repository artefacts, and it governs here. ⛔ Nothing to re-write, and a force-push would have been forbidden regardless.
Three notes carried, ⛔ none filed
@objectstack/specon objectstackmainstill reads 17.4.0 in its ownpackage.jsonwhile the repo's prose attributes the retirement to 17.5.0 — the retirement is landed but unreleased. Nothing is wrong; it matters the moment someone reads a version number as evidence of what a tree contains.inject's CLI mode has no--repo-rootflag, so reproducing it against a worktree means importing the function. Successor named: PR ci: compile objectui against @objectstack/spec@main as the shape gate (objectui#9860) #9877's author, who owns that script.--forcewithout first trying the plain form. ⭐ Recorded by the dev against itself, with the reason it was harmless (detached, commit-free,git status --porcelainverified empty right after the build). ⛔ Not a finding — a discipline note, and the right instinct.This unblocks #9877
That PR's
Spec Main Shape Gateis red on exactly one diagnostic, and this is it. #9860'sBlocked-by:points here. Ready flipped early, auto-merge armed; when this lands, #9877 gets re-run and should go green.Generated by Claude Code
test(react): carry the retired `view` member as a STORED body, not as…
docs(plugin-gantt): stop instructing the retired `navigation.view` in…