Skip to content

fix(cli): os migrate meta can open the retired-key sources it exists to rewrite - #9530

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9418-migrate-meta-retired-key-load
Aug 18, 2026
Merged

fix(cli): os migrate meta can open the retired-key sources it exists to rewrite#9530
os-zhuang merged 1 commit into
mainfrom
claude/issue-9418-migrate-meta-retired-key-load

Conversation

@claude

@claudeclaudeBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes#9418

The defect

os migrate meta --from N 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 defineView / defineAgent / defineFlow across per-artifact modules, and every one of those define* helpers is a Schema.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 under packages/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/main before touching anything, with a defineStack config carrying agent.knowledge:

◆ Migrate · meta
→ Loading configuration…
✗ defineStack validation failed (1 issue):
✗ agents.0.knowledge: `agent.knowledge` was removed in @objectstack/spec 17.0.0 ...
Run `os migrate meta --from 16` to rewrite existing sources automatically.
EXIT=1

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 and meta.ts validates 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 17 and --from 3, --out not 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-in authoredSource mode, set by os migrate meta alone. It installs an esbuild plugin that replaces each @objectstack/spec entrypoint the config imports — the root and the subpaths the example apps author through (/ui 42 imports, /data 38, /ai, ...) — with a generated shim that re-exports the real module and wraps its define* helpers as try-real-then-authored:

exportconstdefineView=(...authored)=>{try{returnrealDefineView(...authored);}catch(error){console.warn(...);returnauthored[0];}};

Three properties keep this a declared = enforced restoration rather than a widening of what the command accepts:

  • A source that loads today loads identically. The real helper runs first, so its defaults and transforms still apply — defineForm still moves schemaId into data, defineStack still merges actions into objects. Only sources that are already refused take the new path.
  • Validation is moved after the conversion, not skipped. The command still parses the migrated stack through ObjectStackDefinitionSchema and reports schemaValid, 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.
  • Every other command still hears the tombstone.os build, os validate, os serve keep 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 --json run's stdout a single parseable document.

Why the helper still runs first, rather than a blanket identity passthrough: defineForm is a counterexample — it transforms (schemaId into data.schemaId), so identity would hand the chain a shape FormViewSchema does not accept. Running the real helper first costs nothing and keeps every existing project bit-identical.

The --stored arm — probed, and NOT affected

Per the triage note. runStored() never calls loadConfig(): it boots through createStandaloneStack, which reads the compiled artifact (dist/objectstack.json) and replays the chain over sys_metadata rows. Probed empirically rather than read — os migrate meta --stored run in the very project directory whose objectstack.config.ts carries the retired key:

Examined 0 stored metadata row(s) ... against the protocol 17.0.0 conversion chain.
ℹ No stored metadata to examine (1157ms)
EXIT=0

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.ts already 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 bare export 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 unpatched main and proves nothing.

The new fixture is shaped like a real project instead: defineStack at the root, per-artifact helpers imported from a spec subpath (@objectstack/spec/ai), retired keys authored inside them. A tolerance scoped to defineStack alone passes the first assertion and fails the last two.

Reverse-verified from the committed state, both source files restored to origin/main with the test kept:

 ❯ test/migrate-meta.e2e.test.ts (14 tests | 1 failed | 12 skipped)
× loads it, converts it, and reports the migrated stack schema-valid
Error: Command failed: ... migrate meta --from 16 --json --out ...
Tests 1 failed | 1 passed | 12 skipped

The direction is what was predicted and both halves matter: the migration pin turns red, and the os validate control stays green — the tolerance really is the codemod's alone. Restoring the fix left the working tree byte-identical to the commit (git status empty).

Verification

All at final head 1644f27c8, after the last commit.

  • pnpm --filter @objectstack/cli test132 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 passed
  • pnpm --filter @objectstack/cli typecheck — clean (script name echoed; not a zero-match filter)
  • Gate union re-derived from the actual changed paths (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 confirm baseline key set verified against 6ce8feb: no files added.
  • pnpm check:type-check-debt on 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-config clean on the three changed files. (That is eslint, not the whole Lint & Repo Gates family — 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: --out writes a JSON snapshot and objectstack.config.ts is 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.execute through defineStack vs. a bare literal) — applied: action-execute-to-target in both. No defect, so no issue.


Generated by Claude Code

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 10 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via os migrate meta (command))
  • content/docs/automation/flows.mdx(via os migrate meta (command))
  • content/docs/automation/hook-bodies.mdx(via os migrate meta (command))
  • content/docs/data-modeling/fields.mdx(via os migrate meta (command))
  • content/docs/data-modeling/objects.mdx(via os migrate meta (command))
  • content/docs/data-modeling/queries.mdx(via os migrate meta (command))
  • content/docs/deployment/cli.mdx(via os migrate meta (command))
  • content/docs/deployment/index.mdx(via os migrate meta (command))
  • content/docs/protocol/objectql/query-syntax.mdx(via os migrate meta (command))
  • content/docs/protocol/objectui/actions.mdx(via os migrate meta (command))
  • content/docs/protocol/objectui/widget-contract.mdx(via os migrate meta (command))
  • content/docs/ui/apps.mdx(via os migrate meta (command))
  • content/docs/upgrading.mdx(via os migrate meta (command))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx(via os migrate meta (command))
  • content/docs/releases/v17.mdx(via os migrate meta (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/mainpackageMentionDocs.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os migrate meta --from <N> cannot load the retired-key sources it exists to fix — validates through the post-retirement schema before converting

2 participants

@os-zhuang@claude