Skip to content

0.24.5 / 7.24.5 — codegen ownership is the framework story

Choose a tag to compare

@dmealing dmealing released this 30 Aug 15:54
· 567 commits to main since this release

All four registries publish, and not one of them is a version-parity bump — each carries a
changed product file of its own (npm: FR-040 + the eight-defect batch; PyPI/NuGet/Maven:
columnNamingStrategy, @column, the Maven <sourceDir> fix, the C# hash-manifest anchor, and
the agent-context staleness nudge in every port). That the first release under publish-what-changed
publishes everything is a coincidence of what landed, not the rule reasserting itself.
metamodelVersion stays 0.13: no registered vocabulary changed.

A registry now publishes only when it changed. The version-parity rule standing since
0.20.13 is retired, and this is the first release cut under its replacement — so the four
registries may legitimately carry different numbers from here on.

Changed — publish what changed; converge the number when you do

docs/RELEASING.md contradicted itself five lines apart. It mandated "every release bumps
all four registries, with version-parity bumps where a port has no changed file"
and then stated
that the conformance corpus + CAPABILITIES.json"not a shared version" — is the coordination
point. Both cannot be the rule. If the shared version is not what carries the cross-language
guarantee, publishing byte-identical content to three registries to keep it aligned buys nothing;
the phrase "version-parity bump" appears ten times in this file paying for it. 0.24.3 and
0.24.4 were each a single changed file in cli, and each became a four-registry event.

The new rule: a registry publishes only when it has a changed product file, and when it does it
adopts the current shared minor.patch
, skipping the numbers it sat out. Two carve-outs, because
only ONE lockstep relaxes — the 14 npm packages still move atomically with each other (they
cross-depend), and a change to expected-registry.json / metamodelVersion still forces all
four
, because that is the cross-port contract every port byte-matches.

A lagging version becomes information: PyPI at 0.24.4 while npm is at 0.24.7 says PyPI has had
no product change since 0.24.4. Under parity that was unreadable, because every registry carried
the same number whether or not anything in it had moved.

scripts/release-verify.mjs gains --registries=npm,pypi,nuget,maven so a cut verifies only what
it published; without it the script reports a red ✗ for a port behaving exactly as the rule
requires, and a gate that fails on correct behaviour is one people learn to run with their eyes
closed. The default stays all four — forgetting the flag over-checks rather than under-checks — and
a scoped run names the registries it did not check on every invocation, because a partial run
printing only ✓s reads exactly like a full one.

Fixed — a context newer than the install is not stale (all five ports)

The rule above broke the agent-context staleness nudge, in every port, and the fix ships in the
same release. A port now legitimately sits behind npm — while meta agent-docs, the canonical
scaffolder for every port, stamps the npm version it ran from. So a Python install at 0.24.4
whose context was scaffolded by npm 0.24.7 is correct, and every port nudged it; running the
suggested remedy re-stamps 0.24.7, so the advisory could never be satisfied and fired on every
build forever. That is #347 exactly,
reborn in four ports at once — the JVM through stalenessAcrossVersionLines, and TS / C# / Python
through plain equality on their own 0.x line. One bug, three different routes.

A context stamped by a strictly newer release is no longer treated as stale. The suppression is
deliberately narrow because it contradicts a documented decision — all three non-JVM ports carried
"Don't 'fix' this into a semver compare", written when install and scaffolder always matched.
That property is preserved exactly: anything not orderable as a plain N.N.N still nudges —
prereleases, build metadata, non-numeric versions, and the 0.0.0 unresolved-install sentinel,
which must never assert "in sync".

Two bounds, stated rather than hidden. Ordering on minor.patch assumes both versions sit in
the same release series; across the one-time 1.0/8.0 cut a 0.24.x context against a 1.0.0
install reads as "ahead" and the nudge is suppressed once — a missed advisory, never a wrong action.
And the opposite failure is narrowed, not closed: equal coordinates still assert in-sync, so a
port parked at 24.4 across several npm releases is not told its context has moved. Settling that
needs the shipped context hashed rather than its version compared, and the JVM ships no
agent-context content — only the manifest reader.

Added — FR-040: codegen ownership is the framework story

