Unit 1 of the advanced modeling series (see
docs/superpowers/plans/2026-07-19-advanced-modeling-series-plan.md and the
design doc at docs/superpowers/specs/2026-07-19-advanced-modeling-series-design.md).
A course/program publishing platform (package root acme::learn) that exercises
four advanced metadata patterns together, in one model — the interaction
between them is the thing the rest of the series teaches. This is the index
Units 2–4 (developer deep-dives, agent-context decision rules, video scripts)
all cite.
This directory is authored metadata + metaobjects.config.ts + committed
generated output (src/generated/**), verified by a drift gate. It is
not a runnable application — there is no server, no database, no
package.json/node_modules for the example itself. That is a deliberate
scope boundary (see the design doc, Decision 3): a live app would buy demo
polish at the cost of a permanent maintenance tax; metadata + committed
output gives docs/agent-context/videos a real, current artifact to cite at a
fraction of the upkeep.
The drift gate is a freshness + CLI-composition smoke check, not a
correctness gate. It proves the committed output still matches what
meta gen produces right now, and that all four patterns still compose
through the real CLI path. It does not re-prove the patterns' behavior
— that is owned by fixtures/*-conformance/ and the codegen golden tests in
server/typescript/packages/codegen-ts/test/. Do not describe it otherwise.
| # | Pattern | Metadata | Generated output |
|---|---|---|---|
| 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) |
| 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>) |
| 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) |
| 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 |
metaobjects/meta.catalog.yaml also declares Author and Purchase — the
minimal supporting entities the four patterns above need (an FK to join
across for pattern 1; a transactional record with its own currency/enum
fields, structurally independent of Program's relationship.composition
lessons). metaobjects/meta.content.yaml declares Lesson (the
relationship.composition target ProgramSummary.lessonCount aggregates
over).
From the repo root:
cd server/typescript
bun run packages/cli/bin/meta.ts gen --cwd ../../examples/advanced-modeling
bun run packages/cli/bin/meta.ts verify --cwd ../../examples/advanced-modeling --codegen
bun run packages/cli/bin/meta.ts verify --cwd ../../examples/advanced-modeling --templates --prompts templates(--prompts templates is required for --templates: this example's mustache
lives under templates/, the directory the render-helper codegen generator
resolves at generation time — not verify's own prompts/ default. See
"Gotchas" below.)
examples/ is not part of the Bun workspace (the repo-root
package.json globs only server/typescript/packages/* and
client/web/packages/*), so there is no local install step — the CLI's
config loader resolves the @metaobjectsdev/* imports in
metaobjects.config.ts from the CLI's own install regardless of the
example's node_modules (which doesn't exist).
server/typescript/packages/cli/test/integration/advanced-modeling-drift.test.ts
runs three checks against this directory as committed:
meta verify --templates --prompts templates— the declaredtemplate.output's mustache is drift-free against its payload VO's field tree.meta verify --codegen—src/generated/**as committed is byte-identical to a fresh regen from the current metadata.- Every committed
.ts/.tsxfile undersrc/generated/parses as valid TypeScript (Bun.Transpiler, resolution-free —examples/has nonode_modulesto resolvedrizzle-orm/zod/@metaobjectsdev/react/etc. against, so a full typecheck isn't available here). This check exists because (2) alone is a pure string diff: a generator that emits deterministically invalid syntax still "matches a fresh regen" cleanly. That is not hypothetical — see "Gotchas" below.
That test file lives in the cli package, whose full bun test suite is
already part of scripts/ci-local.sh's gate_conf_ts step (the ts-fast
lane) — this is a fast-lane addition to an existing package suite, not a
new CI job, per the plan's resolved decision.
Recon-first (per the plan's Step 1) still surfaced four real issues once the
metadata actually ran through the full meta gen → meta verify pipeline —
worth recording here since they're exactly the kind of thing a from-memory
authoring pass would miss:
identity.referencedoes not register@onDelete/@onUpdate.docs/features/relationships.mddocuments these attrs as applying to "both"relationship.*andidentity.reference; onlyrelationship.*(all three subtypes —association/aggregation/composition, each with a sensible default: composition→cascade, aggregation→set-null, association→restrict) actually has them registered. The referential action belongs on the relationship side (Program.lessons/Program.purchases), never restated on theidentity.referenceFK side. The doc is stale on this one point; this example follows the registered vocabulary, not the doc.routesFile()/formFile()don't special-case a pureobject.value.entityFile()already renders a VO as shape-only (interface + Zod, no Drizzle table) — butroutesFile/formFileassumed every entity has a backing table/entity-constants object, and would emit routes/forms referencing exports that don't exist for a VO. Worked around here via each generator'sfilteroption (metaobjects.config.ts) rather than fixed incodegen-ts, since it's also the semantically correct call for this domain — none ofSyllabusSection/InstructorProfile/ProgramDescriptionPayloadis independently CRUD-addressable or form-submitted.render-helper's payload type wasn't package-stripped. A resolved@payloadRefcan arrive fully-qualified (acme::learn::X); every other TS- identifier-emitting generator (payload-codegen.ts,entity-file.ts, …) already callsstripPackage()on a resolved ref before emitting it as an identifier —render-helper.tsdidn't, so atemplate.outputpayload in a named package emittedimport type { acme::learn::X } from "./acme::learn::X.js", which is not legal TypeScript. Fixed incodegen-ts/src/templates/render-helper.ts(this PR) — narrow, mirrors the existing pattern, covered by the existing render-helper test suite.meta verify --codegendidn't carry the project'stemplates/dir into its throwaway regen root, so any project usingrender-helper()(or other provider-backed template generators) would spuriously fail--codegenwith an "unresolved" error unrelated to actual drift. Fixed incli/src/lib/codegen-drift.ts(this PR) — mirrors the existing outDir-copy logic, extended to the templates dir.- A relative
outDirresolved against the ambientprocess.cwd(), not the resolved--cwd.meta gen/meta verify --codegensilently wrote/read the wrong location whenever the CLI was invoked with--cwdpointing somewhere other than the actual process working directory (exactly this drift gate's own invocation pattern: abun testrun frompackages/cli/targetingexamples/advanced-modeling/). The common case — runningmeta genfrom inside your own project, no--cwd— happened to work by accident (process.cwd() === projectRoot), which is why this had gone unnoticed. Fixed incodegen-ts/src/runner.ts(the write path) andcli/src/lib/codegen-drift.ts(the read path), both anchoring a relativeoutDirto the resolved project root.
None of the above are metamodel vocabulary gaps (ADR-0023) — they're either a
stale doc claim or codegen/CLI bugs, fixed narrowly and covered by the
existing test suites (all four ports' conformance corpora and the cli/
codegen-ts package suites stayed green throughout).