Skip to content

Commit 7eba5b8

Browse files
dmealingclaude
andcommitted
docs(fr-029): design the metadata API + browser read-model; rule out a runtime data surface
FR-029's UI-1 and UI-2. `GET /_meta` serves the whole model as EFFECTIVE canonical JSON, and `runtime-web` reads it into a slim structural model that the already-shipped `buildGrid()` can walk. `buildGrid` landed in ea2b9e9 and has had no way to obtain a MetaObject in a browser ever since; this is that missing half. Two decisions carry the design: - **Effective, not raw.** A raw model carries `extends` refs, so a client would have to resolve them — and per ADR-0039 an own-vs-resolving error there silently corrupts exactly the attrs a runtime grid reads (@columns, @pageSize, @sortableDefaultOrder). Serving effective moves that to the server, where it is already byte-gated in five ports, and makes the browser model a reader that cannot get it wrong. - **A slim client read-model, not the loader in the browser.** The metadata root barrel transitively imports `node:url` and cannot be bundled (#287, gated by runtime-web/test/browser-bundleable.test.ts). `buildGrid` already imports MetaObject as a TYPE only and values from the browser-safe /constants subpath, so a structurally compatible model suffices. The metadata package's module graph does not move. Only TypeScript has a web-bound runtime home: C#'s runtime package references only YamlDotNet, Java's core-spring has spring-context but no spring-web, and neither of Python's runtime packages imports FastAPI. So UI-1 ships as a contract plus a framework-free metaJson(root) helper per port, with a mount only in TS. The durable deliverable is the contract, which is what lets one browser read-model work against any backend (UI-8). Also records, in the design's section 8, that a four-port runtime metadata-driven REST DATA surface was considered and rejected: FR-029 never scoped one (roadmap:330 — UI-8 says "the existing data REST API"), no issue asks for one, EntityFetcher is a bare path fetch so the client tier cannot tell how an endpoint was produced, and the 2026-07-12 oversell review records that the flagship adopter did not use runtime metadata at all. Written down because the same design was independently re-derived in 2026-09 having already been specified in 2026-06. Amends the 2026-06-16 slice-1 design: its server half (sortableFields, handleList, mountMetaCrudRoutes) is retired, its client half (buildColumns, useMetaGrid) kept as UI-3. Neither retired piece was ever built; this records the decision that was never written down. Gap-table corrections found while grounding: - UI-5 was marked MISSING but shipped. use-entity-form.tsx derives from the GENERATED schema, which is what makes it UI-5 and not UI-4. UI-4 remains genuinely missing. - UI-3 is PARTIAL, not missing — buildGrid shipped. Two unrelated doc-drift fixes, same commit rather than filed: - AGENTS.md listed a `server/java/dynamic` module that does not exist. - AGENTS.md billed EF Core under the runtime pillar. The 2026-07-12 oversell review ruled that dishonest ("EF Core executing generated code is codegen, not a metadata-driven runtime") and docs/llms/llms.txt already says so; AGENTS.md was the outlier. Verified: the runtime MetaObjects package depends only on YamlDotNet. - The api-contract corpus README still said Actions is disabled. It is on since 2026-09-20, integration-tests.yml runs the corpus on tag push and dispatch, and local-ci.yml's ts-slow/java-slow lanes carry it on push to main. None run on a PR, so the advice to run ci-local.sh before opening one is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NrWUxA6euwvnFcwKxcK6A
1 parent b14b8d7 commit 7eba5b8

4 files changed

Lines changed: 275 additions & 12 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The metamodel is the **durable spine**; generated code is the **disposable artif
1111
The first four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior. The fifth ships its vocabulary in every port, its `meta verify` checks in the Node `meta` CLI, and its test scaffolding in TypeScript only. The sixth is content the other five compose, shipped as named opt-in artifacts:
1212