MetaObjects does not need a codegen package per framework. It needs the ownership doctrine
it already publishes to be true all the way down
— and in three places it was not. A codegen
library cannot chase frameworks: there are more of them than any library can carry, they turn
over faster than a release line, and each one added is a permanent liability the metamodel —
the actual durable asset — gains nothing from. The shipped agent context already says the
right thing ("treat this as a first-class, expected activity — not an escape hatch"). This is
the release that makes it true. The bar is not "Next.js works": it is that an agent adopting
onto a stack nobody wrote a recipe for — Svelte, Nuxt, Qwik — reaches a working generator
unaided, and treats having done so as normal.

How it was found, and the framing error worth recording. A cold adoption probe ran the
documented TypeScript quickstart against published 0.24.4 inside a Next.js 16 / React 19 app
on Turbopack with a real Postgres. Its substantive result was positive and is not in
dispute
: the schema and server tiers work on that stack unmodified — queriesFile() takes
db as a parameter, so a Server Component calls it with no HTTP hop; routesFileHono() is
deps-injected, the shape an App Router Route Handler wants; the full cross-port API contract
held. The probe then reported eight "findings", and most were mis-framed as defects. The
default templates target Fastify on Node; that they do not emit Next.js output is the
templates doing what they say. Recording that error is the point — a report that reads a
template mismatch as a product failure will keep proposing framework packages forever. Three
findings survived re-scoring, and they are what shipped.

  • meta eject <generator> — the ownership move is now a command. ADR-0034 had meta init
    eagerly copy four generators (entity, queries, routes, barrel) into
    codegen/generators/ and own them. The other five could be used but never owned: there
    was no supported way to get one's source, at init time or after. meta eject is that same
    copy operation generalised to every ejectable name in every package, callable at any time —
    for a generator you skipped at init, or one a package gained since. meta eject --list
    names all nine grouped by package (entity, queries, routes, routes-hono, barrel
    from codegen-ts; form from codegen-ts-react; hooks, grid, grid-hook from
    codegen-ts-tanstack). It never clobbers without --force, matching init's rule, and it
    reports the import line to paste by parsing it out of the template's own header rather
    than re-deriving it from the file name — a generator's exported symbol does not follow its
    file name (hooks.ts exports tanstackQuery, routes-hono.ts exports routesFileHono),
    so a derived map would drift from what the template already tells a human to paste.

  • The UI tier became ownable at all. It was the gap with no workaround: an RSC app needs
    "use client" at the top of generated form/hook files, and there was no seam to add it —
    the choice was use the package's output verbatim or hand-write the tier and leave metadata
    behind. Four new reference templates (form, hooks, grid, grid-hook) plus a new
    routes-hono, and the render layer is promoted to public APIrenderFormFile,
    renderHooksFile, renderColumnsFile, renderGridHookFile, each a stable
    (entity, ctx) => string — so an owned generator composes the engine and replaces only the
    step its framework disagrees about, instead of copying ~600 lines of package internals out.
    codegen-ts already exported renderRoutesFile; this applies the same pattern to the UI
    packages. The reference-template reader is now a per-package factory, so a package that
    gains templates later is picked up by meta eject with no other change.

  • Every reference template documents its own swap point. A targets: header block on all
    nine states what the template targets, when to use it, what it emits, where to change it,
    and what it composes with — so an agent retargeting reads the seam out of the file rather
    than inferring it. Paired with a new "Your framework isn't the default — the retargeting
    procedure"
    section in the metaobjects-codegen skill, which is the general answer the
    per-framework recipes were standing in for.

  • clientDirective — the "use client" knob. Set it and the four generated client
    artifacts (form, hooks, columns, grid-hook) get the directive React Server Components
    frameworks require; leave it off (the default) and output is byte-identical to before.
    It is config, never a metadata attribute: the directive is a fact about the
    adopter's bundler topology, not about the entity, and registering it would give every
    non-TS port vocabulary it can never dispatch on — the source.rdb @role mistake that
    retired four members in 0.21.0. It defaults off because the directive is only
    required under RSC and is inert-but-warned-about elsewhere; the asymmetry that would
    argue for defaulting on (a runtime error for RSC adopters versus a build warning for
    everyone else) is precisely what the rest of this release removes — before it, an RSC
    adopter had no seam at all. <Entity>.meta.ts is deliberately excluded: it is plain
    data imported by a client component, and in RSC the boundary is the importing
    component, not everything it reaches.

