Skip to content

Commit c2eb915

Browse files
dmealingclaude
andcommitted
Merge: cross-port generator stable-name conformance (ADR-0021 D3 fan-out)
Stabilizes generator names across all 5 ports + adds a conformance gate: - Canonical manifest fixtures/generator-registry-conformance/registry.json is the single source of truth for generator stable names (concept + tier + which ports expose each). Shared concepts (entity, routes, output-parser, output-prompt, render-helper, extractor, template, filter-allowlist, payload) are now spelled IDENTICALLY in every port. - Each port gained a generator registry conformant to the manifest: TS + C# (registries pre-existed; added conformance tests), and NEW registries for Java (11), Kotlin (13, no template), Python (9). Python also got metaobjects gen --list + --generators selection (the 5th-port CLI parity). - Every port's conformance test asserts bidirectional set-equality + tier against the one manifest, so any rogue/missing/misspelled generator name fails CI. - CI wired to run the new conformance tests in the C#/Java/Kotlin jobs (TS+Python already run whole suites/dirs) — the gate actually fires. Maven/dotnet selection-by-stable-name (vs the registry powering identity + the gate) remains a staged follow-on. No generated-output change; per-port emission and default suites unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents 0e96fa5 + 01b7872 commit c2eb915

13 files changed

Lines changed: 1374 additions & 11 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ jobs:
6666
dotnet test MetaObjects.Render.Tests/MetaObjects.Render.Tests.csproj --nologo --verbosity quiet
6767
# Validator-parity corpus (generated DataAnnotations input validation).
6868
dotnet test MetaObjects.Codegen.Tests/MetaObjects.Codegen.Tests.csproj --filter "FullyQualifiedName~ValidationConformance" --nologo --verbosity quiet
69+
# Generator stable-name registry conformance (ADR-0021 D3).
70+
dotnet test MetaObjects.Codegen.Tests/MetaObjects.Codegen.Tests.csproj --filter "FullyQualifiedName~GeneratorRegistryConformance" --nologo --verbosity quiet
6971
7072
- name: Java conformance
7173
if: matrix.language == 'java'
@@ -79,8 +81,9 @@ jobs:
7981
mvn -pl metadata test -Dtest='ConformanceTest,YamlConformanceTest,ObjectModelConformanceTest' -q
8082
# Byte-exact render / verify / extract / output-prompt corpora (render module).
8183
mvn -pl render test -Dtest='RenderCrossPortReportTest,VerifyConformanceTest,ExtractConformanceTest,OutputPromptConformanceTest' -q
82-
# Validator-parity corpus (generated DTO jakarta.validation, codegen-spring module).
83-
mvn -pl codegen-spring test -Dtest='ValidationConformanceTest' -q
84+
# Validator-parity corpus (generated DTO jakarta.validation, codegen-spring module)
85+
# + generator stable-name registry conformance (ADR-0021 D3).
86+
mvn -pl codegen-spring test -Dtest='ValidationConformanceTest,GeneratorRegistryConformanceTest' -q
8487
8588
- name: Python conformance
8689
if: matrix.language == 'python'
@@ -92,6 +95,8 @@ jobs:
9295
uv run pytest tests/render -q
9396
# Validator-parity corpus (generated Pydantic input validation).
9497
uv run pytest tests/codegen/test_validation_conformance.py -q
98+
# Generator registry CLI ergonomics (gen --list / --generators selection, ADR-0021 D3).
99+
uv run pytest tests/codegen/test_cli_registry.py -q
95100
96101
conformance-kotlin:
97102
# Kotlin (codegen-kotlin, KotlinPoet on the JVM) ships no loader / YAML /
@@ -116,8 +121,9 @@ jobs:
116121
# SNAPSHOT-resolution race when the test module references freshly-built
117122
# artifacts.
118123
mvn -pl codegen-kotlin -am install -DskipTests -q
119-
# object-model + output-prompt + validator-parity corpora (the corpora Kotlin has).
120-
mvn -pl codegen-kotlin test -Dtest='ObjectModelConformanceTest,OutputPromptConformanceTest,ValidationConformanceTest' -q
124+
# object-model + output-prompt + validator-parity corpora (the corpora Kotlin has)
125+
# + generator stable-name registry conformance (ADR-0021 D3).
126+
mvn -pl codegen-kotlin test -Dtest='ObjectModelConformanceTest,OutputPromptConformanceTest,ValidationConformanceTest,GeneratorRegistryConformanceTest' -q
121127
122128
completeness-gate:
123129
needs: [conformance, conformance-kotlin]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# generator-registry conformance
2+
3+
The canonical cross-port manifest of **stable generator names** (ADR-0021 D3).
4+
`registry.json` is the single source of truth; every port's generator registry is
5+
conformance-tested against it.
6+
7+
## The contract each port's test enforces
8+
9+
Given the port's own generator registry (stable-name → generator), the port's
10+
conformance test asserts:
11+
12+
1. **No rogue names** — every stable name the port registers appears in
13+
`registry.json`.
14+
2. **Presence both ways** — for every manifest entry whose `ports` array includes
15+
this port, the port's registry exposes that name; and the port does **not**
16+
expose a name whose `ports` array omits it.
17+
3. **Tier agreement** — a name marked `tier: "neutral"` is flagged neutral in the
18+
port (owned by `meta docs`, not the recommended native suite).
19+
20+
Because all five ports validate against this one file, a **shared concept is
21+
spelled identically everywhere** (e.g. the REST surface is `routes` in every
22+
port, never `controller`/`router`). That cross-port spelling stability is the
23+
whole point.
24+
25+
## Shared (cross-port) names
26+
27+
These concepts MUST use the same stable name wherever a port implements them:
28+
`entity`, `routes`, `output-parser`, `output-prompt`, `render-helper`,
29+
`extractor`, `template`, `filter-allowlist`, `payload`.
30+
31+
## Changing the surface
32+
33+
Adding, removing, or renaming a generator means editing **both** `registry.json`
34+
**and** the port's registry in the same change — the conformance gate fails on
35+
any drift (a typo'd name, a missing registration, a port that quietly diverges).
36+
This is the mechanism that keeps the codegen surface coherent as it grows.
37+
38+
## Port ids
39+
40+
`typescript`, `csharp`, `java`, `kotlin`, `python`.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
{
2+
"$comment": "CANONICAL generator stable-name manifest (ADR-0021 D3). The single cross-port source of truth for generator stable names. Every port's generator registry is conformance-tested against this file: (1) every generator a port exposes MUST use a name listed here; (2) for each generator, the `ports` array lists exactly the ports expected to expose it (presence is checked both ways); (3) because all ports validate against this one file, a shared concept (e.g. `routes`) is spelled identically everywhere. Port ids: typescript, csharp, java, kotlin, python. `tier`: native (idiomatic per-port code) or neutral (Tier-2, owned by `meta docs`). Adding/renaming a generator = edit THIS file + the port registry together; the conformance gate fails otherwise.",
3+
"ports": ["typescript", "csharp", "java", "kotlin", "python"],
4+
"generators": {
5+
"entity": {
6+
"concept": "Per-entity model/class — the entity module (table-backed or value object).",
7+
"tier": "native",
8+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
9+
},
10+
"routes": {
11+
"concept": "Per-entity REST endpoint surface (controllers / routes / router).",
12+
"tier": "native",
13+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
14+
},
15+
"output-parser": {
16+
"concept": "Per-template tolerant output parser (recover-on-receipt).",
17+
"tier": "native",
18+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
19+
},
20+
"output-prompt": {
21+
"concept": "Per-template output-format prompt fragment generator.",
22+
"tier": "native",
23+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
24+
},
25+
"render-helper": {
26+
"concept": "Per-template.output render helper (document/email typed wrappers).",
27+
"tier": "native",
28+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
29+
},
30+
"extractor": {
31+
"concept": "Per-template strict typed extract<Name> helper (strict payload extraction).",
32+
"tier": "native",
33+
"ports": ["typescript", "csharp", "java", "kotlin", "python"]
34+
},
35+
"template": {
36+
"concept": "Generic Mustache template primitive (walk + template -> files).",
37+
"tier": "native",
38+
"ports": ["typescript", "csharp", "java", "python"]
39+
},
40+
"filter-allowlist": {
41+
"concept": "Per-entity REST filter allowlist (queryable-field guard).",
42+
"tier": "native",
43+
"ports": ["csharp", "java", "kotlin", "python"]
44+
},
45+
"payload": {
46+
"concept": "Per-template payload value object (the strict payload type).",
47+
"tier": "native",
48+
"ports": ["java", "kotlin", "python"]
49+
},
50+
"queries": {
51+
"concept": "Per-entity typed query helpers (findById/create/...).",
52+
"tier": "native",
53+
"ports": ["typescript"]
54+
},
55+
"callable": {
56+
"concept": "Per-entity callable/service surface wrapping the query helpers.",
57+
"tier": "native",
58+
"ports": ["typescript"]
59+
},
60+
"routes-hono": {
61+
"concept": "Per-entity Hono CRUD routes.",
62+
"tier": "native",
63+
"ports": ["typescript"]
64+
},
65+
"barrel": {
66+
"concept": "Single index module re-exporting every generated entity module.",
67+
"tier": "native",
68+
"ports": ["typescript"]
69+
},
70+
"prompt-render": {
71+
"concept": "Per-template prompt-render helper over the render engine.",
72+
"tier": "native",
73+
"ports": ["typescript"]
74+
},
75+
"db-context": {
76+
"concept": "Single EF Core DbContext binding every generated entity.",
77+
"tier": "native",
78+
"ports": ["csharp"]
79+
},
80+
"repository": {
81+
"concept": "Per-entity Spring Data repository.",
82+
"tier": "native",
83+
"ports": ["java"]
84+
},
85+
"dto": {
86+
"concept": "Per-entity Spring DTO.",
87+
"tier": "native",
88+
"ports": ["java"]
89+
},
90+
"exposed-table": {
91+
"concept": "Per-entity Kotlin Exposed table object.",
92+
"tier": "native",
93+
"ports": ["kotlin"]
94+
},
95+
"relations": {
96+
"concept": "Cross-entity relationship helpers.",
97+
"tier": "native",
98+
"ports": ["kotlin"]
99+
},
100+
"spring-config": {
101+
"concept": "Spring wiring/configuration for the generated surface.",
102+
"tier": "native",
103+
"ports": ["kotlin"]
104+
},
105+
"stored-proc": {
106+
"concept": "Stored-procedure binding helpers.",
107+
"tier": "native",
108+
"ports": ["kotlin"]
109+
},
110+
"validator": {
111+
"concept": "Per-entity input validator.",
112+
"tier": "native",
113+
"ports": ["kotlin"]
114+
},
115+
"docs": {
116+
"concept": "Neutral per-entity / per-template Markdown documentation pages.",
117+
"tier": "neutral",
118+
"note": "Owned by `meta docs` (the single docs door, ADR-0021 D1); not part of the recommended native suite.",
119+
"ports": ["typescript"]
120+
},
121+
"mermaid-er": {
122+
"concept": "Neutral Mermaid ER diagram of the entity/relationship model.",
123+
"tier": "neutral",
124+
"note": "Neutral artifact owned by the docs engine (ADR-0020); surfaced via `meta docs`.",
125+
"ports": ["typescript"]
126+
}
127+
}
128+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Text.Json;
2+
using MetaObjects.Codegen;
3+
using Xunit;
4+
5+
namespace MetaObjects.Codegen.Tests;
6+
7+
/// <summary>
8+
/// Cross-port conformance for the C# generator registry (ADR-0021 D3). Validates
9+
/// <see cref="GeneratorRegistry"/> against the CANONICAL stable-name manifest at
10+
/// <c>fixtures/generator-registry-conformance/registry.json</c> — the single
11+
/// cross-port source of truth every port's registry is checked against.
12+
///
13+
/// Contract for the <c>csharp</c> port:
14+
/// (1) every stable name the C# registry exposes appears in the manifest;
15+
/// (2) presence both ways — every manifest entry whose <c>ports</c> includes
16+
/// <c>csharp</c> IS in the C# registry, and the C# registry exposes NO name
17+
/// whose manifest <c>ports</c> omits <c>csharp</c> (i.e. the two sets are EQUAL);
18+
/// (3) tier agreement — every native manifest name is non-neutral in the registry
19+
/// (C# has no neutral generators per the manifest).
20+
///
21+
/// On mismatch this REPORTS the exact diff (extras / missing / tier) so the manifest
22+
/// and registry can be reconciled. It never mutates either.
23+
/// </summary>
24+
public sealed class GeneratorRegistryConformanceTests
25+
{
26+
private const string Port = "csharp";
27+
28+
// Walk upward from the test assembly to the repo root (contains the manifest dir),
29+
// mirroring RenderHelperConformanceTests.Corpus().
30+
private static string ManifestPath()
31+
{
32+
var dir = AppContext.BaseDirectory;
33+
while (dir is not null &&
34+
!Directory.Exists(Path.Combine(dir, "fixtures", "generator-registry-conformance")))
35+
dir = Directory.GetParent(dir)?.FullName;
36+
if (dir is null)
37+
throw new InvalidOperationException(
38+
"fixtures/generator-registry-conformance not found walking up from " + AppContext.BaseDirectory);
39+
return Path.Combine(dir, "fixtures", "generator-registry-conformance", "registry.json");
40+
}
41+
42+
private sealed record ManifestEntry(string Name, string Tier, IReadOnlyList<string> Ports);
43+
44+
private static IReadOnlyList<ManifestEntry> LoadManifest()
45+
{
46+
using var doc = JsonDocument.Parse(File.ReadAllText(ManifestPath()));
47+
var generators = doc.RootElement.GetProperty("generators");
48+
var entries = new List<ManifestEntry>();
49+
foreach (var prop in generators.EnumerateObject())
50+
{
51+
var tier = prop.Value.GetProperty("tier").GetString()!;
52+
var ports = prop.Value.GetProperty("ports").EnumerateArray()
53+
.Select(p => p.GetString()!).ToList();
54+
entries.Add(new ManifestEntry(prop.Name, tier, ports));
55+
}
56+
return entries;
57+
}
58+
59+
/// <summary>(1)+(2): the manifest's csharp slice equals the C# registry's stable names.</summary>
60+
[Fact]
61+
public void RegistryStableNames_EqualManifestCsharpSlice()
62+
{
63+
var manifest = LoadManifest();
64+
65+
var expected = manifest
66+
.Where(e => e.Ports.Contains(Port))
67+
.Select(e => e.Name)
68+
.ToHashSet(StringComparer.Ordinal);
69+
70+
var actual = GeneratorRegistry.Entries.Keys.ToHashSet(StringComparer.Ordinal);
71+
72+
var missing = expected.Except(actual).OrderBy(n => n, StringComparer.Ordinal).ToList();
73+
var extras = actual.Except(expected).OrderBy(n => n, StringComparer.Ordinal).ToList();
74+
75+
Assert.True(
76+
missing.Count == 0 && extras.Count == 0,
77+
$"C# generator registry disagrees with the canonical manifest's `{Port}` slice " +
78+
$"(fixtures/generator-registry-conformance/registry.json).\n" +
79+
$" MISSING (manifest says `{Port}` exposes it, but the registry does not): " +
80+
$"[{string.Join(", ", missing)}]\n" +
81+
$" EXTRA (registry exposes it, but the manifest omits `{Port}` for it): " +
82+
$"[{string.Join(", ", extras)}]\n" +
83+
"Reconcile by editing the manifest AND the registry together — do not force one to match.");
84+
}
85+
86+
/// <summary>(3): every native manifest name is non-neutral (Native) in the C# registry.</summary>
87+
[Fact]
88+
public void TierAgreement_NativeManifestNames_AreNativeInRegistry()
89+
{
90+
var manifest = LoadManifest();
91+
92+
var tierMismatches = manifest
93+
.Where(e => e.Ports.Contains(Port))
94+
.Where(e => GeneratorRegistry.Entries.TryGetValue(e.Name, out var reg)
95+
&& !TierMatches(e.Tier, reg.Tier))
96+
.Select(e =>
97+
$"{e.Name}: manifest tier=`{e.Tier}` but registry tier=`{GeneratorRegistry.Entries[e.Name].Tier}`")
98+
.ToList();
99+
100+
Assert.True(
101+
tierMismatches.Count == 0,
102+
"C# generator registry tier disagrees with the canonical manifest:\n " +
103+
string.Join("\n ", tierMismatches));
104+
}
105+
106+
private static bool TierMatches(string manifestTier, GeneratorTier registryTier) =>
107+
manifestTier switch
108+
{
109+
"native" => registryTier == GeneratorTier.Native,
110+
"neutral" => registryTier == GeneratorTier.Neutral,
111+
_ => false,
112+
};
113+
}

0 commit comments

Comments
 (0)