@metaobjectsdev/sdk 0.24.4 → 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 (59) 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 +249 -10
  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 +190 -6
  21. package/agent-context/skills/metaobjects-prompts/references/typescript.md +10 -1
  22. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
  23. package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
  25. package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
  26. package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
  27. package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +34 -7
  28. package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
  29. package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
  30. package/agent-context/templates/always-on.md.mustache +45 -4
  31. package/dist/agent-context/assemble.d.ts.map +1 -1
  32. package/dist/agent-context/assemble.js +52 -4
  33. package/dist/agent-context/assemble.js.map +1 -1
  34. package/dist/agent-context/scaffold.d.ts +16 -1
  35. package/dist/agent-context/scaffold.d.ts.map +1 -1
  36. package/dist/agent-context/scaffold.js +81 -4
  37. package/dist/agent-context/scaffold.js.map +1 -1
  38. package/dist/forge-types.d.ts.map +1 -1
  39. package/dist/forge-types.js +19 -1
  40. package/dist/forge-types.js.map +1 -1
  41. package/package.json +4 -9
  42. package/src/agent-context/assemble.ts +86 -6
  43. package/src/agent-context/scaffold.ts +91 -5
  44. package/src/forge-types.ts +21 -0
  45. package/dist/agent-docs/body.d.ts +0 -6
  46. package/dist/agent-docs/body.d.ts.map +0 -1
  47. package/dist/agent-docs/body.js +0 -593
  48. package/dist/agent-docs/body.js.map +0 -1
  49. package/dist/agent-docs/content-hash.d.ts +0 -8
  50. package/dist/agent-docs/content-hash.d.ts.map +0 -1
  51. package/dist/agent-docs/content-hash.js +0 -23
  52. package/dist/agent-docs/content-hash.js.map +0 -1
  53. package/dist/agent-docs/index.d.ts +0 -3
  54. package/dist/agent-docs/index.d.ts.map +0 -1
  55. package/dist/agent-docs/index.js +0 -4
  56. package/dist/agent-docs/index.js.map +0 -1
  57. package/src/agent-docs/body.ts +0 -592
  58. package/src/agent-docs/content-hash.ts +0 -25
  59. package/src/agent-docs/index.ts +0 -8
@@ -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.
@@ -20,15 +20,20 @@ this package.
20
20
  ```bash
21
21
  npm install @metaobjectsdev/tanstack @metaobjectsdev/runtime-web
22
22
  npm install --save-dev @metaobjectsdev/codegen-ts-tanstack
23
+ npm i @tanstack/react-table@^8.21.3
23
24
  ```
24
25
 
25
- Peer-deps: `@tanstack/react-query`, `@tanstack/react-table`.
26
+ Peer-deps: `@tanstack/react-query`, `@tanstack/react-table`. **Pin the react-table
27
+ major explicitly** — the registry's `latest` is v9, which removed `useReactTable`
28
+ and `getCoreRowModel` (both used by `<EntityGrid>`), so a bare
29
+ `npm i @tanstack/react-table` installs a version this package's `^8.20.0` peer range
30
+ rejects and poisons every later install in the project with `ERESOLVE`.
26
31
 
27
32
  ## Key exports
28
33
 
29
34
  | Export | Purpose |
30
35
  |---|---|
31
- | `<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. |
32
37
  | `useEntityFetcher()` | reads the fetcher from context (generated hooks call this) |
33
38
  | `<EntityGrid>` | opinionated TanStack Table component |
34
39
  | `<CellRendererProvider>` + `defaultCellRenderers` | renderer overrides keyed by the column's `meta.view` |
@@ -58,7 +63,7 @@ const fetcher = async <T,>(path: string, init?: RequestInit): Promise<T> => {
58
63
 
59
64
  export function App() {
60
65
  return (
61
- <EntityFetcherProvider value={fetcher}>
66
+ <EntityFetcherProvider fetcher={fetcher} baseUrl="/api">
62
67
  <AuthorList />
63
68
  </EntityFetcherProvider>
64
69
  );
@@ -150,10 +155,15 @@ don't have to.
150
155
 
151
156
  ## Cell renderer overrides
152
157
 
153
- `<EntityGrid>` routes rendering through `CellRendererProvider`, keyed by `meta.view`
154
- (`text` / `number` / `date` / `boolean` / `currency` / `dropdown` / …). Override a
155
- key without touching generated code; per-column `cell` always wins, the provider
156
- 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.
157
167
 
158
168
  ```tsx
