You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(metadata,codegen-ts,migrate-ts,runtime-ts): identify nodes by type, not instanceof
`x instanceof MetaSource` is only sound when the class object and the instance
come from the same physical copy of @metaobjectsdev/metadata. A globally
installed or linked meta CLI alongside a project-local dependency puts two
copies in one process and the class check goes false for a node that is a
source in every observable respect — the class-identity defect that split
ts-poet's Code objects in 0.21.6, which 400c53f fixed for source-detect only.
Across this boundary the failure is SILENT rather than loud. In codegen-ts an
entity reads as "not backed by any store", so no table, queries or routes are
emitted and meta gen still reports success. In migrate-ts it is worse: the
entity drops out of the EXPECTED schema, so migrate sees a live table with no
counterpart and proposes to DROP it, while verify reports drift against a model
that is correct.
The exposure is not "which package the code lives in" — it is who created the
node the function is looking at. Loader-internal work (the validators, parser,
subtype rules) and methods on a node are immune by construction. Anything
taking a CALLER-SUPPLIED node is exposed, and that includes metadata's own
exported helpers: resolveTableName / resolveTableSchema are called by migrate-ts
on loader nodes, and their fallthrough is not a dropped table but the
entity-name fallback — a DIFFERENT table name, which migrate emits as a rename
against a live database. That one is the sharpest edge in the set.
metadata now exports cross-realm guards (shared/node-guards.ts): isMetaRoot /
isMetaObject / isMetaField / isMetaSource / isWritableSource / isReadOnlySource.
They key on the metamodel `type` — the registry binds one class per type, so on
a single-copy tree they and instanceof answer identically — and read behaviour
through the node, failing closed when it cannot answer. All 13 exposed sites are
converted; the rule is recorded in CLAUDE.md under "Coding discipline (TS)".
Output is unchanged: regenerating the canonical Postgres schema with this change
is byte-identical (sha256) to regenerating without it.
Gated by metadata/test/node-guards.test.ts and
migrate-ts/test/expected-schema-cross-realm.test.ts, which simulate the second
copy by re-prototyping a REAL loaded node onto a clone of its own prototype —
every method still resolves, instanceof is false. Each fix was verified by
reverting it and watching the gate reproduce the original symptom.
The migrate gate needed de-blinding first: entity "Order" with @table "orders"
has a name-derived fallback of "orders", the identical string, so it passed with
the defect present. It now uses a @table that differs from the fallback plus a
non-default @Schema. Same failure shape as the case-aligned `like` corpus 0.21.6
had to de-blind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTGT5ksntpcJDZVJ5VyXHS
Copy file name to clipboardExpand all lines: CLAUDE.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -511,6 +511,7 @@ These are the load-bearing principles that have emerged through implementation.
511
511
-**String literals OK only for**: error message text, instance/entity names that are user data, and test data values that aren't metamodel-level concepts.
512
512
-**No backwards-compat hacks.**
513
513
-**No `any` escape hatches.** Use `unknown` and narrow.
514
+
- **Never `instanceof` a metadata node from another package.** Cross-package code (`codegen-ts`, `migrate-ts`, `runtime-ts`, `cli`) identifies nodes with the guards `@metaobjectsdev/metadata` exports — `isMetaRoot` / `isMetaObject` / `isMetaField` / `isMetaSource` / `isWritableSource` / `isReadOnlySource` — never `x instanceof MetaSource`. Two physical copies of the package in one process (a globally-installed or linked `meta` CLI plus a project-local dependency) give the class object and the instance different identities, so `instanceof` returns **false for a real node**. The failure is **silent**: in `codegen-ts` the entity reads as "not backed by any store" and simply emits no table/queries/routes; in `migrate-ts` it drops the table from the EXPECTED schema, so `meta migrate` proposes `DROP TABLE` against a live database. This is the same class-identity defect that split ts-poet's `Code` objects in 0.21.6. The CLI's alias map (`load-metaobjects-config.ts` `CLI_PKG_PATHS`) closes it for `meta gen`/`migrate` **only** — a consumer embedding `runGen()` or the migrate engine programmatically never runs it. Sites **inside** `metadata` are immune by construction (a package's own module graph resolves its own files) and keep using `instanceof`. Mechanism + blast radius: `metadata/src/shared/node-guards.ts`.
0 commit comments