Skip to content

1.0.4

Latest

Choose a tag to compare

@dmealing dmealing released this 14 Sep 14:19
· 1 commit to main since this release

All four registries publish: npm 1.0.4 (full lockstep across all 14 @metaobjectsdev/*
publish candidates), Maven Central 8.0.4, PyPI 1.0.4, NuGet 1.0.4 — every port has changed
product code. metamodelVersion stays 1.0.

Upgrading: run meta gen. Every generated file's header comment changed (#367), and
generated routes files gain an auth-seam JSDoc block, so meta verify --codegen reports
drift on any project that commits its generated output until it is regenerated. For most
projects that regen diff is comments only — zero non-comment lines. Two shapes change
for real: a model with two identity.reference nodes onto the same target gets corrected
FK wiring and referential actions (#368 — including an ON DELETE / ON UPDATE diff on
the next meta migrate), and an M:N relationship inherited through extends now derives
navigation that was previously dropped from the output with no error.

Added

  • Libraries: reusable declared design you opt into (FR-043, the sixth pillar).
    "libraries": ["iam"] in .metaobjects/config.json brings a shipped, requirement-backed
    model into your project. Two ship: iam (preview) — users, nestable typed groups,
    roles as permission bundles, grants global or scoped to a group, nine entities and eleven
    requirements — and ai (stable), the LLM-call trace envelope that already existed.

    A library is LAYERED, and the core layer is INERT. The core declares no source.rdb,
    and a sourceless object generates nothing and migrates to nothing (#248), so
    ["iam"] adds zero tables and zero generated code — the design is present and
    resolvable, and nothing else happens until you add ["iam", "iam/db"]. A layer token
    implies its core; a token whose layer is unknown is dropped whole rather than reduced to
    it, because answering a mistyped iam/database with an inert core and no tables is the
    worst of the available outcomes.

    Copy is the expected mode. meta eject <library> copies every layer into your first
    DECLARED source root with a provenance header, and meta eject --list reports how far
    your copy has drifted from the shipped tree — nodes changed, only-upstream, only-yours —
    matched by name with the package neutralized, because renaming the package is something
    you are invited to do. Ejecting and leaving the library in libraries is refused at load
    (ERR_LIBRARY_PACKAGE_COLLISION): both trees merge, and the merge is asymmetric —
    additions take effect, deletions do not. Its mirror ERR_LIBRARY_PACKAGE_NOT_OWNED
    refuses a NEW node declared into a library's package; an overlay: true amendment stays
    open. See libraries.md.

    meta gen --list carries kind: "library" rows beside the generators — one door, one
    namespace — with useWhen, layers, provides, and under --probe what your selection
    actually added here.

  • overlay: true licenses an attribute override. ERR_MERGE_CONFLICT now fires only
    on an UNMARKED conflicting redeclaration. The flag is the author saying "I know about the
    other declaration and I mean to change it", and without this an adopter could not disagree
    with a library's shipped requirement without ejecting the whole ledger. All four loaders
    (Kotlin inherits the JVM's); one new conformance fixture takes over the unmarked-conflict
    error branch, so the coverage moved rather than being deleted.

  • layer joins the cross-port generator manifestmodel / persistence / api /
    client / docs / capability — gated by all five ports' registry-conformance tests
    exactly as tier is. Five TypeScript generators that were in no manifest at all (form,
    hooks, grid, grid-hook, requirement-tests) join it too, so the catalog describes
    34 generators where the manifest described 29.

  • meta gen audits the selection. Two self-extinguishing warnings, neither a build
    failure: a wired generator whose requires are not wired (its output will import a
    module nothing emits), and two api-layer generators declaring different frameworks
    (routes + routes-hono emit to different paths, so nothing conflicts and two complete
    HTTP surfaces appear silently). There is deliberately no equivalent rule on the client
    layer: @metaobjectsdev/tanstack peers on react, so a form generator plus the TanStack
    hook/grid generators is the intended composition.

  • The auth seam is printed in the generated routes handler's JSDoc (#367). Stock
    CRUD is unauthenticated and no metadata attribute should change that — authentication
    is not derivable from a model (ADR-0023). Both frameworks already compose a guard
    around the generated mount; only the generated output never said so. Fastify hooks are
    encapsulated per plugin scope and inherited by child scopes, so wrapping the call
    reaches through the handler's own register(..., { prefix }); Hono's trailing wildcard
    matches the collection path itself, so one app.use covers list, get and every write.
    Both recipes are executed as tests (runtime-ts/test/route-auth-seam.test.ts) rather
    than asserted in prose, and each names the generator that emitted the file
    (routesFile / routesFileHono) for narrowing with expose. The JSDoc also states
    what no mount can express: a row-ownership rule is not a property of a route, so those
    verbs are hand-written and the generated file narrowed around them.

  • mountReadOnlyCrudRoutes accepts routeOptions (@metaobjectsdev/runtime-ts,
    Fastify), the option mountCrudRoutes and mountM2mRoute already took. A projection's
    generated routes could not carry a route-level hook the way an entity's could — an
    inconsistency, not a policy. Read-only is not public.

Changed

  • libraries moved to .metaobjects/config.json, out of metaobjects.config.ts,
    outright and with no dual-read — a sweep of the estate found zero uses of the key. Which
    designs a project adopts is a fact about the PROJECT, not about how one port generates
    code from it. A config still carrying the old key gets a pointed error rather than
    silence.

  • library/ai is SPLIT into model + db layers, and its requirements are new.
    Opting into "ai" alone no longer proposes CREATE TABLE llm_call — that moved to
    "ai/db". Breaking-ish for an ai adopter tracking the library: add "ai/db" to keep
    the table. The concrete-LlmCall wart was previously carried as accepted on the grounds
    that splitting would change what existing adopters get; the estate sweep found there are
    none, so it was closed rather than documented.

  • trace-helper keys on a declared ANCHOR, not a hard-coded entity name — and the
    name it hard-coded was never actually matching the shipped base. It compared
    "LlmCallBase" against the SHORT name, so any adopter entity of that name in any package
    emitted a helper writing columns that entity does not declare. It now resolves the anchor
    its library's manifest declares and compares by node identity, in all three ports that
    ship it. Two self-extinguishing warnings cover the halves of the choice: a library opted
    into whose implied generator is not wired, and a generator wired whose library is not.

  • Object coverage activates on ADOPTER-authored requirements only. A library shipping
    its own ledger would otherwise switch the unclaimed-entity gate on across a project that
    has never written a requirement. Library entries are still counted and still checked;
    they simply cannot volunteer you. meta verify prints coverage: not measured (no project-authored requirements) rather than a ratio, and the JSON omits the pair rather
    than zeroing it — 0/0 claimed and "not measured" mean opposite things.

    A project that already has a ledger sees its coverage denominator grow to include an
    opted-in library's entities. They are all claimed by the library's own ledger, so no new
    warnings appear, but the printed numbers move.

  • A shipped library's files carry a stable library:<ref>.yaml source id in every
    build.
    It was the file's basename in a checkout and library:… when embedded, so one
    node's error envelope read differently depending on how the library was resolved — and
    collided with an adopter file of that name.

  • Codegen is OPT-IN: no port ships a default generator suite (ADR-0034 Amendment 2).
    A new project got code it never asked for — TypeScript's meta init copied and wired
    five generators, C# ran nine for a caller who named none, Python eight; Java never had
    a default set and has been right all along. Deciding which code an application needs
    belongs to whoever is building it, increasingly an LLM in the repo, which is well able
    to make that call given a truthful catalog and is badly served by a default that
    pre-empts it.

    This is a PATCH and no existing project changes by one byte. An adopter already has
    their owned copies on disk and their selection committed in their own config; meta gen
    keeps running exactly that list, verify --codegen keeps checking exactly that output,
    and re-running init never clobbers a file that exists. What changes is what a new
    project starts with. docs/compatibility-policy.md is narrowed in the same change: the
    scaffold-and-own promise is the LAYOUT and the INTERFACES, not which generators a fresh
    scaffold wires.

    What this means per port:

    • TypeScriptmeta init scaffolds codegen/generators/ empty, a config with
      generators: [], and no dependencies. dbImport and the throwing src/db.ts stub are
      gone with it: both existed only because the scaffold wired routesFile(), whose output
      emits import { db } from …. dbImport is now a declared configKey on the routes
      catalog entry, reported by meta eject routes to the adopter who chose routes.
    • C# / Python--generators is REQUIRED; a run that names none is a usage error
      and writes nothing. verify --codegen re-runs the SELECTION, so with none named it
      reports that there is nothing to check rather than regenerating a suite the project
      never ran. Python's verify gains --generators, matching C#.
  • meta gen --list is now the generator CATALOG, and --probe answers it against your
    own model.
    --list --format json emits one document per generator: its layer,
    framework, what it emits, what it requires, the consolidated install set, the config
    keys it reads, and whether this project already owns a copy. --probe constructs every
    generator and dry-runs it against the loaded model, reporting how many files each would
    emit — so output-parser: 3, callable: 0, requirement-tests: 7 replaces a category
    label, and cannot go stale, because it runs the generators rather than describing them.

  • meta eject takes many names and reports one consolidated install set. A real
    selection is several generators, and three separate invocations produced three separate
    install lines for the same package. --format json carries, per file, the import line
    and the entry to wire, plus one install set with third-party ranges read from the runtime
    package's own peerDependencies. An unknown name refuses the whole call before writing
    anything.

Fixed

  • An M:N relationship inherited through extends derived its junction FK columns
    against the wrong entity — and in codegen the failure was silent.
    The derivation
    classified the self-join, and matched the junction's source-side
    identity.reference, against the entity the CALLER was iterating. Every caller walks
    a resolving relationship accessor, so for a relationship declared on a base and
    reached through a subclass that is the INHERITING entity, not the one that declared
    it. Two failures followed: an inherited self-join compared @objectRef (the base)
    against the child, read as hetero, looked for a junction reference to the child and
    found none; and an inherited hetero whose junction references the base found nothing
    either. TypeScript codegen and the docs-site link graph catch the resulting error and
    return null, so the navigation was dropped from the generated output with no
    error at all
    ; C# codegen did the same; the TypeScript, Java, Kotlin and Python
    runtime and codegen paths let it escape, so the traversal or the generation run
    failed outright.

    The declaring entity now comes from the relationship's own parent — the same shape as
    #368's loader fix — in all
    four derivations (TypeScript, Java, C#, Python; Kotlin calls the Java helper). The
    entity being navigated from is kept alongside it rather than discarded: under
    inheritance both are legitimate names for the relationship's subject, because a
    junction FK usually references the concrete child while @objectRef on a hoisted
    self-join names the base. The authoring contract this establishes — the junction FK
    may reference either the declaring base or the concrete child, and only those two

    is now written down in
    docs/features/relationships.md.

    C# additionally fixes M2MNavigation.IsSelfJoin, which had the same confusion one
    layer up. It never ran on an inherited self-join before (the derivation threw first),
    and DbContextGenerator uses it to decide whether to emit EF UsingEntity wiring —
    so fixing only the derivation would have turned a silent drop into silently wrong EF
    configuration.

    Not a pure widening. One shape that derived before now refuses: a base declaring
    @objectRef: <itself> + @through with neither @symmetric nor @sourceRefField,
    reached through a subclass, used to be misread as hetero and returned an arbitrary FK
    direction; it is now correctly recognised as an ambiguous self-join and refused. That
    model was already broken — deriving the same relationship from the base itself threw —
    so codegen emitted for the child and dropped it for the base. The refusal is the
    correct behaviour, but on Java, Kotlin and Python, whose callers do not catch, it
    moves from "generates wrongly" to "the generation run fails", and the fix is to add
    @symmetric or @sourceRefField.

    Three narrower resolution changes come with moving the junction matches onto identity,
    all three matching what the Java port already did. A junction @references (or an
    @objectRef) that is package-qualified must now resolve exactly: a
    partially-qualified or stale package no longer falls back to matching the bare tail, so
    a reference that used to bind by luck now matches neither the subject nor the
    target — both sides are affected, not just the subject side. A bare reference whose
    short name exists in more than one package resolves first-declared-wins, which can pick
    the wrong-package entity — the pre-existing
    #174 behaviour, now reached
    by M:N derivation as well. And on Python only, a junction whose @references use
    the dotted Entity.field form now resolves: that port compared the whole attr value, so
    Team.id never matched the entity Team and an M:N through such a junction failed
    derivation outright. Both junction matches now take the entity head through the same
    canonical parse the loader uses, so those models derive where they previously raised.

    Cross-port divergence goes DOWN, not up. Both junction matches — "does this
    reference name the relationship's subject?" and "does this one name the target?" — now
    resolve the name to an ENTITY and compare identity in all four derivations, which is
    what the Java port already did on both sides. Matching only one side would be worse
    than matching neither: the two searches are independent and nothing excludes the
    source-side reference from the target search, so a cross-package M:N could bind the
    same junction column as BOTH sides and emit (srcFk, srcFk) silently. C#'s
    M2MNavigation descriptor resolves its target the same way for the same reason — its
    IsSelfJoin feeds the EF UsingEntity wiring, and a descriptor that disagreed with
    the derivation would mis-map the relationship. TypeScript, C# and Python had been
    comparing package-stripped short names, so a genuine cross-package hetero M:N onto a
    target whose short name matched the subject's (a::NodeBase relating to b::NodeBase)
    was misread as a self-join on those three — a regression the two-name subject
    introduced, caught in review and fixed rather than documented.

    What changed, exactly: both junction matches in the four derivations, the C# navigation
    builder's own target/junction resolution (so the descriptor cannot disagree with the
    derivation feeding it), and Python's reference head-parse, which now delegates to the
    loader's canonical helper instead of keeping a third copy. @through resolution and
    every comparison outside M:N derivation are untouched, and this is not a general
    ADR-0041 sweep — the
    resolver added here is deliberately narrow and is not the port's general reference
    resolver.

    No vocabulary change: metamodelVersion stays 1.0 and the registry manifest is
    untouched.

  • Both shipped libraries failed meta verify's requirement gate, in metadata an
    adopter cannot fix: every L4 in ai claimed FIELDS (ERR_REQUIREMENT_L4_NOT_OBJECT),
    and both libraries wrote their concerns as SIBLINGS of the L2 segment their own comments
    said they were children of, leaving that L2 claiming nothing in its whole subtree. The
    load test proved they LOAD clean, which is a different claim, and nothing checked the
    other one. Both ledgers are fixed as the model intends — concerns nested under their L2,
    each L4 naming the OBJECT with its fields in an L5 child — and a new standalone gate
    holds every shipped library, and every future one, to zero loader errors, zero loader
    warnings, zero gate findings, zero lint findings, no unruled gaps, and every entity
    claimed by its own ledger.

  • Generated files no longer point at a plugin point that does not exist (#367).
    Every emitted header carried Customize via <Entity>.extra.ts in this directory, and
    routes files added (e.g., auth, additional handlers). Nothing in any toolchain imports
    a sibling module — the name is a convention, documented as one in
    own-your-codegen.md, and EXTRA_SUFFIX was
    deleted from codegen-ts long ago for implying otherwise. The cost was measured: an
    agent wiring authentication onto a real API read that line on a routes file, went
    looking for the seam, found none, and removed routesFile() from its config rather
    than mount five unauthenticated endpoints over a password-hash table. The line now
    reads "Extend in your own module (e.g. Order.extra.ts) — nothing imports it for
    you."
    , spelled once in sidecarLine (TypeScript) and generated_header (Python)
    rather than copy-pasted to eleven emitters.

  • Two identity.reference nodes onto the same entity no longer make every
    @cardinality: one relationship join the first one's FK column (#368).
    An entity
    may legitimately declare more than one FK to the same target — Match.homeTeamRef
    and Match.awayTeamRef both -> Team — but a relationship names only its target via
    @objectRef, never which reference it means. Four call sites took the first matching
    reference and never noticed the second: the TypeScript codegen relations() block
    wired the relationship to one FK column regardless, the runtime relation traversal
    resolved every such navigation through it, a projection's @via join hop picked it
    even when the hop explicitly named the other reference, and the docs-site link graph
    drew the wrong edge. All four produce a join that typechecks, emits correct DDL, and
    passes meta verify — the only symptom is wrong rows. The referential-actions
    correlation (TypeScript's migrate-ts, the C# port, and the JVM tree's Kotlin
    Exposed table generator; Python does not implement this correlation) had the same
    defect one level over: every FK past the first silently inherited the first
    relationship's @onDelete / @onUpdate instead of its own, so a model mixing
    restrict and cascade across two references to the same target emitted the wrong
    action on whichever FK wasn't examined first. All three are now correlated by
    inverting the same ladder, and fail closed rather than guessing.

    Resolution is now explicit and identical across TypeScript, Python, C# and Java: an
    ambiguous @cardinality: one reference set resolves by a ladder — the sole
    candidate, else a declared @sourceRefField naming the candidate's FK field, else a
    name-pairing match between the relationship's name and a candidate's name/FK field,
    else ERR_INVALID_RELATIONSHIP at load, naming every candidate
    (ADR-0029
    Amendment 1). @sourceRefField is now legal on a @cardinality: one relationship —
    it previously failed to load there as an M:N-only attribute. Three limits are
    documented rather than fixed here: the ladder matches a candidate's first FK field
    only, so two composite references sharing a first column stay indistinguishable (and
    resolve to the first, rather than being refused); the loader gate covers
    @cardinality: one relationships only — a many-cardinality relationship, or a bare
    identity.reference pair with no relationship wrapper, still reaches codegen
    unvalidated; and the gate needs at least one candidate on the holder, so an inverted
    shape with both FKs on the far side loads clean and codegen then silently drops the
    relation. See docs/features/relationships.md.

    Two consequences worth stating outright. Java's relationship validation changed
    from eager-throw-on-first-violation to collect-all-findings

    validateRelationshipsM2M now returns a List<MetaDataException> rather than
    throwing, and the new rule-(e) pass collects the same way, so a Java loader run
    reports every broken relationship where it previously reported only the first; that
    is a change to Java loader OUTPUT, not an internal refactor. And adopters with the
    affected shapes will see FK referential-action diffs on their next meta migrate
    :
    a second FK to the same target now resolves its OWN @onDelete / @onUpdate
    instead of inheriting the first relationship's, so the generated ON DELETE /
    ON UPDATE (and a Kotlin Exposed table's ReferenceOption arguments) legitimately
    change.

    No vocabulary was added, removed or retyped — the only change to
    expected-registry.json corrects @sourceRefField's own description, which no
    longer claims the attribute is M:N-only. Per
    docs/RELEASING.md, any change to that file forces all four
    registries (npm / PyPI / NuGet / Maven) to publish together at the next release,
    changed product files or not — that consequence is recorded here so it isn't a
    surprise at release time. metamodelVersion stays 1.0.