fix(cli): scaffold outDir defaults to src/generated, not ./src/db - #1
Closed
dmealing wants to merge 1 commit into
Closed
fix(cli): scaffold outDir defaults to src/generated, not ./src/db#1dmealing wants to merge 1 commit into
dmealing wants to merge 1 commit into
Conversation
`meta init` scaffolded `outDir: "./src/db"` alongside `dbImport: "../db"`,
which puts generated files in src/db/ right next to the src/db.ts the user
must create for their DB instance — same path stem, two different things —
and the generated routes' `import { db } from "../db"` can resolve to the
generated barrel (src/db/index.ts) instead of the user's src/db.ts. Default
to `src/generated` (matches docs/ports/typescript.md) so generated code and
the user's db module stay clearly separate. Found while building the
five-minute quickstart demo against published 0.9.0.
Adds an init test asserting the scaffolded outDir.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dmealing
added a commit
that referenced
this pull request
Jun 25, 2026
…om the migration binding (#1) The meta init scaffold conflated a generic invariant with a project-specific binding in the always-injected AGENTS.md/CLAUDE.md, and guessed the binding: 1. The "never hand-write" principle listed only code artifacts (FKs, CRUD, validators, finders) and never named the DB schema/migrations as metadata-derived — so an agent would hand-write a raw ALTER TABLE against a generated schema, silently reintroducing the exact drift meta verify exists to catch. 2. The stack line asserted "migrations are TS" — a guessed binding that's wrong for adopters who consume meta migrate output as Flyway/Liquibase/etc. Fix (tool states invariants; project states bindings; tool never guesses): - Extend the principle to name the database schema and migrations as metadata-derived, tool-agnostically: "The schema is a disposable, generated artifact: change the metadata and regenerate, never hand-write SQL." - Drop the "; migrations are TS" clause from the assembled stack line. Deliberately NOT adding a persistence/migrations config field (issue's optional "last point"): the generic invariant stops the agent hand-writing migrations, and a project that needs a specific line states it in a hand-written region of AGENTS.md (three-way merge already preserves those) — no new config surface. The always-installed metaobjects-verify/migration reference fragment already carries the project-agnostic how. Regenerated the 4 cross-stack conformance fixtures; agent-context conformance + root/sdk byte-identity gates green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
dmealing
added a commit
that referenced
this pull request
Jun 25, 2026
…idator removal (#73) * fix(agent-context): hedge the JVM startup-validator claim in verify skill (invariant vs binding) Audit of the agent-context corpus (5 skill bodies + 16 reference fragments + README) for the issue #1 defect class — conflating a generic invariant with a project-specific binding, or guessing a binding — found it broadly clean: per-stack reference fragments correctly carry their own stack's binding without guessing a second one, LLM callers are explicitly bring-your-own, and the "schema/migrations are metadata-derived" framing is intact across all ports. One consistency fix: verify/SKILL.md asserted that on the JVM ports "a runtime startup validator catches generated-table drift at app boot" as a flat fact — but whether an adopter wires that validator is an opt-in project binding. migration.md already hedges the same capability ("can catch"); this aligns verify/SKILL.md to it: "can catch ... an optional complementary check (if your project wires one)". Regenerated the 4 cross-stack conformance fixtures (the skill body is installed for every stack). Conformance + byte-identity gates green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * no-mistakes(document): sync Java migration/drift docs to ADR-0015 validator removal --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dmealing
added a commit
that referenced
this pull request
Jun 25, 2026
Lockstep npm release of the @metaobjectsdev/* packages (13 publish candidates). Ships the agent-friendly meta CLI (--format/TOON output, deploy-all agent-context references, #71) plus the agent-context scaffold fixes (#1, #73): the injected AGENTS.md/CLAUDE.md now name the database schema + migrations as metadata-derived and no longer guess the migration binding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
dmealing
added a commit
that referenced
this pull request
Jun 30, 2026
…ime (all 5 ports) (#126) * feat: ADR-0036 Wave 2 (TS) — field.timestamp instant-by-default + @localTime Flip field.timestamp to instant / timezone-aware BY DEFAULT (Postgres timestamptz) and add a boolean @localTime attr on field.timestamp as the rare naive / wall-clock opt-out (timestamp without time zone). Retire the @dbColumnType: timestamp_with_tz escape hatch entirely — the @dbColumnType legal set shrinks to { uuid, jsonb }. ADR-0036 Wave 2, TS reference port. This establishes the canonical golden + canonical Postgres schema + shared fixtures the other four ports byte-match. Metamodel (metadata): - db-constants.ts: add FIELD_ATTR_LOCAL_TIME; drop timestamp_with_tz from DB_COLUMN_TYPE_VALUES + DB_COLUMN_TYPE_LEGAL_SUBTYPES (now [uuid, jsonb]). - spec/metamodel/db.json: new boolean @localTime on field.timestamp; updated @dbColumnType description/allowedValues. Regenerated db-definition.embedded.ts. Schema / codegen: - migrate-ts expected-schema.ts: field.timestamp → timestamptz by default, @localTime:true → TIMESTAMP; removed the timestamp_with_tz override branch. - codegen-ts column-mapper.ts: field.timestamp → timestamp({ withTimezone: true }) by default, @localTime:true → withTimezone:false; removed timestamp_with_tz. Shared cross-port fixtures (other ports byte-match these): - persistence-conformance meta.fitness.json: dropped @dbColumnType on the two tz fields (recordedAt, tsTzVal — now bare = tz); marked the wire-naive fields (createdAt, observedAt, tsVal) @localTime:true to preserve their naive wire form. Canonical schema.postgres.sql + every roundtrip/query expected VALUE are byte-identical (regenerated, no diff). - flattened-kitchen-sink conformance: dropped timestamp_with_tz on updatedAt (now bare tz) across input + expected + expected-effective. - Regenerated registry-conformance golden (expected-registry.json + coverage-report.json) and metamodel-docs. ADR-0019: temporal clause updated — field.timestamp instant/tz-aware by default, @localTime the naive opt-out. Tests: field-definition/db-definition-completeness, attr-schema-validate, expected-schema, column-mapper, golden, sqlite-roundtrip (now builds expected with dialect:"sqlite" so SQLite's tz-blind introspection matches). All affected TS suites green; bun run --filter '*' build green. Live-PG integration tests are CI/Testcontainers-gated (not run locally). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * docs(adr): ADR-0036 decision #1 — @localTime attribute (not field.localDateTime subtype) Corrected per ADR-0037: timezone-awareness is a same-kind orthogonal modifier of field.timestamp, so it's a boolean @localTime attribute (default false = instant), not a sibling subtype. Matches the Wave-2 implementation. * feat: ADR-0036 Wave 2 (Python) — field.timestamp instant-by-default + @localTime Port the TS reference (fa40542) to Python. field.timestamp is now an absolute instant / timezone-aware BY DEFAULT; the rare naive / wall-clock case opts out with the boolean @localTime attr. The @dbColumnType: timestamp_with_tz escape hatch is retired — the legal @dbColumnType set shrinks to { uuid, jsonb }. The canonical golden + shared persistence fixtures are TS-owned and unchanged; this makes the Python port byte-match them. Metamodel: - db_constants.py: add FIELD_ATTR_LOCAL_TIME; drop DB_COLUMN_TYPE_TIMESTAMP_TZ from VALID_DB_COLUMN_TYPES (now [uuid, jsonb]); tombstone the retired constant. - db_provider.py: register @localTime (boolean, NO allowed_values) on field.timestamp only; description sourced from the embedded spec_metamodel/db.json by apply_spec_descriptions (single-source). @dbColumnType allowed_values shrinks via VALID_DB_COLUMN_TYPES. - spec_metamodel/db.json: re-copied from the TS-updated repo-root spec/metamodel/db.json (byte-identity gate) — new boolean @localTime on field.timestamp + slimmed @dbColumnType allowedValues/description. Loader: - validation_passes.py: drop timestamp_with_tz from the @dbColumnType legal pairing map (now uuid/jsonb→field.string only). A timestamp_with_tz value now fails Rule 1 (unrecognized value) → still ERR_BAD_ATTR_VALUE, so the shared error-dbcolumntype-illegal-pairing fixture stays green (trigger shifts from illegal-pairing to unknown-value). Runtime (the core native-type flip): - object_manager.py write codec: default field.timestamp → tz-aware datetime (UTC tzinfo → driver binds timestamptz); @localTime:true → naive datetime (no tzinfo → driver binds plain timestamp). Inverts the prior default. The normalization boundary (tests/integration/normalization.py) already keys tz-aware↔Z / naive↔no-Z off tzinfo, so it is unchanged. Codegen/schema: Python is pure data-access (ADR-0015) — no DDL/schema emitter and the Pydantic type map is datetime.datetime for both modes, so no codegen column-type branch existed to flip. Tests: registry-conformance byte-match green; spec_metamodel embed byte-match green; full conformance (301) green incl. the shared illegal-pairing fixture; test_field_uuid_dbcolumntype updated for the retirement + a new @localTime-ok case. Native-type rule unit-verified (default aware / @localTime naive). The persistence op:roundtrip (tsVal naive / tsTzVal tz-aware) needs pg8000 + Testcontainers Postgres — CI-gated, not run locally. The 2 failing test_cli_staleness_nudge tests are pre-existing (unrelated agent-docs message wording), failing identically on the base branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * feat: ADR-0036 Wave 2 (JVM: Java + Kotlin) — field.timestamp instant-by-default + @localTime Flip field.timestamp to instant / timezone-aware BY DEFAULT (Postgres timestamptz, java.time.Instant) and add a boolean @localTime attr on field.timestamp as the rare naive / wall-clock opt-out (timestamp without time zone, java.time.LocalDateTime). Retire the @dbColumnType: timestamp_with_tz escape hatch entirely — the @dbColumnType legal set shrinks to { uuid, jsonb }. Java + Kotlin share metadata, so both ports are flipped in lockstep. Byte-matches the TS-established golden + shared fixtures (NOT modified here): expected-registry.json now carries @localTime (boolean, no allowedValues) on field.timestamp and @dbColumnType allowedValues=[uuid, jsonb]. Metadata (shared Java metadata module, used by both ports): - CoreDBMetaDataProvider: add LOCAL_TIME constant; drop DB_COLUMN_TYPE_TIMESTAMP_TZ from VALID_DB_COLUMN_TYPES (now [uuid, jsonb]); refresh Javadoc. - TimestampField: register @localTime (boolean) on field.timestamp (manifest description/allowedValues sourced from the shared spec/metamodel/db.json the TS commit already updated — read off the classpath by SpecMetamodelReader). - ValidationPhase.validateDbColumnType: drop the timestamp_with_tz legal pairing; the retired value now trips Rule 1 (unrecognized value) → still ERR_BAD_ATTR_VALUE. Native type binding flip (INVERTS the pre-Wave-2 timestampWithTzOptIn logic): - Java SpringTypeMapper: default field.timestamp → java.time.Instant; @localTime:true → java.time.LocalDateTime. Helper timestampWithTzOptIn → localTimeOptIn (reads @localTime). - Java OMDB JdbcCodecs.TimestampCodec: default binds/reads as a UTC instant (setObject TIMESTAMP_WITH_TIMEZONE); @localTime binds/reads naive (setTimestamp(UTC Calendar)). Helper isTimestampTz → isLocalTime. SimpleMappingHandlerDB / ColumnDef: drop the dead COLTYPE_TIMESTAMP_TZ hint (timestamp tz is no longer a @dbColumnType hint — the codec reads @localTime off the field directly). - Kotlin KotlinTypeMapper: default → Instant + Exposed instantWithTimeZone(...) / TIMESTAMP WITH TIME ZONE; @localTime:true → LocalDateTime + Exposed datetime(...). Helper timestampWithTzOptIn → localTimeOptIn; usesInstantWithTimeZone now true for the DEFAULT (drives the per-package instantWithTimeZone support file). Tests (JVM): - metadata: registry-conformance byte-matches the golden (Java + Kotlin both green); DbColumnTypeValidationTest rewired (retired-tz-value-rejected, @localTime-legal, illegal-pairing message on uuid-on-timestamp). - codegen-spring/codegen-kotlin: SpringTypeMapperTest / KotlinTypeMapperTest flipped; SpringDtoGeneratorTest + the Kotlin Exposed/projection/snapshot tests updated (regenerated single-entity-primitives snapshot: createdAt → Instant / instantWithTimeZone + new MetaInstantWithTimeZoneColumnType.kt support file). - OMDB codec round-trip (Derby): meta.codec.json tsVal + the raw-codec probe marked @localTime so the naive (Derby-compatible) path is exercised; the instant/tz default is Postgres-only and gated by persistence-conformance. - Testcontainers AllTypes op:roundtrip GREEN both ports (Java 24/24, Kotlin 24/24): tsVal (@localTime naive LocalDateTime) + tsTzVal (default tz Instant) write+read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * feat(csharp): ADR-0036 Wave 2 — field.timestamp default flip to instant/tz-aware + @localTime Port the C# implementation of ADR-0036 Wave 2 to match the TS-established golden registry + shared fixtures: - Register @localTime (boolean, no allowedValues) on field.timestamp only, via the db provider (DbConstants.FIELD_ATTR_LOCAL_TIME + DbSchema.LocalTimeSchema + DbProvider field.timestamp Extend). Description byte-matches the golden. - Retire @dbColumnType: timestamp_with_tz — drop it from VALID_DB_COLUMN_TYPES (legal set is now [uuid, jsonb]) and from the ValidateDbColumnType pairing; update the @dbColumnType descriptions in DbSchema.cs + SpecMetamodel/db.json (copied from spec/metamodel/db.json) to byte-match the golden. Exempt @dbColumnType from the generic allowedValues check (Check 3) so a retired-value still emits a single ERR_BAD_ATTR_VALUE (mirrors the TS Check-3 exemption), not a duplicate. - CLR-type binding (the core change): default field.timestamp → DateTimeOffset (an absolute instant); field.timestamp @localTime:true → DateTime (naive). CSharpNaming gains IsLocalTime + the field-aware ScalarForField; the entity generators emit the conditional CLR type. - EF/DDL column mapping: default field.timestamp → `timestamp with time zone` (timestamptz); @localTime:true → `timestamp without time zone`. Removed the timestamp_with_tz override branch. - Regenerated the committed integration fixtures (AllTypes/Asset/AppDbContext): tsTzVal + Asset.RecordedAt now DateTimeOffset/timestamptz; tsVal + observedAt stay DateTime/timestamp via @localTime. - Integration test-harness: DateTimeOffset write coercion (UTC) + EntityRow pass-through + Normalization UTC formatter (fix the double-Z) for the persistence roundtrip; the api-contract generated server provisions createdAt as timestamptz + interprets an offset-less instant body as UTC so the EF write succeeds on any CI host timezone. The registry-conformance byte-match passes; @localTime + the slimmed @dbColumnType allowedValues byte-match the golden. All C# suites green (Conformance 668, Codegen 251, Render 290, CLI 42, IntegrationTests 82 incl. the AllTypes roundtrip + api-contract generated lane against Testcontainers PG). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * fix(jvm): ADR-0036 Wave 2 — api-contract harness + Kotlin controller emit Instant for createdAt `Author.createdAt` is a bare `field.timestamp`, which under ADR-0036 Wave 2 generates as `java.time.Instant` (instant/tz-aware default). Align the JVM api-contract test harnesses (Java + Kotlin) — and close one genuine Wave-2 generator gap — so the generated-controller lane compiles and round-trips. createdAt wire form is now an absolute UTC instant (`yyyy-MM-ddTHH:mm:ssZ`). Offset-less corpus seed/scenario strings are interpreted as UTC (append `Z`), mirroring the C# lane. Java: - generated/GeneratedAuthorControllerHarness: DTO ctor reflection signature `LocalDateTime.class` → `Instant.class`; build createdAt via `Instant.parse` (UTC-interpreting); register a UTC Instant Jackson deserializer so offset-less request bodies deserialize into the generated DTO's `Instant` field. - generated/InMemoryAuthorRepositorySource: `AuthorDto.createdAt` is `Instant`; filter comparison parses operands as UTC instants; refreshed stale comment. - AuthorApiServer (reference lane): `createdAt` column → `timestamp with time zone`; read/write/filter via UTC `OffsetDateTime`/`Instant`; emit `...Z` wire. Kotlin: - generated/GeneratedAuthorControllerHarness: register a UTC Instant Jackson deserializer for the generated `Author.createdAt: Instant`. - AuthorApiServer (reference lane): `AuthorTable.createdAt` → Exposed `timestamp()` (Instant); parse/format/filter via UTC instants; emit `...Z`. - KotlinSpringControllerGenerator (generator gap): a default `field.timestamp` column is `Column<Instant>`, but the controller imported only `LocalDateTime` and the timestamp filter-coercer produced a `LocalDateTime` — a missing import + a ClassCast at the `p.value as Instant` dispatch cast. Now import `java.time.Instant` when an Instant-typed timestamp column is in the filter surface, and emit an Instant-producing (UTC-interpreting) coercer for it. Instant-free entities stay byte-identical (snapshot golden unchanged). Verified against Testcontainers Postgres (Docker available): all Java + Kotlin api-contract integration tests green (53/53 each, incl. generated/TPH/jsonb/m2m lanes); codegen-kotlin 255/255 (snapshot golden byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n --------- Co-authored-by: Doug Mealing <noreply@anthropic.com>
dmealing
added a commit
that referenced
this pull request
Jul 14, 2026
…#3, #11 FR-036 made the router validate POST/PATCH against the read/entity model, which was wrong for a create/patch WIRE body. Introduce dedicated wire-shaped validation models (entity_model.py) and bind them in the router: - New <Entity>Create model for POST: excludes the auto-gen PK (identity.primary @generation increment|uuid) [#2] and @readonly fields; makes a @required field that is SERVER-FILLED — ANY @default (literal OR sql-expr now()/gen_random_uuid()) or @autoset — OPTIONAL [#1, the regression: a normal POST omitting createdAt @required @default:"now()" no longer 400s]; keys by the WIRE name field.name [#3] (was @column, so a column-renamed entity's constraints checked the wrong key — bypass or spurious 400); carries the field constraints. Mirrors the TS InsertSchema (fieldWillBeOptional / autoGenPkFieldNames). - <Entity>Patch now excludes the PK [#11] and keys by the wire name [#3]. - The read model <Entity> (@column-named) is unchanged — no consumer break. FR-035 tristate + the repo create/update dict seam are unchanged. Router: POST validates <Entity>Create(**dto) / per-subtype <Sub>Create(**dto); PATCH validates <Entity>Patch(**dto). Gated by 4 new unit tests (server-filled optional, auto-gen PK excluded, wire-name validation, patch excludes PK). Regenerated 3 goldens. Verified: entity-model + validation-conformance + generated api-contract lanes 54/0 (the only broader-suite failures are the 2 pre-existing, unrelated cli-staleness-nudge tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGQ7oSuNcjhsMHWwZzhBwr
dmealing
added a commit
that referenced
this pull request
Aug 19, 2026
…can load its input
The Python port shipped both halves of the AI trace stack and not the metadata in
between. `runtime/llm_recorder.py` was there. The `trace-helper` generator was there
and registered for the CLI. But there was no `library/` package and no `libraries`
loader option, so `metaobjects::ai::LlmCallBase` could not be loaded on this port at
all: the documented `extends: metaobjects::ai::LlmCallBase` failed with
ERR_UNRESOLVED_SUPER. A generator shipped without its input.
The more useful part is why nothing caught it. The codegen suite hand-builds its own
abstract `LlmCallBase` — exactly what ADR-0024 already recorded ("the green tests pass
only because they bypass the shipped base with bespoke entities"). A test that builds
its own fixture proves nothing about the path an adopter follows, so the port's docs
could describe a path that could not run.
No new vocabulary — no type, subtype, or attribute — so expected-registry.json is
untouched. This mirrors the TypeScript design rather than inventing a second one: same
package names, same refs (path under library/ minus .yaml), same on-disk-first order.
library/ library_sources(packages) returns a FileSource when the repo-root
library/ tree is reachable, so editing the canonical YAML takes
effect immediately, and falls back to the generated embed
otherwise (the wheel-in-site-packages case).
scripts/ regenerates the embed. Embedded as a .py module, not shipped as
package data, so no build-backend config can silently drop it.
libraries=[...] on from_directory (hence load_directory). Opt-in and lazily
imported: a load requesting no libraries neither pays the import
nor gets extra names in its model. Sources are prepended for a
deterministic, TS-matching order — NOT because resolution needs
it; resolve_supers runs once after every root merges.
config `libraries` threaded into the CLI's load path. The option first landed only
on the loader — which is also all TypeScript exposes — so
`metaobjects gen` still could not load the metadata the registered
trace-helper generator exists to consume. The generator was
reachable from the command line while its input was not. The TS
CLI still lacks the key; that is a parity follow-up, not drift
introduced here.
An unknown package name is a ConfigError naming the valid ones when it comes from a
config file, and a silent skip when it comes through the API — matching TypeScript.
A name a human typed is a mistake worth failing on; an API caller asking for a package
this version does not ship should still load its own metadata.
Four gates ship with it, because each of these failed silently before:
* the embed is byte-compared against the canonical YAML (the drift pattern already
used for spec/metamodel/), so a stale generated module cannot reach a wheel;
* `extends` is asserted to FAIL without the opt-in and resolve 18 inherited fields
with it — the negative half is what proves the opt-in is doing the work;
* ADR-0024 FIX #1 is now enforced: build_llm_call_row's keys equal LlmCallBase's
effective fields, both directions. The ADR asked for this gate; it did not exist;
* the acceptance test RUNS the generated helper against a capturing recorder and
asserts every key it writes is a field the entity declares. The first version of
that test asserted the strings voRequest/voResponse appeared in the emitted source,
against a fixture declaring neither column — so it passed while blessing a helper
that raises on its first write. That is the same bypass ADR-0024 warns about,
reappearing inside a test written to prevent it. A substring assertion over
generated code is not an end-to-end test.
Not addressed, and documented rather than changed: the opt-in also brings the library's
own concrete LlmCall entity (table llm_call) alongside the abstract base, so it appears
in codegen output and in a schema diff unless filtered. library/ai/llm-call.yaml is
shared by every port, so splitting it is a cross-port decision.
1639 tests pass; ruff clean on the changed files; no new mypy errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018k3CqBZLFkbP4Qs4FZhh96
This was referenced Aug 21, 2026
dmealing
added a commit
that referenced
this pull request
Aug 21, 2026
…nsumes it has an input (#332) The Java port shipped `LlmTraceHelperGenerator` and no way at all to load the metadata it exists to consume: no `libraries` loader option, no embed of the canonical `library/` tree. A JVM adopter following the documented `extends: "metaobjects::ai::LlmCallBase"` path could not get past load. A generator shipped without its input. What let that survive is the more useful half. Every test of that generator declares its OWN `LlmCallBase` inline under a different package — the bypass ADR-0024 already names, "the green tests pass only because they bypass the shipped base with bespoke entities" — so the suite could not tell a world where the library loads from one where it does not exist. A hand-copied base also drifts silently: the copies stay green while the shipped file moves. Three pieces, each mirroring TS and Python rather than inventing a third shape: - `EmbeddedLibrary` — a generated CLASS of string constants, not a `src/main/resources` copy. A resource can be dropped or mangled by build configuration (resource filtering, shading, repackaging) and the failure would surface much later as ERR_UNRESOLVED_SUPER against the adopter's own metadata; a class constant cannot go missing without the class going missing. Same rationale Python records for embedding as a source module. Emitted by the EXISTING `scripts/generate-embedded-library.ts` rather than a second script — two scripts walking one tree are two things that can drift, and an embed's whole job is to be byte-identical to its source. - `LibrarySources` — on-disk-first (a checkout picks up edits to the canonical YAML with no regeneration), embedded fallback (every consumer of the published jar). An unrecognised package contributes no sources and is NOT an error here: that is the cross-port contract for the programmatic door, so a caller asking for a package this version does not ship can still load its own metadata. - The opt-in: `MetaDataLoader.setLibraries(...)`, a `fromDirectory(..., libraries)` overload, `LoaderConfiguration.getLibraries()` (a `default` method — this interface is the build-tool seam and an implementor outside this repo must keep compiling), and a pom `<loader><libraries><library>ai</library></libraries>`. Libraries load BEFORE the project's own sources: super resolution is order-independent, so that is determinism rather than correctness, but a load order that differs per port is the kind of difference that only shows up in someone else's bug report. An unknown name in a POM is a HARD failure listing the packages this version ships, matching what the TS and Python config readers do. The asymmetry with the programmatic door is deliberate and is the same one they draw: a name a human typed into a build file is a mistake worth failing on, because skipped it resurfaces as ERR_UNRESOLVED_SUPER pointing at the module's OWN metadata — the wrong place to send someone looking. Gated by three things a green suite could not previously distinguish: - `LibraryLoadTest` — the positive arm, the NEGATIVE arm (without the opt-in the same model must still fail, and the assertion names `LlmCallBase` rather than merely requiring non-empty errors: the loader wraps the real diagnostic in a "Failed to load from directory <path>" envelope that names nothing, and the first draft of this assertion passed on that envelope), and the freshness gate. - `TraceHelperOnShippedLibraryTest` — runs the generator against the SHIPPED base, with ADR-0024 FIX #1 asserted BOTH directions. One direction alone is worthless: "every row key is a field" passes on a recorder that writes nothing, "every field is a row key" passes on one that writes the whole world, and only the equality says the two agree. It is also the claim a hand-copied base can never make, because it compares a copy against itself. - The freshness gate was proven by BREAKING it — appending a line to `library/ai/llm-call.yaml` turns it red with "EmbeddedLibrary is stale for ref …; run: bun run scripts/generate-embedded-library.ts", then restored. metadata + maven-plugin + codegen-spring: 202 + 34 + all green, 0 failures. C# has the same gap and is not addressed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
meta initscaffoldsmetaobjects.config.tswithoutDir: "src/generated"instead of"./src/db".Why
The old default put generated files in
src/db/right next to thesrc/db.tsthe user must hand-create for their DB instance — same path stem, two different things. Worse, the generated routes'import { db } from "../db"(fromdbImport: "../db") can resolve to the generated barrelsrc/db/index.tsinstead of the user'ssrc/db.ts.docs/ports/typescript.mdalready usessrc/generated; this aligns the scaffold with the docs so generated code and the user's db module stay clearly separated.Found while building the five-minute quickstart demo end-to-end against published
0.9.0(the demo otherwise works great —meta init → gen → Fastify → curlreturns 201/200 JSON + 400 Zod validation).Changes
cli/src/commands/init.ts— scaffold templateoutDir→src/generated.cli/test/init.test.ts— new test asserting the scaffoldedoutDir(and that./src/dbis not used).Test
bun test packages/cli→ 276 pass, 0 fail.🤖 Generated with Claude Code