Docs stop promising what the project does not intend to ship. AGENTS.md and the port
docs described a first-party package per framework as the way to reach a new framework; that
was never the plan and is now stated as the ownership move it actually is. The agent-facing
quickstart in docs/llms/ had been teaching the deprecated @metaobjectsdev/codegen-ts/generators
import path, and the ownership one-liner samples in the docs did not type-check — both fixed,
with the samples now compiled.

meta init scaffold honesty. Four fixes where the scaffold said something untrue about
its own output, the same class the 0.24.4 line was cut for: it stopped eagerly scaffolding
an unwired routes-hono.ts (a file nothing imported, presented as if it were live); it now
scaffolds a throwing src/db.ts stub, so the module the default routesFile() imports
exists and fails with an instruction rather than TS2307; it names the .gen-state manifest
and explains dbImport instead of leaving both as unexplained config; and its fastify
devDependency is aligned to runtime-ts's peer range, which it contradicted. Separately, the
TanStack Table v8 requirement is now discoverable@metaobjectsdev/tanstack bounds the
peer at v8, but nothing told an adopter installing @tanstack/react-table themselves.

One recipe, explicitly a convenience. docs/recipes/nextjs-vercel.md
walks the Next.js App Router + Vercel path — the extStyle: "none" / clientDirective: true
config delta, routesFileHono() mounted at app/api/[[...route]]/route.ts via hono/vercel,
and generated query helpers called straight from a Server Component. It changes no package
file, and it opens by saying the general procedure lives in the metaobjects-codegen skill:
it is a shortcut past reasoning an agent could do unaided, which is the only relationship
FR-040 permits it to have. Two of its notes exist because the failure is SILENT — a Server
Component reading the database is not a dynamic signal, so the page prerenders at build and
serves build-time rows forever while looking correct in next dev; and apiPrefix is baked
into the emitted route path, so a Hono basePath on top double-prefixes it.

Design: docs/superpowers/specs/2026-08-29-fr-040-framework-agnostic-codegen-ownership-design.md.
Amends ADR-0034.

Review round. Five of the fixes above are review findings on FR-040's own first
draft, and one is worth naming because it is the shape this project keeps convicting
itself of: the five new templates shipped with no equivalence gate. ADR-0034 makes a
copyable template safe by running it and the built-in it was copied from over a fixture
corpus and requiring byte-identical output — and that gate covered only the four
meta init scaffolds. Since src/reference is excluded from tsconfig, the five new ones
were imported by nothing, executed by nothing and type-checked by nothing: a renamed
engine export, or a drifted filter deciding WHICH entities emit, would have reached an
adopter running meta eject before it reached a red lane. The tsconfig comment even
asserted the coverage, having been copy-pasted into the two UI packages from the one
where it was true. Every template is now gated in every package, the file SET is
compared as well as the contents (a drifted filter changes what is emitted, not how), and
each gate asserts its own coverage equals REFERENCE_GENERATOR_NAMES so the tenth
template cannot repeat this. Also fixed from that round: meta eject told you to paste
an import that collides with the package import already in the documented config — whose
quiet failure mode is a config that keeps running the PACKAGED generator while you edit
the ejected file — and never named the dependency the ejected file imports, so the
adopter's own tsc reported TS2307 on the file the CLI had just said they owned.

Second review round. Seven more findings, and the two worth naming share a shape with
the first round's: a change that was RIGHT drew its line one notch too wide, and nothing
could see the difference. The retargeting split took four ports' own*() guidance with
it.
Moving meta eject and the metaobjects.config.ts keys out of the port-agnostic
SKILL.md was correct — a Python project runs metaobjects gen and has no eject command,
so those adopters' agents were being handed a procedure their toolchain cannot execute.
But the same move carried off the ADR-0039 section, whose per-port own↔resolving table
is port-agnostic by construction
: its entire content is the OTHER ports' accessor names,
including the trap that TS attr() resolves while Python attr() is own. It landed in
references/typescript.md, the one page a Java, Python or C# adopter never installs,
while metaobjects-authoring still told them to go read it there. And SKILL.md closed
by sending every reader to "this skill's references/ fragment for your server language",
which for four of the five contains no retargeting content at all — a pointer to nothing,
where before the split there had at least been a procedure (a wrong one, which is what the
split fixed). The three port-agnostic sections are back in SKILL.md, and the closing
pointer now says what is true per port, including that the other ports have no eject
command and owning a generator there means implementing that port's generator interface.

