Skip to content

Commit f5e2bf5

Browse files
dmealingclaude
andcommitted
docs(agents): move porting and codegen detail into path-scoped rules
AGENTS.md is always-loaded context for every session in this repo, and two of its sections are reference material that only matters when you are touching specific paths: - Cross-language porting (13.8 KB) -> .claude/rules/cross-language-porting.md, auto-loads on the per-language server trees, client/web, spec/, the conformance fixtures and docs/ports/ - Codegen architecture (9.7 KB) -> .claude/rules/codegen-architecture.md, auto-loads on packages/, templates/, any codegen tree and the codegen conformance fixtures Each leaves a short pointer inline naming the load trigger, so the contract that matters most stays visible: a port is correct because the conformance corpus passes, not because it compiles. Status stays inline and untouched. It is not reference material — it is live 1.0 gating state, including the one gate still open, plus standing doctrine an agent must not rediscover. 74,930 -> 52,484 bytes, a 30% reduction. Verified lossless: every line of the original is reachable either inline or in a rules file, with no backticked identifier, link or code fence dropped, and all 20 sections preserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mDsFRJ7yg22YLEbnPS96D
1 parent 1c9d451 commit f5e2bf5

3 files changed

Lines changed: 279 additions & 236 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
paths:
3+
- "packages/**"
4+
- "templates/**"
5+
- "**/codegen/**"
6+
- "server/java/codegen-spring/**"
7+
- "fixtures/codegen-conformance/**"
8+
- "fixtures/template-codegen-conformance/**"
9+
---
10+
11+
## Codegen architecture (Vite-style plugins)
12+
13+
`@metaobjectsdev/codegen-ts` follows a Vite-style plugin model.
14+
15+
**Core interface** — every emitter implements `Generator`:
16+
17+
```ts
18+
import type { Generator, GenContext, EmittedFile } from "@metaobjectsdev/codegen-ts";
19+
20+
interface Generator {
21+
name: string; // kebab-case; surfaces in diagnostics
22+
filter?: (entity: MetaData) => boolean;
23+
generate(ctx: GenContext): EmittedFile[] | Promise<EmittedFile[]>;
24+
}
25+
```
26+
27+
Helpers `perEntity()` and `oncePerRun()` cover the common "file per entity" / "one-shot" cases.
28+
29+
**Built-in factories**: `entityFile`, `queriesFile`, `routesFile`, `formFile`, `barrel`. Per ADR-0034 (scaffold-and-own) and its Amendment 2 (opt-in codegen), `meta init` scaffolds `codegen/generators/` EMPTY with `generators: []`; `meta gen --list --probe` is the catalog, and `meta eject <name>...` copies each chosen reference template into the consumer repo at `codegen/generators/*.ts` and prints the import and entry to wire. The owned copy is the ONLY import path for `entityFile`/`queriesFile`/`routesFile`/`barrel`: the deprecated `@metaobjectsdev/codegen-ts/generators` re-export of them was **removed at the 1.0 cut**.
30+
31+
**User wiring** (`metaobjects.config.ts`):
32+
33+
```ts
34+
import { defineConfig } from "@metaobjectsdev/cli";
35+
// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none).
36+
import { entityFile } from "./codegen/generators/entity";
37+
import { queriesFile } from "./codegen/generators/queries";
38+
import { routesFile } from "./codegen/generators/routes";
39+
import { barrel } from "./codegen/generators/barrel";
40+
41+
export default defineConfig({
42+
outDir: "packages/database/src/generated",
43+
dialect: "sqlite",
44+
apiPrefix: "/api",
45+
generators: [entityFile(), queriesFile(), routesFile(), barrel()],
46+
});
47+
```
48+
49+
**Per-target output directories.** Each generator can write to its own
50+
directory/package via a named `targets` registry + per-generator `target`, so
51+
generated code lands with its runtime concern (model → database package, routes →
52+
API app, hooks/forms/grids → web app). A target is `{ outDir, importBase?,
53+
outputLayout?, dbImport? }`; the top-level `outDir` is the implicit `default`
54+
(entity-module) target. Cross-target references to the entity module are emitted as
55+
extension-less `importBase` package paths (`@acme/database/generated/acme/commerce/Program`)
56+
while same-target references stay relative; the entity-module target must set
57+
`importBase` when any generator routes elsewhere. With no `targets`, output is
58+
byte-identical to a single-`outDir` project. Full config reference: `@metaobjectsdev/cli`
59+
README, "Multiple output targets".
60+
61+
**Two config files, by design:**
62+
- `metaobjects.config.ts` (TypeScript) — generator wiring, type-checked.
63+
- `.metaobjects/config.json` (JSON) — static project state. Parseable by non-TS tooling (CI scripts, etc.).
64+
65+
The runner `runGen()` (1) loads metadata, (2) resolves targets + derives the
66+
entity-module target, (3) precomputes shared render state once, (4) runs each
67+
generator with a per-target `RenderContext`, (5) errors on *conflicting* duplicate
68+
full output paths — two emissions of the same path whose CONTENT differs, where the
69+
result would depend on generator order — while byte-identical duplicates collapse to
70+
one file (#266: a shared artifact rendered from the whole loaded root, like the shared
71+
`enums.ts`, is emitted by every `entityFile()` instance); also errors on an unknown
72+
target, missing `importBase` for cross-target imports, or any generator throw, (6) writes each file under its target's `outDir`, deciding from
73+
`.metaobjects/.gen-state/` — a three-way merge against the snapshot body when one is
74+
present, else the committed `.hashes.json` (hash matches what we recorded writing ⇒
75+
overwrite; edited or unrecorded ⇒ refused). The `@generated` header is **informational**:
76+
every use of it is an emitter stamping the marker into output, and the overwrite decision
77+
never reads it. Because the snapshot bodies are gitignored while `.hashes.json` is
78+
committed, a fresh clone or CI runner takes the second branch — so a hand-edited generated
79+
file is REFUSED there rather than merged.
80+
81+
### Filter syntax + sort (Project D)
82+
83+
Generated CRUD endpoints support a typed, metadata-driven filter + sort layer:
84+
85+
**URL grammar** (bracketed qs): `?filter[field][op]=value&sort=field:asc|desc&limit=N&offset=N`. Bare value is sugar for `eq`.
86+
87+
**Nine operators**, gated by field subtype:
88+
- `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `isNull`
89+
- Strings get `eq/ne/in/like/isNull`; numbers + dates get `eq/ne/gt/gte/lt/lte/in/isNull`; booleans get `eq/isNull`.
90+
91+
**Authoring:** mark fields with `@filterable: true`. `@sortable` inherits from `@filterable` by default.
92+
93+
**Generated artifacts per entity**:
94+
- `<Entity>FilterAllowlist` — server-side allowlist
95+
- `<Entity>SortAllowlist` — server-side sort allowlist
96+
- `<Entity>Filter` — client TS filter type
97+
98+
**Client usage:**
99+
```tsx
100+
import { useSubscribers } from "./generated/Subscriber.hooks";
101+
102+
const { data } = useSubscribers({
103+
email: { like: "amy@%" },
104+
subscribed: true,
105+
sort: "createdAt:desc",
106+
limit: 25,
107+
});
108+
```
109+
110+
**Server validation:** every request validated against the allowlist. Unknown field / disallowed op / invalid value → 400 with structured error code.
111+
112+
**Leading wildcards are rejected by default (TS generated routes):** the generated allowlist ships `leadingWildcard: false` on every field, so a `like` pattern starting with `%` (e.g. `"%@example.com"`) → 400 `filter.leading_wildcard_disallowed` — an unanchored LIKE defeats index usage, so it is fail-closed. Opt in per field by hand-editing that field's entry in the generated `<Entity>FilterAllowlist` to `leadingWildcard: true` (hand edits inside generated files are preserved by the three-way merge). This gate is a TS-only extension — the other ports do not enforce it (see `docs/features/api-contract.md`, "TS-only filter extensions").
113+
114+
**Architecture:** `parseFilterParams` (in `@metaobjectsdev/runtime-ts/drizzle-fastify`) translates parsed qs into a Drizzle expression tree. `buildFilterQs` (in `@metaobjectsdev/runtime-web` and `@metaobjectsdev/tanstack`) serializes a typed filter object back to a bracketed qs URL.
115+
116+
### Source-aware entities + projections (Project E)
117+
118+
`source` is a top-level metadata type describing where an object's data lives. Subtypes: `dbTable` (writable, default) and `dbView` (read-only).
119+
120+
**Authoring a projection:**
121+
122+
```jsonc
123+
{ "object.entity": {
124+
"name": "ProgramSummary",
125+
"extends": "Program",
126+
"children": [
127+
{ "source.dbView": { "@name": "v_program_summary" }},
128+
{ "field.int": { "name": "weekCount", "children": [
129+
{ "origin.aggregate": {
130+
"@agg": "count", "@of": "Week.id", "@via": "Program.weeks" }}
131+
]}},
132+
{ "identity.primary": { "@fields": ["id"] }}
133+
]
134+
}}
135+
```
136+
137+
**`origin`** subtypes: `passthrough` (cross-entity field reference) and `aggregate` (count/sum/avg/min/max). Origins drive view DDL.
138+
139+
**Source-aware codegen dispatch:**
140+
- Projection (dbView only) → read-only Zod, read-only routes, read-only hooks.
141+
- Write-through (dbTable + dbView) → mutations target table, queries target view.
142+
- Vanilla entity → standard behavior.
143+
144+
**`columnNamingStrategy`** in `metaobjects.config.ts`: `snake_case` (default) | `literal` | `kebab-case`.
145+
146+
### Currency (Project F)
147+
148+
`field.currency` declares "this column stores money as integer minor units."
149+
150+
```jsonc
151+
{ "field.currency": {
152+
"name": "priceCents",
153+
"@currency": "USD",
154+
"children": [
155+
{ "view.currency": { "@locale": "en-US" }}
156+
]
157+
}}
158+
```
159+
160+
**Storage:** integer minor units (cents for USD, yen for JPY). Wire format is unchanged from `long`. Server never formats currency; all formatting is client-side via `Intl.NumberFormat`.
161+
162+
**Runtime imports** (browser-safe sub-paths):
163+
164+
```tsx
165+
import { formatCurrency, parseCurrency } from "@metaobjectsdev/runtime-web";
166+
import { CurrencyInput } from "@metaobjectsdev/react";
167+
```
168+
169+
**Cross-language ports** must preserve the wire contract: integer minor-unit storage, `@currency` (ISO 4217), `@locale` (BCP 47) attrs.
170+
171+
### TanStack codegen + metadata-driven grids (Project B)
172+
173+
`@metaobjectsdev/codegen-ts-tanstack` ships two generators:
174+
175+
- `tanstackQuery()` — emits `<Entity>.hooks.ts` per entity (5 hooks: `useEntity`, `useEntities`, `useCreate/Update/Delete<Entity>`).
176+
- `tanstackGrid()` — emits `<Entity>.columns.tsx` per entity with a `layout.dataGrid` child.
177+
178+
**Grid metadata:**
179+
180+
```jsonc
181+
{ "layout.dataGrid": {
182+
"name": "default",
183+
"@columns": ["email", "firstName", "subscribed", "createdAt"],
184+
"@defaultSortField": "createdAt",
185+
"@defaultSortOrder": "desc",
186+
"@pageSize": 25
187+
}}
188+
```
189+
190+
**Runtime surface (`@metaobjectsdev/runtime-web` and `@metaobjectsdev/tanstack`)**:
191+
- `<EntityFetcherProvider value={fetcher}>` — supplies the fetcher function.
192+
- `<CellRendererProvider value={{...}}>` — renderer overrides keyed by view subtype.
193+
- `<EntityGrid columns={...} grid={...} data={...} />` — opinionated TanStack Table component.
194+
195+
**Narrowing what emits**: wire only the generators whose output you import, and narrow one with its `filter` option — `filter` is ANDed with the generator's built-in gates, so it can only narrow. There is no `@emit*` metadata attribute for this (`@emitTanstack` / `@emitRoutes` / `@emitForm` / `@emitGrid` / `@emitAngular` were never registered vocabulary — they passed `meta gen` and failed `meta verify`; `meta upgrade --apply` removes them). The one opt-IN, a TPH subtype's own per-subtype grid, widens rather than narrows, so it is a generator option: `tanstackGrid({ tphSubtypeGrids })`, with the same predicate passed to `tanstackGridHook()`.

0 commit comments

Comments
 (0)