@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.
Files changed (58) hide show
  1. package/README.md +1 -7
  2. package/agent-context/servers/csharp.meta.json +3 -1
  3. package/agent-context/servers/java.meta.json +3 -1
  4. package/agent-context/servers/kotlin.meta.json +3 -1
  5. package/agent-context/servers/python.meta.json +3 -1
  6. package/agent-context/servers/typescript.meta.json +3 -1
  7. package/agent-context/skills/metaobjects-audit/SKILL.md +15 -8
  8. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +6 -1
  9. package/agent-context/skills/metaobjects-audit/references/csharp.md +10 -1
  10. package/agent-context/skills/metaobjects-audit/references/java.md +10 -0
  11. package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -0
  12. package/agent-context/skills/metaobjects-audit/references/python.md +12 -2
  13. package/agent-context/skills/metaobjects-audit/references/typescript.md +7 -2
  14. package/agent-context/skills/metaobjects-authoring/SKILL.md +35 -6
  15. package/agent-context/skills/metaobjects-codegen/SKILL.md +210 -15
  16. package/agent-context/skills/metaobjects-codegen/references/csharp.md +1 -0
  17. package/agent-context/skills/metaobjects-codegen/references/java.md +1 -0
  18. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +1 -0
  19. package/agent-context/skills/metaobjects-codegen/references/python.md +1 -0
  20. package/agent-context/skills/metaobjects-codegen/references/typescript.md +85 -6
  21. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
  22. package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
  23. package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
  25. package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
  26. package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +28 -6
  27. package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
  28. package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
  29. package/agent-context/templates/always-on.md.mustache +45 -4
  30. package/dist/agent-context/assemble.d.ts.map +1 -1
  31. package/dist/agent-context/assemble.js +52 -4
  32. package/dist/agent-context/assemble.js.map +1 -1
  33. package/dist/agent-context/scaffold.d.ts +16 -1
  34. package/dist/agent-context/scaffold.d.ts.map +1 -1
  35. package/dist/agent-context/scaffold.js +21 -1
  36. package/dist/agent-context/scaffold.js.map +1 -1
  37. package/dist/forge-types.d.ts.map +1 -1
  38. package/dist/forge-types.js +19 -1
  39. package/dist/forge-types.js.map +1 -1
  40. package/package.json +4 -9
  41. package/src/agent-context/assemble.ts +86 -6
  42. package/src/agent-context/scaffold.ts +33 -2
  43. package/src/forge-types.ts +21 -0
  44. package/dist/agent-docs/body.d.ts +0 -6
  45. package/dist/agent-docs/body.d.ts.map +0 -1
  46. package/dist/agent-docs/body.js +0 -593
  47. package/dist/agent-docs/body.js.map +0 -1
  48. package/dist/agent-docs/content-hash.d.ts +0 -8
  49. package/dist/agent-docs/content-hash.d.ts.map +0 -1
  50. package/dist/agent-docs/content-hash.js +0 -23
  51. package/dist/agent-docs/content-hash.js.map +0 -1
  52. package/dist/agent-docs/index.d.ts +0 -3
  53. package/dist/agent-docs/index.d.ts.map +0 -1
  54. package/dist/agent-docs/index.js +0 -4
  55. package/dist/agent-docs/index.js.map +0 -1
  56. package/src/agent-docs/body.ts +0 -592
  57. package/src/agent-docs/content-hash.ts +0 -25
  58. 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
- From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
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. Per-entity opt-out:
184
- `@emitTanstack: false` on the entity skips its hook + column files.
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; the
195
- runner overwrites those and refuses to touch files without it. Hand-customizations
196
- that metadata can't express live in sibling `<Entity>.extra.ts` files.
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 value={fetcher}>` | supplies the single `EntityFetcher` every generated hook reads |
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 value={fetcher}>
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
- (`text` / `number` / `date` / `boolean` / `currency` / `dropdown` / …). Override a
160
- key without touching generated code; per-column `cell` always wins, the provider
161
- fills in otherwise.
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 (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 <url>` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
15
- - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
16
- - Use the generated constants for any string that names metadata.
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;AAuCpB,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,aAAa,EAAE,CA0CrF"}
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
- function applyTemplate(tpl, vars) {
31
- return tpl.replace(/\{\{(\w+)\}\}/g, (_m, k) => {
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":"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"}
1
+ {"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"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"}