@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.
- package/README.md +1 -7
- package/agent-context/servers/csharp.meta.json +3 -1
- package/agent-context/servers/java.meta.json +3 -1
- package/agent-context/servers/kotlin.meta.json +3 -1
- package/agent-context/servers/python.meta.json +3 -1
- package/agent-context/servers/typescript.meta.json +3 -1
- package/agent-context/skills/metaobjects-audit/SKILL.md +15 -8
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +6 -1
- package/agent-context/skills/metaobjects-audit/references/csharp.md +10 -1
- package/agent-context/skills/metaobjects-audit/references/java.md +10 -0
- package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -0
- package/agent-context/skills/metaobjects-audit/references/python.md +12 -2
- package/agent-context/skills/metaobjects-audit/references/typescript.md +7 -2
- package/agent-context/skills/metaobjects-authoring/SKILL.md +35 -6
- package/agent-context/skills/metaobjects-codegen/SKILL.md +249 -10
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/java.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/kotlin.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/python.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +190 -6
- package/agent-context/skills/metaobjects-prompts/references/typescript.md +10 -1
- package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +34 -7
- package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
- package/agent-context/templates/always-on.md.mustache +45 -4
- package/dist/agent-context/assemble.d.ts.map +1 -1
- package/dist/agent-context/assemble.js +52 -4
- package/dist/agent-context/assemble.js.map +1 -1
- package/dist/agent-context/scaffold.d.ts +16 -1
- package/dist/agent-context/scaffold.d.ts.map +1 -1
- package/dist/agent-context/scaffold.js +81 -4
- package/dist/agent-context/scaffold.js.map +1 -1
- package/dist/forge-types.d.ts.map +1 -1
- package/dist/forge-types.js +19 -1
- package/dist/forge-types.js.map +1 -1
- package/package.json +4 -9
- package/src/agent-context/assemble.ts +86 -6
- package/src/agent-context/scaffold.ts +91 -5
- package/src/forge-types.ts +21 -0
- package/dist/agent-docs/body.d.ts +0 -6
- package/dist/agent-docs/body.d.ts.map +0 -1
- package/dist/agent-docs/body.js +0 -593
- package/dist/agent-docs/body.js.map +0 -1
- package/dist/agent-docs/content-hash.d.ts +0 -8
- package/dist/agent-docs/content-hash.d.ts.map +0 -1
- package/dist/agent-docs/content-hash.js +0 -23
- package/dist/agent-docs/content-hash.js.map +0 -1
- package/dist/agent-docs/index.d.ts +0 -3
- package/dist/agent-docs/index.d.ts.map +0 -1
- package/dist/agent-docs/index.js +0 -4
- package/dist/agent-docs/index.js.map +0 -1
- package/src/agent-docs/body.ts +0 -592
- package/src/agent-docs/content-hash.ts +0 -25
- package/src/agent-docs/index.ts +0 -8
|
@@ -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
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
14
|
-
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a JVM stack
|
|
15
|
-
- Never hand-edit generated
|
|
16
|
-
-
|
|
51
|
+
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V<n>__`/`U<n>__` files a Flyway runner expects, and leaves applying them to Flyway.
|
|
52
|
+
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db <url>` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
|
|
53
|
+
- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `<Entity>.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one.
|
|
54
|
+
- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source scaffolded into your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill.
|
|
55
|
+
- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `<Entity>Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs.
|
|
56
|
+
- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported.
|
|
17
57
|
- The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases.
|
|
18
58
|
- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope.
|
|
19
59
|
|
|
20
60
|
## Authoring rules you must not violate
|
|
61
|
+
- **Search the vocabulary before concluding it cannot be expressed — `meta types <term>`.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types <term>`, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types <type>.<subType> --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts-only]` are TypeScript-only vocabulary the cross-port contract does not carry. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty.
|
|
21
62
|
- Nodes are fused-key maps: `{"<type>.<subType>": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
|
|
22
63
|
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
|
|
23
64
|
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,EACyD,KAAK,aAAa,EAAE,KAAK,KAAK,EAC7F,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,EACyD,KAAK,aAAa,EAAE,KAAK,KAAK,EAC7F,MAAM,YAAY,CAAC;AAmHpB,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG,aAAa,EAAE,CA8CrF"}
|
|
@@ -25,10 +25,58 @@ function stackLine(contentRoot, stack) {
|
|
|
25
25
|
// fragment carries the project-agnostic how. See issue #1.
|
|
26
26
|
line: `Stack: ${serverPart}, ${clientPart}.`,
|
|
27
27
|
codegenCommand: meta ? meta.codegenCommand : "meta gen",
|
|
28
|
+
docsCommand: meta ? meta.docsCommand : "meta docs",
|
|
29
|
+
apiSubDir: meta ? meta.apiSubDir : "api",
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* True when this stack runs the Node `meta` toolchain over a `metaobjects.config.ts`.
|
|
34
|
+
*
|
|
35
|
+
* This is the condition for the `agent/` docs surface EXISTING AT ALL: `meta docs` gates
|
|
36
|
+
* those three pages on a loadable gen config, because the physical names, the dialect and
|
|
37
|
+
* the view dispatch all come from it. A TypeScript server has one; so does any stack
|
|
38
|
+
* declaring a web CLIENT, because the React/TanStack tiers are generated by `meta gen`
|
|
39
|
+
* whatever the backend is. A port-only stack (java alone, python alone) has none, and for
|
|
40
|
+
* it the pointer must name only the one file its own docs command writes.
|
|
41
|
+
*
|
|
42
|
+
* The pointer landed verbatim in every stack before this existed, so a Python project was
|
|
43
|
+
* told to read four files that cannot exist and to run a command that cannot make them —
|
|
44
|
+
* on the one page an agent is told to trust.
|
|
45
|
+
*/
|
|
46
|
+
function hasNodeDocsSurface(stack) {
|
|
47
|
+
return stack.servers.includes("typescript") || stack.clients.length > 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Substitute `{{var}}` and the two section forms `{{#name}}…{{/name}}` (render when the
|
|
51
|
+
* flag is true) and `{{^name}}…{{/name}}` (render when it is false).
|
|
52
|
+
*
|
|
53
|
+
* Sections do NOT nest — one pass, non-greedy to the matching close tag — which is all the
|
|
54
|
+
* always-on template needs. An unknown variable or section name THROWS, and so does any
|
|
55
|
+
* section tag left standing after the pass (mistyped close tag, unclosed section, nesting):
|
|
56
|
+
* a silently empty substitution, or a literal `{{#…}}` shipped into a file an agent is told
|
|
57
|
+
* to trust, is the failure this whole surface exists to prevent.
|
|
58
|
+
*/
|
|
59
|
+
function applyTemplate(tpl, vars, sections) {
|
|
60
|
+
const resolved = tpl.replace(/\{\{([#^])(\w+)\}\}\n?([\s\S]*?)\{\{\/\2\}\}\n?/g, (_m, sigil, name, body) => {
|
|
61
|
+
if (!(name in sections)) {
|
|
62
|
+
throw new Error(`agent-context: unknown template section {{${sigil}${name}}}`);
|
|
63
|
+
}
|
|
64
|
+
// `#` shows its body when the flag is true, `^` when it is false.
|
|
65
|
+
const shownWhen = sigil === "#";
|
|
66
|
+
return sections[name] === shownWhen ? body : "";
|
|
67
|
+
});
|
|
68
|
+
// A section tag that SURVIVED the pass above is malformed — a mistyped close tag, an
|
|
69
|
+
// unclosed section, or a nested one (the single non-greedy pass matches the first close
|
|
70
|
+
// tag, leaving the inner pair behind). Every one of those would otherwise ship a literal
|
|
71
|
+
// `{{#…}}` into a file an agent is told to trust, which is the failure this mechanism
|
|
72
|
+
// exists to prevent, so the leftover is an ERROR rather than passed through. The
|
|
73
|
+
// variable pass below cannot catch it: `{{#a}}` is not a `\w+` match.
|
|
74
|
+
const leftover = resolved.match(/\{\{[#^/]\w*\}\}/);
|
|
75
|
+
if (leftover !== null) {
|
|
76
|
+
throw new Error(`agent-context: unresolved template section tag ${leftover[0]} — a section must be a ` +
|
|
77
|
+
"well-formed {{#name}}…{{/name}} or {{^name}}…{{/name}} pair, and sections do not nest");
|
|
78
|
+
}
|
|
79
|
+
return resolved.replace(/\{\{(\w+)\}\}/g, (_m, k) => {
|
|
32
80
|
if (!(k in vars))
|
|
33
81
|
throw new Error(`agent-context: unknown template variable {{${k}}}`);
|
|
34
82
|
return vars[k];
|
|
@@ -40,8 +88,8 @@ export function assemble(opts) {
|
|
|
40
88
|
const out = [];
|
|
41
89
|
// 1. Always-on (AGENTS.md + CLAUDE.md, identical contents).
|
|
42
90
|
const tpl = readFileSync(join(contentRoot, "templates", "always-on.md.mustache"), "utf8");
|
|
43
|
-
const { line, codegenCommand } = stackLine(contentRoot, stack);
|
|
44
|
-
const alwaysOn = applyTemplate(tpl, { stackLine: line, codegenCommand });
|
|
91
|
+
const { line, codegenCommand, docsCommand, apiSubDir } = stackLine(contentRoot, stack);
|
|
92
|
+
const alwaysOn = applyTemplate(tpl, { stackLine: line, codegenCommand, docsCommand, apiSubDir }, { nodeDocsSurface: hasNodeDocsSurface(stack) });
|
|
45
93
|
out.push({ path: ".metaobjects/AGENTS.md", contents: alwaysOn });
|
|
46
94
|
out.push({ path: ".metaobjects/CLAUDE.md", contents: alwaysOn });
|
|
47
95
|
// 2. Skills: body + only the references whose token is in the stack.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,GAC7D,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,kFAAkF;AAClF,iFAAiF;AACjF,oDAAoD;AACpD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,YAAY,EAAE,GAAG,iBAAiB,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/agent-context/assemble.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,7 +30,22 @@ export interface ScaffoldDecision {
|
|
|
30
30
|
}[];
|
|
31
31
|
/** the manifest to persist after writing. */
|
|
32
32
|
manifest: Manifest;
|
|
33
|
-
/**
|
|
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,
|
|
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
|
|
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
|
|
53
|
-
//
|
|
54
|
-
//
|
|
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.`);
|