Skip to content

feat(metadata): add field.duration, validator.exactlyOne and schema conventions - #535

Merged
dmealing merged 13 commits into
mainfrom
fm/mo-schema-conventions-land
Nov 5, 2026
Merged

dmealing merged 13 commits into
mainfrom
fm/mo-schema-conventions-land

Conversation

@dmealing

@dmealing dmealing commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

What Changed

Validation

Risk Assessment

✅ Low: The only change since the last review is a fixture-count correction (448 to 449) in AGENTS.md, docs/CONFORMANCE.md and the showcase payload, which matches the 449 fixture directories on disk. The earlier fix-round changes I re-read are correct: the FK refit now maps live FK names through the same-migration renames, and the duration-literal check uses an equivalent end-anchored, ASCII-digit pattern in TS, C#, Java and Python. The branch is about 490 files and I did not read most of the other changed files this round, so this rests on the fix-round code and the earlier rounds' review, not a full re-read.

Testing

Eight scenarios ran against the real products: the TS meta CLI, the C# CLI and a disposable Postgres container. These covered the flattened-column collision refusal, a valid prefix migration, non-ISO duration literal refusal, exactlyOne and atLeastOne @max, wire.durations, and the primary-key rename. All passed. The migrate-ts integration suite also passed (259 tests) against the scratch Postgres. The full ci-local.sh --strict-toolchains run went green on all lanes except ts-integ: there, one Miniflare D1 test (report-export-too-large, not touched by this change) failed with a worker-thread IPC assertion while six test processes ran in parallel. Rerunning that file alone twice gave 84 pass, 0 fail, so I treat it as load flakiness. Nothing UI-facing, so no screenshots. I stopped the scratch container and removed build output. Evidence is in the evidence directory.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
CR-1: two flattened field.objects with empty prefix are refused at load with ERR_COLUMN_NAME_COLLISION ✅ pass live meta verify in p1: ERR_COLUMN_NAME_COLLISION: fields "billTo.street" and "shipTo.street" ... both resolve to physical column "street"
Distinct flattened prefixes migrate to a valid CREATE TABLE with no duplicate column ✅ pass live p2 meta migrate --from-db --dialect sqlite --apply: columns bill_street, bill_city, ship_street, ship_city
Non-ISO duration literal in @condition or @equals is refused at load (ERR_BAD_ATTR_VALUE) ✅ pass live meta verify on error-validator-condition-duration-literal: '"1 hour", which is not an ISO-8601 duration'
validator.exactlyOne: one-field is refused; valid form emits a CHECK that SQLite enforces on both-set and none-set rows ✅ pass live verify refusal message; INSERT with both set and none set REFUSED by CK_StoreHours_ExactlyOneLocation, exactly one OK
CR-16: @max on validator.atLeastOne is refused with ERR_UNKNOWN_ATTR ✅ pass live pmax meta verify: Unknown attribute '@max' on validator.atLeastOne 'anyTime'
CR-2: wire.durations accepts native and iso, rejects other values ✅ pass live meta verify native and iso clean; bogus: 'wire.durations must be one of "native", "iso"'
CR-2: C# codegen follows wire.durations (native = plain TimeSpan, iso = [DurationWire.Iso]) ✅ pass live diff of pcs/out_native and pcs/out_iso Timer.g.cs
CR-3: changing the primary-key naming convention on live Postgres renames the key in place (RENAME CONSTRAINT), applies cleanly, and leaves no drift ✅ pass live up.sql: ALTER TABLE "orders" RENAME CONSTRAINT "orders_pkey" TO "pk_orders"; pg_constraint shows pk_orders, pk_order_lines; verify --db clean; follow-up migrate: 'no schema changes'
migrate-ts Postgres integration suite (FK refit, constraint-backed index, CHECK round trip, duration round trip) passes on a real Postgres ✅ pass live MIGRATE_TS_PG_URL bun test test/integration: 259 pass, 0 fail
Full regression: ci-local.sh full parity ✅ pass live ci-local-full.log: all lanes green except ts-integ; its sole failure (Miniflare D1 report-export-too-large, an unrelated worker IPC assertion) passed 84/84 twice when rerun alone
Evidence: full ci-local run log
metaobjects local CI  (mode: full — all ports + reactor + docker, 8 lanes at once)

