Repository navigation
fix(spec): the retirement sentence names --write: it applies the edits it can prove, you apply the rest - #22142
Conversation
…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
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK
…irement-sentence-write
… 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
📓 Docs Drift CheckThis PR changes 3 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: 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 Gate verdicts (the check-runs on the head): 42 runs, 38 ① Derived judgmentsEach item names what the diff implies and whether it is right.
② Semver level
③ Boundary flagsDev deviations, report
Dev deviations, report
Out-of-scope findings, both reports, answered:
New from this review, in neither report:
No Implemented-by: VERDICT: PASS Generated by Claude Code |
Contract reviewServed-tier: 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 Gate verdicts (the check-runs on the head): 42 runs, 38 ① Derived judgments
② Semver level
③ Boundary flagsReport
Report
Report
Report
New from this review, in no report:
No Implemented-by: VERDICT: PASS Generated by Claude Code |
…irement-sentence-write
…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
|
Regen-provenance: 6053587390 · fe3af56 → 7530236 ·
|
|
The merge queue refused this PR, as designed; the seat's pointer did not certify the hop.
|
Contract reviewServed-tier: Rendered 2026-10-08T07:39Z. PR #22142 at this head against its merge base Why this record is owed, re-derived. This head is
Check-runs on this head. 36 runs: 34 ① Derived judgmentsSurface (a), the shipped prescription text across
Surface (b),
② Semver level
One completeness gap, non-blocking: the changeset's ③ Boundary flagsEvery deviation and finding in
Nothing above blocks. Both surfaces hold at this head, and the hop added nothing to the PR's net diff. Implemented-by: VERDICT: PASS Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37748210189 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
The red merge-group build above is not this PR's.
|
…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>
…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>
…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>
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):
After:
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 dashboardcompareTo.offsetand the script node'sconfig.actionType.Checked against the tool on
main(51290bca).packages/cli/src/commands/migrate/meta.tsdeclareswrite: 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--outsnapshot. The wording satisfies every ruling that binds it:6045697201. The sentence never says "rewrite existing sources automatically" unqualified ("the ones it can prove"), and it names--writebecause the default run still only lists.meta.ts.os migrate meta --writebeside 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 ofmainadded no site)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 handsurvives only in the pin's own RED fixtures.migrations/registry.ts: 3 sentences, regenerated from the movedentries/semantic/18.*.tsbygen:migration-registry.chartConfig.xAxis.fieldhint invalidate-widget-bindings.ts: a template literal with interpolation after the sentence, so the pin cannot see it (Acceptance notes).retiredKey()docblock example.apply them by handin 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.form-layout-inline-grid-retired.test.ts, the turso / driver-memorytoContain('os migrate meta --from 17')) are untouched, because they stay true..changeset/9591-retirement-sentence-write.md:patchfor@objectstack/spec,@objectstack/lintand@objectstack/driver-turso, the three packages whose shipped text moves.content/docs/references/**: 32 files (+268/−268) frompnpm --filter @objectstack/spec check:generated --fix;check:docswas the only stale artifact.check:generatedthen exited 0.The class pin (
retired-key-migrate-sentence.test.ts)HOUSE_AT_MARKERandMIXED_AT_MARKER, and their markdown twins, require the new clause. Theos migrate metanever 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--writewithout the qualification, and one that qualifies it but leaves the rest unowned.WITHDRAWN_CLAIMis unchanged: the unqualified automatic-rewrite claim stays a hard RED everywhere. A new non-vacuity case proves neither legal shape trips it.os migrate meta's own flag table. Awriteboolean flag must exist and must havedefault: false, the two facts the sentence rests on. The read is covered by@objectstack/spec's existingpackages/**/*.tscross-package declaration.packages/drivers/driver-turso/srcjoins, on [lint] validate-expressions 的 script 退役键提示仍说 "rewrite it" — #6856 house 句式的最后一个域外站点 #7030's terms. Its threetursoconfig tombstones carry the house sentence, and their docblock defers toretired-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.retired-key.tsmodule 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
017761f0throughscripts/ablation-replace.mjs(each anchor hit as declared and was restored to a blob equal to HEAD withgit diff HEADempty). Expected direction: red.turso.zod.tssentences back to the #9529 wording (anchor ×3→0, blobe25cca4d5508→a05fe3f09733)driver-turso:spec/turso.zod.ts:63,:75,:82meta.tswriteflagdefault: false→true(blobc036012c63c9→71acff21ef8c)meta.tsflag renamedwrite→inPlace(blobc036012c63c9→29a8a9402284)writeboolean 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_RETIREDThe three
record:chatter/record:discussionpositionvalue prescriptions ('sidebar','inline','drawer', inui/component.zod.ts) told the author to run a bareos migrate meta. The command refuses that withMissing required flag --from(meta.tsrun(), theflags.from === undefinedbranch). The conversion isrecord-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_TEMPLATEandSKILL_MIXED_TEMPLATEmust 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.mjsexits 0. ⛔ No publishedskills/**file changes: PR #22122 ownsskills/objectstack-upgrade/SKILL.md, and no published skill carries the sentence (git grepcount 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 headf9ca14d548)pnpm --filter @objectstack/spec build: VERDICT command-exit 0.check:generated --fixregenerated the one stale artifact;check:generatedthen exited 0 (15 of 15 current), and again in the gate run atf9ca14d548.vitest run --project local: Test Files 623 passed (623), Tests 18613 passed, 1 todo, at017761f0.vitest run --project repo(53 files incl. the class pin): 53 passed (53), Tests 903 passed (903), at017761f0.@objectstack/driver-tursovitest run: Test Files 88 passed (88), Tests 2373 passed, 33 skipped, at017761f0(afterpnpm --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).typecheckfor spec (tsc --noEmit+check:scripts-typecheck+check:test-typecheck), lint and driver-turso: exit 0 at017761f0.main(033e5c536d: docs(skills): objectstack-upgrade namesos migrate meta --writebeside 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) asf9ca14d548, 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 editsvalidate-expressions.ts): the class pin 15 passed (15);@objectstack/lintvitest run: Test Files 123 passed (123), Tests 5688 passed (5688); linttypecheckexit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatf9ca14d548derives 124 commands (the claim-time 79 plus 45). All 124 exit 0 atf9ca14d548.--ranreconciliation: 124 derived, 124 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. (At017761f0, 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 thef9ca14d548run.)node scripts/pm/check-skill-line-ratchet.mjs: exit 0; the playbook is 337 lines (ceiling 337), and no line is over 120 bytes.pnpm exec eslint --no-inline-config --format jsonover the 66 changed.tsfiles reports 66 files, 0 errors and 0 warnings. The population iseslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}glob, which excludes the changed.md/.mdxfiles. 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-widepnpm lintis 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
mainbefore landing.Acceptance notes
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 aredomain:devxpages outside this claim, so they are not touched here.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 theos migrate metanever 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.migrations/registry.tsmigration notes (outside the pin's scope by design) and theconversions/registry.tscomments.chartConfig.xAxis.fieldhint (validate-widget-bindings.ts) moved, but it remains invisible to the class pin: a template literal, withsuggestName(…)and the suppress hint interpolated after the sentence.skills/objectstack-data/rules/indexing.md:31says "runos migrate meta --from 16to strip them automatically", andskills/objectstack-data/SKILL.md:377says the command "strips them". The default run strips nothing from sources, and--writestrips only what it can prove.WITHDRAWN_CLAIMhas no strip spelling, so the pin cannot see this. Widening it here would redmainon a Tier H file this PR may not touch, so it is reported to the PM for the skills lane.config.actionTypetwo-clause tail ("the stub and marker values are removed") is unchanged in substance; only the--writeclause was inserted before it.Patch round 1 (written by the PM seat from the dev's report
6052088494)Merge:
origin/mainef1fcb26a2(PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103) was merged throughos-regen-merge.shasfeca6b5ace. The three reference pages both sides had changed (api/metadata,data/object,system/migration) were regenerated from the merged tree as98e2f6e373. That brings back feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103'sunique?: false | 'global' | 'organization'rows, which the driver had dropped.feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103's sites: the merge brought two non-test sites with the old tail and one test that asserts it verbatim. All three carry the new sentence at
b9d6e82619:packages/spec/src/data/object.zod.ts(DECLARED_INDEX_BARE_TRUE_RETIRED);packages/lint/src/data-model-rules.ts(theunique-unscoped-declared-indexfix text);unique-scope-message.test.ts.The pin now judges 163 sentences:
spec158 (156 house + 2 two-clause),lint2,driver-turso3.The pin's blind spot: the
data-model-rules.tssentence sat in a template literal, which the judge cannot read (escaped backticks), so it was never judged. It is now plain-quoted, as invalidate-expressions.ts, and the pin's Mechanism paragraph records that template literals are invisible to the scan. Ablation D (that sentence back to the old tail) gives 3 failed / 12 passed, naminglint:data-model-rules.ts:463.validate-widget-bindings.tsstays the one template-literal site the pin cannot judge (an Acceptance note).Verification at
b9d6e82619:--project local: 623 files / 18,619 tests;--project repo: 53 / 903;dispatch-gates --ran: 124 derived / 124 run / 0 NOT-MEASURED;Patch round 2 (written by the PM seat from the dev's report
6053397952; claim revised6052335087)fec87e7e07) landed first with a two-clause prescription lacking the--writeclause, which this PR's class pin refuses. The sibling rule here ("whichever lands later carries the new sentence") puts the edit in this PR.origin/main959c209d56was merged throughos-regen-merge.shas3b6335b9be, with no hand-written conflict.content/docs/references/api/protocol.mdxwas regenerated asc40b3babd7.fe3af5642c, 4 files beyond the merge):packages/spec/src/kernel/manifest.zod.tsPLUGIN_PERMISSIONS_LIST_FORMnow closes with "Runos migrate meta --from 17to list the mechanical edits for the package manifest case;--writeapplies 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--writeclause, the seat's wording.manifest-permissions-string-list.test.tsmoved with it.compareTo.offset, the script node'sconfig.actionType, and the package manifestpermissionscase. The pin judges 164 sentences (spec 159 = 156 house + 3 two-clause; lint 2; driver-turso 3) with 0 bad sites.fe3af5642c:--project local: 625 files / 18,661 tests;--project repo: 53 / 903;dispatch-gates --ran: 124 / 124 / 0 NOT-MEASURED;Generated by Claude Code