Skip to content

fix(cli): fix large-app smoke-test defects in routes, migrate and verify - #559

Merged
dmealing merged 2 commits into
mainfrom
fm/mo-large-app-smoke
Nov 12, 2026
Merged

dmealing merged 2 commits into
mainfrom
fm/mo-large-app-smoke

Conversation

@dmealing

@dmealing dmealing commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Generated Postgres routes: ?search= skips string rules that carry a format (uuid FKs no longer 500). A malformed uuid in :id or a body FK now answers 400 invalid_id or validation error instead of 500. Zod insert/update schemas check field.uuid against an 8-4-4-4-12 hex regex; read schemas keep a bare z.string(). Owned template copies and examples are resynced and regenerated.
  • meta migrate: new view-names-renamed-column schema advisory (warning, not a block) when a recreated view's @sql names a column the same change set renames. The "recreated only because it reads an altered table" note excludes such views.
  • meta verify advisories: the anti-pattern scanner skips comment lines; money-float fires only on money-named operands; .reduce counts as a hand-rolled aggregate only when it accumulates into its first parameter with +, and not in .tsx/.jsx; all unindexed foreign keys collapse into one finding that names each key and its index.lookup; with a structured --format payload on stdout, advisory sections print a header and pointer on stderr. The requirement gate output is unchanged. Advisories never affect exit codes.
Validation

Risk Assessment

✅ Low: Each of the six fixes is narrowly scoped and matches the stated intent. I traced the search skip, uuid id and body validation, rename advisory, scanner narrowing, FK collapse and stderr pointer through the code and found no wrong-result path. Behavior changes are the ones the intent calls for, advisories never touch exit codes, and regression tests run the stock routes against Postgres.

Testing

The six fixes were driven live. Postgres uuid routes passed through the stock generated routes, and that test fails on the base commit with the exact reported Postgres errors. meta migrate printed the new renamed-column view warning and dropped the "recreated only because" note. meta verify was silent on comment lines, totalHours.toFixed(2), a max reduce and a .tsx reduce. It still flagged totalAmount.toFixed(2), price * 100 and a + reduce. Three unindexed FKs collapsed into one advisory naming every key and its index.lookup. On stderr the structured mode printed headers plus a pointer, and the exit code stayed 0. The CHANGELOG was not read; the doc changes for 5.4 and 5.5 were checked via the diff and the ci-local.sh --quick drift gates only. The complete ci-local.sh --quick run passed. Not run: the C#/Java/Python/Kotlin conformance lanes, the java-reactor, the integration-tests matrix and the cube live check (all skipped under --quick; this change is TS-only apart from regenerated agent-context fixtures and doc text).

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
5.1 Stock generated routes on real Postgres: ?search= on an entity with a uuid FK answers 200 and searches text columns ✅ pass live integration-tests/test/generated-uuid-routes-pg.test.ts (7 pass); fails on base with operator does not exist: uuid ~~* unknown; stock-uuid-routes-pg.log
5.2 Malformed uuid in :id (GET/PATCH/DELETE) and in a body FK (POST/PATCH) answers 400 invalid_id / validation instead of 500; Fastify and Hono adapters ✅ pass live generated-uuid-routes-pg.test.ts and runtime-ts/test/uuid-input-pg.test.ts against real Postgres 18; base reproduces invalid input syntax for type uuid 500s
5.2 Zod insert/update schemas accept either-case 8-4-4-4-12 uuids and reject malformed ones (not Zod .uuid()) ✅ pass live codegen-ts zod-validators-execution.test.ts (39 pass) executes the generated schemas
5.3 meta migrate --dry-run on Postgres renaming a column that a view's @SQL still names: warning names view, column and new name; recreate note excludes that view ✅ pass live migrate-rename-view.txt: warnings[1] names v_hours, time_entries.hours and duration_hours; the 'recreated only because' note is absent. The 'dropped and recreated; its grants are carried over' note is…
5.6 meta verify advisory noise: comments skipped; totalHours.toFixed(2) silent; money-named math, + reduce still flagged; max-reduce and .tsx reduce silent ✅ pass live verify-human.txt: findings only for a.ts lines 3, 4, 5
5.6 Three unindexed FKs collapse into ONE advisory naming every key and its index.lookup; structured stdout has stderr header + pointer only; exit code unchanged (0) ✅ pass live verify-human.txt (one unindexed-foreign-key row naming fkA, fkB, fkC) and verify-human.err ('(listed in the --format payload on stdout)'); exit=0
5.4/5.5 Docs: table-scoped index-name examples and the list-hook naming rule; owned template copies, examples, site payload and agent-context fixtures in sync ✅ pass live scripts/ci-local.sh --quick passed (owned template copies current, site payload is true, site reference fresh, shipped doc examples load, agent-context conformance); doc diffs reviewed
Complete regression: scripts/ci-local.sh --quick (TS + drift gates) ✅ pass live ci-local-quick.log: LOCAL CI PASSED, exit=0, 19m30s; non-TS port lanes skipped by --quick design
Evidence: Stock routes uuid-on-Postgres test run
bun test v1.3.14 (0d9b296a)

 7 pass
 0 fail
 15 expect() calls