1313
1. **Codegen** — emit idiomatic per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via `codegen-spring`, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin via `codegen-kotlin`). Hand-edit-preserving regen via three-way merge.
14-
2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin), EF Core (C#).
14+
2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin). **C# runtime metadata is on the roadmap** — its EF Core output is generated code, not a metadata-driven runtime; the runtime `MetaObjects` package depends only on YamlDotNet and ships no data access.
1515
3. **Drift detection** — `meta verify` catches divergence between code and metadata (covers entity codegen, prompt templates, output parsers, schema). Quality-of-life on top of codegen + runtime.
1616
4. **Prompt construction** — a prompt is code, not a string scattered across services. Declare a prompt's payload as a typed projection (payload bloat becomes a diff), keep its text external and provider-resolved, and render it deterministically: snapshot-testable, cache-stable (no whitespace change silently breaking exact-prefix prompt-cache hits), and drift-checked at build time so a renamed field can't degrade a prompt. Conformance-gated, so the guarantee holds in every language port. **Render + payload-VO codegen + `verify` + parser-on-receipt for a *responding* `template.prompt` — one carrying `@responseRef` (FR-006) — + the output-format prompt fragment & tolerant `extract` parser (FR-010) ship in all five ports today** (since 0.24.0 the whole inbound tier keys off `@responseRef`; a `template.output` is outbound-only and emits no parser — ADR-0052) — the library-side building blocks of the pillar are complete. The one remaining library-side piece is MCP exposure of declared prompts/tools (see `spec/roadmap.md`); the application-level consolidation (eval harness, end-to-end declared-prompt orchestration) and consumer adoption are exercised in adopter projects, not in this library repo. Designed in `docs/superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md`.
1717
5. **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. The other four pillars keep the code honest about the *model*; this one asks whether the thing you said the software does is actually built — an absence no test can fail on, because a test exercises code that exists. `requirement.functional` (existence: `meta verify` WARNS when nothing implements it) and `requirement.architectural` (universality: `meta verify` FAILS a live policy applied to nothing, and does not check that each claimed node complies) are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum. `@implementedBy` is **resolved, not trusted** — it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build instead of going quietly stale. `meta verify` reports the ledger on every run (unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on) plus an authoring lint whose findings can never fail a build; `meta docs` renders it for humans and for agents. **The port split, stated exactly: the vocabulary loads and validates in all five ports; the `meta verify` checks run in the Node `meta` CLI only (no other port's CLI runs them — `docs/CONFORMANCE.md` "Split coverage"); `requirementTests()` — which scaffolds a test stub per claim — is TypeScript-only.** A green run proves referential integrity, never that a status is true or an implementation correct (`docs/features/requirements.md`, "What a green run does not prove"). A project that declares no `requirement.*` nodes sees no change at all. Note the standing carve-out: `agent-context/skills/metaobjects-fit-assessment/SKILL.md` deliberately does NOT treat `requirement.*` as an assessment axis — see the ruling in that file before "finishing the job" there.
@@ -65,7 +65,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
6565

6666
- **TypeScript** — `codegen-ts` (Vite-style plugins; Drizzle, Zod, Fastify) + `runtime-ts` + `migrate-ts` + the universal web client packages (`runtime-web`, `react`, `tanstack`).
6767
- **C#** — `MetaObjects` (loader + canonical serializer + conformance) + `MetaObjects.Render` (Mustache + payload-VO + `verify`) + `MetaObjects.Codegen` (EF Core entities + `AppDbContext` + CRUD minimal-API routes). Schema migrations are TS-owned (ADR-0015): the C# migrate engine and the `migrate`/`--from-db` CLI surface were removed; the C# CLI is `gen`/`verify` only, packaged as a .NET tool invoked **`dotnet meta`** (not a bare `meta` — that name belongs to the Node schema CLI). EF Core runtime data-access stays per-port.
68-
- **Java** — `metadata` + `omdb` + `om` + `dynamic` + `core-spring` + `metadata-ktx` (Kotlin facade) + `codegen-spring` (Spring controllers + DTOs + repositories + filter allowlists + payload records + output parsers) + `codegen-mustache` + `codegen-plantuml` + `render` + `maven-plugin` (`metaobjects:generate`/`metaobjects:editor` + a `metaobjects:verify` **codegen-drift** goal — distinct from the removed live-DB `metaobjects:verify`; Kotlin generators run through `metaobjects:generate` via the shared SPI). FR-003 (OMDB runtime persistence + binding registry + typed jsonb + Spring-tx + source/origin metamodel) fully shipped, including Plan 4 (engine-debt remediation: atomic mapping cache, JDBC codec registry, `inTransaction` template). **Schema migrations are owned by the TypeScript toolchain** (`@metaobjectsdev/cli migrate`); the Java port's diff-and-converge migration engine and its `metaobjects:migrate` / live-DB-drift `metaobjects:verify` Maven goals were removed, and per ADR-0015 Decision 2 the dev/test runtime auto-create path (`MetaClassDBValidatorService` + the drivers' `createTable`/`createIndex`/`createForeignKey`/`createSequence` DDL) is **also** removed — **OMDB is pure data-access** (CRUD/query/codec/transactions only).
68+
- **Java** — `metadata` + `omdb` + `om` + `core-spring` + `metadata-ktx` (Kotlin facade) + `codegen-spring` (Spring controllers + DTOs + repositories + filter allowlists + payload records + output parsers) + `codegen-mustache` + `codegen-plantuml` + `render` + `maven-plugin` (`metaobjects:generate`/`metaobjects:editor` + a `metaobjects:verify` **codegen-drift** goal — distinct from the removed live-DB `metaobjects:verify`; Kotlin generators run through `metaobjects:generate` via the shared SPI). FR-003 (OMDB runtime persistence + binding registry + typed jsonb + Spring-tx + source/origin metamodel) fully shipped, including Plan 4 (engine-debt remediation: atomic mapping cache, JDBC codec registry, `inTransaction` template). **Schema migrations are owned by the TypeScript toolchain** (`@metaobjectsdev/cli migrate`); the Java port's diff-and-converge migration engine and its `metaobjects:migrate` / live-DB-drift `metaobjects:verify` Maven goals were removed, and per ADR-0015 Decision 2 the dev/test runtime auto-create path (`MetaClassDBValidatorService` + the drivers' `createTable`/`createIndex`/`createForeignKey`/`createSequence` DDL) is **also** removed — **OMDB is pure data-access** (CRUD/query/codec/transactions only).
6969
- **Python** — `metaobjects` (loader + canonical serializer + conformance + render + verify + codegen) + an `ObjectManager` runtime layer. The `migrate` module was removed (schema is TS-owned, ADR-0015); Python is pure data-access (codegen + ObjectManager runtime), with a **`metaobjects` console-script** (`gen`/`verify` codegen — no `migrate`). All five conformance corpora green.
7070
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.
7171

