Skip to content

fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest - #22142

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-9591-retirement-sentence-write
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-9591-retirement-sentence-write

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #9591
Clause-②: no (prescription text only; no input's accept or reject result changes)

This is the spec-lane half of the card: the shared retirement sentence names --write, and the class-wide pin moves in the same PR. The codemod itself landed in PR #22108 (a959493cdf).

The sentence

Before (the #9529 wording):

Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand.

After:

Run `os migrate meta --from N` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.

The one allowed two-clause variant (a conversion that covers only part of a value) carries the same clause: … to list the mechanical edits for the X case; --write applies the ones it can prove, and WHAT-HAPPENS-TO-THE-REST. Its two members are dashboard compareTo.offset and the script node's config.actionType.

Checked against the tool on main (51290bca). packages/cli/src/commands/migrate/meta.ts declares write: Flags.boolean({ … default: false, exclusive: ['stored'] }). Its help text says it rewrites the authored sources in place "for each mechanical change traced to one literal in one project file; every other change is listed with the reason it was not written". Without the flag the run writes only the --out snapshot. The wording satisfies every ruling that binds it:

  • Triage 6045697201. The sentence never says "rewrite existing sources automatically" unqualified ("the ones it can prove"), and it names --write because the default run still only lists.
  • "It must be TRUE of the tool." Every clause is a property of the command, read from meta.ts.
  • "One antecedent." "existing sources" still names one thing. "The ones" can only be edits, because an edit is what gets applied. The key's fate stays in the body prose.
  • Vocabulary. The wording matches PR docs(skills): objectstack-upgrade names os migrate meta --write beside the default run #22122's skill text ("lists the mechanical edits"; --write "rewrites in place each edit it can trace to one literal in one project file, lists every other with the reason it was not written").

Where it moved (counted at 017761f0; the merge of main added no site)

  • 161 sentences the pin judges. packages/spec/src: 157 (155 house form, 2 two-clause). packages/lint/src: 1. packages/drivers/driver-turso/src: 3. Every one passes the new anchors; apply them by hand survives only in the pin's own RED fixtures.
  • Not judged by the pin, moved anyway:
    • migrations/registry.ts: 3 sentences, regenerated from the moved entries/semantic/18.*.ts by gen:migration-registry.
    • The lint chartConfig.xAxis.field hint in validate-widget-bindings.ts: a template literal with interpolation after the sentence, so the pin cannot see it (Acceptance notes).
    • The retiredKey() docblock example.
  • Mechanical replacement. A script replaced the tail apply them by hand in 63 files (188 occurrences; old tail left: 0) and printed per-file before/after counts. Two seams split mid-phrase (translation.zod.ts) and the two two-clause sites were rewritten by anchored edits that had to hit exactly once.
  • Pins in other test files. 23 test files asserted the old sentence verbatim, as string or regex. Each now asserts the new sentence verbatim, so a revert reds them. The ones that assert only the unchanged prefix (form-layout-inline-grid-retired.test.ts, the turso / driver-memory toContain('os migrate meta --from 17')) are untouched, because they stay true.
  • Changeset. .changeset/9591-retirement-sentence-write.md: patch for @objectstack/spec, @objectstack/lint and @objectstack/driver-turso, the three packages whose shipped text moves.
  • Generated. content/docs/references/**: 32 files (+268/−268) from pnpm --filter @objectstack/spec check:generated --fix; check:docs was the only stale artifact. check:generated then exited 0.

The class pin (retired-key-migrate-sentence.test.ts)

  • Anchors. HOUSE_AT_MARKER and MIXED_AT_MARKER, and their markdown twins, require the new clause. The os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 sentence, which does not name --write, is now RED. Two further spellings are RED: one that names --write without the qualification, and one that qualifies it but leaves the rest unowned.
  • Withdrawn claim. WITHDRAWN_CLAIM is unchanged: the unqualified automatic-rewrite claim stays a hard RED everywhere. A new non-vacuity case proves neither legal shape trips it.
  • Truth anchor (new). The pin reads os migrate meta's own flag table. A write boolean flag must exist and must have default: false, the two facts the sentence rests on. The read is covered by @objectstack/spec's existing packages/**/*.ts cross-package declaration.
  • Corpus widened by one root. packages/drivers/driver-turso/src joins, on [lint] validate-expressions 的 script 退役键提示仍说 "rewrite it" — #6856 house 句式的最后一个域外站点 #7030's terms. Its three turso config tombstones carry the house sentence, and their docblock defers to retired-key.ts, but the pin never walked them. Without this, a rewording leaves them behind with every assertion green. The lint-only anti-vacuity case now covers each widened corpus.
  • Header and docblock. The pin header and the retired-key.ts module docblock record the new sentence and why. The "the claim may be restored" note is gone, replaced by what was restored and how far.

Reverse verification, run from committed HEAD 017761f0 through scripts/ablation-replace.mjs (each anchor hit as declared and was restored to a blob equal to HEAD with git diff HEAD empty). Expected direction: red.

Mutation Result
A. the three turso.zod.ts sentences back to the #9529 wording (anchor ×3→0, blob e25cca4d5508→a05fe3f09733) 3 failed / 12 passed, naming driver-turso:spec/turso.zod.ts:63, :75, :82
B. meta.ts write flag default: false → true (blob c036012c63c9→71acff21ef8c) 1 failed / 14 passed: "the sentence is TRUE of the command it names"
C. meta.ts flag renamed write → inPlace (blob c036012c63c9→29a8a9402284) 1 failed / 14 passed: "os migrate meta declares no write boolean flag"

Under the old corpora, mutation A would have stayed green, because driver-turso was in no corpus.

One bounded fix on the same sentences: CHATTER_POSITION_RETIRED

The three record:chatter / record:discussion position value prescriptions ('sidebar', 'inline', 'drawer', in ui/component.zod.ts) told the author to run a bare os migrate meta. The command refuses that with Missing required flag --from (meta.ts run(), the flags.from === undefined branch). The conversion is record-chatter-position-vocabulary, toMajor: 18, so they now name --from 17. They therefore join the pin's judged set in house form, and the "(registered under protocol major 18)" aside goes. The fix qualifies as bounded: the same sentence class, a mechanical change to an already-pinned form, a file inside this claim's surface, and the same gate family. No test pinned the old text.

Governed surface: .claude/skills/spec-property-retirement/SKILL.md (Tier S)

The pin requires the retirement playbook to teach both shapes (SKILL_HOUSE_TEMPLATE and SKILL_MIXED_TEMPLATE must match its convention 5). So changing the sentence forces the playbook edit, and this PR lands as Tier S. Convention 5 now carries the two new templates. Its note that the command "never writes a source file" was made false by PR #22108, so it is deleted. Line count 337 → 337 (ceiling 337), with every line within the 120-byte budget: node scripts/pm/check-skill-line-ratchet.mjs exits 0. ⛔ No published skills/** file changes: PR #22122 owns skills/objectstack-upgrade/SKILL.md, and no published skill carries the sentence (git grep count 0).

维护者速读(草稿)

改了什么:所有退役键报错末尾那句统一提示,从「运行 os migrate meta --from N 列出机械修改,然后手工改」改为「……列出机械修改;--write 会写入它能证明的那些,其余你手工改」。共 161 处被 pin 判定的报错文案(含 2 处两从句变体),外加生成的 registry 3 处、lint 模板字符串 1 处;其中 3 处原先写成不带 --from 的命令(该命令会直接拒绝),一并改正。守这句话的 pin 同步更新,并新增一条断言:CLI 必须真有 --write 且默认不写。

为什么改:--write 已随 PR #22108 落地,旧句只说「手工改」,低估了工具;但 --write 只写能证明的站点,所以不能说「自动重写源文件」。新句两头都如实。

风险与代价(含回滚):纯文案,不改任何 schema、键、类型、导出或错误码;解析结果不变。依赖旧整句原文匹配的调用方会失配(仓内 23 个测试已同步);前缀「…for existing sources;」不变。回滚即 revert 本 PR。在途的兄弟 PR 若新增处方仍用旧句,会被 pin 打红,后落地者改用新句。

席位意见:

你要做的:无需操作;本 PR 触 .claude/**(Tier S),由席位按合同审查记录落地。

Verification (branch base 51290bca; final head f9ca14d548)

  • pnpm --filter @objectstack/spec build: VERDICT command-exit 0. check:generated --fix regenerated the one stale artifact; check:generated then exited 0 (15 of 15 current), and again in the gate run at f9ca14d548.
  • spec vitest run --project local: Test Files 623 passed (623), Tests 18613 passed, 1 todo, at 017761f0.
  • spec vitest run --project repo (53 files incl. the class pin): 53 passed (53), Tests 903 passed (903), at 017761f0.
  • @objectstack/driver-turso vitest run: Test Files 88 passed (88), Tests 2373 passed, 33 skipped, at 017761f0 (after pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/driver-turso...' build; the first run, before that build, could not resolve unbuilt dependencies and is NOT MEASURED, not red).
  • typecheck for spec (tsc --noEmit + check:scripts-typecheck + check:test-typecheck), lint and driver-turso: exit 0 at 017761f0.
  • After merging main (033e5c536d: docs(skills): objectstack-upgrade names os migrate meta --write beside the default run #22122, fix(lint)!: a conditional validation rule's nested then / otherwise predicate meets the build's expression verdict, at os build and the object save door (#22042) #22127, fix(objectql): skipAutomations never skips the builtin audit stamps #22106) as f9ca14d548, with no conflict (fix(lint)!: a conditional validation rule's nested then / otherwise predicate meets the build's expression verdict, at os build and the object save door (#22042) #22127 also edits validate-expressions.ts): the class pin 15 passed (15); @objectstack/lint vitest run: Test Files 123 passed (123), Tests 5688 passed (5688); lint typecheck exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at f9ca14d548 derives 124 commands (the claim-time 79 plus 45). All 124 exit 0 at f9ca14d548. --ran reconciliation: 124 derived, 124 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. (At 017761f0, five gates first answered exit 3, PREREQUISITE NOT MET: one shallow-clone fixture and four that need unbuilt dists. The clone was deepened as the gate asked, and all five are green in the f9ca14d548 run.)
  • node scripts/pm/check-skill-line-ratchet.mjs: exit 0; the playbook is 337 lines (ceiling 337), and no line is over 120 bytes.
  • eslint, narrowed: pnpm exec eslint --no-inline-config --format json over the 66 changed .ts files reports 66 files, 0 errors and 0 warnings. The population is eslint.config.mjs's **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} glob, which excludes the changed .md / .mdx files. The config never enables type-aware linting (its own comment at :326–:328), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.

Siblings in flight

#21982, PR #22094 (#13458) and PR #22103 (#5082) each add prescriptions with today's sentence. Whichever lands after this one carries the new sentence; the class pin reds it at that merge otherwise. Whichever of those lands first, this branch merges main before landing.

Acceptance notes

  • Hand-written docs still use the old sentence (content/docs/automation/flows.mdx ×2, protocol/objectql/query-syntax.mdx, data-modeling/queries.mdx, protocol/objectui/actions.mdx, ui/apps.mdx ×2). Each is the page's own advice, not a quoted error, and still true of the default run; each undersells --write. They are domain:devx pages outside this claim, so they are not touched here.
  • QA checklist item cli.migrate-meta-codemod (docs/qa/platform-checklist/areas/cli.json). Its RESTART CHECK fired when PR feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources #22108 added --write, and the item still asserts a print-only command. Its step 1 greps for the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 sentence verbatim and now finds none. Re-authoring the item belongs to the checklist author, not this PR.
  • Comments that say the default run "lists the mechanical edits" stay as they are, because they are still true: the migrations/registry.ts migration notes (outside the pin's scope by design) and the conversions/registry.ts comments.
  • The lint chartConfig.xAxis.field hint (validate-widget-bindings.ts) moved, but it remains invisible to the class pin: a template literal, with suggestName(…) and the suppress hint interpolated after the sentence.
  • A published skill still claims an automatic strip. skills/objectstack-data/rules/indexing.md:31 says "run os migrate meta --from 16 to strip them automatically", and skills/objectstack-data/SKILL.md:377 says the command "strips them". The default run strips nothing from sources, and --write strips only what it can prove. WITHDRAWN_CLAIM has no strip spelling, so the pin cannot see this. Widening it here would red main on a Tier H file this PR may not touch, so it is reported to the PM for the skills lane.
  • The config.actionType two-clause tail ("the stub and marker values are removed") is unchanged in substance; only the --write clause was inserted before it.

Patch round 1 (written by the PM seat from the dev's report 6052088494)

Patch round 2 (written by the PM seat from the dev's report 6053397952; claim revised 6052335087)

  • Why: PR feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094 (Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458, fec87e7e07) landed first with a two-clause prescription lacking the --write clause, which this PR's class pin refuses. The sibling rule here ("whichever lands later carries the new sentence") puts the edit in this PR.
  • Merge: origin/main 959c209d56 was merged through os-regen-merge.sh as 3b6335b9be, with no hand-written conflict. content/docs/references/api/protocol.mdx was regenerated as c40b3babd7.
  • The edit (fe3af5642c, 4 files beyond the merge):
    • packages/spec/src/kernel/manifest.zod.ts PLUGIN_PERMISSIONS_LIST_FORM now closes with "Run os migrate meta --from 17 to list the mechanical edits for the package manifest case; --write applies the ones it can prove, and a granted-permission record is not a source it reads." It keeps Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458's own second clause and adds the house --write clause, the seat's wording.
    • Its verbatim pin manifest-permissions-string-list.test.ts moved with it.
    • The changeset's two-clause bullet now names three members and says "before their second clause".
  • The two-clause variant now has three members: dashboard compareTo.offset, the script node's config.actionType, and the package manifest permissions case. The pin judges 164 sentences (spec 159 = 156 house + 3 two-clause; lint 2; driver-turso 3) with 0 bad sites.
  • Verification at fe3af5642c:
    • spec --project local: 625 files / 18,661 tests;
    • spec --project repo: 53 / 903;
    • class pin: 15 / 15, and the manifest pin: 17 / 17;
    • dispatch-gates --ran: 124 / 124 / 0 NOT-MEASURED;
    • CI: 33 success, 2 expected skips.

Generated by Claude Code

claude added 4 commits October 8, 2026 00:49
…dits it can prove, you apply the rest

The house `os migrate meta` sentence closing every ADR-0087-covered
prescription now reads:

  Run `os migrate meta --from <N>` to list the mechanical edits for existing
  sources; `--write` applies the ones it can prove, and you apply the rest by
  hand.

The MIXED two-clause variant carries the same `--write` clause. The class pin
moves with it, widens to driver-turso's three tombstones, and reads the two
facts the sentence rests on from the command's own flag table.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
`pnpm --filter @objectstack/spec check:generated --fix` (check:docs was the
one stale artifact).

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

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
… from the merged tree

`bash scripts/pm/os-regen-merge.sh` step 2 took main's side of
content/docs/references/{api/metadata,data/object,system/migration}.mdx (the
os-regen driver kept one side); regenerated with `gen:schema && gen:docs`
from the merge commit's tree.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
…; the lint index rule's sentence joins the class pin

Carries the house sentence into the two sites the merged index-scope
retirement brought (DECLARED_INDEX_BARE_TRUE_RETIRED and the lint
unique-unscoped-declared-index fix text) and into the test that pins the
former verbatim. The lint sentence was a template literal, which the class
pin cannot read, so it is now plain-quoted, as the lint corpus's other site is.

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

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-turso, @objectstack/lint, @objectstack/spec, touching 111 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/shared/retired-key.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13aea189591b935d81eb306f9be8a50c9045f661.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/shared/retired-key.ts) — 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 — 142 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 13aea189591b935d81eb306f9be8a50c9045f661 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 13aea189591b935d81eb306f9be8a50c9045f661

⚠️ 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 13aea189591b935d81eb306f9be8a50c9045f661 → 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: b9d6e82619337f9542a9ef7f54724d671729d834
Local-runs: none

Read at 2026-10-08T04:30Z, from the inputs the brief names and nothing else: card #9591 (body and all 12 comments; page 2 came back empty), PR #22142 (body; its 102-file list is identical to git diff --name-only ef1fcb26a2..b9d6e82619, the merge base being ef1fcb26a2), the 42 check-runs on the head, packages/spec/src/shared/retired-key.ts and retired-key-migrate-sentence.test.ts on ef1fcb26a2 and on the head, and packages/cli/src/commands/migrate/meta.ts on the head (read, not changed by the PR: git diff over packages/cli is empty). Head repo is the base repo, not a fork; the PR is a draft; one .claude/** path makes it Tier S.

Gate verdicts (the check-runs on the head): 42 runs, 38 success, 4 skipped, 0 failure. Test Core and its six shards, Lint & Repo Gates, the four Type Check jobs, Governed Surface Queue Guard, Check Changeset, Spec property liveness, Build Core, Build Docs, Dogfood and Temporal Conformance are all green. The four skips are each in the roster: Console Pin Gate (the console path filter names .objectui-sha, scripts/build-console.sh, packages/spec/package.json, packages/spec/tsup.config.ts and the like, none of which this diff touches, so the filter contract skips it), Packed-tarball smoke (opt-in label), and Auto Label plus Check PR Size on the second, PR-edited workflow run, whose first-run twins succeeded.

① Derived judgments

Each item names what the diff implies and whether it is right.

  1. The sentence is TRUE of os migrate meta on both routes — right. The sentence: "Run os migrate meta --from N to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand." Default route: meta.ts declares write: Flags.boolean({ … default: false, exclusive: ['stored'] }) (lines 762–769); without it the authored chain replays the conversions in memory, prints the applied list, and writes only the --out snapshot when asked (writeStackSnapshot, and the writeFileSync(resolve(flags.out), …) on the --json branch). "To list" holds. --write route: writeSources() (lines 999–1053) plans with planAuthoredSourceWrite, writes, re-loads and re-runs the chain, verifies with verifyAuthoredSourceWrite, and on any disagreement calls restoreAuthoredSources and exits 1; every site it does not write is reported with its refusal kind. "Applies the ones it can prove, and you apply the rest by hand" holds, and the re-run is part of the proof (the docblock says "held to a re-run of the chain"), so a restored run is not a counter-example. --write rides the same invocation (--from is still required at line 832; the command's own example is --from 16 --write), which is how the sentence reads it.

  2. One antecedent — kept. "Existing sources" is still the single object of "for". "The ones" has two plural nouns before it (edits, sources), but "applies" and "apply" take an edit, and — the point of the 2026-08-09 ruling — neither reading says anything about the KEY's fate, which stays in the body prose. The pin forces the qualification: "--write applies them." is a RED fixture.

  3. "Automatically" unqualified — never said. Every added line of the diff was grepped for it: the hits are the changeset's own negation ("never says the tool rewrites your sources automatically"), the docblock and pin comments quoting the withdrawn claim, and one regenerated reference row whose pre-existing clause "Stored flows are converted automatically — the conversion does the quoting for you" (flow.zod.ts waitEventConfig.timeoutMs) describes the D2 load-path conversion of stored rows, not an authored-source rewrite; it is not a WITHDRAWN_CLAIM spelling and it is true of the tool. Right.

  4. The class pin's new assertions — each holds what it claims.

    • RED cases: the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 house sentence and its MIXED spelling (silent on --write), "--write applies them." (unqualified by omission), "--write applies the ones it can prove." (the rest unowned), and the re-spelled buried-sentence case. Read against HOUSE_AT_MARKER (which demands the full ", and you apply the rest by hand." tail before the closing quote) and MIXED_AT_MARKER (which demands "for the X case; --write applies the ones it can prove, and …"), every one fails both anchors. Right.
    • Truth anchor: the regex \n\s*write: Flags\.boolean\(\{([\s\S]*?)\n\s*\}\), lands on meta.ts lines 762–769 and its lazy group ends at that flag's own }), (no }), sits inside its description), so the captured body carries default: false. It is a source read, not a built CLI — conservative (an explicit default: false deleted in favour of oclif's implicit false would red it with behaviour unchanged), and it proves existence and default only; what --write does is the CLI's own pins' job (feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources #22108). The PR's ablations B and C agree. Right.
    • Per-corpus anti-vacuity loop: iterates every corpus but spec, requiring at least one judged site and all ok. At the head the lint corpus has 2 judged sites (validate-expressions.ts:1832 and data-model-rules.ts:463, the latter plain-quoted in this PR) and driver-turso has 3. Right.
    • Corpus widening to driver-turso: TURSO_SRC_ROOT resolves to packages/drivers/driver-turso/src; the walk yields non-test .ts; the three turso.zod.ts tombstones are the whole marker population there (turso.test.ts carries three more and is skipped as a test). The PR's statement that mutation A would have stayed green under the old corpora is right: the file was in no corpus. Right.
    • Markdown judge: the playbook is judged flat (runs of whitespace collapsed), so the house template's new two-line wrap inside one double-backtick span is still one sentence; SKILL_HOUSE_TEMPLATE and SKILL_MIXED_TEMPLATE both match the playbook at the head, and the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 template is a new RED fixture. Right.
    • Independent count: marker occurrences (the pin's unit) on non-comment lines of non-test .ts under the three roots, minus migrations/registry.ts, are spec 158, lint 2, driver-turso 3 — the 163 the second report states. Across all of packages/** the old tail survives only in the pin's two RED fixtures, and no file outside the three corpora carries the new sentence, so after the widening every shipped carrier is judged except the two acknowledged blind spots (migrations/registry.ts, out of scope by design and regenerated from in-scope entries; validate-widget-bindings.ts, a template literal).
  5. The bounded CHATTER_POSITION_RETIRED fix — right. Before: "Run os migrate meta to list the mechanical edits for existing sources (registered under protocol major 18); apply them by hand." A bare os migrate meta exits 1 with "Missing required flag --from" (meta.ts lines 832–844), so the three prescriptions named a command that refuses. record-chatter-position-vocabulary is toMajor: 18 (conversions/registry.ts 6652–6654, retiredFromLoadPath: true); migrations/chain.ts applies each step's conversionIds through CONVERSION_BY_ID from ALL_CONVERSIONS with no retired filter, and step 18's ids are CONVERSIONS_BY_MAJOR[18], so --from 17 replays it and the listing claim is true; N is toMajor minus one, as meta.ts's own docblock prescribes. The three now carry a --from marker and join the judged set in house form; no test pinned the old text; the four bounded-fix conditions hold and the claim was revised in the same round (6051261867).

  6. The playbook's two templates (Tier S, judged as the agent-facing rule) — right. Convention 5 now teaches exactly the two shapes the pin enforces, each a closed code span, with the --from operand placeholder unchanged; the note that the command never writes a source file was made false by feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources #22108 and is deleted, and nothing else in the file still says so. The templates match the pin's markdown anchors, so an author who copies from the playbook lands green; the rationale stays reachable through the pin's pointer to the retired-key.ts docblock. Net 0 lines; the skill line ratchet sits in the green Lint & Repo Gates. No published skills/** file is in the diff, and at the head no published skill carries a house-form os migrate meta --from sentence at all.

  7. Accept set and public surface — unchanged; Clause-②: no confirmed. Every +/- line under packages/**, .claude/** and .changeset/** that is not the sentence swap was listed: comments, the pin, the retired-key.ts docblock and @example, the lint plain-quoting (template literal to string concatenation; emitted bytes identical apart from the sentence), the two-clause seams, the chatter --from 17 with its comment, the changeset and the playbook. No z. call, key, type, export or signature moves; retiredKey() is byte-identical. The 268 changed rows across the 32 regenerated reference pages are sentence-only. 23 test files (22 spec, 1 lint) assert the new sentence verbatim, so a revert reds them. The prefix up to "for existing sources;" is unchanged, as the changeset says.

② Semver level

.changeset/9591-retirement-sentence-write.md declares patch for @objectstack/spec, @objectstack/lint and @objectstack/driver-turso — exactly the three published packages whose shipped text moves; @objectstack/cli is read, not changed, and rightly absent. Clause-②: no in the changeset equals PR body line 2 and the claim; no arm, which AGENTS.md allows for no. Finding ①7 shows no accept/reject result and no public export moves, so no minor is owed; a prescription-text fix in released packages is the patch case. The body carries the FROM and TO sentences and the prefix-compatibility note; each factual bullet (two-clause members, chatter --from 17, lint's two sites, turso's three tombstones) matches the diff. Check Changeset is green. Right.

③ Boundary flags

Dev deviations, report 6051224331:

  • Playbook outside the claim surface — forced by the pin's own playbook assertion; claim revised (6051261867); judged at ①6. Accepted.
  • turso.zod.ts outside the surface — three live sentences never walked; corpus widened; domain:engine declared in the revision. Accepted.
  • meta.ts read, not edited — a cross-package test input; check:cross-package-test-inputs sits in the green Lint & Repo Gates. Accepted.
  • validate-widget-bindings.ts second lint site — moved; a template literal, invisible to the pin, recorded in the pin's Mechanism paragraph. Accepted as a standing one-site blind spot.
  • Bounded chatter fix — ①5. Accepted.
  • Pin gained assertions, not a gate — right; same vitest file, no CI wiring moved.
  • Clause-② line with a parenthetical — scripts/pm/clause2-line.mjs reads the value, then an arm only when the parenthetical's first token is widening or narrowing; this one yields arm null, declared no, well-formed.
  • Attribution trailer amended pre-push, git fetch --unshallow, an os-verify-lock queue timeout — local process; no effect on the diff or on the verdicts above.

Dev deviations, report 6052088494:

  • field.zod.ts, view.zod.ts and migrations/registry.ts needed no edit in the merged tree — verified: the old tail has zero hits under packages/** outside the pin fixtures. Accepted.
  • data-model-rules.ts plain-quoted rather than widening the corpus — the right remedy: the lint corpus already walked the file, and reconstruct() merges only single- and double-quote seams, so a template literal is invisible; the emitted fix text is identical apart from the sentence. Accepted.
  • os-regen-merge.sh stopped at step 3; the three reference pages regenerated from the merged tree as 98e2f6e373 — the net diff on those pages is sentence-only and check:generated is green. Accepted.
  • Gate re-run loop refused by the harness and re-run by name; a merge commit without the trailer pair — local process; the check-runs are the verdict; the queue squashes.

Out-of-scope findings, both reports, answered:

New from this review, in neither report:

No open_questions were raised by the dev, and none arise here. The seat's ACCEPT names one pure re-sync hop with main before landing (a generated page both sides moved); a regenerate-only hop with a Regen-provenance: pointer carries this record forward, and a hop that changes anything else needs a new record on the new head.

Implemented-by: claude/issue-9591-retirement-sentence-write
Reviewed-by: session_01LAi5BVvQNiYzepSAcsoFLK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Read at 2026-10-08T06:10Z, from the brief's inputs and nothing else: card #9591 (body and all 15 comments; page 2 came back empty), PR #22142 (body; its 104-file list is identical to git diff --name-only 959c209d56..fe3af5642c, the merge base being 959c209d56, which the head merges), the 42 check-runs on the head (every one carries fe3af5642c as its head sha), packages/spec/src/shared/retired-key.ts and retired-key-migrate-sentence.test.ts at the head, packages/cli/src/commands/migrate/meta.ts at the head (blob c036012c63c9, identical on 959c209d56; the PR changes nothing under packages/cli), and PLUGIN_PERMISSIONS_LIST_FORM in packages/spec/src/kernel/manifest.zod.ts on 959c209d56 (PR #22094's landed text) against the head. Head repo is the base repo; the PR is a draft; the one .claude/** path (spec-property-retirement/SKILL.md) makes it Tier S. This head is not a regeneration hop from b9d6e82619: three commits follow the merge of main (3b6335b9be), and fe3af5642c hand-edits three files, so the earlier PASS (6052278193) does not carry. This record judges the whole PR at this head and re-verified every finding it reuses.

Gate verdicts (the check-runs on the head): 42 runs, 38 success, 4 skipped, 0 failure. Test Core and its six shards, Lint & Repo Gates, the four Type Check jobs, Governed Surface Queue Guard, Check Changeset (both runs), Spec property liveness, Build Core, Build Docs, Dogfood and Temporal Conformance are green. The four skips are each in the roster: Console Pin Gate (path filter; nothing this diff touches is on it), Packed-tarball smoke (opt-in label), and Auto Label plus Check PR Size on the second, PR-edited workflow run (37735136811), whose first-run twins succeeded.

① Derived judgments

  1. The house sentence is TRUE of both os migrate meta routes — right. "Run os migrate meta --from N to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand." Default route: meta.ts declares write: Flags.boolean({ … default: false, exclusive: ['stored'] }) (lines 762–769); --from is validated at line 832 and a bare run exits 1 with "Missing required flag --from"; without --write the run normalizes with { convert: false }, replays applyMetaMigrations in memory, prints the applied list and writes only the --out snapshot. "To list" holds. --write route: writeSources() (lines 999–1053) plans with planAuthoredSourceWrite, writes, re-loads the config, re-runs the chain, verifies with verifyAuthoredSourceWrite, and on any disagreement restores every file and the command exits 1; the codemod's docblock and CodemodRefusalKind (11 kinds) name what it refuses, and every refused site is listed with its reason. "Applies the ones it can prove, and you apply the rest by hand" holds on the same invocation (--from N --write).

  2. One antecedent — kept. "Existing sources" is the single object of "for"; "the ones" takes "edits" (an edit is what gets applied); the key's fate stays in the body prose. The pin forces the qualification: "--write applies them." and "--write applies the ones it can prove." (the rest unowned) are both RED fixtures.

  3. "Automatically" unqualified — never said. Every added line of the net diff outside the regenerated pages was grepped: the hits are the changeset's own negation and two comment lines, in the retired-key.ts docblock and the pin header, that quote the withdrawn claim. The flow.zod.ts waitEventConfig.timeoutMs row's pre-existing "Stored flows are converted automatically" (the D2 load-path conversion of stored rows) is a context line this PR does not touch, not a WITHDRAWN_CLAIM spelling, and true. WITHDRAWN_CLAIM spellings survive at the head only inside the pin's own fixtures.

  4. The class pin's assertions — each holds. HOUSE_AT_MARKER and MIXED_AT_MARKER require the --write clause and the literal-final period; the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 house and MIXED spellings, the two half-qualified spellings and the buried-sentence case are RED. The truth anchor /\n\s*write: Flags\.boolean\(\{([\s\S]*?)\n\s*\}\),/ lands on lines 762–769 and its lazy group ends at that flag's own }), (no }), sits inside its description), so the captured body carries default: false. The per-corpus anti-vacuity loop iterates every corpus but spec. TURSO_SRC_ROOT resolves to packages/drivers/driver-turso/src, whose three turso.zod.ts tombstones are its whole marker population. reconstruct() merges the cross-line seam the manifest sentence now uses (a literal ending in case; and the next line reopening at --write), so the split literal is judged whole. Independent census on the head tree (marker occurrences on non-comment lines of non-test .ts under the three roots, migrations/registry.ts excluded): spec 159, lint 2, driver-turso 3 — the 164 the newest report states. The old tail survives across packages/**, .claude/** and skills/** only in the pin's two RED fixtures.

  5. The bounded CHATTER_POSITION_RETIRED fix — right. The three prescriptions named a bare os migrate meta, which exits 1 for the missing --from. record-chatter-position-vocabulary is toMajor: 18; migrations/chain.ts applies each step's conversionIds through CONVERSION_BY_ID with no retired filter, so --from 17 replays it and the listing claim is true. Same sentence class, mechanical, inside the surface, same gate family; no test pinned the old text; the claim was revised in the same round (6051261867).

  6. The playbook's templates (Tier S) — right. Convention 5 teaches exactly the two shapes the pin enforces, each a closed double-backtick span; flattened, the house template matches SKILL_HOUSE_TEMPLATE and the variant matches SKILL_MIXED_TEMPLATE; the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 template is a RED fixture. The note that the command "never writes a source file" was made false by feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources #22108 and is deleted; nothing else in the file still says so. 337 lines in, 337 out (ceiling 337); the four lines this PR adds are 40–94 bytes, inside the ratchet's 120-byte cap; check:pm-skill-ratchet sits in the green Lint & Repo Gates. No published skills/** file is in the diff.

  7. NEW at this head — the third two-clause member. PLUGIN_PERMISSIONS_LIST_FORM now closes with "Run os migrate meta --from 17 to list the mechanical edits for the package manifest case; --write applies the ones it can prove, and a granted-permission record is not a source it reads."

    • True of the tool. manifest-permissions-string-list-removed is toMajor: 18, retiredFromLoadPath: true; the chain replays it at --from 17 (finding 5's mechanism), and it emits one (removed) entry per manifest.permissions or packages[].manifest.permissions list, so the default run lists the manifest case. The codemod takes a removed key as an op: 'delete' change (diffStacks, line 270) and writes it when the walk reaches one literal in one project file through defineStack — a config-literal manifest list is provable; a packages[] manifest under node_modules is refused outside-project and listed. The second clause stays true: the chain reads the authored stack; an environment artifact's grantedPermissions value is never in it, and --write is exclusive with --stored.
    • Inside the ruled two-clause shape. The concatenated sentence matches MIXED_AT_MARKER (the seam reconstructed; checked against the anchor's regex). Its second clause names the other carrier of the one declaration and what the tool does with it (does not read it) — within "what the tool does with the rest". That clause is feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094's own, accepted under the pre---write MIXED anchor; this PR inserts only the house clause and re-opens nothing.
    • Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458's claim intact. The second clause is byte-identical to the 959c209d56 text; the message's prefix, through "when the plugin needs none.", is unchanged.
    • The pin moved without weakening. PRESCRIPTION in manifest-permissions-string-list.test.ts differs from 959c209d56 only by the inserted clause; its start and end anchors, /s flag and the two .* gaps are the same, and all four toMatch(PRESCRIPTION) sites, the accept-set, tsc, D2 replay and not-on-the-authoring-funnel assertions are untouched.
  8. Accept set and public surface — unchanged; Clause-②: no confirmed. Every +/- line under packages/**, .claude/** and .changeset/** that is not the sentence swap was listed: the pin, the retired-key.ts docblock and @example, three comment updates (validate-expressions.ts, data-model-rules.ts, component.zod.ts), the data-model-rules.ts plain-quoting (template literal to string concatenation; emitted bytes identical apart from the sentence), the three two-clause seams, the chatter --from 17, the changeset and the playbook. No z. call, key, type, export or signature moves; retiredKey() is byte-identical. The 32 regenerated reference pages (268 changed rows, api/protocol.mdx among them) are sentence-only: a pairwise compare with both tails stripped is empty.

② Semver level

.changeset/9591-retirement-sentence-write.md: patch for @objectstack/spec, @objectstack/lint and @objectstack/driver-turso — the three published packages whose shipped text moves; @objectstack/cli is read, not changed, and rightly absent. Clause-②: no in the changeset equals PR body line 2 and the claim; the body's parenthetical opens with "prescription", not a word of the arm family, so clause2-line.mjs reads it as declared no, arm null, no contradiction. The edited bullet — "The three prescriptions … (dashboard compareTo.offset, the script flow node's config.actionType and the package manifest permissions case) carry the same --write clause before their second clause" — is true of the diff, and the old "before the clause about the rest of the value" would have been false of the manifest member. Every other bullet (chatter --from 17, lint's two sites, turso's three tombstones, the prefix-compatibility note) matches the diff. Check Changeset is green on both runs. Right.

③ Boundary flags

Report 6051224331 (round 0):

  • Playbook outside the claim surface — forced by the pin's playbook assertion; claim revised (6051261867); judged at ①6. Accepted.
  • turso.zod.ts outside the surface — three live sentences never walked; corpus widened; domain:engine declared. Accepted.
  • meta.ts read, not edited — a cross-package test input; check:cross-package-test-inputs sits in the green Lint & Repo Gates. Accepted.
  • validate-widget-bindings.ts — moved; a template literal with interpolation after the sentence, invisible to the pin; recorded in the pin's Mechanism paragraph. Accepted as the standing one-site blind spot.
  • Bounded chatter fix — ①5. Pin gained assertions, not a gate — same vitest file, no CI wiring moved. Clause-② parenthetical — well-formed (②). Attribution trailer, git fetch --unshallow, lock timeout — local process.
  • Out of scope: the skills/objectstack-data strip claim → skills(objectstack-data): two published lines say os migrate meta --from 16 strips retired keys "automatically", but the default run writes no source and --write strips only the sites it can prove #22144 (the WITHDRAWN_CLAIM strip-spelling widening rides that PR, since the pin walks skills/**); the stale QA item cli.migrate-meta-codemod, the hand-written domain:devx pages and the cross-package-test-inputs.mjs line numbers — not shipped prescription text; escalated to the PM as before, nothing new to add.

Report 6052088494 (patch round 1):

  • field.zod.ts, view.zod.ts, migrations/registry.ts needed no edit — verified at this head: zero old-tail hits outside the fixtures. Accepted.
  • data-model-rules.ts plain-quoted rather than widening — the right remedy; the corpus already walked the file. Accepted.
  • os-regen-merge.sh stop at step 3, regeneration as 98e2f6e373, the gate loop re-run by name, a merge commit without the trailer pair — local process; the check-runs are the verdict; the queue squashes.

Report 6052319001 (blocked landing):

Report 6053397952 (patch round 2):

  • The changeset bullet's "before their second clause" — true; the old wording would have been false of the manifest member. Accepted.
  • The split quote-and-plus literal — the pin reconstructs it and the emitted message is byte-identical to the verbatim sentence (checked by concatenation). Accepted.
  • os-regen-merge.sh step-3 stop, lock timeouts, the background shard — local process. The pr_body_needs are carried in the body's "Patch round 2" section. One body sentence is loose: "CI: 33 success, 2 expected skips" — the head carries 42 runs, 38 success and 4 skips, the extra seven being the PR-edit re-run (two skipped by design); no red either way.
  • The report's "no line exceeds 120 bytes" is true of the lines this PR adds (①6); seven untouched lines of the playbook measure over 120 bytes and the ratchet is green, so the gate's own measure governs. Immaterial.

New from this review, in no report:

  • .changeset/13458-manifest-permissions-string-list-retired.md line 25 (on main since 959c209d56) carries "os migrate meta --from 17 lists the mechanical edits for existing sources; apply them by hand.", and .changeset/5082-declared-index-unique-scope.md line 42 carries the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 sentence. Release prose outside the pin's corpora, another card's files, true of the default run. Not blocking. For the PM: align both when the release notes are compiled, or leave them as the record of what each PR shipped.

No open_questions were raised by the dev, and none arise here. The seat's ACCEPT (6052105188) named one pure re-sync hop; this head is not one, and this record is the new review it requires. A further hop that only regenerates carries this record forward with a Regen-provenance: pointer; a hop that changes anything else needs a new record on the new head.

Implemented-by: claude/issue-9591-retirement-sentence-write
Reviewed-by: session_01LAi5BVvQNiYzepSAcsoFLK

VERDICT: PASS


Generated by Claude Code

claude added 2 commits October 8, 2026 06:11
…rom the merged tree

`bash scripts/pm/os-regen-merge.sh` step 2 took main's side (the os-regen
driver kept one side); regenerated with `gen:schema && gen:docs` from the
merge commit's tree.

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

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 6053587390 · fe3af56 → 7530236 · comm -23 <(git diff --name-only fe3af5642c 75302366e5 | sort) <(git diff --name-only 959c209d56 13aea18959 | sort) → (empty)

domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T06:16Z. A pointer, not evidence: the queue guard re-runs the test.

  • The hop: 5949dbf0ba merges origin/main 13aea18959 into fe3af5642c. 75302366e5 regenerates content/docs/references/ui/view.mdx, the one generated file both sides changed, from the merged tree. No hand edit.
  • Re-run by this seat: the files that differ across the hop, minus the files main changed (959c209d56..13aea18959), are empty, before any regenerated output is subtracted.
  • The PR's net diff is unchanged: 104 files, +667 / −556 on both sides of the hop. The file sets match, and each file's changed lines are byte-identical between 959c209d56..fe3af5642c and 13aea18959..75302366e5.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 06:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 06:52
@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 failed status checks Oct 8, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as draft October 8, 2026 07:21
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

The merge queue refused this PR, as designed; the seat's pointer did not certify the hop. domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T07:21Z

  • What happened:
    • The queue build's Governed Surface Queue Guard exited 3, unapproved. This was build dfcd924b6b, the run of 06:54Z.
    • The PR left the queue at 07:19Z, and its auto-merge was dropped.
    • The seat replayed the guard locally on that merge group and got the same verdict.
  • Why: the Regen-provenance: pointer 6053668168 did not carry the record 6053587390 from fe3af5642c to 75302366e5.
    • The guard's purity test on the committed trees (unexplainedPathsBetween) counts every path that moved across the hop, carries no merge=os-regen, and that the PR touches at either head.
    • That gives one path: packages/lint/src/validate-expressions.ts. This PR changes one sentence in it, and main changed it too, in 959c209d56..13aea18959.
    • The seat's own reading compared the hop against main's changed files. It found the PR's per-file delta byte-identical, and it called the hop pure. That is a weaker test than the guard's, and the guard's is the one that counts.
    • This was the seat's error, not the dev's.
  • Now:
    • The PR is back to draft, with auto-merge off.
    • A fresh at-tier contract review of 75302366e5 has been commissioned, from the brief's inputs only. It must cover the two-sided file explicitly.
    • The PR is flipped ready and re-queued only on a PASS record that names this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 75302366e563cf2c6529380977f135f9966a84f5
Local-runs: none

Rendered 2026-10-08T07:39Z. PR #22142 at this head against its merge base 13aea18959 (card #9591). Read-only: the net diff, the card body and all 16 of its comments, the PR body, file list and comments, the binding texts at the head and on the base, and the head's check-runs. This is an adversarial re-review of the whole PR at this head; nothing from the prior record 6053587390 (head fe3af5642c) is carried without re-verification below.

Why this record is owed, re-derived. This head is fe3af5642c, plus a merge of main 13aea18959 (5949dbf0ba, parents fe3af5642c and 13aea18959), plus a regenerated content/docs/references/ui/view.mdx (75302366e5: 1 file, +1/-1). The queue guard refused to carry the prior record because packages/lint/src/validate-expressions.ts is PR-touched, hand-written (it carries no merge=os-regen attribute; content/docs/references/** does), and main changed it inside 959c209d56..13aea18959. Re-derived here rather than taken from the Regen-provenance: pointer:

Check-runs on this head. 36 runs: 34 success, 2 skipped (Console Pin Gate and the opt-in packed-tarball smoke, both conditional). All seven required contexts are success, both Governed Surface Queue Guard runs included. The PR carries 0 reviews, auto-merge is unarmed, head repo equals base repo, and the diff is 1,223 changed lines.

① Derived judgments

Surface (a), the shipped prescription text across packages/spec/src/**, packages/lint/src/** and packages/drivers/driver-turso/src/** (the path limb):

  1. The house sentence is true of both os migrate meta routes. Read from packages/cli/src/commands/migrate/meta.ts at the head (blob c036012c63c9, identical on the base; the PR does not edit it). The default run prints Applied N mechanical change(s): (:493), writes no authored source, and writes only the --out snapshot when asked. --write is Flags.boolean({ default: false, exclusive: ['stored'] }) (:762 to :769) and is described as rewriting the authored sources in place for each mechanical change traced to one literal in one project file, listing every other change with the reason it was not written, never the semantic changes; its outcome printer reports Wrote N of M mechanical change(s) and N mechanical change(s) left for you to apply by hand, each with not written [kind]: reason (:353 to :367). The codemod module packages/cli/src/utils/authored-source-codemod.ts exists at the head. So "to list the mechanical edits" is the default route, "--write applies the ones it can prove" is the write route, and "you apply the rest by hand" is the listed remainder. The --stored route is outside the sentence by construction: --from and --write are both exclusive: ['stored'], and a run without --from refuses with Missing required flag --from and names --stored, so the sentence claims nothing it could fail there.
  2. One antecedent. "existing sources" names one thing; "the ones" and "the rest" can only be edits, since an edit is what gets applied; "it" is the tool. The key's fate stays in each prescription's body prose and the sentence never restates it.
  3. No unqualified "automatically". The word does not occur in the sentence. At the head, every WITHDRAWN_CLAIM spelling counts 0 across the three corpora, skills/** and .claude/** in non-test files.
  4. The class pin's assertions (packages/spec/src/shared/retired-key-migrate-sentence.test.ts, +129/-50), verified by reading the file at the head: HOUSE_AT_MARKER and MIXED_AT_MARKER both require the --write clause and the literal-final position; WITHDRAWN_CLAIM is unchanged and its non-vacuity case now also proves neither legal shape trips it; a truth anchor reads meta.ts's own flag table for a write boolean flag with default: false; the corpus gains packages/drivers/driver-turso/src, and the lint-only anti-vacuity case became a per-corpus loop; the RED fixtures add the os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 sentence, the two half-qualified --write spellings, and the markdown os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529 template; the markdown judge's house and two-clause templates require the clause. The old tail survives at the head only inside the pin's two RED fixtures (:420 and :659). The pin has 15 cases, and Test Core is success on this head.
  5. The bounded CHATTER_POSITION_RETIRED fix (ui/component.zod.ts): the three value prescriptions ('sidebar', 'inline', 'drawer') that named a bare os migrate meta, which the command refuses for the missing --from, now name --from 17; the conversion record-chatter-position-vocabulary is toMajor: 18 at the head, so 17 is the right operand. The "(registered under protocol major 18)" aside is removed, the comment above records why, and all three sentences are house-form. Zero bare Run \os migrate meta`` prescriptions remain in the three corpora. The fix stays inside the four bounding conditions.
  6. The third two-clause member (kernel/manifest.zod.ts PLUGIN_PERMISSIONS_LIST_FORM). On the base it closes "Run os migrate meta --from 17 to list the mechanical edits for the package manifest case; a granted-permission record is not a source it reads." (Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458's wording, no --write). At the head the house clause is inserted before that second clause, split across a quote-and-plus seam that the pin's reconstruct merges, and the sentence stays literal-final. It shape-matches MIXED_AT_MARKER; --from 17 matches manifest-permissions-string-list-removed toMajor: 18; the verbatim PRESCRIPTION regex in manifest-permissions-string-list.test.ts moved with it. The other two members (dashboard compareTo.offset and the script node's config.actionType) carry the clause the same way.
  7. The sweep is wording-only. With the house tail swap stripped from the three corpora's non-test sources, what remains is exactly: the retired-key.ts module docblock and its retiredKey() example; the pin; the comment edits in validate-expressions.ts and component.zod.ts; data-model-rules.ts re-spelled plain-quoted so the pin can read it (escaped template backticks hid it); the three two-clause members; the translation.zod.ts seams re-split; the chatter fix. No schema, key, type, export or error-code line moves. Generated references: 268 of 268 changed lines are the house tail swap (267) plus the actionType two-clause row (1), with check:docs the only regenerated artifact. Tests: 25 files, 31 verbatim old-to-new assertion swaps; the only residue is one comment and the manifest regex. The lint corpus at the head holds two plain-quoted judged sites (validate-expressions.ts:1849, data-model-rules.ts:463), one template-literal site the pin cannot read (validate-widget-bindings.ts:1191, moved anyway and reported), and one mid-prose mention (lint-flow-patterns.ts:955) that is not a prescription.

Surface (b), .claude/skills/spec-property-retirement/SKILL.md (Tier S):

  1. The playbook's templates. The diff is confined to convention 5 (+4/-4): the house template gains the clause, wrapped across two lines inside its double-backtick span, which the markdown judge reads whole after collapsing whitespace; the two-clause template gains the clause before its "what the tool does with the rest" placeholder; the note that the command "never writes a source file" is deleted, as it has been false since feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources #22108. 337 to 337 lines against a ceiling of 337. The four lines this PR wrote are 90, 77, 94 and 40 bytes. Seven lines over 120 bytes exist at the head (86, 87, 88, 101, 114, 123, 124) and exist on the base at the same lines with the same byte counts, so they predate this PR. Lint & Repo Gates is success. This is the only governed path in the file list; no skills/** file and no Tier H path is touched.

② Semver level

.changeset/9591-retirement-sentence-write.md declares patch for @objectstack/spec, @objectstack/lint and @objectstack/driver-turso, the three published packages (17.7.0) whose shipped text moves, with Clause-②: no and no arm. The PR body's parenthetical after no is not an arm (the arm vocabulary is the closed pair widening/narrowing), and Check Changeset is success. Checked against the diff: every input that parsed or was refused on the base gets the same verdict at the same path at the head; the chatter fix changes the text of a refusal, not whether it refuses. patch is correct, and Clause-②: no holds.

One completeness gap, non-blocking: the changeset's @objectstack/lint bullet names the script-node diagnostic and the chartConfig.xAxis.field hint but not the data-model-rules.ts unique-unscoped-declared-index fix text that patch round 1 moved. The first bullet covers the whole class, so the entry is incomplete rather than wrong. It can be amended in a docs-only PR; it is not a reason to re-spin this head.

③ Boundary flags

Every deviation and finding in 6051224331, 6052088494, 6052319001, 6053397952 and 6053687194, answered or escalated:

  • Outside-surface files (SKILL.md, turso.zod.ts, validate-widget-bindings.ts, meta.ts read-only), the bounded chatter fix, and the manifest pair: ratified by claim revisions 6051261867 and 6052335087. Every one of the 104 files sits inside the revised surface. Answered.
  • The pin gained assertions, not a new gate: verified; no CI change rides the PR. Answered.
  • Co-Authored-By trailer amended before any push: no published history was rewritten. Answered.
  • git fetch --unshallow origin main in the shared .git: this advanced the shared origin/main, as reported, and the merge it led to was intended and is verified above. Noted; no breach found.
  • Clause-② line copied verbatim with its parenthetical: it parses as no, arm null. Answered.
  • Lock queue-timeouts re-acquired under the same slot, and the backgrounded shard waited on by its own PID: operational, and consistent with the one-PID rule. Answered.
  • data-model-rules.ts plain-quoted instead of widening the corpus: the right remedy, since the corpus already reached the file and the escaped backticks were what hid the sentence; verified at the head. Answered.
  • os-regen-merge.sh stopped at step 3 on each of the three hops, regeneration ran without MERGE_HEAD, the script's record was left at handoff: the outcome is what the rule protects, and it is verified. Each regeneration is its own commit (98e2f6e373, c40b3babd7, 75302366e5), each merge commit touched only files main changed, and the net diff is hunk-identical across every hop. Answered.
  • The merge commits carry git's default message with no trailer pair. Reported for feca6b5ace; 3b6335b9be and 5949dbf0ba are the same shape and were not re-reported. The pre-push hook refuses a model identifier in the pair, not its absence; the regeneration and fix commits carry the model-free pair; landed history is not rewritten. Non-blocking; noted for the seat.
  • Probe merge aborted with nothing pushed; worktree removed: fine. Answered.
  • Changeset bullet reworded to "before their second clause": accurate for the manifest member, whose second clause is not about the rest of a value. Answered.
  • Out-of-scope findings. The skills/objectstack-data strip claim (SKILL.md:377, rules/indexing.md:31) still stands at the head and is filed as skills(objectstack-data): two published lines say os migrate meta --from 16 strips retired keys "automatically", but the default run writes no source and --write strips only the sites it can prove #22144 (open, domain:skills, dispatched); this PR touches no skills/** file, which is right for a Tier H path. The QA checklist item cli.migrate-meta-codemod, the hand-written domain:devx pages, and the cross-package-test-inputs.mjs "as measured" line numbers are carrier-none notes and are correctly absent from this diff. validate-widget-bindings.ts remains the one template-literal site the pin cannot judge; the pin's docblock now records template literals as invisible to the scan, and the site moved anyway. Answered.
  • New, non-blocking, found here: the pin's header still describes the two-clause variant as having two members (the dashboard model and "the other member"), while this head has three. The pin judges by shape, so no behaviour is affected and no gate reads the docblock; a one-line docblock update can ride the pin's next edit. The retired-key.ts docblock names only the model and stays true.
  • New, non-blocking, found here: the PR body says the playbook has no line over 120 bytes; seven pre-existing lines exceed it. The ratchet's enforced rule is the per-file line-count ceiling, met at 337, and every line this PR wrote is within budget. Body prose only.
  • Docs Drift Check (6051366170) is advisory; it notes that retired-key.ts yielded no anchor, a coverage note on the audit, not a defect.

Nothing above blocks. Both surfaces hold at this head, and the hop added nothing to the PR's net diff.

Implemented-by: claude/issue-9591-retirement-sentence-write
Reviewed-by: session_01LAi5BVvQNiYzepSAcsoFLK

VERDICT: PASS


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37748210189 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (6/6) — 失败步骤: Run this shard's tests

    @objectstack/plugin-security:test:  FAIL  src/builtin-positions.boot.test.ts > [ADR-0131 D2] the built-in positions as declared metadata — single, a stored definition under a built-in name
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/plugin-security:test:  FAIL  src/builtin-positions.boot.test.ts > [ADR-0131 D2] the built-in positions as declared metadata — single + organization, a stored definition under a built-in n
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/plugin-security:test:  FAIL  src/builtin-positions.boot.test.ts > [ADR-0131 D2] the built-in positions as declared metadata — walled, a stored definition under a built-in name
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/plugin-security:test:  FAIL  src/bootstrap-declared-positions.test.ts > bootstrapDeclaredPositions — one catalog read, both sources (ADR-0131 C2 S2b) > a stored definition shadowing a bui
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

The red merge-group build above is not this PR's. domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T08:41Z

Merged via the queue into main with commit de70e97 Oct 8, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9591-retirement-sentence-write branch October 8, 2026 09:01
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…does: lists the edits, `--write` applies the proven sites (objectstack-ai#22199)

Fixes objectstack-ai#22144

Clause-②: no

Two published lines in the objectstack-data skill said `os migrate meta
--from 16` strips the retired index keys `type` and `partial` ("to strip
them automatically" at `rules/indexing.md:31`, "strips them" at
`SKILL.md:377`). Neither was true of the tool: the default run writes no
authored source file, and `--write` rewrites only the sites it can
prove. Both lines now carry the house sentence. The closing enumeration
of every `os migrate meta` sentence in `skills/**` is below; every other
hit reads true and is left byte-for-byte alone.

Family: objectstack-ai#22120 is landed (PR objectstack-ai#22122, the upgrade skill) and objectstack-ai#22123 is
landed (PR objectstack-ai#22145, the `--from 10` example); this PR is the family's
closing card. objectstack-ai#9591 (PR objectstack-ai#22142, spec lane) is referenced only: it moves
the spec tombstones to the same sentence and holds the class pin.

## The tool, re-taken on this tree (`0aa5205228`, cut from `origin/main`
at `1e5d322c1e`)

- `packages/cli/src/commands/migrate/meta.ts:762-769`, the flag
declaration, verbatim:

  ```ts
  write: Flags.boolean({
    description:
'Rewrite the authored source files in place for each mechanical change
traced to one literal in one '
+ 'project file; every other change is listed with the reason it was not
written. Never writes the '
      + 'manual (semantic) changes.',
    default: false,
    exclusive: ['stored'],
  }),
  ```

- `packages/cli/src/utils/authored-source-codemod.ts` (module header):
`--write` writes a diff change only when its path leads, through the
authored modules' syntax, to ONE object or array literal in ONE project
file and the loaded value agrees with that literal; anything else is
refused by a named reason and stays on the list for the author.
- `packages/spec/src/migrations/registry.ts:79`: `export const
MIGRATION_SUPPORT_FLOOR = 16;` — `applyMetaMigrations` throws
`MigrationFloorError` for any `--from` under it (`meta.ts:713-716`).
- `packages/spec/src/conversions/registry.ts:3906`: `id:
'object-index-type-partial-removed'` — the 16 → 17 step carries the
conversion, so "lists the mechanical edits" is true of these two keys
specifically, not only in general.
- House sentence (`packages/spec/src/shared/retired-key.ts`: "It must be
TRUE of the tool"). The tombstones on `main` close with "Run `os migrate
meta --from 16` to list the mechanical edits for existing sources; apply
them by hand." (`packages/spec/src/data/object.zod.ts:558,567`); PR
objectstack-ai#22142 moves that to "… `--write` applies the ones it can prove, and you
apply the rest by hand."; the upgrade skill says "By default `os migrate
meta` rewrites no source file"
(`skills/objectstack-upgrade/SKILL.md:153`) and "`--write` … rewrite the
proven sites in place" (`:132`). No third vocabulary is introduced here.

## The two edits

`skills/objectstack-data/rules/indexing.md:31-33` (headroom 1058 tokens
before):

- before: "… run `os migrate meta --from 16` to strip them
automatically. What to do instead is the subject of "Access methods and
partial indexes" below."
- after: "… run `os migrate meta --from 16` to list the mechanical
edits; `--write` applies the ones it can prove, and you apply the rest
by hand. What to do instead is the subject of "Access methods and
partial indexes" below."

`skills/objectstack-data/SKILL.md:377-378` (ceiling 6128, headroom 0
before — the shortest form that stays true):

- before: "Both are now a `tsc` error and a parse error; `os migrate
meta --from 16` strips them. Access methods and partial predicates are
database-layer migrations."
- after: "Both are now a `tsc` error and a parse error; `os migrate meta
--from 16` lists the edits; `--write` applies the ones it can prove, the
rest by hand."

### Token ratchet payment (`node
scripts/check-skills-token-ratchet.mjs`, `ceil(utf8 bytes / 4)` per
file)

The new sentence in `SKILL.md` costs 63 bytes over the old one. It is
paid by deleting one clause the same section restates — never by
re-wrapping, never by touching the ceiling:

- deleted (`SKILL.md:377-378`): "Access methods and partial predicates
are database-layer migrations." (70 bytes with its leading space);
- where its content survives, same file: `SKILL.md:387-388` "See
rules/indexing.md for composite indexes, unique scope, and how to build
partial / gin / gist indexes at the database layer." and `SKILL.md:44`
"Index Strategy (rules/indexing.md) — btree/gin/gist/fulltext, composite
indexes, partial indexes"; and in `rules/indexing.md` § "Access methods
and partial indexes", the section the retirement callout points at;
- net: 24512 → 24506 bytes, 6128 → 6127 tokens, headroom 0 → 1. The
ceiling row in `scripts/check-skills-token-ratchet.mjs` is not touched
(see Acceptance notes).

## `skills/**` readings — lines, and tokens as the ratchet counts them

| File | Before | After |
|:--|:--|:--|
| `skills/objectstack-data/SKILL.md` | 469 lines · 24512 bytes · 6128
tokens (ceiling 6128, headroom 0) | 469 lines · 24506 bytes · 6127
tokens (headroom 1) |
| `skills/objectstack-data/rules/indexing.md` | 229 lines · 8729 bytes ·
2183 tokens (ceiling 3241) | 230 lines · 8805 bytes · 2202 tokens |
| Whole package — every `skills/**/SKILL.md` summed | 4408 lines ·
210279 bytes | 4408 lines · 210273 bytes |
| Whole shipped bundle — the ratchet's own "bundle total" line | 154833
tokens (the gate run in a detached worktree at `1e5d322c1e`) | 154851
tokens (+18: −1 and +19) |

No eval under `skills/objectstack-data/evals/` quotes either sentence
(`grep -rn strip skills/objectstack-data/evals/` — 0 hits). Frontmatter
untouched; `check:skill-docs` and `check:skill-refs` both report in
sync.

## Closing enumeration — every `os migrate meta` sentence in `skills/**`

`git grep -n "os migrate meta" -- skills/` on `0aa5205228`: 30 hits in 5
files (`objectstack-data/SKILL.md` 1,
`objectstack-data/rules/indexing.md` 1, `objectstack-upgrade/SKILL.md`
21, `objectstack-upgrade/evals/protocol-major-upgrade.json` 5,
`objectstack-upgrade/references/examples-upgrade.md` 2) — the same lines
as on `origin/main`, since only the two data-skill lines moved. A
widened `git grep -n "migrate meta" -- skills/` adds one bare hit
(`objectstack-upgrade/SKILL.md:466`), judged too. Three tests per hit:
(a) the default run lists and writes no source; (b) `--write` rewrites
only the provable sites; (c) `--from N` is at or above
`MIGRATION_SUPPORT_FLOOR` (16). A line is changed only when it is false.

| File:line | Sentence (abridged) | Reading | Changed? |
|:--|:--|:--|:--|
| `objectstack-data/SKILL.md:377` | "`os migrate meta --from 16` strips
them." | FALSE on (a) and (b): the default run strips nothing; `--write`
strips only the proven sites | yes |
| `objectstack-data/rules/indexing.md:31` | "run `os migrate meta --from
16` to strip them automatically." | FALSE on (a) and (b) | yes |
| `objectstack-upgrade/SKILL.md:64-66` | Quickstart: `--from 16 --step`,
`--from 16 --json`, `--from 16 --out …`, under the comment "replay the
chain (writes only --out; --write also rewrites the sites it can prove)"
| true: (a) listing runs, `--out` is a snapshot and not a source; (b)
stated; (c) 16 | no |
| `objectstack-upgrade/SKILL.md:74` | "`os migrate meta --from 17
--json` — `applied` must be []" | true: the replay from the target
major; (c) 17 ≥ 16 | no |
| `objectstack-upgrade/SKILL.md:103` | "`os migrate meta --from 16`
replays every step in order … down to the chain's support floor, 16
today" | true: (c) the floor constant is 16 | no |
| `objectstack-upgrade/SKILL.md:124-133` | "What `os migrate meta`
actually does" and its command block (`--out` "write the canonicalized
stack", `--write` "rewrite the proven sites in place") | true on (a),
(b), (c) | no |
| `objectstack-upgrade/SKILL.md:153` | "By default `os migrate meta`
rewrites no source file … `--write` rewrites in place each edit it can
trace to one literal in one project file, lists every other with the
reason …" | true: matches the flag description | no |
| `objectstack-upgrade/SKILL.md:177-179` | `--stored` preview, `--stored
--type …` narrowing, `--stored --apply --yes` | true of the stored pass
(`write` is `exclusive: ['stored']`; `--apply` is the only writer there)
| no |
| `objectstack-upgrade/SKILL.md:215` | "`os migrate meta --from N
--json` → `.specChanges`" | true: the JSON face carries `specChanges`
(`meta.ts:881,928`); N is bounded by the floor sentence at `:103` | no |
| `objectstack-upgrade/SKILL.md:313` | "`os migrate meta --from 16
--json`" piped into `node -e`, reading `.todos` | true: (a) a listing
run; `todos` is the key (`meta.ts:459`) | no |
| `objectstack-upgrade/SKILL.md:404` | "`os migrate meta --from
TARGET_MAJOR --json` — `applied` must be []" (the placeholder is spelled
out here) | true: (a); (c) by construction | no |
| `objectstack-upgrade/SKILL.md:449` | "`os migrate meta --stored
--json` — 0 = every row canonical, 1 = work left" | true of the stored
pass: read-only, exit 1 while rows are pending (`storedMigrationClean`)
| no |
| `objectstack-upgrade/SKILL.md:466` | "`migrate meta` reports changes,
but the files are unchanged — working as designed, the default run only
lists — pass `--write`, or port the printed edits by hand" | true on (a)
and (b) | no |
| `objectstack-upgrade/evals/protocol-major-upgrade.json:7` | "`--step`,
`--json`, and `--out …` — the command rewrites nothing on disk, so port
the printed edits into the sources" | true of the invoked forms (none
carries `--write`; `--out` writes a new snapshot and rewrites no source)
| no |
| `objectstack-upgrade/evals/protocol-major-upgrade.json:10,20` |
`must_contain` token lists | not behaviour claims | no |
| `objectstack-upgrade/evals/protocol-major-upgrade.json:17` | "runs `os
migrate meta --from AUTHORED_MAJOR` to attribute the site to its
`conversionId`, ports `requiredWhen` …" (placeholder spelled out) |
true: (a) the listing attributes; the port is by hand | no |
| `objectstack-upgrade/evals/protocol-major-upgrade.json:37` | "Replays
`os migrate meta --from 17 --json` and reads `applied` … runs `os
migrate meta --stored` (read-only) then `--stored --apply --yes`" | true
on (a), (c), and of the stored pass | no |
| `objectstack-upgrade/references/examples-upgrade.md:57` | "Ported into
sources from `os migrate meta --out`." | true: hand-ported from the
snapshot | no |
| `objectstack-upgrade/references/examples-upgrade.md:79` | "`os migrate
meta --stored --apply` — rows rehydrate correctly today; this makes it
durable." | true of the stored pass | no |

## Local verification (tree `0aa5205228`; every exit code captured
before any pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths; the tool took the changeset from
the merge-base itself): 24 commands. Run after the final commit as one
union with the tool's own loop idiom, recorded as `cmd :: exit N`, and
reconciled: `dispatch-gates --ran` — "24 derived, 24 run, 0
NOT-MEASURED, 0 UNRUN". All 24 exit 0:

`check-ci-filter-parity` · `check-closing-keyword-parity` (+
`--self-test`) · `check-comment-mask-corpus` · `check-doc-route-spelling
--advisory` (+ `--self-test`) · `check-skills-token-ratchet` (+
`--self-test`) · `@objectstack/lint check:doc-formula-expressions` ·
`@objectstack/spec check:skill-docs` · `check:agent-test-spelling` ·
`check:corpus-claim-drift` · `check:cross-package-test-inputs` ·
`check:doc-authoring` · `check:driver-memory-census` ·
`check:gitlink-declared` · `check:nul-bytes` ·
`check:pm-governed-merges` · `check:refd-timer-probe` ·
`check:role-word` · `check:skill-compatibility` ·
`check:skill-frame-sync` · `check:skill-identifier-liveness` ·
`check:watch-hint-literal`.

- `check:doc-formula-expressions` first answered exit 3 (PREREQUISITE
NOT MET — `@objectstack/formula` and `@objectstack/lint` were unbuilt;
nothing measured). After `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint
--concurrency=2` behind `scripts/pm/os-verify-lock.sh` (VERDICT
command-exit 0; held 92 s, waited 0 s) it exits 0.
- Also run, outside the derivation: `pnpm --filter @objectstack/spec run
check:skill-refs` exit 0 ("9 generated files in sync"); `pnpm
check:skill-top-level-keys` exit 0.
- NOT MEASURED locally, by design: the type-check lanes, the Test Core
shards, the 11 wide-population families and the 51 roster families the
derivation names are CI's; the 15 pending-changeset families do not
apply (no changeset — this diff publishes nothing from any released
package; label `skip-changeset`).

### Reverse verification of the ratchet payment (one-off; committed
first; `scripts/ablation-replace.mjs`)

- Attempt 1 was a no-op by the tool's own count check (the anchor was a
substring of its replacement, anchor count 1 → 1): refused and restored,
nothing measured.
- Attempt 2, re-anchored on "prove, the rest by hand." with the deleted
clause re-added: mutation landed on disk (blob `7220a34363d3` →
`efe8f6825326`); `node scripts/check-skills-token-ratchet.mjs` exit 1 —
"`skills/objectstack-data/SKILL.md` is 6144 tokens; the ratchet ceiling
is 6128 (over by 16)". Restore proven: blob == HEAD `7220a34363d3`, `git
diff HEAD` empty, `git status --porcelain` empty.

## Acceptance notes

- The class pin
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts`
(`WITHDRAWN_CLAIM`) matches only "to rewrite … automatically" and the
"rewrites (it for you / existing sources / authored sources / your
sources / your source files)" spellings — no "strip" — so it never saw
these two lines, and nothing in this PR is implied to be covered by it.
Widening it is spec-lane work and the file is held by PR objectstack-ai#22142; it is
not touched here. Carrier: the `domain:spec` seat, via the skills seat's
relay.
- `skills/objectstack-data/SKILL.md` now sits 1 token under its
unchanged 6128 ceiling. Lowering the ceiling to 6127 is legitimate per
the gate's own header and outside this card's file surface. Carrier: the
next PR that touches `scripts/check-skills-token-ratchet.mjs`, or the
skills seat.
- Tier H (`skills/**`): this PR stays draft and lands on an authorized
APPROVED review or the maintainer's hand; no seat flips it ready.

## 维护者速读(草稿)

**改了什么**:objectstack-data
技能里两句话(`rules/indexing.md:31`、`SKILL.md:377`)原本说 `os migrate meta --from
16` 会"自动剥掉"/"剥掉"已退役的索引键 `type` 和 `partial`。改成工具的真实行为:默认只列出机械修改;`--write`
只落它能证明的那些站点;其余由作者手工完成。顺带把 `skills/**` 里所有 `os migrate meta`
句子逐条核对了一遍(上表),其余均属实,一字未动。

**为什么改**:这是对外发布的技能包(`npx skills add` 原样装进客户项目),读到这句的 AI
作者会以为源码已被清理,把退役键留在原地,直到 `tsc` 或解析报错才发现。`docs/NORTH-STAR.md` 优先级第 4
条:发布面上的错句就是产品缺陷;`retired-key.ts` 的内部裁决是"这句话必须对工具为真"。

**风险与代价(含回滚)**:纯文本改动,不碰代码、不碰 spec、不发包、无 changeset。`SKILL.md` 已顶在 token
上限,新句子靠删掉同一段里被下文重述的一句付账(内容仍在 `SKILL.md:387-388` 与
`rules/indexing.md`)。回滚 = revert 这一个提交。

**席位意见**:

**你要做的**:确认两句新措辞与 PR objectstack-ai#22142 正在采用的 house sentence 一致;同意则 APPROVE,由席位落地。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…printable block subset; the zero-reader document schemas retire whole (objectstack-ai#22158) (objectstack-ai#22193)

Fixes objectstack-ai#22158

Clause-②: yes (narrowing)

Card ① (spec only) of the ruling of record on objectstack-ai#8346. objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 (objectstack-ai#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`, objectstack-ai#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 + objectstack-ai#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-test`s: 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`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…l, organization, ownership, and every guest key's fate (objectstack-ai#22239)

Part of objectstack-ai#22146
Clause-②: no

## What this PR is

Round 3 of objectstack-ai#22146: one new decision record,
`docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md`,
**Status: Proposed**. It transcribes the maintainer's ruling
[`6054113537`](objectstack-ai#22146 (comment))
(batch objectstack-ai#290 item 1, 「22146 同意」: A on all eight questions and G2 on the
gap, with the Q4 and Q2 clarifications) into nine decisions. Each one
states its contract and its enforcement point. The revision round
applies the ruling supplement
[`6056614963`](objectstack-ai#22146 (comment))
(maintainer 「D1 A′ D2b R」): D1 is revised to A′, and D2b is closed as R.

- **No code changes.** Nothing changes in `packages/**`,
`content/docs/**` or `skills/**`. The ruling places every code change
after acceptance, as execution cards E1 to E4 (the record's *Execution
plan*).
- **Tier H.** The diff touches `docs/adr/**`, so this PR stays draft and
lands only by the maintainer's hand.
- **The card stays open** for the execution cards, so line 1 is `Part
of`.

## What the record says, one line per decision

- **D1, identity and ownership (A′).** The guest is a principal and
never owns a record; a forged owner is refused; the audit names the
guest as the actor. Who owns a guest-written row is the business
scenario's own metadata: the door's declaration, the object's hooks,
record-change flows and assignment rules. The platform stamps nothing
and declares no default owner. Empty state: the owner stays unset, as
the form doors do today, with the existing authoring advisory and no
publish refusal. The enforcer is the guest branch of the owner-anchor
stamp in `SecurityPlugin` (card E3).
- **D2, the closed list of doors.** Exactly five door classes serve an
unauthenticated request:
  - the public form doors;
  - share links;
  - the public book and doc reads;
  - `authRequired: false` endpoints of type `object_operation`;
  - `authRequired: false` endpoints of type `flow`.

Everything else answers 401, decided once per domain by
`shouldDenyAnonymous`. The control-plane allowlist, the signed
inbound-hook channel and MCP are credentialed or infrastructure, so they
are outside the guest model.
- **D2b, anonymous door × elevated flow (R).** Publish refuses an
`authRequired: false` flow endpoint whose target declares `runAs:
'system'`, in both directions, with a prescription: an authenticated
endpoint, the signed inbound-hook channel, a public form, and `runAs:
'automation'` once ADR-0073 M2 lands. No door triggers an elevated flow
directly. Card E2 implements it with a registered ADR-0087 semantic
entry. Record-change flows fired by a guest-written row stay recorded
and undecided.
- **D3, the grants channel (ADR-0090 D9, enforced).** The `guest`
anchor's bindings resolve for the guest, read through the one binding
reader every position uses. An empty set denies all. There is no second
channel: not the baseline, not `everyone`, and not the position-name
fold. The binding tier is unchanged. The row scope runs on one pipeline,
and E1 pins it per sharing model.
- **D4, organization.** The guest's organization is resolved only when
the deployment has a unique organization, using the same predicate as
ADR-0131 D9. On a multi-organization deployment the request is refused
until D5 exists. The question is asked only where the organization is
needed.
- **Clarification (1), carried into the record:** the form doors'
declared default-organization binding stays as it is. Nothing that
serves today starts refusing.
- **D5, site binding.** The record gives the shape only: match,
organization, guest grants and allowed doors. ⛔ It declares no metadata
type and reserves no key. The binding is built when a named deployment
needs it.
- **D6 and D7.** ADR-0106 D7, explain's `EXTERNAL` floor and ADR-0121 D6
are unchanged. The webhook signature vocabulary goes to a follow-up
card, F1. That card is named in the record and not filed.
- **D8, guest keys.** Every declared guest key gets a fate and an
ADR-0087 disposition.
- `sys_record_share`'s `guest` recipient is to be **removed** on its own
card, E4. The basis is ADR-0090 D11 and the already-registered
`sharing-rule-recipient-reconcile` entry.
- The form doors' `guest_portal` set name was not on the card's list. It
is recorded too, with the fate keep.
- **D9.** The record now holds the maintainer's ruling of 2026-08-08
(Option A, `f586f1a89`), which until now lived only in the module doc of
`assemble-execution-context.ts`.

## What the revision changed (supplement
[`6056614963`](objectstack-ai#22146 (comment)))

- **D1 → A′.** Points 3, 5 and 6 of the draft (the organization-level
default owner, its cardinality, the empty-state refusal) are replaced:
ownership is the scenario's own metadata, and the empty state is the
owner left unset. Points 1, 2 and 4 are kept.
- **D5.** The default-owner element is removed from the shape.
- **D2b → R**, with its enforcement text. The four-axis table stays as
the reasoning. M and the first-drafted default owner move to
*Alternatives considered*.
- **Execution plan.** E2 carries R and its registered ADR-0087 semantic
entry. E3 shrinks to the stamp's guest branch (never the guest, never
the system principal, a forged owner refused, the owner unset unless the
scenario sets it): no new key and no disposition.
- **Elsewhere.** The Consequences paragraph on owner-assigning hooks is
withdrawn. *What the ruling did not settle* loses D2b and D1's empty
state. Acceptance criterion 3 (D2b chosen) is met. The supplement is
added to *Decided by*.
- **Consistency edits the ruled changes forced:** the Status line, the
Consumers line (the spec change is now D2b's refusal, not a D1 key), D4
point 3 (no owner stamp left to need the organization), D2's class 5
row, follow-up F3 (M2 now only extends R's prescription), and the
References.

## What stays open

- **The indirect path:** record-change flows fired by a guest-written
row are recorded and not decided.
- **The spelling of D5's binding** belongs to the card that builds it.
- **The guest's row scope** is stated as a contract; E1 pins its
measured outcome.

## How the number was chosen: 0138

- `origin/main` at `73a0a6bf1d`, after this round's merge, tops out at
0137, and 0136 is absent. Still no open PR adds 0136 or 0138; objectstack-ai#22198,
the one ADR PR at the first count, has landed on ADR-0048.
- **I checked every open PR's file list.** That is all 20 open PRs,
paged to the end for objectstack-ai#22142 (104 files) and objectstack-ai#21988 (229 files). Only
objectstack-ai#22198 touches `docs/adr/`, and it touches
`0048-cross-package-metadata-collision.md`. No open PR adds 0136 or
0138.
- **0136 is not reused.** It was handed to a decision once: unmerged PR
objectstack-ai#18480 added `0136-declared-journeys-as-priority-anchor.md`, and objectstack-ai#18985
renumbered its own record from 0136 to 0137 because of it.
`scripts/check-adr-anchors.mjs` computes the next free number as the
highest number plus one, which gives 0138.

## Back-pointers: none in this PR, by house practice

- **Where the lines go.** The house form for an amended record is a
status-line continuation. ADR-0042, ADR-0046 and ADR-0131 carry lines of
the form "· **Amended** (date, ADR-NNNN Dk) — …". These lines are
written when the amendment is in force. No record in the registry
carries such a line pointing at a Proposed record. So the exact lines
for **ADR-0090** (D9) and **ADR-0056** (D2) are written into the
record's *Acceptance criteria*, to land with the accepting change.
- **Which ADRs get no line.** ADR-0106 D7, ADR-0121 D6 and ADR-0096
D5/E1 are left unchanged by this record, so they get none. **ADR-0135**
gets none either: it mirrors cloud ADR-0024 and adds no clause of its
own, and this record amends none of its decisions.

## Verification at `2d69b2b714` (the revision, on a merge of main
`73a0a6bf1d`)

I ran every command in the claim-time gate list at the revision head.
Each exit code was captured before any pipe.

| Command | Exit |
|:--|:--|
| `node scripts/check-adr-links.mjs` (and `--self-test`) | 0, 0 |
| `node scripts/check-adr-symbol-anchors.mjs` (and `--self-test`) | 0, 0
|
| `node scripts/check-ci-filter-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`) |
0, 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 (see note) |
| `pnpm check:adr-anchors` | 0 |
| `pnpm check:cross-package-test-inputs` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:gitlink-declared` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:pm-prior-rulings` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:watch-hint-literal` | 0 |

- **`check-adr-symbol-anchors`.** It reports "2225 anchors across 141
records resolve". Positive control: the record count is 141, which is
the 140 at base plus this record. No anchor carries a line number.
- **`check:pm-prior-rulings`.** The self-test passes 155 cases. The
tool's `--card 22146` read returns 16 ADR decision hits and 1 ruling on
the thread (`6054113537`).
- **`check:doc-formula-expressions`.** In the first round its first run
exited 3 (PREREQUISITE NOT MET: the closure was not built), which
measured nothing. I rebuilt the `@objectstack/formula` and
`@objectstack/lint` closure under the verify lock after this round's
merge (VERDICT command-exit 0), and the gate exited 0.
- **Re-derivation.** `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `2d69b2b714` gives the same 19 families.
The `--ran` reconciliation shows 19 run and 0 NOT-MEASURED, a zero
derived from the recorded exit codes. The change set is one file,
+640/−0, against merge base `73a0a6bf1`.

## Not measured, and named

- **The G2 sweep (the booted per-door-class anonymous sweep).** I did
not run it, by the ruling: it is an acceptance precondition, to be run
in an environment that permits the probe. This round wrote no driver and
no probe.
- **The platform documentation links.** I could not re-fetch them,
because the container's proxy answers 403 to CONNECT for those hosts.
The record carries the URLs that the measurement round recorded.
- **The cloud repository's anonymous surfaces.** This round did not read
them.

## Changeset

I read the Check Changeset job in `pr-automation.yml`. It has two
exemptions: the `skip-changeset` label (read live, twice) and the
Changesets release PR. Its failure text prescribes: "if it releases
nothing …, apply the 'skip-changeset' label". This PR adds no
`.changeset/*.md` and changes none, and `docs/adr/**` ships in no
package's `files[]`. So the label is `skip-changeset`.

## Acceptance notes

- **A doc comment that describes the old form-door semantics.** The
comment above `RestServer.registerFormEndpoints` in
`packages/rest/src/rest-server.ts` still says that security is delegated
to a `guest_portal` set carried on the context, and that the middleware
falls open when none is registered. What admits a form submission today
is the `publicFormGrant` branch in `SecurityPlugin`.
- This is comment drift only, and no reach was measured. It is noted,
not filed.
  - Carrier: none. Card E2, once cut, edits that domain.

## 维护者速读(草稿)

- **改了什么:** 按你「D1 A′ D2b R」的补充裁决修订 ADR-0138 草稿,其余内容不动。
- D1:访客永不拥有记录、伪造的 owner 一律拒绝、操作留痕记在访客名下,这三条保留。"本组织声明一个默认
owner"整条删掉:访客写进来的记录归谁,由各业务场景在元数据里自己定(入口声明、对象钩子、记录触发流程、分配规则),平台不打任何默认值。没人设置时
owner 就空着,跟今天公开表单一样;表单的作者视图照常提示,发布不拒绝。
- D2b:匿名入口不能触发以系统身份运行的流程。发布时直接拒绝,端点和流程两头都检查,并给出替代办法(带凭据的端点、签名 webhook
通道、公开表单,以及 M2 落地后的 automation)。
  - 站点绑定的形状里去掉"默认 owner";执行卡 E3 缩成只校验访客分支,不新增任何键。
- **为什么改:** 你指出归属是业务场景的事,平台级默认值在多数部署里是错的,还会抢在对象自己的分配逻辑前面。D2b 选 R 之后,AI
照着 `runAs` 的说明写出"匿名入口 + 系统身份流程"时,发布就会被拦下。
- **风险与代价(含回滚):**
  - 本 PR 仍只改一个文档文件,合并后运行时行为不变;回滚就是删掉这个文件。
  - D2b 的拒绝会让"匿名端点指向系统身份流程"这种写法从此发布不了。仓内没有这样的声明;仓外的部署本轮没有测。
  - 接受前提还剩一项:在允许探测的环境里做一次启动后的逐类匿名入口实测(G2)。
- **席位意见:**
- **你要做的:** 修订版就绪后,批准这份 ADR(Tier H,点 Approve 或亲手合并)。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): os migrate meta --write — the AST codemod that rewrites authored sources for the mechanical applied set (v18)

2 participants