Ran 7 tests across 1 file. [8.93s]
Evidence: meta migrate --dry-run with a renamed column named by a view (warning present, recreate note excluded)
meta: migrate: view v_hours still names time_entries.hours, which this migration renames to duration_hours. A rename never rewrites a view body, so this view is recreated as declared and the migration fails at apply; change its definition (its @sql) to duration_hours first.
meta: migrate: compared the metadata with the committed snapshot; no database was read, so a change made directly in the database is not seen here (meta migrate --from-db or meta verify --db compares with it).
meta: migrate: view public.v_hours: dropped and recreated; its grants are carried over.
meta: -- UP --
CREATE TEMP TABLE IF NOT EXISTS _mo_view_grants (view_key text, grantee text, privilege text, grantable boolean, column_name text);
INSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)
SELECT 'public.v_hours', CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(a.grantee)) END, a.privilege_type, a.is_grantable, NULL
FROM pg_class c, aclexplode(c.relacl) a
WHERE c.oid = to_regclass('"v_hours"') AND c.relacl IS NOT NULL AND a.grantee <> c.relowner;
INSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)
SELECT 'public.v_hours', CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(x.grantee)) END, x.privilege_type, x.is_grantable, t.attname
FROM pg_class c JOIN pg_attribute t ON t.attrelid = c.oid, aclexplode(t.attacl) x
WHERE c.oid = to_regclass('"v_hours"') AND t.attacl IS NOT NULL AND NOT t.attisdropped AND x.grantee <> c.relowner;
DROP VIEW IF EXISTS "v_hours";

ALTER TABLE "time_entries" RENAME COLUMN "hours" TO "duration_hours";

CREATE VIEW "v_hours" AS
SELECT id, hours FROM time_entries;
COMMENT ON VIEW "v_hours" IS 'metaobjects:v1:sha256:c2ee00932749d567c901a35b217f20362de65a104cf19684fba3b06a838cb38b';
-- grants on "v_hours" are carried over from before the drop
DO $mo_grants$
DECLARE r record; n integer := 0;
BEGIN
  IF to_regclass('pg_temp._mo_view_grants') IS NOT NULL THEN
    FOR r IN SELECT grantee, privilege, grantable, column_name FROM _mo_view_grants WHERE view_key = 'public.v_hours' LOOP
      IF r.column_name IS NULL THEN
        EXECUTE format('GRANT %s ON %s TO %s%s', r.privilege, '"v_hours"', r.grantee,
          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);
        n := n + 1;
      ELSIF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('"v_hours"')
                    AND attname = r.column_name AND attnum > 0 AND NOT attisdropped) THEN
        EXECUTE format('GRANT %s (%I) ON %s TO %s%s', r.privilege, r.column_name, '"v_hours"', r.grantee,
          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);
        n := n + 1;
      END IF;
    END LOOP;
    DELETE FROM _mo_view_grants WHERE view_key = 'public.v_hours';
  END IF;
  RAISE NOTICE 'view "v_hours": % privilege(s) carried over', n;
END
$mo_grants$;

-- DOWN --
CREATE TEMP TABLE IF NOT EXISTS _mo_view_grants (view_key text, grantee text, privilege text, grantable boolean, column_name text);
INSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)
SELECT 'public.v_hours', CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(a.grantee)) END, a.privilege_type, a.is_grantable, NULL
FROM pg_class c, aclexplode(c.relacl) a
WHERE c.oid = to_regclass('"v_hours"') AND c.relacl IS NOT NULL AND a.grantee <> c.relowner;
INSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)
SELECT 'public.v_hours', CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(x.grantee)) END, x.privilege_type, x.is_grantable, t.attname
FROM pg_class c JOIN pg_attribute t ON t.attrelid = c.oid, aclexplode(t.attacl) x
WHERE c.oid = to_regclass('"v_hours"') AND t.attacl IS NOT NULL AND NOT t.attisdropped AND x.grantee <> c.relowner;
DROP VIEW "v_hours";

ALTER TABLE "time_entries" RENAME COLUMN "duration_hours" TO "hours";

