Skip to content

chore(metaobjects): upgrade to MetaObjects 1.0.0 and adopt generated names constants - #36

Merged
dmealing merged 8 commits into
mainfrom
chore/metaobjects-1.0
Sep 9, 2026
Merged

chore(metaobjects): upgrade to MetaObjects 1.0.0 and adopt generated names constants#36
dmealing merged 8 commits into
mainfrom
chore/metaobjects-1.0

Conversation

@dmealing

@dmealing dmealing commented Sep 9, 2026

Copy link
Copy Markdown
Member

Intent

Take this project — the public reference consumer for MetaObjects — off the MetaObjects 0.x/release-candidate line and onto the released MetaObjects 1.0.0, and land that as the project's 1.0 upgrade PR.

Context a reviewer reading only the diff would not have: this branch is one of nine maintainer-owned adopter estates that were deliberately run against every 1.0 release candidate in turn (rc.1, rc.2, rc.4, rc.5, rc.6, rc.7, then 1.0.0 itself) to satisfy the MetaObjects 1.0 release gate that an adopter estate must exercise the candidate with the drift gates ENFORCED before promote. That is why the history is a sequence of version-move commits rather than one bump: each was a real gate run at that candidate, and each commit message records the counts it produced. Keep that history; do not squash the story out of it.

Deliberate decisions embedded in the diff:

  • namesFile() was added to generators: [...] and the physical table/column names are now spelled through the generated Names constants instead of string literals. This is the maintainer's added requirement for the 1.0 pass. Typed ORM handles (Drizzle column objects) were deliberately NOT replaced with string constants — that would be a regression — and neither were JSON payload keys or result-set aliases.
  • The four owned generators under codegen/generators/ are scaffold-and-own copies (MetaObjects ADR-0034). They are the project's code to edit, not vendored library files.
  • Files under src/db/generated/ are codegen output. Hand edits there are preserved by a three-way merge and are legitimate, but the correct way to change that output is the generator or the metadata, not the file.
  • The agent context under .metaobjects/ is refreshed by 'meta init --docs-only --refresh-docs'; its diff is upstream content, not authored here.

Acceptance: every gate this project itself declares must pass at 1.0.0 — npm run check (meta gen, meta verify, meta verify:db against a local D1, tsc -b, vitest) — and CI must be green before merge.

