Skip to content

Commit 1f2fcc8

Browse files
committed
merge origin/main (FR-040 codegen ownership) into the site-gate work
2 parents 5826a95 + 14386ac commit 1f2fcc8

79 files changed

Lines changed: 4219 additions & 280 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ Worked examples: a Drizzle TS-server integration → `server/typescript/packages
150150
- `runtime-web/` (`@metaobjectsdev/runtime-web`) — pure framework-agnostic browser core (currency, filter-qs, EntityFetcher contract, GridConfig). Zero React, zero TanStack.
151151
- `react/` (`@metaobjectsdev/react`) — React runtime: `useEntityForm`, `<CurrencyInput>`.
152152
- `tanstack/` (`@metaobjectsdev/tanstack`) — TanStack runtime: `EntityFetcherProvider`, `<EntityGrid>`, default cell renderers.
153-
- Future: `angular/`, `svelte/`, `react-native/`.
153+
- MetaObjects does not add a first-party package per framework. React ships a codegen+runtime pair;
154+
Angular ships source-only (ADR-0048's promotion bar). Any other framework is reached by owning and
155+
retargeting the generators (FR-040), not by waiting for an official package.
154156

155157
### Framework integration: separate codegen and runtime packages
156158

@@ -175,7 +177,9 @@ Runtime side (browser): Codegen side (server):
175177
└── @metaobjectsdev/tanstack
176178
```
177179

178-
Future framework integrations (Angular, Svelte, React Native) follow the same two-package pattern.
180+
The two-package split is the shape a first-party integration takes when there is one — it is not a
181+
commitment to add more. Reaching another framework is an ownership move, not a roadmap item: eject
182+
the generator and retarget its emit (FR-040).
179183

180184
A user's `metaobjects.config.ts`:
181185

CHANGELOG.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,170 @@ port parked at `24.4` across several npm releases is not told its context has mo
6464
needs the shipped context hashed rather than its version compared, and the JVM ships no
6565
agent-context content — only the manifest reader.
6666

