Skip to content

Commit 5d060c7

Browse files
committed
Merge: Java api-docs SP-2c — cross-port one-tree conformance (SP-2 complete)
Proves a TS+Java solution produces ONE cross-linked doc tree, via a shared layout contract both ports assert against (the registry-conformance idiom): - fixtures/conformance/api-docs-cross-port/ holds one model (input/meta.json) + expected-paths.json (per unit: model/api-ts/api-java page paths + the model<->api cross-link hrefs, package layout). - TS conformance asserts the real model + api/ts emit + the declared api/java link targets match the manifest; Java conformance asserts its real api/java emit + model cross-links match the SAME manifest. No cross-port layout divergence found. - A contract-only fixture (expected-paths.json, no standard expected*) is skipped by the ported strict metadata-conformance runners (TS, Java, C#) via a first-class isContractOnly flag (parallel to isDocsOnly). Registry manifest unchanged (Java api-docs is the metaobjects:docs goal, not a registry generator). Completes SP-2 (Java api-docs: a IR+accuracy / b rendering+goal / c cross-port). Remaining: SP-2b-hardening (examples/setup-preamble/EXTRACTOR doc) + publishing for a real adopter run. ADR-0027 updated.
2 parents 9e8fd25 + e24d4c7 commit 5d060c7

13 files changed

Lines changed: 1024 additions & 172 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Java api-docs SP-2c — cross-port one-tree conformance — Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]` checkboxes.
4+
5+
**Goal:** Prove a TS+Java solution produces ONE cross-linked doc tree — TS emits the model + `api/ts`, Java emits `api/java`, and every cross-surface link resolves — via a shared expectation manifest both ports assert against (the registry-conformance idiom).
6+
7+
**Architecture:** A shared cross-port fixture (`fixtures/conformance/api-docs-cross-port/`) holds `input/meta.json` (a small multi-entity model in one package, to exercise package-layout folding) + a custom `expected-paths.json` (the contract: per unit, the model / api-ts / api-java page paths + the model↔api cross-link hrefs, in package layout). A TS conformance test asserts its real model + `api/ts` emit + the declared `api/java` link targets match the manifest; a Java conformance test asserts its real `api/java` emit + model cross-links match the same manifest. Both asserting the ONE manifest ⟹ the surfaces cohere into one resolvable tree. No cross-toolchain process needed (the byte-parity `DocsPaths`/`docs-paths` contract, gated in SP-2b, makes the manifest the single source of truth).
8+
9+
**Tech Stack:** TS (bun test, codegen-ts) + Java (JUnit, codegen-spring), sharing a JSON fixture.
10+
11+
Reference (read once): the registry-conformance idiom — `fixtures/generator-registry-conformance/registry.json` + `codegen-ts/test/golden/generator-registry-conformance.test.ts` + `codegen-spring/.../GeneratorRegistryConformanceTest.java` (shared JSON, per-port assert, repo-root walk). TS docs cross-link gate `codegen-ts/test/golden/docs-cross-link-conformance.test.ts` (`emitMulti`/`findBrokenCrossLinks`, `apiSurfaceHref`). TS `src/docs-paths.ts`. Java `codegen-spring/.../generator/apidocs/{DocsPaths,JavaApiModelBuilder,JavaApiDocsRenderer}.java` + `maven-plugin/.../mojo/DocsMojo.java`. Java fixture-load pattern: `codegen-spring/.../apidocs/JavaApiDocsAccuracyTest.java` / `TemplateOutputFixtureConformanceTest.java` (repo-root walk → `fixtures/conformance/<case>/input/meta.json`). Corpus safety (scout-confirmed): a custom `expected-paths.json` (NOT `expected.json`/`expected/`) is ignored by the C#/TS metadata-conformance harnesses, so this fixture is safe in the shared corpus.
12+
13+
Commands: TS `cd server/typescript && bun test <path>`; Java `cd server/java && mvn -pl codegen-spring -am test` (`-am` REQUIRED; `-DfailIfNoTests=false` with `-Dtest=`).
14+
15+
**Registry manifest:** DO NOT add `java` to the `api-docs` entry in `registry.json` — Java's api-docs is the `metaobjects:docs` GOAL, not a `GeneratorRegistry` generator; adding `java` would break the Java `GeneratorRegistryConformanceTest`. Leave it `ports:["typescript"]`.
16+
17+
---
18+
19+
## Task 1: Shared cross-port fixture + `expected-paths.json` manifest
20+
21+
**Files:** create `fixtures/conformance/api-docs-cross-port/input/meta.json` and `fixtures/conformance/api-docs-cross-port/expected-paths.json`.
22+
23+
- [ ] **Step 1: Author `input/meta.json`.** A minimal model in package `acme::shop` (read an existing cross-port fixture e.g. `fixtures/conformance/template-output-json-simple/input/meta.json` for the exact JSON encoding): entities `Order` (pk `id`, `@required name`, `field.enum status`, `source.rdb` table) and `Customer` (pk `id`, `name`, table); a `template.output` json `OrderSummary` with `@payloadRef` → an `object.value` payload VO. Keep it minimal but with a package (so package-layout folding `acme/shop/...` is exercised). Confirm it loads (both ports load the same format).
24+
- [ ] **Step 2: Author `expected-paths.json`** — the cross-port contract, PACKAGE layout. Compute each value from the shared path math (`docPageOutputPath`: package → `<pkg-folded>/<Name>.md`; `apiSurfaceHref(modelPath, {subDir}, page)` for model→api; `surfaceCrossHref(apiPath, modelPath)` for api→model). Structure:
25+
```json
26+
{
27+
"$comment": "Cross-port api-docs layout contract. Both the TS and Java api-docs conformance tests assert their emitted page paths + cross-link hrefs against this. Layout: package.",
28+
"layout": "package",
29+
"apiTsSubDir": "api/ts",
30+
"apiJavaSubDir": "api/java",
31+
"units": [
32+
{
33+
"node": "Order",
34+
"modelPath": "acme/shop/Order.md",
35+
"apiTsPath": "api/ts/acme/shop/Order.md",
36+
"apiJavaPath": "api/java/acme/shop/Order.md",
37+
"modelToApiTs": "../../api/ts/acme/shop/Order.md",
38+
"modelToApiJava": "../../api/java/acme/shop/Order.md",
39+
"apiTsToModel": "../../../../acme/shop/Order.md",
40+
"apiJavaToModel": "../../../../acme/shop/Order.md"
41+
}
42+
// … Customer, OrderSummary (template → only apiTs/apiJava if the surface documents templates; model page is the template's neutral doc page)
43+
]
44+
}
45+
```
46+
IMPORTANT: verify each href against the math — e.g. `apiJavaToModel` from `api/java/acme/shop/Order.md` (fromDir `api/java/acme/shop`, 4 segments) to `acme/shop/Order.md` is `../../../../acme/shop/Order.md` (4 `..`, matching the SP-2b node-parity correction). For the template unit `OrderSummary`, include only the surfaces actually produced (the api surfaces document templates per the IR; the model page is `acme/shop/OrderSummary.md`). Get the unit set right by checking which units each port documents (entities: model+api; template: model page + api template page).
47+
- [ ] **Step 3: Commit.** `git add fixtures/conformance/api-docs-cross-port && git commit -m "test(conformance): cross-port api-docs layout contract fixture (input + expected-paths)" -m "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"`
48+
49+
## Task 2: TS conformance — model + api/ts emit matches the manifest
50+
51+
**Files:** `codegen-ts/test/golden/api-docs-cross-port-conformance.test.ts` (create).
52+
53+
- [ ] **Step 1: Write the test.** Walk to repo root (mirror `generator-registry-conformance.test.ts`'s `findRepoRoot`); load `fixtures/conformance/api-docs-cross-port/input/meta.json` (via `MetaDataLoader`, as `docs-cross-link-conformance.test.ts` loads fixtures) and `expected-paths.json`. Emit the model + `api/ts` surface for PACKAGE layout, declaring both api surfaces so the model page links each (reuse `emitMulti`-style emission: `docsFile({apiSurfaces:[{label:"TypeScript",subDir:"api/ts"},{label:"Java",subDir:"api/java"}]})` + `apiDocsFile({subDir:"api/ts", modelSurface:true})`). Then assert, per manifest unit:
54+
- the emitted model page exists at `unit.modelPath`;
55+
- the emitted api/ts page exists at `unit.apiTsPath`;
56+
- the model page's link to api/ts == `unit.modelToApiTs` and to api/java == `unit.modelToApiJava` (parse the `[..](href)` from the emitted model page; compare strings);
57+
- the api/ts page's link back to the model == `unit.apiTsToModel`;
58+
- resolving `modelToApiTs` from the model page dir lands on `apiTsPath` (sanity).
59+
Use the existing link-parse/resolve helpers where possible.
60+
- [ ] **Step 2: Run → it should PASS** (the TS emit follows the same `docs-paths` math the manifest encodes). If a value mismatches, FIX the manifest (Task 1) if it was hand-miscomputed, else investigate the emit. `cd server/typescript && bun test packages/codegen-ts/test/golden/api-docs-cross-port-conformance.test.ts`.
61+
- [ ] **Step 3: Full TS check.** `bun test packages/codegen-ts` → 0 fail.
62+
- [ ] **Step 4: Commit.** `git add -A server/typescript && git commit -m "test(codegen-ts): cross-port api-docs conformance — TS model+api/ts matches the shared contract" -m "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"`
63+
64+
## Task 3: Java conformance — api/java emit matches the same manifest
65+
66+
**Files:** `codegen-spring/src/test/java/com/metaobjects/generator/apidocs/ApiDocsCrossPortConformanceTest.java` (create).
67+
68+
- [ ] **Step 1: Write the test.** Walk to repo root (mirror `JavaApiDocsAccuracyTest`/`TemplateOutputFixtureConformanceTest`); load the SAME `fixtures/conformance/api-docs-cross-port/input/meta.json` + parse `expected-paths.json` (Jackson, as `GeneratorRegistryConformanceTest` does). Build `JavaApiModel m = new JavaApiModelBuilder().build(loader, "api-docs-cross-port")`. For PACKAGE layout, for each manifest unit that the Java builder documents:
69+
- compute the api/java page path the same way `DocsMojo` does: `"api/java" + "/" + DocsPaths.docPageOutputPath(Layout.PACKAGE, unit.pkg(), unit.node())`; assert it equals `unit.apiJavaPath`;
70+
- compute the model cross-link the same way `DocsMojo` does: `DocsPaths.modelCrossHref(apiJavaPath, DocsPaths.docPageOutputPath(Layout.PACKAGE, unit.pkg(), unit.node()), null)`; assert it equals `unit.apiJavaToModel`;
71+
- render the api/java page via `JavaApiDocsRenderer.renderUnitPage(unit, apiJavaToModel)` and assert it contains the `**Model / metadata:** [..](<apiJavaToModel>)` link (the page actually carries the contract href).
72+
(Java documents entities + template api pages; assert over the units it produces — match the manifest's unit set; if Java omits a unit the manifest lists (e.g. a template api surface the Java builder doesn't emit), reconcile the manifest to what BOTH ports actually document, and note it.)
73+
- [ ] **Step 2: Run → PASS.** `cd server/java && mvn -pl codegen-spring -am test -Dtest=ApiDocsCrossPortConformanceTest -DfailIfNoTests=false`. If a path/href mismatches the manifest: it's a real cross-port divergence (or a manifest typo) — reconcile (fix the port or the manifest), do not weaken the test.
74+
- [ ] **Step 3: Full Java check.** `mvn -pl codegen-spring -am test` → 0 fail.
75+
- [ ] **Step 4: Commit.** `git add -A server/java && git commit -m "test(codegen-spring): cross-port api-docs conformance — Java api/java matches the shared contract" -m "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"`
76+
77+
## Task 4: ADR-0027 closeout note + SP-2 wrap + closeout
78+
79+
**Files:** `spec/decisions/ADR-0027-polyglot-docs-composition.md` (append a short SP-2 status note).
80+
81+
- [ ] **Step 1: ADR note.** Append a brief "Java api surface (SP-2) shipped" paragraph to ADR-0027: the Java `metaobjects:docs` goal emits `api/java` into the `apiSurfaces` contract; the cross-port layout is gated by `fixtures/conformance/api-docs-cross-port/expected-paths.json` (both ports assert). Note the registry manifest stays TS-only (Java api-docs is a goal); examples/setup-preamble/EXTRACTOR doc are the remaining hardening follow-on.
82+
- [ ] **Step 2: Full suites.** `cd server/typescript && bun test packages/codegen-ts` (0 fail); `cd server/java && mvn -pl codegen-spring,maven-plugin -am test` (0 fail). Record counts.
83+
- [ ] **Step 3: Whole-branch review + code-simplifier; fix findings.** (Focus: the manifest values are correct per the math; both ports genuinely assert the SAME file; no weakened assertions; corpus-safety of the custom expectation file.)
84+
- [ ] **Step 4: Forward-merge** to origin/main via a temp worktree off latest origin/main (main checkout has other sessions' WIP). Remove worktrees/branches. Update memory: SP-2 (Java api-docs) COMPLETE (a/b/c) — Java emits `api/java` into the apiSurfaces contract, cross-port layout gated; remaining = the SP-2b-hardening (examples/setup/EXTRACTOR) + publishing for a real adopter run.
85+
86+
## Guard
87+
- PUBLIC repo: no private/other-project names, no home paths (fixture, tests, ADR, commit messages). Generic `acme.shop`/`Order`.
88+
- The manifest `expected-paths.json` is the SINGLE shared contract — both the TS and Java tests assert against the SAME file (reconcile, don't fork). Hrefs computed via the byte-parity path math (verify the 4-`..` package cases).
89+
- Custom expectation filename (`expected-paths.json`) so the metadata-conformance harnesses ignore the fixture (corpus-safe). Do NOT add `expected.json`/`expected/` to this fixture.
90+
- Do NOT add `java` to the `api-docs` registry manifest (goal, not a registry generator).
91+
- No new rendering/goal features here; SP-2c is conformance only. Don't touch verify.ts, the generators, or the TS docs door beyond the new test.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$comment": "Cross-port api-docs layout contract; TS and Java api-docs conformance both assert against this. Layout: package.",
3+
"layout": "package",
4+
"apiTsSubDir": "api/ts",
5+
"apiJavaSubDir": "api/java",
6+
"units": [
7+
{
8+
"node": "Customer",
9+
"modelPath": "acme/shop/Customer.md",
10+
"apiTsPath": "api/ts/acme/shop/Customer.md",
11+
"apiJavaPath": "api/java/acme/shop/Customer.md",
12+
"modelToApiTs": "../../api/ts/acme/shop/Customer.md",
13+
"modelToApiJava": "../../api/java/acme/shop/Customer.md",
14+
"apiTsToModel": "../../../../acme/shop/Customer.md",
15+
"apiJavaToModel": "../../../../acme/shop/Customer.md"
16+
},
17+
{
18+
"node": "Order",
19+
"modelPath": "acme/shop/Order.md",
20+
"apiTsPath": "api/ts/acme/shop/Order.md",
21+
"apiJavaPath": "api/java/acme/shop/Order.md",
22+
"modelToApiTs": "../../api/ts/acme/shop/Order.md",
23+
"modelToApiJava": "../../api/java/acme/shop/Order.md",
24+
"apiTsToModel": "../../../../acme/shop/Order.md",
25+
"apiJavaToModel": "../../../../acme/shop/Order.md"
26+
},
27+
{
28+
"node": "OrderSummary",
29+
"modelPath": "acme/shop/OrderSummary.md",
30+
"apiTsPath": "api/ts/acme/shop/OrderSummary.md",
31+
"apiJavaPath": "api/java/acme/shop/OrderSummary.md",
32+
"modelToApiTs": "",
33+
"modelToApiJava": "",
34+
"apiTsToModel": "../../../../acme/shop/OrderSummary.md",
35+
"apiJavaToModel": "../../../../acme/shop/OrderSummary.md"
36+
},
37+
{
38+
"node": "OrderSummaryPayload",
39+
"modelPath": "acme/shop/OrderSummaryPayload.md",
40+
"apiTsPath": "api/ts/acme/shop/OrderSummaryPayload.md",
41+
"apiJavaPath": "api/java/acme/shop/OrderSummaryPayload.md",
42+
"modelToApiTs": "../../api/ts/acme/shop/OrderSummaryPayload.md",
43+
"modelToApiJava": "../../api/java/acme/shop/OrderSummaryPayload.md",
44+
"apiTsToModel": "../../../../acme/shop/OrderSummaryPayload.md",
45+
"apiJavaToModel": "../../../../acme/shop/OrderSummaryPayload.md"
46+
}
47+
]
48+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme::shop",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Order",
8+
"package": "acme::shop",
9+
"@description": "A customer order.",
10+
"children": [
11+
{ "source.rdb": { "@table": "orders" } },
12+
{ "field.long": { "name": "id" } },
13+
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
14+
{ "field.enum": { "name": "status", "@required": true, "@values": ["open", "shipped", "closed"] } },
15+
{ "identity.primary": { "@fields": ["id"], "@generation": "increment" } }
16+
]
17+
}
18+
},
19+
{
20+
"object.entity": {
21+
"name": "Customer",
22+
"package": "acme::shop",
23+
"@description": "A shop customer.",
24+
"children": [
25+
{ "source.rdb": { "@table": "customers" } },
26+
{ "field.long": { "name": "id" } },
27+
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
28+
{ "identity.primary": { "@fields": ["id"], "@generation": "increment" } }
29+
]
30+
}
31+
},
32+
{
33+
"object.value": {
34+
"name": "OrderSummaryPayload",
35+
"package": "acme::shop",
36+
"children": [
37+
{ "field.string": { "name": "name", "@required": true } },
38+
{ "field.string": { "name": "status", "@required": true } }
39+
]
40+
}
41+
},
42+
{
43+
"template.output": {
44+
"name": "OrderSummary",
45+
"package": "acme::shop",
46+
"@payloadRef": "OrderSummaryPayload",
47+
"@textRef": "shop/order-summary",
48+
"@format": "json",
49+
"@promptStyle": "inline"
50+
}
51+
}
52+
]
53+
}
54+
}

server/csharp/MetaObjects.Conformance.Tests/ConformanceTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ public class ConformanceTests
1919
/// <summary>
2020
/// xUnit MemberData source — one entry per discovered fixture. Docs-only
2121
/// fixtures (an <c>expected/*.md</c> directory, no strict expectation files)
22-
/// drive the docs-conformance runner only and are skipped here.
22+
/// and contract-only fixtures (an <c>expected-paths.json</c> cross-port layout
23+
/// manifest, no strict expectation files) drive bespoke conformance runners
24+
/// only and are skipped here.
2325
/// </summary>
2426
public static IEnumerable<object[]> Fixtures() =>
2527
FixtureDiscovery.DiscoverFixtures(CorpusRoot.Path)
26-
.Where(f => !f.IsDocsOnly)
28+
.Where(f => !f.IsDocsOnly && !f.IsContractOnly)
2729
.Select(f => new object[] { f });
2830

2931
// -------------------------------------------------------------------------

server/csharp/MetaObjects.Conformance.Tests/FixtureDiscovery.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace MetaObjects.Conformance.Tests;
2222
/// <param name="HasExpectedWarnings">Whether expected-warnings.json is present.</param>
2323
/// <param name="HasScript">Whether script.json is present.</param>
2424
/// <param name="HasDocsExpected">Whether expected/ holds at least one .md doc golden.</param>
25+
/// <param name="HasContractManifest">Whether a cross-port expected-paths.json layout contract is present.</param>
2526
public sealed record Fixture(
2627
string Name,
2728
string Dir,
@@ -32,7 +33,8 @@ public sealed record Fixture(
3233
bool HasExpectedErrors,
3334
bool HasExpectedWarnings,
3435
bool HasScript,
35-
bool HasDocsExpected)
36+
bool HasDocsExpected,
37+
bool HasContractManifest)
3638
{
3739
/// <summary>
3840
/// A fixture is "docs-only" when it ships an <c>expected/</c> directory of
@@ -48,6 +50,23 @@ public sealed record Fixture(
4850
!HasExpectedErrors &&
4951
!HasExpectedWarnings &&
5052
!HasScript;
53+
54+
/// <summary>
55+
/// A fixture is "contract-only" when it ships a cross-port layout manifest
56+
/// (<c>expected-paths.json</c>) but NONE of the strict metadata expectation
57+
/// files. Such fixtures drive the dedicated api-docs cross-port conformance
58+
/// runner only; the strict metadata conformance runner skips them rather than
59+
/// flagging "declares no expectation files". Mirrors the TS
60+
/// <c>isContractOnlyFixture</c> and the Java <c>isContractOnly</c>.
61+
/// </summary>
62+
public bool IsContractOnly =>
63+
HasContractManifest &&
64+
!HasExpected &&
65+
!HasExpectedEffective &&
66+
!HasExpectedErrors &&
67+
!HasExpectedWarnings &&
68+
!HasScript &&
69+
!HasDocsExpected;
5170
}
5271

5372
/// <summary>
@@ -107,7 +126,8 @@ public static IReadOnlyList<Fixture> DiscoverFixtures(string corpusRoot)
107126
HasExpectedErrors: File.Exists(System.IO.Path.Combine(dir, "expected-errors.json")),
108127
HasExpectedWarnings: File.Exists(System.IO.Path.Combine(dir, "expected-warnings.json")),
109128
HasScript: File.Exists(System.IO.Path.Combine(dir, "script.json")),
110-
HasDocsExpected: HasDocsExpectedDir(dir)));
129+
HasDocsExpected: HasDocsExpectedDir(dir),
130+
HasContractManifest: File.Exists(System.IO.Path.Combine(dir, "expected-paths.json"))));
111131
}
112132

113133
return fixtures;

0 commit comments

Comments
 (0)