|
| 1 | +# Consumer Feedback: wizardsofodd.com — Friction in 0.6.0 |
| 2 | + |
| 3 | +**Status:** Proposal for review |
| 4 | +**Date:** 2026-05-25 |
| 5 | +**Author:** Claude (via Doug) during wizardsofodd Phase 2 / MO 0.6.0 adoption |
| 6 | +**Consumer project:** [`wizardsofodd.com`](../../../../wizardsofodd.com) — live Cloudflare Worker |
| 7 | +deployment with persistence + template-driven prompts + React UI in flight |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +`wizardsofodd.com` is in active mid-adoption of MO 0.6.0 across four surfaces: |
| 12 | +**persistence** (D1 via codegen-emitted Drizzle), **template-driven prompts** |
| 13 | +(`generateRenderHandle()` + `generatePayloadInterfaces()` from |
| 14 | +`@metaobjectsdev/codegen-ts`), **React UI** (Vite-built SPA in Workers Static |
| 15 | +Assets), and **projection vocabulary** (declared in metadata, hand-rolled |
| 16 | +queries). The full spec + plan lives at: |
| 17 | + |
| 18 | +- `wizardsofodd.com/docs/superpowers/specs/2026-05-25-metaobjects-0.6-mega-design.md` |
| 19 | +- `wizardsofodd.com/docs/superpowers/plans/2026-05-25-metaobjects-0.6-mega.md` |
| 20 | + |
| 21 | +Consumer's deployment shape (worth keeping in mind as you weigh fixes): |
| 22 | + |
| 23 | +- **Runtime:** Cloudflare Workers (V8 isolate — no Node-only deps allowed) |
| 24 | +- **Persistence:** Cloudflare D1 via `drizzle-orm/d1` |
| 25 | +- **Use of MO:** codegen output ONLY (no `@metaobjectsdev/runtime-ts` since |
| 26 | + Fastify/Kysely can't run in Workers). Codegen runs at dev/build time; |
| 27 | + generated artifacts are committed. |
| 28 | +- **Build chain:** `meta gen` → `vite build` → `wrangler deploy`. |
| 29 | + |
| 30 | +The good news up front: **0.6.0 is unblocking**. The fixes from 0.5.0 |
| 31 | +(`field.enum` registration, `--dialect d1`, `generateRenderHandle()`) are real |
| 32 | +and consumed. The items below are *friction*, not blockers. Each one is |
| 33 | +something a consumer pays a small recurring tax on, that would compound as |
| 34 | +the consumer audience widens. |
| 35 | + |
| 36 | +Items are ranked by leverage (consumer-pain × scope-of-affected-consumers ÷ |
| 37 | +estimated implementation effort). |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Tier 1 — Highest leverage, smallest cost |
| 42 | + |
| 43 | +### #1 — Auto-wire `generateRenderHandle()` + `generatePayloadInterfaces()` into a stock generator |
| 44 | + |
| 45 | +**Where:** `server/typescript/packages/codegen-ts/src/generators/` — add a new |
| 46 | +generator alongside `entityFile()`, `queriesFile()`, `barrel()`. |
| 47 | + |
| 48 | +**Today:** both helpers are exported from `@metaobjectsdev/codegen-ts/payload-codegen` |
| 49 | +(verified at `payload-codegen.ts:78-83` and `:89-106`) and individually work |
| 50 | +— but no generator in `generators/` calls them. Consumers wanting typed |
| 51 | +`renderXxx()` handles must write a custom `Generator` themselves and add it |
| 52 | +to their `metaobjects.config.ts`. From the wizardsofodd plan |
| 53 | +(Phase B, Task 5): |
| 54 | + |
| 55 | +```ts |
| 56 | +// We have to write this in metaobjects.config.ts to get typed prompt handles |
| 57 | +import { generatePayloadInterfaces, generateRenderHandle } from "@metaobjectsdev/codegen-ts"; |
| 58 | +const promptCodegen: Generator = { |
| 59 | + name: "prompt-render", |
| 60 | + emit(ctx) { |
| 61 | + const payloads = ctx.entities.filter(e => e.kind === "object.value"); |
| 62 | + const prompts = ctx.entities.filter(e => e.kind === "template.prompt"); |
| 63 | + return [{ |
| 64 | + path: "src/render/generated/prompts.ts", |
| 65 | + content: [ |
| 66 | + generatePayloadInterfaces(payloads), |
| 67 | + ...prompts.map(generateRenderHandle), |
| 68 | + ].join("\n"), |
| 69 | + }]; |
| 70 | + }, |
| 71 | +}; |
| 72 | +``` |
| 73 | + |
| 74 | +**Suggested fix:** Export a `promptRender()` (or `renderHandles()`) generator |
| 75 | +from `@metaobjectsdev/codegen-ts/generators` that does the above. Consumers |
| 76 | +who want typed prompts just add it to their generators list: |
| 77 | + |
| 78 | +```ts |
| 79 | +// metaobjects.config.ts (consumer side, after fix) |
| 80 | +import { entityFile, queriesFile, barrel, promptRender } from "@metaobjectsdev/codegen-ts/generators"; |
| 81 | +export default defineConfig({ |
| 82 | + // ... |
| 83 | + generators: [entityFile(), queriesFile(), barrel(), promptRender()], |
| 84 | +}); |
| 85 | +``` |
| 86 | + |
| 87 | +**Effort:** ~30 minutes. Both helpers already exist; this is glue + one new |
| 88 | +file in `generators/`. |
| 89 | + |
| 90 | +**Consumer impact:** Saves ~20 LOC of identical custom-generator boilerplate |
| 91 | +per consumer. More importantly, eliminates a "huh, why isn't this just |
| 92 | +working?" moment that takes a new consumer 30+ minutes to figure out (the |
| 93 | +docs say `generateRenderHandle()` exists; the default `meta gen` doesn't |
| 94 | +seem to use it). |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +### #2 — Parameterize generated `*.queries.ts` to accept `db` as a parameter |
| 99 | + |
| 100 | +**Where:** `server/typescript/packages/codegen-ts/src/templates/queries.ts` |
| 101 | +(or equivalent template). |
| 102 | + |
| 103 | +**Today:** generated `Council.queries.ts` does: |
| 104 | +```ts |
| 105 | +import { db } from "../db"; |
| 106 | +// ... |
| 107 | +export async function findCouncilById(id: string) { return db.select()... } |
| 108 | +``` |
| 109 | + |
| 110 | +This forces every consumer to ship a `src/db/db.ts` module at the import |
| 111 | +path the generated code expects. In a Cloudflare Worker, **the `db` is |
| 112 | +per-request** (`drizzle(env.DB)` requires the request-scoped binding) — there |
| 113 | +is no module-level `db` to import. So consumers ship a runtime-throwing stub |
| 114 | +purely to satisfy the typecheck: |
| 115 | + |
| 116 | +```ts |
| 117 | +// wizardsofodd Phase A Task 5 (will exist in every Worker consumer) |
| 118 | +export const db = (() => { |
| 119 | + throw new Error("type-only stub — use getDb(env) in the handler"); |
| 120 | +})() as unknown as BaseSQLiteDatabase<"async", { rowsAffected: number }>; |
| 121 | +``` |
| 122 | + |
| 123 | +This is dead code in the bundle that exists ONLY for typecheck. Every Worker |
| 124 | +consumer pays this tax forever. |
| 125 | + |
| 126 | +**Suggested fix:** generate query helpers that accept `db` as their first |
| 127 | +parameter: |
| 128 | + |
| 129 | +```ts |
| 130 | +// generated Council.queries.ts (after fix) |
| 131 | +import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; |
| 132 | +export async function findCouncilById(db: BaseSQLiteDatabase, id: string) { |
| 133 | + return db.select()... |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +Consumers call them with their per-request `db`: |
| 138 | +```ts |
| 139 | +const db = drizzle(env.DB, { schema }); |
| 140 | +const council = await findCouncilById(db, "abc123"); |
| 141 | +``` |
| 142 | + |
| 143 | +This is the standard Drizzle / Kysely / TypeORM pattern in 2026. |
| 144 | + |
| 145 | +**Effort:** 1–2 hours — template change in the queries codegen + updating |
| 146 | +docs to show the new call shape. Backward-compatibility could be handled by |
| 147 | +shipping both shapes briefly (with the parameterless variant calling a |
| 148 | +default-export `db`), or it's a breaking change consumers absorb in 0.7. |
| 149 | + |
| 150 | +**Consumer impact:** Eliminates a dead-code kludge every Worker consumer |
| 151 | +ships. Aligns with the dominant ORM-helper pattern in TS — current Drizzle |
| 152 | +docs all show parameter-passing. |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +### #3 — Add `expected/` output to the `template-prompt-simple` conformance fixture |
| 157 | + |
| 158 | +**Where:** `fixtures/conformance/template-prompt-simple/`. |
| 159 | + |
| 160 | +**Today:** The fixture has `input/meta.ai.json` and `expected.json` (the |
| 161 | +parsed metadata tree). It does NOT have an `expected/` directory showing |
| 162 | +what the **codegen** produces. A consumer trying to understand "what does |
| 163 | +`template.prompt` look like after `meta gen`?" has no canonical example to |
| 164 | +read — they have to run the codegen themselves. |
| 165 | + |
| 166 | +This is partly why my initial recon agent (looking at the metaobjects repo |
| 167 | +to scope the wizardsofodd integration) reported "no typed codegen for |
| 168 | +prompts in 0.6.0" — there was no expected output file to find. The codegen |
| 169 | +IS there (`generateRenderHandle()` works), but invisible to anyone reading |
| 170 | +the fixture corpus. |
| 171 | + |
| 172 | +**Suggested fix:** add `fixtures/conformance/template-prompt-simple/expected/prompts.ts` |
| 173 | +containing the actual output of `generateRenderHandle()` + `generatePayloadInterfaces()` |
| 174 | +applied to the input metadata. Either generated-and-committed, or asserted |
| 175 | +to match in the conformance test. |
| 176 | + |
| 177 | +**Effort:** ~20 minutes — generate once, commit, add an assertion. If a |
| 178 | +conformance check enforces "what `meta gen` produces equals `expected/`," |
| 179 | +even better (catches codegen regressions). |
| 180 | + |
| 181 | +**Consumer impact:** Docs-as-data. Consumers and AI agents alike can read |
| 182 | +the fixture and immediately understand the codegen contract without running |
| 183 | +anything. |
| 184 | + |
| 185 | +--- |
| 186 | + |
| 187 | +## Tier 2 — High value, somewhat larger |
| 188 | + |
| 189 | +### #4 — Document the Vite + Cloudflare Workers recipe |
| 190 | + |
| 191 | +**Where:** `README.md` or a new `docs/recipes/cloudflare-workers.md`. |
| 192 | + |
| 193 | +**Today:** Consumers wanting to use MO codegen output in a Cloudflare Worker |
| 194 | +with a Vite-built React SPA frontend (a very common 2026 deployment shape) |
| 195 | +have to figure out for themselves: |
| 196 | +- Which packages run in V8 isolates (codegen output ✅; `@metaobjectsdev/runtime-ts` ❌) |
| 197 | +- How to integrate Vite output with Workers Static Assets without wiping |
| 198 | + hand-curated public files (the `emptyOutDir: false` discipline) |
| 199 | +- How to wire `meta gen` into the build chain alongside `vite build` and |
| 200 | + `wrangler deploy` |
| 201 | +- The dev-only nature of codegen (run locally, commit output, never invoke |
| 202 | + at runtime) |
| 203 | + |
| 204 | +**Suggested fix:** a documented recipe section + ideally a tiny example repo |
| 205 | +(could even be the wizardsofodd shape, lightly anonymized). The recipe |
| 206 | +documents the exact constraints that took us multiple sessions to learn. |
| 207 | + |
| 208 | +**Effort:** ~1 hour for the README section; ~2-3 hours if you want a |
| 209 | +demo repo. |
| 210 | + |
| 211 | +**Consumer impact:** Cloudflare Workers is one of the most common edge |
| 212 | +deployment targets in 2026. Documenting MO's Worker story explicitly grows |
| 213 | +the addressable consumer base. |
| 214 | + |
| 215 | +### #5 — `CHANGELOG.md` at the monorepo root (or per-package) |
| 216 | + |
| 217 | +**Where:** Monorepo root; per-package optional. |
| 218 | + |
| 219 | +**Today:** Release notes only exist in `docs/RELEASING.md` (process docs). |
| 220 | +Consumers — including me, doing the 0.5→0.6 upgrade survey — have to do |
| 221 | +archaeology to figure out what changed (compare package versions across |
| 222 | +tags, grep for renames, etc.). For the wizardsofodd Phase 2 unblock |
| 223 | +specifically, the key questions "did `field.enum` register?" and "did |
| 224 | +`--dialect d1` ship?" took an Explore agent ~10 minutes to answer; a |
| 225 | +CHANGELOG would have made it 10 seconds. |
| 226 | + |
| 227 | +**Suggested fix:** standard Keep-a-Changelog format. Backfill 0.5.0 → 0.6.0 |
| 228 | +once; then add notes per release going forward. |
| 229 | + |
| 230 | +**Effort:** ~30 minutes for the initial backfill; ~5 minutes per release. |
| 231 | + |
| 232 | +**Consumer impact:** Every consumer needs to know what changed. AI agents |
| 233 | +specifically benefit — a CHANGELOG is high-signal context per token spent. |
| 234 | + |
| 235 | +### #6 — Better error messages on entity JSON shape mismatches |
| 236 | + |
| 237 | +**Where:** `server/typescript/packages/metadata/src/loader/` (or wherever |
| 238 | +the JSON-to-metadata mapper raises validation errors). |
| 239 | + |
| 240 | +**Today:** When a consumer authors `metaobjects/council.json` and a field |
| 241 | +key is wrong (e.g. forgets `@` prefix on `@maxLength`, or uses `enum` |
| 242 | +instead of `field.enum`), the error is generic ("unknown field key X"). No |
| 243 | +line number, no suggestion, no link to the canonical fixture. |
| 244 | + |
| 245 | +The wizardsofodd Phase 2 implementer last session had to mirror conformance |
| 246 | +fixtures (`enum-inline/input/meta.enums.json`) because the entity-JSON |
| 247 | +shape in my high-level spec didn't match what the loader actually accepted. |
| 248 | +Cost a session's worth of iteration. |
| 249 | + |
| 250 | +**Suggested fix:** error messages with (a) the JSON path to the offending |
| 251 | +key, (b) a "did you mean ___" suggestion based on Levenshtein distance to |
| 252 | +known keys, (c) a link to the most-relevant conformance fixture as a |
| 253 | +working example. Even just (a) + (c) would dramatically reduce time-to-fix. |
| 254 | + |
| 255 | +**Effort:** ~4-6 hours. The loader knows where it is in the JSON tree; just |
| 256 | +needs to expose that on raised errors. |
| 257 | + |
| 258 | +**Consumer impact:** Onboarding pain reduction. The "first hour with MO" |
| 259 | +experience is dominated by entity-shape iteration; making errors actionable |
| 260 | +shrinks that to "first 10 minutes." |
| 261 | + |
| 262 | +### #7 — Document camelCase vs snake_case mapping as a deliberate design choice |
| 263 | + |
| 264 | +**Where:** `README.md` + a "conventions" section in the codegen docs. |
| 265 | + |
| 266 | +**Today:** Generated Drizzle uses camelCase TS properties (`councilId`, |
| 267 | +`createdAt`) for snake_case SQL columns (`council_id`, `created_at`). This |
| 268 | +bit our queries.ts implementation multiple times — every time someone |
| 269 | +writes a query they have to remember "TS=camel, SQL=snake." The mapping is |
| 270 | +sensible (idiomatic both ways) but the discovery is by-error. |
| 271 | + |
| 272 | +**Suggested fix:** 1-paragraph docs section: "MO codegen maps `snake_case` |
| 273 | +metadata field names to `camelCase` TS property names by default, with the |
| 274 | +underlying SQL column staying `snake_case`. Override per-field with |
| 275 | +`@dbColumn`." Plus a worked example. |
| 276 | + |
| 277 | +**Effort:** ~15 minutes. |
| 278 | + |
| 279 | +**Consumer impact:** Prevents the "wait, why does Drizzle complain about |
| 280 | +`councilId` not existing when I wrote `council_id`?" stumble every consumer |
| 281 | +hits at least once. |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +## Tier 3 — Nice-to-have |
| 286 | + |
| 287 | +| # | What | Why it's lower priority | |
| 288 | +|---|---|---| |
| 289 | +| 8 | `metaobjects/` directory name configurable in `metaobjects.config.ts` (currently hard-coded in CLI's `loadMemory`) | Mild irritation; a single rename in the consumer repo gets you there | |
| 290 | +| 9 | `meta init` opt-out for `.metaobjects/` agent-docs scaffolding (~50 KB of AGENTS.md/CLAUDE.md) | Easy to delete after the fact if you don't want them | |
| 291 | +| 10 | `template.output` codegen for LLM output schemas | Real value for consumers doing structured-output LLMs, but bigger scope; current `generateRenderHandle()` handles both prompt and output identically — meaningful divergence requires its own design | |
| 292 | +| 11 | Wider `@metaobjectsdev/react` component coverage (entity detail / list / filter UI in addition to `useEntityForm` + `<CurrencyInput>`) | Bigger product decision; wizardsofodd v1 React work doesn't need it | |
| 293 | + |
| 294 | +--- |
| 295 | + |
| 296 | +## Tier 4 — Verification (might already be done; ~10 min spot-checks) |
| 297 | + |
| 298 | +### #12 — `meta migrate --dialect d1` end-to-end |
| 299 | + |
| 300 | +The `writeMigrationD1()` function exists at |
| 301 | +`server/typescript/packages/migrate-ts/src/write-migration-d1.ts`, but does |
| 302 | +the CLI flag (`--dialect d1`) actually wire through? Do the output |
| 303 | +filenames match wrangler's `NNNN_name.sql` convention, or does the consumer |
| 304 | +have to rename? |
| 305 | + |
| 306 | +The existing `.claude/worktrees/meta-migrate-d1-dialect/` worktree suggests |
| 307 | +you're already on this — just confirm wizardsofodd can use it cleanly |
| 308 | +without filename gymnastics. If a flat `NNNN_init.sql` lands in |
| 309 | +`migrations/` and `wrangler d1 migrations apply` picks it up, we're good. |
| 310 | + |
| 311 | +### #13 — `deleteXById` D1 response-shape bug fix status |
| 312 | + |
| 313 | +In 0.5.0, the generated `deleteCouncilById(id)` helper assumed `result.rowsAffected` |
| 314 | +which is a libsql/Turso field — D1 doesn't return it. Helper returned |
| 315 | +`false` even on a successful delete. Was this fixed in 0.6.0? |
| 316 | + |
| 317 | +Not on the wizardsofodd hot path (councils are immutable), but worth knowing. |
| 318 | + |
| 319 | +--- |
| 320 | + |
| 321 | +## Suggested ordering if you have appetite for Tier 1 |
| 322 | + |
| 323 | +If you tackle Tier 1 in order, each item compounds the value of the next: |
| 324 | + |
| 325 | +1. **#3 (add `expected/` to conformance fixture)** — 20 minutes. Immediately |
| 326 | + makes the next two changes easier to test/document. |
| 327 | +2. **#1 (auto-wire `promptRender()` generator)** — 30 minutes after #3 ships. |
| 328 | + Lets consumers drop the custom-generator wiring. |
| 329 | +3. **#2 (parameterize `db` in generated queries)** — 1–2 hours after #1. |
| 330 | + Biggest "every-consumer-pays-this-tax-forever" win. |
| 331 | + |
| 332 | +Tier 2 items can be batched whenever (#4, #5, #7 are all small docs adds). |
| 333 | + |
| 334 | +--- |
| 335 | + |
| 336 | +## What wizardsofodd does in the meantime |
| 337 | + |
| 338 | +This proposal is **not blocking** for wizardsofodd Phase A — every item has |
| 339 | +a documented workaround in our plan |
| 340 | +(`wizardsofodd.com/docs/superpowers/plans/2026-05-25-metaobjects-0.6-mega.md`). |
| 341 | +We'll ship Phase A with the current 0.6.0 (custom-generator wiring, stub |
| 342 | +`db.ts`, etc.) and incorporate any upstream improvements when they land |
| 343 | +during Phases B/C/D. |
| 344 | + |
| 345 | +If Tier 1 ships before Phase B, we delete ~50 LOC of workaround code and |
| 346 | +import from `@metaobjectsdev/codegen-ts/generators` instead. |
0 commit comments