@metaobjectsdev/sdk 0.23.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/agent-context/skills/metaobjects-audit/SKILL.md +20 -7
  2. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +64 -18
  3. package/agent-context/skills/metaobjects-audit/references/csharp.md +2 -1
  4. package/agent-context/skills/metaobjects-audit/references/java.md +3 -2
  5. package/agent-context/skills/metaobjects-audit/references/kotlin.md +2 -1
  6. package/agent-context/skills/metaobjects-audit/references/requirements.md +3 -4
  7. package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
  8. package/agent-context/skills/metaobjects-authoring/SKILL.md +28 -3
  9. package/agent-context/skills/metaobjects-authoring/references/requirements.md +49 -23
  10. package/agent-context/skills/metaobjects-codegen/SKILL.md +4 -3
  11. package/agent-context/skills/metaobjects-codegen/references/csharp.md +2 -2
  12. package/agent-context/skills/metaobjects-codegen/references/java.md +3 -3
  13. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +4 -4
  14. package/agent-context/skills/metaobjects-codegen/references/python.md +2 -2
  15. package/agent-context/skills/metaobjects-codegen/references/typescript.md +3 -3
  16. package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +4 -4
  17. package/agent-context/skills/metaobjects-prompts/SKILL.md +53 -32
  18. package/agent-context/skills/metaobjects-prompts/references/csharp.md +26 -21
  19. package/agent-context/skills/metaobjects-prompts/references/java.md +24 -18
  20. package/agent-context/skills/metaobjects-prompts/references/kotlin.md +25 -20
  21. package/agent-context/skills/metaobjects-prompts/references/python.md +68 -26
  22. package/agent-context/skills/metaobjects-prompts/references/typescript.md +29 -23
  23. package/agent-context/skills/metaobjects-verify/references/migration.md +1 -1
  24. package/agent-context/skills/metaobjects-verify/references/requirements.md +36 -22
  25. package/agent-context/templates/always-on.md.mustache +3 -1
  26. package/dist/agent-docs/body.d.ts +1 -1
  27. package/dist/agent-docs/body.d.ts.map +1 -1
  28. package/dist/agent-docs/body.js +24 -2
  29. package/dist/agent-docs/body.js.map +1 -1
  30. package/dist/collection.d.ts +71 -0
  31. package/dist/collection.d.ts.map +1 -0
  32. package/dist/collection.js +121 -0
  33. package/dist/collection.js.map +1 -0
  34. package/dist/config.d.ts +51 -21
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +76 -10
  37. package/dist/config.js.map +1 -1
  38. package/dist/discovery.d.ts +43 -0
  39. package/dist/discovery.d.ts.map +1 -0
  40. package/dist/discovery.js +100 -0
  41. package/dist/discovery.js.map +1 -0
  42. package/dist/index.d.ts +10 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +17 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/memory.d.ts +42 -19
  47. package/dist/memory.d.ts.map +1 -1
  48. package/dist/memory.js +38 -95
  49. package/dist/memory.js.map +1 -1
  50. package/dist/metadata-files.d.ts +67 -0
  51. package/dist/metadata-files.d.ts.map +1 -0
  52. package/dist/metadata-files.js +155 -0
  53. package/dist/metadata-files.js.map +1 -0
  54. package/dist/records/any.d.ts +8 -8
  55. package/dist/records/decision.d.ts +2 -2
  56. package/dist/records/glossary.d.ts +4 -4
  57. package/dist/records/principle.d.ts +2 -2
  58. package/dist/scope.d.ts +16 -0
  59. package/dist/scope.d.ts.map +1 -0
  60. package/dist/scope.js +75 -0
  61. package/dist/scope.js.map +1 -0
  62. package/dist/sources.d.ts +86 -0
  63. package/dist/sources.d.ts.map +1 -0
  64. package/dist/sources.js +130 -0
  65. package/dist/sources.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/agent-docs/body.ts +24 -2
  68. package/src/collection.ts +183 -0
  69. package/src/config.ts +80 -12
  70. package/src/discovery.ts +110 -0
  71. package/src/index.ts +26 -9
  72. package/src/memory.ts +65 -99
  73. package/src/metadata-files.ts +158 -0
  74. package/src/scope.ts +95 -0
  75. package/src/sources.ts +160 -0
@@ -1,15 +1,17 @@
1
1
  # TypeScript parser-on-receipt
2
2
 
3
- For every `template.output` in your metadata, the `outputParser()` generator (from
4
- `@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates an
5
- LLM/raw response against the template's `@payloadRef` payload value-object. This is
6
- the receive side; codegen emits **no** provider/LLM-call layer you compose the
7
- call yourself.
3
+ For every RESPONDING `template.prompt` in your metadata one declaring
4
+ `@responseRef` the `outputParser()` generator (from
5
+ `@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates a
6
+ model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never
7
+ `@payloadRef` (which types the request the prompt renders outbound), and a
8
+ `template.output` gets no parser at all. This is the receive side; codegen emits **no**
9
+ provider/LLM-call layer — you compose the call yourself.
8
10
 
9
11
  ## Contents
10
12
  - Wire the generator
11
13
  - What it emits
12
- - The output-format prompt fragment (FR-010)
14
+ - The response-format prompt fragment (FR-010)
13
15
  - The three-step consumer pattern
14
16
  - Recommended LLM caller (bring-your-own)
15
17
  - Drift gate
@@ -27,7 +29,7 @@ export default defineConfig({
27
29
  generators: [
28
30
  entityFile(), queriesFile(), barrel(),
29
31
  promptRender(), // render<Name>() per template.prompt (the send side)
30
- outputParser(), // parse*/safeParse* per template.output (the receive side)
32
+ outputParser(), // parse*/safeParse* per responding template.prompt (the receive side)
31
33
  ],
32
34
  });
33
35
  ```
@@ -37,9 +39,10 @@ options.
37
39
 
38
40
  ## What it emits
39
41
 
40
- Per `template.output` (say named `NpcResponseOutput`, `@payloadRef:
41
- "NpcResponsePayload"`), `meta gen` writes a self-contained `NpcResponseOutput.output.ts`
42
- with a Zod schema + a dual API:
42
+ Per responding `template.prompt` (say named `NpcReview`, `@responseRef:
43
+ "NpcResponse"`), `meta gen` writes a self-contained `NpcReview.response.ts` with a Zod
44
+ schema + a dual API. The strict tier is JSON-only — an `@responseFormat: xml` reply gets
45
+ the tolerant `extract` and no `parse`/`safeParse`:
43
46
 
44
47
  ```ts
45
48
  import { z } from "zod";
@@ -76,11 +79,11 @@ including `field.enum` — falls through to `z.unknown()` in the strict
76
79
  in the entity insert/update schemas, not in this output parser; the lenient extract
77
80
  path carries the enum-as-string handling).
78
81
 
79
- ## The output-format prompt fragment (FR-010)
82
+ ## The response-format prompt fragment (FR-010)
80
83
 
81
- For every json/xml-format `template.output`, the `outputPrompt()` generator (same
82
- import path, `@metaobjectsdev/codegen-ts/generators`) emits a
83
- `<TemplateName>.prompt.ts` exporting `render<TemplateName>Format(overrides?)` —
84
+ For every responding `template.prompt`, the `outputPrompt()` generator (same import
85
+ path, `@metaobjectsdev/codegen-ts/generators`) emits a
86
+ `<PromptName>.responseFormat.ts` exporting `render<PromptName>Format(overrides?)` —
84
87
  backed by the render engine's `renderOutputFormat()`. This is the "produce your
85
88
  answer like this" fragment you splice into the prompt text so the model returns the
86
89
  shape the parser above expects:
@@ -99,14 +102,16 @@ export default defineConfig({
99
102
  });
100
103
  ```
101
104
 
102
- `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
105
+ `@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
103
106
  controls the fragment's presentation: `guide` is a prose field list + example
104
107
  skeleton, `inline` is one skeleton with inline placeholders/enum choices,
105
108
  `exampleOnly` is just the filled skeleton. Guidance is never emitted as comments —
106
- models ignore them. Skipped for non-json/xml `@format` outputs and for any output
107
- whose `@payloadRef` doesn't resolve to a value-object — the same skip contract as
108
- `outputParser()`. The baked spec's root name matches the payload class name, so the
109
- fragment and the parser's `extract()` agree on the same root.
109
+ models ignore them. Skipped for `template.output` and for any prompt whose
110
+ `@responseRef` doesn't resolve — the same skip contract as `outputParser()`. There is NO
111
+ format gate: the old `@format {json,xml}` test read the syntax of the outbound BODY to
112
+ decide whether to describe the REPLY. `@responseFormat` selects which syntax the fragment
113
+ teaches. The baked spec's root name matches the response shape, so the fragment and the
114
+ parser's `extract()` agree on the same root.
110
115
 
111
116
  ## The three-step consumer pattern
112
117
 
@@ -164,16 +169,17 @@ recorder.
164
169
 
165
170
  ## Drift gate
166
171
 
167
- `meta verify` walks every `template.output`'s `@payloadRef` resolution and fails the
168
- build (exit 1, `(output)`-prefixed diagnostic) if a reference can't be resolved
169
- catching payload-VO parser drift at build time. The emitted parser imports `zod`;
172
+ `meta verify` walks every template's `@payloadRef` resolution and fails the build
173
+ (exit 1) if a reference can't be resolved. A `@responseRef` that does not resolve to a
174
+ value-object (or sourceless projection) is caught one layer earlier it is a LOAD
175
+ error in every port. The emitted parser imports `zod`;
170
176
  it's usually already a dependency (Drizzle / `runtime-ts` lean on it), else
171
177
  `npm i zod`.
172
178
 
173
179
  ## See which fields a template consumes
174
180
 
175
181
  Run `meta docs` to emit the model surface to `./docs` — one page per
176
- `template.output` at `docs/<Template>.md`. Each template page has a
182
+ `template.*` at `docs/<Template>.md`. Each template page has a
177
183
  `## Template source` section that shows the Mustache source with every `{{var}}`
178
184
  linked to that field's doc page (`docs/<Owner>.md#field-<name>`), plus a variables
179
185
  table — so you can see exactly which payload fields a template reads. Those links are
@@ -162,7 +162,7 @@ Flyway / a hand-migration, use **`@unmanaged`**.
162
162
  A read-only projection (`object.projection` with a `source.rdb` `@kind: view` child)
163
163
  does **not** get a hand-written `CREATE VIEW`. `meta migrate` synthesizes the view
164
164
  DDL from the projection's `origin.*` children — `passthrough` columns, `aggregate`