meta init claimed a stub it had just decided not to write. Its next-steps block was
one static string describing the scaffolded src/db.ts in the imperative, printed on every
run — including the meta init --force in a project keeping its own config, where the
write is deliberately skipped. Worse was the silence beside it: wroteScaffoldedConfig is
only "no config existed", so it is false for the config init itself wrote one command
earlier, and a scaffolded project whose src/db.ts is deleted lands in the same branch as
a project that owns its config — nothing written, nothing preserved, nothing said, and
the next tsc reporting TS2307 on a file the command had just chosen not to restore. It
warns now rather than writing, because dropping a file into a project that owns its config
is the unilateral host-project touch FR-040 §4.4 lists as a defect.

Also from the round: meta eject stated which import a project currently has, which
it never checks and which is wrong for exactly the four meta init scaffolds — the
--force re-sync case — so it names the goal and the three branches instead; its
dependency notes could not see a subpath import and reported a peer-declared package as
missing
, advice that if followed adds the competing physical copy this repo has been
bitten by twice; and nothing gated clientDirective on the generated form, the one
client artifact the RSC story centres on. That last one is the round's own theme again,
and the proof is sharper than the finding: with the directive dropped from a reference
template alone — the two halves genuinely different — reference-byte-identical stays
green, because it generates with the knob off, where the call is a no-op and removing
it changes no byte it compares. The equivalence gate cannot see that defect in either
direction. Both UI packages now run the knob against the built-in and the reference
template, and each package's template coverage is a Record keyed by ejectable name whose
keys are the proof and whose values are the wiring — so a tenth template is a compile
error, not a hand-maintained list that can be edited to claim a gate nobody wrote.

Fixed — eight defects found by adopting the product from scratch, twice