CREATE VIEW "v_hours" AS
SELECT id, hours FROM time_entries;
COMMENT ON VIEW "v_hours" IS 'metaobjects:v1:sha256:c2ee00932749d567c901a35b217f20362de65a104cf19684fba3b06a838cb38b';
-- grants on "v_hours" are carried over from before the drop
DO $mo_grants$
DECLARE r record; n integer := 0;
BEGIN
  IF to_regclass('pg_temp._mo_view_grants') IS NOT NULL THEN
    FOR r IN SELECT grantee, privilege, grantable, column_name FROM _mo_view_grants WHERE view_key = 'public.v_hours' LOOP
      IF r.column_name IS NULL THEN
        EXECUTE format('GRANT %s ON %s TO %s%s', r.privilege, '"v_hours"', r.grantee,
          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);
        n := n + 1;
      ELSIF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('"v_hours"')
                    AND attname = r.column_name AND attnum > 0 AND NOT attisdropped) THEN
        EXECUTE format('GRANT %s (%I) ON %s TO %s%s', r.privilege, r.column_name, '"v_hours"', r.grantee,
          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);
        n := n + 1;
      END IF;
    END LOOP;
    DELETE FROM _mo_view_grants WHERE view_key = 'public.v_hours';
  END IF;
  RAISE NOTICE 'view "v_hours": % privilege(s) carried over', n;
END
$mo_grants$;
changes[3]{kind,count}:
  rename-column,1
  drop-view,1
  create-view,1
written: []
summary: "1 rename-column, 1 drop-view, 1 create-view; preview only (nothing written)"
help[1]: re-run without --dry-run to write the migration
warnings[1]: "view v_hours still names time_entries.hours, which this migration renames to duration_hours. A rename never rewrites a view body, so this view is recreated as declared and the migration fails at apply; change its definition (its @sql) to duration_hours first."
notes[2]: "compared the metadata with the committed snapshot; no database was read, so a change made directly in the database is not seen here (meta migrate --from-db or meta verify --db compares with it).","view public.v_hours: dropped and recreated; its grants are carried over."
sql:
  up: "CREATE TEMP TABLE IF NOT EXISTS _mo_view_grants (view_key text, grantee text, privilege text, grantable boolean, column_name text);\nINSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)\nSELECT 'public.v_hours', CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(a.grantee)) END, a.privilege_type, a.is_grantable, NULL\nFROM pg_class c, aclexplode(c.relacl) a\nWHERE c.oid = to_regclass('\"v_hours\"') AND c.relacl IS NOT NULL AND a.grantee <> c.relowner;\nINSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)\nSELECT 'public.v_hours', CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(x.grantee)) END, x.privilege_type, x.is_grantable, t.attname\nFROM pg_class c JOIN pg_attribute t ON t.attrelid = c.oid, aclexplode(t.attacl) x\nWHERE c.oid = to_regclass('\"v_hours\"') AND t.attacl IS NOT NULL AND NOT t.attisdropped AND x.grantee <> c.relowner;\nDROP VIEW IF EXISTS \"v_hours\";\n\nALTER TABLE \"time_entries\" RENAME COLUMN \"hours\" TO \"duration_hours\";\n\nCREATE VIEW \"v_hours\" AS\nSELECT id, hours FROM time_entries;\nCOMMENT ON VIEW \"v_hours\" IS 'metaobjects:v1:sha256:c2ee00932749d567c901a35b217f20362de65a104cf19684fba3b06a838cb38b';\n-- grants on \"v_hours\" are carried over from before the drop\nDO $mo_grants$\nDECLARE r record; n integer := 0;\nBEGIN\n  IF to_regclass('pg_temp._mo_view_grants') IS NOT NULL THEN\n    FOR r IN SELECT grantee, privilege, grantable, column_name FROM _mo_view_grants WHERE view_key = 'public.v_hours' LOOP\n      IF r.column_name IS NULL THEN\n        EXECUTE format('GRANT %s ON %s TO %s%s', r.privilege, '\"v_hours\"', r.grantee,\n          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);\n        n := n + 1;\n      ELSIF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('\"v_hours\"')\n                    AND attname = r.column_name AND attnum > 0 AND NOT attisdropped) THEN\n        EXECUTE format('GRANT %s (%I) ON %s TO %s%s', r.privilege, r.column_name, '\"v_hours\"', r.grantee,\n          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);\n        n := n + 1;\n      END IF;\n    END LOOP;\n    DELETE FROM _mo_view_grants WHERE view_key = 'public.v_hours';\n  END IF;\n  RAISE NOTICE 'view \"v_hours\": % privilege(s) carried over', n;\nEND\n$mo_grants$;"
  down: "CREATE TEMP TABLE IF NOT EXISTS _mo_view_grants (view_key text, grantee text, privilege text, grantable boolean, column_name text);\nINSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)\nSELECT 'public.v_hours', CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(a.grantee)) END, a.privilege_type, a.is_grantable, NULL\nFROM pg_class c, aclexplode(c.relacl) a\nWHERE c.oid = to_regclass('\"v_hours\"') AND c.relacl IS NOT NULL AND a.grantee <> c.relowner;\nINSERT INTO _mo_view_grants (view_key, grantee, privilege, grantable, column_name)\nSELECT 'public.v_hours', CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(x.grantee)) END, x.privilege_type, x.is_grantable, t.attname\nFROM pg_class c JOIN pg_attribute t ON t.attrelid = c.oid, aclexplode(t.attacl) x\nWHERE c.oid = to_regclass('\"v_hours\"') AND t.attacl IS NOT NULL AND NOT t.attisdropped AND x.grantee <> c.relowner;\nDROP VIEW \"v_hours\";\n\nALTER TABLE \"time_entries\" RENAME COLUMN \"duration_hours\" TO \"hours\";\n\nCREATE VIEW \"v_hours\" AS\nSELECT id, hours FROM time_entries;\nCOMMENT ON VIEW \"v_hours\" IS 'metaobjects:v1:sha256:c2ee00932749d567c901a35b217f20362de65a104cf19684fba3b06a838cb38b';\n-- grants on \"v_hours\" are carried over from before the drop\nDO $mo_grants$\nDECLARE r record; n integer := 0;\nBEGIN\n  IF to_regclass('pg_temp._mo_view_grants') IS NOT NULL THEN\n    FOR r IN SELECT grantee, privilege, grantable, column_name FROM _mo_view_grants WHERE view_key = 'public.v_hours' LOOP\n      IF r.column_name IS NULL THEN\n        EXECUTE format('GRANT %s ON %s TO %s%s', r.privilege, '\"v_hours\"', r.grantee,\n          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);\n        n := n + 1;\n      ELSIF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('\"v_hours\"')\n                    AND attname = r.column_name AND attnum > 0 AND NOT attisdropped) THEN\n        EXECUTE format('GRANT %s (%I) ON %s TO %s%s', r.privilege, r.column_name, '\"v_hours\"', r.grantee,\n          CASE WHEN r.grantable THEN ' WITH GRANT OPTION' ELSE '' END);\n        n := n + 1;\n      END IF;\n    END LOOP;\n    DELETE FROM _mo_view_grants WHERE view_key = 'public.v_hours';\n  END IF;\n  RAISE NOTICE 'view \"v_hours\": % privilege(s) carried over', n;\nEND\n$mo_grants$;"
