Skip to content

feat(spec)!: a page gains an optional print declaration and a linted printable block subset; the zero-reader document schemas retire whole (#22158) - #22193

Merged
objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/issue-22158-print-page-spec
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/issue-22158-print-page-spec

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22158

Clause-②: yes (narrowing)

Card ① (spec only) of the ruling of record on #8346. #8346 remains open for cards ② (objectui: the print CSS mapping, the printable blocks, the record page's print action), ③ (the render service, the Chromium driver, the archive) and ④ (the record-page "generate PDF" action); none of them is in this PR.

The ruling this executes (verbatim, comment 6051470224 on #8346, maintainer 「8346 B′」, 2026-10-08T03:18Z)

B′. A document is a page with a print declaration; no new template type.

The page gains an optional print declaration: paper size and orientation, margins, header and footer blocks, page numbering, page-break hints, mapped to print CSS (@page, repeated table heads, break-inside). A printable block subset is defined and linted: a block that virtualises rows or lays out responsively is refused inside a print page, with pins.

List export does not regain 'pdf'; the retirement sentence in packages/spec/src/ui/list-view-export-options.ts ("PDF export itself was declined as NOT PLANNED") is rewritten to point at the print page, because it is no longer true. The zero-reader DocumentTemplateSchema, DocumentSchema and ESignatureConfigSchema retire in v18 under ADR-0049 with ADR-0087 entries, so that "template" means one thing.

⛔ Not taken: A (restoring 'pdf' on list export …), B (a second authoring vocabulary beside pages …), C as the end state …, a Word-upload template kind, batch merging or packaged downloads …, and the in-process pdfmake-class driver.

What changes

1. PageSchema.print — the print declaration (additive, PagePrintSchema, closed)

key type print CSS when omitted
paperSize 'A4' | 'A5' | 'Letter' | 'Legal' @page { size } size keyword A4
orientation 'portrait' | 'landscape' @page { size } orientation keyword portrait
margins closed { top, right, bottom, left }, numbers ≥ 0, millimetres @page { margin } renderer default per side
repeatHeader boolean the page's own header region repeated on every sheet off (prints once)
repeatFooter boolean the page's own footer region repeated on every sheet off (prints once)
pageNumbers boolean @page margin box with counter(page) / counter(pages) off
repeatTableHeaders boolean (page-break hint) thead { display: table-header-group } on
avoidBreakInside boolean (page-break hint) break-inside: avoid on every block off

No .default() anywhere, so nothing materializes into parsed pages (PagePrint is one type, pinned in type-alias-convention.pin.test.ts). Every .describe() names its consumer: the console's browser print rendering (card ②) and later the server-side renderer (card ③). Declared-equals-enforced: nothing reads these keys until ② lands, so liveness/page.json carries print and its 10 nested keys as planned (11 rows; the container carries authorWarn + authorHint), naming ② as the carrier; the page form records the omission in metadata-form-zod-reconciliation.test.ts for the same reason.

Refused at the parse — checkPagePrintComposition, attached by identifier and exported for .shape mirrors (pinned in object-refinement-check-exports.test.ts): print on a slotted page (unwritten slots draw the synthesized default layout), on an html / jsx / react page (blocks come from source), on a type: 'list' page (the list's print control is interfaceConfig.allowPrinting), on a type: 'utility' page (a floating panel, not a document — added in patch round 1), on a full page with no regions (synthesized default layout), and repeatHeader / repeatFooter with no region of that name. A print page is therefore a kind: 'full' page of type: 'record', 'home' or 'app' with its blocks in regions, and every refusal names those three types from one phrase.

2. The printable block subset and its lint

PRINTABLE_PAGE_COMPONENT_TYPES (13 types, an allow list) and PRINT_REFUSED_PAGE_COMPONENT_TYPES (39 types, each with its reason) live in page.zod.ts, so card ② reads the same set the lint enforces. page-print.test.ts pins that the two classify every live vocabulary type exactly once (56 known = 13 printable + 39 refused + 4 retired-by-name).

@objectstack/lint gains the gating rule print-page-block-unprintable (validatePrintPageBlocks, packages/lint/src/validate-print-page-blocks.ts) in the existing page-block family (it walks with the shared walkPageComponents, as component-type-unknown does). Registered in authoring-rules.ts on all three commands (os validate / os build / os lint) and the page save door (surfaces: CLI_AND_RUNTIME, runtimeTypes: ['page']): unlike its sibling, which is held off the runtime door pending a false-refusal budget over stored tenant rows, this rule can only speak about a page carrying print, a key no stored row or config file could carry before this PR (the shape is closed) — the budget is zero by construction, and the judgment is page-local, so a page write's one-page snapshot is its whole input.

Block-type classification (measured by declaration, renderer spot-checked at the .objectui-sha pin a58626c88)

Printable = draws everything it declares, in full, laid out the same at any width. Refused = it virtualises / windows its rows, or lays itself out to the screen (the ruling's two reasons), or has no printable content of its own (other).

type verdict reason
action:button refused — other it is an action control, with nothing to print
action:group refused — other it is a group of action controls, with nothing to print
action:icon refused — other it is an action control, with nothing to print
action:menu refused — other it is an action menu, with nothing to print
ai:chat_window refused at the parse (retired by name) —
ai:suggestion refused — other it is an AI suggestion generated for each viewer, not document content
app:launcher refused — other it is shell navigation chrome, not document content
cloud-connection:panel refused — other it is a console administration widget, not document content
element:button refused — other it is an action control, with nothing to print
element:definition-list printable static term/value list
element:divider printable static rule
element:filter refused at the parse (retired by name) —
element:form refused at the parse (retired by name) —
element:image printable image
element:metadata_viewer refused — other it is an interactive metadata browser, not document content
element:number printable single value
element:record_picker refused — other it is an input control, with nothing to print
element:repeater printable record list with no pagination control
element:text printable text
element:text_input refused — other it is an input control, with nothing to print
global:notifications refused — other it is shell chrome, not document content
global:search refused — other it is shell chrome, not document content
marketplace:installed-list refused — other it is a console administration widget, not document content
mcp:connect-agent refused — other it is a console administration widget, not document content
nav:breadcrumb refused — other it is shell navigation chrome, not document content
nav:menu refused — other it is shell navigation chrome, not document content
object-calendar refused — responsive layout it is a calendar grid sized to the screen, showing one period at a time
object-form refused — other it is an input form, laid out to the screen (mobile); print a record's values with record:details
object-gantt refused — responsive layout it is a timeline canvas sized to the screen and scrolled to the visible range
object-grid refused — virtualises / windows rows it pages its rows (pagination / pageSize), so a printed copy carries only the page on screen
object-kanban refused — responsive layout it is a board of columns that runs sideways past the screen edge
object-map refused — responsive layout it is an interactive tile map sized to the screen
object-master-detail-form refused — other it is an input form; print a record's values with record:details and its lines with record:line_items
object-metric printable single value
object-timeline refused — virtualises / windows rows it draws a window of its items (limit) along a time axis scrolled to the visible range
object-tree refused — virtualises / windows rows it draws only the nodes a viewer has expanded, so a printed copy depends on who expanded what
page:accordion refused — other it folds its panels, so a closed panel never reaches paper
page:card printable container — draws every child
page:footer printable container — draws every child
page:header refused — responsive layout it lays its action bar out to the screen width (maxVisible / mobileMaxVisible) and carries the record chrome (star, copy id)
page:section printable container — draws every child
page:sidebar refused — responsive layout it is a side column laid out beside the main region on a wide screen and folded away on a narrow one
page:tabs refused — other it shows one panel at a time, so the panels not on screen never reach paper
record:activity refused — virtualises / windows rows it draws a window of the activity feed (limit) with comment and reaction controls, so a printed copy carries only part of it
record:alert refused — other it is a banner each viewer can dismiss, so what prints would depend on who prints it
record:chatter refused — responsive layout it is a docked side panel laid out to the screen (position, width, collapsible) around a live feed
record:details printable field block
record:discussion refused — responsive layout it is a docked side panel laid out to the screen (position, width, collapsible) around a live feed
record:highlights printable field block
record:history refused — virtualises / windows rows it draws a window of the history feed (limit, 50 unless set), so a printed copy carries only part of it
record:line_items printable child-record table, no pagination (all lines up to its limit cap, 500 unless set)
record:path refused — other it is an interactive stage control; the current stage is a field value, which record:details prints
record:quick_actions refused — other it is a row of action controls, with nothing to print
record:reference_rail refused — responsive layout it is a rail docked beside the record and laid out to the screen
record:related_list refused — virtualises / windows rows it draws only the first limit related records (5 unless set) behind a "View all" link, so a printed copy carries a window of the rows, never all of them
user:profile refused at the parse (retired by name) —
any type outside the vocabulary (flex, object-chart, a plugin widget) refused — other nothing answers for how it prints

Evidence behind the windowing verdicts, by declaration: record:related_list.limit defaults to 5 behind showViewAll; object-grid declares pagination / pageSize / showPagination; record:history.limit renders 50 by default; record:line_items declares "The grid has no pagination" (renderer LineItemsPanel.tsx at the pin carries no pagination control), so it is the printable all-rows table. page:header declares maxVisible / mobileMaxVisible (a viewport-switched action bar); object-form declares mobile.

3. The list-export retirement sentence

LIST_VIEW_EXPORT_PDF_RETIRED no longer says "(PDF export itself was declined as NOT PLANNED)". 'pdf' stays refused; the prescription now points at the view's allowPrinting for printing a list as shown and at "a page that declares print" for a document. PR #22142 landed first (de70e97a2, the house sentence names --write); the merge round stacks both intents in this one string: the sentence ends "--write applies the ones it can prove, and you apply the rest by hand." and still points at allowPrinting and the print page. Pins: view.test.ts (the two NOT PLANNED assertions replaced by the pointer assertions plus a not.toMatch(/NOT PLANNED|declined/)).

4. The retirements (ADR-0049, ADR-0087 D3)

data/document.zod.ts is deleted: DocumentTemplateSchema, DocumentSchema, ESignatureConfigSchema — the ruling's three — plus DocumentVersionSchema, whose only carrier was DocumentSchema.versioning.versions (the retirement playbook's orphan-value-schema rule; see Deviations). Route: whole-def removal, no tombstone and no D2 conversion (none of them is a stack collection member or a metadata type — no seam a conversion could run on), the change-management-family precedent (#15513):

  • RETIRED_DEFS_BY_MAJOR[18]: data/DocumentTemplate, data/Document, data/ESignatureConfig, data/DocumentVersion (four entry files under migrations/entries/retired-defs/, registry regenerated);
  • D3 semantic entry document-schemas-retired + a STEP18_RATIONALE fragment (order 88);
  • the ESignatureConfig deadline-key entries in RETIRED_KEYS_BY_MAJOR[18] stay as history (gate (b2) accepts an entry naming a key the build no longer emits);
  • hand-deletions gate (a) asks for: json-schema.manifest/data.json −4 defs, authorable-surface/data.json −30 rows, authorable-defaults/data.json −2; generated: api-surface / declaration-map / export-origins shards, content/docs/references/data/document.mdx removed, the data nav and index regenerated; llms.txt 203 → 202 / data 31 → 30; PROTOCOL_MAP.md row dropped; the strictness ledger's two document.zod.ts rows dropped; the quick reference's Document row dropped (16 of 30 → 15 of 29);
  • pin: src/data/document-schemas-retirement.test.ts (registry rows, the exports gone from @objectstack/spec/data and the root entry with DocumentSchemaValidationSchema as the lit survivor, and a tree-scoped absence walk over the radius @objectstack/spec#test already declares, registered in vitest.repo-tests.json); document.test.ts and esignature-deadline-keys-retirement.test.ts leave with the module.

Zero-reader census, each against a lit control:

tree hits for the family's exported names outside the retirement itself control
objectstack fec87e7e0 (all packages, apps, examples, docs, skills) 0 code readers — only generated reference docs, release notes, CHANGELOG.md and one audits ledger row 148 files outside packages/spec name FieldSchema
objectui .objectui-sha pin a58626c88 and main cef0eee 0 (word-bounded exact names; also no Data.Document… / spec-import of Document) 55 files name PageSchema
hotcrm 1e88edc (public, read-only clone) 0 77 files name defineStack / ObjectSchema

cloud: NOT MEASURED (no checkout in this session).

Release state (H5, re-read in the merge round): .changeset/pre.json is now PRESENT on origin/main ({"mode": "pre", "tag": "next"}, entered by a87d8be29, with the v18 opening major marker 22080-v18-line-opens.md). In pre mode check-changeset-no-major stands aside (its RC exemption: a major only ever produces an X.0.0-next.N prerelease), so the launch-window "breaking ships as minor" convention no longer forces the level: the export removal is graded major on @objectstack/spec, and @objectstack/lint stays minor (a new refusal over a population that could not exist before, plus new exports). The fixed group is already at 18 through the opening marker, so this grade moves no version by itself. Disposition marker unchanged: registered document-schemas-retired. Changeset: .changeset/22158-print-page-spec.md (@objectstack/spec major, @objectstack/lint minor).

Deviations from the dispatch's mechanism hypotheses (each measured, stated here for the contract review)

  1. Running header and footer = the page's own header / footer regions, not page-block arrays under print. H1 proposed header / footer as block arrays. A second component tree would be a new composition root that every page walker must learn in lockstep — lint's walkPageComponents, the ADR-0087 conversion walker and the exported walkAddressedPageComponents behind translatePage (held equal by page-walk-conversion-parity.test.ts), plus objectui's validator and Studio's designer. The regions are already walked, translated, designed and placed at the top and bottom of the page on screen, so the printed sheet and the on-screen preview agree. The declaration carries repeatHeader / repeatFooter, and print.header / print.footer are answered by a guidance prescription naming the region.
  2. DocumentVersionSchema retires with the three named schemas. The ruling names three; the fourth export of the module had one carrier, DocumentSchema.versioning.versions, and the retirement playbook's orphan-value-schema rule takes it with its carrier (an exported schema with no consumer reads as a capability). If the review wants it kept, it is one entry file and one export to restore.
  3. The structural refusals live in the schema, the block subset in lint. kind / type / regions / region-name checks are page-local field checks and sit beside the two existing PageSchema refinements (every door, the save door included); the subset needs the nested component walk, which lives in the lint page family as the claim says.
  4. The subset rule runs on the page save door from birth, while its sibling validateComponentTypes is CLI-only pending a stored-row false-refusal budget. The claim said "wired to the authoring doors that family already reaches"; the card and the ruling ask for the save door. The budget argument is in the rule's header (zero population by construction).
  5. Files outside the claim's declared surface, each forced by a gate or by the retirement kit, none behavioural: type-alias-convention.pin.test.ts, object-refinement-check-exports.test.ts, metadata-form-zod-reconciliation.test.ts (the form-omission ledger row for print), view.test.ts, scripts/file-description.test.ts (a fixture named the deleted schema), vitest.repo-tests.json, llms.txt, PROTOCOL_MAP.md, liveness/README.md + state-counts/page.md, docs/audits/2026-07-unknown-key-strictness-ledger.md (+ generated counts), content/docs/getting-started/quick-reference.mdx, and four CLI transcript pages whose printed author-time rule count moves 49 → 50 (check:docs-transcript-drift).

Acceptance notes (not filed; carrier named)

  • content/docs/ui/views.mdx:112 still says "PDF export was declined" — rewritten in patch round 1: 'pdf' is not an export format; a list prints as shown through the view's allowPrinting; a printable document is a page that declares print.
  • objectui still carries the old "declined as NOT PLANNED" sentence (types/src/objectql.ts, ListView.tsx, ReportViewer.tsx, app-shell styles.css). Carrier: card ②.
  • "The Studio author's skill learns the print page when ① lands" (the ruling): skills/** is a governed surface and not in this card; skills/objectstack-ui/rules/actions.md also still teaches a per-project PDF endpoint. Carrier: the seat, as its own governed PR.
  • The page template (multi-column templates) is not judged by the subset; whether a print page constrains it is card ②'s call. object-chart is outside the component vocabulary (no props row), so a chart is refused in a print page today; a monthly report with charts is a named pull to widen the subset additively.
  • docs/NEXT_STEP.md:55 still plans a ui/document-template.zod.ts. Carrier: whoever next edits that roadmap file.

Patch round 1 (head 0b96e61e1)

  • CI red at d4b428426, this PR's: @objectstack/metadata-protocol protocol.meta-types-degenerate-derivation.test.ts — "control: page still serves 24 top-level properties", expected 25 to be 24. Reproduced locally at d4b428426 (exit 1). print is the 25th top-level key of the served page schema, a declared key and not a derivation change, so the control moves 24 → 25 with that reason beside field and object's own moves; it still pins every other type's count.
  • CI red at 9077995ae, this PR's: @objectstack/spec type-alias-convention.pin.test.ts — "still declares all 772 isomorphic pins", expected [ …(773) ] to have a length of 772 but got 773. Not a pin main brought in (main 959c209d5 carries 772 export type Iso_… lines and the literal 772): the PagePrintSchema pin added one, and the same first edit dropped the space in the untouched Iso_ui_page__PageTypeSchema = Assert line (it read =Assert), which the count's pattern — ^export type Iso\w+ = Assert followed by a less-than sign — then missed — so d4b428426 read 772 and stayed green by accident. Restoring the space (the review's nit) made the count honest: literal, case title and header prose now say 773, with an arrow entry. Red → green reproduced locally: the file as of 9077995ae → exit 1 with CI's exact message; HEAD → exit 0 (restore by git checkout HEAD --, proven by blob hash and an empty git diff HEAD).
  • Contract-review nits folded in (record 6053373285): print is refused at parse on a type: 'utility' page, in the list refusal's voice, pinned beside it; the admitted types (record, home, app) are named from one phrase in every refusal and in the print describe; content/docs/ui/views.mdx no longer says PDF export was declined.

Merge round (head 9f9fa3dba)

The merge queue removed this PR on MERGE_CONFLICT after the contract review's PASS on 0b96e61e1. origin/main 31cd2104d merged in through bash scripts/pm/os-regen-merge.sh (merge commit bf3359dff; no rebase, no force-push; gen:schema ran only after the merge commit existed).

  • The one conflict — packages/spec/src/ui/list-view-export-options.ts, LIST_VIEW_EXPORT_PDF_RETIRED, both intents stacked. Main (de70e97a2, fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest #22142) kept the old body ("PDF export itself was declined as NOT PLANNED") and changed the closing house sentence to name --write; this branch rewrote the body to point at allowPrinting and the print page and kept the old closing sentence. Resolution: this branch's body + main's closing sentence — "… a printable document (an invoice, a delivery order, a letter) is a page that declares print — its paper, margins and running header and footer — with its blocks in regions. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand." view.test.ts auto-merged with both sides' pins (the pointer pins and the --write sentence pin), and both pass.
  • page.zod.ts, migrations/registry.ts, authoring-rules.ts, view.test.ts and build-with-claude-code.mdx auto-merged; content/docs/references/ui/page.mdx was regenerated on the merged tree as its own commit (5f68ebe48).
  • Level, re-read: .changeset/pre.json is present on main (pre mode, tag next, a87d8be29). scripts/check-changeset-no-major.mjs (its RC exemption: "Accumulating the next major's breaking changes is precisely what an RC window is FOR, so this guard stands aside") and the AGENTS.md checklist (yes takes at least minor, (narrowing) is BREAKING) leave the level to the change: @objectstack/spec is graded major (an export removal), @objectstack/lint minor. The a87d8be29 edit to check-adr-0087-registration.mjs is one constant naming the consumed-prerelease directory; the disposition rule is unchanged.
  • Clause-②, re-read: the line stays Clause-②: yes (narrowing). The criterion (references/execution-duties.md, 「本卡放宽接受集或扩大公开面吗」) answers yes — PageSchema accepts a new key and the spec and lint entries gain exports. The new rule (5d5a88eff, 「收窄已发布接受集的卡是 Clause-②: no」) covers a card that only narrows; scripts/pm/clause2-line.mjs names this card's case exactly: "yes (narrowing) — a diff that widens one surface and narrows another. Both facts are true and both are read." Same line in the changeset.

Tests and gates (all at 9f9fa3dba, the final commit; the shared box, commands under scripts/pm/os-verify-lock.sh)

  • Full turbo build (73 tasks): exit 0.
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 (both projects): 678 files passed, 19583 tests passed, 1 todo, exit 0.
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 126 files, 5778 tests passed, exit 0.
  • The pins from both sides: page-print.test.ts, view.test.ts (this branch's pointer pins + fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest #22142's --write pins) and shared/retired-key-migrate-sentence.test.ts (the class-wide sentence pin): 3 files, 501 tests passed, exit 0.
  • node scripts/check-changeset-no-major.mjs --base origin/main --event EVENT.json (a pull_request payload carrying this body): exit 0 — "Changesets is in pre-release mode (tag: next) — major bumps are the expected product of an RC window", and "LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch". check-adr-0087-registration.mjs --base origin/main with the same event: exit 0 — ".changeset/22158-print-page-spec.md [major+BREAKING+bang+clause-②-narrowing] registered document-schemas-retired (new here: document-schemas-retired)". Both --self-tests: exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 15 artifacts current on the merged tree.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 117 families at 9f9fa3dba; every one ran, plus the 44 artifact-roster commands it prints apart (check:docs-image-tag joined the roster with main's changes): 161 commands, every exit code 0. --ran: "117 derived famil(ies) accounted for — 117 run, 0 NOT-MEASURED (a DERIVED zero — all 117 recorded an exit code and none of them is 3)".
  • Earlier rounds, not re-run in the merge round (this PR's files in those packages did not change in it; main's own changes there carry main's CI): the full @objectstack/metadata-protocol suite at 0b96e61e1 (221 files, 28253 tests passed), the PageSchema-shape sweep across 13 packages (all exit 0), and the save-door probe through runRuntimeAuthoringRules({ type: 'page' }) (a print page holding object-grid refused with print-page-block-unprintable; a printable-only page passes).

Authored by the domain:spec seat 3 dispatch, session session_01RPo7FUd6bSnAfkWMAKi848.

claude added 11 commits October 8, 2026 03:40
…rintable block subset

The print page, card 1 of the B-prime ruling on #8346: PagePrintSchema on
PageSchema.print (paper size, orientation, margins in millimetres, the
running header and footer as the page's own header/footer regions, page
numbers, page-break hints), the parse-time composition check
checkPagePrintComposition, the printable block subset and its refusal
reasons in the spec, and validatePrintPageBlocks in @objectstack/lint on
all three commands and the page save door.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…; print rows in the liveness ledger

The 17.0 retirement of 'pdf' stands; the sentence 'PDF export itself was
declined as NOT PLANNED' stopped being true when PDF documents were
re-planned for v18, so the prescription now names the view's allowPrinting
for printing a list and a page that declares print for a document. The
print declaration's eleven rows are planned with authorWarn on the
container, naming card 2 of the #8346 ruling as the carrier.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…mplate, Document, ESignatureConfig, DocumentVersion)

ADR-0049 enforce-or-remove, by the B-prime ruling on #8346: a printable
document is a page that declares print, so 'template' means one thing.
Four defs leave @objectstack/spec/data via RETIRED_DEFS_BY_MAJOR[18] with
the D3 semantic entry document-schemas-retired and a step-18 rationale
fragment; the ESignatureConfig deadline-key entries stay as history.
Generated artifacts follow in the next commit.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…e print page and the document retirement

json-schema.manifest/data.json -4 defs, authorable-surface/data.json -30
rows and authorable-defaults/data.json -2 (the deliberate hand-deletions
gate (a) asks for on a whole-def retirement); ui.json +1 def and +9 rows for
PagePrint and Page:print; api-surface, declaration-map and export-origins
follow; the data/document reference page removed and the page reference
regenerated; the strictness ledger drops its two document.zod.ts rows.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…document family retirement

Clause-② yes (narrowing): the print declaration widens PageSchema and the
document family's exports leave @objectstack/spec/data.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
The merge script's designed collection point: content/docs/references/index.mdx
regenerated from the merged sources, so main's API row (+1 schema) and this
branch's data and ui rows compose.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…ement records

check:objectui-pin-citations finds a pin citation by its backticks; the
retired-defs entries, the retirement pin and the print liveness rows now
spell every cited sha that way.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…e prose count

The page form does not offer print until the console's print rendering
lands (the ruling puts its authoring UI in the page designer with it), so
the reconciliation ledger records the omission. The list-page refusal
names the page types in quoted form so the app-root prose pin keeps
reading the page-type sentences it counts.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…geBlocks joins the registry)

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

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 61 documentable anchor(s). ⚠️ 23 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/PROTOCOL_MAP.md, packages/spec/api-surface/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via expiresAt (literal, a string literal in a comment on a changed line))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES))
  • content/docs/kernel/cluster.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))
  • content/docs/kernel/contracts/auth-service.mdx (via expiresAt (literal, a string literal in a comment on a changed line))
  • content/docs/kernel/contracts/storage-service.mdx (via downloadUrl (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/authentication.mdx (via expiresAt (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/index.mdx (via sharedWith (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/permissions-matrix.mdx (via sharedWith (literal, a string literal in a comment on a changed line))
  • content/docs/permissions/sharing-rules.mdx (via sharedWith (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx (via user_123 (literal, a string literal in a comment on a changed line), user_456 (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via user_123 (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectql/security.mdx (via sharedWith (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectui/actions.mdx (via client_name (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectui/layout-dsl.mdx (via PageSchema (symbol, a top-level const))

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

  • content/docs/releases/v14.mdx (via createdBy (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-0.mdx (via sharedWith (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-4.mdx (via ESignatureConfig (symbol, a top-level type))
  • content/docs/releases/v17/17-5.mdx (via client_name (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-6.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))

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
  • 23 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/PROTOCOL_MAP.md, packages/spec/api-surface/data.json, …) — pages documenting those are invisible to this run
  • 14 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 — 139 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 73a0a6bf1db3216a0c82bee1503011040c6a1394 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 73a0a6bf1db3216a0c82bee1503011040c6a1394

⚠️ 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 73a0a6bf1db3216a0c82bee1503011040c6a1394 → 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

Contract review

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

① Derived judgments

Read, and nothing else: card #22158 (body and its three comments: claim 6051529167, os-dev-report 6053181767, seat ACCEPT 6053217012); the ruling of record 6051470224 on #8346, verbatim; PR #22193 (body, its one comment 6053160817 — the docs-drift advisory —, the 54-file list, the net diff against main at the head: +1690 / −1565); the head's check-runs. The PR head is the brief's head and has not moved. Nothing was built, run or re-run; the gate verdicts are the check-runs' own conclusions, tallied at the end of ③.

Every accept-set and public-surface change the diff implies, each named right or wrong against the ruling's words.

Widenings (additive):

  1. PageSchema.print — an optional, closed PagePrintSchema: paperSize, orientation, margins { top, right, bottom, left } in millimetres, repeatHeader, repeatFooter, pageNumbers, and the page-break hints repeatTableHeaders, avoidBreakInside. No .default() anywhere (one PagePrint type, pinned in type-alias-convention.pin.test.ts); every .describe() names the print-CSS mapping and the consumer (card ②, later ③). Right. This is the ruling's own list — "paper size and orientation, margins, header and footer blocks, page numbering, page-break hints, mapped to print CSS (@page, repeated table heads, break-inside)" — every item present and nothing beyond it (page-print.test.ts pins the key set exactly). Declared-equals-enforced holds: liveness/page.json carries the declaration as planned with authorWarn + authorHint naming card ② as the carrier (state-counts planned 0 → 11, the generated count being the authority), and the page form omits print with a ledger row in metadata-form-zod-reconciliation.test.ts. The aliases (paper, pdf, printLayout, …) and guidance (header, footer, template) are strictObject refusal prescriptions for near-miss spellings, not second spellings. Right.
  2. New @objectstack/spec/ui exports: PagePrintSchema, PagePrint, PRINTABLE_PAGE_COMPONENT_TYPES, PRINT_REFUSED_PAGE_COMPONENT_TYPES, checkPagePrintComposition — mirrored in api-surface/ui.json, export-origins/ui.json, declaration-map/ui.json, json-schema.manifest/ui.json (ui/PagePrint), authorable-surface/ui.json (+9 rows) and the regenerated references/ui/page.mdx. Right — one list in the spec is what lets card ② render the same set the lint enforces.
  3. New @objectstack/lint exports validatePrintPageBlocks, PRINT_PAGE_BLOCK_UNPRINTABLE and the type PrintPageBlockFinding; the gating rule print-page-block-unprintable registered in authoring-rules.ts on ALL commands with surfaces: CLI_AND_RUNTIME, runtimeTypes: ['page']; the author-time rule count moves 49 → 50 in the four CLI transcript pages. Right.

Narrowings (each a refusal that did not exist before this head):

  1. Parse-time refusals, checkPagePrintComposition, attached by identifier and pinned in object-refinement-check-exports.test.ts (+7 fixtures): print on kind: 'slotted' | 'html' | 'jsx' | 'react', on type: 'list', on a full page with no regions; repeatHeader / repeatFooter with no region of that name. Right — a print page must print exactly the blocks it authors or the subset has nothing to judge, and the list refusal correctly routes to interfaceConfig.allowPrinting. One looseness, not a breach: PageTypeSchema at the head also carries utility (a floating panel), which the check admits and the list refusal text does not name (it names record, home, app). A nit in ③.
  2. The printable block subset: 13 printable, 39 refused each with its reason, 4 retired-by-name left to the parse, and the pin that the two lists classify every live vocabulary member exactly once (page-print.test.ts). The lint refuses every other type inside a print page at its type path with its reason and the printable list as the fix, one it.each case per refused type, every printable type in every container position, the ruling's invoice, and nothing on a page without print (validate-print-page-blocks.test.ts, 49 cases). Right as to the ruling's two mandatory refusals — every row-windowing block (object-grid, record:related_list, record:history, record:activity, object-timeline, object-tree) and every screen-laid-out block (page:sidebar, page:header, record:chatter, record:discussion, record:reference_rail, object-kanban, object-calendar, object-gantt, object-map) is refused — and as to the ruling's named body vocabulary (field blocks record:details / record:highlights, the all-rows table record:line_items, element:text, element:image) all being in. I checked the borderline declarations in component.zod.ts at the head: element:repeater.limit is an author-set cap with no renderer default and no pagination (printable — right); record:line_items.limit is a 500-row renderer cap with "The grid has no pagination" declared (printable — right, with the cap disclosed; a card-② note in ③); object-metric and element:number declare no windowing or viewport key (printable — right); record:alert declares dismissible + dismissKey (per-viewer localStorage) and record:path is a stage control over statusField — refusing both by type is conservative, not wrong: the ruling's two reasons do not reach them and an allow list widens additively. A type outside the vocabulary (object-chart, a plugin widget, an SDUI layout block) is refused because nothing answers for how it prints. Right.
  3. LIST_VIEW_EXPORT_PDF_RETIRED: 'pdf' stays refused in view.exportOptions formats (bare array and { formats } alike); the sentence "PDF export itself was declined as NOT PLANNED" is gone; the prescription now names the view's allowPrinting for printing a list as shown and "a page that declares print" for a document; the os migrate meta --from 16 tail is untouched, so it stacks with open fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest #22142 either way. view.test.ts pins the pointer and not.toMatch(/NOT PLANNED|declined/). Right — the ruling's sentence to the letter, and Not-taken A (restoring 'pdf') is not taken.
  4. The retirement of data/document.zod.ts whole: DocumentTemplateSchema, DocumentSchema, ESignatureConfigSchema and DocumentVersionSchema, with every type alias, out of @objectstack/spec/data and the root entry. ADR-0087: four RETIRED_DEFS_BY_MAJOR[18] entries (entry files + regenerated registry.ts), one D3 semantic entry document-schemas-retired (surface, FROM → TO replacement, reason, acceptance criteria, no conversionIds), a STEP18_RATIONALE fragment; generated baselines −4 defs / −30 authorable rows / −2 defaults; references/data/document.mdx removed and the data index, nav, llms.txt (203 → 202, data 31 → 30), PROTOCOL_MAP.md, the strictness ledger (+ counts) and the quick reference (16 of 30 → 15 of 29) brought to match; pin document-schemas-retirement.test.ts with a tree-scoped absence walk over the declared radius, anti-vacuity floors, and a lit control (DocumentSchemaValidationSchema survives). Right for the ruling's three. The fourth is judged in ③ (within the ruling). No tombstone and no D2 conversion is right: none of the four is a stack collection member or a metadata type, so a conversion would have no seam that runs (the spec: the rest of the incident-response, training and change-management families — every remaining key and all fifteen defs — has zero readers; whole-def enforce-or-remove is the open question left after #14477 #15513 precedent); the ESignatureConfig deadline-key rows stay in RETIRED_KEYS_BY_MAJOR[18] as history.
  5. The pinned-sibling question an export removal must answer (AGENTS.md's "does the pinned sibling import what you are removing?"): the dev's census reports zero readers of the family in objectui at the .objectui-sha pin a58626c88 and at main cef0eee, in hotcrm 1e88edc, and in this repo outside packages/spec, each against a lit control (55 files name PageSchema; 148 name FieldSchema; 77 name defineStack / ObjectSchema); cloud is NOT MEASURED and says so. The Console Pin Gate was path-filtered to skipped on this head, so the census is the evidence, and it is stated with its controls. Accepted.

The Not-taken list, checked against the diff: no 'pdf' on list export; no second authoring vocabulary (the running header and footer are page regions); no Word-upload template kind; no batch merge or packaged download; no in-process layout driver. None taken.

② Semver level

.changeset/22158-print-page-spec.md: @objectstack/spec: minor, @objectstack/lint: minor; a BREAKING section with the FROM → TO table and the one-line fix ("delete the import; author a printable document as a page with a print block"), the ADR-0087 disposition marker adr-0087: registered document-schemas-retired, and the declaration Clause-②: yes (narrowing) at a line start in both the changeset and the PR body — the arm the card's contract notes prescribe. .changeset/pre.json is absent on main and at the head (both 404 when read); @objectstack/spec and @objectstack/lint are 17.7.0 at the head. Under AGENTS.md's rule — "yes takes at least minor, (narrowing) is BREAKING", with the migration and the disposition in the body — minor + BREAKING + migration + marker matches what the diff publishes: the print declaration and the lint rule are additive, the export removal is the narrowing, and the changeset carries its migration. (The dev's phrase "launch-window convention" is not AGENTS.md's spelling; the rule it invokes is.) Check Changeset on the head: success. The Clause-② line is correct.

③ Boundary flags

Every dev flag and both open_questions, each judged against the ruling's own words.

Open question 2 — the running header and footer as the page's own header / footer regions (repeatHeader / repeatFooter), not block arrays under print. WITHIN THE RULING. The ruling's declaration list says "header and footer blocks"; its design sentences say "The template is a page. The body vocabulary is the page's own blocks", "no new visual designer: Studio's page designer is the designer, the print declaration is a few fields on it", and Not-taken B refuses "a second authoring vocabulary beside pages". A block array under print would be a second composition root — a new designer surface and a new walk for lint's walkPageComponents, the ADR-0087 conversion walker and walkAddressedPageComponents (held equal by page-walk-conversion-parity.test.ts) — the opposite of "a few fields on it". The header and footer ARE blocks: authored in the page's regions, already walked, designed and placed top and bottom on screen, so sheet and preview agree; the two booleans govern their repetition on every sheet, and print.header / print.footer are answered by guidance naming the region. A print-only letterhead that differs from the screen header would be additive later. No director ruling needed; the seat's A stands.

Open question 1 — the 13-type allow list against a narrower list. WITHIN THE RULING. The ruling says "A printable block subset is defined and linted: a block that virtualises rows or lays out responsively is refused inside a print page, with pins" — it prescribes what MUST be refused and leaves the subset to be defined. Every block its two criteria reach is refused (①.5); every body-vocabulary kind it names is in; the additions (element:definition-list, element:divider, element:repeater, element:number, object-metric, and the three containers) each draw all they declare at any width, by their declarations at the head. Refusing controls, inputs, shell chrome, folding containers and per-viewer banners goes beyond the ruling's two reasons but contradicts nothing in it, and the allow list is the loud form: widening is additive on a named pull, while narrowing later would be breaking. Option B (the ruling's four named kinds plus containers) would refuse object-metric and element:number, pushing a monthly report's totals onto blocks the subset then refuses — against the ruling's own second-axis sentence (the port customer's monthly report). No director ruling needed; the seat's A stands. First candidates for a named widening pull, for the record: record:alert when not dismissible, and object-chart (outside the vocabulary today).

Deviation 2 — DocumentVersionSchema retires as a fourth export. WITHIN THE RULING. The ruling's operative criterion is zero-reader under ADR-0049 ("The zero-reader … retire in v18 under ADR-0049 with ADR-0087 entries"); it names the three top-level schemas. DocumentVersionSchema's only carrier was DocumentSchema.versioning.versions, so the ruling's own removal makes it a zero-reader export by construction, and ADR-0049 enforce-or-remove then takes it — keeping it would leave exactly the declared-unenforced surface the ruling removes. It is registered the same way (the data/DocumentVersion def entry, named in the D3 entry and in the changeset's FROM → TO). Restorable as one entry file and one export if the director reads the ruling narrower; I do not.

Deviation 4 — the subset rule on the page save door from birth (CLI_AND_RUNTIME), while its sibling validateComponentTypes is CLI-only. WITHIN THE CARD AND THE RULING. The card asks for "os validate / os build and the save door"; the ruling says "the page's blocks are validated at publish today and the declaration adds a few checked fields". The false-refusal budget the sibling waits on is zero here by construction: PageSchema is closed and print did not exist, so no stored tenant row and no config file can carry it; the judgment is page-local, so the one-page per-write snapshot is its whole input. The dev's save-door probe through runRuntimeAuthoringRules({ type: 'page' }) and metadata-protocol's runtime-authoring-gate pins cover the wiring; the head's Test Core check-runs are the gate.

Deviation 1 (H1 falsified) and deviation 3 (structural refusals in the schema, the block walk in lint): mechanism choices the ruling does not constrain. The parse-time refusals sit beside the two existing PageSchema refinements and reach every door; the nested walk reuses the page-block family's shared walkPageComponents. Right.

Deviation 5 — about 15 files outside the claim's declared surface (pins, transcript counts, ledger rows, llms.txt, PROTOCOL_MAP.md, the strictness ledger, the quick reference, vitest.repo-tests.json): each reads in the diff as a gate-forced or kit-forced edit carrying no behaviour. Accepted. One cosmetic regression rides along in type-alias-convention.pin.test.ts: the untouched Iso_ui_page__PageTypeSchema line lost the space after = (=Assert). Harmless; a one-character fix on the next touch.

Out-of-scope findings — carriers judged:

  • content/docs/ui/views.mdx still says "PDF export was declined — [P1] export: no print / PDF export — procurement orders & audit reports cannot be delivered #1301" (read at the head). The ruling names only the .ts sentence, so this PR is within its letter; but it is the same sentence the ruling calls "no longer true", one hop away in a hand-written page. Not blocking. Recommendation to the seat: land it as a docs-only follow-up now rather than wait for card ②, which is an objectui card.
  • objectui still carries the old "declined as NOT PLANNED" sentence — card ②'s, right.
  • "The Studio author's skill learns the print page when ① lands": the ruling sequences it after ①, and skills/** is governed and untouched here (0 of 54 paths on the governed register). The seat's governed PR is the carrier — right.
  • docs/NEXT_STEP.md:55 still planning ui/document-template.zod.ts: a stale roadmap line, acceptance note — right.
  • The page template (multi-column) not judged by the subset, and object-chart refused: card ②'s call and a named pull — right.

Nits for the seat (none blocks the verdict):

  • checkPagePrintComposition admits print on type: 'utility' (a floating utility panel) while the list refusal text names only record, home and app, and page-print.test.ts pins acceptance on those three alone. Either refuse utility or name it — a one-line follow-up.
  • record:line_items prints every line only up to its 500-row renderer cap; card ②'s print rendering should lift the cap under print or make the truncation visible on paper.
  • Prose counts: the liveness README's "eleven planned rows (the container, its eight keys and the four margins sides)" describes thirteen entries and eleven counted leaves; the PR body's "10 nested keys" is loose the same way. The generated state-counts/page.md (planned 11) is the authority; the prose is loose, not wrong.

Check-runs on the head d4b428426 (their conclusions are the gate verdicts; three reads, none waited on): first read 2026-10-08T05:45Z — 36 runs, 20 completed (15 success, 5 skipped), 16 in progress, 0 red; second read 2026-10-08T05:51Z and final read 2026-10-08T05:54Z — 38 runs, 31 completed (26 success, 5 skipped), 7 in progress, 0 red. Green by the final read (25 distinct names): Check Changeset, Spec property liveness, Governed Surface Queue Guard, Type Check (source gates, debt ledger, consumer gates, workspace) and TypeScript Type Check, Build Core, Build Docs, Dogfood Regression Gate (parent and 1/3 – 3/3), Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Check Documentation Links, Flag docs affected by code changes, Check PR Size, Auto Label, filter, and the three PR-context checks (the card this PR closes must claim this branch; no other open PR may claim the same issue or the same single-writer path; part-of PR must not also close its card). Skipped: Console Pin Gate and Packed-tarball smoke (opt-in), both path-filtered, plus one duplicate skipped instance each of Auto Label and Check PR Size whose sibling run succeeded. Still in progress at the final read: Lint & Repo Gates and Test Core 1/6 – 6/6. This PASS stands on the head's check-runs as read; the seat reads those seven green itself before landing, and a red among them voids this record on this head.

Implemented-by: claude/issue-22158-print-page-spec
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

Rendered 2026-10-08T05:54Z; an isolated contract-review subagent of the session named above, read-only.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on d4b428426, and it is this PR's. A fix round has been dispatched

domain:spec seat 3 (#18883) · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-08T05:55Z.

  • Failing: Test Core (3/6) → @objectstack/metadata-protocol → src/protocol.meta-types-degenerate-derivation.test.ts:325. The test "metadata-protocol: /meta/types serves an EMPTY JSON Schema for action — the output-mode derivation of its ZodPipe has no properties, and the hand-crafted fallback never fires #17501 … control: page still serves 24 top-level properties" now reads expected 25 to be 24.
  • Why it is this PR's: adding PageSchema.print moves the page top-level property count that the /meta/types control pins. The dev's local run covered one test file in that package, not the full suite.
  • Done:
    • Patch round 1 is dispatched to the same dev on the same branch. It reproduces the failure, moves the control to the new count with its reason, runs the full @objectstack/metadata-protocol suite, and sweeps every other test that pins PageSchema's shape.
    • The contract review that had started on d4b428426 was stopped, because that head will not land. A fresh review runs on the fixed head.
  • needs:contract-review stays on until then.

claude added 3 commits October 8, 2026 06:02
…/meta/types page control counts print

Patch round 1 on the print page. The /meta/types control pinned page at 24
top-level properties; print is the 25th, a declared key, not a derivation
change (the same note field and object carry). A utility page is a
floating panel, not a document, so the parse refuses print there beside
list, and every refusal names the admitted types (record, home, app) from
one phrase. The type-alias pin's PageTypeSchema line gets its space back,
and views.mdx stops saying PDF export was declined: pdf stays out of
exportOptions, a list prints through allowPrinting, a document is a page
that declares print.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…ing its admitted page types

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…counted once its neighbour's space came back

The PagePrintSchema pin added one isomorphic pin; the same edit dropped the
space in the untouched PageTypeSchema line, whose '= Assert<' the count's
pattern then missed, so the count read 772 and stayed green by accident.
Restoring the space made it 773; the literal, the case title and the
header's prose follow, with the arrow entry naming why.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0b96e61e15d26cf5936a39d5904db5e07bbc9ce5
Local-runs: none

① Derived judgments

Read, and nothing else: card #22158 (body and all four comments: claim 6051529167, os-dev-report 6053181767, seat ACCEPT 6053217012, patch-round os-dev-report 6054947204); the ruling of record 6051470224 on #8346, verbatim; PR #22193 (body as patched 2026-10-08T07:28Z, the 56-file list, its three comments 6053160817 / 6053373285 / 6053378296, the 14 commits, the net diff against main at the head: +1750 / −1570, and the round's delta against d4b428426: 3 commits, 8 files, +70 / −15); the head's check-runs. The PR head is the brief's head and has not moved. Nothing was built, run or re-run: files were fetched and read at the head (and, for the one pin whose history the round narrates, at main, d4b428426 and 9077995ae); the gate verdicts are the check-runs' conclusions, tallied at the end of ③. The record 6053373285 names d4b428426, which went red and will not land; it is used here as a map of what to check, and this head is judged whole.

Widenings (additive against main):

  1. PageSchema.print — optional, closed PagePrintSchema: paperSize, orientation, margins { top, right, bottom, left } (millimetres, non-negative), repeatHeader, repeatFooter, pageNumbers, repeatTableHeaders, avoidBreakInside; no .default() anywhere, one PagePrint type; every .describe() names the print-CSS mapping and the consumer (card ②, later ③). page-print.test.ts pins the key set exactly. Right — the ruling's own list ("paper size and orientation, margins, header and footer blocks, page numbering, page-break hints, mapped to print CSS (@page, repeated table heads, break-inside)"), nothing beyond it. Declared-equals-enforced holds: liveness/page.json carries the declaration as planned with authorWarn + authorHint and names card ② as the carrier (state-counts/page.md planned 0 → 11, classified 24 → 35, the generated count being the authority); the page form omits print with its ledger row in metadata-form-zod-reconciliation.test.ts. The aliases (paper, pdf, printLayout, …) and guidance (header, footer, template) are strictObject refusal prescriptions for near-miss spellings, not second spellings. Right.
  2. New @objectstack/spec/ui exports PagePrintSchema, PagePrint, PRINTABLE_PAGE_COMPONENT_TYPES, PRINT_REFUSED_PAGE_COMPONENT_TYPES, checkPagePrintComposition, mirrored in the api-surface / export-origins / declaration-map / json-schema.manifest (ui/PagePrint) / authorable-surface (+9 rows) shards and the regenerated references/ui/page.mdx, whose print describe at this head names the admitted page types. Right — one list in the spec is what lets card ② render the same set the lint enforces.
  3. New @objectstack/lint exports validatePrintPageBlocks, PRINT_PAGE_BLOCK_UNPRINTABLE and the type PrintPageBlockFinding; the rule validatePrintPageBlocks registered tier: 'gating', commands: ALL, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['page'], input: 'normalized'; the author-time rule count moves 49 → 50 in the four CLI transcript pages. Right.

Narrowings (each a refusal that did not exist before this head):

  1. Parse-time refusals, checkPagePrintComposition, exported and attached by identifier (pinned in object-refinement-check-exports.test.ts: +8 fixtures, one declaration and one attachment in the source, reference identity through ./index): print on kind: 'slotted' | 'html' | 'jsx' | 'react'; on type: 'list' (routed to interfaceConfig.allowPrinting); new on this head, on type: 'utility'; on a full page with no regions; repeatHeader / repeatFooter with no region of that name. Right. On the utility refusal in particular: PageTypeSchema at the head is the closed enum record | home | app | utility | list and type defaults to record; the check is written as membership in the closed PRINT_PAGE_TYPES (record, home, app) with a utility-specific reason, so a type added to the enum later is refused until admitted — closed like the subset — and type absent is the spec default record, admitted. Against main this is no narrowing at all, because print did not exist; against d4b428426 it narrows the key's accept set, in the voice of the list refusal, and every refusal and the print describe now name the admitted types from one phrase (PRINT_PAGE_TYPES_PHRASE). Pinned in page-print.test.ts: utility refused at print with "floating panel … not a document" and the admitted types named; record / home / app accepted with and without kind: 'full'; the list and kind refusals name the same admitted types; a utility page without print still parses. A floating utility panel is not "a page with a print declaration" in the ruling's sense — a document the console previews as the page itself — so refusing it is within the ruling and closes the loose end the prior record named. Right.
  2. The printable block subset: 13 printable, 39 refused each with its reason, and 4 retired-by-name (user:profile, element:filter, ai:chat_window, element:form) left to the parse — all three sets counted in page.zod.ts at the head. page-print.test.ts pins that every live member of KNOWN_COMPONENT_TYPES (the enum, the props-map rows and the string-arm registrations) is classified exactly once and that neither list names an undeclared or retired type, so a vocabulary member added tomorrow cannot reach a print page unclassified. validate-print-page-blocks.test.ts pins one case per refused type derived from the spec's map (path at the node's type, the reason, the printable list as the fix); a plugin / SDUI type refused with "nothing answers for how it prints"; refused blocks reached in every container position (children, a card's footer, a tab panel); the header and footer regions judged like any other region; every printable type in every container position passing; the ruling's invoice passing; a page without print never judged; retired types left to the parse and component-type-unknown; a malformed stack yielding nothing. The rule walks with the shared walkPageComponents (regions, container children, card footer, tab / accordion panels). Right as to the ruling's two mandatory refusals — every row-windowing block (object-grid, record:related_list, record:history, record:activity, object-timeline, object-tree) and every screen-laid-out block (page:sidebar, page:header, record:chatter, record:discussion, record:reference_rail, object-kanban, object-calendar, object-gantt, object-map) is refused — and as to its named body vocabulary (record:details / record:highlights, the all-rows table record:line_items, element:text, element:image) all being in. Unchanged since d4b428426 and re-read on this head.
  3. LIST_VIEW_EXPORT_PDF_RETIRED: 'pdf' stays refused in view.exportOptions formats (bare array and { formats } alike); "PDF export itself was declined as NOT PLANNED" is gone; the prescription names the view's allowPrinting for printing a list as shown and "a page that declares print" for a document; the os migrate meta --from 16 tail is untouched, so it stacks with open fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest #22142 either way. view.test.ts pins the pointer and not.toMatch(/NOT PLANNED|declined/). Right — the ruling's sentence to the letter, and Not-taken A (restoring 'pdf') not taken. On this head the same correction reaches the one hand-written doc sentence the ruling's words also falsify: content/docs/ui/views.mdx no longer says PDF export was declined and points at allowPrinting and the page reference. Right.
  4. The retirement of data/document.zod.ts whole: DocumentTemplateSchema, DocumentSchema, ESignatureConfigSchema and DocumentVersionSchema, with the eight type aliases, out of @objectstack/spec/data and the root entry. I read the deleted module: those are its four value exports, and DocumentVersionSchema is referenced in it only at DocumentSchema.versioning.versions. ADR-0087: four RETIRED_DEFS_BY_MAJOR[18] entries (entry files and the regenerated registry.ts), one D3 semantic entry document-schemas-retired (surface, FROM → TO replacement, reason, acceptance criteria, no conversionIds), a STEP18_RATIONALE fragment (order 88); the ESignatureConfig deadline-key rows stay in RETIRED_KEYS_BY_MAJOR[18] as history; generated baselines −4 defs / −30 authorable rows / −2 defaults; references/data/document.mdx removed and the data index, nav, llms.txt (203 → 202, data 31 → 30), PROTOCOL_MAP.md, the strictness ledger (+ counts) and the quick reference (16 of 30 → 15 of 29) brought to match; pin document-schemas-retirement.test.ts (registry rows, the exports gone from @objectstack/spec/data and the root entry, DocumentSchemaValidationSchema as the lit control, a tree-scoped absence walk over the declared radius with an anti-vacuity matcher case and floors). No tombstone and no D2 conversion is right: none of the four is a stack collection member or a metadata type, so a conversion would have no seam that runs (the spec: the rest of the incident-response, training and change-management families — every remaining key and all fifteen defs — has zero readers; whole-def enforce-or-remove is the open question left after #14477 #15513 precedent). Right for the ruling's three; the fourth is judged in ③.
  5. The pinned-sibling question an export removal must answer: the dev's census reports zero readers of the family in objectui at the .objectui-sha pin a58626c88 and at main cef0eee, in hotcrm 1e88edc, and in this repo outside packages/spec, each against a lit control (55 files name PageSchema; 148 name FieldSchema; 77 name defineStack / ObjectSchema); cloud is NOT MEASURED and says so. Console Pin Gate is path-filtered to skipped on this head, so the census is the evidence. I could not re-measure it from here (the code-search endpoint is closed to this session), so this record accepts the census as stated, with its controls, as the prior record did. Accepted.

The two moved pins — each still proves what its title says:

  • protocol.meta-types-degenerate-derivation.test.ts, page: 24 → 25. The control sums the served top-level properties and the retired top-level count and pins the sum to the card's number, so a degenerate derivation (an empty document, an input-side strip) cannot pass it. print is one declared top-level key of PageSchema — the only shape key the diff adds; the five new aliases are not properties — so the honest number is 25, recorded beside field 74 → 75 and object 43 → 44 with the same reason, and the twelve other counts are untouched. "control: page still serves 25 top-level properties" still proves the derivation serves the declared page whole. Right. The red at d4b428426 was this PR's, as 6053378296 said; Test Core (3/6) is green on this head.
  • type-alias-convention.pin.test.ts, 772 → 773. Counted on the fetched files, not taken from the report: main has 772 lines matching the companion's = Assert pattern and the literal 772; d4b428426 has 773 export type Iso lines but only 772 matching the pattern (one line spelled =Assert), literal 772 — green by accident, exactly as the round report says; 9077995ae has 773 matching and the literal 772 — red; the head has 773 matching, zero no-space lines, and the literal, the case title and the header prose all say 773, with an arrow entry naming the cause. PagePrintSchema carries no default and no transform, so it belongs in the isomorphic family. The pin now counts every pin it declares. Right, and the prior record's =Assert nit is closed.

The Not-taken list, checked against the diff: no 'pdf' on list export; no second authoring vocabulary (the running header and footer are the page's own regions); no Word-upload template kind; no batch merge or packaged download; no in-process layout driver. None taken. Governed surfaces: 0 of 56 paths; Governed Surface Queue Guard green.

② Semver level

.changeset/22158-print-page-spec.md: @objectstack/spec: minor, @objectstack/lint: minor; Clause-②: yes (narrowing) at a line start in the changeset and in the PR body; a BREAKING section with the FROM → TO table and the one-line fix ("delete the import; author a printable document as a page with a print block"); exactly one ADR-0087 disposition marker, adr-0087: registered document-schemas-retired, matching the D3 entry's id. The round's one changeset edit adds utility to the refused-at-parse sentence. .changeset/pre.json is absent at the head and on main (both 404 when read); @objectstack/spec and @objectstack/lint are 17.7.0 at the head. AGENTS.md's rule — "yes takes at least minor, (narrowing) is BREAKING", a breaking changeset carries its migration (FROM → TO and the one-line fix) and states its ADR-0087 disposition in writing — is met on every clause: the print declaration and the lint rule are additive, the export removal is the narrowing, the migration and the marker are in the body. Check Changeset on the head: success, twice (the second on the PR-body re-trigger). The Clause-②: line is correct. The changeset's phrase "under the launch-window convention" is not AGENTS.md's spelling (a nit in ③); the rule it invokes is.

③ Boundary flags

Every flag of both rounds and both first-round open_questions, each judged against the ruling's own words on this head.

Open question 2 — the running header and footer as the page's own header / footer regions (repeatHeader / repeatFooter), not block arrays under print. WITHIN THE RULING. The ruling's declaration list says "header and footer blocks"; its design sentences say "The template is a page. The body vocabulary is the page's own blocks", "no new visual designer: Studio's page designer is the designer, the print declaration is a few fields on it", and Not-taken B refuses "a second authoring vocabulary beside pages". The header and footer ARE blocks: authored in the regions every page walker already walks (lint's walkPageComponents, the ADR-0087 conversion walker, walkAddressedPageComponents behind translatePage), designed in the page designer and placed top and bottom on screen, so the printed sheet and the on-screen preview agree; the two booleans govern repetition on every sheet; print.header / print.footer are answered by guidance naming the region; and this head's lint pin judges the header and footer regions like any other region. A block array under print would be a second composition root — the opposite of "a few fields on it". A print-only letterhead that differs from the screen header would be additive later. No director ruling needed; the seat's A stands.

Open question 1 — the 13-type allow list against a narrower list. WITHIN THE RULING. The ruling says "A printable block subset is defined and linted: a block that virtualises rows or lays out responsively is refused inside a print page, with pins" — it prescribes what MUST be refused and leaves the subset to be defined. Every block its two criteria reach is refused (①.5); every body-vocabulary kind it names is in; the additions (element:definition-list, element:divider, element:repeater, element:number, object-metric, and the three containers) each draw all they declare at any width, by their declarations at the head. Refusing controls, inputs, shell chrome, folding containers and per-viewer banners goes beyond the ruling's two reasons but contradicts nothing in it, and the allow list is the loud form: widening is additive on a named pull, while narrowing later would be breaking. Option B (the ruling's four named kinds plus containers) would refuse object-metric and element:number, pushing a monthly report's totals onto blocks the subset then refuses — against the ruling's own second-axis sentence (the port customer's monthly report). No director ruling needed; the seat's A stands. First candidates for a named widening pull, for the record: record:alert when not dismissible, and object-chart (outside the vocabulary today).

Deviation 2 — DocumentVersionSchema retires as a fourth export. WITHIN THE RULING. The ruling's operative criterion is zero-reader under ADR-0049 ("The zero-reader … retire in v18 under ADR-0049 with ADR-0087 entries"); it names the three top-level schemas. DocumentVersionSchema's only carrier was DocumentSchema.versioning.versions (read in the deleted module), so the ruling's own removal makes it a zero-reader export by construction, and ADR-0049 enforce-or-remove then takes it — keeping it would leave exactly the declared-unenforced surface the ruling removes. It is registered the same way (the data/DocumentVersion def entry, named in the D3 entry and in the changeset's FROM → TO). Restorable as one entry file and one export if the director reads the ruling narrower; I do not.

Deviation 4 — the subset rule on the page save door from birth (CLI_AND_RUNTIME, runtimeTypes: ['page']), while its sibling validateComponentTypes is CLI-only. WITHIN THE CARD AND THE RULING. The card asks for "os validate / os build and the save door"; the ruling says "the page's blocks are validated at publish today and the declaration adds a few checked fields". The false-refusal budget the sibling waits on is zero here by construction: PageSchema is closed and print did not exist, so no stored tenant row and no config file can carry it; the judgment is page-local, so the one-page per-write snapshot is its whole input. The rule's header and its registry entry both state this. The dev's save-door probe through runRuntimeAuthoringRules({ type: 'page' }) and the head's Test Core runs cover the wiring.

Deviations 1 and 3 (regions instead of H1's block arrays; the structural refusals in the schema beside the two existing PageSchema refinements, the block walk in lint): mechanism choices the ruling does not constrain. The parse-time refusals reach every door; the nested walk reuses the page-block family's shared walker. Right.

Deviation 5 — files outside the claim's declared surface (pins, transcript counts, ledger rows, llms.txt, PROTOCOL_MAP.md, the strictness ledger, the quick reference, vitest.repo-tests.json, and on this head the metadata-protocol control): each reads in the diff as a gate-forced or kit-forced edit carrying no behaviour. Accepted.

Patch-round flags (os-dev-report 6054947204), each answered:

  • The utility check written as membership in a closed PRINT_PAGE_TYPES rather than a bare type === 'utility' branch — right (①.4): closed like the subset; type absent admitted as the spec default record.
  • The app-root prose pin's count of backticked app in page.zod.ts staying at 3 because the new refusals spell the admitted types in quoted form — a wording constraint from an existing pin, not a behaviour; the spec suite is green on the head. Accepted.
  • The PR-body PATCH, the worktree lifecycle and the lock-queue timeouts (re-queued; the reported runs exit 0) — process notes this record does not rely on: the head's check-runs are the gates.
  • open_questions: [] and out_of_scope_findings: [] this round; the first round's findings keep their carriers below.

Out-of-scope findings — carriers judged:

  • content/docs/ui/views.mdx still saying PDF export was declined — closed on this head (①.6).
  • objectui still carries the old "declined as NOT PLANNED" sentence — card ②'s. Right.
  • "The Studio author's skill learns the print page when ① lands": skills/** is Tier H and untouched here (0 of 56 paths); the seat's governed PR is the carrier. Right.
  • docs/NEXT_STEP.md:55 still planning ui/document-template.zod.ts: a stale roadmap line, acceptance note. Right.
  • The page template (multi-column) not judged by the subset, and object-chart refused: card ②'s call and a named pull. Right.

Nits for the seat (none blocks the verdict):

  • record:line_items prints every line only up to its 500-row renderer cap; card ②'s print rendering should lift the cap under print or make the truncation visible on paper.
  • Prose counts, unchanged from the first round: the liveness README's "eleven planned rows (the container, its eight keys and the four margins sides)" lists thirteen entries for eleven counted leaves, and the PR body's "10 nested keys" is loose the same way (twelve nested entries). The generated state-counts/page.md (planned 11) is the authority; the prose is loose, not wrong.
  • The changeset's "shipped as minor under the launch-window convention for accept-set narrowings" names a house convention in text that ships to consumers as CHANGELOG.md; AGENTS.md's spelling is the rule itself. A one-line reword before landing is the seat's call; once the release consumes the changeset the sentence is release-owned.

Check-runs on the head 0b96e61e1 (their conclusions are the gate verdicts; two reads, none waited on): first read 2026-10-08T07:33:03Z and final read 2026-10-08T07:42:00Z, identical — 42 runs, all completed: 38 success, 4 skipped, 0 failure, 0 in progress. Green, every distinct name: Lint & Repo Gates; Test Core (parent and 1/6 – 6/6); Build Core; Build Docs; Type Check (source gates, debt ledger, consumer gates, workspace) and TypeScript Type Check; Dogfood Regression Gate (parent and 1/3 – 3/3); Dogfood Verify CLI; Temporal Conformance (live PG + MySQL); Check Changeset; Spec property liveness; Governed Surface Queue Guard; Check Documentation Links; Flag docs affected by code changes; Check PR Size; Auto Label; filter; and the four PR-context checks (the card this PR closes must claim this branch; no other open PR may claim the same issue; no other open PR may claim the same single-writer path; part-of PR must not also close its card). The PR-body edit at 07:28Z re-ran the PR-context checks and Check Changeset on the same head, green again. Skipped: Console Pin Gate and Packed-tarball smoke (opt-in), both path-filtered, plus one duplicate skipped instance each of Auto Label and Check PR Size from that re-trigger, whose earlier instances succeeded. The combined commit status is success (one Vercel status, build ignored). Nothing was in progress at either read: this PASS stands on a fully settled head.

Implemented-by: claude/issue-22158-print-page-spec
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

Rendered 2026-10-08T07:42Z; an isolated contract-review subagent of the session named above, read-only. The record 6053373285 names d4b428426 and does not cover this head; this is the first record on 0b96e61e1.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 07:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 07:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 8, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as draft October 8, 2026 09:02
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Removed from the merge queue on a conflict. Back to draft, needs:contract-review back on, and a merge round dispatched

domain:spec seat 3 (#18883) · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-08T09:03Z.

claude added 3 commits October 8, 2026 09:04
…int-page-spec

# Conflicts:
#	packages/spec/src/ui/list-view-export-options.ts
The merge script's collection point: content/docs/references/ui/page.mdx
regenerated from the merged sources, so main's --write sentence and this
branch's print describe compose.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…angesets pre mode

main entered pre mode (tag next) with the v18 opening marker; the no-major
guard stands aside in pre mode, so the export removal is graded what it is.
@objectstack/lint stays minor. Clause-② yes (narrowing) unchanged: the
print key and the new exports widen, the retirements narrow.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9f9fa3dba1c6046ce9dffc8f385c73aaae367c5f
Local-runs: none

① Derived judgments

Read, and nothing else: card #22158 (body and all five comments: claim 6051529167, os-dev-report 6053181767, seat ACCEPT 6053217012, patch-round report 6054947204, merge-round report 6057572366); the ruling of record 6051470224 on #8346, verbatim; PR #22193 (body as patched in the merge round, the 56-file list, its five comments 6053160817 / 6053373285 / 6053378296 / 6055163536 / 6056492441, the 17 commits, the net diff against main at the head — 31cd2104d...9f9fa3dba, 56 files, +1750 / −1570 — and the round's delta against 0b96e61e1); the head's check-runs. The PR head is the brief's head and has not moved (read 2026-10-08T10:11Z). Nothing was built, run or re-run: files were fetched and read at the head, at main 31cd2104d, at 0b96e61e1 and at their merge base 959c209d5; the gate verdicts are the check-runs' conclusions, tallied at the end of ③. The record 6055163536 (PASS on 0b96e61e1) is used as a map; this head is judged whole, and where it differs is said.

Where this head differs from 0b96e61e1, measured: the round adds three commits — the merge commit bf3359dff (second parent origin/main 31cd2104d, 31 of main's commits, no PR content), the regeneration 5f68ebe48 and the changeset regrade 9f9fa3dba. 0b96e61e1 is an ancestor of the head (compare: ahead 34, behind 0), so no history was rewritten. Hunk for hunk, 52 of the 56 files in the net diff are byte-identical to the 0b96e61e1 net diff against 959c209d5; the four that differ are .changeset/22158-print-page-spec.md (the level, ②), packages/spec/src/ui/list-view-export-options.ts (the conflict, ①.6), packages/spec/src/ui/view.test.ts (auto-merged: one assertion now expects main's --write closing sentence; this PR's pointer pins and the not.toMatch(/NOT PLANNED|declined/) pin are unchanged) and content/docs/references/ui/page.mdx (regenerated on the merged tree: the index line, and the assignedProfiles / responsive REMOVED rows now carry main's --write sentence as context — main's content, not this PR's; the print rows are unchanged). Everything judged in ① below was re-read on this head.

Widenings (additive against main):

  1. PageSchema.print — optional, closed PagePrintSchema: paperSize (A4 | A5 | Letter | Legal), orientation, margins { top, right, bottom, left } (millimetres, min(0), closed, side aliases up / down / start / end), repeatHeader, repeatFooter, pageNumbers, repeatTableHeaders, avoidBreakInside. No .default() anywhere, one PagePrint type (isomorphic pin added). Every .describe() names its print-CSS mapping and its consumer (the console's browser print rendering, card ②; later the server-side renderer, card ③). Right — the ruling's list ("paper size and orientation, margins, header and footer blocks, page numbering, page-break hints, mapped to print CSS (@page, repeated table heads, break-inside)"), nothing beyond it; page-print.test.ts pins the key set exactly. Declared-equals-enforced holds: liveness/page.json carries print and its nested keys as planned (state-counts/page.md planned 0 → 11, classified 24 → 35) with authorWarn + authorHint on the container and card ② named as the carrier in every row; the page form omits print with its ledger row in metadata-form-zod-reconciliation.test.ts. The page-level aliases (printLayout, printSettings, printConfig, pdf, paper → print) and the declaration's own aliases / guidance (header, footer, template) are strictObject refusal prescriptions for near-miss spellings, not second spellings. Right.
  2. New @objectstack/spec/ui exports PagePrintSchema, PagePrint, PRINTABLE_PAGE_COMPONENT_TYPES, PRINT_REFUSED_PAGE_COMPONENT_TYPES, checkPagePrintComposition (ui/index.ts is export * from page.zod), mirrored in api-surface/ui.json (+5), export-origins/ui.json (+5), declaration-map/ui.json (+2), json-schema.manifest/ui.json (ui/PagePrint), authorable-surface/ui.json (+9: ui/Page:print and the eight ui/PagePrint:* rows) and the regenerated references/ui/page.mdx (the print row, the Page.print nested shape, the PagePrint section with its margins shape; "6 more" → "7 more"; the UI protocol's schema count 165 → 166 in the references index). Right — one list in the spec is what lets card ② render the same set the lint enforces.
  3. New @objectstack/lint exports validatePrintPageBlocks, PRINT_PAGE_BLOCK_UNPRINTABLE and the type PrintPageBlockFinding; the rule registered in authoring-rules.ts as tier: 'gating', input: 'normalized', commands: ALL, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['page']; the author-time rule count 49 → 50 in the four CLI transcript pages. Right.

Narrowings (each a refusal that did not exist before this head):

  1. Parse-time refusals, checkPagePrintComposition, exported and attached by identifier (object-refinement-check-exports.test.ts: +8 fixtures, one declaration and one attachment in the source, reference identity through ./index). Checked against the enums at the head: kind is full | slotted | html | react | jsx (default full), type is record | home | app | utility | list (default record), regions defaults to []. The check refuses print on every non-full kind (slotted: the synthesized default layout; html / jsx: compiled from source; react: executed from source), on type: 'list' (routed to interfaceConfig.allowPrinting), on any type outside the closed PRINT_PAGE_TYPES (record, home, app) — today utility, with its own reason, and a type added to the enum later until admitted — on a full page with no regions, and repeatHeader / repeatFooter with no region of that name; every refusal names the admitted types from one phrase. type absent is the spec default record, admitted; kind absent is full. Pinned in page-print.test.ts (each kind at print; list; utility; both empty-regions spellings; each repeat key at its own path, with false needing no region; record / home / app accepted with and without kind: 'full'; the same pages without print still parse). Right — against main this is the shape of a key that did not exist, not a narrowing of anything accepted before; a print page is a kind: 'full' page of type: 'record', 'home' or 'app' with its blocks in regions, which is the only page whose authored blocks are the whole printed body.
  2. The printable block subset: 13 printable (three containers, two field blocks, record:line_items, five element:* content blocks, element:number, object-metric — counted) and 39 refused each with its reason (counted: 6 that window their rows, 9 that lay out to the screen, 24 with nothing printable of their own), the 4 retired-by-name types left to the parse. page-print.test.ts pins that the two lists classify every live member of KNOWN_COMPONENT_TYPES exactly once and name nothing undeclared or retired, so a vocabulary member added tomorrow cannot reach a print page unclassified. validate-print-page-blocks.test.ts pins one case per refused type derived from the spec's map (path at the node's type, the reason, the printable list as the fix), the ruling's two reasons on object-grid and object-kanban, undeclared types (flex, object-chart, a plugin widget) refused with "nothing answers for how it prints", refused blocks reached in every container position the shared walkPageComponents visits (a section's children, a card's footer, a tab panel's children, and the page:tabs node itself), the header and footer regions judged like any other region, every printable type passing in every container position, the ruling's invoice passing, a page without print never judged, only the print page of two judged, retired types skipped (the parse and component-type-unknown already answer), a malformed stack yielding nothing. The walker at the head reaches regions[].components, containers' properties.children, page:card.footer, page:tabs / page:accordion items[].children and slots (moot: a slotted page is refused at the parse). Right as to the ruling's two mandatory refusals (every row-windowing block and every screen-laid-out block is refused) and as to its named body vocabulary (field blocks, the all-rows table record:line_items, text, images) all being in; the borderline rows (object-metric, element:number, element:repeater in; record:alert, record:path out) stand as the prior records judged them by their declarations.
  3. The conflict resolution in list-view-export-options.ts, judged on its own. Four points read whole: the merge base 959c209d5, ours 0b96e61e1, theirs main 31cd2104d, the result at the head. Base → theirs (what fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest #22142 did here) is exactly one line: the closing sentence gains "--write applies the ones it can prove, and you apply the rest by hand." Ours → result is exactly that same one line. Theirs → result is exactly this PR's rewrite: the [#22158] comment block, the body of LIST_VIEW_EXPORT_PDF_RETIRED with "(PDF export itself was declined as NOT PLANNED)" gone and the two live answers in its place (the view's allowPrinting for a list as shown; "a printable document (an invoice, a delivery order, a letter) is a page that declares print — its paper, margins and running header and footer — with its blocks in regions"), and the ListViewExportFormatSchema docblock naming the ruling. Both intents kept, nothing of either side dropped. 'pdf' is still refused: ListViewExportFormatSchema is z.enum(['csv', 'xlsx', 'json']) with its error map keyed on issue.input === 'pdf', unchanged from base. No conflict marker in the 139-line file. The resolved string carries no issue number (the not.toMatch(/#\d{3,5}\b/) pin), no "NOT PLANNED" / "declined", and closes with the exact class sentence retired-key-migrate-sentence.test.ts requires at the head (its ^Run \os migrate meta --from \d+` … `--write` applies the ones it can prove, and you apply the rest by hand.pattern).view.test.tsat the head carries both sides' pins. **Right** — the ruling's sentence to the letter, Not-taken A not taken, and the serial constraint the claim named ("whichever lands later mergesmainand stacks both intents") discharged as written. The hand-writtencontent/docs/ui/views.mdx` sentence stays corrected from the patch round. Right.
  4. The retirement of data/document.zod.ts whole: DocumentTemplateSchema, DocumentSchema, ESignatureConfigSchema and DocumentVersionSchema, with the eight type aliases, out of @objectstack/spec/data and the root entry (data/index.ts drops the export * and records why). Read in the deleted module: those are its four value exports, and DocumentVersionSchema is referenced only at DocumentSchema.versioning.versions. ADR-0087: four RETIRED_DEFS_BY_MAJOR[18] entries (entry files and the regenerated registry.ts), one D3 semantic entry document-schemas-retired (surface, FROM → TO replacement naming the print page, reason, acceptance criteria, no conversionIds), a STEP18_RATIONALE fragment (order 88); the ESignatureConfig deadline-key rows stay in RETIRED_KEYS_BY_MAJOR[18] as history. Generated: json-schema.manifest/data.json −4 defs, authorable-surface/data.json −30 rows (counted), authorable-defaults/data.json −2, api-surface −12, declaration-map −8, export-origins −12, references/data/document.mdx removed with the data index, nav and references index brought to match (196 → 195 pages, 1517 → 1514 schemas; data 30 → 29 pages, 178 → 174 schemas), llms.txt 203 → 202 / data 31 → 30, the PROTOCOL_MAP.md row, the strictness ledger's two document.zod.ts rows with their counts (data 163 → 155, strip 82 → 74; ui 208 → 210 for the two new page.zod.ts sites), the quick reference (16 of 30 → 15 of 29). The module's own tests leave with it (document.test.ts, 26 cases over the four schemas; esignature-deadline-keys-retirement.test.ts, importing only the deleted module and the registry). Pin: document-schemas-retirement.test.ts (the four registry rows, the D3 entry, the rationale, the deadline keys as history, the exports absent from @objectstack/spec/data and the root, DocumentSchemaValidationSchema as the lit control, a tree-scoped absence walk over the declared radius with anti-vacuity matcher cases and floors), registered in vitest.repo-tests.json; scripts/file-description.test.ts renames a fixture that spelled the deleted schema. No tombstone and no D2 conversion is right: none of the four is a stack collection member or a metadata type, so a conversion would have no seam that runs (the spec: the rest of the incident-response, training and change-management families — every remaining key and all fifteen defs — has zero readers; whole-def enforce-or-remove is the open question left after #14477 #15513 precedent). Right for the ruling's three; the fourth is judged in ③.
  5. The pinned-sibling question an export removal must answer: the dev's census reports zero readers of the family in objectui at the .objectui-sha pin a58626c88 and at main cef0eee, in hotcrm 1e88edc, and in this repo outside packages/spec, each against a lit control (55 files name PageSchema; 148 name FieldSchema; 77 name defineStack / ObjectSchema); cloud is NOT MEASURED and says so. Console Pin Gate is path-filtered to skipped on this head, so the census is the evidence, and the in-repo radius is also walked mechanically by the new pin (Test Core green). I could not re-measure the sibling repos from here; this record accepts the census as stated, with its controls, as the prior records did. Accepted.

The two moved pins, re-read on this head: protocol.meta-types-degenerate-derivation.test.ts page: 24 → 25 — print is the one declared top-level key the diff adds (the five new page-level aliases are not properties), recorded beside field and object's own moves, twelve other counts untouched; type-alias-convention.pin.test.ts 772 → 773 — the PagePrintSchema pin added, the Iso_ui_page__PageTypeSchema line spelled = Assert again, literal, case title and header prose all 773 with an arrow entry. Right, both.

The Not-taken list, checked against the diff: no 'pdf' on list export; no second authoring vocabulary (the running header and footer are the page's own regions); no Word-upload template kind; no batch merge or packaged download; no in-process layout driver. None taken. Governed surfaces: 0 of 56 paths; Governed Surface Queue Guard green.

② Semver level

.changeset/22158-print-page-spec.md at the head: @objectstack/spec: major, @objectstack/lint: minor; Clause-②: yes (narrowing) at a line start in the changeset and in the PR body; a BREAKING section with the FROM → TO table and the one-line fix ("delete the import; author a printable document as a page with a print block"); exactly one ADR-0087 disposition marker, registered document-schemas-retired, matching the D3 entry's id at the head; the title carries the bang. The round's one changeset edit is the level minor → major on @objectstack/spec and the BREAKING banner's reason sentence.

The release state at merged main 31cd2104d, read: .changeset/pre.json is present — { "mode": "pre", "tag": "next" } (a87d8be29, #22084) — beside the v18 opening marker 22080-v18-line-opens.md (@objectstack/spec: major, Clause-②: no); the packages are 17.7.0 until the next version cut. The rules, read at the head: AGENTS.md's checklist — "yes takes at least minor, (narrowing) is BREAKING", a breaking changeset carries its migration and states its ADR-0087 disposition — is met on every clause, and major is "at least minor". The launch-window convention that breaking ships minor is stated in pr-automation.yml as a consequence of the gate ("During the launch window major stays refused by check-changeset-no-major and breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level"), and scripts/check-changeset-no-major.mjs stands that refusal down in pre mode by its own text ("a major bump only ever produces a X.0.0-tag.N PRE-RELEASE version … Accumulating the next major's breaking changes is precisely what an RC window is FOR, so this guard stands aside for the duration"; judge returns exempt, exit 0, printing the introduced majors as notices). With the refusal lifted, the level is free to say what the change is: an export removal on a published entry is a major-class change, so major on @objectstack/spec matches what the diff publishes, and because the fixed group already carries the opening major, the grade moves no version on its own. @objectstack/lint: minor is right: new exports and a new gating rule that can speak only about a page carrying print, a key the closed PageSchema refused at every door before this PR, so no input that passed before is refused now. The gate's LEVEL AXIS reads yes (narrowing) with no moved package graded patch — clean. Check Changeset on the head: success, twice (09:22Z, and 10:09Z on the PR-body re-trigger). a87d8be29's edit to check-adr-0087-registration.mjs is one constant extraction (CONSUMED_PRERELEASE_EXCLUDED), read in its diff; the disposition rule is unchanged, and the marker names an id the registry carries.

The Clause-②: line against the current definition. The criterion (execution-duties.md:67, 「本卡放宽接受集或扩大公开面吗」) answers yes: PageSchema accepts a new key, and @objectstack/spec/ui and @objectstack/lint gain exports. The arm (narrowing) names the export removal. scripts/pm/clause2-line.mjs reads the line-initial Clause-②: yes (narrowing) as declared, value yes, arm narrowing, and names this combination exactly: "a diff that widens one surface and narrows another. Both facts are true and both are read." The line 5d5a88eff added (execution-duties.md:105, 「收窄已发布接受集的卡是 Clause-②: no,不移车道;入队前欠一次契约复审档复核」) governs a card that only narrows a published accept set; this card also widens, so yes is the truthful value and no (narrowing) would be false to the first fact — and the contract-review-tier review that line owes before the queue is this record. The Clause-②: line is correct.

③ Boundary flags

Every flag of all three rounds and both first-round open_questions, each judged against the ruling's own words on this head.

The merge-queue removal (6056492441) and what it asked of this round — ANSWERED. The one conflict is resolved with both intents and nothing dropped (①.6); the generated files were regenerated on the merged tree (5f68ebe48; the net diff's generated shards match the schema at the head, and Lint & Repo Gates / Build Docs are green); the changeset level and the Clause-② line were re-derived from the rules at merged main (②). The two main changes the seat named — a87d8be29 (pre mode) and 5d5a88eff (the narrowing rule) — are each read above and each bears exactly as the seat said.

Merge-round deviation 1 — the spec-property-retirement skill's changeset line ("@objectstack/spec 用 minor,⛔ 不用 major") predates pre mode; the dev followed the gate and left the governed skill alone. RIGHT. The skill line's stated reason is the gate's refusal ("Check Changeset 跑 scripts/check-changeset-no-major.mjs,发射窗口期内拒收 major,照抄即红"), and that refusal is suspended in pre mode by the gate's own text, so the line's premise has lapsed for the window; editing .claude/skills/** is a governed act outside this card. Escalated to the seat, not blocking: the skill line should be reworded to say the level is minor only while check-changeset-no-major refuses major, in a governed PR of its own.

Merge-round deviation 2 — check-changeset-no-major.mjs's LEVEL AXIS arm prose still prints "a BREAKING change; during the launch window it ships minor" while the same run reports the pre-mode exemption. Informational text at :1502, exit 0 either way; stale under pre mode. Noted for the gate's owner; not blocking.

Merge-round deviation 3 — main moved 11 commits past the merge, one touching packages/lint/src/authoring-rules.ts; no second merge taken. GitHub reads the PR mergeable and clean against the current base (read 10:11Z); this PR's authoring-rules.ts hunk is +21 / −0 and identical in GitHub's three-dot file list and in the net diff, so the later main change there merges clean. The seat's landing checks own the next merge if one is needed. Not blocking.

Merge-round deviation 4 (worktree lifecycle) and the patch-round process notes (PR-body PATCH, lock-queue timeouts re-queued): process notes this record does not rely on; the head's check-runs are the gates.

Open question 2 — the running header and footer as the page's own header / footer regions (repeatHeader / repeatFooter), not block arrays under print. WITHIN THE RULING. The ruling's declaration list says "header and footer blocks"; its design sentences say "The template is a page. The body vocabulary is the page's own blocks", "no new visual designer: Studio's page designer is the designer, the print declaration is a few fields on it", and Not-taken B refuses "a second authoring vocabulary beside pages". The header and footer ARE blocks, authored in the regions every page walker already walks, designed in the page designer and placed top and bottom on screen, so sheet and preview agree; the two booleans govern repetition on every sheet; print.header / print.footer are answered by guidance naming the region; the lint judges those regions like any other. A block array under print would be a second composition root — the opposite of "a few fields on it". No director ruling needed; the seat's A stands.

Open question 1 — the 13-type allow list against a narrower list. WITHIN THE RULING. The ruling prescribes what MUST be refused ("a block that virtualises rows or lays out responsively") and leaves the subset to be defined. Every block its two criteria reach is refused (①.5); every body-vocabulary kind it names is in; the additions each draw all they declare at any width, by their declarations at the head. Refusing controls, inputs, shell chrome, folding containers and per-viewer banners goes beyond the two reasons but contradicts nothing in the ruling, and the allow list is the loud form: widening is additive on a named pull, narrowing later would be breaking. Option B would refuse object-metric and element:number, pushing a monthly report's totals onto blocks the subset then refuses — against the ruling's own second-axis sentence (the port customer's monthly report). No director ruling needed; the seat's A stands. First candidates for a named widening pull, for the record: record:alert when not dismissible, and object-chart (outside the vocabulary today).

Deviation 2 — DocumentVersionSchema retires as a fourth export. WITHIN THE RULING. The ruling's operative criterion is zero-reader under ADR-0049 ("The zero-reader … retire in v18 under ADR-0049 with ADR-0087 entries"); it names the three top-level schemas. DocumentVersionSchema's only carrier was DocumentSchema.versioning.versions (read in the deleted module), so the ruling's own removal makes it a zero-reader export by construction, and ADR-0049 enforce-or-remove then takes it — keeping it would leave exactly the declared-unenforced surface the ruling removes. It is registered the same way (the data/DocumentVersion def entry, named in the D3 entry and in the changeset's FROM → TO). Restorable as one entry file and one export if the director reads the ruling narrower; I do not.

Deviation 4 — the subset rule on the page save door from birth (CLI_AND_RUNTIME, runtimeTypes: ['page']), while its sibling validateComponentTypes is CLI-only. WITHIN THE CARD AND THE RULING. The card asks for "os validate / os build and the save door"; the ruling says "the page's blocks are validated at publish today and the declaration adds a few checked fields". The false-refusal budget the sibling waits on is zero here by construction: PageSchema is closed and print did not exist, so no stored tenant row and no config file can carry it; the judgment is page-local, so the one-page per-write snapshot is its whole input. The rule's header and its registry entry both state this; the dev's save-door probe and the head's Test Core runs cover the wiring.

Deviations 1 and 3 (regions instead of H1's block arrays; the structural refusals in the schema beside the two existing PageSchema refinements, the block walk in lint): mechanism choices the ruling does not constrain. Right.

Deviation 5 — files outside the claim's declared surface (pins, transcript counts, ledger rows, llms.txt, PROTOCOL_MAP.md, the strictness ledger, the quick reference, vitest.repo-tests.json, the metadata-protocol control): each reads in the diff as a gate-forced or kit-forced edit carrying no behaviour. Accepted.

Patch-round flags (6054947204), each still answered on this head: the utility check as closed-set membership rather than a bare branch — right (①.4); the app-root prose pin's count held by spelling the admitted types in quoted form — a wording constraint, spec suite green; open_questions: [] and out_of_scope_findings: [] in both later rounds.

Out-of-scope findings — carriers judged, unchanged: content/docs/ui/views.mdx — closed on this head (①.6); objectui's old "declined as NOT PLANNED" sentence — card ②'s; "the Studio author's skill learns the print page when ① lands" — skills/** is governed and untouched (0 of 56 paths), the seat's governed PR is the carrier; docs/NEXT_STEP.md:55 — a stale roadmap line; the page template and object-chart — card ②'s call and a named pull. Right, each.

Nits for the seat (none blocks the verdict):

  • record:line_items prints every line only up to its 500-row renderer cap; card ②'s print rendering should lift the cap under print or make the truncation visible on paper.
  • Prose counts, unchanged: the liveness README's "eleven planned rows (the container, its eight keys and the four margins sides)" lists thirteen entries for eleven counted leaves, and the PR body's "10 nested keys" is loose the same way. The generated state-counts/page.md (planned 11) is the authority.
  • The prior record's changeset nit (a house convention named in consumer-facing text) is resolved in substance: the banner now states the release state the grade rests on.

Check-runs on the head 9f9fa3dba (their conclusions are the gate verdicts; two reads, none waited on): first read 2026-10-08T10:12:12Z and final read 2026-10-08T10:20:03Z, identical — 42 runs, all completed: 38 success, 4 skipped, 0 failure, 0 in progress. Green, every distinct name: Lint & Repo Gates; Test Core (parent and 1/6 – 6/6); Build Core; Build Docs; Type Check (source gates, debt ledger, consumer gates, workspace) and TypeScript Type Check; Dogfood Regression Gate (parent and 1/3 – 3/3); Dogfood Verify CLI; Temporal Conformance (live PG + MySQL); Check Changeset (twice); Spec property liveness; Governed Surface Queue Guard; Check Documentation Links; Flag docs affected by code changes; Check PR Size; Auto Label; filter; and the four PR-context checks (the card this PR closes must claim this branch; no other open PR may claim the same issue; no other open PR may claim the same single-writer path; part-of PR must not also close its card), each green at 09:21Z and again at 10:08Z on the PR-body re-trigger. Skipped: Console Pin Gate and Packed-tarball smoke (opt-in), both path-filtered, plus one duplicate skipped instance each of Auto Label and Check PR Size from the 10:07Z re-trigger, whose earlier instances succeeded. The combined commit status is success (one Vercel status, build ignored). Nothing was in progress at either read: this PASS stands on a fully settled head.

Implemented-by: claude/issue-22158-print-page-spec
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

Rendered 2026-10-08T10:20Z; an isolated contract-review subagent of the session named above, read-only. The record 6055163536 names 0b96e61e1 and does not cover this head; this is the first record on 9f9fa3dba.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 10:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 10:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 326a90a Oct 8, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22158-print-page-spec branch October 8, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants