Skip to content

feat(spec)!: a page region named main takes only width: 'full' - #24362

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24326-main-region-width
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24326-main-region-width

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24326

Clause-②: yes (narrowing)

The seat's ruling (route: refuse) is carried out here. A page region named main now takes only width: 'full'. 'small', 'medium' and 'large' on main are refused at publish, with a located message. 'full' keeps its page-wide meaning, and every other region accepts the same widths as before.

What changes

Door Before Now
definePage / PageSchema parse main with width: 'large' parses refused at regions.N.width (code: 'custom'), naming the region and 'full'
defineStack (os validate, os build) accepted STACK_SCHEMA_INVALID 422 at pages.N.regions.M.width
metadata save door accepted 422 INVALID_METADATA (the same PageSchema parse)
a stored sys_metadata page row, an artifact inside its declared-floor window, a package body registered with its declared range accepted converted on read: the width is deleted, with one OS_METADATA_CONVERTED notice per site, and the page renders as before
width: 'full' on main; any width on sidebar, aside, header, footer or a dashboard cell accepted unchanged
  • packages/spec/src/ui/page.zod.ts: a new exported check, checkPageMainRegionWidth. It is attached to PageSchema by identifier, after checkPageRecordBindingPlacement, so every door that runs checkPageRegionPlacement runs it too. It judges every page kind and template, because no layout reads a non-'full' width on main on any of them. It matches the region name case-insensitively, as the renderer does (name.toLowerCase() === 'main'). PageRegionSchema.width gains a .describe() that states the rule.
  • The message, as it ships: "Region main is refused with width: 'large' (since @objectstack/spec 18): a main region takes only width: 'full'. It is drawn at the width its template leaves it — beside sidebar and aside it takes the space they leave, and on dashboard it is one grid cell — so 'small', 'medium' and 'large' change nothing there. 'full' is page-wide: the page drops its max-width cap and fills the screen. Delete width to keep the page exactly as it renders, or write width: 'full' for a full-width page."
  • ADR-0087 D3: packages/spec/src/migrations/entries/semantic/18.page-main-region-width-refused.ts.
  • ADR-0087 D2, the data-at-rest half: page-main-region-width-removed in packages/spec/src/conversions/registry.ts. It sets toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.7.0' and order: 69. It is inserted at its id-sorted position and defined directly above pageRequiresNonCompiledKindRemoved, the entry after it. It is a pure lossless delete, scoped exactly as the refusal is.
  • Generated: packages/spec/api-surface/ui.json and packages/spec/export-origins/ui.json (one added export each), and content/docs/references/ui/page.mdx (the new width description). All three came from check:generated's stale list, regenerated by its own commands.

