Skip to content

Commit 22c1296

Browse files
dmealingclaude
andcommitted
fix(migrate): only advance the snapshot when a migration was written
Independent review of the preceding three commits found the new guard (`!dryRun && exitCode === 0 && !applyFailed`) also fires on the zero-change branch, where the live path writes nothing. Reproduced on a real sqlite engine: a database already matching its metadata but reached WITHOUT any committed migration — a restored dump, hand-run DDL, an adopted DB — turned a plain `meta migrate --from-db` drift check into an implicit baseline. It recorded the target schema as already-applied with zero CREATE TABLE anywhere, so the offline path reported `no changes` permanently and `apply-pending` provisioned an empty database on CI. That is the 0.20.1 greenfield-baseline trap re-entered through a different door. The snapshot means "the schema the committed migrations land you in", so it now advances exactly when this run wrote one — the rule runOfflineGenerate already follows, returning on `no changes` before writing. Both motivating scenarios (greenfield --from-db --apply, and --apply without --from-db) always write files, so neither regresses; re-verified end to end in a cold project. Also from the review: - The snapshot carries the engine version introspection just captured. Writing bare `expected` dropped `meta.sqliteVersion`, which `emit` reads to choose native ALTER vs recreate-and-copy; on SQLite < 3.35 a later offline migration would have been un-appliable. - A writeSnapshot failure is reported as itself rather than surfacing as "unexpected error" after the migration already applied. - The post-write hint said "apply with `meta migrate --db <url> --apply`", which re-runs the diff: it demands --slug again (exit 2) and, if given one, emits a second migration with the same DDL. Now `meta migrate apply-pending --db <url>` (#242), verified by running the hint verbatim. - Two stale `258` fixture counts survived in the files the refresh targeted (README.md, spec/roadmap.md); both are 260. - "`--dialect` is always required" was false and contradicted the CLI's own --help: it is required offline and on `baseline`, and auto-detected from the URL scheme with `--db`. Corrected in the skill, its reference and both llms files. - CHANGELOG [Unreleased] now states its registry scope (npm · PyPI · NuGet; Maven has no changed product file) and covers the agent-context fix, which ships to adopters in three of those packages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3msoGxjRMwx94PhKSLDuE
1 parent bc2e9b1 commit 22c1296

19 files changed

Lines changed: 190 additions & 45 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
**Scope: npm · PyPI · NuGet** — Maven Central has no changed product file (its
11+
`AgentDocsMojo` no longer scaffolds agent context; it redirects to the Node CLI).
12+
The migrate fix is npm-only (`@metaobjectsdev/cli`); the agent-context correction
13+
ships wherever the scaffolded skills ship — `@metaobjectsdev/sdk` on npm, the
14+
bundled content tree in the PyPI package, and the C# CLI on NuGet.
15+
1016
### Fixed — the live-DB `meta migrate` path now advances the committed snapshot
1117

1218
The day-1 command `meta init` prints as its next step —
@@ -36,10 +42,50 @@ blocked, refused, or apply-failed run leaves the snapshot untouched rather than
3642
recording a schema the database is not in. The now-obsolete
3743
`--from-db did not advance the committed snapshot` warning is gone.
3844

45+
The snapshot advances **exactly when the run wrote a migration** — the rule the
46+
offline path already follows (it returns on `no changes` before writing). A live
47+
run that emits nothing leaves it alone: recording the target schema as
48+
already-applied with no `CREATE TABLE` anywhere would be the greenfield-`baseline`
49+
trap by another door, leaving the offline path reporting `no changes` forever and
50+
`apply-pending` provisioning an empty database. The snapshot also now carries the
51+
engine version introspection just captured, which `emit` reads to choose native
52+
`ALTER` vs recreate-and-copy on older SQLite.
53+
3954
Gated by a real-engine round-trip (`cli/test/integration/migrate-fromdb-snapshot.test.ts`):
4055
greenfield `--from-db … --apply` → offline incremental emits a real migration →
41-
apply → live re-diff converges to empty, plus the `--dry-run` and blocked-change
42-
no-write assertions.
56+
apply → live re-diff converges to empty, plus no-write assertions for `--dry-run`,
57+
a blocked destructive change, and a zero-change live run against a hand-migrated
58+
database.
59+
60+
### Fixed — the post-write next-step hint named a command that fails
61+
62+
After writing a migration, `meta migrate` printed ``apply with `meta migrate --db
63+
<url> --apply` ``. That re-runs the diff, so it demands `--slug` again (exit 2) and,
64+
if given one, emits a *second* migration carrying the same DDL. The hint now names
65+
`meta migrate apply-pending --db <url>` (#242), which replays what was just written
66+
with no diff. This matters more alongside the snapshot fix above: the snapshot now
67+
advances on a files-only run, so the offline path afterwards correctly reports
68+
`no changes` and no longer doubles as an accidental reminder that something is
69+
pending.
70+
71+
### Fixed — the migrate guidance shipped to adopters as agent context
72+
73+
The `metaobjects-verify` skill that `meta init` scaffolds into every adopter repo
74+
(and its `llms.txt` mirror) told agents to run `meta migrate baseline --dialect
75+
sqlite` on a **fresh** database — the exact greenfield trap `0.20.1` hardened the
76+
CLI to refuse. An offline baseline derives the "existing" snapshot from the
77+
metadata, recording the target shape as already applied, so no table is ever
78+
created and the failure surfaces later at the API layer as `no such table`. An
79+
agent following the shipped skill walked a newcomer straight into it. Corrected to
80+
the working `--from-db … --slug init --apply` path, with `baseline --from-db` kept
81+
for its real use — adopting a database that already has its schema.
82+
83+
Same family, same files: `meta migrate --db <url> --slug <name>` was described as
84+
"diff metadata vs the live DB", but without `--from-db`/`--apply` that is the
85+
offline path and `--db` is ignored. Every example now distinguishes the offline,
86+
live (`--from-db`) and replay (`apply-pending`) forms, and states `--dialect`
87+
accurately (required offline and on `baseline`; auto-detected from the URL scheme
88+
when `--db` is given).
4389

4490
### Fixed — the documented Node server-boot command (`docs/ports/typescript.md`)
4591

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ metaobjects/
166166
├── CLAUDE.md # project instructions for Claude
167167
├── spec/ # canonical metamodel docs, ADRs, roadmap
168168
├── fixtures/ # 19 cross-language conformance corpora — the oracle
169-
│ ├── conformance/ # metamodel (loader + serializer + navigation), 258 fixtures
169+
│ ├── conformance/ # metamodel (loader + serializer + navigation), 260 fixtures
170170
│ ├── yaml-conformance/ # YAML authoring desugar
171171
│ ├── render-conformance/ # FR-004 byte-identical render oracle
172172
│ ├── verify-conformance/ # FR-004 template-drift gate

‎agent-context/skills/metaobjects-verify/SKILL.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,11 @@ What this means in practice:
155155
meta migrate --dialect postgres --slug add-user-shipping --dry-run # preview without writing
156156
```
157157

158-
`--dialect` is always required — it selects the diff pipeline, not just the SQL
159-
flavor. Do **not** run `meta migrate baseline` on a database that does not exist
160-
yet; see `references/migration.md`.
158+
Always pass `--dialect` — it selects the diff pipeline, not just the SQL flavor.
159+
It is *required* on the offline path and on `baseline`; with `--db` the CLI can
160+
auto-detect it from the URL scheme, but being explicit keeps the two paths
161+
reading the same. Do **not** run `meta migrate baseline` on a database that does
162+
not exist yet; see `references/migration.md`.
161163

162164
- Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
163165
- The JVM and Python ports have **no** migration command of their own — their

‎agent-context/skills/metaobjects-verify/references/migration.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres
8282
# diff vs the LIVE database instead
8383
```
8484

85-
`--dialect` is always required and is load-bearing — it selects the diff
86-
pipeline, not just the SQL flavor. The default path is offline (`--db` is
85+
`--dialect` is load-bearing — it selects the diff pipeline, not just the SQL
86+
flavor. It is *required* offline and on `baseline`; with `--db` it is
87+
auto-detected from the URL scheme. The default path is offline (`--db` is
8788
ignored without `--from-db` or `--apply`); pass `--from-db` when you want the
8889
diff taken against the live database.
8990

‎docs/llms/llms-full.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ $ meta verify --codegen # regen-and-diff vs committed output
272272
$ meta verify --templates # prompt {{field}} ↔ payload-VO drift
273273
$ meta verify --db postgres://... # live-DB schema drift (Node meta only)
274274

275-
# Emit migration SQL files (Node meta only, used by every port). --dialect is always
276-
# required — it selects the diff pipeline, not just the SQL flavor.
275+
# Emit migration SQL files (Node meta only, used by every port). --dialect selects the
276+
# diff pipeline, not just the SQL flavor: required offline, auto-detected from --db.
277277
$ meta migrate --from-db --db file:./dev.db --dialect sqlite --slug init --apply # first migration, brand-new DB
278278
$ meta migrate --dialect sqlite --slug add-subscriber # everyday: diff vs the committed snapshot
279279
$ meta migrate --dialect sqlite --slug add-subscriber --db file:./dev.db --apply # ...and apply it

‎docs/llms/llms.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ MetaObjects deliberately does **not** ship one universal binary. Schema operatio
6969
- `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts`, **plus** the agent context: a slim `.metaobjects/AGENTS.md` + `CLAUDE.md` (auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`) and six `metaobjects-*` Claude Code skills under `.claude/skills/` scoped to the project's stack. `meta init --refresh-docs` updates only the agent docs.
7070
- `meta gen [<entity>...]` — TS codegen from entities defined under `metaobjects/`. Supports `--dry-run` and `--watch`. Generators (`entityFile()`, `queriesFile()`, `routesFile()`, `formFile()`, `tanstackQuery()`, `tanstackGrid()`, `barrel()`) come from `@metaobjectsdev/codegen-ts/generators` (and per-framework siblings) and are wired in `metaobjects.config.ts`. Per-port codegen runs in that port's own build tool: `mvn metaobjects:generate` (Java/Kotlin), `dotnet meta gen` (C#), `metaobjects gen` (Python).
7171
- `meta verify` — drift check. `verify --codegen` (regen-and-diff vs committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), `verify --db` (live-DB schema drift, Node `meta` only). Per-port codegen verify: `mvn metaobjects:verify -Dmeta.verify.mode=codegen|templates` (Java/Kotlin), `dotnet meta verify` (C#), `metaobjects verify` (Python).
72-
- `meta migrate --dialect <d> --slug <name>` — diff metadata vs the committed schema snapshot and emit migration SQL files under `.metaobjects/migrations`; add `--db <url> --apply` to run them. `--dialect` is always required and selects the diff pipeline, not just the SQL flavor. On a brand-new database the first command is `meta migrate --from-db --db <url> --dialect <d> --slug init --apply` (diff against the empty database, emit `CREATE TABLE`, apply, record the snapshot) — **not** `meta migrate baseline`, which is for adopting a database that already has its schema. `meta migrate apply-pending --db <url> --dialect <d>` replays committed migrations with no diff (fresh DB / CI). Supports SQLite (`file:`, `libsql:`), Postgres (`postgres:`, `postgresql:`), and Cloudflare D1 (TS only). `--dry-run` prints SQL to stdout. **Schema migration is owned by the Node `meta` CLI (ADR-0015) and used by every port regardless of backend language** — it is shipped as a standalone binary, so a JVM/Python/C# project needs no per-language migrate engine. There is no Maven, .NET, or Python migrate command.
72+
- `meta migrate --dialect <d> --slug <name>` — diff metadata vs the committed schema snapshot and emit migration SQL files under `.metaobjects/migrations`; add `--db <url> --apply` to run them. `--dialect` selects the diff pipeline, not just the SQL flavor — required offline and on `baseline`, auto-detected from the URL scheme when `--db` is given. On a brand-new database the first command is `meta migrate --from-db --db <url> --dialect <d> --slug init --apply` (diff against the empty database, emit `CREATE TABLE`, apply, record the snapshot) — **not** `meta migrate baseline`, which is for adopting a database that already has its schema. `meta migrate apply-pending --db <url> --dialect <d>` replays committed migrations with no diff (fresh DB / CI). Supports SQLite (`file:`, `libsql:`), Postgres (`postgres:`, `postgresql:`), and Cloudflare D1 (TS only). `--dry-run` prints SQL to stdout. **Schema migration is owned by the Node `meta` CLI (ADR-0015) and used by every port regardless of backend language** — it is shipped as a standalone binary, so a JVM/Python/C# project needs no per-language migrate engine. There is no Maven, .NET, or Python migrate command.
7373
- `meta export [--out <file>]` — flatten loaded metadata to one canonical JSON artifact.
7474

7575
## Monorepo layout

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,11 @@ What this means in practice:
155155
meta migrate --dialect postgres --slug add-user-shipping --dry-run # preview without writing
156156
```
157157

158-
`--dialect` is always required — it selects the diff pipeline, not just the SQL
159-
flavor. Do **not** run `meta migrate baseline` on a database that does not exist
160-
yet; see `references/migration.md`.
158+
Always pass `--dialect` — it selects the diff pipeline, not just the SQL flavor.
159+
It is *required* on the offline path and on `baseline`; with `--db` the CLI can
160+
auto-detect it from the URL scheme, but being explicit keeps the two paths
161+
reading the same. Do **not** run `meta migrate baseline` on a database that does
162+
not exist yet; see `references/migration.md`.
161163

162164
- Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
163165
- The JVM and Python ports have **no** migration command of their own — their

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres
8282
# diff vs the LIVE database instead
8383
```
8484

85-
`--dialect` is always required and is load-bearing — it selects the diff
86-
pipeline, not just the SQL flavor. The default path is offline (`--db` is
85+
`--dialect` is load-bearing — it selects the diff pipeline, not just the SQL
86+
flavor. It is *required* offline and on `baseline`; with `--db` it is
87+
auto-detected from the URL scheme. The default path is offline (`--db` is
8788
ignored without `--from-db` or `--apply`); pass `--from-db` when you want the
8889
diff taken against the live database.
8990

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,11 @@ What this means in practice:
155155
meta migrate --dialect postgres --slug add-user-shipping --dry-run # preview without writing
156156
```
157157

158-
`--dialect` is always required — it selects the diff pipeline, not just the SQL
159-
flavor. Do **not** run `meta migrate baseline` on a database that does not exist
160-
yet; see `references/migration.md`.
158+
Always pass `--dialect` — it selects the diff pipeline, not just the SQL flavor.
159+
It is *required* on the offline path and on `baseline`; with `--db` the CLI can
160+
auto-detect it from the URL scheme, but being explicit keeps the two paths
161+
reading the same. Do **not** run `meta migrate baseline` on a database that does
162+
not exist yet; see `references/migration.md`.
161163

162164
- Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
163165
- The JVM and Python ports have **no** migration command of their own — their

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres
8282
# diff vs the LIVE database instead
8383
```
8484

85-
`--dialect` is always required and is load-bearing — it selects the diff
86-
pipeline, not just the SQL flavor. The default path is offline (`--db` is
85+
`--dialect` is load-bearing — it selects the diff pipeline, not just the SQL
86+
flavor. It is *required* offline and on `baseline`; with `--db` it is
87+
auto-detected from the URL scheme. The default path is offline (`--db` is
8788
ignored without `--from-db` or `--apply`); pass `--from-db` when you want the
8889
diff taken against the live database.
8990

0 commit comments

Comments
 (0)