159
169
  import { CellRendererProvider } from "@metaobjectsdev/tanstack";
@@ -163,3 +173,20 @@ import { formatCurrency } from "@metaobjectsdev/runtime-web";
163
173
  <EntityGrid {...gridProps} />
164
174
  </CellRendererProvider>
165
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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"}
@@ -30,7 +30,22 @@ export interface ScaffoldDecision {
30
30
  }[];
31
31
  /** the manifest to persist after writing. */
32
32
  manifest: Manifest;
33
- /** paths the prior manifest tracked that are no longer assembled (e.g. stack shrank) — reported, never auto-deleted. */
33
+ /**
34
+ * Orphans safe to DELETE: the prior manifest tracked them, this stack no longer
35
+ * assembles them, and the on-disk content still hashes to what we recorded writing.
36
+ *
37
+ * Same predicate that separates `writes` from `conflicts` — we only ever remove a file
38
+ * we wrote and nobody has touched, so deleting is exactly as safe as the overwrite that
39
+ * predicate already authorises for a file still in the stack.
40
+ */
41
+ prunes: string[];
42
+ /**
43
+ * Orphans that are HAND-EDITED — reported, never auto-deleted. Losing an adopter's
44
+ * writing is worse than leaving a stale file behind.
45
+ *
46
+ * An orphan already absent from disk appears in neither list: there is nothing to delete
47
+ * and nothing to report. It simply leaves the manifest.
48
+ */
34
49
  removed: string[];
35
50
  }
36
51
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/agent-context/scaffold.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEvD,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,qCAAqC,CAAC;AAE9E,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,CAAC;IACX;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,uFAAuF;IACvF,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAC;IACnB,wHAAwH;IACxH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC5B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAClD,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,gBAAgB,CA8BnB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,MAAM,GAAG,IAAI,CAYhB"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/agent-context/scaffold.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEvD,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,qCAAqC,CAAC;AAE9E,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,CAAC;IACX;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,uFAAuF;IACvF,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,6CAA6C;IAC7C,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC5B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAClD,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,gBAAgB,CA8CnB;AAwDD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,MAAM,GAAG,IAAI,CAahB"}
@@ -30,15 +30,90 @@ export function planScaffold(opts) {
30
30
  conflicts.push({ path: f.path, newPath: `${f.path}.new`, contents: f.contents });
31
31
  }
32
32
  }
33
+ // An orphan is a path the prior manifest tracked that this stack no longer assembles —
34
+ // typically a language fragment left by `--refresh-docs --server <other>`. It used to be
35
+ // reported and never deleted whatever its state, so a python.md sat in a TypeScript
36
+ // project forever while every SKILL.md footer told the reader to read every
37
+ // references/*.md "one per server language in this project's stack".
33
38
  const assembledPaths = new Set(assembled.map((f) => f.path));
34
- const removed = prior ? Object.keys(prior.files).filter((p) => !assembledPaths.has(p)) : [];
39
+ const prunes = [];
40
+ const removed = [];
41
+ for (const path of prior ? Object.keys(prior.files) : []) {
42
+ if (assembledPaths.has(path))
43
+ continue;
44
+ const current = readCurrent(path);
45
+ if (current === undefined)
46
+ continue; // already gone — nothing to do
47
+ if (hashContents(current) === prior?.files[path])
48
+ prunes.push(path);
49
+ else
50
+ removed.push(path);
51
+ }
35
52
  return {
36
53
  writes,
37
54
  conflicts,
55
+ // `files` holds only what this run assembled, so a pruned orphan does not survive into
56
+ // the new manifest — otherwise it would be re-reported as an orphan on every run.
38
57
  manifest: { version: 1, generatedBy, servers: stack.servers, clients: stack.clients, files },
58
+ prunes,
39
59
  removed,
40
60
  };
41
61
  }