Why the D2 entry is in this PR (outside the claim's listed file surface)

The claim lists one semantic entry. The D2 conversion lives in packages/spec/src/conversions/registry.ts, and its pins live in a new conversions/page-main-region-width-removed.test.ts and in a JOIN row in shared/retired-key-removal-version.pin.test.ts. Those three paths are not on the claim's list. They are here because the narrowing alone breaks data at rest:

  • Every page with a main region in packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json declares width: 'large' (5 of its 8 pages). Without a conversion, the strict body parse in resolveArtifactPackageOrder refuses them. That is the door the whole option-B artifact case in registry-installed-package-retired-page-declarations.test.ts drives. It is the same kernel-build stop that the isDefault and full-width narrowing had to fix after it landed.
  • The delete is lossless (no layout read the value), so this is the precedent's own shape: a D3 entry plus a retired D2 entry that data at rest replays. Dispatch note G1 named "a conversion that drops a non-'full' width from main" as the possible owed entry.

Premises (verified before code)

  • P1: true. At the console pin .objectui-sha 0707b4ae3712, objectui packages/components/src/renderers/layout/page.tsx draws main as flex-1 min-w-0. That happens in RegionLayout (:434, used by the default template and by every template-less page type), in HeaderSidebarMainTemplate (:507) and in ThreeColumnTemplate (:541). DashboardTemplate draws main as one grid cell with no width class (:574). The only read of a main region's width is PageRenderer's full-bleed, for 'full' (:889–:894). It removes the max-width cap. Re-read at objectui origin/main 43efd078aa, as fetched in this container: the same lines.
  • P2: true. git grep -n "width" 0707b4ae37 -- packages | grep -i main gives 21 hits. Every hit outside page.tsx is a test, a CHANGELOG, a comment, or an unrelated width (Gantt geometry, mobile copy). A wider read of region width readers at the pin (region.width, main.width and similar) finds page.tsx (header/footer via getRegionWidthClass, sidebar/aside via getSidebarWidthClass, and the main full-bleed === 'full') and one display-only label in Studio's PageBlockCanvas.tsx:760. That label prints the value and does not lay anything out. No renderer honours small, medium or large on main.
  • P3: true. PageRegionSchema.width is z.enum(['small','medium','large','full']) (page.zod.ts:49), shared by every region. The refusal is a PageSchema-level check keyed on the region's name, so PageRegionSchema itself accepts exactly what it did, and so does every other region. findRegion matches case-insensitively (r.name?.toLowerCase() === name.toLowerCase(), page.tsx:90), and so does the full-bleed read. The check uses the same toLowerCase() comparison against 'main'.

In-repo sites that declared a non-'full' width on main (G2)

Each change drops width: 'large' from main and nothing else. The page renders the same. None was changed to 'full', which would make the page full-bleed.

  • examples/app-crm/src/pages/welcome.page.ts
  • examples/app-showcase/src/ui/pages/contact-form.page.ts, index.ts, my-work.page.ts, new-project-wizard.page.ts, page-variables.page.ts, project-workspace.page.ts
  • packages/mcp/src/connect-ui.ts (the shipped Connect an Agent page) and packages/cloud-connection/src/cloud-connection-ui.ts (the shipped Cloud Connection page). Both are published, so both carry a patch in the changeset.
  • content/docs/ui/pages.mdx: three snippets, one of them os:check, plus the width row of the region table, which now states the rule.
  • packages/spec/src/ui/page.test.ts: the two parse fixtures (:187, :377).
  • Left as is: packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json. It is a byte-identical published artifact, and the D2 entry converts it at the data-at-rest doors.
  • Searched with no hit: skills/**. In content/docs/protocol/objectui/layout-dsl.mdx, the multiline search matched a sidebar width only.

Tests

  • packages/spec/src/ui/page-default-and-region-placement.test.ts, new block "a region named main takes only width: 'full'":
    • one refusal per refused value on a three-column main, asserting path regions.2.width, code: 'custom', the message's first sentence, 'full', @objectstack/spec 18, and no tracker number;
    • the case-insensitive match (Main);
    • the refusal on a template-less page, on header-sidebar-main, on dashboard, on a slotted page and on an html page;
    • the stack door: { code: 'STACK_SCHEMA_INVALID', status: 422 } at pages.1.regions.1.width;
    • controls: 'full' on main is accepted with its value kept; large on header, every width on sidebar and aside, small on a dashboard cell, and a main with no width are all accepted;
    • the ledger: the D3 entry is in step 18, and a stored row with a large main converts on read with the named notice and then parses.
  • packages/spec/src/ui/object-refinement-check-exports.test.ts: checkPageMainRegionWidth joins the catalogue with its own failure matrix (parity across three runners, count and bijection, attached by identifier, barrel identity).
  • packages/spec/src/conversions/page-main-region-width-removed.test.ts: registration, copy-on-write, idempotence, the stored-row seam, and an agreement matrix. Over every width, name casing, kind and template in it, the conversion deletes a width exactly where the parse refuses one.
  • packages/spec/src/shared/retired-key-removal-version.pin.test.ts: the JOIN row. The prescription names @objectstack/spec 18, which is what retiredAfter: '17.7.0' derives.

Verification (head 7263210e1, the final head; every reading below is from this run)

  • pnpm --filter @objectstack/spec build: exit 0.
  • pnpm --filter @objectstack/spec check:generated: exit 0. "All 14 generated artifacts are up to date".
  • pnpm --filter @objectstack/spec test: exit 0. 695 files passed, 21225 tests passed, 1 todo.
  • pnpm --filter @objectstack/spec typecheck: exit 0. That covers tsc --noEmit, check:scripts-typecheck and check:test-typecheck. The test layer compiles under tsconfig.test.json, and the debt ledger is unchanged (52 files, 242 errors, 132 pinned signatures). None of the new test files is in it.
  • Build of the dependency closure of @objectstack/mcp, @objectstack/cloud-connection, @objectstack/example-crm and @objectstack/example-showcase: 63 of 63 tasks, exit 0.
  • @objectstack/mcp: test 40 files, 479 passed; typecheck exit 0.
  • @objectstack/cloud-connection: test 52 files, 614 passed (this includes marketplace-install-local-convert-forward.test.ts, which reads the hotcrm 4.0.1 artifact); typecheck exit 0.
  • @objectstack/example-crm: test 6 files, 46 passed; typecheck exit 0. @objectstack/example-showcase: test 36 files, 431 passed; typecheck exit 0.
  • The suites that read the hotcrm 4.0.1 artifact fixture: objectql registry-installed-package-retired-page-declarations.test.ts (8 passed) and service-package hydration-convert-forward.test.ts (5 passed).
  • pnpm check:adr-0087-registration: exit 0. Its verdict line: ".changeset/24326-main-region-width-refused.md [BREAKING+bang+clause-②-narrowing] registered page-main-region-width-refused, page-main-region-width-removed (new here: page-main-region-width-refused, page-main-region-width-removed)".
  • Gate families: node scripts/pm/dispatch-gates.mjs --commands (no paths, merge base dd72a047f, 22 paths) derived 114 commands. All 114 were run, and every one exited 0. The reconciliation with --ran and exit codes recorded: "114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3)".
  • pnpm check:dev-prereqs (a local-only family): exit 0.
  • Left to CI: pnpm lint, test:repo, and the rest of the farm.

Ablations (each committed first, restored from HEAD and proved by blob hash)

  • A1: the check refuses nothing. In checkPageMainRegionWidth, the guard was replaced so that every string width returns early. The anchor count went from 1 to 0 and the marker count to 1. The four pinning suites then failed: 89 failed, 422 passed. Restored: the blob is 448b9617687d, equal to HEAD, and git diff HEAD is empty.
  • A2: the D2 entry never fires. The conversion's width guard was replaced, and the spec package was rebuilt. The marker was present in 8 dist/ files. The spec conversion and placement suites failed (58 failed, 430 passed). The objectql option-B artifact case failed too (1 failed, 7 passed: "Release artifact packages[0] is …" refused). That is the kernel-build stop this entry exists to prevent. Restored: the blob is 4f56b2223e64, equal to HEAD. After a rebuild the marker is absent from all 232 dist/ files.

Landing order with the seat's other narrowing

This seat's other narrowing in this window is PR 24347 (stack.zod.ts), and it is in review. If both touch the migration registry, whichever lands second merges main through scripts/pm/os-regen-merge.sh. This branch already merged main that way at 7263210e1.

Acceptance notes

  • The known external author. objectstack-ai/hotcrm src/sales/pages/home.page.ts declares width: 'large' on its three-column page's main (the card's measurement). It must drop the key when it takes the next spec release. Its built artifact, if it was built against a floor at or below 17.7.0, and its stored rows convert on read. hotcrm is not touched here.
  • An objectui fixture that pins the refused shape as spec-valid. objectui packages/types/src/__tests__/passing-keys-arms-11440.test.ts:83 asserts that SpecPageSchema.safeParse(page).success is true for a main region with width: 'large' (three cases: record, home, utility). It is at the pin 0707b4ae37 and at objectui origin/main 43efd078aa. It goes red at objectui's next @objectstack/spec bump, not before. The Console Pin Gate builds the pin against this tree and runs no objectui tests. Its build reads PageRegionSchema.width's type, and that type is unchanged. The same document appears in packages/cli/src/__tests__/validate-passing-keys-11440.test.ts:98, run through objectui validate. That command judges with objectui's own authoring face, so whether it goes red is NOT MEASURED. The edit is the same one-key delete. No objectui change is made here, per the claim. The finding is in the report for the seat, to be filed in objectui. Owner: none.
  • Same family, not filed (no producer pulls it). At the pin, width on header and footer is read only by RegionLayout, which serves the default template and template-less pages. ThreeColumnTemplate, HeaderSidebarMainTemplate and DashboardTemplate draw header and footer without a width class. DashboardTemplate draws every grid cell without one too. The spec accepts a width on all of them. No in-repo or measured external page declares a non-'full' header/footer width on those templates, or any width on a dashboard cell, so nothing on screen differs today. Owner: none.
  • os migrate meta replays the retired D2 entry against sources by id, as for every step-18 retirement. This PR makes no claim about --write rewriting a TypeScript source.

Generated by Claude Code

PageSchema refuses width small, medium or large on a region named main
(case-insensitive) at regions.N.width, on every page kind and template:
no layout reads those values, and the only reader of main.width is the
page's full-bleed, for 'full'. Exported as checkPageMainRegionWidth.

The data-at-rest half is the retired D2 conversion
page-main-region-width-removed (a lossless delete); the D3 entry is
page-main-region-width-refused. In-repo pages that declared a large main
(examples, the mcp and cloud-connection system pages, docs, spec tests)
drop the key.

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

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cloud-connection, @objectstack/mcp, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 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.

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

  • content/docs/protocol/objectui/layout-dsl.mdx (via PageSchema (symbol, a top-level const))

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

  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object pageMainRegionWidthRemoved), retiredFromLoadPath (symbol, a field of const object pageMainRegionWidthRemoved))

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
  • 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
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 150 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 a21fd6c531e50a53deb26a7d04886fbb073164e8 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 a21fd6c531e50a53deb26a7d04886fbb073164e8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24362 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T15:55Z

ACCEPT at head 7263210e149fb5f971b44069e16c1b295daedd69 (claim 6250714038, report 6251931793 on #24326; the dev resumed after a container restart from 7691b8a7e). A contract review at CONTRACT_REVIEW_TIER is owed: this narrows what a page publishes (Clause-②: yes (narrowing)), in non-test packages/spec/src/**. It runs on this head, isolated from the seat; needs:contract-review is on the PR until its record is adopted. The PR lands after a PASS there and a fully green head.

item reading
Shape Draft, base main, assignee zhuangjianguo. The body's line 1 is Fixes #24326, line 3 is Clause-②: yes (narrowing); no other closing keyword. 22 files, +492/-26; not governed. The changeset is minor on @objectstack/spec with the ! title, plus patch on @objectstack/mcp and @objectstack/cloud-connection. It has one ADR-0087 marker (registered page-main-region-width-refused, page-main-region-width-removed) and a FROM → TO table.
The seat's ruling, against the diff Refuse route as ruled. checkPageMainRegionWidth (page.zod.ts) refuses small / medium / large on a region named main at regions.N.width; the name match is case-insensitive, as the renderer's is. The message names the region and 'full'. It is attached beside checkPageRegionPlacement, so definePage, defineStack and the save door reach it. 'full' and every other region are unchanged. P1–P3 are answered in the PR body; the seat's own P1 reading at the pin (6250714038) agrees.
Accepted deviation 1: the D2 conversion page-main-region-width-removed in conversions/registry.ts, with its pins, is outside the claim's listed surface. It is the data-at-rest half: without it, the strict artifact parse refuses this repo's hotcrm 4.0.1 fixture (5 main regions with large). The dev's ablation A2 shows it. The brief's G1 named this entry as possibly owed, and it follows the precedent's D3 + retired-D2 shape. The contract review judges it.
Accepted deviation 2: two shipped system pages packages/mcp/src/connect-ui.ts and packages/cloud-connection/src/cloud-connection-ui.ts drop width: 'large' from main. Each is a one-line delete with a patch bump. They are domain:cli files; the seat posts the cross-lane declaration on that lane's seat post in this act. The examples and content/docs/ui/pages.mdx drop the same key, and content/docs/** is domain:devx's: a note goes there too. No open PR touches any of the 22 files (all 7 open PRs' file lists were read).
Local runs (dev, on 7263210e1) spec build, check:generated 14/14, 695 test files and 21225 tests passed, typecheck exit 0. The closure build covers mcp, cloud-connection and both examples: their tests and typechecks are green. Ablations: A1 (the check) turned 89 tests red; A2 (the conversion) turned 58 red plus the objectql artifact case. Both restores were proven by blob hash. Gates: 114 derived, 114 run with exit 0, reconciled with --ran.
Out of scope (1) objectui's passing-keys-arms-11440 test (packages/types, :83) and its packages/cli twin pin a main region with width: 'large' as accepted. The seat re-read both at the pin. They go red at objectui's next @objectstack/spec bump that carries this change. The seat files the objectui card in this act and links it here. (2) The header and footer width family on the non-default templates has no producer: Acceptance notes. (3) hotcrm's home.page.ts must drop the key at its next spec bump; its stored rows convert on read: Acceptance notes.
Landing After the contract review PASS and a fully green head: pr_ready then automerge_enable. The merge closes #24326.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Carrier for the objectui finding · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T16:00Z

A correction to the ACCEPT 6251963511, out-of-scope item (1). The seat did not file a new objectui card. objectstack-ai/objectui#12971 already owns the next objectstack pin bump, and it follows every contract change the new pin carries. So a card for this would duplicate it: one comment on the existing card carries it. That note is objectstack-ai/objectui#12971 comment 6252002879. It names both fixtures (passing-keys-arms-11440.test.ts and validate-passing-keys-11440.test.ts) and the one-key delete. carrier: objectstack-ai/objectui#12971.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7263210e149fb5f971b44069e16c1b295daedd69
Local-runs: none
Scope: packages/spec/src/ui/page.zod.ts, packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/semantic/18.page-main-region-width-refused.ts, .changeset/24326-main-region-width-refused.md + objectui packages/components/src/renderers/layout/page.tsx at 0707b4ae3712 (the renderer the refusal is measured against), packages/spec/src/stack.zod.ts:721 and packages/spec/src/kernel/metadata-type-schemas.ts:113 (the stack and save doors), packages/metadata-protocol/src/protocol.ts (the save door's judge: every non-flow body is parsed as written), packages/spec/src/conversions/walk.ts#mapPages, packages/metadata/src/plugin.ts#_convertArtifactForward and docs/adr/0087 (the artifact-ingestion window), packages/objectql/src/fixtures/hotcrm-4.0.1-published-pages.artifact.json (the data at rest the D2 entry exists for), packages/spec/src/shared/retired-key-removal-version.pin.test.ts (the derivation of @objectstack/spec 18), scripts/check-changeset-no-major.mjs (the launch-window convention), content/docs/ui/pages.mdx, packages/mcp/src/connect-ui.ts, packages/cloud-connection/src/cloud-connection-ui.ts, the seven example pages, and the three new or extended spec test files.

Read-only review of the net diff against the merge base dd72a047f (22 files, +492/-26), card #24326 with every comment (triage 6250190637, the seat's claim and route ruling 6250714038, the dev's report 6251931793), the PR body, and the check-runs on this head. The face was read with reviewFace on the PR's full file list: schema face, hits page.zod.ts, conversions/registry.ts, 18.page-main-region-width-refused.ts. Nothing was built, tested or re-run.

① Derived judgments

  1. The narrowing, named right. PageSchema now refuses width: 'small' | 'medium' | 'large' on a region whose name lowercases to main, at regions.N.width, code: 'custom', on every page kind and template. Measured independently against objectui page.tsx at the console pin 0707b4ae3712: main is drawn flex-1 min-w-0 in RegionLayout (:434, the default template and every template-less type), HeaderSidebarMainTemplate (:507) and ThreeColumnTemplate (:541); DashboardTemplate grids it with no width class (:574). The one read of main.width is PageRenderer's full-bleed (:893, r.name?.toLowerCase() === 'main' && r.width === 'full'), which swaps the page body's max-width cap for max-w-none — and that body wrapper (:932) is shared by every dispatch branch, so "page-wide" is true for every kind. findRegion (:91) matches toLowerCase(); the check and the conversion both compare name.toLowerCase() === 'main'. The name match is the renderer's exactly. Right.
  2. The doors, named right. The check is attached to PageSchema by identifier (.superRefine(checkPageMainRegionWidth), after checkPageRecordBindingPlacement), so it reaches every door checkPageRegionPlacement reaches: definePage (PageSchema.parse, page.zod.ts:2126), defineStack (stack.zod.ts:721, pages: z.array(PageSchema), pinned as STACK_SCHEMA_INVALID 422 at pages.1.regions.1.width), and the metadata save door (metadata-type-schemas.ts:113 maps page to PageSchema; protocol.ts's save judges every non-flow body as written, schema.safeParse(request.item), so a page carrying the width is refused 422 INVALID_METADATA whether or not a row of that name exists). Those are the three doors the changeset, the D3 entry and the docblock name — no more, no fewer. PageRegionSchema has one parent (PageSchema.regions, page.zod.ts:1905), so the new .describe() on width is no wider than where the check lives. Right.
  3. The public surface. One added export, checkPageMainRegionWidth on @objectstack/spec/ui, with api-surface/ui.json and export-origins/ui.json regenerated one line each, and the eight-export identity pin extended in object-refinement-check-exports.test.ts (own failure matrix, attached-by-identifier count, barrel identity). An additive export beside an accept-set narrowing: Clause-②: yes (narrowing) is the right declaration. Right.
  4. The message and its path. Region \main` is refused with `width: 'large'` (since @objectstack/spec 18): a `main` region takes only `width: 'full'`. … names the region as written (MainstaysMain), the refused value, the one accepted value, why (drawn at the template's width; one grid cell on dashboard), what 'full'means, and the one-key edit; located atregions.N.width; no tracker number. @objectstack/spec 18is what the pin test derives fromretiredAfter: '17.7.0'under the prerelease label (the JOIN row added here holds it). Every sentence of the describe, the docblock, the changeset andpages.mdx:151checked against the renderer: true, and none wider than the enforcement. One imprecision, not a falsehood: "besidesidebarandasideit takes the space they leave" also coversheader-sidebar-main, which has no aside; main` still takes what the rail leaves there. Right.
  5. ADR-0087 shape. D3 page-main-region-width-refused in step 18 with no conversionIds (no mechanical edit on the authoring surface) plus the retired D2 page-main-region-width-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '17.7.0', order: 69 — unique and the highest in MAJOR_18_CONVERSIONS, inserted at its id-sorted slot above pageRequiresNonCompiledKindRemoved). This is the cited precedent's shape exactly (18.page-template-closed-region-unplaced-refused.ts with page-template-full-width-to-default, same retiredAfter). Scope is right: mapPages walks stack.pages[], the collection every door reads a package's pages from; the stored-row seam wraps one page as pages[0] (rowToData, pinned); the artifact door replays a retired entry when the declared floor is at or below its retiredAfter (ADR-0087 window, _convertArtifactForward) — the hotcrm 4.0.1 fixture declares ^17.7.0 and carries five main regions at large, which is the kernel-build stop ablation A2 reproduces. It deletes exactly what the check refuses and nothing else: the three widths on main (any case), 'full' kept, every other region untouched, copy-on-write, expectedNotices: 3 for the three stripped regions; the 7 names x 4 widths x 6 shapes matrix pins conversion-deletes if and only if parse-refuses. Lossless: no layout read the value, so the rendered result is the same. The authoring funnel does not replay it (pinned), so a live author meets the refusal. Right.
  6. The pure removals. examples/app-crm (1), examples/app-showcase (6), packages/spec/src/ui/page.test.ts (2 fixtures), content/docs/ui/pages.mdx (3 snippets), packages/mcp/src/connect-ui.ts and packages/cloud-connection/src/cloud-connection-ui.ts: each deletes one width: 'large' line on a main region, none writes 'full', so none becomes full-bleed and each renders the same (main is flex-1 with or without the key). A residual grep at the head over examples/, packages/, content/, skills/ finds no main region with a non-'full' width outside the D2 entry's own fixture and the byte-identical hotcrm artifact. Right.
  7. Generated. content/docs/references/ui/page.mdx carries the new describe in its two PageRegion tables, from the generator. Right.

② Semver level

@objectstack/spec is at 18.0.0-next.3 (pre mode). The diff narrows a published authoring accept set: BREAKING, and under the launch-window convention (scripts/check-changeset-no-major.mjs: a breaking change ships as minor during the launch window; the sibling narrowings on main — 24162-record-json-retired.md, 24230-marketplace-installed-list-retired.md — ship the same way) minor with ! on the title is the grade the diff publishes. @objectstack/mcp and @objectstack/cloud-connection are published packages (no private) whose shipped pages each lose one inert key: patch is right. The two examples are private: true and owe nothing. The changeset carries exactly one ADR-0087 marker (registered page-main-region-width-refused, page-main-region-width-removed), the FROM to TO table with the one-line fix, and the Clause-② line. Clause-②: yes (narrowing) on the PR body and the changeset, matching the diff. Check Changeset is green on this head.

③ Boundary flags

  • Dev flag 1 — objectui passing-keys-arms-11440.test.ts:83. Verified at the pin: specPage() is a header-sidebar-main, kind: 'full' page whose main declares width: 'large', and the lit control asserts SpecPageSchema.safeParse(page).success === true, where SpecPageSchema is spec's PageSchema re-exported (authoring-nodes.ts:100). It goes red at objectui's next @objectstack/spec bump that carries this change, not before. Not this PR's to fix: the claim forbids an objectui change, and the Console Pin Gate (ci.yml:2918) builds the console at the pin and checks the stamp and injection — it runs no objectui test, and the width type it compiles against is unchanged — so this is not a main-red on merge and Post-Task Checklist 4's ship-together clause is not triggered. Escalated to the seat: file in objectui with PR feat(spec)!: a page region named main takes only width: 'full' #24362 back-linked. The same card should name objectui layout.zod.ts:1069, which builds the authoring face from stripImportedDefaults(SpecPageSchema).shape and so drops every superRefine: objectui validate keeps accepting large on main until it re-attaches checkPageMainRegionWidth, which is what the export is for.
  • Dev flag 2 — header, footer and dashboard-cell widths with no producer. Verified at the pin: RegionLayout applies getRegionWidthClass to header and footer (:416, :422); the three templates draw header, footer and every dashboard cell with no width class. The same declared-but-unenforced class, a different key family, and no reach: (no in-repo or measured external page declares one). Under the filing gate a finding without reach: goes to Acceptance notes with its carrier, which is where the dev put it. Not this PR's: the claim and the route ruling are main-scoped. The seat decides whether to fold it into a sweep card.
  • Dev flag 3 — hotcrm src/sales/pages/home.page.ts. External producer; its built artifact and stored rows convert on read inside the window; the source edit is hotcrm's at its next spec take. Not this PR's.
  • Deviation 1 — the D2 entry and its two pin files outside the claim's listed surface. Owed, not optional: without it the strict artifact parse refuses the hotcrm 4.0.1 fixture (five main regions at large, floor ^17.7.0), the dispatch's G1 named this very conversion, and it is the precedent's shape. Accepted.
  • Deviation 2 — the two system pages and their patch bumps. Required for the packages to keep building against the refusing spec; pure removals. Accepted.
  • open_questions: none.
  • Check-runs on 7263210e1, read 2026-10-18T16:06:45Z: 44 runs, 0 non-green. Lint & Repo Gates, TypeScript Type Check (and its four lanes), Build Core, Governed Surface Queue Guard, Check Changeset, Spec property liveness, Lint · ESLint, Build Docs, the five lint lanes and the card-claim guards: success. Console Pin Gate, Temporal Conformance, dogfood shards, Packed-tarball smoke: skipped (full-battery jobs). Test Core (1/6) to (6/6): still in_progress at that read — the dev reports the spec suite (695 files, 21225 tests), the mcp, cloud-connection, example and hotcrm-fixture suites green on this head; the queue will hold the PR until those six report.

Implemented-by: claude/issue-24326-main-region-width
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-18T16:09Z

  • Adopted: contract review 6252093173 (Served-tier: CONTRACT_REVIEW_TIER, Head-sha 7263210e149fb5f971b44069e16c1b295daedd69, the current head, PASS, no FAIL item). Its readings agree with the seat's ACCEPT 6251963511. It read the renderer at the pin: every template draws main at the width the layout leaves it, 'full' is the one read, and the name match is the renderer's toLowerCase(). It read the three doors as the changeset names them: definePage, defineStack and the save door. needs:contract-review is removed in this act.
  • Its judgments on the deviations: the D2 conversion page-main-region-width-removed is owed, not optional: without it the strict artifact parse refuses the hotcrm 4.0.1 fixture. The two system-page removals are required for those packages to keep building. The seat holds the same view.
  • Its boundary answers: objectui's passing-keys-arms-11440 fixtures are not this PR's to fix (carrier: deps(pin): move .objectstack-sha from the next.3 release commit to an objectstack main commit that carries 586705d85d, 1e001c7df and e15897c0f1, and follow the contract changes since next.3 (the first pin bump under ruling B on objectui#12955) objectui#12971 6252002879). The header and footer width family has no reach: and stays in Acceptance notes.
  • Landing: once every check on this head is green: pr_ready then automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 18, 2026 16:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 18, 2026 16:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 3a734e0 Oct 18, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24326-main-region-width branch October 18, 2026 16:55
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/l tests tooling

Projects

None yet

2 participants