What Changed

  • Bump all six @metaobjectsdev/* packages from ^0.24.x to pinned 1.0.0 and regenerate src/db/generated/ with the 1.0 toolchain; the commit history deliberately walks each 1.0 release candidate (rc.1 → rc.7 → 1.0.0), one gate-run commit per candidate.
  • Adapt the owned scaffold-and-own generators under codegen/generators/ to the 1.0 engine (ts-poet combinators imported from @metaobjectsdev/codegen-ts to share one instance, TPH-subtype and write-through entity routing) and wire the new namesFile() generator in metaobjects.config.ts, emitting Council.names.ts / CouncilTurn.names.ts so physical table and column names are declared once as importable constants.
  • Replace hardcoded council-status string literals in src/db/queries.ts, src/render/share-html.ts, and src/render/share-page.tsx with the generated CouncilStatusEnum.enum.* members (no behavior change), and refresh the generated agent context (.metaobjects/, .claude/skills/metaobjects-*) plus MetaObjects version refs in CLAUDE.md and docs/runbook.md.

Risk Assessment

✅ Low: A well-bounded, behavior-equivalent version move: I verified statically that the names constants resolve to the exact pre-change physical table/column/check-constraint names matching migrations DDL, enum-constant adoption is value-identical, drift-gate and agent-context hash state match every committed file, and no consumer reads any removed descriptor field — with build/test/CI enforcement owned by later pipeline steps.

Testing

Drove the full npm run check gate plus the dedicated demo:drift drift gate live and both came back completely green with zero drift against the committed 1.0.0 generated output; added an adversarial hand-edit to a generated names artifact and confirmed the drift gate actually fails on it, then confirmed it goes green again once reverted. Beyond static gates, stood up the real Worker with wrangler dev and drove the actual product: a real POST /api/council SSE convene persisted through the regenerated Council/CouncilTurn Drizzle schema into a local D1 database, and the resulting council rendered correctly on both the HTML share route and its JSON sibling; a second, manually-seeded council was screenshotted end-to-end in a real browser for visual proof. The only gap is that real Anthropic-authored wizard content wasn't observed, since this sandbox's ANTHROPIC_API_KEY is empty — that's outside this diff's changed surface (no LLM-calling code changed) and the orchestrator's graceful-degradation path that fired instead is itself legitimate, exercised code, not a stub.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Every gate this project declares passes at MetaObjects 1.0.0 (npm run check) ✅ pass live Ran meta gen (31/31 files unchanged — zero drift vs. committed generated output), verify:portraits, verify:db (wrangler d1 migrations apply --local + meta verify --dialect d1 --d1 DB → "schema…
demo:drift (meta verify --templates --codegen --prompts) is green at 1.0.0 ✅ pass live npm run demo:drift → "22 template(s) clean" and "every file's generated contribution is current (no codegen drift)", exit 0. 03-demo-drift.log, reproduced fresh.
Adversarial: the drift gate actually catches a hand-edited generated <Entity>Names artifact ✅ pass live Hand-edited src/db/generated/Council.names.ts and re-ran demo:drift: it failed (exit 1) with "codegen drift (1 file(s) differ from a fresh regen): ~ src/db/generated/Council.names.ts (hand-edited; a n…
Physical table/column names are sourced from the generated <Entity>Names constants, not string literals, and match the live D1 schema ✅ pass live Queried sqlite_master on the local D1 (wrangler d1 execute --local): CREATE TABLE councils/council_turns exactly matches the column names declared in Council.names.ts/CouncilTurn.names.ts (e.g. verd…
End-to-end council convene (POST /api/council SSE) persists through the regenerated DB layer and renders on the share route ✅ pass live Started wrangler dev, POSTed a real question to /api/council, captured the live SSE stream (event: council/error.../verdict/done), then confirmed the council + 5 turns were persisted in local D1 wit…
Metadata-driven slug validator rejects a share-route slug using the excluded alphabet (0/O/1/I/l) ✅ pass live GET /c/nomistk1 (contains a disallowed '1') and GET /c/bad!! both returned 404 from the running dev server, matching the Council.id shortSlug regex sourced from metaobjects/abstracts/meta-short-slug-f…
A council seeded directly in D1 renders correctly through the full generated-schema → SSR pipeline, visually ✅ pass live Seeded a valid-slug council + turn row directly into local D1, hit GET /c/nomistk2 on the running Worker, and captured a real browser screenshot (Chrome headless) of the rendered share page showing th…
Evidence: npm run check (meta gen zero-drift, verify:portraits, verify:db vs local D1, tsc -b, vitest) — full green run

> check
> npm run gen:db && npm run verify && npm run verify:db && tsc -b && vitest run


> gen:db
> meta gen

gen[31]{file,status}:
  src/db/generated/CouncilTurn.ts,unchanged
  src/db/generated/Council.ts,unchanged
  src/db/generated/Dissent.ts,unchanged
  src/db/generated/RivalLens.ts,unchanged
  src/db/generated/RosterLens.ts,unchanged
  src/db/generated/SourceLens.ts,unchanged
  src/db/generated/TurnLens.ts,unchanged
  src/db/generated/VerdictOutput.ts,unchanged
  src/db/generated/VerdictSystemPayload.ts,unchanged
  src/db/generated/VerdictUserPayload.ts,unchanged
  src/db/generated/WizardOutput.ts,unchanged
  src/db/generated/WizardSystemPayload.ts,unchanged
  src/db/generated/WizardUserPayload.ts,unchanged
  src/db/generated/Wizard.ts,unchanged
  src/db/generated/CouncilFramePayload.ts,unchanged
  src/db/generated/DoneFramePayload.ts,unchanged
  src/db/generated/ErrorFramePayload.ts,unchanged
  src/db/generated/VerdictFramePayload.ts,unchanged
  src/db/generated/WizardFramePayload.ts,unchanged
  src/db/generated/enums.ts,unchanged
  src/db/generated/CouncilTurn.queries.ts,unchanged
  src/db/generated/Council.queries.ts,unchanged
  src/db/generated/CouncilTurn.names.ts,unchanged
  src/db/generated/Council.names.ts,unchanged
  src/db/generated/index.ts,unchanged
  src/render/generated/prompts.ts,unchanged
  src/render/generated/templates.ts,unchanged
  src/personas/generated/wizards.ts,unchanged
  src/personas/generated/registry.ts,unchanged
  src/llm/generated/toolcalls.ts,unchanged
  src/web/generated/council-frames.ts,unchanged
summary: 31 unchanged
help[2]: typecheck the generated code with `npx tsc`,create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`
antiPatterns:
  status: ran
  total: 0
  rows: []

> verify
> npm run verify:portraits


> verify:portraits
> tsx scripts/verify-portraits.ts

verify-portraits OK: 10 wizards × portraits matched

> verify:db
> npm run db:migrate:local && meta verify --dialect d1 --d1 DB


> db:migrate:local
> wrangler d1 migrations apply wizardsofodd --local


 ⛅️ wrangler 4.127.0 (update available 4.130.0)
───────────────────────────────────────────────
Resource location: local 

Use --remote if you want to access the remote instance.

Migrations to be applied:
┌──────────────────────────────┐
│ name                         │
├──────────────────────────────┤
│ 0001_init.sql                │
├──────────────────────────────┤
│ 0002_structured_outputs.sql  │
├──────────────────────────────┤
│ 0003_verdict_agree_split.sql │
├──────────────────────────────┤
│ 0004_followup_rounds.sql     │
├──────────────────────────────┤
│ 0005_drop_dead_followups.sql │
├──────────────────────────────┤
│ 0006_reconcile-schema.sql    │
└──────────────────────────────┘
? About to apply 6 migration(s)
Your database may not be available to serve requests during the migration, continue?
🤖 Using fallback value in non-interactive context: yes
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 3 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ 🕒️    │
└──────────────────────────────┴────────┘
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 14 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ ✅     │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ 🕒️    │
└──────────────────────────────┴────────┘
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 4 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ ✅     │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ 🕒️    │
└──────────────────────────────┴────────┘
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 3 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ ✅     │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ ✅     │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ 🕒️    │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ 🕒️    │
└──────────────────────────────┴────────┘
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ ✅     │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ ✅     │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ 🕒️    │
└──────────────────────────────┴────────┘
🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 15 commands executed successfully.
┌──────────────────────────────┬────────┐
│ name                         │ status │
├──────────────────────────────┼────────┤
│ 0001_init.sql                │ ✅     │
├──────────────────────────────┼────────┤
│ 0002_structured_outputs.sql  │ ✅     │
├──────────────────────────────┼────────┤
│ 0003_verdict_agree_split.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0004_followup_rounds.sql     │ ✅     │
├──────────────────────────────┼────────┤
│ 0005_drop_dead_followups.sql │ ✅     │
├──────────────────────────────┼────────┤
│ 0006_reconcile-schema.sql    │ ✅     │
└──────────────────────────────┴────────┘
meta: meta verify — schema in sync with d1:DB (local).
verify[6]{gate,ran,ok}:
  templates,false,true
  schema,true,true
  codegen,false,true
  docs,false,true
  requirements,true,true
  replay,false,true
exitCode: 0
summary: "2 gate(s) ran, all clean"
help[1]: no drift and nothing advisory to answer — nothing to do
antiPatterns:
  status: ran
  total: 0
  rows: []
requirements:
  status: ran
  total: 0
  rows: []
notRepresented[2]: "per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr

 RUN  v4.1.11 ~/.no-mistakes/worktrees/1ee7d9cb9cbc/01M22VQ0FV5NXXMCPJPKYDN41S


 Test Files  33 passed (33)
      Tests  204 passed (204)
   Start at  06:48:01
   Duration  6.92s (transform 10.71s, setup 58.45s, import 3.57s, tests 12.04s, environment 4.46s)
Evidence: demo:drift baseline pass (templates+codegen+prompts clean)

> demo:drift
> meta verify --templates --codegen --prompts data/templates

meta: meta verify — 22 template(s) clean.
meta: meta verify — every file's generated contribution is current (no codegen drift).
verify[6]{gate,ran,ok}:
  templates,true,true
  schema,false,true
  codegen,true,true
  docs,false,true
  requirements,true,true
  replay,false,true
exitCode: 0
summary: "3 gate(s) ran, all clean"
help[1]: no drift and nothing advisory to answer — nothing to do
antiPatterns:
  status: ran
  total: 0
  rows: []
requirements:
  status: ran
  total: 0
  rows: []
notRepresented[2]: "per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
Evidence: Adversarial: hand-edited Council.names.ts → demo:drift correctly fails with exit 1 (codegen drift detected)

> demo:drift
> meta verify --templates --codegen --prompts data/templates

meta: meta verify — 22 template(s) clean.
meta: meta verify — codegen drift (1 file(s) differ from a fresh regen):
meta:   ~ src/db/generated/Council.names.ts (hand-edited; a names artifact must match a fresh regen exactly — put constants of your own in a file of your own)
meta: For the names artifact(s) above, 'meta gen' will NOT help — it preserves a hand edit by design. Revert the edit (or delete the file and re-run 'meta gen' to re-emit it), and keep constants of your own in a file of your own.
verify[6]{gate,ran,ok}:
  templates,true,true
  schema,false,true
  codegen,true,false
  docs,false,true
  requirements,true,true
  replay,false,true
exitCode: 1
summary: 1 of 3 gate(s) failed (codegen)
help[1]: the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only
antiPatterns:
  status: ran
  total: 0
  rows: []
requirements:
  status: ran
  total: 0
  rows: []
notRepresented[2]: "per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
Evidence: demo:drift green again after reverting the hand-edit

> demo:drift
> meta verify --templates --codegen --prompts data/templates

meta: meta verify — 22 template(s) clean.
meta: meta verify — every file's generated contribution is current (no codegen drift).
verify[6]{gate,ran,ok}:
  templates,true,true
  schema,false,true
  codegen,true,true
  docs,false,true
  requirements,true,true
  replay,false,true
exitCode: 0
summary: "3 gate(s) ran, all clean"
help[1]: no drift and nothing advisory to answer — nothing to do
antiPatterns:
  status: ran
  total: 0
  rows: []
requirements:
  status: ran
  total: 0
  rows: []
notRepresented[2]: "per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
Evidence: Real SSE stream from POST /api/council against running wrangler dev
event: council
data: {"id":"AHBrPdUb"}

event: error
data: {"wizardId":"grumbel","wizardName":"Grumbel the Overcaffeinated","reason":"Grumbel is lost in thought and does not answer."}

event: error
data: {"wizardId":"vexil","wizardName":"Madame Vexil the Riddling","reason":"Madame Vexil is lost in thought and does not answer."}

event: error
data: {"wizardId":"lorekeeper","wizardName":"The Lorekeeper of Footnotes","reason":"The Lorekeeper is lost in thought and does not answer."}

event: error
data: {"wizardId":"ozzimandias","wizardName":"Ozzimandias the Inevitable","reason":"Ozzimandias is lost in thought and does not answer."}

event: error
data: {"wizardId":"pib","wizardName":"Pib the Smol","reason":"Pib is lost in thought and does not answer."}

event: verdict
data: {"verdict":{"stance":"unanswerable","confidence":0,"takeMarkdown":"No wizard was able to answer this time, so the Guild has no verdict to offer. Please try again in a moment.","dissents":[],"evidenceQuality":"none","agreements":[],"splits":[],"verifyNote":""}}

event: done
data: {"ok":true}
Evidence: D1 state after live convene: council + 5 turns persisted through the regenerated Council/CouncilTurn Drizzle tables

 ⛅️ wrangler 4.127.0 (update available 4.130.0)
───────────────────────────────────────────────
Resource location: local 

Use --remote if you want to access the remote instance.

🌀 Executing on local database wizardsofodd (37b76295-e8f3-40af-bd2c-049264101eb1) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
[
  {
    "results": [
      {
        "id": "AHBrPdUb",
        "status": "complete",
        "q": "Should I upgrade my dependencies to 1.0.0?",
        "verdict_stance": "unanswerable",
        "verdict_confidence": 0,
        "done_ts": 1
      }
    ],
    "success": true,
    "meta": {
      "duration": 0
    }
  },
  {
    "results": [
      {
        "council_id": "AHBrPdUb",
        "ordinal": 1,
        "wizard_id": "grumbel",
        "kind": "error",
        "reason": "Grumbel is lost in thought and does not "
      },
      {
        "council_id": "AHBrPdUb",
        "ordinal": 2,
        "wizard_id": "vexil",
        "kind": "error",
        "reason": "Madame Vexil is lost in thought and does"
      },
      {
        "council_id": "AHBrPdUb",
        "ordinal": 3,
        "wizard_id": "lorekeeper",
        "kind": "error",
        "reason": "The Lorekeeper is lost in thought and do"
      },
      {
        "council_id": "AHBrPdUb",
        "ordinal": 4,
        "wizard_id": "ozzimandias",
        "kind": "error",
        "reason": "Ozzimandias is lost in thought and does "
      },
      {
        "council_id": "AHBrPdUb",
        "ordinal": 5,
        "wizard_id": "pib",
        "kind": "error",
        "reason": "Pib is lost in thought and does not answ"
      }
    ],
    "success": true,
    "meta": {
      "duration": 0
    }
  }
]
Evidence: SSR share page (GET /c/:slug) for the live-persisted council, rendered by share-html.ts/share-page.tsx
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>The Wizards of Odd weigh in on: Should I upgrade my dependencies to 1.0.0?</title>
    <meta name="robots" content="noindex" />
    <link rel="stylesheet" href="/styles.css" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
    <meta property="og:title" content="The Wizards of Odd weigh in on: Should I upgrade my dependencies to 1.0.0?" />
    <meta property="og:description" content="No wizard was able to answer this time, so the Guild has no verdict to offer. Please try again in a moment." />
    <meta property="og:image" content="/portraits/verdict-seal.webp" />
    <meta property="og:type" content="article" />
  </head>
  <body>
    <div id="root"><main class="homepage share-page"><header class="masthead"><h1>The Wizards of Odd</h1></header><section class="question-block"><h2>The question</h2><blockquote>Should I upgrade my dependencies to 1.0.0?</blockquote></section><section class="council"><article class="card card-error" aria-label="Grumbel the Overcaffeinated error"><img class="portrait" src="/portraits/grumbel.webp" alt=""/><div class="body"><header><h2>Grumbel the Overcaffeinated</h2><span class="stance stance-error">unable</span></header><p class="reason">Grumbel is lost in thought and does not answer.</p></div></article><article class="card card-error" aria-label="Madame Vexil the Riddling error"><img class="portrait" src="/portraits/vexil.webp" alt=""/><div class="body"><header><h2>Madame Vexil the Riddling</h2><span class="stance stance-error">unable</span></header><p class="reason">Madame Vexil is lost in thought and does not answer.</p></div></article><article class="card card-error" aria-label="The Lorekeeper of Footnotes error"><img class="portrait" src="/portraits/lorekeeper.webp" alt=""/><div class="body"><header><h2>The Lorekeeper of Footnotes</h2><span class="stance stance-error">unable</span></header><p class="reason">The Lorekeeper is lost in thought and does not answer.</p></div></article><article class="card card-error" aria-label="Ozzimandias the Inevitable error"><img class="portrait" src="/portraits/ozzimandias.webp" alt=""/><div class="body"><header><h2>Ozzimandias the Inevitable</h2><span class="stance stance-error">unable</span></header><p class="reason">Ozzimandias is lost in thought and does not answer.</p></div></article><article class="card card-error" aria-label="Pib the Smol error"><img class="portrait" src="/portraits/pib.webp" alt=""/><div class="body"><header><h2>Pib the Smol</h2><span class="stance stance-error">unable</span></header><p class="reason">Pib is lost in thought and does not answer.</p></div></article><article class="card card-verdict" aria-label="Verdict of the Guild"><img class="portrait" src="/portraits/verdict-seal.webp" alt=""/><div class="body"><header><h2>Verdict of the Guild</h2><span class="stance stance-unanswerable">Unanswerable</span><span class="confidence" title="confidence 0">0<!-- -->%</span><span class="evidence evidence-none">evidence: <!-- -->none</span></header><div class="take">No wizard was able to answer this time, so the Guild has no verdict to offer. Please try again in a moment.</div></div></article></section></main></div>
  </body>
</html>
Evidence: Share JSON endpoint (GET /c/:slug.json) for the live-persisted council
{"council":{"id":"AHBrPdUb","visitorId":"test-visitor-01","question":"Should I upgrade my dependencies to 1.0.0?","status":"complete","verdictStance":"unanswerable","verdictConfidence":0,"verdictMarkdown":"No wizard was able to answer this time, so the Guild has no verdict to offer. Please try again in a moment.","verdictEvidenceQuality":"none","dissents":[],"verdictAgreements":[],"verdictSplits":[],"verdictVerifyNote":"","createdAt":1788951089869,"completedAt":1788951090430},"turns":[{"councilId":"AHBrPdUb","ordinal":1,"round":0,"wizardId":"grumbel","wizardName":"Grumbel the Overcaffeinated","kind":"error","stance":"abstains","takeMarkdown":"Grumbel is lost in thought and does not answer.","oneLineSummary":"Grumbel is lost in thought and does not answer.","confidence":0,"keyClaims":[],"citations":null},{"councilId":"AHBrPdUb","ordinal":2,"round":0,"wizardId":"vexil","wizardName":"Madame Vexil the Riddling","kind":"error","stance":"abstains","takeMarkdown":"Madame Vexil is lost in thought and does not answer.","oneLineSummary":"Madame Vexil is lost in thought and does not answer.","confidence":0,"keyClaims":[],"citations":null},{"councilId":"AHBrPdUb","ordinal":3,"round":0,"wizardId":"lorekeeper","wizardName":"The Lorekeeper of Footnotes","kind":"error","stance":"abstains","takeMarkdown":"The Lorekeeper is lost in thought and does not answer.","oneLineSummary":"The Lorekeeper is lost in thought and does not answer.","confidence":0,"keyClaims":[],"citations":null},{"councilId":"AHBrPdUb","ordinal":4,"round":0,"wizardId":"ozzimandias","wizardName":"Ozzimandias the Inevitable","kind":"error","stance":"abstains","takeMarkdown":"Ozzimandias is lost in thought and does not answer.","oneLineSummary":"Ozzimandias is lost in thought and does not answer.","confidence":0,"keyClaims":[],"citations":null},{"councilId":"AHBrPdUb","ordinal":5,"round":0,"wizardId":"pib","wizardName":"Pib the Smol","kind":"error","stance":"abstains","takeMarkdown":"Pib is lost in thought and does not answer.","oneLineSummary":"Pib is lost in thought and does not answer.","confidence":0,"keyClaims":[],"citations":null}]}
![Rendered share page in an actual browser (Chrome headless) for a manually-seeded council, proving the generated-names-driven Drizzle schema round-trips through SSR to real HTML](https://github.com/user-attachments/assets/6c7958ee-eb89-4e39-9247-f5f6fb1fb6fa)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ src/db/queries.ts:129 - setCouncilError still spells the Council.status member by hand (status: &#34;error&#34;), while the change's stated purpose (commit 3610293: "stop spelling enum members by hand"; "src/db/queries.ts … now read CouncilStatusEnum.enum.<member>") converted the three sibling sites in this same file (lines 34, 62, 121). Unlike the documented StreamStatus exceptions in useCouncilStream/HomePage (a deliberately separate client vocabulary), this is the same vocabulary, same column, same file — a missed site, not a documented decision. No functional impact: the Drizzle column's enum typing still type-checks the literal, so this is mechanical incompleteness of the conversion itself. The same-file kind: &#34;error&#34; at line 92 is the same pattern now that the engine emits CouncilTurnKindEnum.
  • ℹ️ CLAUDE.md:28 - The project CLAUDE.md tech-stack table still pins @metaobjectsdev/*@0.15.18, and docs/runbook.md:283 still claims "the project now tracks the current published line (@metaobjectsdev/*@0.15.x)" — both already stale at base (0.24.5) and now two version lines wrong after this 1.0.0 move. This is the exact class of stale version claim commit a0cda82 deliberately corrected in metaobjects.config.ts's comments ("two stale claims … now wrong in a way that would mislead someone reading it as the reference it is meant to be"); these two were missed. Mechanical doc correction inside the version-move PR's own subject.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Every gate this project declares passes at MetaObjects 1.0.0 (npm run check) ✅ pass live Ran meta gen (31/31 files unchanged — zero drift vs. committed generated output), verify:portraits, verify:db (wrangler d1 migrations apply --local + meta verify --dialect d1 --d1 DB → "schema…
demo:drift (meta verify --templates --codegen --prompts) is green at 1.0.0 ✅ pass live npm run demo:drift → "22 template(s) clean" and "every file's generated contribution is current (no codegen drift)", exit 0. 03-demo-drift.log, reproduced fresh.
Adversarial: the drift gate actually catches a hand-edited generated <Entity>Names artifact ✅ pass live Hand-edited src/db/generated/Council.names.ts and re-ran demo:drift: it failed (exit 1) with "codegen drift (1 file(s) differ from a fresh regen): ~ src/db/generated/Council.names.ts (hand-edited; a n…
Physical table/column names are sourced from the generated <Entity>Names constants, not string literals, and match the live D1 schema ✅ pass live Queried sqlite_master on the local D1 (wrangler d1 execute --local): CREATE TABLE councils/council_turns exactly matches the column names declared in Council.names.ts/CouncilTurn.names.ts (e.g. verd…
End-to-end council convene (POST /api/council SSE) persists through the regenerated DB layer and renders on the share route ✅ pass live Started wrangler dev, POSTed a real question to /api/council, captured the live SSE stream (event: council/error.../verdict/done), then confirmed the council + 5 turns were persisted in local D1 wit…
Metadata-driven slug validator rejects a share-route slug using the excluded alphabet (0/O/1/I/l) ✅ pass live GET /c/nomistk1 (contains a disallowed '1') and GET /c/bad!! both returned 404 from the running dev server, matching the Council.id shortSlug regex sourced from metaobjects/abstracts/meta-short-slug-f…
A council seeded directly in D1 renders correctly through the full generated-schema → SSR pipeline, visually ✅ pass live Seeded a valid-slug council + turn row directly into local D1, hit GET /c/nomistk2 on the running Worker, and captured a real browser screenshot (Chrome headless) of the rendered share page showing th…
  • npm run gen:db (meta gen) — 31/31 unchanged, zero drift
  • npm run verify (verify:portraits)
  • npm run verify:db (wrangler d1 migrations apply --local + meta verify --dialect d1 --d1 DB)
  • npx tsc -b
  • npx vitest run (33 files / 204 tests)
  • npm run demo:drift (meta verify --templates --codegen --prompts data/templates)
  • Adversarial hand-edit of src/db/generated/Council.names.ts to confirm demo:drift fails, then reverted and re-verified green
  • wrangler d1 execute --local against sqlite_master to confirm physical schema matches the generated <Entity>Names constants
  • Live wrangler dev run: POST /api/council (SSE), GET /c/:slug, GET /c/:slug.json, GET /api/health
  • Manual D1 seed of a valid-slug council + turn, then GET /c/:slug screenshotted in headless Chrome
  • GET /c/<slug-with-disallowed-char> and /c/bad!! to confirm the metadata-sourced slug validator 404s
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 8 commits September 7, 2026 10:07
Package pins → 1.0.0-rc.1 (metamodelVersion 1.0). Agent context + skills refreshed.

The owned generators were the real work. codegen/generators/{entity,queries,barrel}.ts
dated from around 0.23.2 while package.json pinned ^0.24.5 — five minor lines stale — and
carried three patterns MetaObjects has since retired:

  - a bare `import { code, joinCode } from "ts-poet"`, which is the 0.21.6 split-tree
    defect: with a globally-installed or linked CLI the project and the CLI resolve
    different physical ts-poet copies, `instanceof Code` fails across them, and every
    section stringifies with its own import header (duplicate imports, TS2300).
  - `skipNonQueryable` gating persistability on `e.subType !== OBJECT_SUBTYPE_VALUE` —
    the subtype check #248 replaced with "derives from a declared source.rdb".
  - the delegation branch missing `isWriteThrough` (#214).

Nothing had ever reported any of this. They were brought forward by three-way merge
against 0.23.2 as the base, NOT by `meta eject --force`, which replaces the file
wholesale — this copy of queries.ts is deliberately trimmed to READ-ONLY finders (this
app's writes are bespoke) and a force-eject silently destroys that. The trim is preserved;
the #203 insertPreserving block that came with the merge is dropped along with the rest of
the write surface it belongs to.

Generated output changes are the expected 1.0 ones: $apiPrefix leaves the entity
descriptor (a deployment fact, now supplied by the runtime provider — nothing here read
it), enum fields report view "dropdown" rather than "text", and the Drizzle Db type alias
gains its third type parameter.

Gates: meta verify --templates --codegen --prompts data/templates clean · D1 schema in
sync · tsc -b clean · 204/204 vitest.

.metaobjects/adoption-audit.json records the audit. Two findings are upstream product
defects, not this repo's: an inline field.enum's members are inlined four times per
generated file and exported nowhere (a shared package-level enum does get a named const),
and the descriptor now says view "dropdown" while carrying no options.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3v6M6uqECZ2Sb5eUv6YY
… by hand

rc.2 carries three fixes that came out of upgrading this repo to rc.1, and two of them
change what is generated here:

  - a `field.enum`'s form descriptor now carries its `options`. 0.25.0 moved an enum field's
    view from "text" to "dropdown" and never gave the descriptor a member list, so it asked
    for a <select> and supplied nothing to fill one.
  - an inline `field.enum` now emits a runtime constant (`CouncilStatusEnum`) beside its
    type alias. The alias is erased at runtime, so code needing a MEMBER had nothing to
    import — which is why "complete" was a literal in five files here.

`src/db/queries.ts`, `src/render/share-html.ts` and `src/render/share-page.tsx` now read
`CouncilStatusEnum.enum.<member>`. The `"complete"` in `useCouncilStream.ts` and
`HomePage.tsx` is deliberately LEFT: that is `StreamStatus`, a client-only state machine
whose members merely overlap with Council.status. Binding it to the entity's enum would
couple two unrelated vocabularies because a grep matched.

The owned generators are re-synced against the rc.2 reference. entity.ts and barrel.ts are
now byte-identical to it — the previous pass had left entity.ts without the engine `ts-poet`
import block and barrel.ts without the ADR-0044 collision-safe emitted-name call, and
`meta eject`'s new staleness report is what caught both. queries.ts is the one that stays
different, by 9 lines: the READ-ONLY trim, re-applied on top of the current reference so it
now carries the #248 source-derived persistability check and the #214 write-through branch
it had been missing.

Gates: verify --templates --codegen clean · D1 schema in sync · tsc -b · 204/204 vitest ·
production build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3v6M6uqECZ2Sb5eUv6YY
…once

The names artifact is the 1.0 headline feature and this project emitted none of
it, because `namesFile()` was never in `generators: [...]`. That is not an
oversight anyone here made: three of the agent-context skills stated "on
TypeScript (`meta init`), C# and Python the names generator is in the default
suite", and there is no default suite on TypeScript — `generators: [...]` IS the
complete list, exactly like the JVM's `<generators>`. `meta init` SCAFFOLDS it,
into a project initialized at 1.0; this one predates that, and upgrading a
package never edits a config. Upstream corrected the skills; the refreshed copies
are in this commit.

Wiring it changes what the entity generator emits: `councils` becomes
`CouncilNames.sources.primary.table` and every `text("visitor_id")` becomes
`text(CouncilNames.fields.visitorId.column)`. That diff is the point — the
physical name is now spelled once per run, in the artifact resolved from the same
`source.rdb` declaration the DDL comes from, and everything else references it.

Nothing hand-written had to change. A sweep of all 28 declared physical names
across src/, test/, scripts/ and codegen/ (generated dirs excluded) found zero
literal spellings: the only hits are the Drizzle table objects, which are the
typed handle the doctrine prefers. The .sql files under migrations/ are
`meta migrate` output, and test/setup.ts replays those files rather than
restating a single name.

Also corrects two stale claims in the config's own comments, both now wrong in a
way that would mislead someone reading it as the reference it is meant to be:

  - entity/queries/routes/barrel are not "deprecated on
    @metaobjectsdev/codegen-ts/generators" any more — 1.0 REMOVED them, and
    there is no export left to fall back to.
  - "only the entity/queries/routes/barrel generators are ownable" conflated two
    different axes. `meta eject --list` names nine; `names` and `routes-hono`
    have ownable copies AND are still exported. Removed-from-the-package and
    has-an-ownable-copy are independent facts, so the comment now states both.

`namesFile` is imported from the package rather than ejected: there is no reason
to fork an artifact whose shape we do not intend to change, and the config
already says not to cargo-cult owning everything.

Gates, all at rc.4: `meta verify --templates --codegen --prompts` clean (22
templates), `meta verify --dialect d1 --d1 DB` in sync, `tsc -b` clean, 204 tests
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3v6M6uqECZ2Sb5eUv6YY
rc.4 was 29 commits stale. All three of its breaking changes are inert here: this
project registers no `useEntityFetcher`, calls `attr("isArray")` nowhere, and runs
no `verify --docs` gate. `meta gen` reports 31 unchanged — the generated output is
byte-identical to rc.4's.

Only the agent-context refresh moved: two skill reference pages gained the
`excluded.<column>` guidance and the per-driver identifier section, both of which
came out of this estate's own pass.

Gates at rc.5, every one CI runs: `npm ci` from a cold `node_modules` · `npm run
demo:drift` 3 gates clean · `npm run verify:db` (migrations applied to a fresh
local D1, then the schema drift gate) 2 gates clean · `npx tsc -b` · `npm run
build:web` · `npm test` 204 passed across 33 files. Identical counts to the rc.4
baseline taken before the bump.

NOT part of this change, but observed while taking that baseline: the committed
`public/assets/index-*.js` bundle is stale — a fresh `npm run build:web` emits a
different content hash, at rc.4 and at rc.5 alike, so it is unrelated to
MetaObjects. CI builds the bundle but never diffs it, which is why nothing has
said so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
Both of rc.6's breaking changes are inert here — no forge vocabulary, and `meta docs`
is not part of this project's surface — so this is the estate that answers "what does
the candidate cost a project it does not affect?" The answer is nothing: `npm run
check` (gen → verify → verify:db → tsc -b → vitest) exits 0 unchanged, with `meta gen`
reporting **31 unchanged**, `meta verify --dialect d1 --d1 DB` schema in sync against
the local D1, and 204 tests passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rc.6 -> rc.7, carrying the four CLI fixes the estates found while running rc.6.
`npm run check` (gen -> verify -> verify:db on local D1 -> tsc -b -> vitest)
exit 0: 31 unchanged, 204 tests, both drift gates clean. Identical to the rc.6
result, which is what a candidate that only changes CLI diagnostics should look
like from here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
Off the release candidates and onto 1.0.0 itself. This branch IS the 1.0 upgrade,
so it should name the release rather than a candidate. Agent context refreshed in
the same commit, which clears the staleness nudge the version move would otherwise
leave behind.

Gates were re-run at the released version rather than assumed from the candidate
round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
@dmealing
dmealing merged commit 826b868 into main Sep 9, 2026
2 checks passed
@dmealing
dmealing deleted the chore/metaobjects-1.0 branch September 9, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant