Skip to content

Commit 89a9ff2

Browse files
committed
Merge branch 'main' into worktree-wa4-loader-consolidation
2 parents b0c86bb + 9ac9d94 commit 89a9ff2

31 files changed

Lines changed: 1018 additions & 159 deletions
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# FR: Actionable JSON-shape loader errors (path + did-you-mean + fixture link)
2+
3+
**Status:** Design proposal — needs brainstorm before implementation
4+
**Date:** 2026-05-25
5+
**Scope:** Cross-language — every port's metadata loader raises JSON-shape validation
6+
errors using the same `ERR_*` codes from the conformance corpus. This FR proposes a
7+
cross-port UX improvement.
8+
**Origin:** A downstream consumer onboarding to 0.6.0 reported that the "first hour with
9+
MO" was dominated by entity-JSON-shape iteration: typos like `enum` instead of
10+
`field.enum`, or forgotten `@` prefixes on attributes, produced generic
11+
"unknown field key X" errors with no line number, no suggestion, and no link to a
12+
working example.
13+
14+
## Goal
15+
16+
Make loader errors **actionable**: when an authored `metaobjects/*.json` doesn't validate,
17+
the error should tell the user:
18+
19+
1. **Where in the JSON the problem is** — the JSON path (`metadata.root.children[2].object.entity.children[1]`).
20+
2. **A "did you mean" suggestion** — Levenshtein-nearest known key (`enum``field.enum`).
21+
3. **A link to a working example** — the most-relevant conformance fixture.
22+
23+
All three are existing knowledge in the loader pipeline; surfacing them on the raised
24+
error reduces "time to fix" from minutes-of-grepping to seconds-of-reading.
25+
26+
## Why this is cross-language
27+
28+
The conformance corpus defines a fixed set of `ERR_*` codes (`ERR_UNKNOWN_TYPE`,
29+
`ERR_UNKNOWN_SUBTYPE`, `ERR_BAD_ATTR_VALUE`, `ERR_MISSING_REQUIRED_ATTR`,
30+
`ERR_RESERVED_ATTR`, etc.) that **every port emits identically**. The error code itself
31+
is cross-language. The *message text* attached to each error is currently per-port and
32+
varies in quality. This FR proposes:
33+
34+
1. A cross-language **schema** for the error payload (path, suggestion, fixture link).
35+
2. Per-port **implementation** of populating that payload — Tier 3 (idiomatic) freedom
36+
on the exact rendering, Tier 1 (invariant) commitment to the fields.
37+
38+
This needs a brainstorm because the error-payload schema decision will affect Java,
39+
C#, Python, and TypeScript loaders simultaneously.
40+
41+
## Current state by port (rough)
42+
43+
| Port | Path info? | Did-you-mean? | Fixture link? |
44+
|---|---|---|---|
45+
| TypeScript (`@metaobjectsdev/metadata`) | Partial (some errors include node names) | No | No |
46+
| C# (`MetaObjects.Metadata`) | Partial (similar) | No | No |
47+
| Java (`server/java/metadata`) | Partial | No | No |
48+
| Python (`server/python/src/metaobjects`) | Partial | No | No |
49+
50+
None of the four ports raises errors with the full information today, but all four
51+
have the information available inside the loader pipeline at the point of failure.
52+
53+
## Brainstorm topics (open)
54+
55+
### 1. Error payload schema — cross-language fields
56+
57+
Strawman:
58+
59+
```jsonc
60+
{
61+
"code": "ERR_BAD_ATTR_VALUE",
62+
"message": "Attribute '@values' on 'field.enum[ColorEnum]' must be a non-empty string array; got string",
63+
"path": "metadata.root.children[0].object.entity.children[1].field.enum.@values",
64+
"suggestions": [
65+
"Did you mean to use the inline form '[...]' instead of a single string?",
66+
"See fixtures/conformance/enum-inline/input/meta.enums.json for the canonical shape."
67+
],
68+
"fixture": "enum-inline",
69+
"node": { "type": "field", "subtype": "enum", "name": "ColorEnum" }
70+
}
71+
```
72+
73+
Decisions to brainstorm:
74+
- What's the minimum set of fields every port must populate? (`code`, `message`, `path`
75+
probably; `suggestions`, `fixture`, `node` optional?)
76+
- How is the payload surfaced — exception object, error response, log line, all three?
77+
Per-port idiomatic.
78+
- How does the conformance harness verify the payload? Today it only checks
79+
the `code`. Adding payload checks raises corpus expectations.
80+
81+
### 2. "Did you mean" — distance threshold + key sources
82+
83+
Levenshtein distance is the obvious metric. Open questions:
84+
- Threshold (≤ 2? ≤ 3? proportional to key length?).
85+
- Key universe per error type:
86+
- `ERR_UNKNOWN_TYPE` → list of registered type names.
87+
- `ERR_UNKNOWN_SUBTYPE` → list of registered subtypes for the type.
88+
- `ERR_RESERVED_ATTR` → the reserved-keyword set.
89+
- `ERR_BAD_ATTR_VALUE` for missing `@` prefix → look in the registered attr names.
90+
- Single best suggestion vs. ranked list of up to N?
91+
92+
### 3. Fixture link — how does the loader know which fixture is canonical?
93+
94+
Two approaches:
95+
96+
**(a) Lookup table.** A registry mapping `(error_code, type, subtype)` → fixture name.
97+
Lives at `fixtures/conformance/ERROR_TO_FIXTURE.json`, regenerated by tooling. Each
98+
port loads it.
99+
100+
**(b) Heuristic.** "Look for a fixture whose name contains the type + subtype." Fragile;
101+
not all fixtures follow that convention.
102+
103+
Recommendation hypothesis: **(a)** — explicit, deterministic, regeneratable.
104+
105+
### 4. Cross-port rollout
106+
107+
This is a per-port implementation effort once the schema is fixed. Suggested order:
108+
1. Schema design + an ADR in `spec/decisions/`.
109+
2. TypeScript impl first (largest active consumer base).
110+
3. C# next (active port, full surface).
111+
4. Python + Java in parallel as those ports' conformance ledgers close.
112+
113+
## Out of scope (until brainstorm)
114+
115+
- Rewriting the loader pipeline. The payload-surfacing change should slot into existing
116+
error-raising sites with minimal pipeline disturbance.
117+
- Localization of error message text. Strings stay English.
118+
- IDE / LSP integration that consumes the payload to render diagnostics inline. That's
119+
a follow-up consumer — getting the payload right comes first.
120+
121+
## Open questions
122+
123+
1. Does the conformance harness gain payload checks, or do those stay per-port?
124+
2. Should the error payload field names match TypeScript convention (camelCase) across
125+
all ports, or each port uses its own (`snake_case` for Python, `PascalCase` for C#)?
126+
The JSON-on-the-wire form is identical regardless.
127+
3. Is there a desire for a `--strict-author` mode that raises *every* deviation rather
128+
than the first one? Today's loader is fail-fast; multi-error reporting is a separate
129+
shape.
130+
131+
## Related work
132+
133+
- The Documentation Provider (FR-004 family) already produces structured XML-doc and
134+
COMMENT-ON output from metadata `description` / `notes` attrs. That's downstream
135+
(output), not upstream (input authoring); the schemas are unrelated, but the cross-
136+
port discipline is the same.
137+
- ADR-0006 (AI-first YAML authoring) lowers the discoverability bar somewhat by accepting
138+
sigil-free attrs, but `meta.json` authoring is the canonical interchange and still
139+
needs first-class error UX.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# FR: Cloudflare Workers + Vite deploy recipe (TS docs)
2+
3+
**Status:** Design — implementation-ready
4+
**Date:** 2026-05-25
5+
**Scope:** TypeScript / documentation only (`README.md` + a new
6+
`docs/recipes/cloudflare-workers.md`)
7+
**Origin:** Friction observed in a downstream consumer assembling Cloudflare Workers +
8+
Vite + MO codegen on their own — multiple sessions spent figuring out which packages run
9+
in V8 isolates, how to wire `meta gen` into `vite build` + `wrangler deploy`, and how to
10+
integrate Vite output with Workers Static Assets without wiping hand-curated files.
11+
12+
## Goal
13+
14+
Document MO's Cloudflare Workers story explicitly so adopters don't have to derive it.
15+
Cover the four constraints that took multiple sessions of trial-and-error to learn:
16+
17+
1. Which `@metaobjectsdev/*` packages are V8-isolate compatible.
18+
2. How to integrate Vite output with Workers Static Assets (the `emptyOutDir: false`
19+
discipline).
20+
3. How `meta gen` slots into the dev/build chain alongside `vite build` and `wrangler deploy`.
21+
4. The dev-only / build-time nature of codegen (run locally, commit output, never invoke
22+
from a request handler).
23+
24+
## Why TS-only
25+
26+
Cloudflare Workers run V8 isolates with a Node.js compatibility flag. The constraints
27+
described here are JavaScript-runtime-specific. Java / C# / Python ports do not run in
28+
Workers and need separate deploy recipes for their respective runtimes (already-existing
29+
JVM containers, .NET Lambdas, FastAPI on Cloud Run, etc.) when those ports ship runtime
30+
surfaces.
31+
32+
## Design
33+
34+
### Section 1 — Which packages run in Workers
35+
36+
| Package | Workers-compatible? | Notes |
37+
|---|---|---|
38+
| `@metaobjectsdev/cli` | Build-time only | Run on the developer's machine; never bundled. |
39+
| `@metaobjectsdev/metadata` | Build-time only | Same. |
40+
| `@metaobjectsdev/codegen-ts` | Build-time only | Same. |
41+
| `@metaobjectsdev/migrate-ts` | Build-time only | Migrations emit SQL, then `wrangler d1 migrations apply` runs them. |
42+
| `@metaobjectsdev/render` | ✅ Runtime-safe | Pure JS, no Node-only deps. |
43+
| `@metaobjectsdev/runtime-ts` | ❌ Node-only | Fastify + Kysely drivers. Not for Workers. |
44+
| `@metaobjectsdev/runtime-web` | ✅ Runtime-safe | Pure browser/edge JS. |
45+
| `@metaobjectsdev/react` | ✅ Runtime-safe | Browser; works in Workers if you render React on the edge. |
46+
| `@metaobjectsdev/tanstack` | ✅ Runtime-safe | Browser-side. |
47+
| Generated `<Entity>.entity.ts` / `<Entity>.queries.ts` | ✅ Runtime-safe | The generated code is the runtime; uses `drizzle-orm/d1`. |
48+
| Generated `<Entity>.routes.ts` | ⚠️ Mostly | The route handlers themselves are Workers-safe, but the Fastify adapter from `runtime-ts` is not. Worker adopters skip the routes generator and write their own minimal-API mounting. |
49+
50+
### Section 2 — Vite + Workers Static Assets integration
51+
52+
The minimum-friction shape for a Workers + Vite + MO project:
53+
54+
```
55+
project-root/
56+
├── metaobjects/ # MO metadata (source of truth)
57+
├── metaobjects.config.ts
58+
├── src/
59+
│ ├── worker.ts # Worker entrypoint (the SPA's API)
60+
│ └── ui/ # React SPA source
61+
│ ├── index.html
62+
│ └── main.tsx
63+
├── public/ # Hand-curated static (favicons, robots.txt, etc.)
64+
├── dist/ # Vite output (gitignored)
65+
└── wrangler.toml # Workers config
66+
```
67+
68+
`vite.config.ts` must set `build.emptyOutDir: false` if you want hand-curated files in
69+
`dist/` to survive a `vite build`. With Vite 5+'s default of `emptyOutDir: true`, the
70+
build wipes anything Vite didn't produce itself — destroying any pre-staged assets.
71+
72+
`wrangler.toml` points `assets.directory = "./dist"` and `main = "./src/worker.ts"`.
73+
74+
### Section 3 — Build chain
75+
76+
The dev-time loop:
77+
78+
```
79+
$ meta gen # codegen runs locally; output is committed
80+
$ vite dev # SPA dev server
81+
$ wrangler dev # worker dev server (proxied for /api)
82+
```
83+
84+
The deploy loop:
85+
86+
```
87+
$ meta gen # in case metadata changed; commit any drift
88+
$ vite build # bundles the SPA into ./dist/
89+
$ wrangler deploy # uploads worker + assets
90+
```
91+
92+
**`meta gen` runs once per metadata change, not per build.** The output is committed.
93+
A CI gate can run `meta gen` then `git diff --exit-code` to enforce that committed
94+
generated code matches the metadata — drift fails the build.
95+
96+
### Section 4 — D1 migration loop
97+
98+
```
99+
$ meta migrate --dialect d1 --slug my-change # emits migrations/<seq>_my-change.sql
100+
$ wrangler d1 migrations apply <BINDING> # local
101+
$ wrangler d1 migrations apply <BINDING> --remote # production
102+
```
103+
104+
`migrations_dir` in `wrangler.toml` and `--out-dir` in `meta migrate` should agree
105+
(default `migrations/` matches Wrangler's expectation).
106+
107+
## Out of scope
108+
109+
- A starter / template repo. Worth doing eventually; not part of this FR.
110+
- Edge-side cron, queue, or workflow integrations.
111+
- React SPA on the edge (SSR / streaming). The recipe assumes client-side render.
112+
113+
## Open questions
114+
115+
1. Should `routesFile()` get an optional `target: "fastify" | "workers" | "hono"` flag so
116+
Workers adopters can opt into a generated minimal-API handler instead of skipping
117+
route generation entirely? Useful, but a separate FR — file as follow-up if appetite
118+
materializes.

0 commit comments

Comments
 (0)