67+
### Added — FR-040: codegen ownership is the framework story
68+
69+
**MetaObjects does not need a codegen package per framework. It needs the ownership doctrine
70+
it already publishes to be true all the way down** — and in three places it was not. A codegen
71+
library cannot chase frameworks: there are more of them than any library can carry, they turn
72+
over faster than a release line, and each one added is a permanent liability the metamodel —
73+
the actual durable asset — gains nothing from. The shipped agent context already says the
74+
right thing ("treat this as a first-class, expected activity — not an escape hatch"). This is
75+
the release that makes it true. **The bar is not "Next.js works": it is that an agent adopting
76+
onto a stack nobody wrote a recipe for — Svelte, Nuxt, Qwik — reaches a working generator
77+
unaided, and treats having done so as normal.**
78+
79+
**How it was found, and the framing error worth recording.** A cold adoption probe ran the
80+
documented TypeScript quickstart against published `0.24.4` inside a Next.js 16 / React 19 app
81+
on Turbopack with a real Postgres. Its substantive result was **positive and is not in
82+
dispute**: the schema and server tiers work on that stack unmodified — `queriesFile()` takes
83+
`db` as a parameter, so a Server Component calls it with no HTTP hop; `routesFileHono()` is
84+
deps-injected, the shape an App Router Route Handler wants; the full cross-port API contract
85+
held. The probe then reported eight "findings", and **most were mis-framed as defects.** The
86+
default templates target Fastify on Node; that they do not emit Next.js output is the
87+
templates doing what they say. Recording that error is the point — a report that reads a
88+
template mismatch as a product failure will keep proposing framework packages forever. Three
89+
findings survived re-scoring, and they are what shipped.
90+
91+
- **`meta eject <generator>` — the ownership move is now a command.** ADR-0034 had `meta init`
92+
eagerly copy four generators (`entity`, `queries`, `routes`, `barrel`) into
93+
`codegen/generators/` and own them. The other five could be *used* but never *owned*: there
94+
was no supported way to get one's source, at init time or after. `meta eject` is that same
95+
copy operation generalised to every ejectable name in every package, callable at any time —
96+
for a generator you skipped at init, or one a package gained since. `meta eject --list`
97+
names all nine grouped by package (`entity`, `queries`, `routes`, `routes-hono`, `barrel`
98+
from `codegen-ts`; `form` from `codegen-ts-react`; `hooks`, `grid`, `grid-hook` from
99+
`codegen-ts-tanstack`). It never clobbers without `--force`, matching `init`'s rule, and it
100+
reports the import line to paste by **parsing it out of the template's own header** rather
101+
than re-deriving it from the file name — a generator's exported symbol does not follow its
102+
file name (`hooks.ts` exports `tanstackQuery`, `routes-hono.ts` exports `routesFileHono`),
103+
so a derived map would drift from what the template already tells a human to paste.
104+
- **The UI tier became ownable at all.** It was the gap with no workaround: an RSC app needs
105+
`"use client"` at the top of generated form/hook files, and there was no seam to add it —
106+
the choice was use the package's output verbatim or hand-write the tier and leave metadata
107+
behind. Four new reference templates (`form`, `hooks`, `grid`, `grid-hook`) plus a new
108+
`routes-hono`, and the render layer is **promoted to public API** — `renderFormFile`,
109+
`renderHooksFile`, `renderColumnsFile`, `renderGridHookFile`, each a stable
110+
`(entity, ctx) => string` — so an owned generator composes the engine and replaces only the
111+
step its framework disagrees about, instead of copying ~600 lines of package internals out.
112+
`codegen-ts` already exported `renderRoutesFile`; this applies the same pattern to the UI
113+
packages. The reference-template reader is now a per-package factory, so a package that
114+
gains templates later is picked up by `meta eject` with no other change.
115+
- **Every reference template documents its own swap point.** A `targets:` header block on all
116+
nine states what the template targets, when to use it, what it emits, where to change it,
117+
and what it composes with — so an agent retargeting reads the seam out of the file rather
118+
than inferring it. Paired with a new **"Your framework isn't the default — the retargeting
119+
procedure"** section in the `metaobjects-codegen` skill, which is the general answer the
120+
per-framework recipes were standing in for.
121+
122+
- **`clientDirective` — the `"use client"` knob.** Set it and the four generated client
123+
artifacts (form, hooks, columns, grid-hook) get the directive React Server Components
124+
frameworks require; leave it off (the default) and output is byte-identical to before.
125+
It is **config, never a metadata attribute**: the directive is a fact about the
126+
adopter's bundler topology, not about the entity, and registering it would give every
127+
non-TS port vocabulary it can never dispatch on — the `source.rdb @role` mistake that
128+
retired four members in `0.21.0`. It defaults **off** because the directive is only
129+
*required* under RSC and is inert-but-warned-about elsewhere; the asymmetry that would
130+
argue for defaulting on (a runtime error for RSC adopters versus a build warning for
131+
everyone else) is precisely what the rest of this release removes — before it, an RSC
132+
adopter had no seam at all. `<Entity>.meta.ts` is deliberately excluded: it is plain
133+
data imported *by* a client component, and in RSC the boundary is the importing
134+
component, not everything it reaches.
135+
136+
**Docs stop promising what the project does not intend to ship.** `AGENTS.md` and the port
137+
docs described a first-party package per framework as the way to reach a new framework; that
138+
was never the plan and is now stated as the ownership move it actually is. The agent-facing
139+
quickstart in `docs/llms/` had been teaching the **deprecated** `@metaobjectsdev/codegen-ts/generators`
140+
import path, and the ownership one-liner samples in the docs did not type-check — both fixed,
141+
with the samples now compiled.
142+
143+
**`meta init` scaffold honesty.** Four fixes where the scaffold said something untrue about
144+
its own output, the same class the `0.24.4` line was cut for: it stopped eagerly scaffolding
145+
an unwired `routes-hono.ts` (a file nothing imported, presented as if it were live); it now
146+
scaffolds a **throwing** `src/db.ts` stub, so the module the default `routesFile()` imports
147+
exists and fails with an instruction rather than `TS2307`; it names the `.gen-state` manifest
148+
and explains `dbImport` instead of leaving both as unexplained config; and its `fastify`
149+
devDependency is aligned to `runtime-ts`'s peer range, which it contradicted. Separately, the
150+
**TanStack Table v8 requirement is now discoverable** — `@metaobjectsdev/tanstack` bounds the
151+
peer at v8, but nothing told an adopter installing `@tanstack/react-table` themselves.
152+
153+
**One recipe, explicitly a convenience.** [`docs/recipes/nextjs-vercel.md`](docs/recipes/nextjs-vercel.md)
154+
walks the Next.js App Router + Vercel path — the `extStyle: "none"` / `clientDirective: true`
155+
config delta, `routesFileHono()` mounted at `app/api/[[...route]]/route.ts` via `hono/vercel`,
156+
and generated query helpers called straight from a Server Component. It changes no package
157+
file, and it opens by saying the general procedure lives in the `metaobjects-codegen` skill:
158+
it is a shortcut past reasoning an agent could do unaided, which is the only relationship
159+
FR-040 permits it to have. Two of its notes exist because the failure is SILENT — a Server
160+
Component reading the database is not a dynamic signal, so the page prerenders at build and
161+
serves build-time rows forever while looking correct in `next dev`; and `apiPrefix` is baked
162+
into the emitted route path, so a Hono `basePath` on top double-prefixes it.
163+
164+
Design: `docs/superpowers/specs/2026-08-29-fr-040-framework-agnostic-codegen-ownership-design.md`.
165+
Amends [ADR-0034](spec/decisions/ADR-0034-codegen-scaffold-and-own.md).
166+
167+
**Review round.** Five of the fixes above are review findings on FR-040's own first
168+
draft, and one is worth naming because it is the shape this project keeps convicting
169+
itself of: **the five new templates shipped with no equivalence gate.** ADR-0034 makes a
170+
copyable template safe by running it *and* the built-in it was copied from over a fixture
171+
corpus and requiring byte-identical output — and that gate covered only the four
172+
`meta init` scaffolds. Since `src/reference` is excluded from tsconfig, the five new ones
173+
were imported by nothing, executed by nothing and type-checked by nothing: a renamed
174+
engine export, or a drifted `filter` deciding WHICH entities emit, would have reached an
175+
adopter running `meta eject` before it reached a red lane. The tsconfig comment even
176+
asserted the coverage, having been copy-pasted into the two UI packages from the one
177+
where it was true. Every template is now gated in every package, the file SET is
178+
compared as well as the contents (a drifted filter changes what is emitted, not how), and
179+
each gate asserts its own coverage equals `REFERENCE_GENERATOR_NAMES` so the tenth
180+
template cannot repeat this. Also fixed from that round: `meta eject` told you to *paste*
181+
an import that collides with the package import already in the documented config — whose
182+
quiet failure mode is a config that keeps running the PACKAGED generator while you edit
183+
the ejected file — and never named the dependency the ejected file imports, so the
184+
adopter's own `tsc` reported TS2307 on the file the CLI had just said they owned.
185+
186+
**Second review round.** Seven more findings, and the two worth naming share a shape with
187+
the first round's: a change that was RIGHT drew its line one notch too wide, and nothing
188+
could see the difference. **The retargeting split took four ports' `own*()` guidance with
189+
it.** Moving `meta eject` and the `metaobjects.config.ts` keys out of the port-agnostic
190+
`SKILL.md` was correct — a Python project runs `metaobjects gen` and has no eject command,
191+
so those adopters' agents were being handed a procedure their toolchain cannot execute.
192+
But the same move carried off the ADR-0039 section, whose per-port own↔resolving **table
193+
is port-agnostic by construction**: its entire content is the OTHER ports' accessor names,
194+
including the trap that TS `attr()` resolves while Python `attr()` is own. It landed in
195+
`references/typescript.md`, the one page a Java, Python or C# adopter never installs,
196+
while `metaobjects-authoring` still told them to go read it there. And `SKILL.md` closed
197+
by sending every reader to "this skill's `references/` fragment for your server language",
198+
which for four of the five contains no retargeting content at all — a pointer to nothing,
199+
where before the split there had at least been a procedure (a wrong one, which is what the
200+
split fixed). The three port-agnostic sections are back in `SKILL.md`, and the closing
201+
pointer now says what is true per port, including that the other ports have **no** eject
202+
command and owning a generator there means implementing that port's generator interface.
203+
204+
**`meta init` claimed a stub it had just decided not to write.** Its next-steps block was
205+
one static string describing the scaffolded `src/db.ts` in the imperative, printed on every
206+
run — including the `meta init --force` in a project keeping its own config, where the
207+
write is deliberately skipped. Worse was the silence beside it: `wroteScaffoldedConfig` is
208+
only *"no config existed"*, so it is false for the config `init` itself wrote one command
209+
earlier, and a scaffolded project whose `src/db.ts` is deleted lands in the same branch as
210+
a project that owns its config — nothing written, nothing preserved, **nothing said**, and
211+
the next `tsc` reporting `TS2307` on a file the command had just chosen not to restore. It
212+
warns now rather than writing, because dropping a file into a project that owns its config
213+
is the unilateral host-project touch FR-040 §4.4 lists as a defect.
214+
215+
Also from the round: `meta eject` **stated which import a project currently has**, which
216+
it never checks and which is wrong for exactly the four `meta init` scaffolds — the
217+
`--force` re-sync case — so it names the goal and the three branches instead; its
218+
dependency notes could not see a subpath import and reported a **peer-declared package as
219+
missing**, advice that if followed adds the competing physical copy this repo has been
220+
bitten by twice; and **nothing gated `clientDirective` on the generated form**, the one
221+
client artifact the RSC story centres on. That last one is the round's own theme again,
222+
and the proof is sharper than the finding: with the directive dropped from a reference
223+
template alone — the two halves genuinely different — `reference-byte-identical` stays
224+
**green**, because it generates with the knob off, where the call is a no-op and removing
225+
it changes no byte it compares. The equivalence gate cannot see that defect in either
226+
direction. Both UI packages now run the knob against the built-in and the reference
227+
template, and each package's template coverage is a `Record` keyed by ejectable name whose
228+
keys are the proof and whose values are the wiring — so a tenth template is a **compile**
229+
error, not a hand-maintained list that can be edited to claim a gate nobody wrote.
230+
67231
### Fixed — eight defects found by adopting the product from scratch, twice
68232

69233
Two adoption runs against the published `0.24.4`, docs followed literally and nothing fixed

0 commit comments

Comments
 (0)