165
- rollups (`count`/`sum`/`avg`/`min`/`max`), and `collection` joins — through the one
165
+ rollups (`count`/`sum`/`avg`/`min`/`max`), and `collect` array rollups — through the one
166
166
  canonical view-SQL emitter shared with drift detection.
167
167
 
168
168
  - Change the projection (add a passthrough, change an aggregate) → `meta migrate`
@@ -8,7 +8,7 @@ subverb**: requirements are metadata, so they are checked on *every* `meta verif
8
8
  | | owns |
9
9
  |---|---|
10
10
  | **loader** | `@status` enum, required attrs, child rules, levels — unconditional |
11
- | **`verify`** | `@implementedBy` / `@verifiedBy` resolution — **severity depends on `@status`** |
11
+ | **`verify`** | `@implementedBy` resolution — **severity depends on `@status`** |
12
12
 
13
13
  A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you
14
14
  see that, no other diagnostic in the run is trustworthy — fix it first and re-run.
@@ -18,12 +18,11 @@ see that, no other diagnostic in the run is trustworthy — fix it first and re-
18
18
  The **same** unresolved `@implementedBy` reference is:
19
19
 
20
20
  - an **error** on `live` / `partial` — the model moved and the requirement is stale;
21
- - **allowed** on `abandoned` / `superseded` — those nodes are *supposed* to be gone. That is
21
+ - **allowed** on `planned` — those nodes do not exist YET. That is
22
22
  the entry doing its job, not drift.
23
23
 
24
- So do not "fix" a dangling reference on an abandoned requirement by deleting it. Deleting it
25
- destroys the record that something was deliberately retired, which is the single thing this
26
- mechanism exists to preserve.
24
+ So on a `planned` entry a dangling reference is the entry doing its job, not drift — do not
25
+ "fix" it by deleting the reference, or you delete the plan.
27
26
 
28
27
  ## Exit codes
29
28
 
@@ -31,34 +30,49 @@ mechanism exists to preserve.
31
30
  |---|---|
32
31
  | clean tree, or no `requirement.*` nodes at all | 0 |
33
32
  | dangling `@implementedBy` on `live`/`partial` | 1 |
34
- | the same reference on `abandoned`/`superseded` | 0 |
33
+ | the same reference on `planned` | 0 |
35
34
  | `@implementedBy` above the L4 link floor | 1 |
36
35
  | live `requirement.architectural` claimed by nothing | 1 |
37
- | `@verifiedBy` naming a test that exists nowhere | 1 |
38
- | `@verifiedBy` naming a name found only in an **unrecognised** test file | 0 (warning) |
39
- | `@verifiedBy` naming a test that is **skipped** | 0 (warning) |
40
36
  | an entity no requirement claims | 0 (warning) |
41
37
 
42
- ## What counts as a test file is YOUR project's call
38
+ ## The error codes, and the fix for each
43
39
 
44
- The scan ships patterns for jest/vitest/bun, JUnit, Maven Failsafe (`*IT`), xUnit/NUnit,
45
- pytest and Kotlin. Those are a convenience, **not an authority** a built-in list is a guess
46
- about your repository, and a wrong guess reports a real test as a broken claim. Declare your
47
- conventions and they are added to the built-ins:
40
+ Match on the stable `code`, never the message text (ADR-0009). The runtime messages already
41
+ name the remedy; this table exists so you can act on one without re-deriving the rule.
48
42
 
49
- ```ts
50
- // metaobjects.config.ts
51
- export default defineConfig({ verify: { testFiles: ["**/*IT.kt", "**/*.feature"] } });
52
- ```
43
+ | code | what it means | fix |
44
+ |---|---|---|
45
+ | `ERR_REQUIREMENT_DANGLING_REF` | an `@implementedBy` ref does not resolve in the loaded model, on a status that requires live nodes | the model moved and the claim is stale — repoint the ref, or change the status if the capability really went away. **Check the ref is not merely unloaded**: a source missing from `sources` makes a live node look deleted. |
46
+ | `ERR_REQUIREMENT_L5_NOT_MEMBER` | an L5 entry's ref names an OBJECT | move the entry to **L4**, or repoint to a member as `pkg::Owner.member`. Moving the level is usually right — the ref is usually what you meant. |
47
+ | `ERR_REQUIREMENT_L4_NOT_OBJECT` | an L4 entry's ref names a MEMBER | the symmetric case — move the entry to **L5**, or repoint at the owning object. |
48
+ | `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | `@implementedBy` on an L1–L3 node | L1–L3 are problem-domain altitude and never reference the model. Push the claim down to the L4/L5 child that actually carries it. |
49
+ | `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | a `live`/`partial` `requirement.architectural` that nothing implements | a policy declared and applied to nothing. Claim the nodes it governs, or drop it to `planned` — which is exempt, because it is not applied yet by definition. |
50
+ | `ERR_REQUIREMENT_LEVEL_NESTING` | a node's `@level` disagrees with the level of the parent it nests under | nesting IS the hierarchy. Move the node to the right parent rather than editing the level to match where it happens to sit. |
51
+ | `ERR_REQUIREMENT_BAD_LEVEL` | `@level` is not an integer inside the allowed range | levels are L1–L5 and nothing else. |
52
+ | `ERR_MISSING_REQUIRED_ATTR` | a required attr is absent | `@statement`, `@status` and `@counterexample` are required on both subtypes; `@level` is required on `functional` and optional on `architectural`. |
53
+ | `ERR_BAD_ATTR_VALUE` | a closed-enum attr has an unknown value | `@status` and `@disposition` are enforced by the LOADER, so a typo fails the load in every port rather than passing in some. |
53
54
 
54
- If a named test is missing from the corpus but present in some other source file, `verify`
55
- warns and names that file rather than failing an unrecognised convention is the tool's
56
- ignorance, not your mistake.
55
+ **A note on the L4/L5 pair.** They are enforced at the same site and fail for opposite
56
+ reasons, so reading only the code you hit can send you the wrong way. The question is not
57
+ "is this ref valid?" — usually it is — but "does the LEVEL match the shape of the ref?"
58
+
59
+ ## `verify` does not look at your tests
60
+
61
+ It used to. `@verifiedBy` asked you to name a test, and `verify` checked that the **name**
62
+ occurred somewhere in your test sources. That is existence evidence, never proof — an audit of
63
+ one real 19-name ledger opened every named test and found **4 that did not verify their
64
+ claim**: one matched a comment, one a dependency-injection key, one a real test of a
65
+ *different* claim, and one a test of the entry's output where the claim was about its source
66
+ text. `verify` reported zero errors throughout. The author picks the string, so the cheapest
67
+ way to satisfy the check is to find any name that already exists.
68
+
69
+ The attribute is retired. Tying a requirement to a test is the job of a generator that emits
70
+ the test **from** the requirement, so the link is structural rather than a name someone chose.
57
71
 
58
72
  ## What a green run does NOT prove
59
73
 
60
74
  It proves **referential integrity**: statuses parse, levels are in range, links sit at or
61
- below the floor, references resolve, named tests exist and are not skipped.
75
+ below the floor, references resolve.
62
76
 
63
77
  It cannot prove a status is **true**, or that a node genuinely implements the requirement
64
78
  claiming it. No test can. That judgement is yours.
@@ -11,7 +11,7 @@ spine; generated code is the disposable artifact. Regenerate with `{{codegenComm
11
11
  ## Principles
12
12
  - **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
13
13
  - Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files (Flyway/Liquibase), they must **match** the generated schema, not invent it.
14
- - The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a JVM stack whose migration tool `meta migrate` does not emit for (e.g. **Flyway**/Liquibase) author the migration to match the generated schema (the source of truth). A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db` after any DB-touching work to catch that drift early.
14
+ - The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a JVM stack whose migration tool `meta migrate` does not emit for (e.g. **Flyway**/Liquibase) author the migration to match the generated schema (the source of truth). A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db <url>` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
15
15
  - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
16
16
  - Use the generated constants for any string that names metadata.
17
17
  - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases.
@@ -26,6 +26,8 @@ spine; generated code is the disposable artifact. Regenerate with `{{codegenComm
26
26
  ## Keep all MetaObjects ports in sync
27
27
  MetaObjects ships as separate packages per language on DIFFERENT version lines (npm/PyPI/NuGet `0.x`/`1.x`, Maven Central `7.x`/`8.x`). Because the numbers differ by ecosystem, a stale port is INVISIBLE — an old TS client next to a new Java backend *looks* fine. Ports are only truly in sync when every one implements the same **Metamodel spec version** (`metamodelVersion`, on the registry manifest). Upgrade ALL ports together and confirm they land on the same Metamodel version; a lagging port silently disagrees on vocabulary + wire behavior.
28
28
 
29
+ `metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number.
30
+
29
31
  ## Going deeper (Claude Code)
30
32
  For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the
31
33
  matching `metaobjects-*` skill — its body links the `references/<lang>.md` fragment
@@ -2,5 +2,5 @@
2
2
  * @deprecated The single-blob agent doc is replaced by the assembled agent-context
3
3
  * (see `@metaobjectsdev/sdk/agent-context`). Kept only for back-compat; not scaffolded by `meta init`.
4
4
  */
5
- export declare const AGENT_DOCS_BODY = "# Meta Forge \u2014 agent reference\n\nThis file is scaffolded by `meta init` and lives alongside your `metaobjects/` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with `meta init --refresh-docs`.\n\n## Five working principles (read first)\n\nThese shape every interaction with a metaobjects-driven project. Follow them when you author metadata, write hand-coded business logic, or review someone else's work.\n\n### 1. If it's pattern-derivable from metadata, generate it. Never hand-write boilerplate.\n\nThe metaobjects raison d'\u00EAtre is that anything the metadata fully describes \u2014 schemas, FK references, basic CRUD, query helpers, Zod validators, route handlers, RHF rules, form fields \u2014 should be produced by codegen, not hand-typed. If you find yourself hand-writing something the metadata already knows about, stop and use the generated artifact.\n\nThe first version of the trainer website's database layer had hand-written Drizzle schemas, Zod schemas, and CRUD endpoints. Every one of those is now generated. The hand-written code that remains is real business logic (Stripe webhooks, Loops integration, custom auth flows) \u2014 things the generator genuinely cannot derive.\n\nWhen you're about to add a new field or entity: edit `metaobjects/*.json` and re-run `meta gen`. Don't reach for the generated file directly.\n\n### 2. Use the generated constants. Never use magic strings that touch metadata.\n\nAfter `meta gen`, each entity file exports a rich metadata-constants block. Each non-dollar-prefixed key is a per-field object that carries everything a consumer might need (name, label, view, html input type, placeholder, RHF validation rules):\n\n```ts\nexport const Subscriber = {\n $entity: \"Subscriber\", // entity name string\n $table: \"subscribers\", // SQL table name\n $path: \"/subscribers\", // REST resource path\n\n email: {\n name: \"email\", // field name string (use for filters, register())\n label: \"Email Address\", // humanized fallback or @label override\n view: \"text\", // MetaView subtype\n htmlType: \"email\", // optional; maps view \u2192 HTML <input type=>\n placeholder: \"you@example.com\", // optional; only when @placeholder is set on the view\n helpText: \"We never share this.\", // optional; only when @helpText is set\n rules: { // optional; derived from validator children\n required: \"Email is required\",\n maxLength: { value: 255, message: \"Too long\" },\n pattern: { value: /.../, message: \"Invalid email\" },\n },\n },\n firstName: { name: \"firstName\", label: \"First Name\", view: \"text\", htmlType: \"text\", rules: { required: \"First Name is required\" } },\n // ...\n} as const;\n```\n\n**Use them everywhere \u2014 in both generated AND hand-written code:**\n\n```tsx\n// \u2717 Don't:\n<input name=\"email\" type=\"email\" placeholder=\"Email\" />\n\n// \u2713 Do:\n<input\n type={Subscriber.email.htmlType}\n name={Subscriber.email.name}\n placeholder={Subscriber.email.placeholder}\n aria-label={Subscriber.email.label}\n/>\n```\n\nRename a field in `metaobjects/` and re-gen \u2014 TypeScript catches every stale reference.\n\n**Special case \u2014 Drizzle column access:** when you're already inside Drizzle's typed builder, just use the column properties directly. Drizzle's table-const types are themselves derived from metadata, so `weeks.programId` is already TS-safe:\n\n```ts\n// \u2713 Use Drizzle's typed accessor directly \u2014 no constants needed here:\ndb.select().from(weeks).where(eq(weeks.programId, X))\n\n// \u2717 Don't do this \u2014 it's redundant indirection:\ndb.select().from(weeks).where(eq(weeks[Week.programId.name], X))\n```\n\nUse the constants when you need a STRING (filter object keys, registration arguments, REST paths, labels). Use Drizzle properties directly when the type system already does the work.\n\n### 3. Forms: spread `form.input.<field>` from useEntityForm. One line per input.\n\nFor React forms, use `useEntityForm` from `@metaobjectsdev/react`. It returns the standard React Hook Form surface plus a pre-bound `.input` accessor \u2014 one entry per field, ready to spread onto an `<input>`:\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema } from './generated/Subscriber';\n\nconst form = useEntityForm(Subscriber, SubscriberInsertSchema);\n\n<label>{Subscriber.email.label}</label>\n<input {...form.input.email} /> // \u2190 type, placeholder, name, rules, aria-label all spread automatically\n```\n\nFor non-`<input>` controls (textarea, select), the `type` attr is omitted from `form.input.X` \u2014 pick the right element yourself.\n\nThe same Zod schema (`SubscriberInsertSchema`) validates on the server (in Fastify routes) and on the client (via the resolver). One schema, two surfaces, zero drift.\n\n### 4. Routes: use the generated `<Entity>.routes.ts` for stock CRUD. Hand-write only what's custom.\n\n`meta gen` emits a per-entity routes file that mounts the 5 standard verbs via `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify`. The runtime is plain Drizzle + Zod \u2014 no extra ORM.\n\nFor custom flows (Stripe webhooks, side effects, auth-gated actions), hand-write the route \u2014 but import the entity constants + generated Zod schema. The boilerplate (CRUD, validation, 404 mapping, pagination) lives in the helper; your hand-written code is just the business logic.\n\n**Auth pattern:** install a plugin-level Fastify `preHandler` hook at the top of your route plugin. The hook applies to every route registered after it \u2014 both hand-written handlers AND metaobjects-generated routes via the `routeOptions` field. Beats sprinkling `if (!auth(...)) return;` at the top of every handler.\n\n### 5. Hand-coded code is always available, but coexists with generated code.\n\nGenerated code does the boilerplate. Hand-coded code does the business logic. They live in the same project, the same package, sometimes the same file. The hand-coded code consumes the generated constants and generated Zod schemas \u2014 it never duplicates schema, never hard-codes paths, never declares its own validators that metadata could declare.\n\nConcrete pattern from the trainer website:\n- Generated `Subscriber.routes.ts` registers GET / GET-by-id / POST / PATCH / DELETE on `/api/subscribers`.\n- Hand-written `apps/api/src/routes/subscribers.ts` keeps `POST /subscribe` \u2014 the custom endpoint with the Loops side-effect.\n- Both registered with `fastify.register()`. Both validate via `SubscriberInsertSchema`. Both use `Subscriber.email.name` / etc. Neither knows the other exists.\n\n## Metaobjects metamodel \u2014 quick rules\n\nThe format used by `metaobjects/*.json` is **metaobjects metadata**, a cross-language standard. Eight base types:\n\n| Type | Purpose |\n|---|---|\n| `metadata` | Root document wrapper |\n| `object` | An entity (table/record) |\n| `field` | A property on an object |\n| `attr` | Named scalar/array decoration on any parent |\n| `validator` | A validation rule |\n| `view` | A UI control kind |\n| `identity` | A primary/secondary key |\n| `relationship` | An association between objects |\n\n### Two most-violated rules\n\n1. **Attribute uniqueness.** Within a single parent metadata node, all attribute names must be unique. You cannot have two `attr` children both named `alternative`. For multi-value, use a single `stringarray` attr: `\"@alternatives\": [\"a\", \"b\", \"c\"]`.\n\n2. **Inline `@<name>` and `attr` child are the same thing.** `\"@maxLength\": 50` is shorthand for `{\"attr\": {\"name\": \"maxLength\", \"subType\": \"int\", \"value\": \"50\"}}`. The parser converts inline form into attr children. Don't use both forms for the same attribute name on the same parent.\n\n### Object subtypes (v0.3)\n\n- `base` \u2014 abstract template (no runtime semantics)\n- `entity` \u2014 persistent record; should have a primary identity\n- `value` \u2014 value-object; equality by content; must NOT have a primary identity\n\nJava-runtime strategies (pojo / map / proxy) belong on `@javaRuntime`, not in `subType`.\n\n### Reserved structural keys (NOT attributes)\n\n`name`, `subType`, `package`, `extends`, `isAbstract`, `children`, `merge`, `value`.\n\nThe v0.2 keys (`super`, `overlay`, `override`, `isInterface`, `implements`) are **gone**. The current parser will reject them. Use:\n- `extends:` instead of `super:` for the supertype reference\n- `merge: true` instead of `overlay: true` / `override: true` for in-place modification\n- `@isAbstract: true` instead of `isInterface: true` (multiple inheritance is not supported)\n\n### Package paths and inheritance\n\n- Package segments separated by `::` \u2014 `acme::common::id`\n- Relative references in `extends:` \u2014 `..::common::id` means \"go up to parent package, descend into `common::id`\". Relative forms (`..::` parent-relative, leading `::` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with `ERR_RELATIVE_REF_IN_CANONICAL`).\n- Cross-file resolution works as long as all files are passed to Loader (or live in the same `metaobjects/` directory)\n\n### Two special intercepted attrs (parser-routed)\n\n- `@isArray` \u2192 marks a field as a collection\n- `@isAbstract` \u2192 marks a node as abstract (inheritable but not instantiable)\n\n## Validators \u2014 two layers\n\nValidators can attach in two places, and they compose:\n\n**Field-level validators** describe what makes the *stored value* valid. They survive across UI, API, batch import, manual SQL \u2014 anywhere data enters the system. The generated Zod `<Entity>InsertSchema` encodes these.\n\n```json\n{\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"required\"}},\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^[^@]+@[^@]+\\\\.[^@]+$\"}}\n ]\n}}\n```\n\n**View-level validators** describe what makes user *input* valid in a specific UI surface \u2014 possibly stricter, possibly with different messages, possibly format-specific. They run client-side in generated forms. They do NOT necessarily reject the stored value if it's already in the DB.\n\n```json\n{\"field\": {\"name\": \"phone\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^\\\\+?[0-9]+$\"}},\n {\"view\": {\"subType\": \"text-input\", \"@label\": \"Phone\",\n \"children\": [\n {\"validator\": {\"subType\": \"length\", \"@min\": 7, \"@max\": 20,\n \"@message\": \"Phone must be 7-20 digits\"}}\n ]\n }}\n ]\n}}\n```\n\nRule of thumb: rules that protect data integrity \u2192 field. Rules that improve input UX \u2192 view.\n\n## metaobjects.config.ts \u2014 generator wiring (project root)\n\n`meta gen` reads `metaobjects.config.ts` at the project root. This is where you declare which generators run and their options. It is TypeScript, type-checked, and imported via `jiti` at run time.\n\n```ts\nimport { defineConfig } from \"@metaobjectsdev/cli\";\nimport {\n entityFile, queriesFile, routesFile, /* formFile, */ barrel,\n} from \"@metaobjectsdev/codegen-ts/generators\";\n\nexport default defineConfig({\n outDir: \"packages/database/src/generated\",\n extStyle: \"none\",\n dbImport: \"../index\",\n dialect: \"sqlite\",\n generators: [\n entityFile(),\n queriesFile(),\n routesFile(),\n // formFile(), // opt-in: emits stock React forms per entity\n barrel(),\n ],\n});\n```\n\n3rd-party generator example: `import { tanstackQuery } from \"@metaobjectsdev/codegen-ts-tanstack\"; // then add tanstackQuery({ ... }) to the generators array`\n\nFilters live on the generator entry: `routesFile({ filter: e => e.name !== \"AuditLog\" })`\n\n`.metaobjects/config.json` is unchanged \u2014 it still holds static project state (schema_version, pending_in_git, confidence_thresholds). Generator wiring belongs in `metaobjects.config.ts` so TypeScript can type-check the imports.\n\n## Generated hooks + grids (TanStack)\n\nWhen `tanstackQuery()` is in your `metaobjects.config.ts`, every entity gets `<Entity>.hooks.ts` with a query-key factory + `useEntity`, `useEntities`, `useCreate`, `useUpdate`, `useDelete` hooks. When `tanstackGrid()` is in the config, entities with a `layout[dataGrid]` child also get `<Entity>.columns.tsx`.\n\n```tsx\nimport { usePrograms, useCreateProgram } from \"@your-pkg/database/generated/Program.hooks\";\nimport { programDefaultColumns, programDefaultGrid } from \"@your-pkg/database/generated/Program.columns\";\nimport { EntityGrid } from \"@metaobjectsdev/tanstack\";\n\nconst { data, isLoading } = usePrograms();\nconst create = useCreateProgram({ onSuccess: () => navigate(\"/programs\") });\n\n<EntityGrid\n columns={programDefaultColumns}\n grid={programDefaultGrid}\n data={data ?? []}\n isLoading={isLoading}\n onRowClick={(row) => navigate(`/admin/programs/${row.id}`)}\n/>\n```\n\n**Provider setup.** Wrap your app with `<EntityFetcherProvider value={fetcher}>` (supplies the HTTP fetcher to all generated hooks). For an admin subtree with different auth, wrap a second time inside: `<EntityFetcherProvider value={adminFetch}>...</EntityFetcherProvider>` overrides the outer one.\n\n**Metadata layer \u2014 grid definition:**\n\n```jsonc\n{ \"layout\": {\n \"subType\": \"dataGrid\",\n \"name\": \"default\",\n \"@pageSize\": 25,\n \"@defaultSortField\": \"createdAt\",\n \"@defaultSortOrder\": \"desc\",\n \"@filterable\": true,\n \"@columns\": [\"email\", \"firstName\", \"subscribed\", \"createdAt\"]\n}}\n```\n\nThe `@columns` attr is a flat string array listing fields to display. Per-column rendering comes from each field's own `view` subtype (the same one that drives forms); sortability comes from the field's `@sortable` attr; width belongs in app CSS. There are no nested per-column children \u2014 just `@columns`.\n\n**Cell renderers.** Field rendering inside grids comes from each field's own `view` subtype (the same one that drives forms). Override defaults app-wide with `<CellRendererProvider value={{ currency: ({ getValue }) => <Money value={getValue()} /> }}>`.\n\n**Per-entity opt-out.** `@emitTanstack: false` on an entity skips both hooks and columns.\n\n## Filtering generated lists\n\nMark filterable fields in metadata with `@filterable: true`:\n\n```jsonc\n{ \"field\": { \"name\": \"email\", \"subType\": \"string\", \"@filterable\": true } }\n```\n\nThe generated `useSubscribers(filter)` hook accepts a typed filter:\n\n```tsx\nconst { data } = useSubscribers({\n email: { like: \"amy@%\" },\n subscribed: true,\n sort: \"createdAt:desc\",\n limit: 25,\n});\n```\n\nURL sent: `/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25`\n\n**Leading wildcards are rejected by default.** The generated `<Entity>FilterAllowlist` ships `leadingWildcard: false` on every field, so a `like` pattern starting with `%` (e.g. `\"%@example.com\"`) is a 400 `filter.leading_wildcard_disallowed` \u2014 an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to `leadingWildcard: true` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.\n\n**Operators by field subtype:**\n- String: `eq, ne, in, like, isNull`\n- Number/date: `eq, ne, gt, gte, lt, lte, in, isNull`\n- Boolean: `eq, isNull`\n\nIllegal combinations like `useSubscribers({ subscribed: { gte: true } })` fail to compile (booleans don't support `gte`).\n\n**Per-grid preset filter** via layout `@filter`:\n\n```jsonc\n{ \"layout\": { \"subType\": \"dataGrid\", \"name\": \"active\",\n \"@filter\": { \"subscribed\": true },\n \"@columns\": [\"email\", \"firstName\", \"subscribed\"] }}\n```\n\nGenerates `subscriberActiveFilter` const consumable in pages. Compose with ad-hoc filters via object spread.\n\n## Projections (read models with joined/aggregated columns)\n\nWhen a list needs computed columns (counts, sums, joined fields), create a **projection** \u2014 an entity that extends a base entity but reads from a SQL view:\n\n```json\n// metaobjects/meta.commerce.json (inline with Program)\n{\n \"object\": {\n \"name\": \"ProgramSummary\",\n \"subType\": \"entity\",\n \"extends\": \"Program\",\n \"children\": [\n { \"source\": { \"subType\": \"rdb\", \"@kind\": \"view\", \"@table\": \"v_program_summary\" } },\n { \"field\": { \"name\": \"weekCount\", \"subType\": \"int\", \"children\": [\n { \"origin\": { \"subType\": \"aggregate\",\n \"@agg\": \"count\", \"@of\": \"Week.id\", \"@via\": \"Program.weeks\" }}\n ]}},\n { \"identity\": { \"subType\": \"primary\", \"name\": \"id\", \"@fields\": \"id\" } }\n ]\n }\n}\n```\n\n`meta gen` produces a read-only `useProgramSummaries(filter)` hook, a SQL view DDL in the migration, and a read-only GET-only route.\n\n**Aggregate vocabulary** (`origin.aggregate @agg`): `count`, `sum`, `avg`, `min`, `max`; plus `any`/`all` (a boolean predicate quantifier over a required `@filter` \u2014 no `@of`; \"did any/every related row match?\"), and `collect` (an array rollup of `@of` \u2014 the field must be `isArray: true`; `@distinct` dedupes, `@orderBy` sets element order).\n\n**Other read-model origins**: `origin.computed` \u2014 a row-level value from the base row's own fields via a structured `@expr` tree (e.g. `{ \"op\": \"isNotNull\", \"arg\": { \"field\": \"payloadJson\" } }` \u2192 a boolean, to avoid shipping a heavy column); `origin.first` \u2014 the single related row picked by `@orderBy` along `@via`, projecting `@of` (e.g. \"the latest child's status\"; the field must not be `@required` \u2014 an empty related set yields null).\n\n**Multi-level via paths** are supported: `@via: \"Program.weeks.workouts\"` builds a 2-level JOIN tree.\n\n**For pages that need a full nested tree** (e.g., Program \u2192 Weeks \u2192 Workouts \u2192 Exercises), use 4 entity hooks with Project D's filter syntax for batched lookups (no projection needed \u2014 flat hooks + client-side stitching is enough):\n\n```tsx\nconst { data: weeks } = useWeeks({ programId, sort: \"weekNumber:asc\" });\nconst weekIds = weeks?.map((w) => w.id) ?? [];\nconst { data: workouts } = useWorkouts(\n weekIds.length ? { weekId: { in: weekIds } } : undefined,\n);\n```\n\n## Currency fields\n\nDeclare a money field with `subType: \"currency\"`:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"USD\" } }\n```\n\nStorage stays as integer minor units (cents for USD). The generated `<Entity>` constants block carries `view`, `currency`, `locale` so admin grids auto-format prices.\n\n**Imports \u2014 use sub-paths in browser code:**\n\n```tsx\nimport { formatCurrency } from \"@metaobjectsdev/runtime-web\";\nimport { CurrencyInput } from \"@metaobjectsdev/react\";\n```\n\n**Display:**\n\n```tsx\n<span>{formatCurrency(program.priceCents)}</span> // $15.00\n<span>{formatCurrency(p.amountCents, \"EUR\", \"de-DE\")}</span> // 15,00 \u20AC\n```\n\n**Form input:**\n\n```tsx\n<CurrencyInput value={priceCents} onChange={setPriceCents} currency=\"USD\" />\n```\n\nUser types `15.99` \u2192 component emits `1599` to `onChange` on blur. Wire format is always integer cents.\n\n**Locale override** via a `view[currency]` child:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"EUR\",\n \"children\": [{ \"view\": { \"subType\": \"currency\", \"@locale\": \"de-DE\" } }]\n}}\n```\n\nCurrency code lives on the field; locale lives on the view.\n\n## Generated artifacts \u2014 what `meta gen` produces\n\nAfter `meta gen`, you get one barrel + per-entity files in your configured `outDir` (default `packages/database/src/generated/`):\n\n| File | What's in it | When to touch by hand |\n|---|---|---|\n| `<Entity>.ts` | Drizzle table, relations(), inferred types, Zod insert/update schemas, and the rich `<Entity>` constants block (per-field objects with name, label, view, htmlType, rules, etc.) | Never. Regenerate. |\n| `<Entity>.queries.ts` | Typed query helpers (`findUserById`, `listUsers`, `createUser`, ...) using prepared statements | Never. Regenerate. |\n| `<Entity>.routes.ts` | Fastify CRUD plugin delegating to `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify` (5 verbs, Zod validation, 404/204 mapping, Drizzle-direct under the hood) | Never. Regenerate. |\n| `<Entity>.form.tsx` | React form using `useEntityForm` + the entity constants. **OPT-IN at project level:** add `formFile()` to `generators` in `metaobjects.config.ts`. Opt out per-entity via `@emitForm: false`. | Never. Regenerate. |\n| `index.ts` | Barrel re-exporting every entity file | Never. Regenerate. |\n\nFor business logic the generator doesn't cover, create a SIBLING file: `<Entity>.extra.ts` for query/route helpers, or any file you like in your apps directory. Import the constants from the generated `<Entity>.ts`.\n\n### Stock route mounting\n\n```ts\nimport { subscriberRoutes } from \"@your-pkg/database/generated/Subscriber.routes\";\nfastify.register(subscriberRoutes, { prefix: \"/api\" });\n```\n\nThat mounts: `GET /api/subscribers`, `GET /api/subscribers/:id`, `POST /api/subscribers`, `PATCH /api/subscribers/:id`, `DELETE /api/subscribers/:id`. Pagination via `?limit=` & `?offset=`. Validation via the generated Zod schemas. Drizzle calls under the hood.\n\n### Mixing custom routes alongside generated\n\n```ts\nimport { db, subscribers } from \"@your-pkg/database\";\nimport { Subscriber, SubscriberInsertSchema } from \"@your-pkg/database/generated/Subscriber\";\nimport { eq } from \"drizzle-orm\";\n\nfastify.post(\"/subscribe\", async (req, reply) => {\n // Generated Zod schema validates the body \u2014 same schema the API route uses.\n const parsed = SubscriberInsertSchema.safeParse(req.body);\n if (!parsed.success) return reply.code(400).send({ issues: parsed.error.issues });\n\n // Drizzle's typed accessors are already TS-safe; no need for indirection.\n const existing = await db.select().from(subscribers).where(eq(subscribers.email, parsed.data.email)).get();\n if (existing) return reply.code(409).send({ error: \"Already subscribed\" });\n\n const [row] = await db.insert(subscribers).values(parsed.data).returning();\n // ... your business logic (analytics, Loops/Mailchimp, navigation, etc.)\n return reply.code(201).send(row);\n});\n```\n\nThe fact that every metadata-derived value flows from `Subscriber` / `SubscriberInsertSchema` / the typed `subscribers` table is what makes rename-the-field-in-metadata-and-regen safe.\n\n### Hand-written form using `useEntityForm`\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema, type Subscriber as Row } from './generated/Subscriber';\n\nexport function SubscribeForm() {\n const form = useEntityForm(Subscriber, SubscriberInsertSchema);\n const { handleSubmit, formState: { errors } } = form;\n\n return (\n <form onSubmit={handleSubmit(/* your onSubmit */)} className=\"your-design-system\">\n <label>{Subscriber.email.label}</label>\n <input {...form.input.email} />\n {errors.email && <span>{errors.email.message}</span>}\n\n <label>{Subscriber.firstName.label}</label>\n <input {...form.input.firstName} />\n {errors.firstName && <span>{errors.firstName.message}</span>}\n\n <button type=\"submit\">Subscribe</button>\n </form>\n );\n}\n```\n\nSpread `form.input.<field>` \u2014 it carries name, type, placeholder, rules, aria-label automatically. No magic strings.\n\n## Meta Forge additions\n\n### `@forge*` attribute namespace\n\nProvenance and confidence concerns expressed as inline attributes on any metadata child. Names use camelCase (no separator).\n\nMost common:\n- `@forgeConfidence` (double 0..1) \u2014 confidence the record is correct\n- `@forgeSource` (string) \u2014 `human` | `claude` | `ts-ast` | `drizzle` | ...\n- `@forgePrimaryLocation` (string) \u2014 file path for an entity\n- `@forgeRationale` (string, decision only) \u2014 why this decision\n- `@forgeAlternatives` (stringarray, decision only) \u2014 alternatives considered\n\nFull inventory in `packages/sdk/FORGE-METADATA.md`.\n\n### New top-level types\n\nRegistered by `@metaobjectsdev/sdk` into the TypeRegistry:\n\n| Type | Purpose |\n|---|---|\n| `decision` | Architectural or design decision |\n| `principle` | Design principle (advisory/enforced) |\n| `convention` | Coding/structural convention |\n| `glossary` | Domain-term definition |\n| `failure` | Recorded failure mode |\n\nThese coexist with `object` children in the same package files. `meta gen` and `meta migrate` only consume `object`; the descriptive types are context for AI tooling and don't drive codegen.\n\n## File layout\n\n```\nmetaobjects/\n\u251C\u2500\u2500 meta.common.json shared base fields/validators (optional)\n\u251C\u2500\u2500 meta.<domain>.json your entity packages(s)\n\u2514\u2500\u2500 _pending/<pkg>.json proposed packages awaiting review\n\n.metaobjects/\n\u251C\u2500\u2500 config.json static project state\n\u251C\u2500\u2500 migrations/ written by meta migrate\n\u2514\u2500\u2500 .gen-state/ codegen merge base (gitignored)\n\nmetaobjects.config.ts generator wiring (committed)\n```\n\n## Worked example\n\n```json\n{\n \"metadata\": {\n \"package\": \"myapp\",\n \"children\": [\n {\n \"object\": {\n \"name\": \"User\",\n \"subType\": \"entity\",\n \"@forgeConfidence\": 0.95,\n \"@forgeSource\": \"human\",\n \"@forgePrimaryLocation\": \"src/db/users.schema.ts\",\n \"children\": [\n {\"field\": {\"name\": \"id\", \"extends\": \"common::id\"}},\n {\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"@column\": \"email_address\",\n \"children\": [{\"validator\": {\"subType\": \"required\"}}]\n }},\n {\"identity\": {\"name\": \"pk\", \"subType\": \"primary\", \"@fields\": [\"id\"], \"@generation\": \"increment\"}}\n ]\n }\n },\n {\n \"decision\": {\n \"name\": \"useTanstackQuery\",\n \"subType\": \"global\",\n \"@forgeConfidence\": 0.9,\n \"@forgeSource\": \"human\",\n \"@forgeRationale\": \"Real-time invalidation matters for live game state.\",\n \"@forgeAlternatives\": [\"swr\", \"redux-toolkit-query\"]\n }\n }\n ]\n }\n}\n```\n\n## Authoring guidance\n\n| Situation | Action |\n|---|---|\n| Adding a field to an existing entity | Edit the `object`'s `children`; append a `field` node, then `meta gen` |\n| New entity in an existing domain | Append an `object` to the appropriate package file, then `meta gen` |\n| Renaming an entity or field | Edit the metadata, regenerate; TS will surface every stale consumer of the constants |\n| New REST resource | Already done \u2014 `meta gen` produced `<Entity>.routes.ts`. Just `fastify.register(...)` it |\n| Custom business logic (Stripe webhook, side-effects, auth flows) | Hand-write a route/handler that imports the generated constants + `om()` |\n| Architectural choice affecting how entities are built | Add a `decision` with `@forgeRationale` + `@forgeAlternatives` |\n| Coding convention | Add a `convention` with `@forgePatternDescription` + `@forgeAppliesTo` |\n| Domain term | Add a `glossary` entry with `@forgeTerm` + `@forgeDefinition` |\n\n## Deeper references\n\n- `packages/metadata/METAMODEL.md` \u2014 full metamodel reference\n- `packages/sdk/FORGE-METADATA.md` \u2014 full `@forge*` inventory + MetaObjects layout details\n- `docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md` \u2014 current strategy (v0.3 vocab, packages, AI-first loading)\n";
5
+ export declare const AGENT_DOCS_BODY = "# Meta Forge \u2014 agent reference\n\nThis file is scaffolded by `meta init` and lives alongside your `metaobjects/` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with `meta init --refresh-docs`.\n\n## Five working principles (read first)\n\nThese shape every interaction with a metaobjects-driven project. Follow them when you author metadata, write hand-coded business logic, or review someone else's work.\n\n### 1. If it's pattern-derivable from metadata, generate it. Never hand-write boilerplate.\n\nThe metaobjects raison d'\u00EAtre is that anything the metadata fully describes \u2014 schemas, FK references, basic CRUD, query helpers, Zod validators, route handlers, RHF rules, form fields \u2014 should be produced by codegen, not hand-typed. If you find yourself hand-writing something the metadata already knows about, stop and use the generated artifact.\n\nThe first version of the trainer website's database layer had hand-written Drizzle schemas, Zod schemas, and CRUD endpoints. Every one of those is now generated. The hand-written code that remains is real business logic (Stripe webhooks, Loops integration, custom auth flows) \u2014 things the generator genuinely cannot derive.\n\nWhen you're about to add a new field or entity: edit `metaobjects/*.json` and re-run `meta gen`. Don't reach for the generated file directly.\n\n### 2. Use the generated constants. Never use magic strings that touch metadata.\n\nAfter `meta gen`, each entity file exports a rich metadata-constants block. Each non-dollar-prefixed key is a per-field object that carries everything a consumer might need (name, label, view, html input type, placeholder, RHF validation rules):\n\n```ts\nexport const Subscriber = {\n $entity: \"Subscriber\", // entity name string\n $table: \"subscribers\", // SQL table name\n $path: \"/subscribers\", // REST resource path\n\n email: {\n name: \"email\", // field name string (use for filters, register())\n label: \"Email Address\", // humanized fallback or @label override\n view: \"text\", // MetaView subtype\n htmlType: \"email\", // optional; maps view \u2192 HTML <input type=>\n placeholder: \"you@example.com\", // optional; only when @placeholder is set on the view\n helpText: \"We never share this.\", // optional; only when @helpText is set\n rules: { // optional; derived from validator children\n required: \"Email is required\",\n maxLength: { value: 255, message: \"Too long\" },\n pattern: { value: /.../, message: \"Invalid email\" },\n },\n },\n firstName: { name: \"firstName\", label: \"First Name\", view: \"text\", htmlType: \"text\", rules: { required: \"First Name is required\" } },\n // ...\n} as const;\n```\n\n**Use them everywhere \u2014 in both generated AND hand-written code:**\n\n```tsx\n// \u2717 Don't:\n<input name=\"email\" type=\"email\" placeholder=\"Email\" />\n\n// \u2713 Do:\n<input\n type={Subscriber.email.htmlType}\n name={Subscriber.email.name}\n placeholder={Subscriber.email.placeholder}\n aria-label={Subscriber.email.label}\n/>\n```\n\nRename a field in `metaobjects/` and re-gen \u2014 TypeScript catches every stale reference.\n\n**Special case \u2014 Drizzle column access:** when you're already inside Drizzle's typed builder, just use the column properties directly. Drizzle's table-const types are themselves derived from metadata, so `weeks.programId` is already TS-safe:\n\n```ts\n// \u2713 Use Drizzle's typed accessor directly \u2014 no constants needed here:\ndb.select().from(weeks).where(eq(weeks.programId, X))\n\n// \u2717 Don't do this \u2014 it's redundant indirection:\ndb.select().from(weeks).where(eq(weeks[Week.programId.name], X))\n```\n\nUse the constants when you need a STRING (filter object keys, registration arguments, REST paths, labels). Use Drizzle properties directly when the type system already does the work.\n\n### 3. Forms: spread `form.input.<field>` from useEntityForm. One line per input.\n\nFor React forms, use `useEntityForm` from `@metaobjectsdev/react`. It returns the standard React Hook Form surface plus a pre-bound `.input` accessor \u2014 one entry per field, ready to spread onto an `<input>`:\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema } from './generated/Subscriber';\n\nconst form = useEntityForm(Subscriber, SubscriberInsertSchema);\n\n<label>{Subscriber.email.label}</label>\n<input {...form.input.email} /> // \u2190 type, placeholder, name, rules, aria-label all spread automatically\n```\n\nFor non-`<input>` controls (textarea, select), the `type` attr is omitted from `form.input.X` \u2014 pick the right element yourself.\n\nThe same Zod schema (`SubscriberInsertSchema`) validates on the server (in Fastify routes) and on the client (via the resolver). One schema, two surfaces, zero drift.\n\n### 4. Routes: use the generated `<Entity>.routes.ts` for stock CRUD. Hand-write only what's custom.\n\n`meta gen` emits a per-entity routes file that mounts the 5 standard verbs via `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify`. The runtime is plain Drizzle + Zod \u2014 no extra ORM.\n\nFor custom flows (Stripe webhooks, side effects, auth-gated actions), hand-write the route \u2014 but import the entity constants + generated Zod schema. The boilerplate (CRUD, validation, 404 mapping, pagination) lives in the helper; your hand-written code is just the business logic.\n\n**Auth pattern:** install a plugin-level Fastify `preHandler` hook at the top of your route plugin. The hook applies to every route registered after it \u2014 both hand-written handlers AND metaobjects-generated routes via the `routeOptions` field. Beats sprinkling `if (!auth(...)) return;` at the top of every handler.\n\n### 5. Hand-coded code is always available, but coexists with generated code.\n\nGenerated code does the boilerplate. Hand-coded code does the business logic. They live in the same project, the same package, sometimes the same file. The hand-coded code consumes the generated constants and generated Zod schemas \u2014 it never duplicates schema, never hard-codes paths, never declares its own validators that metadata could declare.\n\nConcrete pattern from the trainer website:\n- Generated `Subscriber.routes.ts` registers GET / GET-by-id / POST / PATCH / DELETE on `/api/subscribers`.\n- Hand-written `apps/api/src/routes/subscribers.ts` keeps `POST /subscribe` \u2014 the custom endpoint with the Loops side-effect.\n- Both registered with `fastify.register()`. Both validate via `SubscriberInsertSchema`. Both use `Subscriber.email.name` / etc. Neither knows the other exists.\n\n## Metaobjects metamodel \u2014 quick rules\n\nThe format used by `metaobjects/*.json` is **metaobjects metadata**, a cross-language standard. Eight base types:\n\n| Type | Purpose |\n|---|---|\n| `metadata` | Root document wrapper |\n| `object` | An entity (table/record) |\n| `field` | A property on an object |\n| `attr` | Named scalar/array decoration on any parent |\n| `validator` | A validation rule |\n| `view` | A UI control kind |\n| `identity` | A primary/secondary key |\n| `relationship` | An association between objects |\n\n### Two most-violated rules\n\n1. **Attribute uniqueness.** Within a single parent metadata node, all attribute names must be unique. You cannot have two `attr` children both named `alternative`. For multi-value, use a single `stringarray` attr: `\"@alternatives\": [\"a\", \"b\", \"c\"]`.\n\n2. **Inline `@<name>` and `attr` child are the same thing.** `\"@maxLength\": 50` is shorthand for `{\"attr\": {\"name\": \"maxLength\", \"subType\": \"int\", \"value\": \"50\"}}`. The parser converts inline form into attr children. Don't use both forms for the same attribute name on the same parent.\n\n### Object subtypes (v0.3)\n\n- `base` \u2014 abstract template (no runtime semantics)\n- `entity` \u2014 persistent record; should have a primary identity\n- `value` \u2014 value-object; equality by content; must NOT have a primary identity\n\nJava-runtime strategies (pojo / map / proxy) belong on `@javaRuntime`, not in `subType`.\n\n### Reserved structural keys (NOT attributes)\n\n`name`, `subType`, `package`, `extends`, `isAbstract`, `children`, `merge`, `value`.\n\nThe v0.2 keys (`super`, `overlay`, `override`, `isInterface`, `implements`) are **gone**. The current parser will reject them. Use:\n- `extends:` instead of `super:` for the supertype reference\n- `merge: true` instead of `overlay: true` / `override: true` for in-place modification\n- `@isAbstract: true` instead of `isInterface: true` (multiple inheritance is not supported)\n\n### Package paths and inheritance\n\n- Package segments separated by `::` \u2014 `acme::common::id`\n- Relative references in `extends:` \u2014 `..::common::id` means \"go up to parent package, descend into `common::id`\". Relative forms (`..::` parent-relative, leading `::` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with `ERR_RELATIVE_REF_IN_CANONICAL`).\n- Cross-file resolution works as long as all files are passed to Loader (or live in the same `metaobjects/` directory)\n\n### Two special intercepted attrs (parser-routed)\n\n- `@isArray` \u2192 marks a field as a collection\n- `@isAbstract` \u2192 marks a node as abstract (inheritable but not instantiable)\n\n## Validators \u2014 two layers\n\nValidators can attach in two places, and they compose:\n\n**Field-level validators** describe what makes the *stored value* valid. They survive across UI, API, batch import, manual SQL \u2014 anywhere data enters the system. The generated Zod `<Entity>InsertSchema` encodes these.\n\n```json\n{\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"required\"}},\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^[^@]+@[^@]+\\\\.[^@]+$\"}}\n ]\n}}\n```\n\n**View-level validators** describe what makes user *input* valid in a specific UI surface \u2014 possibly stricter, possibly with different messages, possibly format-specific. They run client-side in generated forms. They do NOT necessarily reject the stored value if it's already in the DB.\n\n```json\n{\"field\": {\"name\": \"phone\", \"subType\": \"string\",\n \"children\": [\n {\"validator\": {\"subType\": \"regex\", \"@pattern\": \"^\\\\+?[0-9]+$\"}},\n {\"view\": {\"subType\": \"text-input\", \"@label\": \"Phone\",\n \"children\": [\n {\"validator\": {\"subType\": \"length\", \"@min\": 7, \"@max\": 20,\n \"@message\": \"Phone must be 7-20 digits\"}}\n ]\n }}\n ]\n}}\n```\n\nRule of thumb: rules that protect data integrity \u2192 field. Rules that improve input UX \u2192 view.\n\n## metaobjects.config.ts \u2014 generator wiring (project root)\n\n`meta gen` reads `metaobjects.config.ts` at the project root. This is where you declare which generators run and their options. It is TypeScript, type-checked, and imported via `jiti` at run time.\n\n```ts\nimport { defineConfig } from \"@metaobjectsdev/cli\";\nimport {\n entityFile, queriesFile, routesFile, /* formFile, */ barrel,\n} from \"@metaobjectsdev/codegen-ts/generators\";\n\nexport default defineConfig({\n outDir: \"packages/database/src/generated\",\n extStyle: \"none\",\n dbImport: \"../index\",\n dialect: \"sqlite\",\n generators: [\n entityFile(),\n queriesFile(),\n routesFile(),\n // formFile(), // opt-in: emits stock React forms per entity\n barrel(),\n ],\n});\n```\n\n3rd-party generator example: `import { tanstackQuery } from \"@metaobjectsdev/codegen-ts-tanstack\"; // then add tanstackQuery({ ... }) to the generators array`\n\nFilters live on the generator entry: `routesFile({ filter: e => e.name !== \"AuditLog\" })`\n\n`.metaobjects/config.json` holds static project state (schema_version, pending_in_git, confidence_thresholds) **and `sources` \u2014 the set of places your metadata comes from** (see \"Where metadata comes from\" below). Generator wiring belongs in `metaobjects.config.ts` so TypeScript can type-check the imports.\n\n## Generated hooks + grids (TanStack)\n\nWhen `tanstackQuery()` is in your `metaobjects.config.ts`, every entity gets `<Entity>.hooks.ts` with a query-key factory + `useEntity`, `useEntities`, `useCreate`, `useUpdate`, `useDelete` hooks. When `tanstackGrid()` is in the config, entities with a `layout[dataGrid]` child also get `<Entity>.columns.tsx`.\n\n```tsx\nimport { usePrograms, useCreateProgram } from \"@your-pkg/database/generated/Program.hooks\";\nimport { programDefaultColumns, programDefaultGrid } from \"@your-pkg/database/generated/Program.columns\";\nimport { EntityGrid } from \"@metaobjectsdev/tanstack\";\n\nconst { data, isLoading } = usePrograms();\nconst create = useCreateProgram({ onSuccess: () => navigate(\"/programs\") });\n\n<EntityGrid\n columns={programDefaultColumns}\n grid={programDefaultGrid}\n data={data ?? []}\n isLoading={isLoading}\n onRowClick={(row) => navigate(`/admin/programs/${row.id}`)}\n/>\n```\n\n**Provider setup.** Wrap your app with `<EntityFetcherProvider value={fetcher}>` (supplies the HTTP fetcher to all generated hooks). For an admin subtree with different auth, wrap a second time inside: `<EntityFetcherProvider value={adminFetch}>...</EntityFetcherProvider>` overrides the outer one.\n\n**Metadata layer \u2014 grid definition:**\n\n```jsonc\n{ \"layout\": {\n \"subType\": \"dataGrid\",\n \"name\": \"default\",\n \"@pageSize\": 25,\n \"@defaultSortField\": \"createdAt\",\n \"@defaultSortOrder\": \"desc\",\n \"@filterable\": true,\n \"@columns\": [\"email\", \"firstName\", \"subscribed\", \"createdAt\"]\n}}\n```\n\nThe `@columns` attr is a flat string array listing fields to display. Per-column rendering comes from each field's own `view` subtype (the same one that drives forms); sortability comes from the field's `@sortable` attr; width belongs in app CSS. There are no nested per-column children \u2014 just `@columns`.\n\n**Cell renderers.** Field rendering inside grids comes from each field's own `view` subtype (the same one that drives forms). Override defaults app-wide with `<CellRendererProvider value={{ currency: ({ getValue }) => <Money value={getValue()} /> }}>`.\n\n**Per-entity opt-out.** `@emitTanstack: false` on an entity skips both hooks and columns.\n\n## Filtering generated lists\n\nMark filterable fields in metadata with `@filterable: true`:\n\n```jsonc\n{ \"field\": { \"name\": \"email\", \"subType\": \"string\", \"@filterable\": true } }\n```\n\nThe generated `useSubscribers(filter)` hook accepts a typed filter:\n\n```tsx\nconst { data } = useSubscribers({\n email: { like: \"amy@%\" },\n subscribed: true,\n sort: \"createdAt:desc\",\n limit: 25,\n});\n```\n\nURL sent: `/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25`\n\n**Leading wildcards are rejected by default.** The generated `<Entity>FilterAllowlist` ships `leadingWildcard: false` on every field, so a `like` pattern starting with `%` (e.g. `\"%@example.com\"`) is a 400 `filter.leading_wildcard_disallowed` \u2014 an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to `leadingWildcard: true` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.\n\n**Operators by field subtype:**\n- String: `eq, ne, in, like, isNull`\n- Number/date: `eq, ne, gt, gte, lt, lte, in, isNull`\n- Boolean: `eq, isNull`\n\nIllegal combinations like `useSubscribers({ subscribed: { gte: true } })` fail to compile (booleans don't support `gte`).\n\n**Per-grid preset filter** via layout `@filter`:\n\n```jsonc\n{ \"layout\": { \"subType\": \"dataGrid\", \"name\": \"active\",\n \"@filter\": { \"subscribed\": true },\n \"@columns\": [\"email\", \"firstName\", \"subscribed\"] }}\n```\n\nGenerates `subscriberActiveFilter` const consumable in pages. Compose with ad-hoc filters via object spread.\n\n## Projections (read models with joined/aggregated columns)\n\nWhen a list needs computed columns (counts, sums, joined fields), create a **projection** \u2014 an entity that extends a base entity but reads from a SQL view:\n\n```json\n// metaobjects/meta.commerce.json (inline with Program)\n{\n \"object\": {\n \"name\": \"ProgramSummary\",\n \"subType\": \"entity\",\n \"extends\": \"Program\",\n \"children\": [\n { \"source\": { \"subType\": \"rdb\", \"@kind\": \"view\", \"@table\": \"v_program_summary\" } },\n { \"field\": { \"name\": \"weekCount\", \"subType\": \"int\", \"children\": [\n { \"origin\": { \"subType\": \"aggregate\",\n \"@agg\": \"count\", \"@of\": \"Week.id\", \"@via\": \"Program.weeks\" }}\n ]}},\n { \"identity\": { \"subType\": \"primary\", \"name\": \"id\", \"@fields\": \"id\" } }\n ]\n }\n}\n```\n\n`meta gen` produces a read-only `useProgramSummaries(filter)` hook, a SQL view DDL in the migration, and a read-only GET-only route.\n\n**Aggregate vocabulary** (`origin.aggregate @agg`): `count`, `sum`, `avg`, `min`, `max`; plus `any`/`all` (a boolean predicate quantifier over a required `@filter` \u2014 no `@of`; \"did any/every related row match?\"), and `collect` (an array rollup of `@of` \u2014 the field must be `isArray: true`; `@distinct` dedupes, `@orderBy` sets element order).\n\n**Other read-model origins**: `origin.computed` \u2014 a row-level value from the base row's own fields via a structured `@expr` tree (e.g. `{ \"op\": \"isNotNull\", \"arg\": { \"field\": \"payloadJson\" } }` \u2192 a boolean, to avoid shipping a heavy column); `origin.first` \u2014 the single related row picked by `@orderBy` along `@via`, projecting `@of` (e.g. \"the latest child's status\"; the field must not be `@required` \u2014 an empty related set yields null).\n\n**Multi-level via paths** are supported: `@via: \"Program.weeks.workouts\"` builds a 2-level JOIN tree.\n\n**For pages that need a full nested tree** (e.g., Program \u2192 Weeks \u2192 Workouts \u2192 Exercises), use 4 entity hooks with Project D's filter syntax for batched lookups (no projection needed \u2014 flat hooks + client-side stitching is enough):\n\n```tsx\nconst { data: weeks } = useWeeks({ programId, sort: \"weekNumber:asc\" });\nconst weekIds = weeks?.map((w) => w.id) ?? [];\nconst { data: workouts } = useWorkouts(\n weekIds.length ? { weekId: { in: weekIds } } : undefined,\n);\n```\n\n## Currency fields\n\nDeclare a money field with `subType: \"currency\"`:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"USD\" } }\n```\n\nStorage stays as integer minor units (cents for USD). The generated `<Entity>` constants block carries `view`, `currency`, `locale` so admin grids auto-format prices.\n\n**Imports \u2014 use sub-paths in browser code:**\n\n```tsx\nimport { formatCurrency } from \"@metaobjectsdev/runtime-web\";\nimport { CurrencyInput } from \"@metaobjectsdev/react\";\n```\n\n**Display:**\n\n```tsx\n<span>{formatCurrency(program.priceCents)}</span> // $15.00\n<span>{formatCurrency(p.amountCents, \"EUR\", \"de-DE\")}</span> // 15,00 \u20AC\n```\n\n**Form input:**\n\n```tsx\n<CurrencyInput value={priceCents} onChange={setPriceCents} currency=\"USD\" />\n```\n\nUser types `15.99` \u2192 component emits `1599` to `onChange` on blur. Wire format is always integer cents.\n\n**Locale override** via a `view[currency]` child:\n\n```json\n{ \"field\": { \"name\": \"priceCents\", \"subType\": \"currency\", \"@currency\": \"EUR\",\n \"children\": [{ \"view\": { \"subType\": \"currency\", \"@locale\": \"de-DE\" } }]\n}}\n```\n\nCurrency code lives on the field; locale lives on the view.\n\n## Generated artifacts \u2014 what `meta gen` produces\n\nAfter `meta gen`, you get one barrel + per-entity files in your configured `outDir` (default `packages/database/src/generated/`):\n\n| File | What's in it | When to touch by hand |\n|---|---|---|\n| `<Entity>.ts` | Drizzle table, relations(), inferred types, Zod insert/update schemas, and the rich `<Entity>` constants block (per-field objects with name, label, view, htmlType, rules, etc.) | Never. Regenerate. |\n| `<Entity>.queries.ts` | Typed query helpers (`findUserById`, `listUsers`, `createUser`, ...) using prepared statements | Never. Regenerate. |\n| `<Entity>.routes.ts` | Fastify CRUD plugin delegating to `mountCrudRoutes` from `@metaobjectsdev/runtime-ts/drizzle-fastify` (5 verbs, Zod validation, 404/204 mapping, Drizzle-direct under the hood) | Never. Regenerate. |\n| `<Entity>.form.tsx` | React form using `useEntityForm` + the entity constants. **OPT-IN at project level:** add `formFile()` to `generators` in `metaobjects.config.ts`. Opt out per-entity via `@emitForm: false`. | Never. Regenerate. |\n| `index.ts` | Barrel re-exporting every entity file | Never. Regenerate. |\n\nFor business logic the generator doesn't cover, create a SIBLING file: `<Entity>.extra.ts` for query/route helpers, or any file you like in your apps directory. Import the constants from the generated `<Entity>.ts`.\n\n### Stock route mounting\n\n```ts\nimport { subscriberRoutes } from \"@your-pkg/database/generated/Subscriber.routes\";\nfastify.register(subscriberRoutes, { prefix: \"/api\" });\n```\n\nThat mounts: `GET /api/subscribers`, `GET /api/subscribers/:id`, `POST /api/subscribers`, `PATCH /api/subscribers/:id`, `DELETE /api/subscribers/:id`. Pagination via `?limit=` & `?offset=`. Validation via the generated Zod schemas. Drizzle calls under the hood.\n\n### Mixing custom routes alongside generated\n\n```ts\nimport { db, subscribers } from \"@your-pkg/database\";\nimport { Subscriber, SubscriberInsertSchema } from \"@your-pkg/database/generated/Subscriber\";\nimport { eq } from \"drizzle-orm\";\n\nfastify.post(\"/subscribe\", async (req, reply) => {\n // Generated Zod schema validates the body \u2014 same schema the API route uses.\n const parsed = SubscriberInsertSchema.safeParse(req.body);\n if (!parsed.success) return reply.code(400).send({ issues: parsed.error.issues });\n\n // Drizzle's typed accessors are already TS-safe; no need for indirection.\n const existing = await db.select().from(subscribers).where(eq(subscribers.email, parsed.data.email)).get();\n if (existing) return reply.code(409).send({ error: \"Already subscribed\" });\n\n const [row] = await db.insert(subscribers).values(parsed.data).returning();\n // ... your business logic (analytics, Loops/Mailchimp, navigation, etc.)\n return reply.code(201).send(row);\n});\n```\n\nThe fact that every metadata-derived value flows from `Subscriber` / `SubscriberInsertSchema` / the typed `subscribers` table is what makes rename-the-field-in-metadata-and-regen safe.\n\n### Hand-written form using `useEntityForm`\n\n```tsx\nimport { useEntityForm } from '@metaobjectsdev/react';\nimport { Subscriber, SubscriberInsertSchema, type Subscriber as Row } from './generated/Subscriber';\n\nexport function SubscribeForm() {\n const form = useEntityForm(Subscriber, SubscriberInsertSchema);\n const { handleSubmit, formState: { errors } } = form;\n\n return (\n <form onSubmit={handleSubmit(/* your onSubmit */)} className=\"your-design-system\">\n <label>{Subscriber.email.label}</label>\n <input {...form.input.email} />\n {errors.email && <span>{errors.email.message}</span>}\n\n <label>{Subscriber.firstName.label}</label>\n <input {...form.input.firstName} />\n {errors.firstName && <span>{errors.firstName.message}</span>}\n\n <button type=\"submit\">Subscribe</button>\n </form>\n );\n}\n```\n\nSpread `form.input.<field>` \u2014 it carries name, type, placeholder, rules, aria-label automatically. No magic strings.\n\n## Meta Forge additions\n\n### `@forge*` attribute namespace\n\nProvenance and confidence concerns expressed as inline attributes on any metadata child. Names use camelCase (no separator).\n\nMost common:\n- `@forgeConfidence` (double 0..1) \u2014 confidence the record is correct\n- `@forgeSource` (string) \u2014 `human` | `claude` | `ts-ast` | `drizzle` | ...\n- `@forgePrimaryLocation` (string) \u2014 file path for an entity\n- `@forgeRationale` (string, decision only) \u2014 why this decision\n- `@forgeAlternatives` (stringarray, decision only) \u2014 alternatives considered\n\nFull inventory in `packages/sdk/FORGE-METADATA.md`.\n\n### New top-level types\n\nRegistered by `@metaobjectsdev/sdk` into the TypeRegistry:\n\n| Type | Purpose |\n|---|---|\n| `decision` | Architectural or design decision |\n| `principle` | Design principle (advisory/enforced) |\n| `convention` | Coding/structural convention |\n| `glossary` | Domain-term definition |\n| `failure` | Recorded failure mode |\n\nThese coexist with `object` children in the same package files. `meta gen` and `meta migrate` only consume `object`; the descriptive types are context for AI tooling and don't drive codegen.\n\n## File layout\n\n```\nmetaobjects/\n\u251C\u2500\u2500 meta.common.json shared base fields/validators (optional)\n\u251C\u2500\u2500 meta.<domain>.json your entity packages(s)\n\u2514\u2500\u2500 _pending/<pkg>.json proposed packages awaiting review\n\n.metaobjects/\n\u251C\u2500\u2500 config.json static project state + `sources`\n\u251C\u2500\u2500 migrations/ written by meta migrate\n\u2514\u2500\u2500 .gen-state/ codegen merge base (gitignored)\n\nmetaobjects.config.ts generator wiring (committed)\n```\n\n### Where metadata comes from \u2014 the `sources` set\n\n**`metaobjects/` is the DEFAULT VALUE of `sources`, not a requirement.** When `sources` is absent or empty in `.metaobjects/config.json`, it takes that default \u2014 the `metaobjects/` directory beside the `.metaobjects/` folder holding the config. `meta init` scaffolds `\"sources\": []`, so a project that does nothing takes the default.\n\nDo NOT assume that directory exists. A project may declare `sources` and put its metadata anywhere \u2014 a sibling module, a shared model repository, a single file \u2014 and need not have a directory of that name at all:\n\n```json\n{\n \"schema_version\": 1,\n \"sources\": [\n { \"path\": \"../model/src/main/resources/metadata\" },\n { \"path\": \"metaobjects\" }\n ]\n}\n```\n\n`meta gen`, `meta migrate`, `meta verify`, `meta docs` and `meta export` all read exactly that set, so pointing `sources` elsewhere moves every command together. A `path` is read **in place and never installed** or copied.\n\n`sources` is read by **all four CLI surfaces** \u2014 the Node `meta` CLI, `dotnet meta` (C#), `metaobjects` (Python) and `metaobjects:generate` (Java and Kotlin, via Maven). Each resolves the same files from the same declaration.\n\n**`sources` is a set, not an ordered list** \u2014 resolution is order-independent, so declaration order never changes what loads. `config.json` also rejects unknown top-level keys, so a misspelled key is an error rather than a silently ignored one.\n\n## Worked example\n\n```json\n{\n \"metadata\": {\n \"package\": \"myapp\",\n \"children\": [\n {\n \"object\": {\n \"name\": \"User\",\n \"subType\": \"entity\",\n \"@forgeConfidence\": 0.95,\n \"@forgeSource\": \"human\",\n \"@forgePrimaryLocation\": \"src/db/users.schema.ts\",\n \"children\": [\n {\"field\": {\"name\": \"id\", \"extends\": \"common::id\"}},\n {\"field\": {\"name\": \"email\", \"subType\": \"string\",\n \"@column\": \"email_address\",\n \"children\": [{\"validator\": {\"subType\": \"required\"}}]\n }},\n {\"identity\": {\"name\": \"pk\", \"subType\": \"primary\", \"@fields\": [\"id\"], \"@generation\": \"increment\"}}\n ]\n }\n },\n {\n \"decision\": {\n \"name\": \"useTanstackQuery\",\n \"subType\": \"global\",\n \"@forgeConfidence\": 0.9,\n \"@forgeSource\": \"human\",\n \"@forgeRationale\": \"Real-time invalidation matters for live game state.\",\n \"@forgeAlternatives\": [\"swr\", \"redux-toolkit-query\"]\n }\n }\n ]\n }\n}\n```\n\n## Authoring guidance\n\n| Situation | Action |\n|---|---|\n| Adding a field to an existing entity | Edit the `object`'s `children`; append a `field` node, then `meta gen` |\n| New entity in an existing domain | Append an `object` to the appropriate package file, then `meta gen` |\n| Renaming an entity or field | Edit the metadata, regenerate; TS will surface every stale consumer of the constants |\n| New REST resource | Already done \u2014 `meta gen` produced `<Entity>.routes.ts`. Just `fastify.register(...)` it |\n| Custom business logic (Stripe webhook, side-effects, auth flows) | Hand-write a route/handler that imports the generated constants + `om()` |\n| Architectural choice affecting how entities are built | Add a `decision` with `@forgeRationale` + `@forgeAlternatives` |\n| Coding convention | Add a `convention` with `@forgePatternDescription` + `@forgeAppliesTo` |\n| Domain term | Add a `glossary` entry with `@forgeTerm` + `@forgeDefinition` |\n\n## Deeper references\n\n- `packages/metadata/METAMODEL.md` \u2014 full metamodel reference\n- `packages/sdk/FORGE-METADATA.md` \u2014 full `@forge*` inventory + MetaObjects layout details\n- `docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md` \u2014 current strategy (v0.3 vocab, packages, AI-first loading)\n";
6
6
  //# sourceMappingURL=body.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AACA;;;GAGG;AACH,eAAO,MAAM,eAAe,su3BAojB3B,CAAC"}
1
+ {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AACA;;;GAGG;AACH,eAAO,MAAM,eAAe,uy6BA0kB3B,CAAC"}
@@ -222,7 +222,7 @@ export default defineConfig({
222
222
 
223
223
  Filters live on the generator entry: \`routesFile({ filter: e => e.name !== "AuditLog" })\`
224
224
 
225
- \`.metaobjects/config.json\` is unchanged — it still holds static project state (schema_version, pending_in_git, confidence_thresholds). Generator wiring belongs in \`metaobjects.config.ts\` so TypeScript can type-check the imports.
225
+ \`.metaobjects/config.json\` holds static project state (schema_version, pending_in_git, confidence_thresholds) **and \`sources\` — the set of places your metadata comes from** (see "Where metadata comes from" below). Generator wiring belongs in \`metaobjects.config.ts\` so TypeScript can type-check the imports.
226
226
 
227
227
  ## Generated hooks + grids (TanStack)
228
228
 
@@ -503,13 +503,35 @@ metaobjects/
503
503
  └── _pending/<pkg>.json proposed packages awaiting review
504
504
 
505
505
  .metaobjects/
506
- ├── config.json static project state
506
+ ├── config.json static project state + \`sources\`
507
507
  ├── migrations/ written by meta migrate
508
508
  └── .gen-state/ codegen merge base (gitignored)
509
509
 
510
510
  metaobjects.config.ts generator wiring (committed)
511
511
  \`\`\`
512
512
 
513
+ ### Where metadata comes from — the \`sources\` set
514
+
515
+ **\`metaobjects/\` is the DEFAULT VALUE of \`sources\`, not a requirement.** When \`sources\` is absent or empty in \`.metaobjects/config.json\`, it takes that default — the \`metaobjects/\` directory beside the \`.metaobjects/\` folder holding the config. \`meta init\` scaffolds \`"sources": []\`, so a project that does nothing takes the default.
516
+
517
+ Do NOT assume that directory exists. A project may declare \`sources\` and put its metadata anywhere — a sibling module, a shared model repository, a single file — and need not have a directory of that name at all:
518
+
519
+ \`\`\`json
520
+ {
521
+ "schema_version": 1,
522
+ "sources": [
523
+ { "path": "../model/src/main/resources/metadata" },
524
+ { "path": "metaobjects" }
525
+ ]
526
+ }
527
+ \`\`\`
528
+
529
+ \`meta gen\`, \`meta migrate\`, \`meta verify\`, \`meta docs\` and \`meta export\` all read exactly that set, so pointing \`sources\` elsewhere moves every command together. A \`path\` is read **in place and never installed** or copied.
530
+
531
+ \`sources\` is read by **all four CLI surfaces** — the Node \`meta\` CLI, \`dotnet meta\` (C#), \`metaobjects\` (Python) and \`metaobjects:generate\` (Java and Kotlin, via Maven). Each resolves the same files from the same declaration.
532
+
533
+ **\`sources\` is a set, not an ordered list** — resolution is order-independent, so declaration order never changes what loads. \`config.json\` also rejects unknown top-level keys, so a misspelled key is an error rather than a silently ignored one.
534
+
513
535
  ## Worked example
514
536
 
515
537
  \`\`\`json
@@ -1 +1 @@
1
- {"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAojB9B,CAAC"}
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0kB9B,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { type ResolvedSource } from "./sources.js";
2
+ export interface Collection {
3
+ /** Directory whose config declared this collection (or the resolved start
4
+ * directory, when nothing was discovered and the default applies). */
5
+ readonly configDir: string;
6
+ /** Canonically-ordered absolute metadata file paths — see `resolveSources`.
7
+ * Canonical, not sorted: within a directory source the walk order the
8
+ * toolchain has always used is preserved, because it survives into
9
+ * generated output. */
10
+ readonly files: readonly string[];
11
+ /** Same set, carrying the contributing spec for provenance. */
12
+ readonly sources: readonly ResolvedSource[];
13
+ /** The distinct roots the declared source specs resolve to, absolute, in the
14
+ * same canonical (content) order `files` uses. Derived from the DECLARED
15
+ * specs, not from the resolved files, so a source directory that legitimately
16
+ * holds no metadata still appears — a consumer listing "where this model
17
+ * comes from" (`meta docs --site` groups its pages by source root) must not
18
+ * silently lose a declared source because it happens to be empty today. */
19
+ readonly sourceRoots: readonly string[];
20
+ /**
21
+ * Output filter for codegen: does this fully-qualified name survive the
22
+ * collection's `scope`? Always defined — an unconfigured project compiles to
23
+ * an empty include/exclude, which admits everything, so callers pass this
24
+ * through unconditionally rather than branching.
25
+ *
26
+ * A PREDICATE rather than the `CompiledScope` it closes over, because nothing
27
+ * consumes a compiled scope as a compiled scope: every consumer immediately
28
+ * wrapped it in exactly this lambda, and `migrateScopePatterns` exists
29
+ * precisely because the compiled form cannot be shown to a human.
30
+ * `compileScope`/`matchesScope` stay exported for the conformance corpus.
31
+ */
32
+ readonly inScope: (fqn: string) => boolean;
33
+ /** Output filter for migrate/verify --db (`migrate.scope`). Undefined => the
34
+ * command governs everything loaded, and that undefined is load-bearing: it
35
+ * is what leaves the expected schema untouched (migrate-ts `scope.ts`). */
36
+ readonly inMigrateScope: ((fqn: string) => boolean) | undefined;
37
+ /** The patterns `inMigrateScope` was compiled FROM, for diagnostics only —
38
+ * `compileScope` produces RegExps, and a regex source is not something to
39
+ * show an author who wrote `acme::platform::**`. Carried so the "your scope
40
+ * matched nothing" refusal can name the patterns that missed. Always in
41
+ * lockstep with `inMigrateScope`: both undefined, or both present. */
42
+ readonly migrateScopePatterns: readonly string[] | undefined;
43
+ }
44
+ /**
45
+ * THE single authority on where metadata lives. Every read path routes
46
+ * through this — `metaobjects/` is the DEFAULT value of `sources`, never an
47
+ * assumption baked into a call site.
48
+ *
49
+ * Resolution order: an explicit `opts.explicitDir` wins outright; otherwise
50
+ * `discoverCollectionRoot` walks up from `startDir` for the nearest directory
51
+ * carrying `.metaobjects/config.json` — the ONLY project marker (`discovery.ts`
52
+ * says why a directory that merely holds metadata is not one) — falling back to
53
+ * `startDir` itself when none is found. When the resolved directory carries a
54
+ * config, its declared `sources`/`scope`/`migrate.scope` govern. Only a
55
+ * genuinely ABSENT `config.json` falls through to `DEFAULT_SOURCES` — the same
56
+ * directory the pre-source-resolution toolchain always read; a config.json
57
+ * that EXISTS but fails to load (malformed JSON, schema violation) is the
58
+ * author's error and propagates rather than silently degrading — a source
59
+ * that fails to resolve must never look like one that was never declared.
60
+ * Throws `ERR_COLLECTION_NOT_FOUND` only when BOTH have failed: no
61
+ * `sources` were declared AND the default source directory does not exist
62
+ * either.
63
+ *
64
+ * A declared source that fails to resolve is a different, louder failure —
65
+ * `resolveSources` throws `ERR_SOURCE_UNRESOLVED` for that case; only the
66
+ * DEFAULT is allowed to be silently absent.
67
+ */
68
+ export declare function resolveCollection(startDir: string, opts?: {
69
+ explicitDir?: string;
70
+ }): Promise<Collection>;
71
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB;2EACuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;4BAGwB;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C;;;;;gFAK4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C;;gFAE4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAChE;;;;2EAIuE;IACvE,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9D;AAaD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9B,OAAO,CAAC,UAAU,CAAC,CA0ErB"}