Skip to content

Commit 5289630

Browse files
dmealingclaude
andcommitted
docs(spec): R13 output-prompt-fragment conformance harness design
Designs a descriptor-driven cross-port corpus pinning OutputFormatRenderer fragment bytes (guide/inline/exampleOnly x json/xml) byte-identically across all 5 ports + a render->recover round-trip property; bundles FR-011 enum-coercion attr-validation negative fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7615d2c commit 5289630

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# R13 — Output-Format Prompt-Fragment Conformance Harness — Design
2+
3+
_Date: 2026-05-30. Status: approved (design). Backlog item: R13 (see `docs/superpowers/specs/2026-05-29-conformance-hardening-review.md`)._
4+
5+
## Problem
6+
7+
FR-010 shipped an **output-format prompt fragment** in all five ports: `OutputFormatRenderer.render(spec, overrides)` emits a comment-free instruction fragment teaching an LLM the exact shape to return, in three presentation styles (`guide` | `inline` | `exampleOnly`) across two formats (`json` | `xml`), driven by `@promptStyle` / `@example` / `@instruction` / `@enumDoc` on a `template.output` + its payload value-object.
8+
9+
The whole point of that fragment is **prompt-cache stability**: the rendered prefix must be byte-stable so exact-prefix prompt-cache hits are not silently broken — and it must be byte-**identical** across ports so a service written in one language emits a cache-compatible prefix to one written in another.
10+
11+
Today that guarantee is **not pinned by any shared corpus**. Each port's renderer is trusted to its own unit tests (`OutputFormatRenderer*Test`, `*OutputPromptGenerator*Test`). The `template-output-*` metamodel fixtures pin only the *declaration* (loader + serializer), not the rendered bytes. So cross-port byte-identity of the fragment is unverified — the exact property the feature exists to provide.
12+
13+
This is backlog item **R13** ("prompt-parser / prompt corpus"), and was a bounded deferral noted during FR-010 ("output-prompt verify-conformance corpus wiring").
14+
15+
## Goal
16+
17+
A strong, shared, cross-port conformance harness that pins the rendered output-format prompt fragment **byte-for-byte across all five ports**, plus a round-trip property tying the request side to the response (`recover`) side. Bundle the adjacent FR-011 loader gap: shared negative fixtures for the enum-coercion attribute validation.
18+
19+
## What this harness pins — and what it does not
20+
21+
**Pins:** given an identical spec descriptor, all five ports' `OutputFormatRenderer` produce **byte-identical** fragments for every (style × format) combination. That is the prompt-cache-stability guarantee.
22+
23+
**Does not pin (by design):** the metadata→spec *extraction* (walking a `template.output` + payload VO to build the `OutputFormatSpec`). That logic lives inside each port's codegen emitter (`OutputFormatSpecEmitter` and peers), which emits `new OutputFormatSpec(...)` as source text — there is no runtime metadata→spec builder, and extracting one across five ports would be a large refactor that risks regressing shipped FR-010 codegen. Extraction stays covered by the existing per-port codegen unit tests + FR-010 compile-run proofs. Because the emitted prompt class simply delegates to the engine, **engine byte-identity (this harness) + the existing "generated class delegates to the engine" compile-run proofs are transitively strong.**
24+
25+
This boundary is stated explicitly so the gate is not oversold — consistent with the lesson that "all corpora green" must mean what it says.
26+
27+
## Architecture
28+
29+
The harness is **descriptor-driven**, mirroring the existing `recover-conformance` corpus (which drives the `recover` engine from a serialized `RecoverSchema`). Each case serializes a spec descriptor; every port deserializes it into its native `OutputFormatSpec`, renders, and asserts byte-equality against checked-in expected output.
30+
31+
### Corpus layout — `fixtures/output-prompt-conformance/<case>/`
32+
33+
```
34+
spec.json # cross-port oracle INPUT (the unified field descriptor)
35+
expected.guide.txt # byte-exact rendered fragment, style = guide
36+
expected.inline.txt # byte-exact rendered fragment, style = inline
37+
expected.exampleOnly.txt # byte-exact rendered fragment, style = exampleOnly
38+
README.md # one paragraph: what this case exercises
39+
```
40+
41+
`format` (`json` | `xml`) is a property of the case (fixed in `spec.json`), so each case is single-format. All three styles are rendered per case via the runtime style override, yielding three expected files.
42+
43+
### `spec.json` descriptor (the unified oracle input)
44+
45+
```jsonc
46+
{
47+
"format": "json", // "json" | "xml"
48+
"rootName": "SupportAnswer", // XML root tag / logical JSON root
49+
"roundTrip": true, // optional; true only when EVERY field has an @example (gates the round-trip assertion)
50+
"fields": [
51+
{
52+
"name": "text",
53+
"kind": "STRING", // STRING | INT | LONG | DOUBLE | BOOLEAN | ENUM | OBJECT
54+
"required": true,
55+
"array": false, // optional, default false
56+
"example": "Your refund will appear in 3-5 days.", // optional
57+
"instruction": "One or two sentences to the customer.", // optional
58+
"enumValues": null, // string[] when kind = ENUM
59+
"enumDoc": null, // { MEMBER: "doc" } when kind = ENUM
60+
"nested": null // a nested { rootName, fields } object when kind = OBJECT
61+
}
62+
]
63+
}
64+
```
65+
66+
One descriptor builds **both**:
67+
- an `OutputFormatSpec` (consuming `example` / `instruction` / `enumValues` / `enumDoc` / `nested`) for rendering, and
68+
- a minimal `RecoverSchema` (consuming `name` / `kind` / `required` / `enumValues` / `array` / `nested`) for the round-trip.
69+
70+
They are sibling descriptors; the superset avoids duplicate per-case files.
71+
72+
### Per-port harness (engine-level, all five ports)
73+
74+
For each case directory:
75+
76+
1. Parse `spec.json` → build the port's `OutputFormatSpec`.
77+
2. For `style` in `{guide, inline, exampleOnly}`: `render(spec, style)` → assert the result is **byte-equal** to `expected.<style>.txt`. **Zero-drift: no ledger.** Any divergence fails the build.
78+
3. **Determinism:** render a second time, assert identical to the first.
79+
4. **Round-trip** (only when `spec.json` has `"roundTrip": true`): build a `RecoverSchema` from the same descriptor → `recover(expected.exampleOnly.txt)` → assert every declared field classifies **RECOVERED** (no `MALFORMED` / `LOST_REQUIRED` / `LOST_OPTIONAL`). This is the example↔recover skew guard: it catches a renderer that emits an example the `recover` parser cannot read back.
80+
81+
**Kotlin** reuses the shared JVM `render` engine (the same Java `OutputFormatRenderer` + `Recover` classes), so its harness drives those classes directly. This still runs the corpus end-to-end in the Kotlin module — closing, for this corpus, the "Kotlin runs only 2 of 6 corpora" gap. Engine output is byte-identical to Java by construction; the value is proving the corpus loads and the assertions execute in the Kotlin build.
82+
83+
### Why zero-drift is achievable here
84+
85+
Unlike `render-conformance` (which runs user templates through a Mustache engine and carries one ledgered standalone-comment divergence from Mustache.java), `OutputFormatRenderer` is **hand-written, comment-free, and builds strings directly**. There is no third-party templating engine in the path. The FR-010 renderers were authored per port specifically to be byte-identical, and `recover-conformance` already demonstrates byte-identical canonical values cross-port.
86+
87+
To keep zero-drift robust against the one known cross-runtime hazard — floating-point formatting (the R6 float-fidelity lesson) — **fixture example values are restricted to strings, integers, booleans, and dyadic decimals only**; no raw floats whose textual form differs across runtimes. This restriction is documented in the corpus README. If a genuine platform divergence ever surfaces, it is a renderer bug to fix, not a ledger entry.
88+
89+
## Case matrix
90+
91+
Approximately ten cases × three styles ≈ thirty expected files. Each case is a `spec.json` + three `expected.*.txt`:
92+
93+
| Case | Format | Exercises |
94+
|---|---|---|
95+
| `json-scalars` | json | string/int/bool/dyadic-double scalars; `@example` + `@instruction`; required + optional |
96+
| `xml-scalars` | xml | same field set, XML rendering |
97+
| `json-enum` | json | `field.enum` with `@enumValues` + `@enumDoc` + `@example` |
98+
| `xml-enum` | xml | enum, XML |
99+
| `json-array` | json | scalar array field |
100+
| `json-nested` | json | nested `OBJECT` field (nested rootName + fields) |
101+
| `xml-nested` | xml | nested object, XML |
102+
| `optional-absent` | json | optional fields with no `@example`/`@instruction` (sparsity / skeleton behavior) |
103+
| `instruction-fallback` | json | fields lacking `@instruction` (guide-style fallback) |
104+
| `unicode-example` | json | multibyte `@example` value (byte-identity under UTF-8) |
105+
106+
`roundTrip: true` is set on the cases where every field declares an `@example` (at minimum `json-scalars`, `xml-scalars`, `json-enum`, `xml-enum`, `json-nested`, `xml-nested`, `unicode-example`).
107+
108+
## FR-011 attribute-validation negative fixtures (bundled scope add)
109+
110+
Three shared metamodel fixtures, added under `fixtures/conformance/`, exercising the FR-011 enum-coercion attribute validation that currently lives only in per-port loader unit tests:
111+
112+
| Fixture | Declares | Expected error |
113+
|---|---|---|
114+
| `error-enum-coerce-default-non-member` | `field.enum` with `@coerceDefault` value not in `@values` | `ERR_BAD_ATTR_VALUE` |
115+
| `error-enum-default-non-member` | `field.enum` with `@default` value not in `@values` | `ERR_BAD_ATTR_VALUE` |
116+
| `error-enum-normalize-bad-mode` | `field.enum` with `@normalize` not in `none` / `collapse` / `strip` | `ERR_BAD_ATTR_VALUE` |
117+
118+
These run on the existing metamodel-conformance loader runners (TS / C# / Java / Python). The Kotlin `metadata-ktx` facade is read-only and does not run the metamodel corpus, so it is out of scope for these fixtures — consistent with where the FR-011 validation actually executes.
119+
120+
Each fixture follows the existing `error-*` metamodel-fixture convention — an `input/meta.<name>.json` plus an `expected-errors.json` of the form:
121+
122+
```jsonc
123+
{ "errors": [ { "code": "ERR_BAD_ATTR_VALUE",
124+
"source": { "format": "json", "files": ["meta.enums.json"],
125+
"jsonPath": "$['metadata.root'].children[0]['object.entity'].children[1]['field.enum']" } } ],
126+
"warnings": [] }
127+
```
128+
129+
The loader runners already parse this envelope generically (the `jsonPath` is per-fixture), so the new directories need **no runner change** — only authoring. (Note: missing `@values` is `ERR_MISSING_REQUIRED_ATTR`, already covered by `error-enum-missing-values`; all three FR-011 fixtures are *bad-value* cases → `ERR_BAD_ATTR_VALUE`.)
130+
131+
## Components / file inventory
132+
133+
- **Corpus (shared):** `fixtures/output-prompt-conformance/<case>/{spec.json, expected.*.txt, README.md}` + a top-level `README.md` documenting the descriptor schema and the no-float rule; three `fixtures/conformance/error-enum-*` fixtures.
134+
- **TS (pilot):** `server/typescript/packages/render/test/output-prompt-conformance.test.ts` — descriptor parse → `OutputFormatSpec` → render × 3 styles → byte assert → determinism → round-trip via `recover`.
135+
- **C#:** `csharp/MetaObjects.Render.Tests/OutputPromptConformanceTests.cs`.
136+
- **Java:** `server/java/render/src/test/java/com/metaobjects/render/prompt/OutputPromptConformanceTest.java`.
137+
- **Python:** `server/python/tests/render/test_output_prompt_conformance.py`.
138+
- **Kotlin:** a runner in the Kotlin render/test module driving the shared JVM `OutputFormatRenderer` + `Recover` against the corpus.
139+
- **Descriptor → spec builders:** a small per-port test helper that maps `spec.json` → the native `OutputFormatSpec` (and → `RecoverSchema` for round-trip). Test-only; not production code.
140+
141+
Each runner asserts a **corpus-count guard** (the number of case directories matches an expected constant) so a port silently skipping cases fails — the same guard `recover-conformance` runners carry.
142+
143+
## Testing strategy
144+
145+
The harness *is* the test. Validation that the harness itself is sound:
146+
- The TS pilot authors the corpus and is the reference; expected files are generated by the TS renderer and then **independently reproduced** by each port's runner (a port that cannot reproduce them fails — that is the gate working).
147+
- Determinism assertion (render twice) guards against accidental nondeterminism (map ordering, etc.).
148+
- The round-trip assertion guards example↔recover skew.
149+
- Corpus-count guards guard silent skips.
150+
- Zero-drift (no ledger) guards against quiet divergence acceptance.
151+
152+
## Build order and merge strategy
153+
154+
TS pilot (corpus + reference assertions + round-trip) → C# → Java → Python → Kotlin port the runner against the shared corpus → FR-011 error fixtures (small; loader ports) → close-out (KNOWN_GAPS/roadmap/memory).
155+
156+
All work stays on **one branch with a single final merge** (as with FR-011): the shared corpus would red the other ports if the pilot merged alone. Each unit passes a spec-compliance + code-quality review gate before the final merge. Forward-only on `main`.
157+
158+
## Out of scope (explicit)
159+
160+
- Runtime metadata→`OutputFormatSpec` extraction (stays in codegen; covered by existing compile-run proofs).
161+
- The codegen-emitted prompt class compile-run tier (already covered per-port by FR-010 proofs; the design pins the engine the class delegates to).
162+
- Payload-VO codegen corpus (backlog item **R12** — separate future plan).
163+
- Raw-float example values (excluded to preserve zero-drift).
164+
- Kotlin metamodel-corpus participation for the FR-011 error fixtures (read-only facade).

0 commit comments

Comments
 (0)