62
+ /**
63
+ * The ordered release coordinate of a version — `[minor, patch]` — or `null` when the
64
+ * version cannot be ordered as a plain release.
65
+ *
66
+ * The MAJOR is deliberately dropped. It is a per-registry constant, not information:
67
+ * npm/PyPI/NuGet ship `0.<m>.<p>` and Maven Central the same `<m>.<p>` on its historical
68
+ * major `7`, so the minor.patch IS the shared release coordinate across all four (this is
69
+ * the same reduction the JVM's `releaseCoordinate` has always made for equality).
70
+ *
71
+ * Returns `null` — meaning "not orderable, so nudge" — for anything that is not exactly
72
+ * three dot-separated integers. That deliberately covers prereleases (`0.24.5-rc.1`),
73
+ * build metadata (`0.24.5+abc`), and the `0.0.0` sentinel a port emits when it cannot
74
+ * resolve its own installed version. Each must keep nudging: an RC-scaffolded context
75
+ * against a final release is still worth refreshing, and an unknown install must never
76
+ * be allowed to assert "in sync".
77
+ */
78
+ function releaseSeries(version) {
79
+ if (version === undefined)
80
+ return null;
81
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
82
+ if (m === null)
83
+ return null;
84
+ if (version.trim() === UNRESOLVED_VERSION)
85
+ return null; // never assert in-sync on unknown
86
+ return [Number(m[2]), Number(m[3])];
87
+ }
88
+ /** The sentinel a port stamps when it cannot resolve its own installed version. */
89
+ const UNRESOLVED_VERSION = "0.0.0";
90
+ /**
91
+ * True when the manifest was stamped by a release STRICTLY NEWER than the installed one.
92
+ *
93
+ * This is the one exemption from "any drift nudges", and it exists because of the
94
+ * publish-what-changed rule (docs/RELEASING.md): a registry publishes only when it has a
95
+ * changed product file, so a port legitimately sits behind npm — while `meta agent-docs`,
96
+ * the canonical scaffolder for EVERY port, stamps the npm version it was run from. A
97
+ * Python install at `0.24.4` whose context was scaffolded by npm `0.24.7` is correct, and
98
+ * nudging it is [#347](https://github.com/metaobjectsdev/metaobjects/issues/347) exactly:
99
+ * the remedy re-runs the scaffolder, which re-stamps `0.24.7`, so the advisory can never
100
+ * be satisfied and fires on every build forever. An advisory that cries wolf in the inner
101
+ * loop gets tuned out, and then it is not there for the upgrade it exists for.
102
+ *
103
+ * KNOWN BOUND, stated rather than hidden: ordering on minor.patch assumes both versions
104
+ * sit in the same release SERIES. That holds for every release to date and for every
105
+ * release after the 1.0/8.0 cut, but not ACROSS it — at that one cut a `0.24.x`-stamped
106
+ * context against a `1.0.0` install compares (24,x) > (0,0) and is read as "ahead", so the
107
+ * nudge is suppressed once when it should fire. The cost is a missed advisory, never a
108
+ * wrong action, and re-scaffolding at 1.0 is part of the cut anyway.
109
+ */
110
+ function contextIsAheadOfInstall(generatedBy, currentVersion) {
111
+ const stamped = releaseSeries(generatedBy);
112
+ const installed = releaseSeries(currentVersion);
113
+ if (stamped === null || installed === null)
114
+ return false; // not orderable → nudge
115
+ return stamped[0] > installed[0] || (stamped[0] === installed[0] && stamped[1] > installed[1]);
116
+ }
42
117
  /**
43
118
  * A one-line nudge if the scaffolded agent context predates the installed MetaObjects
44
119
  * (so `gen`/`verify` can remind the user to refresh the skills after an upgrade), or
@@ -49,11 +124,13 @@ export function agentContextStaleness(opts) {
49
124
  const { manifest, currentVersion } = opts;
50
125
  if (manifest === undefined)
51
126
  return null; // no agent context here → nothing to nudge
52
- // Exact-equality on purpose: ANY drift nudges (a re-scaffold is cheap + idempotent).
53
- // Don't "fix" this into a semver compare a prerelease/build-metadata difference is
54
- // still a reason to refresh, and the nudge is advisory, never a gate.
127
+ // Exact-equality FIRST: ANY drift nudges (a re-scaffold is cheap + idempotent). A
128
+ // prerelease/build-metadata difference is still a reason to refresh, so this is not a
129
+ // semver compare see releaseSeries() for the ONE case that is exempt.
55
130
  if (manifest.generatedBy === currentVersion)
56
131
  return null; // in sync
132
+ if (contextIsAheadOfInstall(manifest.generatedBy, currentVersion))
133
+ return null;
57
134
  const from = manifest.generatedBy ?? "an older MetaObjects";
58
135
  return (`MetaObjects agent context was generated by ${from}; you're on ${currentVersion}. ` +
59
136
  `Re-run 'meta init --docs-only --refresh-docs' to refresh the .claude/skills docs.`);