Evidence: meta verify structured stdout (one FK finding, comment/toFixed/max-reduce/tsx silent)
verify[6]{gate,ran,ok}:
  templates,true,true
  schema,false,true
  codegen,false,true
  deps,false,true
  requirements,true,true
  replay,false,true
exitCode: 0
summary: "2 gate(s) ran, all clean; 4 advisory finding(s)"
help[2]: "3 authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run `meta types <construct>`","foreign key(s) with no covering index — see antiPatterns.rows[] (rule \"unindexed-foreign-key\"), one row naming each key and the index.lookup to declare"
antiPatterns:
  status: ran
  total: 4
  rows[4]{file,line,rule,construct,message}:
    src/a.ts,3,money-float,field.currency,"src/a.ts:3 — money handled as a float / hand-rolled minor units — MetaObjects has field.currency (integer minor units stored, formatted client-side). Run `meta types field.currency`."
    src/a.ts,4,money-float,field.currency,"src/a.ts:4 — money handled as a float / hand-rolled minor units — MetaObjects has field.currency (integer minor units stored, formatted client-side). Run `meta types field.currency`."
    src/a.ts,5,hand-rolled-aggregate,origin.aggregate,"src/a.ts:5 — you're computing an aggregate by hand — MetaObjects derives it: for grouped, served or reported figures declare an object.report (dimensions + measures, a read-only list route); an object.projection with an origin.aggregate child still fits a per-row figure on one entity read through its generated query. Run `meta types origin.aggregate`."
    meta.fk.json,0,unindexed-foreign-key,"acme::Doc.fkA, acme::Doc.fkB, acme::Doc.fkC","3 foreign key(s) have no index covering their columns, so joins from the referenced table and its ON DELETE checks scan the referencing one. To index them, declare an index.lookup on each: Doc.fkA { name: docByAId, fields: aId }; Doc.fkB { name: docByBId, fields: bId }; Doc.fkC { name: docByCId, fields: cId }"
requirements:
  status: ran
  total: 0
  rows: []
overlays:
  status: ran
  total: 0
  rows: []
names:
  status: ran
  total: 0
  rows: []
deprecations:
  status: ran
  total: 0
  rows: []
fields:
  status: ran
  total: 0
  rows: []
notRepresented[2]: "per-gate drift detail for every gate but schema (which template variable drifted, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict, and the schema gate's differences in schemaDrift",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
Evidence: meta verify stderr (headers plus pointer)
meta: meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta: meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta: meta verify — no template.* nodes found; the template gate had nothing to check.
meta: meta verify — 3 place(s) hand-roll what MetaObjects can model (advisory — does not fail the build):
meta:   (listed in the --format payload on stdout)
meta: meta verify — 3 foreign key(s) with no index on the referencing column(s) (advisory — does not fail the build):
meta:   (listed in the --format payload on stdout)
meta: meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it
Evidence: scripts/ci-local.sh --quick full log (exit 0)
metaobjects local CI  (mode: quick — TS + drift gates only)

