Skip to content

Commit 18e7116

Browse files
dmealingclaude
andcommitted
feat(examples): canonical advanced-modeling spine (projections, entity views, VO-jsonb, prompt payloads)
Unit 1 of the advanced modeling series (docs/superpowers/plans/2026-07-19-advanced-modeling-series-plan.md): a course/program publishing model (package acme::learn) demonstrating all four advanced patterns together — projections (ProgramSummary: passthrough/aggregate count/aggregate sum+filter/computed), entity views (Program's enum/textarea+rows/ currency/view.image form controls), value objects as jsonb columns (array-of-VO syllabus + single-VO instructorProfile), and value objects as document/LLM payloads (ProgramDescriptionPayload + a template.output). Metadata + committed `meta gen` output + a fast-lane drift gate (cli/test/integration/advanced-modeling-drift.test.ts, joins the existing cli package suite — no new CI job). Building it surfaced and fixed four real, narrowly-scoped bugs (all covered by existing test suites, none touching metamodel vocabulary): - render-helper.ts didn't stripPackage() a resolved @payloadRef before emitting it as a TS identifier, producing invalid syntax for any payload in a named package. - verify --codegen's throwaway regen root didn't carry the project's templates/ dir forward, breaking --codegen for any project using render-helper(). - a relative outDir resolved against ambient process.cwd() instead of the resolved --cwd, in both meta gen's write path (runner.ts) and verify --codegen's read path (codegen-drift.ts). - docs/features/relationships.md documents @onDelete/@onUpdate as applying to identity.reference; only relationship.* actually registers them (doc-only fix, reflected in the example, not the docs — out of scope here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeGSV3StPCcJGZNNJ4ZfAb
1 parent 04f114f commit 18e7116

39 files changed

Lines changed: 2268 additions & 28 deletions

docs/superpowers/plans/2026-07-19-advanced-modeling-series-plan.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
66
**Design spec:** `docs/superpowers/specs/2026-07-19-advanced-modeling-series-design.md`
77

8-
## STATUS — how to resume (updated 2026-07-19)
8+
## STATUS — how to resume (updated 2026-07-20)
99

10-
**Nothing is built yet.** Design + plan are approved and committed; Unit 1 has not started.
10+
**Unit 1 (the canonical spine) is DONE.** Design + plan are approved and committed. Full detail:
11+
`.superpowers/sdd/unit1-report.md`.
1112

1213
- ✅ Design spec written, reviewed, and **reconciled against the adjacent artifacts** (conformance
1314
corpora, the public reference application) — see the design's "four tiers" section. The reconciliation
@@ -16,9 +17,15 @@
1617
jsonb + prompt payloads, but **zero** projections and **zero** view/form/currency vocabulary).
1718
- ✅ Maintainer decisions locked: `examples/` top-level · fast-lane drift gate inside a package suite ·
1819
**YAML** authoring · the public reference app may be named/linked.
19-
-**NEXT: Unit 1** (the canonical spine) — start at "Unit 1, Step 1: Recon the authoring surface".
20-
Step 1 is not optional: the 0.19.0 line moved the view-attr home to the `metaobjects-ui-web`
21-
provider, so authoring `view.*` from memory will be wrong.
20+
-**Unit 1 built**`examples/advanced-modeling/` (metadata + committed `src/generated/**` + README
21+
pattern index) + the drift gate (`server/typescript/packages/cli/test/integration/advanced-modeling-drift.test.ts`,
22+
joins the `cli` package's existing `bun test` suite → `ci-local.sh`'s `gate_conf_ts` / `ts-fast` lane).
23+
Recon (Step 1) surfaced 5 real gaps only once the full `meta gen``meta verify` pipeline actually ran
24+
(a stale doc claim + 4 codegen/CLI bugs, all fixed narrowly with existing-suite coverage staying green) —
25+
see the report's "What did not match reality" section and the example's own README "Gotchas".
26+
-**NEXT: Unit 2** (four developer deep-dives under `docs/features/advanced/`) — depends on Unit 1
27+
(done). See the "Unit 2 — developer deep-dives (SKETCH)" section below; every code block must quote
28+
the Unit 1 spine, never a hand-written parallel snippet.
2229

2330
**Context a fresh session needs:** the four patterns and the domain rationale are in the design spec;
2431
the tier discipline + anti-drift rule are binding on every unit; the drift gate is a freshness /
@@ -75,51 +82,51 @@ four patterns, with committed generated output and a CI gate proving `meta gen`
7582
- Create `examples/advanced-modeling/src/generated/**` — committed output.
7683
- Wire the drift gate (see Step 6).
7784

