@metaobjectsdev/sdk 0.24.5 → 0.25.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.
- package/README.md +1 -7
- package/agent-context/servers/csharp.meta.json +3 -1
- package/agent-context/servers/java.meta.json +3 -1
- package/agent-context/servers/kotlin.meta.json +3 -1
- package/agent-context/servers/python.meta.json +3 -1
- package/agent-context/servers/typescript.meta.json +3 -1
- package/agent-context/skills/metaobjects-audit/SKILL.md +15 -8
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +6 -1
- package/agent-context/skills/metaobjects-audit/references/csharp.md +10 -1
- package/agent-context/skills/metaobjects-audit/references/java.md +10 -0
- package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -0
- package/agent-context/skills/metaobjects-audit/references/python.md +12 -2
- package/agent-context/skills/metaobjects-audit/references/typescript.md +7 -2
- package/agent-context/skills/metaobjects-authoring/SKILL.md +35 -6
- package/agent-context/skills/metaobjects-codegen/SKILL.md +210 -15
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/java.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/kotlin.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/python.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +85 -6
- package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +28 -6
- package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
- package/agent-context/templates/always-on.md.mustache +45 -4
- package/dist/agent-context/assemble.d.ts.map +1 -1
- package/dist/agent-context/assemble.js +52 -4
- package/dist/agent-context/assemble.js.map +1 -1
- package/dist/agent-context/scaffold.d.ts +16 -1
- package/dist/agent-context/scaffold.d.ts.map +1 -1
- package/dist/agent-context/scaffold.js +21 -1
- package/dist/agent-context/scaffold.js.map +1 -1
- package/dist/forge-types.d.ts.map +1 -1
- package/dist/forge-types.js +19 -1
- package/dist/forge-types.js.map +1 -1
- package/package.json +4 -9
- package/src/agent-context/assemble.ts +86 -6
- package/src/agent-context/scaffold.ts +33 -2
- package/src/forge-types.ts +21 -0
- package/dist/agent-docs/body.d.ts +0 -6
- package/dist/agent-docs/body.d.ts.map +0 -1
- package/dist/agent-docs/body.js +0 -593
- package/dist/agent-docs/body.js.map +0 -1
- package/dist/agent-docs/content-hash.d.ts +0 -8
- package/dist/agent-docs/content-hash.d.ts.map +0 -1
- package/dist/agent-docs/content-hash.js +0 -23
- package/dist/agent-docs/content-hash.js.map +0 -1
- package/dist/agent-docs/index.d.ts +0 -3
- package/dist/agent-docs/index.d.ts.map +0 -1
- package/dist/agent-docs/index.js +0 -4
- package/dist/agent-docs/index.js.map +0 -1
- package/src/agent-docs/body.ts +0 -592
- package/src/agent-docs/content-hash.ts +0 -25
- package/src/agent-docs/index.ts +0 -8
|
@@ -120,6 +120,7 @@ All live in `metaobjects-codegen-kotlin` under
|
|
|
120
120
|
| `KotlinRenderHelperGenerator` | the typed render helper for a `template.prompt` payload |
|
|
121
121
|
| `KotlinValidatorGenerator` | `MetadataStartupValidator.kt` + `ExposedTableValidator.kt` (once per project) |
|
|
122
122
|
| `KotlinSpringConfigGenerator` | `MetadataExposedConfig.kt` — `@Configuration` wiring `Database.connect()` + the startup validator (once per project) |
|
|
123
|
+
| `KotlinNamesGenerator` | `<Entity>Names.kt` — an `object` of `const val` names that MIRRORS THE METADATA TREE. Every node carries its own `TYPE`/`SUB_TYPE`/`NAME`, so `AuthorNames.NAME` is the OBJECT's name (`"Author"`), **not** a table name, and a physical name sits under the member that says what it is: `SOURCE_<ROLE>_TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` / `_PROC` / `_FUNCTION`, from the metamodel's own `@kind`-to-alias map, with `<ROLE>` being `PRIMARY` or `REPLICA` (a write-through entity has a member for each). Plus a `<FIELD>_FIELD`/`<FIELD>_COLUMN` pair per field and a complete `COLUMNS_BY_FIELD`; an `identity.secondary` or `index.lookup` also carries `IDENTITY_<NAME>_INDEX` / `INDEX_<NAME>_INDEX`, while `identity.primary` deliberately carries no index member (migrate names a PK by a dialect-conditional formula this artifact must not restate). **There is no `READ_ONLY`** — it was a derivation over `@kind`, never metadata, so ask `SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, no source). Kotlin has no static inheritance — an `object` cannot extend another — so an artifact whose object extends another **re-exports** the parent's constants by reference (`const val SOURCE_PRIMARY_TABLE: String = AuthNames.SOURCE_PRIMARY_TABLE`) instead of restating the literal. Wire it alongside `KotlinExposedTableGenerator`: the Maven plugin then turns the table binding's constant substitution ON automatically (`AuthorTable : Table(AuthorNames.SOURCE_PRIMARY_TABLE)`); a run without it keeps the literals, so the output still compiles. |
|
|
123
124
|
| `KotlinStoredProcGenerator` | stored-procedure call wrappers for `source.rdb` `@kind="storedProc"` |
|
|
124
125
|
| `KotlinFilterAllowlistGenerator` | per-entity filter allowlist |
|
|
125
126
|
|
|
@@ -61,6 +61,7 @@ a renamed physical column).
|
|
|
61
61
|
| `routes` | a **FastAPI `APIRouter`** per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). The router declares a repository **`Protocol`** you implement and inject. A TPH `@discriminator` base emits ONE polymorphic router: `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. Its repository `Protocol` is subtype-keyed (`subtype=None` for the polymorphic base) so your implementation applies the single-table discriminator scope. |
|
|
62
62
|
| `filter-allowlist` | per-entity filter allowlist (FR-009 — the server-side field+operator allowlist the routes validate against). |
|
|
63
63
|
| `payload` / `output-parser` / `output-prompt` / `extractor` / `render-helper` / `trace-helper` | the prompt-pillar artifacts for a **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: these tiers are INBOUND; `template.output` is outbound only and emits no parser). The payload VO, the strict parser (`<template>_response_parser`), the **output-format prompt fragment** (`<template>_response_format`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), the tolerant `extract`, the typed render helper, and the LLM-trace helper. See the **prompts** reference. |
|
|
64
|
+
| `names` | `<entity_snake>_names.py` — module-level `Final` constants mirroring the object's metadata tree. Every node carries its own `_TYPE`/`_SUB_TYPE`/`_NAME`; `<ENTITY>_NAME` is the OBJECT's name, and a physical name sits under the member naming what it is: `<ENTITY>_SOURCE_<ROLE>_{TABLE,VIEW,MATERIALIZED_VIEW,PROC,FUNCTION}` (`<ROLE>` is `PRIMARY` or `REPLICA`, so a write-through entity's read view has a slot), plus `<ENTITY>_SOURCE_<ROLE>_{KIND,SCHEMA}`, a `<ENTITY>_<FIELD>_FIELD`/`_COLUMN` pair each, `<ENTITY>_IDENTITY_<NAME>_*` / `<ENTITY>_INDEX_<NAME>_*` carrying `_INDEX` (the database index name) for `identity.secondary` and `index.lookup`, and a complete `<ENTITY>_COLUMNS_BY_FIELD`. No `_READ_ONLY` — it was derived from `@kind`, never declared; ask `_SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns and keys only, no `_SOURCE_*` — it has no table and must never acquire one). Python has no static inheritance, so a module whose object extends another **imports and re-exports** the parent's constants (`AUTHOR_CREATED_AT_COLUMN: Final[str] = BASEENTITY_CREATED_AT_COLUMN`) instead of restating the literal; a TPH subtype re-exports `_SOURCE_PRIMARY_*` too, since it shares its base's table. **This port generates no SQL**, so nothing generated consumes these — they exist for the repository `Protocol` implementation you write. |
|
|
64
65
|
| `template` | the generic Mustache `template` primitive. |
|
|
65
66
|
|
|
66
67
|
**Projections + entity read-views.** An `object.projection` (read-only `source.rdb`
|
|
@@ -8,6 +8,7 @@ packages. Codegen runs through the Node `meta` CLI (`@metaobjectsdev/cli`, binar
|
|
|
8
8
|
- Install
|
|
9
9
|
- `metaobjects.config.ts`
|
|
10
10
|
- The generators
|
|
11
|
+
- Declarative template-codegen (Mustache)
|
|
11
12
|
- Run
|
|
12
13
|
- Multiple output targets
|
|
13
14
|
- Field subtype → column mapping
|
|
@@ -82,7 +83,10 @@ PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself.
|
|
|
82
83
|
|
|
83
84
|
## The generators
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
Server-side, framework-neutral. The first four are **scaffolded into your repo** by
|
|
87
|
+
`meta init` and imported from `./codegen/generators/*` (ADR-0034); the rest come from the
|
|
88
|
+
package main entry, `@metaobjectsdev/codegen-ts`. Do **not** import any of them from
|
|
89
|
+
`@metaobjectsdev/codegen-ts/generators` — that subpath is deprecated and removed at 1.0.
|
|
86
90
|
|
|
87
91
|
| Generator | Emits per entity |
|
|
88
92
|
|---|---|
|
|
@@ -93,6 +97,7 @@ From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
|
|
|
93
97
|
| `promptRender()` | `render<Name>()` per `template.prompt` |
|
|
94
98
|
| `outputParser()` | `<Name>.response.ts` (`parse*` / `safeParse*`) per **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: this tier is INBOUND; `template.output` is outbound only and emits nothing here). Siblings: `outputPrompt()` → `<Name>.responseFormat.ts` (the FR-010 output-format fragment, presentation via `@promptStyle`), `extractor()` → `<Name>.extractor.ts` (the tolerant `extract` mapper). |
|
|
95
99
|
| `callableFile()` | `<Entity>.callable.ts` — an FR-015 `call<Entity>` wrapper for a `source.rdb` `@kind: storedProc`/`tableFunction` (args from the `@parameterRef` value object, in declaration order) |
|
|
100
|
+
| `namesFile()` | `<Entity>.names.ts` — `export const <Entity>Names`, mirroring the object's metadata tree. Every node carries its own `type`/`subType`/`name`; `name` is the OBJECT's name, and a physical name sits under the key naming what it is: `sources.<role>.{table,view,materializedView,proc,function}` (`<role>` is `primary` or `replica`, so a write-through entity's read view has a slot), plus `sources.<role>.{kind,schema}`, `fields.<field>.{name,column}`, and `identities.<name>` / `indexes.<name>` carrying `.index` (the database index name) for `identity.secondary` and `index.lookup`. No `readOnly` — it was derived from `kind`, never declared. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, `sources: {}` — it has no table and must never acquire one). An artifact whose object extends another spreads the parent's collections rather than restating them, and a TPH subtype spreads `...AuthNames.sources` too, since it shares its base's table. With this generator in the run, **no generated TypeScript spells a physical name at all** — table, view, proc, column, schema and index name all travel as references; drop it from the suite and they fall back to literals. |
|
|
96
101
|
|
|
97
102
|
**Projections (read-only views).** For an `object.projection` (a read-only `source.rdb`
|
|
98
103
|
`@kind: view` child), `entityFile()` emits a `pgView(...)` + read-only Zod + a read-only
|
|
@@ -180,8 +185,73 @@ From `@metaobjectsdev/codegen-ts-tanstack`: `tanstackQuery()` → `<Entity>.hook
|
|
|
180
185
|
`tanstackGridHook()` → `<Entity>.grid.tsx`.
|
|
181
186
|
|
|
182
187
|
`entityFile({ allowlists: false })` drops the `runtime-ts/drizzle-fastify` import
|
|
183
|
-
for edge/worker consumers that don't mount server routes.
|
|
184
|
-
|
|
188
|
+
for edge/worker consumers that don't mount server routes.
|
|
189
|
+
|
|
190
|
+
**Wire a generator only for output you consume, and narrow it with its `filter`.**
|
|
191
|
+
Every generator factory takes `{ filter?: (entity) => boolean }`, ANDed with the
|
|
192
|
+
generator's built-in gates — so it can only NARROW what emits, never widen it:
|
|
193
|
+
`tanstackQuery({ filter: (e) => e.name !== "InternalAudit" })` emits no hooks for
|
|
194
|
+
that entity. There is no `@emit*` metadata attribute to do this — `@emitTanstack`,
|
|
195
|
+
`@emitRoutes`, `@emitForm`, `@emitGrid` and `@emitAngular` were never registered
|
|
196
|
+
vocabulary, so they passed `meta gen` and failed `meta verify`. If a project carries
|
|
197
|
+
one, `meta upgrade --apply` removes it.
|
|
198
|
+
|
|
199
|
+
The one thing a `filter` can't express is opting a TPH subtype IN to its own
|
|
200
|
+
per-subtype grid (that WIDENS): `tanstackGrid({ tphSubtypeGrids: (e) => … })`,
|
|
201
|
+
default `() => false`. Pass the same predicate to `tanstackGridHook()` or you get
|
|
202
|
+
a `<Sub>.grid.ts` whose `<Sub>.columns.tsx` is never emitted.
|
|
203
|
+
|
|
204
|
+
## Declarative template-codegen (Mustache)
|
|
205
|
+
|
|
206
|
+
Everything above is the **programmatic** path. A generator can also be **declarative** —
|
|
207
|
+
a Mustache template plus a scope, no generator code — and on TypeScript you have both.
|
|
208
|
+
Pick a template when the output SHAPE is what you are iterating on, or when you want the
|
|
209
|
+
same output across languages; pick programmatic when the logic is gnarly or the run is
|
|
210
|
+
hot.
|
|
211
|
+
|
|
212
|
+
**There is no `--template-spec` flag on `meta gen`.** Do not look for one and do not
|
|
213
|
+
report its absence as a gap. `metaobjects.config.ts` takes generator VALUES, so a
|
|
214
|
+
template generator is declared there like any other — which is also what keeps it
|
|
215
|
+
visible to `meta verify --codegen`, a gate that re-runs the config's generator list.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { templateGenerator } from "@metaobjectsdev/codegen-ts";
|
|
219
|
+
|
|
220
|
+
export default defineConfig({
|
|
221
|
+
generators: [
|
|
222
|
+
entityFile(),
|
|
223
|
+
templateGenerator({
|
|
224
|
+
name: "entity-service",
|
|
225
|
+
template: "service/entity-service", // → templates/service/entity-service.mustache
|
|
226
|
+
scope: "perEntity", // "perEntity" | "perPackage" | "perModel"
|
|
227
|
+
outputPattern: "{package}/{Name}Service.ts",
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `template` resolves under the project's `templates/` dir first, then framework defaults.
|
|
234
|
+
- `outputPattern` placeholders: `{name}`, `{Name}`, `{package}` (its `::` segments become
|
|
235
|
+
nested directories). An unknown placeholder throws.
|
|
236
|
+
- `scope` and `walk` are mutually exclusive — supply exactly one. `walk` is the escape
|
|
237
|
+
hatch for a walk none of the three scopes expresses.
|
|
238
|
+
- Abstract objects are excluded from every scope.
|
|
239
|
+
|
|
240
|
+
**Reusing a C#/Python spec.** Those ports declare the same generators as a JSON
|
|
241
|
+
template-spec because their registries are closed and the flag is their only seam. Parse
|
|
242
|
+
it and spread it:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { parseTemplateSpec, templateSpecToGenerators } from "@metaobjectsdev/codegen-ts";
|
|
246
|
+
|
|
247
|
+
const spec = parseTemplateSpec(JSON.parse(readFileSync("./template-spec.json", "utf8")));
|
|
248
|
+
// generators: [entityFile(), ...templateSpecToGenerators(spec)]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Portability runs ONE way: TS also accepts a `target` field that the CLI ports reject, so
|
|
252
|
+
a spec written there always runs here, but not the reverse. Keep `target` out of a shared
|
|
253
|
+
spec. The data dict a template renders against is the cross-port byte-gated contract —
|
|
254
|
+
`docs/features/codegen-data-shapes.md`.
|
|
185
255
|
|
|
186
256
|
## Run
|
|
187
257
|
|
|
@@ -191,9 +261,18 @@ npx meta gen --dry-run # preview without writing
|
|
|
191
261
|
npx meta gen Author Post # scope to named entities
|
|
192
262
|
```
|
|
193
263
|
|
|
194
|
-
Generated files carry an `@generated by @metaobjectsdev/codegen-ts` header
|
|
195
|
-
|
|
196
|
-
|
|
264
|
+
Generated files carry an `@generated by @metaobjectsdev/codegen-ts` header. It is
|
|
265
|
+
**informational** — the write decision never reads it. `.metaobjects/.gen-state/`
|
|
266
|
+
decides: the snapshot body if this machine has one (three-way merge), otherwise the
|
|
267
|
+
committed `.hashes.json` (byte-for-byte what it wrote ⇒ overwrite; anything else ⇒
|
|
268
|
+
refused, path named, exit 1). So the merge is machine-local: a file you edited and
|
|
269
|
+
pushed is REFUSED on a fresh clone or in CI, not merged. Recovery is in
|
|
270
|
+
`docs/features/own-your-codegen.md`.
|
|
271
|
+
|
|
272
|
+
Hand-customizations that metadata can't express go in a sibling module you create and
|
|
273
|
+
import yourself — `<Entity>.extra.ts` by convention. The name carries no tool behaviour:
|
|
274
|
+
the file is safe because codegen writes only the paths it records, and the generated
|
|
275
|
+
barrel (built from the model, not a directory listing) does **not** re-export it.
|
|
197
276
|
|
|
198
277
|
**Output format:** `meta gen` (and the CLI generally) is TTY-aware — human-readable
|
|
199
278
|
text on a terminal, TOON on a pipe or agent. Override with `--format toon|json|text`.
|
|
@@ -43,6 +43,28 @@ This keeps generated code free of global state, makes it testable, and lets one
|
|
|
43
43
|
process talk to multiple databases (multi-tenant, read-replica). Construct/own the
|
|
44
44
|
context in your app; thread it through every generated call.
|
|
45
45
|
|
|
46
|
+
## Physical names come from the generated names artifact, never a literal
|
|
47
|
+
|
|
48
|
+
The runtime tier is where the code codegen leaves to you gets written — a repository
|
|
49
|
+
implementation, a raw query, a migration script, a log line — and that is where a table
|
|
50
|
+
or column name gets respelled. Don't. A physical name is declared **once**, in metadata
|
|
51
|
+
(`@table` / `@view` / `@column` / `@schema`), and every port emits a per-object names
|
|
52
|
+
artifact carrying it: `ProgramNames.fields.createdAt.column` on TypeScript,
|
|
53
|
+
`ProgramNames.CreatedAtColumn` on C#, `ProgramNames.CREATED_AT_COLUMN` on the JVM,
|
|
54
|
+
`PROGRAM_CREATED_AT_COLUMN` on Python. Reference it. `@column` is free-form —
|
|
55
|
+
`callPurpose` may live in `purpose_code` — so deriving a column from the field name is a
|
|
56
|
+
guess that fails silently, and a literal is a second spelling of a fact the metadata
|
|
57
|
+
owns: rename the column in metadata and the constant follows, the literal does not, and
|
|
58
|
+
nothing tells you.
|
|
59
|
+
|
|
60
|
+
**Prefer a typed handle where one exists.** A Drizzle column object, an Exposed `Column`,
|
|
61
|
+
an EF Core property is type-checked against the schema; swapping it for a string constant
|
|
62
|
+
trades a compile error for a runtime one. The constants are for the places with no
|
|
63
|
+
handle: raw SQL, a string-keyed query builder, and a hand-written repository on a port
|
|
64
|
+
whose generated model carries no persistence binding at all (Java, Python). Your server
|
|
65
|
+
reference names the handle and the artifact for this stack; on the JVM the names
|
|
66
|
+
generator is opt-in, and the `metaobjects-codegen` skill says how to wire it.
|
|
67
|
+
|
|
46
68
|
## The REST contract
|
|
47
69
|
|
|
48
70
|
Generated (or hand-written) routes speak one cross-port HTTP contract so the same
|
|
@@ -88,3 +88,18 @@ handler calls the `FilterParser` / `EfCoreFilterDispatch` runtime helpers in
|
|
|
88
88
|
`MetaObjects.Codegen`, so your ASP.NET host references that assembly at runtime. The same
|
|
89
89
|
universal TS/Angular web client consumes those routes unchanged — the wire format matches
|
|
90
90
|
the Java, Kotlin, and Python backends byte-for-byte.
|
|
91
|
+
|
|
92
|
+
## Physical names below LINQ
|
|
93
|
+
|
|
94
|
+
Inside LINQ keep the property (`db.Authors.Where(a => a.Name == …)`): it is type-checked
|
|
95
|
+
against the model, and a string constant there trades a compile error for a runtime one.
|
|
96
|
+
Where LINQ does not reach — raw SQL, a migration script, a log line — take the physical
|
|
97
|
+
name from the generated `<Entity>Names.g.cs`. `names` is in the default suite, and the
|
|
98
|
+
generated entity and `AppDbContext` already read it, so it cannot disagree with the
|
|
99
|
+
mapping: `AuthorNames.SourcePrimaryTable` is the table (the member is named for the
|
|
100
|
+
source's `@kind`, so a view reads `SourceReplicaView` / `SourcePrimaryView` and a stored
|
|
101
|
+
procedure `SourcePrimaryProc`), `AuthorNames.<Field>Column` the column,
|
|
102
|
+
`AuthorNames.ColumnsByField` the whole map. `AuthorNames.Name` is the OBJECT's name —
|
|
103
|
+
`Author`, not `authors` — so it is the one member that compiles while meaning something
|
|
104
|
+
else than it did before 0.25.0. Never a literal — and never
|
|
105
|
+
`nameof(Author.Name)`, which is the CLR property, not the column.
|
|
@@ -110,3 +110,36 @@ writable entity, on the cross-port REST contract (five CRUD endpoints, `?sort`,
|
|
|
110
110
|
`<Entity>Repository.java` is a stubbed interface you implement against OMDB (or any
|
|
111
111
|
persistence layer) — wire the controller to call it. The same universal TS/Angular
|
|
112
112
|
web client consumes those controllers unchanged.
|
|
113
|
+
|
|
114
|
+
## Physical names in your repository implementation
|
|
115
|
+
|
|
116
|
+
OMDB resolves columns itself — `setString("name", …)` and `getObjects` key by field — and
|
|
117
|
+
nothing `codegen-spring` emits carries a physical name (the DTO is a record of logical
|
|
118
|
+
names; the repository is a bare interface). The physical names appear in exactly one
|
|
119
|
+
place: the persistence code **you** write behind `<Entity>Repository` (JDBC, jOOQ, a
|
|
120
|
+
Spring Data query). Take them from the generated `<Entity>Names` — never a literal:
|
|
121
|
+
|
|
122
|
+
```java
|
|
123
|
+
jdbc.query("SELECT " + AuthorNames.NAME_COLUMN + " FROM " + AuthorNames.SOURCE_PRIMARY_TABLE
|
|
124
|
+
+ " WHERE " + AuthorNames.ID_COLUMN + " = ?", mapper, id);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The class mirrors the metadata tree. Every node carries its own `TYPE`/`SUB_TYPE`/`NAME`,
|
|
128
|
+
so `AuthorNames.NAME` is the OBJECT's name (`"Author"`) and the physical name sits under
|
|
129
|
+
the member that says what it IS: `SOURCE_<ROLE>_TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` /
|
|
130
|
+
`_PROC` / `_FUNCTION`. `<ROLE>` is `PRIMARY` or `REPLICA`, so a write-through entity's read
|
|
131
|
+
view has a member of its own rather than sharing the write table's. Alongside them sit
|
|
132
|
+
`SOURCE_<ROLE>_KIND` and, when declared, `SOURCE_<ROLE>_SCHEMA`; there is no `READ_ONLY` —
|
|
133
|
+
it was derived from `@kind`, never declared, so ask `SOURCE_<ROLE>_KIND`. An
|
|
134
|
+
`identity.secondary` or `index.lookup` carries `IDENTITY_<NAME>_INDEX` /
|
|
135
|
+
`INDEX_<NAME>_INDEX`, the database index name; `identity.primary` deliberately has none,
|
|
136
|
+
because migrate names a primary key by a dialect-conditional formula this artifact must not
|
|
137
|
+
restate.
|
|
138
|
+
|
|
139
|
+
`AuthorNames.COLUMNS_BY_FIELD` carries the whole map when you need to build a projection
|
|
140
|
+
list. It is opt-in on the JVM — add `SpringNamesGenerator` to the pom's `<generators>`
|
|
141
|
+
(see the codegen reference). Its `columnNaming` defaults to `literal`, the same
|
|
142
|
+
resolution OMDB uses at runtime; pass the same value to both, or the constant names a
|
|
143
|
+
column no row lands in. There is no typed handle to prefer on this port — `<Entity>Names`
|
|
144
|
+
is the only compile-checked route to a physical name, which is also why nothing else here
|
|
145
|
+
catches a wrong pairing for free.
|
|
@@ -35,6 +35,12 @@ object AuthorTable : Table("authors") {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
With `KotlinNamesGenerator` in the same `<generators>` run the binding reads
|
|
39
|
+
`Table(AuthorNames.SOURCE_PRIMARY_TABLE)` and `varchar(AuthorNames.NAME_COLUMN, 200)`
|
|
40
|
+
instead of the literals above — the Maven plugin builds the whole generator list before executing any of
|
|
41
|
+
it, so adding the names generator turns the substitution on (an explicit `<useNames>`
|
|
42
|
+
still wins).
|
|
43
|
+
|
|
38
44
|
## Query + persist with Exposed
|
|
39
45
|
|
|
40
46
|
Obtain a `Database` (the generated `MetadataExposedConfig` `@Configuration` calls
|
|
@@ -73,6 +79,23 @@ Filtered reads use Exposed's `selectAll()` plus a `where { ... }` op tree (e.g.
|
|
|
73
79
|
`integration-tests-kotlin` query-conformance runner does against Testcontainers
|
|
74
80
|
Postgres.
|
|
75
81
|
|
|
82
|
+
## Physical names outside the DSL
|
|
83
|
+
|
|
84
|
+
Inside Exposed, bind to the `Column` object — `AuthorTable.name`, type-checked column by
|
|
85
|
+
column — never `AuthorNames.NAME_COLUMN`. The constant is for what the DSL does not
|
|
86
|
+
reach: `exec("…")` raw SQL, a Flyway script, a log line, an external system's column
|
|
87
|
+
mapping. There it is `AuthorNames.SOURCE_PRIMARY_TABLE` / `AuthorNames.NAME_COLUMN`
|
|
88
|
+
(opt-in: `KotlinNamesGenerator` in the pom — see the codegen reference), never a literal.
|
|
89
|
+
Give both generators the same `columnNaming`, or the table and the constants disagree about
|
|
90
|
+
a column.
|
|
91
|
+
|
|
92
|
+
The object mirrors the metadata tree: `AuthorNames.NAME` is the OBJECT's name
|
|
93
|
+
(`"Author"`), and a physical name sits under the member that says what it IS —
|
|
94
|
+
`SOURCE_<ROLE>_TABLE` / `_VIEW` / `_PROC` and so on, `<ROLE>` being `PRIMARY` or `REPLICA`,
|
|
95
|
+
so a write-through entity's replica view has a member of its own. Index names are
|
|
96
|
+
`IDENTITY_<NAME>_INDEX` / `INDEX_<NAME>_INDEX`, which is what the generated
|
|
97
|
+
`init { uniqueIndex(…) }` block references.
|
|
98
|
+
|
|
76
99
|
## Return-type contract
|
|
77
100
|
|
|
78
101
|
An Exposed read yields **native in-process Kotlin/JVM types** at the column, never
|
|
@@ -92,3 +92,28 @@ Protocol, not `int`. The PATCH body is typed `dict[str, Any]` **deliberately**:
|
|
|
92
92
|
`<Entity>Create` / `<Entity>Patch` Pydantic models already validate constraints over HTTP
|
|
93
93
|
(FR-036), and the `dict` seam preserves the FR-035 present-key PATCH tristate (absent ≠
|
|
94
94
|
explicit-null) — retyping the PATCH parameter to the model would collapse that tristate.
|
|
95
|
+
|
|
96
|
+
## Physical names in your repository implementation
|
|
97
|
+
|
|
98
|
+
`ObjectManager` calls key by field name (`om.find_many("Author", {"name": …})`) and
|
|
99
|
+
resolve the column themselves, so that path never needs a physical name. The moment the
|
|
100
|
+
repository `Protocol` is backed by your own SQLAlchemy Core / asyncpg / psycopg code, it
|
|
101
|
+
does — and nothing Python generates carries one: the Pydantic models, create/patch
|
|
102
|
+
shapes, router and allowlist all key by field. Take it from the generated
|
|
103
|
+
`<entity_snake>_names.py` (`names` is in the default suite):
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from generated.author_names import (
|
|
107
|
+
AUTHOR_SOURCE_PRIMARY_TABLE, AUTHOR_CREATED_AT_COLUMN, AUTHOR_ID_COLUMN,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
row = await conn.fetchrow(
|
|
111
|
+
f"SELECT {AUTHOR_CREATED_AT_COLUMN} FROM {AUTHOR_SOURCE_PRIMARY_TABLE} "
|
|
112
|
+
f"WHERE {AUTHOR_ID_COLUMN} = $1",
|
|
113
|
+
author_id,
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
There is no typed handle to prefer on this port, so this is never the wrong choice here.
|
|
118
|
+
Pass the same `--column-naming` to `metaobjects gen` that you pass as `column_naming=` to
|
|
119
|
+
`ObjectManager` (both default to `literal`), or the constant names a column no row lands in.
|
|
@@ -33,7 +33,7 @@ rejects and poisons every later install in the project with `ERESOLVE`.
|
|
|
33
33
|
|
|
34
34
|
| Export | Purpose |
|
|
35
35
|
|---|---|
|
|
36
|
-
| `<EntityFetcherProvider
|
|
36
|
+
| `<EntityFetcherProvider fetcher={fetcher} baseUrl="/api">` | supplies the single `EntityFetcher` every generated hook reads, plus the base URL it prepends. `baseUrl` is optional (default `""` = same origin at the root); generated hooks emit entity-relative paths. |
|
|
37
37
|
| `useEntityFetcher()` | reads the fetcher from context (generated hooks call this) |
|
|
38
38
|
| `<EntityGrid>` | opinionated TanStack Table component |
|
|
39
39
|
| `<CellRendererProvider>` + `defaultCellRenderers` | renderer overrides keyed by the column's `meta.view` |
|
|
@@ -63,7 +63,7 @@ const fetcher = async <T,>(path: string, init?: RequestInit): Promise<T> => {
|
|
|
63
63
|
|
|
64
64
|
export function App() {
|
|
65
65
|
return (
|
|
66
|
-
<EntityFetcherProvider
|
|
66
|
+
<EntityFetcherProvider fetcher={fetcher} baseUrl="/api">
|
|
67
67
|
<AuthorList />
|
|
68
68
|
</EntityFetcherProvider>
|
|
69
69
|
);
|
|
@@ -155,10 +155,15 @@ don't have to.
|
|
|
155
155
|
|
|
156
156
|
## Cell renderer overrides
|
|
157
157
|
|
|
158
|
-
`<EntityGrid>` routes rendering through `CellRendererProvider`, keyed by `meta.view`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
`<EntityGrid>` routes rendering through `CellRendererProvider`, keyed by `meta.view` —
|
|
159
|
+
which is the field's **registered `view.*` subtype** (`text` / `textarea` / `number` /
|
|
160
|
+
`date` / `month` / `checkbox` / `hotlink` / `currency` / `dropdown` / `radio` /
|
|
161
|
+
`password`), so a key that is not a registered subtype can never be selected. Override a
|
|
162
|
+
key without touching generated code; per-column `cell` always wins, the provider fills in
|
|
163
|
+
otherwise.
|
|
164
|
+
|
|
165
|
+
A `field.timestamp` declares `view.date` and renders date-only by default. To show the
|
|
166
|
+
time as well, override the `date` key — there is no `view.datetime` subtype.
|
|
162
167
|
|
|
163
168
|
```tsx
|
|
164
169
|
import { CellRendererProvider } from "@metaobjectsdev/tanstack";
|
|
@@ -168,3 +173,20 @@ import { formatCurrency } from "@metaobjectsdev/runtime-web";
|
|
|
168
173
|
<EntityGrid {...gridProps} />
|
|
169
174
|
</CellRendererProvider>
|
|
170
175
|
```
|
|
176
|
+
|
|
177
|
+
`view.image` has no default renderer and needs one wired: the field stores an opaque
|
|
178
|
+
storage key, so the cell needs the app's `ImageUploadAdapter` to resolve a `src`. Close
|
|
179
|
+
`imageCell` over your adapter — it is exported from this same package, so this costs no
|
|
180
|
+
extra dependency.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import { CellRendererProvider, imageCell } from "@metaobjectsdev/tanstack";
|
|
184
|
+
|
|
185
|
+
<CellRendererProvider value={{ image: imageCell(adapter, { size: 48 }) }}>
|
|
186
|
+
<EntityGrid {...gridProps} />
|
|
187
|
+
</CellRendererProvider>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`view.base`, `view.web` and `view.hidden` have no renderer either, and want none —
|
|
191
|
+
the first two are abstract roots nothing emits, and a `view.hidden` field is dropped
|
|
192
|
+
from the column set entirely (a blank cell would still carry a header and a sort target).
|
|
@@ -67,6 +67,26 @@ await om.transaction(async (tx) => { /* ... */ });
|
|
|
67
67
|
> `node-postgres`, `@neondatabase/serverless`; NOT on `better-sqlite3` / `bun:sqlite`
|
|
68
68
|
> (no native RETURNING) — use a custom driver or `inMemoryDriver` there.
|
|
69
69
|
|
|
70
|
+
### Physical names: the column object first, `<Entity>Names` where there is none
|
|
71
|
+
|
|
72
|
+
The generated query helpers and `ObjectManager` never make you spell a table or column.
|
|
73
|
+
Where you drop below them, bind to the Drizzle column object (`programs.createdAt` —
|
|
74
|
+
checked against the schema at compile time), never a string. What Drizzle does not
|
|
75
|
+
reach — a `sql` template fragment, a string-keyed Kysely identifier, a migration script,
|
|
76
|
+
a log line — takes the generated `<Entity>.names.ts` (`namesFile()` is in the `meta init`
|
|
77
|
+
scaffold):
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { sql } from "drizzle-orm";
|
|
81
|
+
import { ProgramNames } from "./generated/Program.names.js";
|
|
82
|
+
|
|
83
|
+
sql`SELECT ${sql.identifier(ProgramNames.fields.createdAt.column)}
|
|
84
|
+
FROM ${sql.identifier(ProgramNames.sources.primary.table)}`
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A literal is a second spelling of a fact the metadata owns: `@column` is free-form, and a
|
|
88
|
+
rename in metadata moves the constant, not the string.
|
|
89
|
+
|
|
70
90
|
## Return-type contract
|
|
71
91
|
|
|
72
92
|
The runtime returns **native in-process types**, never wire strings — temporal
|
|
@@ -82,6 +82,16 @@ it and call the generated query/field instead of keeping the hand-rolled version
|
|
|
82
82
|
This is the most common way a build ends up *declaring* a projection yet still
|
|
83
83
|
hand-aggregating in a route — verify catches exactly that.
|
|
84
84
|
|
|
85
|
+
**Read the whole report, not the first twenty lines.** Text output caps each
|
|
86
|
+
advisory section at 20 lines and then says how many it held back; raise it with
|
|
87
|
+
`--limit <n>` or remove it with `--limit all`. You are on a pipe, so your default
|
|
88
|
+
format is already TOON: `meta verify` puts one machine-readable document on stdout
|
|
89
|
+
carrying every gate's verdict, every anti-pattern finding and every requirement
|
|
90
|
+
diagnostic — **uncapped** — with narration on stderr. `--format json` if you want
|
|
91
|
+
JSON. A pass that did not run reports `status: "skipped"` and why, so an empty list
|
|
92
|
+
always means "found nothing", never "never looked"; what the payload does not carry
|
|
93
|
+
is named in its own `notRepresented[]`.
|
|
94
|
+
|
|
85
95
|
**A bare `verify` is a partial check, not the full gate.** The Node/C# default runs
|
|
86
96
|
only `--templates`; Java/Python's bare default runs only `--codegen` — either way,
|
|
87
97
|
paired with the advisory anti-pattern pass above, never all three subverbs. Treat a
|
|
@@ -163,6 +173,24 @@ Make it a **ratchet**: it can't go green until the last offending field is migra
|
|
|
163
173
|
backstop against reintroducing the smell. The same pattern generalizes to any semantic
|
|
164
174
|
metadata rule your project wants enforced that `verify` structurally can't express.
|
|
165
175
|
|
|
176
|
+
The other invariant worth a ratchet is **no physical name as a literal**. A table or
|
|
177
|
+
column string in hand-written code is a second spelling of a declared fact, and nothing
|
|
178
|
+
in `verify` compares your code to the schema — `--codegen` diffs generated files against
|
|
179
|
+
a fresh regen, `--db` diffs the database against the metadata. Neither reads the
|
|
180
|
+
repository you wrote. Build the alternation from the metadata's own `@table` / `@view` /
|
|
181
|
+
`@column` values and fail on any hit outside generated output:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
# fail the build if a declared physical name is spelled in hand-written source.
|
|
185
|
+
# Illustrative — generate the alternation from your metadata, scope it to the
|
|
186
|
+
# directories you hand-write. A typed ORM handle never matches this; a literal does.
|
|
187
|
+
! grep -rEn '"(orders|created_at|purpose_code)"' src/ --exclude-dir=generated
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Generated output is already covered upstream — every port's generators reference the
|
|
191
|
+
names artifact instead of embedding the literal — so this ratchet is only ever about your
|
|
192
|
+
code. The remedy for a hit is the `<Entity>Names` constant (`metaobjects-runtime-ui`).
|
|
193
|
+
|
|
166
194
|
## Schema migrations are the shared TypeScript engine — for every port
|
|
167
195
|
|
|
168
196
|
This is the load-bearing architectural fact (ADR-0015): **schema migrations are
|
|
@@ -8,16 +8,57 @@
|
|
|
8
8
|
MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the durable
|
|
9
9
|
spine; generated code is the disposable artifact. Regenerate with `{{codegenCommand}}`.
|
|
10
10
|
|
|
11
|
+
## Read the generated reference before you touch a tier
|
|
12
|
+
{{#nodeDocsSurface}}
|
|
13
|
+
`meta docs` writes four reference files under this project's docs output directory
|
|
14
|
+
(`./docs` unless `docs.outDir` in `metaobjects.config.ts` says otherwise). They are
|
|
15
|
+
generated from THIS project's metadata, so they describe what it actually is rather than
|
|
16
|
+
what the framework does in general — and reading the relevant one first is how you avoid
|
|
17
|
+
inferring the answer from generated code, which is the disposable half.
|
|
18
|
+
|
|
19
|
+
- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each
|
|
20
|
+
column came from, keys, foreign keys, constraints.
|
|
21
|
+
- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter
|
|
22
|
+
operators the generators actually emitted.
|
|
23
|
+
- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and
|
|
24
|
+
filterability per field, and each declared grid.
|
|
25
|
+
- **`agent/requirements.md`** — before adding a capability. What is already claimed, and
|
|
26
|
+
what was deliberately **retired** — an entry saying a capability must not be rebuilt is
|
|
27
|
+
the one thing you cannot learn from the code, because the code is gone.
|
|
28
|
+
|
|
29
|
+
Not there? Run `meta docs`. It reads the metadata and the config and needs nothing else.
|
|
30
|
+
{{/nodeDocsSurface}}
|
|
31
|
+
{{^nodeDocsSurface}}
|
|
32
|
+
`{{docsCommand}}` writes ONE reference file under this project's docs output directory. It
|
|
33
|
+
is generated from THIS project's metadata, so it describes what the project actually is
|
|
34
|
+
rather than what the framework does in general — and reading it first is how you avoid
|
|
35
|
+
inferring the answer from generated code, which is the disposable half.
|
|
36
|
+
|
|
37
|
+
- **`{{apiSubDir}}/AGENT-API.md`** — before calling generated code. The types, endpoints and
|
|
38
|
+
filter operators the generators actually emitted.
|
|
39
|
+
|
|
40
|
+
Not there? Run `{{docsCommand}}`.
|
|
41
|
+
|
|
42
|
+
The other three pages of that reference — `agent/schema.md` (persistence), `agent/ui.md`
|
|
43
|
+
(forms and grids) and `agent/requirements.md` (what is already claimed, and what was
|
|
44
|
+
deliberately **retired**) — are written by the Node `meta docs` command and need a
|
|
45
|
+
`metaobjects.config.ts`, which this stack does not have. Until it does, read the metadata
|
|
46
|
+
itself for those three: it is the source those pages are generated from.
|
|
47
|
+
{{/nodeDocsSurface}}
|
|
48
|
+
|
|
11
49
|
## Principles
|
|
12
50
|
- **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
|
-
- 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
|
|
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
|
|
15
|
-
- Never hand-edit generated
|
|
16
|
-
-
|
|
51
|
+
- 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V<n>__`/`U<n>__` files a Flyway runner expects, and leaves applying them to Flyway.
|
|
52
|
+
- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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.
|
|
53
|
+
- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `<Entity>.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one.
|
|
54
|
+
- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source scaffolded into your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill.
|
|
55
|
+
- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `<Entity>Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs.
|
|
56
|
+
- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported.
|
|
17
57
|
- 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.
|
|
18
58
|
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
|
|
19
59
|
|
|
20
60
|
## Authoring rules you must not violate
|
|
61
|
+
- **Search the vocabulary before concluding it cannot be expressed — `meta types <term>`.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types <term>`, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types <type>.<subType> --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
|
|
21
62
|
- Nodes are fused-key maps: `{"<type>.<subType>": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
|
|
22
63
|
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
|
|
23
64
|
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,EACyD,KAAK,aAAa,EAAE,KAAK,KAAK,EAC7F,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,EACyD,KAAK,aAAa,EAAE,KAAK,KAAK,EAC7F,MAAM,YAAY,CAAC;AAmHpB,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,aAAa,EAAE,CA8CrF"}
|
|
@@ -25,10 +25,58 @@ function stackLine(contentRoot, stack) {
|
|
|
25
25
|
// fragment carries the project-agnostic how. See issue #1.
|
|
26
26
|
line: `Stack: ${serverPart}, ${clientPart}.`,
|
|
27
27
|
codegenCommand: meta ? meta.codegenCommand : "meta gen",
|
|
28
|
+
docsCommand: meta ? meta.docsCommand : "meta docs",
|
|
29
|
+
apiSubDir: meta ? meta.apiSubDir : "api",
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* True when this stack runs the Node `meta` toolchain over a `metaobjects.config.ts`.
|
|
34
|
+
*
|
|
35
|
+
* This is the condition for the `agent/` docs surface EXISTING AT ALL: `meta docs` gates
|
|
36
|
+
* those three pages on a loadable gen config, because the physical names, the dialect and
|
|
37
|
+
* the view dispatch all come from it. A TypeScript server has one; so does any stack
|
|
38
|
+
* declaring a web CLIENT, because the React/TanStack tiers are generated by `meta gen`
|
|
39
|
+
* whatever the backend is. A port-only stack (java alone, python alone) has none, and for
|
|
40
|
+
* it the pointer must name only the one file its own docs command writes.
|
|
41
|
+
*
|
|
42
|
+
* The pointer landed verbatim in every stack before this existed, so a Python project was
|
|
43
|
+
* told to read four files that cannot exist and to run a command that cannot make them —
|
|
44
|
+
* on the one page an agent is told to trust.
|
|
45
|
+
*/
|
|
46
|
+
function hasNodeDocsSurface(stack) {
|
|
47
|
+
return stack.servers.includes("typescript") || stack.clients.length > 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Substitute `{{var}}` and the two section forms `{{#name}}…{{/name}}` (render when the
|
|
51
|
+
* flag is true) and `{{^name}}…{{/name}}` (render when it is false).
|
|
52
|
+
*
|
|
53
|
+
* Sections do NOT nest — one pass, non-greedy to the matching close tag — which is all the
|
|
54
|
+
* always-on template needs. An unknown variable or section name THROWS, and so does any
|
|
55
|
+
* section tag left standing after the pass (mistyped close tag, unclosed section, nesting):
|
|
56
|
+
* a silently empty substitution, or a literal `{{#…}}` shipped into a file an agent is told
|
|
57
|
+
* to trust, is the failure this whole surface exists to prevent.
|
|
58
|
+
*/
|
|
59
|
+
function applyTemplate(tpl, vars, sections) {
|
|
60
|
+
const resolved = tpl.replace(/\{\{([#^])(\w+)\}\}\n?([\s\S]*?)\{\{\/\2\}\}\n?/g, (_m, sigil, name, body) => {
|
|
61
|
+
if (!(name in sections)) {
|
|
62
|
+
throw new Error(`agent-context: unknown template section {{${sigil}${name}}}`);
|
|
63
|
+
}
|
|
64
|
+
// `#` shows its body when the flag is true, `^` when it is false.
|
|
65
|
+
const shownWhen = sigil === "#";
|
|
66
|
+
return sections[name] === shownWhen ? body : "";
|
|
67
|
+
});
|
|
68
|
+
// A section tag that SURVIVED the pass above is malformed — a mistyped close tag, an
|
|
69
|
+
// unclosed section, or a nested one (the single non-greedy pass matches the first close
|
|
70
|
+
// tag, leaving the inner pair behind). Every one of those would otherwise ship a literal
|
|
71
|
+
// `{{#…}}` into a file an agent is told to trust, which is the failure this mechanism
|
|
72
|
+
// exists to prevent, so the leftover is an ERROR rather than passed through. The
|
|
73
|
+
// variable pass below cannot catch it: `{{#a}}` is not a `\w+` match.
|
|
74
|
+
const leftover = resolved.match(/\{\{[#^/]\w*\}\}/);
|
|
75
|
+
if (leftover !== null) {
|
|
76
|
+
throw new Error(`agent-context: unresolved template section tag ${leftover[0]} — a section must be a ` +
|
|
77
|
+
"well-formed {{#name}}…{{/name}} or {{^name}}…{{/name}} pair, and sections do not nest");
|
|
78
|
+
}
|
|
79
|
+
return resolved.replace(/\{\{(\w+)\}\}/g, (_m, k) => {
|
|
32
80
|
if (!(k in vars))
|
|
33
81
|
throw new Error(`agent-context: unknown template variable {{${k}}}`);
|
|
34
82
|
return vars[k];
|
|
@@ -40,8 +88,8 @@ export function assemble(opts) {
|
|
|
40
88
|
const out = [];
|
|
41
89
|
// 1. Always-on (AGENTS.md + CLAUDE.md, identical contents).
|
|
42
90
|
const tpl = readFileSync(join(contentRoot, "templates", "always-on.md.mustache"), "utf8");
|
|
43
|
-
const { line, codegenCommand } = stackLine(contentRoot, stack);
|
|
44
|
-
const alwaysOn = applyTemplate(tpl, { stackLine: line, codegenCommand });
|
|
91
|
+
const { line, codegenCommand, docsCommand, apiSubDir } = stackLine(contentRoot, stack);
|
|
92
|
+
const alwaysOn = applyTemplate(tpl, { stackLine: line, codegenCommand, docsCommand, apiSubDir }, { nodeDocsSurface: hasNodeDocsSurface(stack) });
|
|
45
93
|
out.push({ path: ".metaobjects/AGENTS.md", contents: alwaysOn });
|
|
46
94
|
out.push({ path: ".metaobjects/CLAUDE.md", contents: alwaysOn });
|
|
47
95
|
// 2. Skills: body + only the references whose token is in the stack.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,GAC7D,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,kFAAkF;AAClF,iFAAiF;AACjF,oDAAoD;AACpD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,YAAY,EAAE,GAAG,iBAAiB,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"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"}
|