‎docs/superpowers/specs/2026-06-13-metadata-runtime-ui-and-serializers-gaps.md‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,29 @@ The principle: the **web UI is TS (browser-native), but it must be drivable by m
1616

1717
| ID | Gap | Status | Scope |
1818
|---|---|---|---|
19-
| UI-1 | **Metadata API endpoint** — each backend serves its loaded metadata as canonical JSON over HTTP (`GET /_meta` / per-entity), so a browser can fetch the model. | ❌ MISSING | all 5 backends |
20-
| UI-2 | **Browser runtime metadata loader** — `runtime-web` loads canonical metadata JSON into an in-browser MetaData read-model (entities/fields/views/validators/layouts queryable client-side). | ❌ MISSING | TS web |
21-
| UI-3 | **Runtime-driven dataGrid** — build columns + cell renderers + sort/filter/page config from fetched metadata at runtime (no codegen). | ❌ MISSING | TS web |
19+
| UI-1 | **Metadata API endpoint** — each backend serves its loaded metadata as canonical JSON over HTTP (`GET /_meta`), so a browser can fetch the model. | 📋 **DESIGNED 2026-09-20** — whole-model, **effective** canonical JSON; a framework-free `metaJson(root)` helper per port + a TS Fastify/Hono mount (only TS has a web-bound runtime home). Per-entity/ETag/versioning deferred. | all 5 backends |
20+
| UI-2 | **Browser runtime metadata loader** — `runtime-web` loads canonical metadata JSON into an in-browser MetaData read-model (entities/fields/views/validators/layouts queryable client-side). | 📋 **DESIGNED 2026-09-20** — a slim structural read-model (`loadMetaModel`), NOT `MetaDataLoader` in the browser (#287: the root barrel pulls `node:url`). Settles this theme's open question. | TS web |
21+
| UI-3 | **Runtime-driven dataGrid** — build columns + cell renderers + sort/filter/page config from fetched metadata at runtime (no codegen). | 🟡 **PARTIAL** — `buildGrid()` shipped `ea2b9e932` (#40). `buildColumns` + `useMetaGrid` remain (2026-06-16 slice-1 design §5.4/§5.5); they consume the EXISTING generated data API. | TS web |
2222
| UI-4 | **Runtime-driven create + edit forms** — build the form fields + client validation from fetched metadata + validators at runtime. | ❌ MISSING | TS web |
23-
| UI-5 | **Codegen edit forms** — `UpdateSchema` is generated but unused; emit `<Entity>EditForm` (load defaults, PATCH). | ❌ MISSING (FR-026) | TS web |
23+
| UI-5 | **Codegen edit forms** — `UpdateSchema` is generated but unused; emit `<Entity>EditForm` (load defaults, PATCH). | ✅ **SHIPPED** (status corrected 2026-09-20 — was stale). `spec/roadmap.md:41` records FR-026 edit forms shipped; `client/web/packages/react/src/use-entity-form.tsx` derives from the GENERATED schema, which is what makes it UI-5 and not UI-4. | TS web |
2424
| UI-6 | **View-render parity (codegen + runtime)** — register `datetime`; add `hotlink`/`month`/`radio` renderers; wire `validator.numeric`/`validator.array` to client rules; view attrs beyond `@locale`. | 🟡 PARTIAL (FR-026) | TS web |
2525
| UI-7 | **"Both ways" demo + docs** — one reference app showing the codegen UI and the runtime-metadata-driven UI side by side, against each backend language. | ❌ MISSING | docs/example |
2626
| UI-8 | **Backend-agnostic guarantee** — the TS UI verified working against all 5 backends (each serving the metadata API + the existing data REST API). | ❌ MISSING | cross-port |
2727

28-
Open questions: metadata-API shape (whole-model vs per-entity), caching/ETag/versioning, auth, and whether the runtime read-model reuses the `@metaobjectsdev/metadata` loader compiled to the browser or a slim client model.
28+
Open questions: caching/ETag/versioning, and per-entity vs whole-model.
29+
30+
**Answered 2026-09-20** (`docs/superpowers/specs/2026-09-20-fr-029-metadata-api-and-browser-read-model-design.md`):
31+
whole-model, serving the **effective** canonical serialization (so the browser never has to
32+
resolve `extends` — an own-vs-resolving error there would silently corrupt exactly the attrs a
33+
runtime grid reads, per ADR-0039); and a **slim client read-model**, not the
34+
`@metaobjectsdev/metadata` loader compiled to the browser, because that loader's root barrel
35+
transitively imports `node:url` and cannot be bundled (#287, gated by
36+
`runtime-web/test/browser-bundleable.test.ts`). Auth: `/_meta` is opt-in to mount and guarded by
37+
the host's own middleware, the same answer #367 gives for generated routes.
38+
39+
**Also ruled 2026-09-20: there is no runtime metadata-driven REST *data* surface.** The runtime
40+
UI consumes the existing generated data API — `EntityFetcher` is a bare path fetch and cannot
41+
tell how an endpoint was produced. Rationale, and what would reopen it, in §8 of that design.
2942

3043
## Theme 2 — DataGrid downloads, all backends → FR-027
3144

0 commit comments

Comments
 (0)