@metaobjectsdev/sdk 0.16.0 → 0.17.0-rc.1
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/agent-context/skills/metaobjects-audit/SKILL.md +25 -12
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +54 -20
- package/agent-context/skills/metaobjects-audit/references/csharp.md +7 -4
- package/agent-context/skills/metaobjects-audit/references/java.md +12 -10
- package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -7
- package/agent-context/skills/metaobjects-audit/references/python.md +7 -4
- package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
- package/agent-context/skills/metaobjects-authoring/SKILL.md +171 -39
- package/agent-context/skills/metaobjects-codegen/SKILL.md +45 -16
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +21 -1
- package/agent-context/skills/metaobjects-codegen/references/java.md +43 -5
- package/agent-context/skills/metaobjects-codegen/references/kotlin.md +34 -4
- package/agent-context/skills/metaobjects-codegen/references/python.md +32 -8
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +19 -3
- package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +55 -18
- package/agent-context/skills/metaobjects-prompts/SKILL.md +65 -4
- package/agent-context/skills/metaobjects-prompts/references/csharp.md +21 -0
- package/agent-context/skills/metaobjects-prompts/references/java.md +32 -3
- package/agent-context/skills/metaobjects-prompts/references/kotlin.md +33 -3
- package/agent-context/skills/metaobjects-prompts/references/python.md +29 -3
- package/agent-context/skills/metaobjects-prompts/references/typescript.md +34 -1
- package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +1 -1
- package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +90 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/python.md +94 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +50 -15
- package/agent-context/skills/metaobjects-verify/references/migration.md +67 -14
- package/dist/agent-docs/body.d.ts +1 -1
- package/dist/agent-docs/body.d.ts.map +1 -1
- package/dist/agent-docs/body.js +3 -1
- package/dist/agent-docs/body.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-docs/body.ts +3 -1
|
@@ -8,6 +8,7 @@ provider/LLM-call layer; you compose the call yourself.
|
|
|
8
8
|
## Contents
|
|
9
9
|
- Wire the generator
|
|
10
10
|
- What it emits
|
|
11
|
+
- The output-format prompt fragment (FR-010)
|
|
11
12
|
- The three-step consumer pattern
|
|
12
13
|
- Recommended LLM caller (bring-your-own)
|
|
13
14
|
- Drift gate
|
|
@@ -52,6 +53,28 @@ rather than a throw. The payload record itself comes from `SpringPayloadGenerato
|
|
|
52
53
|
— the parser is a companion to it, so the parser and payload VO can't silently
|
|
53
54
|
drift.
|
|
54
55
|
|
|
56
|
+
## The output-format prompt fragment (FR-010)
|
|
57
|
+
|
|
58
|
+
For every json/xml-format `template.output`, `codegen-spring`'s
|
|
59
|
+
`SpringOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt` class with a
|
|
60
|
+
static `renderFormat()` / `renderFormat(PromptOverrides)` pair, backed by
|
|
61
|
+
`OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
|
|
62
|
+
answer like this" fragment for the model. Wire it alongside
|
|
63
|
+
`SpringOutputParserGenerator` in the Maven plugin's `<generators>` list:
|
|
64
|
+
|
|
65
|
+
```xml
|
|
66
|
+
<generator>
|
|
67
|
+
<classname>com.metaobjects.generator.spring.SpringOutputPromptGenerator</classname>
|
|
68
|
+
<args><outputDir>${project.build.directory}/generated-sources/java</outputDir></args>
|
|
69
|
+
</generator>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
|
|
73
|
+
controls the fragment's presentation; guidance is never emitted as comments. Skipped
|
|
74
|
+
for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
|
|
75
|
+
the same skip contract as the parser generator. The `SPEC`'s root name is the
|
|
76
|
+
capitalized payload class name, agreeing with the parser's extract-codegen root.
|
|
77
|
+
|
|
55
78
|
## The three-step consumer pattern
|
|
56
79
|
|
|
57
80
|
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
@@ -87,9 +110,15 @@ NpcResponsePayload npc = NpcResponseParser.parse(response); // the generated p
|
|
|
87
110
|
Non-Spring JVM apps: **LangChain4j** (`ChatLanguageModel.generate(prompt)`) is the
|
|
88
111
|
equivalent one-call seam.
|
|
89
112
|
|
|
90
|
-
> The typed-trace recorder
|
|
91
|
-
>
|
|
92
|
-
>
|
|
113
|
+
> The typed-trace **recorder** has shipped on this port too — `LlmTraceHelperGenerator`
|
|
114
|
+
> emits a `record<Entity>(...)` helper (per concrete entity extending `LlmCallBase`
|
|
115
|
+
> with a `@responseRef`-carrying `template.prompt`) that extracts the typed response,
|
|
116
|
+
> builds the base trace row, and persists it via the OMDB `LlmCallRecorder` seam. What's
|
|
117
|
+
> still TS-only is the **`call<Entity>` render→call→record convenience loop** — Java
|
|
118
|
+
> intentionally does not emit it, because the `LlmClient` seam it wraps is BYO /
|
|
119
|
+
> vendor-neutral here (ADR-0024). So you compose render → your LLM call → the
|
|
120
|
+
> generated `record<Entity>(...)` yourself; the parser above is the standalone
|
|
121
|
+
> receive side if you don't even want the recorder.
|
|
93
122
|
|
|
94
123
|
## Drift gate
|
|
95
124
|
|
|
@@ -10,6 +10,7 @@ the parser and the payload VO can't silently drift.
|
|
|
10
10
|
## Contents
|
|
11
11
|
- Wire the generators
|
|
12
12
|
- What it emits
|
|
13
|
+
- The output-format prompt fragment (FR-010)
|
|
13
14
|
- The three-step consumer pattern
|
|
14
15
|
- Consumer dependency
|
|
15
16
|
- Recommended LLM caller (bring-your-own)
|
|
@@ -62,6 +63,28 @@ null) and a `extractLenient(loader, text)` overload that delegates to the runtim
|
|
|
62
63
|
components. The lenient mirror type (`<Name>Extracted`) uses nullable fields per
|
|
63
64
|
the Kotlin null-safety port — a missing/malformed component is `null`, not a throw.
|
|
64
65
|
|
|
66
|
+
## The output-format prompt fragment (FR-010)
|
|
67
|
+
|
|
68
|
+
For every json/xml-format `template.output`, `codegen-kotlin`'s
|
|
69
|
+
`KotlinOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt.kt` `object`
|
|
70
|
+
with `renderFormat()` / `renderFormat(overrides: PromptOverrides)`, backed by
|
|
71
|
+
`OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
|
|
72
|
+
answer like this" fragment for the model. Wire it alongside
|
|
73
|
+
`KotlinOutputParserGenerator` in the Maven plugin's `<generators>` list:
|
|
74
|
+
|
|
75
|
+
```xml
|
|
76
|
+
<generator>
|
|
77
|
+
<classname>com.metaobjects.generator.kotlin.KotlinOutputPromptGenerator</classname>
|
|
78
|
+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
|
|
79
|
+
</generator>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
|
|
83
|
+
controls the fragment's presentation; guidance is never emitted as comments. Skipped
|
|
84
|
+
for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
|
|
85
|
+
the same skip contract as the parser generator. The `SPEC`'s root name is the
|
|
86
|
+
capitalized payload class name, agreeing with the parser's extract-codegen root.
|
|
87
|
+
|
|
65
88
|
## The three-step consumer pattern
|
|
66
89
|
|
|
67
90
|
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
@@ -115,9 +138,16 @@ val npc = NpcResponseParser.parseNpcResponse(response) // the generated parser
|
|
|
115
138
|
(`ChatLanguageModel.generate(prompt)`) for non-Spring JVM — both provider-agnostic,
|
|
116
139
|
both a one-call seam Kotlin uses idiomatically.
|
|
117
140
|
|
|
118
|
-
> The typed-trace recorder
|
|
119
|
-
>
|
|
120
|
-
>
|
|
141
|
+
> The typed-trace **recorder** has shipped on the JVM — `codegen-spring`'s
|
|
142
|
+
> `LlmTraceHelperGenerator` emits a Java `record<Entity>(...)` helper (per concrete
|
|
143
|
+
> entity extending `LlmCallBase` with a `@responseRef`-carrying `template.prompt`);
|
|
144
|
+
> Kotlin code calls it directly (same JVM, same classpath) — there is no separate
|
|
145
|
+
> Kotlin-native (`codegen-kotlin`/KotlinPoet) trace-helper emitter yet. What's TS-only
|
|
146
|
+
> is the **`call<Entity>` render→call→record convenience loop** — neither JVM
|
|
147
|
+
> generator emits it, because the `LlmClient` seam it wraps is BYO / vendor-neutral on
|
|
148
|
+
> the JVM (ADR-0024). So you compose render → your LLM call → the generated Java
|
|
149
|
+
> `record<Entity>(...)` yourself; the parser above is the standalone receive side if
|
|
150
|
+
> you don't even want the recorder.
|
|
121
151
|
|
|
122
152
|
## Drift gate
|
|
123
153
|
|
|
@@ -10,6 +10,7 @@ drift.
|
|
|
10
10
|
## Contents
|
|
11
11
|
- Wire the generators
|
|
12
12
|
- What it emits
|
|
13
|
+
- The output-format prompt fragment (FR-010)
|
|
13
14
|
- The three-step consumer pattern
|
|
14
15
|
- Recommended LLM caller (bring-your-own)
|
|
15
16
|
- Consumer dependency
|
|
@@ -54,6 +55,24 @@ best-effort variant — `extract_lenient_<name>(text) -> ExtractionResult[<Name>
|
|
|
54
55
|
per-field report rather than a raise. The lenient mirror (`<Name>PayloadExtracted`)
|
|
55
56
|
uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
|
|
56
57
|
|
|
58
|
+
## The output-format prompt fragment (FR-010)
|
|
59
|
+
|
|
60
|
+
For every json/xml-format `template.output`, the `output-prompt` generator (run via
|
|
61
|
+
`metaobjects gen`) emits one `<template_name>_output_prompt.py` module exposing
|
|
62
|
+
`render_<name>_format(overrides=None) -> str`, backed by the render engine's
|
|
63
|
+
`render_output_format()` — the "produce your answer like this" fragment for the
|
|
64
|
+
model:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
metaobjects gen ./metadata --out ./generated --generators payload,output-prompt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
|
|
71
|
+
controls the fragment's presentation; guidance is never emitted as comments. Skipped
|
|
72
|
+
for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
|
|
73
|
+
the same skip contract as the `output-parser` generator. The baked spec's root name
|
|
74
|
+
is the payload class name, agreeing with the parser's `extract_<name>()` root.
|
|
75
|
+
|
|
57
76
|
## The three-step consumer pattern
|
|
58
77
|
|
|
59
78
|
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
@@ -96,9 +115,16 @@ to enforce the typed shape instead, **Instructor** or **Pydantic-AI** return a
|
|
|
96
115
|
validated Pydantic model — but that overlaps MetaObjects' own typed `extract`, so pick
|
|
97
116
|
one boundary, not both.
|
|
98
117
|
|
|
99
|
-
> The typed-trace recorder
|
|
100
|
-
>
|
|
101
|
-
>
|
|
118
|
+
> The typed-trace **recorder** has shipped on this port too — the `trace-helper`
|
|
119
|
+
> generator emits a `record_<entity>(recorder, input, redact=None)` helper (per
|
|
120
|
+
> concrete entity extending `LlmCallBase` with a `@responseRef`/`@payloadRef`-carrying
|
|
121
|
+
> `template.prompt`) that tolerantly extracts the typed response, builds the base
|
|
122
|
+
> trace row, and persists it once. What's still TS-only is the **`call<Entity>`
|
|
123
|
+
> render→call→record convenience loop** — Python intentionally does not emit it,
|
|
124
|
+
> because the `LlmClient` seam it wraps is BYO / vendor-neutral here (ADR-0024). So
|
|
125
|
+
> you compose render → your LLM call → the generated `record_<entity>(...)` yourself;
|
|
126
|
+
> the parser above is the standalone receive side if you don't even want the
|
|
127
|
+
> recorder.
|
|
102
128
|
|
|
103
129
|
## Consumer dependency
|
|
104
130
|
|
|
@@ -9,6 +9,7 @@ call yourself.
|
|
|
9
9
|
## Contents
|
|
10
10
|
- Wire the generator
|
|
11
11
|
- What it emits
|
|
12
|
+
- The output-format prompt fragment (FR-010)
|
|
12
13
|
- The three-step consumer pattern
|
|
13
14
|
- Recommended LLM caller (bring-your-own)
|
|
14
15
|
- Drift gate
|
|
@@ -66,7 +67,7 @@ The dual API mirrors Zod's idiomatic shape: `parse*` throws a `ZodError`,
|
|
|
66
67
|
structurally identical to the `promptRender()` payload VO, so you can pass values
|
|
67
68
|
between the render and parse sides interchangeably.
|
|
68
69
|
|
|
69
|
-
Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long
|
|
70
|
+
Field-type → Zod mapping: `field.string` → `z.string()`; `field.int`/`long`
|
|
70
71
|
→ `z.number().int()`; `field.double`/`float` → `z.number()`; `field.boolean` →
|
|
71
72
|
`z.boolean()`; `field.object` (with `@objectRef`) → a nested `z.object({...})`;
|
|
72
73
|
`isArray: true` → wrapped in `z.array(...)`. Any subtype outside this scalar set —
|
|
@@ -75,6 +76,38 @@ including `field.enum` — falls through to `z.unknown()` in the strict
|
|
|
75
76
|
in the entity insert/update schemas, not in this output parser; the lenient extract
|
|
76
77
|
path carries the enum-as-string handling).
|
|
77
78
|
|
|
79
|
+
## The output-format prompt fragment (FR-010)
|
|
80
|
+
|
|
81
|
+
For every json/xml-format `template.output`, the `outputPrompt()` generator (same
|
|
82
|
+
import path, `@metaobjectsdev/codegen-ts/generators`) emits a
|
|
83
|
+
`<TemplateName>.prompt.ts` exporting `render<TemplateName>Format(overrides?)` —
|
|
84
|
+
backed by the render engine's `renderOutputFormat()`. This is the "produce your
|
|
85
|
+
answer like this" fragment you splice into the prompt text so the model returns the
|
|
86
|
+
shape the parser above expects:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// metaobjects.config.ts
|
|
90
|
+
import { entityFile, queriesFile, barrel, promptRender, outputParser, outputPrompt }
|
|
91
|
+
from "@metaobjectsdev/codegen-ts/generators";
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
outDir: "src/generated",
|
|
95
|
+
generators: [
|
|
96
|
+
entityFile(), queriesFile(), barrel(),
|
|
97
|
+
promptRender(), outputParser(), outputPrompt(),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
|
|
103
|
+
controls the fragment's presentation: `guide` is a prose field list + example
|
|
104
|
+
skeleton, `inline` is one skeleton with inline placeholders/enum choices,
|
|
105
|
+
`exampleOnly` is just the filled skeleton. Guidance is never emitted as comments —
|
|
106
|
+
models ignore them. Skipped for non-json/xml `@format` outputs and for any output
|
|
107
|
+
whose `@payloadRef` doesn't resolve to a value-object — the same skip contract as
|
|
108
|
+
`outputParser()`. The baked spec's root name matches the payload class name, so the
|
|
109
|
+
fragment and the parser's `extract()` agree on the same root.
|
|
110
|
+
|
|
78
111
|
## The three-step consumer pattern
|
|
79
112
|
|
|
80
113
|
Render the prompt → call your LLM client (provider-agnostic; nothing is generated
|
|
@@ -76,7 +76,7 @@ a field → HTTP 400.
|
|
|
76
76
|
| `in`, `like` | yes | `in` only | – |
|
|
77
77
|
| `gt`, `gte`, `lt`, `lte` | – | yes | – |
|
|
78
78
|
|
|
79
|
-
These
|
|
79
|
+
These nine (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) are the whole
|
|
80
80
|
closed set — every port implements these and only these.
|
|
81
81
|
|
|
82
82
|
### Sort + pagination
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# C# server runtime
|
|
2
|
+
|
|
3
|
+
The C# runtime tier **is the generated EF Core stack itself** — there is no separate
|
|
4
|
+
`ObjectManager` engine to wire. `EntityGenerator` + `DbContextGenerator` emit
|
|
5
|
+
`<Entity>.g.cs` classes plus one `AppDbContext.g.cs` that ARE the persistence layer (EF
|
|
6
|
+
Core), and `RoutesGenerator` emits ASP.NET minimal-API routes that mount on your
|
|
7
|
+
`WebApplication`. Unlike the other ports, C# leaves **no repository seam** — the generated
|
|
8
|
+
code is what runs. Schema is TS-owned (ADR-0015); EF Core is pure data-access at runtime.
|
|
9
|
+
|
|
10
|
+
## The generated persistence layer
|
|
11
|
+
|
|
12
|
+
`dotnet meta gen` emits an entity POCO per `object.entity` / projection and a single
|
|
13
|
+
`AppDbContext` with a `DbSet<T>` per entity (naively pluralized) plus an `OnModelCreating`
|
|
14
|
+
that carries the mapping:
|
|
15
|
+
|
|
16
|
+
```csharp
|
|
17
|
+
// generated AppDbContext.g.cs
|
|
18
|
+
public class AppDbContext : DbContext
|
|
19
|
+
{
|
|
20
|
+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
|
21
|
+
|
|
22
|
+
public DbSet<Author> Authors { get; set; } = default!;
|
|
23
|
+
|
|
24
|
+
protected override void OnModelCreating(ModelBuilder b)
|
|
25
|
+
{
|
|
26
|
+
// .HasConversion<string>() (enums), .OwnsOne(...).ToJson(...) / .OwnsMany(...).ToJson(...)
|
|
27
|
+
// (jsonb object fields), .HasPrecision(p,s) (decimals), .ToView("v_...") (projections),
|
|
28
|
+
// .HasDiscriminator(...).HasValue<Sub>(...) (TPH single-table).
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Query + persist with EF Core
|
|
34
|
+
|
|
35
|
+
Register the generated context against your provider, then query/write with plain LINQ +
|
|
36
|
+
`SaveChangesAsync` — the generated entities and `AppDbContext` are ordinary EF Core:
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(connString));
|
|
40
|
+
// ...
|
|
41
|
+
var authors = await db.Authors.AsNoTracking()
|
|
42
|
+
.Where(a => a.Name == "Ada").ToListAsync();
|
|
43
|
+
|
|
44
|
+
db.Authors.Add(new Author { Name = "Ada" });
|
|
45
|
+
await db.SaveChangesAsync(); // server-generated PK round-trips back onto the entity
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Return-type contract
|
|
49
|
+
|
|
50
|
+
The EF Core query path materializes **native in-process CLR types**, never wire strings
|
|
51
|
+
(ADR-0019), verified by the port's runtime-return-type test:
|
|
52
|
+
|
|
53
|
+
- `field.decimal` → `decimal` — exact, **lossless end-to-end**, no float round-tripping.
|
|
54
|
+
- `field.long` → `long`; other scalars to their native CLR types.
|
|
55
|
+
- a default `field.timestamp` (instant) → `DateTimeOffset` over a `timestamptz` column; a
|
|
56
|
+
naive `@localTime:true` timestamp → `DateTime` — native temporals, not strings.
|
|
57
|
+
- an untyped `@dbColumnType:jsonb` open-JSON column → a `System.Text.Json.JsonDocument` (the
|
|
58
|
+
parsed value, not raw text); a **typed** `field.object @storage:jsonb` (with `@objectRef`)
|
|
59
|
+
materializes the generated value-object class via `OwnsOne`/`OwnsMany(...).ToJson(...)`.
|
|
60
|
+
|
|
61
|
+
Wire canonicalization (currency → integer minor units, temporals → ISO-8601, UUID →
|
|
62
|
+
canonical hex) happens only when a row leaves over HTTP — at the serialization boundary in
|
|
63
|
+
the generated routes — never inside the EF Core query path. Currency is integer minor units
|
|
64
|
+
on the wire and in storage (a native `long` in-process); the server never formats —
|
|
65
|
+
formatting is client-side in the universal (TS/Angular) web client.
|
|
66
|
+
|
|
67
|
+
## Serving the REST contract
|
|
68
|
+
|
|
69
|
+
`RoutesGenerator` emits `<Entity>Routes.g.cs` — full CRUD per writable entity
|
|
70
|
+
(`source.rdb @kind="table"`), read-only list/get for a projection/view, and a collection-GET
|
|
71
|
+
for a composite-/no-PK entity — with a `Map<Entity>Routes(this IEndpointRouteBuilder,
|
|
72
|
+
string prefix = "/api")` extension, on the
|
|
73
|
+
cross-port REST contract (five CRUD endpoints, `?filter[field][op]=`, `?sort=field:asc`,
|
|
74
|
+
`?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). A TPH `@discriminator`
|
|
75
|
+
base emits polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at
|
|
76
|
+
`/<base>/<discriminatorValue lowercased>` (create injects the discriminator, cross-subtype
|
|
77
|
+
get/update/delete → 404). Wire it after registering the context:
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
var app = builder.Build();
|
|
81
|
+
app.MapAuthorRoutes("/api"); // the generated extension; you pass the prefix at mount time (default "/api")
|
|
82
|
+
app.Run();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Filter operators (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) ship via the
|
|
86
|
+
per-entity `<Entity>FilterAllowlist` (from `FilterAllowlistGenerator`); the generated list
|
|
87
|
+
handler calls the `FilterParser` / `EfCoreFilterDispatch` runtime helpers in
|
|
88
|
+
`MetaObjects.Codegen`, so your ASP.NET host references that assembly at runtime. The same
|
|
89
|
+
universal TS/Angular web client consumes those routes unchanged — the wire format matches
|
|
90
|
+
the Java, Kotlin, and Python backends byte-for-byte.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Python server runtime
|
|
2
|
+
|
|
3
|
+
The Python runtime tier is the **`metaobjects.runtime.ObjectManager`** — a
|
|
4
|
+
metadata-driven query/CRUD engine that reads the same metadata at runtime and drives
|
|
5
|
+
persistence with no per-entity ORM boilerplate. It is the cross-port analog of Java's
|
|
6
|
+
OMDB: a method-based API (`find_by_id` / `find_many` / `count` / `create` / `update` /
|
|
7
|
+
`delete` / `relate`) that compiles a Filter dict to parameterized SQL and runs it via a
|
|
8
|
+
pluggable driver. It is **pure data-access** (CRUD / query / codec); schema is owned by
|
|
9
|
+
the Node `meta` migration tool, not the runtime.
|
|
10
|
+
|
|
11
|
+
## Construct an `ObjectManager`
|
|
12
|
+
|
|
13
|
+
`ObjectManager(root, driver)` takes a loaded metadata `root` plus a `PostgresDriver`
|
|
14
|
+
wrapping any DB-API 2 connection (pg8000 / psycopg):
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import pg8000
|
|
18
|
+
from metaobjects import load_directory
|
|
19
|
+
from metaobjects.runtime import ObjectManager, PostgresDriver
|
|
20
|
+
|
|
21
|
+
result = load_directory("metaobjects") # same canonical JSON every port reads
|
|
22
|
+
conn = pg8000.connect(...) # any DB-API 2 connection
|
|
23
|
+
om = ObjectManager(result.root, PostgresDriver(conn))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CRUD + query
|
|
27
|
+
|
|
28
|
+
Rows come back as **plain dicts keyed by metadata field name**, values native (see
|
|
29
|
+
below). Write calls take a field-keyed dict of values in their native authoring forms;
|
|
30
|
+
the write codec coerces each to the native Python type the driver binds:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# Create — returns the inserted row (incl. any server-generated PK) via RETURNING
|
|
34
|
+
author = om.create("Author", {"name": "Ada"})
|
|
35
|
+
|
|
36
|
+
# Read
|
|
37
|
+
one = om.find_by_id("Author", author["id"])
|
|
38
|
+
rows = om.find_many("Author", {"name": {"like": "Ada%"}},
|
|
39
|
+
sort=[("createdAt", "desc")], limit=25, offset=0)
|
|
40
|
+
n = om.count("Author", {"name": {"eq": "Ada"}})
|
|
41
|
+
|
|
42
|
+
# Update (partial, by PK) / Delete
|
|
43
|
+
om.update("Author", author["id"], {"name": "Ada Lovelace"}) # None if no row matched
|
|
44
|
+
om.delete("Author", author["id"]) # bool
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`create()` stamps `@autoSet` timestamps (`createdAt` / `updatedAt`) with a shared `now()`,
|
|
48
|
+
**overriding** any caller-supplied value, and `update()` strips `onCreate`-only columns. For
|
|
49
|
+
a data import/restore that must preserve original timestamps, use `insert_preserving()` (#203).
|
|
50
|
+
|
|
51
|
+
The Filter dict is `{field: value}` (equality shortcut), `{field: {op: value}}` (typed
|
|
52
|
+
ops), or `{"and": [filter, ...]}` (combinator). The operator set is the closed cross-port
|
|
53
|
+
nine — `eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`. `relate()` traverses an M:N
|
|
54
|
+
relationship from a source record to its related rows (hetero / directed / symmetric
|
|
55
|
+
self-joins), resolved generically from the junction's `identity.reference` children.
|
|
56
|
+
|
|
57
|
+
## Return-type contract
|
|
58
|
+
|
|
59
|
+
`ObjectManager` returns **native in-process Python types**, never wire strings (ADR-0019),
|
|
60
|
+
verified by the port's runtime-return-type test:
|
|
61
|
+
|
|
62
|
+
- `field.decimal` → `decimal.Decimal` — exact, **lossless end-to-end**, never via float.
|
|
63
|
+
- temporal fields → native `datetime` / `date` / `time`.
|
|
64
|
+
- `field.object` / jsonb → a native `dict` (pg8000 decodes jsonb).
|
|
65
|
+
- `field.uuid` → `uuid.UUID`; `field.currency` → a native `int` (integer minor units).
|
|
66
|
+
|
|
67
|
+
Wire canonicalization (currency → integer minor units, temporals → ISO-8601, UUID →
|
|
68
|
+
canonical hex) happens only at the serialization boundary, never inside the query path.
|
|
69
|
+
Compute with `Decimal` in-process; let the encoding layer emit the wire form. Currency is
|
|
70
|
+
integer minor units on the wire; the runtime never formats — formatting is client-side in
|
|
71
|
+
the universal (TS/Angular) web client. (Port-specific note: because pg8000 returns a plain
|
|
72
|
+
`int` for both INTEGER and BIGINT, `ObjectManager` exposes each query's per-column Postgres
|
|
73
|
+
OIDs via `last_column_oids` so the **canonical serializer** — the persistence-conformance
|
|
74
|
+
boundary — can apply the cross-port BIGINT→string rule. The generated FastAPI router serves
|
|
75
|
+
`field.long` as a JSON number, and the row *values* always stay native.)
|
|
76
|
+
|
|
77
|
+
## Serving the REST contract
|
|
78
|
+
|
|
79
|
+
The `routes` generator (run via `metaobjects gen`) emits a **FastAPI `APIRouter`** per
|
|
80
|
+
writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (five CRUD
|
|
81
|
+
endpoints, `?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1`
|
|
82
|
+
envelope, 400/404 envelopes). Each router declares a repository **`Protocol`** you
|
|
83
|
+
implement and inject via FastAPI's `app.dependency_overrides` — back it with
|
|
84
|
+
`ObjectManager`, or your own SQLAlchemy Core / asyncpg code. There is **no** generated
|
|
85
|
+
`main.py`; create one and mount the routers (`app.include_router(...)`). The same
|
|
86
|
+
universal TS/Angular web client consumes those routes unchanged.
|
|
87
|
+
|
|
88
|
+
Two shape notes (do **not** hand-edit generated files around them): the generated router
|
|
89
|
+
assumes a **single-field** PK (a composite PK takes `@fields[0]`), and the PK's type is
|
|
90
|
+
**derived from its field subtype** — a `field.uuid` PK binds `uuid.UUID` on the route and
|
|
91
|
+
Protocol, not `int`. The PATCH body is typed `dict[str, Any]` **deliberately**: the generated
|
|
92
|
+
`<Entity>Create` / `<Entity>Patch` Pydantic models already validate constraints over HTTP
|
|
93
|
+
(FR-036), and the `dict` seam preserves the FR-035 present-key PATCH tristate (absent ≠
|
|
94
|
+
explicit-null) — retyping the PATCH parameter to the model would collapse that tristate.
|
|
@@ -19,10 +19,21 @@ metadata that should define it. The ones a developer must actively guard:
|
|
|
19
19
|
- **DB-vs-metadata** — the live database schema has diverged from the metadata
|
|
20
20
|
(a column the metadata no longer declares, a missing index, a type mismatch). A
|
|
21
21
|
**modeled projection's** view body is compared too — a changed `CREATE VIEW` emits
|
|
22
|
-
a `replace-view`.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
a `replace-view`. For a genuinely irreducible view body (recursive CTE, window
|
|
23
|
+
function, set operation) that `origin.*` can't express, use the `source.rdb`
|
|
24
|
+
`@sql` escape: a hand-written body the tool still registers, fingerprints, and
|
|
25
|
+
drift-checks — emitted verbatim instead of synthesized (adopt a pre-existing
|
|
26
|
+
hand-written view once with `meta migrate --allow adopt-view`). For a DB object
|
|
27
|
+
— view **or table** — owned entirely elsewhere (Flyway, a hand-migration,
|
|
28
|
+
another app's schema), mark it `@unmanaged: true`; `meta migrate` never
|
|
29
|
+
touches it and `verify --db` reports it as *external (declared)* rather than
|
|
30
|
+
silently. `@sql` and `@unmanaged` are mutually exclusive on one source. But a
|
|
31
|
+
hand-authored view carrying **neither** marker is *unmanaged by omission*
|
|
32
|
+
(reported as informational, never failed, never dropped) — so an **undeclared**
|
|
33
|
+
hand-written view standing in for an expressible `object.projection` is the one
|
|
34
|
+
drift class `verify --db` can't catch; the `metaobjects-audit` skill is the only
|
|
35
|
+
gate that sees it. See `references/migration.md` → "DDL-ownership escape
|
|
36
|
+
valves (`@sql` / `@unmanaged`) — #208" for the full contract.
|
|
26
37
|
- **Generated-vs-metadata (codegen)** — committed generated code no longer matches
|
|
27
38
|
what the current metadata would emit (someone edited a `@generated` file, or
|
|
28
39
|
forgot to regenerate after changing metadata).
|
|
@@ -49,16 +60,24 @@ it and call the generated query/field instead of keeping the hand-rolled version
|
|
|
49
60
|
This is the most common way a build ends up *declaring* a projection yet still
|
|
50
61
|
hand-aggregating in a route — verify catches exactly that.
|
|
51
62
|
|
|
63
|
+
**A bare `verify` is a partial check, not the full gate.** The Node/C# default runs
|
|
64
|
+
only `--templates`; Java/Python's bare default runs only `--codegen` — either way,
|
|
65
|
+
paired with the advisory anti-pattern pass above, never all three subverbs. Treat a
|
|
66
|
+
bare run as a smoke test: the real done-check is running the subverbs your project
|
|
67
|
+
uses explicitly — `verify --codegen`, and, where a DB exists, `verify --db <url>`.
|
|
68
|
+
|
|
52
69
|
## The `verify` subverbs
|
|
53
70
|
|
|
54
71
|
`verify` has three drift checks. Run them in CI.
|
|
55
72
|
|
|
56
73
|
- **`--db`** — schema drift. Introspects the live database and fails if it has
|
|
57
74
|
diverged from metadata. This is a **schema concern, so it is the Node toolchain's
|
|
58
|
-
job regardless of your server language** (see migrations below).
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
job regardless of your server language** (see migrations below). The JVM ports
|
|
76
|
+
have no schema surface of their own — ADR-0015 Decision 2 removed the old
|
|
77
|
+
dev/test auto-create validator (OMDB is pure data-access) — so a JVM, Python, or
|
|
78
|
+
C# project's dev/test databases are provisioned the same way production is: by
|
|
79
|
+
applying the Node-emitted SQL. The Node `verify --db` is the only DB-vs-metadata
|
|
80
|
+
gate, for every port.
|
|
62
81
|
|
|
63
82
|
- **`--codegen`** — regeneration drift. Re-runs generation and diffs the result
|
|
64
83
|
against the committed generated files; a non-empty diff means someone edited
|
|
@@ -70,6 +89,20 @@ hand-aggregating in a route — verify catches exactly that.
|
|
|
70
89
|
if any reference isn't on the payload VO. This is the build-time gate for the
|
|
71
90
|
prompt-construction pillar.
|
|
72
91
|
|
|
92
|
+
**Only `--db` is Node-universal.** `--codegen` / `--templates` run through each
|
|
93
|
+
port's own build tool, not the Node `meta`:
|
|
94
|
+
|
|
95
|
+
| Port | Codegen drift | Template drift | Schema drift |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| TS | `meta verify --codegen` | `meta verify --templates` | `meta verify --db <url>` |
|
|
98
|
+
| Java / Kotlin | `mvn metaobjects:verify -Dmeta.verify.mode=codegen` | `mvn metaobjects:verify -Dmeta.verify.mode=templates` | Node `meta verify --db` only |
|
|
99
|
+
| C# | `dotnet meta verify --codegen` | `dotnet meta verify --templates` | Node `meta verify --db` only |
|
|
100
|
+
| Python | `metaobjects verify --codegen` | `metaobjects verify --templates` | Node `meta verify --db` only |
|
|
101
|
+
|
|
102
|
+
Every non-TS port's `verify` rejects `--db` outright (exit 2, "schema verify is the
|
|
103
|
+
migrate engine") — schema drift always runs through the Node `meta verify --db`,
|
|
104
|
+
per the shared-migration-engine doctrine below.
|
|
105
|
+
|
|
73
106
|
A clean run is silent; a failure names the entity/template, the drifted artifact,
|
|
74
107
|
and (for templates) the missing reference. **Bias toward trusting the tool** — a
|
|
75
108
|
verify failure almost always means the metadata changed and a derived artifact
|
|
@@ -121,9 +154,11 @@ What this means in practice:
|
|
|
121
154
|
|
|
122
155
|
- Dialects: `postgres` (default), `sqlite`, and `d1` (Cloudflare D1, TS-only).
|
|
123
156
|
- The JVM and Python ports have **no** migration command of their own — their
|
|
124
|
-
former migrate goals/modules were removed
|
|
125
|
-
dev/test
|
|
126
|
-
|
|
157
|
+
former migrate goals/modules were removed, and (ADR-0015 Decision 2) the JVM
|
|
158
|
+
runtime's own dev/test schema auto-create path
|
|
159
|
+
(`MetaClassDBValidatorService` + the drivers' DDL) was removed too: OMDB is
|
|
160
|
+
pure data-access. Every port's schema — dev, test, and production alike — is
|
|
161
|
+
always the Node migrate engine's output.
|
|
127
162
|
|
|
128
163
|
So even in a Java or Python or C# project, schema migration and `verify --db` run
|
|
129
164
|
through the Node `meta` tool. The per-port `gen`/codegen tooling stays native to
|
|
@@ -136,9 +171,9 @@ database by hand** — no `psql`/console `ALTER TABLE` / `CREATE` / `DROP`, not
|
|
|
136
171
|
to patch a mismatch, not to "just unblock" a boot. It is the single most common way a database ends up
|
|
137
172
|
in a state no migration can reproduce:
|
|
138
173
|
|
|
139
|
-
- The column now exists but no migration recorded it, so the next `meta migrate`
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
- The column now exists but no migration recorded it, so the next `meta migrate` tries to add it
|
|
175
|
+
again and dies on `column ... already exists` — or worse, silently diverges and the drift only
|
|
176
|
+
surfaces days later.
|
|
142
177
|
- "I'll just add it real quick so I can see it in the tool" is the exact rationalization to catch. It
|
|
143
178
|
doesn't *feel* like a schema change, so it skips the metadata-first check — but it is one.
|
|
144
179
|
|
|
@@ -159,7 +194,7 @@ render, persistence, API-contract, verify). When a test or conformance fixture
|
|
|
159
194
|
fails:
|
|
160
195
|
|
|
161
196
|
- A **loader** failure cites an `ERR_*` code (e.g. `ERR_RESERVED_ATTR`,
|
|
162
|
-
`
|
|
197
|
+
`ERR_UNRESOLVED_SUPER`, `ERR_MISSING_REQUIRED_ATTR`, `ERR_BAD_ATTR_VALUE`,
|
|
163
198
|
`ERR_YAML_COERCION`) — fix the metadata, not the loader.
|
|
164
199
|
- A **render/verify** failure means the rendered bytes or the template-drift
|
|
165
200
|
result diverged from the pinned expectation — usually a payload/text mismatch.
|
|
@@ -4,9 +4,11 @@ Schema migration is owned by **one shared TypeScript engine** regardless of your
|
|
|
4
4
|
server language (ADR-0015). The Node `meta` CLI (`@metaobjectsdev/cli`, on top of
|
|
5
5
|
`@metaobjectsdev/migrate-ts`) is the migration + live-DB-drift toolchain for **TS,
|
|
6
6
|
Java, Kotlin, C#, and Python alike**. The non-TS ports have **no** migration command
|
|
7
|
-
of their own — their former migrate goals/modules were removed
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
of their own — their former migrate goals/modules were removed, and (ADR-0015
|
|
8
|
+
Decision 2) the JVM runtime's own dev/test schema auto-create path
|
|
9
|
+
(`MetaClassDBValidatorService` + the drivers' DDL) was removed too: OMDB is pure
|
|
10
|
+
data-access. Every port's schema — dev, test, and production alike — is always the
|
|
11
|
+
Node migrate engine's output.
|
|
10
12
|
|
|
11
13
|
So even in a Java / Python / C# / Kotlin project you run `meta migrate` and
|
|
12
14
|
`meta verify --db` through Node. Only schema crosses to Node; per-port `gen`/codegen
|
|
@@ -78,8 +80,9 @@ next-step hint pointing to the exact `baseline` command.
|
|
|
78
80
|
a ledger table:
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
|
-
meta migrate --db postgresql://... --apply
|
|
82
|
-
meta migrate --db postgresql://... --rollback # run down.sql for
|
|
83
|
+
meta migrate --db postgresql://... --apply # run pending up.sql
|
|
84
|
+
meta migrate --db postgresql://... --rollback <target> # run down.sql for migrations newer than <target>
|
|
85
|
+
meta migrate --db postgresql://... --rollback "" # roll back everything (empty target)
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
## Dialects
|
|
@@ -95,9 +98,9 @@ next-step hint pointing to the exact `baseline` command.
|
|
|
95
98
|
`meta verify --db` introspects the live database and fails if its schema has
|
|
96
99
|
diverged from the metadata (a column the metadata no longer declares, a missing
|
|
97
100
|
index, a type mismatch). This is the **authoritative** DB-vs-metadata gate for every
|
|
98
|
-
port — wire it into CI.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
port — wire it into CI. The JVM ports have no runtime schema-validation surface of
|
|
102
|
+
their own (ADR-0015 Decision 2 removed it); the Node `meta verify --db` is the only
|
|
103
|
+
gate that owns DB drift, for every port.
|
|
101
104
|
|
|
102
105
|
A clean run is silent; a failure names the drifted table/column. Bias toward
|
|
103
106
|
trusting the tool — a drift failure almost always means the metadata changed and the
|
|
@@ -129,8 +132,10 @@ A non-unique recency index is `index.lookup`:
|
|
|
129
132
|
|
|
130
133
|
The `@where` / `@using` / `@expr` / `@orders` attributes are **index** physical
|
|
131
134
|
escapes on `identity.secondary` / `index.lookup` — they are NOT a raw-SQL escape
|
|
132
|
-
hatch for views.
|
|
133
|
-
|
|
135
|
+
hatch for views. For a genuinely-irreducible view body, use the `source.rdb`
|
|
136
|
+
**`@sql`** escape (a tool-managed, opaque hand-written body — see the
|
|
137
|
+
"DDL-ownership escape valves" section below); for a DB object owned entirely by
|
|
138
|
+
Flyway / a hand-migration, use **`@unmanaged`**.
|
|
134
139
|
|
|
135
140
|
## Projection views (generated view DDL)
|
|
136
141
|
|
|
@@ -151,10 +156,58 @@ canonical view-SQL emitter shared with drift detection.
|
|
|
151
156
|
`metaobjects-audit` skill, not here.
|
|
152
157
|
|
|
153
158
|
**Do not hand-author view SQL for a shape origins can express** — model it as a
|
|
154
|
-
projection so the view DDL is generated and drift-checked.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
projection so the view DDL is generated and drift-checked. For a genuinely
|
|
160
|
+
irreducible view (recursive CTE, window function, set operation) that origins
|
|
161
|
+
can't express, use the `@sql` escape below rather than a hand-edited migration
|
|
162
|
+
file — that keeps the view tool-managed (emitted, fingerprinted, drift-checked)
|
|
163
|
+
instead of accidentally unmanaged.
|
|
164
|
+
|
|
165
|
+
## DDL-ownership escape valves (`@sql` / `@unmanaged`) — #208
|
|
166
|
+
|
|
167
|
+
Two mutually-exclusive `source.rdb` attributes express *who owns a DB object's
|
|
168
|
+
DDL* (ADR-0043). They are the escape from "a projection's view is always
|
|
169
|
+
synthesized from its `origin.*` children."
|
|
170
|
+
|
|
171
|
+
**`@sql`** — a hand-written view body the tool **registers, fingerprints, and
|
|
172
|
+
drift-checks but never authors or parses**. The value is the body *inside*
|
|
173
|
+
`CREATE VIEW <name> AS …` (not the `CREATE` wrapper, not the name). Legal only on
|
|
174
|
+
a read-only kind; v1 migrate lowers it on `@kind: view` only (matview/proc → a
|
|
175
|
+
hard error, deferred). Authored sigil-free in YAML as a block scalar:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
object.projection:
|
|
179
|
+
name: OrgTree
|
|
180
|
+
children:
|
|
181
|
+
- source.rdb:
|
|
182
|
+
kind: view
|
|
183
|
+
view: v_org_tree
|
|
184
|
+
sql: |
|
|
185
|
+
WITH RECURSIVE t AS (
|
|
186
|
+
SELECT id, parent_id FROM org WHERE parent_id IS NULL
|
|
187
|
+
UNION ALL SELECT o.id, o.parent_id FROM org o JOIN t ON o.parent_id = t.id)
|
|
188
|
+
SELECT * FROM t
|
|
189
|
+
- field.long: { name: id, extends: Org.id }
|
|
190
|
+
- identity.primary: { extends: Org.pk }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `extends`-bound identity/fields declare the read model's shape and row
|
|
194
|
+
identity *without* triggering wrong synthesis (the suppression rule). The view is
|
|
195
|
+
emitted verbatim with a fingerprint COMMENT stamp; a second `meta migrate` is a
|
|
196
|
+
no-op. **Adopting a pre-existing hand-written view** at that name: the first diff
|
|
197
|
+
reports `replace-view` **blocked** (an unstamped view is indistinguishable from
|
|
198
|
+
someone else's SQL) — run **`meta migrate --allow adopt-view`** once to stamp it,
|
|
199
|
+
then it converges. `@sql` **forbids** `origin.*` children and a `@filter` on the
|
|
200
|
+
same host (two sources of truth → load error).
|
|
201
|
+
|
|
202
|
+
**`@unmanaged: true`** — this DB object (a view **or a table**) is managed
|
|
203
|
+
elsewhere (Flyway / a hand-migration owns its DDL). `meta migrate` never creates,
|
|
204
|
+
drops, or drift-checks it; `meta verify --db` reports it as *external (declared)*.
|
|
205
|
+
Legal on any `@kind`, including `table` (the Flyway-owned-entity case). An FK from
|
|
206
|
+
a managed table into an `@unmanaged` table resolves its physical name, but the
|
|
207
|
+
external object must exist before that FK is applied (a documented ordering caveat,
|
|
208
|
+
not enforced).
|
|
209
|
+
|
|
210
|
+
`@sql` and `@unmanaged` are **mutually exclusive** on one source.
|
|
158
211
|
|
|
159
212
|
## Adopting an existing database (non-destructive)
|
|
160
213
|
|