Repository navigation
feat(cli): os migrate meta --write — write the chain's mechanical edits into the authored sources - #22108
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…sals Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…ence; changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
The human report keeps its indentation (a --write failure exits from inside the try, and the catch rethrows an oclif exit), so a later merge of main reconciles cleanly. The pins hold that --write writes exactly the applied set, that semantic TODOs are listed as the dry run lists them (not a pinned listing), and that both failure exits leave no file half-written. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 28 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 1eb5ffe4cc8efb26516210cda30de3215615c536 && git checkout 1eb5ffe4cc8efb26516210cda30de3215615c536
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 172be37da7a767a8364ca841af3ed7dbb426ab5e 94dfa0c23d9af0f49df0daf96f72cdcc9dfabae4 && git checkout -B drift-repro 172be37da7a767a8364ca841af3ed7dbb426ab5e && git merge --no-ff 94dfa0c23d9af0f49df0daf96f72cdcc9dfabae4
node scripts/docs-audit/affected-docs.mjs --json 172be37da7a767a8364ca841af3ed7dbb426ab5e
|
…predicate `test/exit-signal.pin.test.ts` holds every catch that a this.exit() inside its try can reach to open with `if (isExitSignal(error)) throw error;`. The --write failure exits added two such calls, and the catch opened with a hand-rolled `oclif.exit` check instead; the pin named both lines. Same behaviour, the one spelling the pin reads. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Contract reviewServed-tier: Read-only shape held: the net diff Check-runs on ① Derived judgmentsEach accept-set and public-surface change the diff implies, read off the code at the head, judged against the changeset, the PR body and the two docs pages.
② Semver level
Clause-②: yes (widening) — a new public CLI flag that writes users' authored source files. Declared on the PR body (line 2, ③ Boundary flags
Implemented-by: VERDICT: PASS |
…ide the default run (objectstack-ai#22122) Fixes objectstack-ai#22120 Clause-②: no The published upgrade skill said that `os migrate meta` "writes nothing but `--out`" (Quickstart comment, failure-mode row) and that it "does not rewrite your source files" (§1). Since `os migrate meta --write` landed (`a959493cdf`), that is true only of the default run. This PR makes every sentence in `skills/objectstack-upgrade/SKILL.md` that states what the command writes true of both routes, within the skill's token ceiling. ## What changed (one file: `skills/objectstack-upgrade/SKILL.md`) - Quickstart step 1 comment: "(writes only --out; --write also rewrites the sites it can prove)". - Flag table in §1: one new example line, `os migrate meta --from 16 --write # rewrite the proven sites in place`. - §1 "The one fact that surprises every operator": now opens "By default `os migrate meta` rewrites no source file. It lists the mechanical edits and writes only the `--out` JSON snapshot." followed by one sentence for `--write`: it 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, never writes a semantic change, and if re-running the chain over the written files disagrees, restores every file and exits 1. The porting sentence now reads "Porting the edits left unwritten is yours" — true on both routes. - Failure-mode row "`migrate meta` reports changes, but the files are unchanged": cause "Working as designed — the default run only lists."; fix "Pass `--write`, or port the printed edits by hand; then replay from the target major to confirm 0 changes." - Failure-mode row "`--apply` refused / stored-only flag rejected": the tail "the authored-source chain has nothing to write to" (false under `--write`) now mirrors the CLI's own refusal text: "writes only `--out` and, with `--write`, the sources". Every claim is read from `packages/cli/src/commands/migrate/meta.ts` at `db4c45b8c3` (flag description and `exclusive: ['stored']`; `WriteOutcome.status` = `written | restored | unwritten`; `printWriteOutcome` lists each unwritten site as "not written [kind]: reason"; `this.exit(1)` whenever `write.status !== 'written'`) and from the `packages/cli/src/utils/authored-source-codemod.ts` module docblock (one object or array literal in one project file, statically matching the loaded value, no second reference to any binding the walk crossed; semantic TODOs never read). The default run and `--write` are both described; the text does not say what `--out` does on a run with nothing to migrate and does not describe the semantic-notice list (both are in flight on `meta.ts` in objectstack-ai#22121 and objectstack-ai#22115). ## Token ratchet — paid in content, not wrapping `node scripts/check-skills-token-ratchet.mjs` (tokens = ceil(utf8 bytes / 4); ceiling for this file 6193): | reading | lines | bytes | tokens | headroom | |:--|--:|--:|--:|--:| | before (`db4c45b8c3`) | 488 | 24772 | 6193 | 0 | | after (`9bb10014`) | 484 | 24748 | 6187 | 6 | Diff: +13 / −17 lines. Gate line at `9bb10014`: `✓ check-skills-token-ratchet: skills/objectstack-upgrade/SKILL.md is 6187 tokens (ceiling 6193; headroom 6).` The growth (+179 bytes gross) was paid by deleting three pieces of duplicated content, no rule, failure-mode row or needed command among them: 1. the `--out` recheck code block in §1 — its first line was byte-identical to Quickstart step 1 (`os migrate meta --from 16 --out .upgrade/migrated.stack.json`), and the "replay from the target major, 0 changes" recheck is already carried by Quickstart step 3, the §3.3 callout and the failure-mode row; 2. the clause "It rewrites the loaded stack *in memory* and reports the diff" — the mechanism paragraph three lines above already states load → normalize without the load-time pass → replay per hop → parse; 3. the sentence "The authored-source flags and the stored-only flags are mutually exclusive, and mixing them is refused rather than ignored" in the `--stored` subsection — the failure-mode row "`--apply` refused / stored-only flag rejected" carries the same fact with its fix, and the preceding "`--stored` takes no `--from`" keeps the other direction. ## Scope held - No `packages/spec/**` (the retirement sentence in `retired-key.ts` is objectstack-ai#9591's spec-lane remainder; this text uses its vocabulary — "lists the mechanical edits" — so the two read consistently once that lands). No `content/docs/**` (devx lane under objectstack-ai#22108). No generated listing: frontmatter unchanged, `check:skill-docs` green ("Skill docs in sync"). - Siblings untouched and still true of the by-hand route: `references/examples-upgrade.md:57` ("Ported into sources from `os migrate meta --out`") and `evals/protocol-major-upgrade.json` (`must_contain` includes `--out`; the eval's expected answer describes the default route, which still rewrites no source). ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the changeset at `9bb10014` (24 families; identical to the path-derived list). All 24 run, exit codes recorded beside the printed command and reconciled with `--ran` (see the report comment on objectstack-ai#22120 for the table). `check:doc-formula-expressions` first exited 3 (prerequisite: `@objectstack/lint` not built) and was re-run after the prescribed build. ## Acceptance notes - Out of scope, reported for a card: `SKILL.md:103` says "`os migrate meta --from 10` replays every step in order", but `MIGRATION_SUPPORT_FLOOR = 16` (`packages/spec/src/migrations/registry.ts:79`), so that command refuses with `MigrationFloorError` / `unsupported_from_major` — the skill's own failure-mode row says so. Not fixed here: the sentence's point ("several majors late is the designed-for case") cannot be re-exampled truthfully on a 16 → 17 chain, so the fix is a rewording, not a mechanical edit. - Observation, not changed: `evals/protocol-major-upgrade.json` eval 1 `expected_output` says "the command rewrites nothing on disk" — true of the default route it describes; a `--write`-aware eval is a product decision, not a drift fix. ## 维护者速读(草稿) **改了什么**:只改一份对外发布的技能文件 `skills/objectstack-upgrade/SKILL.md`。原文在三处断言 `os migrate meta` "只写 `--out`、不改源文件";自 `--write` 落地后这只对默认运行成立。现在每一句关于"命令写什么"的话都同时对默认运行和 `--write` 成立:默认只列出机械修改、只写 `--out` 快照;`--write` 只就地改写它能证明来源的站点(一个项目文件里的一个字面量),其余逐条列出未写原因,语义修改永不写,复跑不一致时恢复全部文件并以 1 退出。 **为什么改**:客户项目的 AI agent 整包加载这份技能;它读到"命令只写 `--out`"就永远不会发现 `--write`,而读到无条件的"自动改写"又会被误导。文字必须与 CLI 源码一致(`meta.ts` 的 flag 描述、`written | restored | unwritten` 三态、`exit 1`),并与 spec 侧退休句的措辞("列出机械修改")保持一致。 **风险与代价(含回滚)**:纯文本改动,无代码、无 changeset、无生成物。token 棘轮:6193 → 6187(上限 6193),净减 24 字节,靠删除三处重复内容支付,未删任何规则、故障行或命令。回滚即 revert 本 PR 的一个提交。注意 `objectstack-ai#22121`(`--out` 在无变更运行时的行为)与 `objectstack-ai#22115`(语义通知列表)在 `meta.ts` 上并行,本文未对那两点做任何断言。 **席位意见**:(留空,席位定稿) **你要做的**:`skills/**` 为 Tier H 受管面:请以授权账号 APPROVE 一次,或亲手合入;本 PR 保持 draft,不由 agent 翻 ready。 --- _Generated by [Claude Code](https://claude.ai/code/session_0181E4ZeZmWyknawnauxD2CE)_ Co-authored-by: Claude <noreply@anthropic.com>
…its it can prove, you apply the rest (objectstack-ai#22142) Fixes objectstack-ai#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 objectstack-ai#22108 (`a959493cdf`). ## The sentence Before (the objectstack-ai#9529 wording): ```text Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand. ``` After: ```text 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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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`: objectstack-ai#22122, objectstack-ai#22127, objectstack-ai#22106) as `f9ca14d548`, with no conflict (objectstack-ai#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 objectstack-ai#21982, PR objectstack-ai#22094 (objectstack-ai#13458) and PR objectstack-ai#22103 (objectstack-ai#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 objectstack-ai#22108 added `--write`, and the item still asserts a print-only command. Its step 1 greps for the objectstack-ai#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`) - **Merge:** `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103) was merged through `os-regen-merge.sh` as `feca6b5ace`. The three reference pages both sides had changed (`api/metadata`, `data/object`, `system/migration`) were regenerated from the merged tree as `98e2f6e373`. That brings back objectstack-ai#22103's `unique?: false | 'global' | 'organization'` rows, which the driver had dropped. - **objectstack-ai#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` (the `unique-unscoped-declared-index` fix text); - `unique-scope-message.test.ts`. The pin now judges 163 sentences: `spec` 158 (156 house + 2 two-clause), `lint` 2, `driver-turso` 3. - **The pin's blind spot:** the `data-model-rules.ts` sentence sat in a template literal, which the judge cannot read (escaped backticks), so it was never judged. It is now plain-quoted, as in `validate-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, naming `lint:data-model-rules.ts:463`. `validate-widget-bindings.ts` stays the one template-literal site the pin cannot judge (an Acceptance note). - **Verification at `b9d6e82619`:** - spec `--project local`: 623 files / 18,619 tests; - spec `--project repo`: 53 / 903; - lint: 123 / 5,689; - driver-turso: 88 / 2,373; - typecheck: exit 0 for all three packages; - `dispatch-gates --ran`: 124 derived / 124 run / 0 NOT-MEASURED; - CI: 33 success, 2 expected skips. ## Patch round 2 (written by the PM seat from the dev's report `6053397952`; claim revised `6052335087`) - **Why:** PR objectstack-ai#22094 (objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Refs #9591 (os migrate meta --write; the retirement-sentence restoration goes to the spec lane)
Clause-②: yes
What this adds
os migrate meta --from N --writewrites the chain's mechanical changes into the authored source files in place, at every site it can prove is one literal in one project file. It lists every other mechanical change with the reason it was not written. It never writes a semantic TODO. Without--writenothing changes: the dry run, its report and its--jsonpayload are what they were, and--outstill writes its snapshot.New module:
packages/cli/src/utils/authored-source-codemod.ts(it uses thetscompiler namespace thatts-morphre-exports, the same wayemitted-source-parses.tsdoes). The command change is inpackages/cli/src/commands/migrate/meta.ts.The widened surface, stated for the contract review
--writeis a boolean that defaults to false, on the authored-source mode only. It declaresexclusive: ['stored'], soos migrate meta --stored --writeis refused by oclif's flag validation before any work, with exit 2 and--stored=true cannot also be provided when using --write. This is the same mechanism as--out/--from/--to/--step.--stored/--applyare untouched.appliedset at a site it traces to one object or array literal in one project file. The trace goes throughdefine*calls and the.create(…)factories of@objectstack/specexports, module-levelconstbindings, relative imports and re-exports (named andexport *), andObject.values()over a namespace import, in the module namespace's sorted order. A site is written only when three things hold:node_modules.--jsonwrite.manual[]as{ conversionId, path, kind, reason }, and in the human report asnot written [kind]: reason. The kinds form a closed set:computed: the value is an expression;helper: the value is built by a call that is not adefine*helper or a spec.create;spread: a spread supplies the key or may override it;shared: a binding on the way is referenced elsewhere;outside-project: a package, a path alias, or a file outside the project;mismatch: the loaded value disagrees with the literal;injected: the loader supplied the key, such as a map-form collection'sname;unspellable: no literal spelling exists for the new value;layout: a comment or other code shares the site's line, or the edit would not parse;unattributed: no edit could be tied to the entry;entangled: a conversion's edits are written whole or not at all.todos), because the planner is not even handed them. It never writes a site a conversion declines, because a declined site produces no mechanical change.status: 'unwritten', exit 1) when a file changed on disk after it was read. After writing, it re-loads the config and re-runs the chain. Unless the re-run applies exactly the changes it left (matched bypath (conversionId)), it restores every written file to its previous bytes (status: 'restored', exit 1).Wrote N of M mechanical change(s) into K file(s):, per file and line, then the list left to apply by hand, then the re-run verdict.--jsongains awritekey only with--write:status,files,written,manual,unexplained,verification, anderrorwhen there is one.PM readings, measured (on
3d918850; re-run on the merged treeb1799bf8)os migrate meta --from 16 --out snap.jsonprintedApplied 9 mechanical change(s):with one line per site. The md5 of every source file was identical before and after the run. The only file written was the snapshot.appliedentry carries{ toMajor, conversionId, surface, from, to, path }. Itsfrom/toare display strings, not edits (striped→(removed),'previousPeriod'→{ kind: 'previousPeriod' }). Itspathpoints into the normalised stack. Its anchor differs per conversion: the renamed-to key, the removed key, or a container (path: 'api'forapi.requireAuth). So the edit is taken from a structural diff of the stack the chain started from against the stack it produced. Each change is tied back to the entries whose paths explain it, under three ordered rules: (1) the site, a key inside it, or a container around it; (2) a sibling key, which is a rename's old key; (3) a move between two containers of one subject. Writing then requires the proof above, and every refusal class met is in the list above.--write,todoslists exactly what the dry run of the same build lists. Per the seat's heads-up about migrate meta: the stack-derived relevance proof that #20620's ruling names as the only exit for a notice is unbuilt — 17.7.0 still prints all 304 protocol-18 manual notices to an app none of them applies to #22072, this is pinned relative to the dry run and not as a fixed listing.--stored --applyprompt.os i18n extract --out,os datasource introspectandos generatetake their flag as the opt-in, with no prompt. Theconfirm()+--yesprompt belongs to row rewrites, where a write mints a checksum and a history entry. It is also unreachable here:--yesis a--stored-only flag thatstoredOnlyFlagsInrefuses outside--stored, and that code is off limits to this card. The fail-closed re-run is the safety net instead.--write --storedis refused, as the PM leaned.view.list.striped,agent.knowledge,flow.active,object.tenancy.organizationField). A value is rewritten:datasource.driveris a scalar to a scalar, andcompareTo: 'previousPeriod'becomes{ kind: 'previousPeriod' }, a scalar to an object. A key is renamed (refreshInterval→refreshIntervalSeconds, value and trailing comment kept). A key is added (config: { mode: 'inclusive' }on decision nodes, on its own line and inline). The partial case:compareTo: { offset: '1y' }is written as{ kind: 'previousYear' }. The uncovered{ offset: '7d' }produces no mechanical change, so--writeleaves its bytes as they are. The schema verdict still refuses it and the semantic notice still names it, exactly as before.Evidence
examples/app-crmatb1799bf8was back-dated in 3 sites over 2 files: a viewstriped: truereached through a named-re-export barrel andObject.values(views), and two dashboardcompareTovalues in a typed plain literal.--from 16 --writewrote all 3, and the re-run verdict wasok. All 32 source files then compared byte-identical toexamples/app-crmatb1799bf8. The unmodified copy reportsapplied: 0and writes nothing.packages/cli/test/migrate-meta-write.test.ts, unit tier, in-process, temp projects that link the real@objectstack/spec). They cover:7darm untouched;--outcontrols;--stored --writerefused;scripts/ablation-replace.mjs, each restored with the blob equal to HEAD andgit diff HEADempty:Semver
@objectstack/cli: minor. A new flag is additive, andClause-②: yesrequires at leastminor.mainis not in Changesets pre mode:origin/mainatbafb58bbhas no.changeset/pre.json.check-changeset-no-majoris green.Acceptance notes
packages/spec/src/shared/retired-key.tsis not touched. It belongs to the spec lane, per the claim. Its current wording, "list the mechanical edits for existing sources; apply them by hand", stays true and only undersells--write.skills/objectstack-upgrade/SKILL.mdstill says the command "writes nothing but--out" in its troubleshooting row. That stays true for the default invocation and undersells--write. It is a Tier H governed surface, so it is not touched here. Carrier: the spec lane's sentence-restoration PR, or the skills owner.content/docs/deployment/cli.mdx. Only--storeddoes. The authored mode's flag table lives incontent/docs/upgrading.mdx, so--writewas added there, and thecli.mdxparagraph on the two modes now names it.pathsalias is refused asoutside-project;import()or arequire()is not read;Deviation, stated rather than taken silently
content/docs/upgrading.mdxis outside the claim's file surface. It said "os migrate metadoes not rewrite your source files" and called--out"the only file the command writes". This change makes both sentences false.os-dev.mdrequires fixing a published sentence the change makes false, and it wins over the dispatch on conflict. So the callout, the flags table and the loop comment were corrected, and the conflict is reported to the seat.Local verification (head
be29d66funless noted)pnpm --filter @objectstack/cli exec vitest run --project unitatb1799bf8, the merged tree: 260 files, 3815 tests passed.be29d66f: the 4 meta test files passed, 52 tests (migrate-meta-write23,meta.stored-flags,meta.report-order,migrate-meta-strict-factories).pnpm --filter @objectstack/cli typecheck: exit 0, includingcheck:test-typecheck.node scripts/pm/dispatch-gates.mjs --commands) atbe29d66f: 96 commands, all exit 0.--ranreconciliation:96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN. The PM's list pluspnpm check:docs-image-tagwas added by derivation.pnpm lint(the fulleslint . --no-inline-config) atbe29d66f: exit 0.packages/spec/dist. Its three exit-3 results and one exit-1 result were build-in-flight artefacts, NOT MEASURED. Every number above is from the clean re-run.Generated by Claude Code