78-
- [ ] **Step 1: Recon the authoring surface before writing metadata.**
85+
- [x] **Step 1: Recon the authoring surface before writing metadata.**
7986
Read `fixtures/conformance/` entries for the four patterns plus `docs/features/source-kinds.md`
8087
and `docs/features/templates-and-payloads.md` to confirm exact current syntax (`origin.*` attrs,
8188
`@storage: jsonb`, `template.output` attrs, `view.*` names). Do NOT write metadata from memory —
8289
the 0.19.0 line changed the view-attr home (`metaobjects-ui-web`). Record the confirmed syntax in
8390
the task report.
8491

85-
- [ ] **Step 2: Author the catalog model (`meta.catalog.json`).**
92+
- [x] **Step 2: Author the catalog model (`meta.catalog.json`).**
8693
`Program` (entity): `id`, `title` (`view.textarea @rows`), `status` (`field.enum` → select),
8794
`priceCents` (`field.currency` + `view.currency`), `coverKey` (`field.string` + `view.image` with
8895
`@aspectRatio`/`@maxEdge`/`@store`/`@accept`/`@maxBytes`), `authorId`. `Purchase` (entity) with a
8996
relationship to `Program`. This covers **pattern 2 (entity views)** end to end.
9097

91-
- [ ] **Step 3: Author the projection (`ProgramSummary` in `meta.catalog.json`).**
98+
- [x] **Step 3: Author the projection (`ProgramSummary` in `meta.catalog.json`).**
9299
`object.projection` over `Program` with `source.rdb @kind: view`, demonstrating **all** the
93100
origin kinds the series teaches: `origin.passthrough` (author name from a related entity),
94101
`origin.aggregate @agg: count` (lesson count), `origin.aggregate @agg: sum` + `@filter` (revenue
95102
from completed purchases only), and one `origin.computed @expr`. Include `identity.primary`
96103
extending the base entity identity. This covers **pattern 1**.
97104

98-
- [ ] **Step 4: Author the jsonb value-objects (`meta.content.json`).**
105+
- [x] **Step 4: Author the jsonb value-objects (`meta.content.json`).**
99106
A `SyllabusSection` `object.value` (no identity, no source — purity per ADR-0028) referenced from
100107
`Program.syllabus` as `field.object @objectRef @storage: jsonb @isArray: true`, plus a single
101108
(non-array) `Instructor` profile VO on `Program`. This covers **pattern 3**, including the
102109
array-of-VO codec.
103110

104-
- [ ] **Step 5: Author the LLM payload (`meta.prompts.json`).**
111+
- [x] **Step 5: Author the LLM payload (`meta.prompts.json`).**
105112
A payload `object.value` projecting the subset of `Program` a description prompt actually needs,
106113
plus a `template.output` declaring the prompt (kind/payloadRef/textRef/format and the output
107114
contract). This covers **pattern 4** and demonstrates the payload-bloat-is-a-diff property.
108115

109-
- [ ] **Step 6: Generate, commit output, and add the drift gate.**
116+
- [x] **Step 6: Generate, commit output, and add the drift gate.**
110117
Run the CLI against the example (`meta gen`) and commit `src/generated/**`. Then wire a gate that
111118
regenerates and fails on any diff. **Follow the existing pattern** — inspect how the repo's current
112119
golden/drift gates are wired (`server/typescript/packages/codegen-ts/test/golden/`,
113120
`scripts/ci-local.sh`, `scripts/ci-affected-ports.sh`) and match it rather than inventing a new
114121
mechanism. Ensure the example is EXCLUDED from the Bun workspace globs if inclusion would pull it
115122
into package test runs.
116123

117-
- [ ] **Step 7: Verify.**
124+
- [x] **Step 7: Verify.**
118125
`meta gen` is idempotent (second run → no diff); `meta verify` passes; the generated form renders
119126
the expected controls (select / textarea with rows / currency / `<ImageUpload>`); the projection
120127
emits a view; the jsonb VO columns and the payload VO appear in output. Record evidence.
121128