── ▶ leak-scan (security) ──────────────────────────────────────────────
leak-scan: clean
── ✓ leak-scan (security) (0s)

── ▶ pom-version parity ──────────────────────────────────────────────
check-pom-versions: ✓ 3 parent + 1 property pom(s) match reactor 8.3.0
── ✓ pom-version parity (0s)

── ▶ bun-version parity ──────────────────────────────────────────────
check-bun-version: bun 1.3.14 matches the workflow pin.
── ✓ bun-version parity (0s)

── ▶ uv.lock version parity ──────────────────────────────────────────────
check-uv-lock-version: uv.lock agrees with pyproject.toml (1.3.0)
── ✓ uv.lock version parity (0s)

── ▶ publish-intent parity ──────────────────────────────────────────────
publish-intent parity: OK (lockstep 1.3.0; 2 declared source-only)
── ✓ publish-intent parity (0s)

── ▶ publish-set parity ──────────────────────────────────────────────
   1. @metaobjectsdev/metadata  (server/typescript/packages/metadata)
   2. @metaobjectsdev/render  (server/typescript/packages/render)
   3. @metaobjectsdev/docs-site  (server/typescript/packages/docs-site)
   4. @metaobjectsdev/migrate-ts  (server/typescript/packages/migrate-ts)
   5. @metaobjectsdev/codegen-ts  (server/typescript/packages/codegen-ts)
   6. @metaobjectsdev/runtime-ts  (server/typescript/packages/runtime-ts)
   7. @metaobjectsdev/sdk  (server/typescript/packages/sdk)
   8. @metaobjectsdev/runtime-web  (client/web/packages/runtime-web)
   9. @metaobjectsdev/codegen-ts-react  (server/typescript/packages/codegen-ts-react)
  10. @metaobjectsdev/codegen-ts-tanstack  (server/typescript/packages/codegen-ts-tanstack)
  11. @metaobjectsdev/react  (client/web/packages/react)
  12. @metaobjectsdev/tanstack  (client/web/packages/tanstack)
  13. @metaobjectsdev/cli  (server/typescript/packages/cli)
  14. @metaobjectsdev/ai-runtime  (server/typescript/packages/ai-runtime)