Two adoption runs against the published 0.24.4, docs followed literally and nothing fixed
mid-run: a greenfield app built with MetaObjects, and a realistic hand-written app
(Drizzle + Zod + Fastify + string-concatenated prompts, a populated three-table database)
migrated onto it. What the two runs proved out is worth stating first — the drift gate found
a real pre-existing bug in the hand-written app the moment its prompt was declared
(a book.status reference surviving a rename to shelf_state, invisible to tsc), the
filter layer was correct throughout, and swapping hand-written routes for generated ones
preserved the wire shape exactly. Everything below is what went wrong on the way.

  • BLOCKER — adopting an existing SQLite database emitted a migration the tool could not
    apply.
    Any rebuilt table that another populated table references failed with
    SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed — which is most real
    schemas, on the scaffold's default dialect, along the documented adoption path. The
    0.21.4 fix was present and on this path, rewriting the file's PRAGMA foreign_keys = OFF
    to PRAGMA defer_foreign_keys = ON because the former is a no-op inside a transaction.
    Deferral is not a substitute for this recipe: DROP TABLE's implicit delete records a
    deferred violation per referencing row, the repair is ALTER TABLE … RENAME TO — a rename,
    never an insert — so the counter never decrements and COMMIT fails. Proven with a
    controlled pair on identical database copies. The pragma is now issued outside the
    transaction, which is SQLite's own documented procedure, and restored in a finally;
    PRAGMA foreign_key_check is lifted out to run after commit, where its rows are no longer
    discarded. Every existing rebuild test was blind to this because they all rebuild a table
    nothing references.
  • The schema-snapshot gate's printed remedy was a no-op. It named
    meta migrate --from-db, which writes a snapshot only when it has changes to emit — so on
    a database already matching the metadata it reported "nothing to do", wrote nothing, and
    the gate failed again identically. The user follows the instruction, is told everything is
    in sync, and is stuck in a loop. It now names meta migrate baseline --from-db, and the
    new test parses the command out of the message, runs it, and requires the gate to pass
    so any future edit naming a command that does not repair fails, whatever it says.
  • The strict response parser validated 3 of 11 field subtypes. enum, uuid, date,
    time, timestamp, decimal, currency, uri and inet all fell through to
    z.unknown(), which accepts anything — including null on a @required field. The map
    carried class, short and byte, the three subtypes this project cut as non-functional
    stubs, and missed currency and uuid. This inverted the pillar: validation was strongest
    on the payload we control and absent on the reply we do not. It was an internal
    contradiction, not a gap — the tolerant extractor in the same generated file rejects a
    non-member, and so does Python's FieldSpec.enum_field; only the path named parse<Name>
    and documented @throws on validation failure threw the domain away.
  • A declared template.prompt with no prompt generator wired emitted nothing and said
    nothing
    , while meta verify reported the template "clean". The payload value objects
    are emitted (they are object.value nodes), so the run looks like it worked. meta gen
    now warns, naming the templates and distinguishing the missing send side from the missing
    receive side — self-extinguishing, following the layout.dataGrid precedent (#287).
  • A constraint violation returned 500 with the SQL and its bound parameter values in the
    response body.
    Wrong status for a client error, and on a POST carrying PII or a token
    that is user data reflected to an unauthenticated caller out of generated code. Now a 409
    ({"error":"constraint_violation","constraint":"foreign_key"}) with no query text;
    unrecognised driver errors are logged in full and rethrown redacted. Classification walks
    the cause chain, because Drizzle wraps every driver error in a DrizzleQueryError whose
    own message is the query — reading only the top level matched nothing at all, which the
    first cut of this fix did until it was run against a live server.
  • Three generators spelled the same template three waysrenderTriageTicket,
    parsetriageTicket, type triageTicketData — because one applied a private pascal() and
    two concatenated the raw name. A single templateSymbolBase() in naming.ts now owns it.
    Adopter-visible: a template whose name begins lower-case (the spelling the prompts
    skill's own examples use) gets renamed generated symbols; an UpperCamel name is unchanged.
  • The prompts reference's config example moved you off the owned generators, importing
    entityFile/queriesFile/barrel from the ADR-0034-deprecated package path.
  • meta init claimed the manifest "declared no module system" when npm init -y writes
    "type": "commonjs" explicitly — false on the dominant first-touch path. (Introduced by
    0.24.4's own fix to that same line, which corrected the tense and got the premise wrong.)

Fixed — @column is the physical column name, and four ports could not choose one

A field has two names — the one your code calls it and the one the database calls
it — and @column sets the second. The byte-gated registry prose has always said the
first comes "via columnNamingStrategy". Four of five ports could not set that
strategy, one port used @column for the wrong name entirely, and one ignored it.

  • Python's read model renamed itself to @column. <Entity>'s Pydantic field was
    @column or field.name while its own <Entity>Create / <Entity>Patch, its
    generated router (which stamps dto["createdAt"]) and its runtime all key by
    field.nameObjectManager builds {_column_of(f): f.name} on every read and
    RETURNING clause, commented "for cross-port row-shape parity". So one generated
    module disagreed with itself, and the read model's stated reason ("the Python model
    field IS the column, so it binds straight to the row") was never true of that
    runtime. It also leaked a free-form name: callPurpose + @column: purpose_code
    emitted a field purpose_code, neither the wire name nor derived from the field
    name. The read model now keys by field.name, like every other surface in every
    other port.
    Idiomatic snake_case for a Python consumer is a real goal and its lever
    is the strategy below, applied to field.name.
  • Kotlin's Exposed generator ignored @column outright, hardcoding
    camelToSnake(field.name) at every column site — so a field declaring one bound the
    WRONG column at runtime, silently, with no error anywhere and no escape hatch. It
    also made Kotlin the only port hardcoding snake_case while Java's getColumnRef
    resolved literal: one model, two column names, one JVM. Both now go through a
    shared com.metaobjects.database.ColumnNaming.
  • columnNamingStrategy is now selectable in the four ports that lacked it
    dotnet meta gen --column-naming, Python GenConfig(column_naming=…) /
    ObjectManager(…, column_naming=…), Java SimpleMappingHandlerDB.setColumnNaming(…),
    Kotlin's <columnNaming> generator arg. No default moves (TS/Kotlin-codegen
    snake_case, C#/Python/Java literal): a default that moved would silently
    re-point live queries at columns that do not exist. An unknown value is refused
    rather than falling back, because a typo would otherwise bind a whole schema to the
    wrong columns and report success. Why it matters: schema is Node-owned (ADR-0015)
    and meta migrate defaults to snake_case, so a C# or Python adopter with a
    multi-word field name generated data access against a column the migration never
    created — and until now could only fix it by declaring @column on every field.
    Documented, with the per-port default table, in docs/features/field-types.md.
  • The persistence corpus could not see any of it, and now can. Its canonical schema
    is pinned to literal and no fixture carried a @column, so every field's column
    name equalled its field name and nothing could tell a port that resolves @column
    from one that ignores it. Program.createdAt now declares @column: created_ts
    deliberately NOT the snake_case of the field name, which is what a snake_case
    strategy would produce anyway and would have proven nothing. Adding it turned up
    three live defects at once: the Kotlin generator above, plus the Kotlin and C#
    scenario runners both keying result rows by physical column instead of field name.
    All five ports' lanes pass against the de-blinded corpus.

Fixed — two ports scattering or skipping work while reporting success (Maven, NuGet)

Found the same way as the eight above, by a different exercise: generating ONE small model
with every port, from the repo root, to build the corpus the website publishes as real
meta gen output. Both are the shape this release keeps finding — a tool saying something
untrue about work it had just done.

  • Java/Kotlin: <loader><sourceDir> with no <sources> loaded NOTHING, ran the
    generators against an empty model, wrote zero files, and reported BUILD SUCCESS.

    The precedence ladder consults the port-neutral config only when the pom names neither
    key — naming either means the pom owns the concern — so <sourceDir> alone took the
    pom-owns-it branch with an empty source list. That is the shape the shipped adopter
    guidance teaches, and it is also the remedy 0.24.0's own ERR_COLLECTION_NOT_FOUND
    prints ("declare <sourceDir>/<sources> explicitly"), so following the fix for a
    silently-empty model put you back into one. A directory now expands through the SAME
    DirectorySource walk the loader itself uses for a directory source, so "which files
    count as metadata" keeps one definition; a <sourceDir> that exists but holds no
    metadata is ERR_COLLECTION_NOT_FOUND rather than an empty model, since reaching the
    expensive outcome by a different road is still the expensive outcome. A nonexistent
    <sourceDir> already failed and still does. Purely additive — a pom naming
    <sources>, or naming neither, is unaffected.
  • C#: dotnet meta gen anchored .gen-state/.hashes.json on the process working
    directory
    , so generating from anywhere but the project root wrote a stray
    .metaobjects/ into the caller's cwd and left the real project with no record of what
    was written — the entire reason that manifest is committed. The Python port already
    anchors on the metadata dir's parent and its docstring says why. The C# test assembly
    had been doing this into its own bin/
    for as long as the manifest has existed,
    invisible because bin/ is ignored, and worse than untidy: every test in the assembly
    then shared ONE manifest keyed by bare filename — precisely the collision that docstring
    warns about. An explicit <metadataDir> now anchors on its parent; with it omitted the
    .metaobjects/config.json ladder has already resolved relative to cwd, so cwd is the
    project. Not a behaviour change for the documented invocation — only the
    cwd-is-not-the-project case moves, and that case was broken.

Fixed — a retired requirement's generated test stub failed forever, then told you to revive it

The requirement-test renderer decides skip-versus-fail from a set of statuses, and that set
was a literal the vocabulary moved out from under. 0.24.0 retired abandoned and
superseded; 0.24.2 put retired in their place. The set was never moved across, so it
skipped two statuses the loader had begun REFUSING and failed on the one that replaced them:
every @status: retired entry emitted a stub asserting expect.unreachable forever,
reddening an application's suite for a capability nobody intends to rebuild. That is the noise
a suite gets silenced wholesale for — taking the live stubs with it — and it is the exact
outcome the set exists to prevent, stated in its own comment.

The set is now derived from the loader's own enum: a status that is neither live nor
partial is skipped by construction, so the next status move cannot leave it behind.
Restating a closed set next to the closed set is what produced this.

The second half was found by fixing the first. The two skipped statuses mean opposite
things and were sharing one body — "Intended, not built. Write the assertion when this becomes
live."
On a retired entry that instructs the reader to revive the capability, inverting
the one guardrail 0.24.2 restored retired for. A retired stub now states that the capability
was deliberately removed and must not be rebuilt, and that anything asserted there should assert
it STAYS removed.

Found by authoring the first real retired entries outside a conformance fixture. Every
existing test in the file passed throughout, because none of them used a status the vocabulary
had changed. TypeScript-only — the requirement-test renderer ships in codegen-ts and has
no counterpart in the other four ports.