122-
- [ ] **Step 8: README + commit.**
129+
- [x] **Step 8: README + commit.**
123130
`README.md` maps each pattern → the file and lines that demonstrate it (this is the index the
124131
deep-dives, agent-context, and video scripts all cite). Commit.
125132

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.gen-state/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"schema_version": 1,
3+
"pending_in_git": true,
4+
"confidence_thresholds": {
5+
"pending_promote": 0.8,
6+
"drift_warn": 0.7
7+
},
8+
"sources": [],
9+
"extract": {}
10+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Advanced modeling — the canonical spine
2+
3+
**Unit 1** of the advanced modeling series (see
4+
`docs/superpowers/plans/2026-07-19-advanced-modeling-series-plan.md` and the
5+
design doc at `docs/superpowers/specs/2026-07-19-advanced-modeling-series-design.md`).
6+
A course/program publishing platform (package root `acme::learn`) that exercises
7+
four advanced metadata patterns **together, in one model** — the interaction
8+
between them is the thing the rest of the series teaches. This is the index
9+
Units 2–4 (developer deep-dives, agent-context decision rules, video scripts)
10+
all cite.
11+
12+
## What this is — and isn't
13+
14+
This directory is **authored metadata + `metaobjects.config.ts` + committed
15+
generated output (`src/generated/**`)**, verified by a drift gate. It is
16+
**not a runnable application** — there is no server, no database, no
17+
`package.json`/`node_modules` for the example itself. That is a deliberate
18+
scope boundary (see the design doc, Decision 3): a live app would buy demo
19+
polish at the cost of a permanent maintenance tax; metadata + committed
20+
output gives docs/agent-context/videos a real, current artifact to cite at a
21+
fraction of the upkeep.
22+
23+
**The drift gate is a freshness + CLI-composition smoke check, not a
24+
correctness gate.** It proves the committed output still matches what
25+
`meta gen` produces right now, and that all four patterns still compose
26+
through the real CLI path. It does **not** re-prove the patterns' *behavior*
27+
— that is owned by `fixtures/*-conformance/` and the codegen golden tests in
28+
`server/typescript/packages/codegen-ts/test/`. Do not describe it otherwise.
29+
30+
## The four patterns, and where to find them
31+
32+
| # | Pattern | Metadata | Generated output |
33+
|---|---|---|---|
34+
| 1 | **Projections** (`object.projection`, `origin.*`) | `metaobjects/meta.catalog.yaml:127-165``ProgramSummary`: `origin.passthrough` (a join to `Author`, `:138`), `origin.aggregate @agg:count` (`:143-146`), `origin.aggregate @agg:sum` + `filter` (`:153-157`), `origin.computed @expr` (`:163-164`) | `src/generated/ProgramSummary.ts` (the `pgView` declaration + read schema), `src/generated/ProgramSummary.routes.ts` (read-only routes — no POST/PATCH/DELETE) |
35+
| 2 | **Entity views** (`view.*` control family) | `metaobjects/meta.catalog.yaml:25-93``Program`: `field.enum` (`:31-33`, → `<select>`), `view.textarea @rows` (`:40`), `field.currency` + `view.currency` (`:41-45`), `view.image` — all five attrs (`:53-58`) | `src/generated/Program.form.tsx` (select / `<textarea rows={6}>` / `<ImageUpload>` wrapped in `<Controller>`) |
36+
| 3 | **Value objects as jsonb columns** | `metaobjects/meta.catalog.yaml:80-92``Program.syllabus` (array-of-VO, `isArray: true` + `storage: jsonb`) and `Program.instructorProfile` (single VO); the VOs themselves in `metaobjects/meta.content.yaml:27-47` (`SyllabusSection`, `InstructorProfile` — pure `object.value`, ADR-0028: no identity, no source) | `src/generated/Program.ts` (`jsonb("syllabus").$type<SyllabusSection[]>()`, `jsonb("instructor_profile").$type<InstructorProfile>()`); `src/generated/Program.form.tsx` (a repeatable `useFieldArray` sub-form for `syllabus`, an embedded fieldset for `instructorProfile`) |
37+
| 4 | **Value objects as LLM/document payloads** | `metaobjects/meta.prompts.yaml``ProgramDescriptionPayload` (`:21-42`, an `object.value` projecting `Program`/`Author`/`Lesson` via `origin.passthrough`/`origin.aggregate`, no identity/source) + `template.output` (`:44-48`) | `src/generated/prompts.ts` (the payload interface), `src/generated/ProgramDescriptionOutput.output.ts` (tolerant parser), `src/generated/ProgramDescriptionOutput.render.ts` (typed `render<Name>()` wrapper); the mustache text lives at `templates/learn/program-description.mustache` |
38+
39+
`metaobjects/meta.catalog.yaml` also declares `Author` and `Purchase` — the
40+
minimal supporting entities the four patterns above need (an FK to join
41+
across for pattern 1; a transactional record with its own currency/enum
42+
fields, structurally independent of `Program`'s `relationship.composition`
43+
lessons). `metaobjects/meta.content.yaml` declares `Lesson` (the
44+
`relationship.composition` target `ProgramSummary.lessonCount` aggregates
45+
over).
46+
47+
## Regenerating
48+
49+
From the repo root:
50+
51+
```sh
52+
cd server/typescript
53+
bun run packages/cli/bin/meta.ts gen --cwd ../../examples/advanced-modeling
54+
bun run packages/cli/bin/meta.ts verify --cwd ../../examples/advanced-modeling --codegen
55+
bun run packages/cli/bin/meta.ts verify --cwd ../../examples/advanced-modeling --templates --prompts templates
56+
```
57+
58+
(`--prompts templates` is required for `--templates`: this example's mustache
59+
lives under `templates/`, the directory the `render-helper` codegen generator
60+
resolves at generation time — not `verify`'s own `prompts/` default. See
61+
"Gotchas" below.)
62+
63+
`examples/` is **not** part of the Bun workspace (the repo-root
64+
`package.json` globs only `server/typescript/packages/*` and
65+
`client/web/packages/*`), so there is no local install step — the CLI's
66+
config loader resolves the `@metaobjectsdev/*` imports in
67+
`metaobjects.config.ts` from the CLI's own install regardless of the
68+
example's `node_modules` (which doesn't exist).
69+
70+
## The drift gate
71+
72+
`server/typescript/packages/cli/test/integration/advanced-modeling-drift.test.ts`
73+
runs three checks against this directory as committed:
74+
75+
1. `meta verify --templates --prompts templates` — the declared
76+
`template.output`'s mustache is drift-free against its payload VO's field
77+
tree.
78+
2. `meta verify --codegen``src/generated/**` as committed is byte-identical
79+
to a fresh regen from the current metadata.
80+
3. Every committed `.ts`/`.tsx` file under `src/generated/` parses as valid
81+
TypeScript (`Bun.Transpiler`, resolution-free — `examples/` has no
82+
`node_modules` to resolve `drizzle-orm`/`zod`/`@metaobjectsdev/react`/etc.
83+
against, so a full typecheck isn't available here). This check exists
84+
because (2) alone is a pure string diff: a generator that emits
85+
*deterministically* invalid syntax still "matches a fresh regen" cleanly.
86+
That is not hypothetical — see "Gotchas" below.
87+
88+
That test file lives in the `cli` package, whose full `bun test` suite is
89+
already part of `scripts/ci-local.sh`'s `gate_conf_ts` step (the `ts-fast`
90+
lane) — this is a **fast-lane addition to an existing package suite**, not a
91+
new CI job, per the plan's resolved decision.
92+
93+
## Gotchas found building this spine
94+
95+
Recon-first (per the plan's Step 1) still surfaced four real issues once the
96+
metadata actually ran through the full `meta gen``meta verify` pipeline —
97+
worth recording here since they're exactly the kind of thing a from-memory
98+
authoring pass would miss:
99+
100+
- **`identity.reference` does not register `@onDelete`/`@onUpdate`.**
101+
`docs/features/relationships.md` documents these attrs as applying to
102+
"both" `relationship.*` and `identity.reference`; only `relationship.*` (all
103+
three subtypes — `association`/`aggregation`/`composition`, each with a
104+
sensible default: composition→cascade, aggregation→set-null,
105+
association→restrict) actually has them registered. The referential action
106+
belongs on the relationship side (`Program.lessons` /
107+
`Program.purchases`), never restated on the `identity.reference` FK side.
108+
The doc is stale on this one point; this example follows the registered
109+
vocabulary, not the doc.
110+
- **`routesFile()`/`formFile()` don't special-case a pure `object.value`.**
111+
`entityFile()` already renders a VO as shape-only (interface + Zod, no
112+
Drizzle table) — but `routesFile`/`formFile` assumed every entity has a
113+
backing table/entity-constants object, and would emit routes/forms
114+
referencing exports that don't exist for a VO. Worked around here via each
115+
generator's `filter` option (`metaobjects.config.ts`) rather than fixed in
116+
`codegen-ts`, since it's also the semantically correct call for this domain
117+
— none of `SyllabusSection`/`InstructorProfile`/`ProgramDescriptionPayload`
118+
is independently CRUD-addressable or form-submitted.
119+
- **`render-helper`'s payload type wasn't package-stripped.** A resolved
120+
`@payloadRef` can arrive fully-qualified (`acme::learn::X`); every other TS-
121+
identifier-emitting generator (`payload-codegen.ts`, `entity-file.ts`, …)
122+
already calls `stripPackage()` on a resolved ref before emitting it as an
123+
identifier — `render-helper.ts` didn't, so a `template.output` payload in a
124+
named package emitted `import type { acme::learn::X } from "./acme::learn::X.js"`,
125+
which is not legal TypeScript. Fixed in
126+
`codegen-ts/src/templates/render-helper.ts` (this PR) — narrow, mirrors the
127+
existing pattern, covered by the existing render-helper test suite.
128+
- **`meta verify --codegen` didn't carry the project's `templates/` dir into
129+
its throwaway regen root**, so any project using `render-helper()` (or
130+
other provider-backed template generators) would spuriously fail
131+
`--codegen` with an "unresolved" error unrelated to actual drift. Fixed in
132+
`cli/src/lib/codegen-drift.ts` (this PR) — mirrors the existing outDir-copy
133+
logic, extended to the templates dir.
134+
- **A relative `outDir` resolved against the ambient `process.cwd()`, not the
135+
resolved `--cwd`.** `meta gen`/`meta verify --codegen` silently wrote/read
136+
the wrong location whenever the CLI was invoked with `--cwd` pointing
137+
somewhere other than the actual process working directory (exactly this
138+
drift gate's own invocation pattern: a `bun test` run from `packages/cli/`
139+
targeting `examples/advanced-modeling/`). The common case — running `meta
140+
gen` from inside your own project, no `--cwd` — happened to work by
141+
accident (`process.cwd() === projectRoot`), which is why this had gone
142+
unnoticed. Fixed in `codegen-ts/src/runner.ts` (the write path) and
143+
`cli/src/lib/codegen-drift.ts` (the read path), both anchoring a relative
144+
`outDir` to the resolved project root.
145+
146+
None of the above are metamodel vocabulary gaps (ADR-0023) — they're either a
147+
stale doc claim or codegen/CLI bugs, fixed narrowly and covered by the
148+
existing test suites (all four ports' conformance corpora and the `cli`/
149+
`codegen-ts` package suites stayed green throughout).
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { defineConfig } from "@metaobjectsdev/cli";
2+
// This example is embedded IN the metaobjects monorepo (not `npm install`ed),
3+
// so there is no scaffolded ADR-0034 owned-copy target for it to import from
4+
// (`meta init`'s codegen/generators/*.ts copies resolve their OWN imports via
5+
// a real package install, which this repo-local example doesn't have). A real
6+
// consumer project runs `meta init` and gets local, editable owned copies —
7+
// see CLAUDE.md "Codegen architecture (Vite-style plugins)". Here every
8+
// generator is imported directly from the package.
9+
import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators";
10+
// React form codegen (PATTERN 2 — entity views: the generated form dispatches
11+
// per view.* control, incl. <ImageUpload> for view.image).
12+
import { formFile } from "@metaobjectsdev/codegen-ts-react";
13+
// Template/payload codegen (PATTERN 4 — LLM/document payloads).
14+
import {
15+
promptRender,
16+
outputParser,
17+
extractor,
18+
outputPrompt,
19+
renderHelper,
20+
} from "@metaobjectsdev/codegen-ts/generators";
21+
import { OBJECT_SUBTYPE_VALUE, type MetaObject } from "@metaobjectsdev/metadata";
22+
23+
// `entityFile`/`queriesFile` already special-case a pure `object.value` (no
24+
// identity, no source — ADR-0028) as shape-only (interface + Zod, no Drizzle
25+
// table). `routesFile`/`formFile` do not yet make that distinction — they
26+
// assume every entity has a backing table / entity-constants object, which a
27+
// pure VO's `renderValueObjectFile` output doesn't emit. Filtering VOs out of
28+
// these two generators here is the config-level workaround (the `filter`
29+
// option exists for exactly this); it does not belong in this teaching
30+
// example as a code fix. SyllabusSection / InstructorProfile /
31+
// ProgramDescriptionPayload are used only as embedded jsonb shapes / a
32+
// payload — none is independently CRUD-addressable or form-submitted, so
33+
// excluding them is also the semantically correct call for this domain.
34+
const isNotValueObject = (e: MetaObject): boolean => e.subType !== OBJECT_SUBTYPE_VALUE;
35+
36+
export default defineConfig({
37+
outDir: "src/generated",
38+
extStyle: "none",
39+
dbImport: "../db",
40+
dialect: "postgres",
41+
apiPrefix: "/api",
42+
generators: [
43+
entityFile(),
44+
queriesFile(),
45+
routesFile({ filter: isNotValueObject }),
46+
formFile({ filter: isNotValueObject }),
47+
promptRender(),
48+
outputParser(),
49+
extractor(),
50+
outputPrompt(),
51+
renderHelper(),
52+
barrel(),
53+
],
54+
});

0 commit comments

Comments
 (0)