Uh oh!
There was an error while loading. Please reload this page.
fix(cli): os migrate meta can open the retired-key sources it exists to rewrite - #9530
Conversation
…to rewrite (#9418) The codemod refused its own input class. A retired authorable key is a `retiredKey()` tombstone -- `z.never()` carrying the upgrade prescription -- so the current schema does not strip it, it REJECTS it. And a real `objectstack.config.ts` runs that schema itself: `os init` scaffolds `export default defineStack({ ... })`, larger projects spread define* helpers across per-artifact modules, and every one of them is a `Schema.parse()`. The rejection therefore fired while the config module was being EVALUATED, inside the load, before the command reached its first conversion -- so it exited 1 having rewritten nothing, printing the very prescription that sends authors there (144 occurrences across 39 files under packages/spec/src). There was no CLI-side validation step to reorder: the gate lives in the loaded module. So `loadConfig()` gains an opt-in `authoredSource` mode, set by `os migrate meta` alone, that replaces each `@objectstack/spec` entrypoint the config imports -- root and subpaths -- with a shim re-exporting the real module and wrapping its define* helpers as try-real-then-authored. A source that loads today loads identically; a source the current schema refuses reaches the chain exactly as authored, with the swallowed verdict announced on stderr. Validation is not skipped but moved after the conversion: the migrated stack is still parsed and reported through `schemaValid`. `--stored` was probed and is not affected -- it never reads the config file. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not seeCoarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop):
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9418
The defect
os migrate meta --from Nrefused its own input class. A retired authorable key is aretiredKey()tombstone —z.never()carrying the upgrade prescription — so the current schema does not strip it, it rejects it. And a realobjectstack.config.tsruns that schema itself:os initscaffoldsexport default defineStack({ ... }), larger projects spreaddefineView/defineAgent/defineFlowacross per-artifact modules, and every one of thosedefine*helpers is aSchema.parse().So the rejection fired while the config module was being evaluated, inside
bundleRequire— before the command reached its first conversion. It exited 1 having rewritten nothing, printing the very sentence that sends authors there. That sentence ships 144 times across 39 files underpackages/spec/src, so the v17 upgrade path closed a loop on itself: hit a retired key, get told to run the codemod, watch the codemod refuse because of the retired key.Reproduced on
origin/mainbefore touching anything, with adefineStackconfig carryingagent.knowledge:One correction to the card's diagnosis
The card and the dispatch both read this as "a pre-validation step on the file arm, upstream of version dispatch". There is no such step.
loadConfig()validates nothing andmeta.tsvalidates nothing before the chain — the gate lives inside the loaded module. That is why the two directions the card offered are not symmetric here: "run the conversion before validating" has nothing in CLI code to reorder, so the fix has to be the other one, a tolerant load.It also explains every control the card recorded: the same refusal at
--from 17and--from 3,--outnot implicated, normal behaviour on a clean tree — all consistent with a failure that happens before the command's own first statement.The fix
loadConfig()gains an opt-inauthoredSourcemode, set byos migrate metaalone. It installs an esbuild plugin that replaces each@objectstack/specentrypoint the config imports — the root and the subpaths the example apps author through (/ui42 imports,/data38,/ai, ...) — with a generated shim that re-exports the real module and wraps itsdefine*helpers as try-real-then-authored:Three properties keep this a
declared = enforcedrestoration rather than a widening of what the command accepts:defineFormstill movesschemaIdintodata,defineStackstill merges actions into objects. Only sources that are already refused take the new path.ObjectStackDefinitionSchemaand reportsschemaValid, so a source broken for reasons the chain cannot fix is still reported as broken — after the codemod has done the part it can. Nothing new is accepted; the final verdict still comes from the schema.os build,os validate,os servekeep the default strict load — the rejection is their upgrade channel. Pinned in both directions.The swallowed verdict is announced on stderr, never dropped, which also keeps a
--jsonrun's stdout a single parseable document.Why the helper still runs first, rather than a blanket identity passthrough:
defineFormis a counterexample — it transforms (schemaIdintodata.schemaId), so identity would hand the chain a shapeFormViewSchemadoes not accept. Running the real helper first costs nothing and keeps every existing project bit-identical.The
--storedarm — probed, and NOT affectedPer the triage note.
runStored()never callsloadConfig(): it boots throughcreateStandaloneStack, which reads the compiled artifact (dist/objectstack.json) and replays the chain oversys_metadatarows. Probed empirically rather than read —os migrate meta --storedrun in the very project directory whoseobjectstack.config.tscarries the retired key:It never opened that file. The fix is one arm, not two.
The pin, and why the shipped suite could not have caught this
packages/cli/test/migrate-meta.e2e.test.tsalready had a v17 conversion suite — and it passed on a build where the command could not open a single real project. Its fixture is a bareexport default { ... }object literal, and a bare literal is validated by nobody at load. That is exactly the test the card warned about: one that passes on unpatchedmainand proves nothing.The new fixture is shaped like a real project instead:
defineStackat the root, per-artifact helpers imported from a spec subpath (@objectstack/spec/ai), retired keys authored inside them. A tolerance scoped todefineStackalone passes the first assertion and fails the last two.Reverse-verified from the committed state, both source files restored to
origin/mainwith the test kept:The direction is what was predicted and both halves matter: the migration pin turns red, and the
os validatecontrol stays green — the tolerance really is the codemod's alone. Restoring the fix left the working tree byte-identical to the commit (git statusempty).Verification
All at final head
1644f27c8, after the last commit.pnpm --filter @objectstack/cli test— 132 files / 1418 tests passed (whole package, not just the touched file)pnpm --filter @objectstack/cli exec vitest run test/migrate-meta.e2e.test.ts— 14/14 passedpnpm --filter @objectstack/cli typecheck— clean (script name echoed; not a zero-match filter)node scripts/pm/dispatch-gates.mjs), not recalled:check:nul-bytes,check:cross-package-test-inputs,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:type-check-coverage,scripts/docs-audit/check-affected-docs.mjs— all green. Two ratchets confirmbaseline key set verified against 6ce8feb: no files added.pnpm check:type-check-debton the built closure (turbo run build, 70/70 successful first):33 ledger entr(ies) re-measured, 1926 raw tsc error(s), none above its recorded number.eslint --no-inline-configclean on the three changed files. (That is eslint, not the wholeLint & Repo Gatesfamily — the derived members of it are listed above.)Out of scope, filed separately
#9529 — the same 144 messages promise the tool rewrites your source files, and it does not:
--outwrites a JSON snapshot andobjectstack.config.tsis never touched. Pre-existing, downstream of this card (Blocked-by: #9418), and its two resolutions both touch a maintainer-ruled sentence, so it is not an implementer's call. Filed unassigned for triage.One hypothesis was falsified rather than filed:
defineStack's load-time ADR-0087 D2 pass does not pre-apply conversions the chain would otherwise attribute. Measured both ways on the same key (action.executethroughdefineStackvs. a bare literal) —applied: action-execute-to-targetin both. No defect, so no issue.Generated by Claude Code