publish set: OK (lockstep 1.3.0; 14 packages, tier-ordered, closed over sibling deps)
ok:   happy path: private + off-lockstep excluded, order [metadata, render, docs-site, cli]
ok:   untiered member: threw on "publish order undeclared"
ok:   unclosed set: threw on "not closed over its own runtime dependencies"
ok:   inverted tier order: threw on "publishes a dependency AFTER its dependent"
ok:   regression: docs-site (#2) publishes before cli (#12) in the real tree
ok:   regression: docs-site has a declared tier

publish-set tests: all passed
── ✓ publish-set parity (0s)

── ▶ no committed pre-release version ──────────────────────────────────────────────
check-no-prerelease-versions: ✓ no pre-release version in any version declaration
── ✓ no committed pre-release version (0s)

── ▶ script-name hook collisions ──────────────────────────────────────────────
script-name-hooks: ✓ 10 root scripts, no lifecycle-hook collisions
── ✓ script-name hook collisions (0s)

── ▶ ci lane selection ──────────────────────────────────────────────
ok:   [server/typescript/packages/cli/src/x.ts] -> 'ts'
ok:   [client/web/packages/react/src/y.tsx] -> 'ts'
ok:   [server/java/metadata/src/main/java/A.java] -> 'java'
ok:   [server/java/codegen-kotlin/src/main/kotlin/B.kt] -> 'java'
ok:   [server/python/src/metaobjects/loader.py] -> 'python'
ok:   [server/csharp/MetaObjects/Loader.cs] -> 'csharp'
ok:   [fixtures/conformance/foo/meta.json] -> 'ts java python csharp'
ok:   [spec/metamodel.md] -> 'ts java python csharp'
ok:   [scripts/ci-local.sh] -> 'ts java python csharp'
ok:   [.github/workflows/local-ci.yml] -> 'ts java python csharp'
ok:   [agent-context/skills/x.md] -> 'ts java python csharp'
ok:   [weird-new-toplevel/file.txt] -> 'ts java python csharp'
ok:   [docs/features/cli.md] -> ''
ok:   [README.md CHANGELOG.md] -> ''
ok:   [server/typescript/a.ts server/python/b.py] -> 'ts python'
ok:   [docs/x.md fixtures/y.json] -> 'ts java python csharp'
ALL PASS
ok:   union all -> 'ts java python csharp'
ok:   union reorders to canonical -> 'ts java python csharp'
ok:   union deduplicates -> 'ts java'
ok:   union single -> 'python'
ok:   union empty -> ''
ok:   union drops unknown tokens -> 'ts'
ok:   every lane green -> nothing to re-run -> ''
ok:   docs-only run skips a red java -> java still not green -> 'java'
ok:   one red ts lane selects the whole ts port -> 'ts'
ok:   skipped is walked past to the real verdict -> ''
ok:   cancelled is not green -> 'csharp'
ok:   a lane with no verdict in the window is not green -> 'python'
ok:   no history at all -> everything -> 'ts java python csharp'
ok:   every port-testing job in .github/workflows/local-ci.yml is mapped by lane_port -> ''
ok:   every lane in lane_port exists as a job in .github/workflows/local-ci.yml -> ''
all checks passed
── ✓ ci lane selection (0s)

── ▶ extract field kinds ──────────────────────────────────────────────
extract FieldKind: 4 ports agree on [STRING, INT, LONG, DOUBLE, BOOLEAN, ENUM, OBJECT] (1 sanctioned deviation(s): csharp)
── ✓ extract field kinds (0s)

── ▶ no-magic gate wired (5 ports) ──────────────────────────────────────────────
no-magic gate coverage: 5 ports wired; 13 shapes tracked, 0 known gaps
── ✓ no-magic gate wired (5 ports) (0s)

── ▶ test-file references resolve ──────────────────────────────────────────────
test-file references: 34 named, all resolve
── ✓ test-file references resolve (1s)

── ▶ metamodel-version bump ──────────────────────────────────────────────
  metamodel-version: additive change since v1.2.0; declared 1.3 (1.2 → 1.3) — ok.
ok:   no change classifies as nothing
ok:   a removed subtype is BREAKING
ok:   an added subtype is ADDITIVE
ok:   a removed attr is BREAKING
ok:   an added OPTIONAL attr is ADDITIVE
ok:   an added REQUIRED attr is BREAKING
ok:   optional → required is BREAKING
ok:   required → optional is ADDITIVE
ok:   a changed valueType is BREAKING
ok:   a changed isArray is BREAKING
ok:   an enum member REMOVED is BREAKING
ok:   an enum member ADDED is ADDITIVE
ok:   an OPEN attr becoming a closed enum is BREAKING
ok:   a closed enum OPENING is ADDITIVE
ok:   a removed child rule is BREAKING
ok:   an added OPTIONAL child rule is ADDITIVE
ok:   an added MANDATORY child rule is BREAKING
ok:   raising min is BREAKING
ok:   lowering min is ADDITIVE
ok:   capping an unbounded max is BREAKING
ok:   lifting a cap is ADDITIVE
ok:   lowering a finite max is BREAKING
ok:   a removed commonAttr is BREAKING
ok:   a changed default subtype is BREAKING (it changes what an unqualified declaration MEANS)
ok:   a removed default subtype is BREAKING
ok:   an added default subtype is ADDITIVE
ok:   a type description change is PROSE ONLY
ok:   a `rules` change is PROSE ONLY — this is exactly #210, and why prose only warns
ok:   an attr description change is PROSE ONLY
ok:   no change requires no bump
ok:   additive requires a minor

... [489072 bytes truncated] ...

       if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:295:11
-         if (parsed !== undefined) {
+         if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (true) resolveNode(ownerNode);

[Survived] LogicalOperator
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined || ownerNode !== "over-drop") resolveNode(ownerNode);

[Survived] ConditionalExpression
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (true && ownerNode !== "over-drop") resolveNode(ownerNode);

[Survived] ConditionalExpression
src/super-resolve.ts:297:40
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined && true) resolveNode(ownerNode);

[Survived] StringLiteral
src/super-resolve.ts:297:54
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined && ownerNode !== "") resolveNode(ownerNode);

[Survived] ObjectLiteral
src/super-resolve.ts:332:19
-             target: { type: target.type, subType: target.subType },
+             target: {},

[Survived] ObjectLiteral
src/super-resolve.ts:333:21
-             referrer: { type: node.type, subType: node.subType },
+             referrer: {},

[Survived] StringLiteral
src/super-resolve.ts:351:15
-           kind: "unresolved",
+           kind: "",

Ran 0.98 tests per mutant on average.
--------------------|------------------|----------|-----------|------------|----------|----------|
                    | % Mutation score |          |           |            |          |          |
File                |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
All files           |  55.74 |   55.74 |      953 |        32 |        782 |        0 |        0 |
 parser-core.ts     |  47.37 |   47.37 |      476 |        19 |        550 |        0 |        0 |
 provider.ts        |  62.12 |   62.12 |       40 |         1 |         25 |        0 |        0 |
 serializer-json.ts |  64.49 |   64.49 |      199 |         8 |        114 |        0 |        0 |
 subtype-rules.ts   |  69.23 |   69.23 |      117 |         0 |         52 |        0 |        0 |
 super-resolve.ts   |  75.30 |   75.30 |      121 |         4 |         41 |        0 |        0 |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
