Repository navigation
feat(metadata): add field.duration, validator.exactlyOne and schema conventions - #535
Merged
Merged
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
field.durationin every port (TimeSpan,java.time.Duration,timedelta, string in TypeScript), with Postgresintervalor 64-bit microsecond columns, awire.durationssetting (nativedefault,iso), and shared vectors infixtures/duration-wire/. Also addsvalidator.exactlyOneand@constraintName,@condition,@maxLengthonfield.enum,@sequenceStart/@sequenceName,@flattenedColumnPrefix, all validated at load, and bumpsmetamodelVersionto1.3.meta migrate/verifyname and refuse CHECK constraints, compare primary-key names and emit RENAME CONSTRAINT, refuse FK targets that are not unique, and enforce cross-field validators in generated create models. Flattened-field column collisions are now detected in every port.feat/schema-conventions, which is left untouched. This branch is rebased onto currentmainand includes fix: cross-field comparison rule across ports and C# concrete-parent properties #531's cross-kind comparison rule; C# now givesfield.uriandfield.inetthe same filter operators as the other ports.\z, the fixture count now reads 449, and one migrate test was updated for the load-time refusal.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
metaCLI, 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 fullci-local.sh --strict-toolchainsrun went green on all lanes exceptts-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.meta verifyin p1: ERR_COLUMN_NAME_COLLISION: fields "billTo.street" and "shipTo.street" ... both resolve to physical column "street"meta migrate --from-db --dialect sqlite --apply: columns bill_street, bill_city, ship_street, ship_citymeta verifyon error-validator-condition-duration-literal: '"1 hour", which is not an ISO-8601 duration'meta verify: Unknown attribute '@max' on validator.atLeastOne 'anyTime'meta verifynative and iso clean; bogus: 'wire.durations must be one of "native", "iso"'verify --dbclean; follow-up migrate: 'no schema changes'Evidence: full ci-local run log
verify --dbclean afterwards)genoutput withwire.durations: native, kept from a throwaway project (plain TimeSpan, no[DurationWire.Iso])genoutput withwire.durations: iso, kept from a throwaway project ([DurationWire.Iso]on duration properties)Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - 4 findings, all fixed
server/typescript/packages/metadata/src/loader/validation-passes.tsand the C#, Java (Kotlin uses it) and Python loaders - a literal compared with afield.durationin@condition, or a requiredWhen/presentIff@equalsgated 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 andmeta migratefailed late. Every port now refuses it at load with ERR_BAD_ATTR_VALUE, gated by the newerror-validator-condition-duration-literalconformance fixture.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.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.)docs/CONFORMANCE.md,AGENTS.mdandexamples/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 ✅
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsexit 0, LOCAL CI PASSEDscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains: LOCAL CI PASSED (build+typecheck, TS conformance, unit suites, mutation gate)bun testin migrate-ts forcross-field-check-extensions.test.ts: 23 passbun test --timeout 30000in metadata, migrate-ts, codegen-ts, cli, render, runtime-ts, sdk and docs-site: 0 failuresLivemeta verifyagainst two throwaway projects:@conditionliteral "1 hour" on afield.duration, thenPT1H🔧 Fix applied.
✅ Re-checked - no issues remain.
meta verifyin p1: ERR_COLUMN_NAME_COLLISION: fields "billTo.street" and "shipTo.street" ... both resolve to physical column "street"meta migrate --from-db --dialect sqlite --apply: columns bill_street, bill_city, ship_street, ship_citymeta verifyon error-validator-condition-duration-literal: '"1 hour", which is not an ISO-8601 duration'meta verify: Unknown attribute '@max' on validator.atLeastOne 'anyTime'meta verifynative and iso clean; bogus: 'wire.durations must be one of "native", "iso"'verify --dbclean; follow-up migrate: 'no schema changes'scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsmeta verifyon a model with two flattenedfield.objects sharing@flattenedColumnPrefix: ""(ERR_COLUMN_NAME_COLLISION)meta migrate --from-db --dialect sqlite --applyon distinct prefixes (single CREATE TABLE, no duplicate column)meta verifyon the duration-literal error fixture (ERR_BAD_ATTR_VALUE for1 hour,90 minutes,P1M)meta verifyonvalidator.exactlyOnewith one field (refused), and on the valid exactlyOne fixturemeta migrate --applyon SQLite, then INSERTs against the CHECK constraint (both set, none set, exactly one)@maxonvalidator.atLeastOne(ERR_UNKNOWN_ATTR)wire.durationsin.metaobjects/config.json: native, iso and bogus values through the TS CLIC# CLIgenagainst the worktree Codegen project withwire.durationsnative 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 repeatmigratedry-runMIGRATE_TS_PG_URL=... bun test test/integrationin 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.tstwice in integration-tests (84 pass each time)✅ **Document** - stale duration docs fixed; follow-up audit found no drift
field.durationwire docs and the agent-context mirrors that copy them.@flattenedColumnPrefixoverlay 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.