── prelude (serial): install, drift gates, workspace build ──

── ▶ dependencies installed ──────────────────────────────────────────────
bun install v1.3.14 (0d9b296a)

Checked 561 installs across 590 packages (no changes) [27.00ms]
── ✓ dependencies installed (0s)

── ▶ doc-template drift ──────────────────────────────────────────────
(doc templates synced from the canonical templates/ root into codegen-ts: 5 templates, embedded-templates.generated.ts regenerated; local worktree paths omitted)
── ✓ 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)

── ▶ ts build ──────────────────────────────────────────────
bun install v1.3.14 (0d9b296a)

Checked 561 installs across 590 packages (no changes) [8.00ms]
@metaobjectsdev/forge build: Exited with code 0
@metaobjectsdev/render build: Exited with code 0
@metaobjectsdev/metadata build: Exited with code 0
@metaobjectsdev/sdk build: bundled agent-context (into the package's agent-context directory)
@metaobjectsdev/conformance build: Exited with code 0
@metaobjectsdev/runtime-web build: Exited with code 0
@metaobjectsdev/docs-site build: Exited with code 0
@metaobjectsdev/sdk build: Exited with code 0
@metaobjectsdev/angular build: Exited with code 0
@metaobjectsdev/tanstack build: Exited with code 0
@metaobjectsdev/react build: Exited with code 0
@metaobjectsdev/migrate-ts build: Exited with code 0
@metaobjectsdev/runtime-ts build: Exited with code 0
@metaobjectsdev/ai-runtime build: Exited with code 0
@metaobjectsdev/codegen-ts build: Exited with code 0
@metaobjectsdev/codegen-ts-react build: Exited with code 0
@metaobjectsdev/codegen-ts-angular build: Exited with code 0
@metaobjectsdev/codegen-ts-tanstack build: Exited with code 0
@metaobjectsdev/test-generators build: Exited with code 0
@metaobjectsdev/cli build: Exited with code 0
── ✓ ts build (6s)
  ▸ started Postgres sidecar 'metaobjects-ci-sidecar-erDRFt'
    METAOBJECTS_TEST_PG_URL -> 127.0.0.1:55702

── lanes (up to 8 at once; each lane's log prints when it finishes) ──
  ▸ lane java started
  ▸ lane python started
  ▸ lane mutation started
  ▸ lane csharp started
  ▸ lane ts-integ started
  ▸ lane ts started
  ▸ lane cube started
  ▸ lane gates started

╔═════════════ lane: gates ═════════════

── ▶ 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.2.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.2.0)
── ✓ uv.lock version parity (0s)

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

── ▶ 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.2.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 

... [5193534 bytes truncated] ...

src/super-resolve.ts:173:9
-       if (referrerScope === undefined) return undefined;
+       if (false) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:175:9
-       if (parsed === undefined) return undefined; // degenerate (empty segment)
+       if (false) return undefined; // degenerate (empty segment)

[Survived] ConditionalExpression
src/super-resolve.ts:177:9
-       if (current === undefined) return undefined;
+       if (false) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:195:9
-       if (found !== undefined) return found;
+       if (true) return found;

[Survived] ArrayDeclaration
src/super-resolve.ts:264:31
-     const pending: MetaData[] = [];
+     const pending: MetaData[] = ["Stryker was here"];

[Survived] ConditionalExpression
src/super-resolve.ts:266:9
-       if (n.superRef !== undefined) pending.push(n);
+       if (true) pending.push(n);

[Survived] ConditionalExpression
src/super-resolve.ts:278:11
-         if (e instanceof RefOverDropError) return "over-drop";
+         if (true) return "over-drop";

[Survived] ConditionalExpression
src/super-resolve.ts:284:40
-       if (node.superRef === undefined || node.superResolved !== undefined) return;
+       if (node.superRef === undefined || false) return;

[Survived] ConditionalExpression
src/super-resolve.ts:285:9
-       if (attempted.has(node)) return; // already resolved-or-failed this pass — never re-report
+       if (false) return; // already resolved-or-failed this pass — never re-report

[Survived] StringLiteral
src/super-resolve.ts:287:69
-       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "";
+       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "Stryker was here!";

[Survived] ConditionalExpression
src/super-resolve.ts:293:9
-       if (isChildTargetingRef(node.superRef)) {
+       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] ConditionalExpression
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (true && ownerNode !== "over-drop") 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: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 1.00 tests per mutant on average.
--------------------|------------------|----------|-----------|------------|----------|----------|
                    | % Mutation score |          |           |            |          |          |
File                |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
All files           |  54.95 |   54.95 |      966 |         5 |        796 |        0 |        0 |
 parser-core.ts     |  46.03 |   46.03 |      481 |         0 |        564 |        0 |        0 |
 provider.ts        |  62.12 |   62.12 |       40 |         1 |         25 |        0 |        0 |
 serializer-json.ts |  64.49 |   64.49 |      207 |         0 |        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 |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
^[[32m16:01:56 (4186029) INFO MutationTestReportHelper^[[39m Final mutation score of 54.95 is greater than or equal to break threshold 35
INFO HtmlReporter: mutation report written to the metadata package's reports/mutation directory
^[[32m16:01:56 (4186029) INFO MutationTestExecutor^[[39m Done in 16 minutes and 50 seconds.
── ✓ completeness-gate (mutation) (16m51s)
╚═════════════ lane: mutation — ✓ 16m51s ═════════════

══════════════════════ SUMMARY ══════════════════════
  ✓ dependencies installed
  ✓ doc-template drift
  ✓ embedded-library drift
  ✓ ts build  (6s)
  ✓ java-reactor (install)  (6m24s)
  ✓ conformance: python  (5m11s)
  ✓ integration-tests (python)  (1m34s)
  ✓ completeness-gate (mutation)  (16m51s)
  ✓ conformance: csharp  (2m58s)
  ✓ integration-tests (csharp)  (10m16s)
  ✓ migrate-ts real-PG suite  (1m28s)
  ✓ runtime-ts real-PG dialect matrix  (5s)
  ✓ prisma real-PG round trip  (17s)
  ✓ brownfield recipe real-PG check  (6s)
  ✓ ts typecheck  (37s)
  ✓ conformance: typescript  (8m39s)
  ✓ ts unit suites  (50s)
  ✓ integration-tests (kotlin)  (9m16s)
  ✓ integration-tests (java)  (3m43s)
  ✓ showcase current in all ports  (1m17s)
  ✓ cube-model live check (Cube + Postgres)  (3m06s)
  ✓ leak-scan (security)
  ✓ pom-version parity
  ✓ bun-version parity
  ✓ uv.lock version parity
  ✓ publish-intent parity  (1s)
  ✓ 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
  ✓ metamodel-version bump
  ✓ positioning claims  (1s)
  ✓ hosted assess prompt builds
  ✓ migration guides vs registry
  ✓ peer-range bounds
  ✓ owned template copies current
  ✓ reference templates lint  (1s)
  ✓ shipped doc examples load  (3s)
  ✓ no retired $apiPrefix
  ✓ site payload is true  (1m00s)
  ✓ site reference is fresh  (1s)
  ✓ release tag gate  (33s)
  ✓ scripts/ typecheck  (13s)
  ✓ requirements ledger verifies  (5s)
  ✓ requirements cover vocabulary  (2s)
  ✓ metamodel scaffolder  (2s)
  ✓ requirement harness
  ✓ fixture-lint
  ✖ integration-tests (ts)  (7m50s)
── slowest steps ──
    16m51s  completeness-gate (mutation)
    10m16s  integration-tests (csharp)
     9m16s  integration-tests (kotlin)
     8m39s  conformance: typescript
     7m50s  integration-tests (ts)
     6m24s  java-reactor (install)
     5m11s  conformance: python
     3m43s  integration-tests (java)
     3m06s  cube-model live check (Cube + Postgres)
     2m58s  conformance: csharp
     1m34s  integration-tests (python)
     1m28s  migrate-ts real-PG suite
── lanes (wall time, ran 8 at once) ──
    16m51s  mutation
    13m14s  csharp
    10m06s  ts
     9m46s  ts-integ
     9m16s  kotlin-integ
     6m45s  python
     6m24s  java
     3m43s  java-integ
     3m06s  cube
     2m02s  gates
     1m17s  showcase
  total wall time: 17m03s
══════════════════════════════════════════════════════
LOCAL CI FAILED — 1 gate(s) red.
  • Evidence: SQLite CREATE TABLE with distinct flattened prefixes, kept as the generated migration files of a throwaway project (columns bill_street, bill_city, ship_street, ship_city; no duplicate column)
  • Evidence: Postgres primary-key rename migration, kept as the generated migration files of a throwaway project (RENAME CONSTRAINT to pk_orders and pk_order_lines; verify --db clean afterwards)
  • Evidence: C# gen output with wire.durations: native, kept from a throwaway project (plain TimeSpan, no [DurationWire.Iso])
  • Evidence: C# gen output with wire.durations: iso, kept from a throwaway project ([DurationWire.Iso] on duration properties)
  • Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (3h2m51s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - 4 findings, all fixed
  • 🔧 Fixed: server/typescript/packages/metadata/src/loader/validation-passes.ts and the C#, Java (Kotlin uses it) and Python loaders - a literal compared with a field.duration in @condition, or a requiredWhen/presentIff @equals gated on one, was never checked to be an ISO-8601 duration, so "1 hour" loaded clean in all five ports, the generated insert schema silently dropped the guarded rule and meta migrate failed late. Every port now refuses it at load with ERR_BAD_ATTR_VALUE, gated by the new error-validator-condition-duration-literal conformance fixture.
  • 🔧 Fixed: server/csharp/MetaObjects/Loader/ValidationPasses.cs - the comment above ValidateColumnNameCollisions said field.object is skipped, which CR-1 made untrue. It now says a flattened field.object contributes one column per member (PhysicalColumnsOf) and any other field.object is one jsonb column.
  • 🔧 Fixed: server/csharp/MetaObjects/Loader/ValidationPasses.cs - the C# duration-literal pattern ended in $, which in .NET also matches before a trailing newline, so "PT1H\n" loaded in C# only. The pattern now ends in \z, as the other ports already behave. (The review's side note about out-of-range magnitudes does not apply: the TypeScript literal check has no range limit either.)
  • 🔧 Fixed: docs/CONFORMANCE.md, AGENTS.md and examples/showcase/site-payload.json - the metamodel fixture count read 448 after the new error fixture; it now reads 449 everywhere.

✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • 🚨 configured test command failed with exit code 1
  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Run the configured TS gate (ts-fast + ts-unit); it exits 0 ✅ pass live scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains exit 0, LOCAL CI PASSED
meta verify refuses a non-ISO duration literal in @condition at load ✅ pass live cli-duration-literal.log: ERR_BAD_ATTR_VALUE ... not an ISO-8601 duration, exit=1
meta verify accepts a valid ISO duration literal PT1H ✅ pass live cli-duration-literal.log: good project exit=0
Full per-package TS unit suites show no regression ✅ pass live ts-unit-direct.log: 0 fail across metadata, migrate-ts, codegen-ts, cli, render, runtime-ts, sdk and docs-site
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains: LOCAL CI PASSED (build+typecheck, TS conformance, unit suites, mutation gate)
  • bun test in migrate-ts for cross-field-check-extensions.test.ts: 23 pass
  • bun test --timeout 30000 in metadata, migrate-ts, codegen-ts, cli, render, runtime-ts, sdk and docs-site: 0 failures
  • Live meta verify against two throwaway projects: @condition literal "1 hour" on a field.duration, then PT1H

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
CR-1: two flattened field.objects with empty prefix are refused at load with ERR_COLUMN_NAME_COLLISION ✅ pass live meta verify in p1: ERR_COLUMN_NAME_COLLISION: fields "billTo.street" and "shipTo.street" ... both resolve to physical column "street"
Distinct flattened prefixes migrate to a valid CREATE TABLE with no duplicate column ✅ pass live p2 meta migrate --from-db --dialect sqlite --apply: columns bill_street, bill_city, ship_street, ship_city
Non-ISO duration literal in @condition or @equals is refused at load (ERR_BAD_ATTR_VALUE) ✅ pass live meta verify on error-validator-condition-duration-literal: '"1 hour", which is not an ISO-8601 duration'
validator.exactlyOne: one-field is refused; valid form emits a CHECK that SQLite enforces on both-set and none-set rows ✅ pass live verify refusal message; INSERT with both set and none set REFUSED by CK_StoreHours_ExactlyOneLocation, exactly one OK
CR-16: @max on validator.atLeastOne is refused with ERR_UNKNOWN_ATTR ✅ pass live pmax meta verify: Unknown attribute '@max' on validator.atLeastOne 'anyTime'
CR-2: wire.durations accepts native and iso, rejects other values ✅ pass live meta verify native and iso clean; bogus: 'wire.durations must be one of "native", "iso"'
CR-2: C# codegen follows wire.durations (native = plain TimeSpan, iso = [DurationWire.Iso]) ✅ pass live diff of pcs/out_native and pcs/out_iso Timer.g.cs
CR-3: changing the primary-key naming convention on live Postgres renames the key in place (RENAME CONSTRAINT), applies cleanly, and leaves no drift ✅ pass live up.sql: ALTER TABLE "orders" RENAME CONSTRAINT "orders_pkey" TO "pk_orders"; pg_constraint shows pk_orders, pk_order_lines; verify --db clean; follow-up migrate: 'no schema changes'
migrate-ts Postgres integration suite (FK refit, constraint-backed index, CHECK round trip, duration round trip) passes on a real Postgres ✅ pass live MIGRATE_TS_PG_URL bun test test/integration: 259 pass, 0 fail
Full regression: ci-local.sh full parity ✅ pass live ci-local-full.log: all lanes green except ts-integ; its sole failure (Miniflare D1 report-export-too-large, an unrelated worker IPC assertion) passed 84/84 twice when rerun alone
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • meta verify on a model with two flattened field.objects sharing @flattenedColumnPrefix: "" (ERR_COLUMN_NAME_COLLISION)
  • meta migrate --from-db --dialect sqlite --apply on distinct prefixes (single CREATE TABLE, no duplicate column)
  • meta verify on the duration-literal error fixture (ERR_BAD_ATTR_VALUE for 1 hour, 90 minutes, P1M)
  • meta verify on validator.exactlyOne with one field (refused), and on the valid exactlyOne fixture
  • meta migrate --apply on SQLite, then INSERTs against the CHECK constraint (both set, none set, exactly one)
  • @max on validator.atLeastOne (ERR_UNKNOWN_ATTR)
  • wire.durations in .metaobjects/config.json: native, iso and bogus values through the TS CLI
  • C# CLI gen against the worktree Codegen project with wire.durations native vs iso ([DurationWire.Iso] appears only under iso)
  • Scratch Postgres 18: init migration, then primary-key-name convention change, migrate apply-pending, meta verify --db, and a repeat migrate dry-run
  • MIGRATE_TS_PG_URL=... bun test test/integration in migrate-ts against the scratch Postgres (259 pass)
  • scripts/ci-local.sh --strict-toolchains (full run: gates, java, python, csharp, ts, kotlin/java/ts integration, cube, showcase, mutation)
  • bun test test/api-contract-report-sqlite.test.ts twice in integration-tests (84 pass each time)
✅ **Document** - stale duration docs fixed; follow-up audit found no drift
  • 🔧 Fixed: stale field.duration wire docs and the agent-context mirrors that copy them.
  • ✅ Audited on request: the docs for the smaller change requests (efcore preset labelling, the sequence docs and SQLite advisory, the @flattenedColumnPrefix overlay merge conflict) were checked against the code and all match it; no change needed. The request also named a "conventions prefix", which is not a setting the code has; the flattened-prefix overlay conflict is documented correctly.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

… every port (metamodelVersion 1.3)

Registers the 1.3 vocabulary in all five ports, with the loader checks and
registry-conformance coverage:

- field.duration, a fixed-length span of time.
- validator.exactlyOne (@fields, two or more). It replaces atLeastOne @max,
  which is removed with no shim.
- @condition (attr.expression) on all five cross-field validators.
- @constraintName on validators and on field.enum.
- @flattenedColumnPrefix on a flattened field.object.
- @sequenceStart and @sequenceName on identity.primary.
- @maxlength on field.enum, plus the enums.maxLength project setting.
- The upperFirst column naming rule.

Every port's config reader also reads wire.durations (native by default, or iso)
beside decimals and nullMembers.

ADR-0023 Amendment 1 records why each new attribute cannot be computed, and
each registered description points to it. The loader now checks flattened
member columns for collisions.
- migrate.conventions names the database objects no code addresses. efcore
  is a reference preset.
- migrate.checks.skipRedundant leaves out CHECKs the column type already
  enforces.
- Validators emit CHECKs:
  - with a declared @constraintName
  - with a @condition, as NOT (cond) OR rule
  - for exactlyOne, as num_nonnulls(...) = 1, or a sum of presence tests on
    SQLite and D1
- Identity sequences (@sequenceStart / @sequenceName) are emitted and read
  back. A failure to read them is reported instead of being swallowed.
- A constraint or index whose only change is its name is renamed in place on
  Postgres.
- A key that changes both its name and its kind is renamed first, then
  promoted or demoted, in the same migration (#285).
- An FK whose target is not a unique key:
  - Postgres and D1 refuse it.
  - SQLite only warns, because it accepts the CREATE TABLE. `meta verify --db`
    reports the same.
- An enum column whose members are longer than its length is refused by name.
…'s generated code

- field.duration is mapped natively in every port: TimeSpan, java.time.Duration,
  timedelta, and a string in TypeScript.
- wire.durations=iso makes every port write one ISO-8601 form, and accept it
  as filter operands.
- A malformed duration filter operand now returns 400 invalid_filter_value in
  Java and Python, where it used to throw.
- The generated create models enforce validator.exactlyOne, and check any rule
  that has an @condition only when the condition is TRUE under SQL
  three-valued logic. Ports:
  - Zod superRefine
  - IValidatableObject
  - Bean Validation
  - KotlinCrossFieldRules
  - Pydantic model_validator
- Names artifacts carry a flattened value object's member columns, and no
  longer carry a column for the field itself.

New conformance:
- fixtures/duration-wire vectors
- an api-contract duration/ sub-corpus, generated lane, in all five ports
- a Shift entity in the cross-field validation corpus: 14 cases for
  exactlyOne and @condition
- CHANGELOG entries for metamodelVersion 1.3.
- Updated feature docs: entities, field types, migrations and drift, metadata
  sources, api-contract, physical naming.
- Updated conformance counts.
- Agent-context skill references.
- Regenerated site reference and example payloads.
- The requirements ledger pin rises to 150 for the two new requirement nodes
  (fieldDuration, validatorExactlyOne).

This lands the work from feat/schema-conventions on main as a few logical
commits, with the review fixes applied, and supersedes that branch.
…xt mirrors. The 449 fixture count was already correct. Unaudited: smaller CR docs.
…land

Conflicts were additive on both sides: the branch's schema build settings and
#534's run output and requirement ledger now travel together through GenContext,
the runner, meta gen, meta verify --codegen and the codegen-drift probe.
CHANGELOG keeps the branch's consolidated Unreleased section and adds #534's
two entries.
@dmealing
dmealing merged commit 342bd8c into main Nov 5, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-schema-conventions-land branch November 5, 2026 02:15
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