03:44:35 (1016337) INFO MutationTestReportHelper Final mutation score of 55.74 is greater than or equal to break threshold 35
03:44:36 (1016337) INFO HtmlReporter Your report can be found at: file://{run-worktree}/server/typescript/packages/metadata/reports/mutation/mutation.html
03:44:36 (1016337) INFO MutationTestExecutor Done in 13 minutes and 12 seconds.
── ✓ completeness-gate (mutation) (13m13s)

── ▶ doc-template drift ──────────────────────────────────────────────
synced: {run-worktree}/templates/docs/entity-page.md.mustache -> {run-worktree}/server/typescript/packages/codegen-ts/templates/docs/entity-page.md.mustache
synced: {run-worktree}/templates/docs/template-page.md.mustache -> {run-worktree}/server/typescript/packages/codegen-ts/templates/docs/template-page.md.mustache
synced: {run-worktree}/templates/api/agent-api.md.mustache -> {run-worktree}/server/typescript/packages/codegen-ts/templates/api/agent-api.md.mustache
synced: {run-worktree}/templates/api/entity-api.md.mustache -> {run-worktree}/server/typescript/packages/codegen-ts/templates/api/entity-api.md.mustache
synced: {run-worktree}/templates/api/index.md.mustache -> {run-worktree}/server/typescript/packages/codegen-ts/templates/api/index.md.mustache
generated {run-worktree}/server/typescript/packages/codegen-ts/src/render-engine/embedded-templates.generated.ts (5 template(s): api/agent-api.md, api/entity-api.md, api/index.md, docs/entity-page.md, docs/template-page.md)
doc templates synced from canonical root: {run-worktree}/templates (docs api)
── ✓ doc-template drift (0s)

── ▶ embedded-library drift ──────────────────────────────────────────────
wrote 6 embedded library file(s) + 2 manifest(s) to server/typescript/packages/metadata/src/library/embedded-library.generated.ts, server/java/metadata/src/main/java/com/metaobjects/library/EmbeddedLibrary.java and server/csharp/MetaObjects/Library/EmbeddedLibrary.cs
── ✓ embedded-library drift (0s)

── ⊘ --quick: SKIPPING the other-language ports (csharp/java/python/kotlin),
        the full Java reactor, the docker integration suite and the cube live
        check. Run the full `scripts/ci-local.sh` (no flag) before opening/merging
        a PR or tagging.

══════════════════════ SUMMARY ══════════════════════
  ✓ leak-scan (security)
  ✓ pom-version parity
  ✓ bun-version parity
  ✓ uv.lock version parity
  ✓ publish-intent parity
  ✓ publish-set parity
  ✓ no committed pre-release version
  ✓ script-name hook collisions
  ✓ ci lane selection
  ✓ extract field kinds
  ✓ no-magic gate wired (5 ports)
  ✓ test-file references resolve  (1s)
  ✓ metamodel-version bump
  ✓ positioning claims
  ✓ hosted assess prompt builds
  ✓ migration guides vs registry
  ✓ dependencies installed
  ✓ peer-range bounds
  ✓ owned template copies current
  ✓ reference templates lint
  ✓ shipped doc examples load  (2s)
  ✓ no retired $apiPrefix
  ✓ site payload is true  (30s)
  ✓ site reference is fresh  (1s)
  ✓ release tag gate  (13s)
  ✓ scripts/ typecheck  (5s)
  ✓ requirements ledger verifies  (3s)
  ✓ requirements cover vocabulary  (1s)
  ✓ metamodel scaffolder
  ✓ requirement harness
  ✓ fixture-lint  (1s)
  ✓ ts build + typecheck  (25s)
  ✓ conformance: typescript  (4m30s)
  ✓ ts unit suites  (25s)
  ✓ completeness-gate (mutation)  (13m13s)
  ✓ doc-template drift
  ✓ embedded-library drift
  ⊘ csharp/java/python/kotlin conformance (--quick)
  ⊘ java-reactor (--quick)
  ⊘ integration-tests (--quick)
  ⊘ cube-model live check (--quick)
── slowest steps ──
    13m13s  completeness-gate (mutation)
     4m30s  conformance: typescript
       30s  site payload is true
       25s  ts build + typecheck
       25s  ts unit suites
       13s  release tag gate
        5s  scripts/ typecheck
        3s  requirements ledger verifies
        2s  shipped doc examples load
        1s  fixture-lint
        1s  requirements cover vocabulary
        1s  site reference is fresh
  total wall time: 19m30s
══════════════════════════════════════════════════════
LOCAL CI PASSED (with 4 skipped — see ⊘ above).
exit=0

