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 |
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