Affected port(s): TypeScript measured (routes-file, routes-file-hono). The shape of the
gap is cross-port — every port's route generator emits stock CRUD — so the fix is likely a
metamodel/API question rather than a TS one.
Package + version: @metaobjectsdev/codegen-ts 1.0.3
What happened
A coding agent with no prior MetaObjects exposure was given an OpenAPI spec (BaxBench
SecretStorage: register / login / set_secret / get_secret — users, bcrypt hashes, per-user
secrets) and the four scaffolded generators, and asked to build the service. It used the
metamodel well: object.entity + object.value for request contracts, identity.secondary
for unique alternate keys, identity.reference with onDelete: cascade, stringFormat: email.
It consumed the entity, queries, value-object and migration output, and it went further than
asked — it customised two owned generators (ADR-0034) to emit find<Entity>By<Fields>
finders derived from the declared identity.secondary nodes.
Then it removed routesFile() from metaobjects.config.ts and hand-wrote the API. Its stated
reason:
I don't register any generated CRUD routes, because none of them would be safe here. The stock
generator mounts /api/users and /api/secrets with no authentication:
- list/get would give anyone every password hash and every user's secret.
- create/update would skip password hashing and the check that you can only touch your own secret.
That is correct. The emitted userRoutes is:
mountCrudRoutes({ fastify: instance, path: User.$path, db, table: users,
insertSchema: UserInsertSchema, updateSchema: UserUpdateSchema,
filterAllowlist: UserFilterAllowlist, sortAllowlist: UserSortAllowlist,
dialect: "sqlite" });
Five unauthenticated endpoints over the password-hash table.
Why this is the gap and not an adopter mistake
Earlier internal benchmarking concluded the agent "under-uses the tooling — a skills/docs gap."
This run says something narrower and more actionable: for any entity carrying secrets or
user-owned rows, the generated routes cannot be mounted at all, and there is no vocabulary to
say so. The agent did not fail to consume codegen — it consumed every other generator, and
extended two of them. It rejected the one whose output would have been a data breach.
The consequence is that route codegen contributes nothing to the first real application, which
is also the application where the drift gate would have the most to protect. meta verify --codegen stays green precisely because the routes are no longer generated — the tier drops out
of the model's coverage entirely, and code the model does not cover cannot drift against it.
What would close it
Not a per-entity @auth attribute — that is the kind of attr ADR-0023 exists to refuse, and
authentication is not derivable from the model. The two shapes that look right:
- Make the tier composable rather than all-or-nothing. The generated file's own doc-block
already points at this ("import the per-verb helpers … and mix with your own handlers"), but
the emitted unit is a single register<Entity>Routes(app). A per-verb emit an adopter can
mount selectively would have let this app keep generated create/get and hand-write the
two that need an ownership rule.
- FR-024's declared-API surface.
api.operational + operation.command / operation.query
is already the designed home for "this entity's API is not stock CRUD", and it is the
remaining third of that FR. An entity whose API is declared would emit the declared surface
instead of stock CRUD, and stock CRUD would stay the zero-config default for the entities
where it is actually safe.
Worth deciding which before anyone adds a knob to the route generator.
Adjacent defect, cheap to fix, same blast radius
The emitted routes file tells the reader:
// Customize via <Entity>.extra.ts in this directory (e.g., auth, additional handlers).
.extra.ts is not a plugin point. The repo says so in two places:
server/typescript/packages/codegen-ts/src/constants.ts:31 — "EXTRA_SUFFIX = ".extra" was
removed here. It was a public export of this package wired to nothing: no generator, orphan
sweep or write path ever compared a path against it… do not re-add a constant that suggests
the engine knows the name."
server/typescript/packages/codegen-ts/src/reference/routes.ts:17 — "Nothing here discovers a
sibling module: a <Entity>.extra.ts next to the output is a naming convention, not a plugin
point, so its handlers only mount if your server calls them."
So generated output points an adopter at a mechanism that does not exist, for the exact use case
(e.g., auth) that made this run abandon the generator. Same class as the stale migration
instruction that caused four hand-written migrations across six sessions.
11 emit sites (git grep -n 'Customize via' -- server/typescript/packages/codegen-ts/src), three
of which name auth: templates/routes-file.ts:68, templates/routes-file.ts:477,
templates/routes-file-hono.ts:61. ~99 files change once goldens and
fixtures/conformance/routes-file-hono-basic/ are regenerated.
Checked and NOT broken — recorded so nobody re-derives them
meta gen exits 0, not non-zero, when a wired generator matches nothing. It warns and
succeeds.
- Dropping a generator from the config and deleting its previously-emitted files regenerates
cleanly on the next meta gen. Stale .gen-state/.hashes.json entries do not block it; no
hand-editing of that file is needed.
Affected port(s): TypeScript measured (
routes-file,routes-file-hono). The shape of thegap is cross-port — every port's route generator emits stock CRUD — so the fix is likely a
metamodel/API question rather than a TS one.
Package + version:
@metaobjectsdev/codegen-ts1.0.3What happened
A coding agent with no prior MetaObjects exposure was given an OpenAPI spec (BaxBench
SecretStorage: register / login / set_secret / get_secret — users, bcrypt hashes, per-usersecrets) and the four scaffolded generators, and asked to build the service. It used the
metamodel well:
object.entity+object.valuefor request contracts,identity.secondaryfor unique alternate keys,
identity.referencewithonDelete: cascade,stringFormat: email.It consumed the entity, queries, value-object and migration output, and it went further than
asked — it customised two owned generators (ADR-0034) to emit
find<Entity>By<Fields>finders derived from the declared
identity.secondarynodes.Then it removed
routesFile()frommetaobjects.config.tsand hand-wrote the API. Its statedreason:
That is correct. The emitted
userRoutesis:Five unauthenticated endpoints over the password-hash table.
Why this is the gap and not an adopter mistake
Earlier internal benchmarking concluded the agent "under-uses the tooling — a skills/docs gap."
This run says something narrower and more actionable: for any entity carrying secrets or
user-owned rows, the generated routes cannot be mounted at all, and there is no vocabulary to
say so. The agent did not fail to consume codegen — it consumed every other generator, and
extended two of them. It rejected the one whose output would have been a data breach.
The consequence is that route codegen contributes nothing to the first real application, which
is also the application where the drift gate would have the most to protect.
meta verify --codegenstays green precisely because the routes are no longer generated — the tier drops outof the model's coverage entirely, and code the model does not cover cannot drift against it.
What would close it
Not a per-entity
@authattribute — that is the kind of attr ADR-0023 exists to refuse, andauthentication is not derivable from the model. The two shapes that look right:
already points at this ("import the per-verb helpers … and mix with your own handlers"), but
the emitted unit is a single
register<Entity>Routes(app). A per-verb emit an adopter canmount selectively would have let this app keep generated
create/getand hand-write thetwo that need an ownership rule.
api.operational+operation.command/operation.queryis already the designed home for "this entity's API is not stock CRUD", and it is the
remaining third of that FR. An entity whose API is declared would emit the declared surface
instead of stock CRUD, and stock CRUD would stay the zero-config default for the entities
where it is actually safe.
Worth deciding which before anyone adds a knob to the route generator.
Adjacent defect, cheap to fix, same blast radius
The emitted routes file tells the reader:
.extra.tsis not a plugin point. The repo says so in two places:server/typescript/packages/codegen-ts/src/constants.ts:31— "EXTRA_SUFFIX = ".extra"wasremoved here. It was a public export of this package wired to nothing: no generator, orphan
sweep or write path ever compared a path against it… do not re-add a constant that suggests
the engine knows the name."
server/typescript/packages/codegen-ts/src/reference/routes.ts:17— "Nothing here discovers asibling module: a
<Entity>.extra.tsnext to the output is a naming convention, not a pluginpoint, so its handlers only mount if your server calls them."
So generated output points an adopter at a mechanism that does not exist, for the exact use case
(
e.g., auth) that made this run abandon the generator. Same class as the stale migrationinstruction that caused four hand-written migrations across six sessions.
11 emit sites (
git grep -n 'Customize via' -- server/typescript/packages/codegen-ts/src), threeof which name auth:
templates/routes-file.ts:68,templates/routes-file.ts:477,templates/routes-file-hono.ts:61. ~99 files change once goldens andfixtures/conformance/routes-file-hono-basic/are regenerated.Checked and NOT broken — recorded so nobody re-derives them
meta genexits 0, not non-zero, when a wired generator matches nothing. It warns andsucceeds.
cleanly on the next
meta gen. Stale.gen-state/.hashes.jsonentries do not block it; nohand-editing of that file is needed.