Validated in one no-mistakes run.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
5.1 Stock generated routes on real Postgres: ?search= on an entity with a uuid FK answers 200 and searches text columns ✅ pass live integration-tests/test/generated-uuid-routes-pg.test.ts (7 pass); fails on base with operator does not exist: uuid ~~* unknown; stock-uuid-routes-pg.log
5.2 Malformed uuid in :id (GET/PATCH/DELETE) and in a body FK (POST/PATCH) answers 400 invalid_id / validation instead of 500; Fastify and Hono adapters ✅ pass live generated-uuid-routes-pg.test.ts and runtime-ts/test/uuid-input-pg.test.ts against real Postgres 18; base reproduces invalid input syntax for type uuid 500s
5.2 Zod insert/update schemas accept either-case 8-4-4-4-12 uuids and reject malformed ones (not Zod .uuid()) ✅ pass live codegen-ts zod-validators-execution.test.ts (39 pass) executes the generated schemas
5.3 meta migrate --dry-run on Postgres renaming a column that a view's @SQL still names: warning names view, column and new name; recreate note excludes that view ✅ pass live migrate-rename-view.txt: warnings[1] names v_hours, time_entries.hours and duration_hours; the 'recreated only because' note is absent. The 'dropped and recreated; its grants are carried over' note is…
5.6 meta verify advisory noise: comments skipped; totalHours.toFixed(2) silent; money-named math, + reduce still flagged; max-reduce and .tsx reduce silent ✅ pass live verify-human.txt: findings only for a.ts lines 3, 4, 5
5.6 Three unindexed FKs collapse into ONE advisory naming every key and its index.lookup; structured stdout has stderr header + pointer only; exit code unchanged (0) ✅ pass live verify-human.txt (one unindexed-foreign-key row naming fkA, fkB, fkC) and verify-human.err ('(listed in the --format payload on stdout)'); exit=0
5.4/5.5 Docs: table-scoped index-name examples and the list-hook naming rule; owned template copies, examples, site payload and agent-context fixtures in sync ✅ pass live scripts/ci-local.sh --quick passed (owned template copies current, site payload is true, site reference fresh, shipped doc examples load, agent-context conformance); doc diffs reviewed
Complete regression: scripts/ci-local.sh --quick (TS + drift gates) ✅ pass live ci-local-quick.log: LOCAL CI PASSED, exit=0, 19m30s; non-TS port lanes skipped by --quick design
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test test/generated-uuid-routes-pg.test.ts in integration-tests (stock routesFile() output on Postgres 18, uuid keys + uuid FK): 7 pass. Rerun with runtime-ts/codegen-ts/test-generators src reverted to the base commit: search and :id cases fail with operator does not exist: uuid ~~* unknown / invalid input syntax for type uuid; restored afterwards, worktree clean
  • METAOBJECTS_TEST_PG_URL=... bun test test/uuid-input-pg.test.ts test/drizzle-fastify/filter-parser.test.ts in runtime-ts (Hono + Fastify adapters, real Postgres): 45 pass
  • bun test for codegen-ts zod-validators-execution (39 pass), migrate-ts view-names-renamed-column (3 pass), cli anti-patterns, verify-fk-index-lint, view-recreate-note-renamed-column, advisory-structured-output (59 pass)
  • Live CLI: temp project on a real Postgres, meta migrate --apply created time_entries and v_hours, then the column was renamed with meta migrate --dry-run --rename-column time_entries.hours=duration_hours, with the view @sql still naming hours
  • Live CLI: temp project with meta verify (default text and structured stdout/stderr) over comment lines, totalHours.toFixed(2), totalAmount.toFixed(2), price * 100, a + reduce, a max reduce, a .tsx reduce, and three unindexed FKs
  • scripts/ci-local.sh --quick: PASSED, exit 0, 19m30s (hygiene gates, owned template copies current, site payload true, requirements ledger, shipped doc examples, TS conformance and agent-context fixtures)
  • Baseline already ran: scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ter advisories

Building one large app on the generated stack turned up six defects:

- Generated routes on Postgres answered 500 for ordinary bad uuid input.
  `?search=` ILIKEd a uuid FK (no `uuid ~~* text` operator); a malformed uuid
  `:id` reached Postgres as a literal. The search now skips formatted string
  rules, `coerceIdForColumn` rejects a malformed uuid (400 invalid_id), and
  the generated Zod schemas check a field.uuid's shape (400 validation).
  A new integration test runs the stock `routesFile()` output against
  Postgres with uuid keys and a uuid FK.
- `meta migrate` now warns when a view still names a column the migration
  renames, since the recreate then fails at apply.
- The index-name examples are table-scoped and say names are schema-wide;
  the list-hook name rule is stated correctly everywhere.
- `meta verify` advisories: comments are skipped, money-float needs a
  money-named operand, UI `.reduce` sums are not aggregates, unindexed FKs
  are one finding, and a structured run does not repeat findings on stderr.
@dmealing
dmealing merged commit 27ebe6b into main Nov 12, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-large-app-smoke branch November 12, 2026 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant