Skip to content

Commit b90c413

Browse files
dmealingclaude
andcommitted
docs(plan): FR-010 Python port — native recover engine + renderer + codegen (3 phases)
Python is the last of the 5 ports. Ships FR-006 + render + verify + codegen + conformance, but FR-010's recover engine + output-format renderer don't exist yet. 3-phase native port mirroring C#/TS: (1) recover engine + dirty-input conformance, (2) output-format renderer, (3) attrs + canonical-serializer properties-key-sort fix + recover()/output-prompt codegen. Python-specific: importlib-and-call proof of emitted code; mypy --strict gate; radix-prefix numeric guard for cross-port parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cc08aa0 commit b90c413

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# FR-010 Python port — native recover engine + output-format renderer + codegen
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development + cross-language-porting. Java (`server/java/render/.../recover/` + `.../prompt/`) is the reference; C# (`server/csharp/MetaObjects.Render/{Recover,Prompt}/` + `MetaObjects.Codegen/Generators/`) and TS are fresh native ports to mirror. The conformance corpus is the ORACLE (`fixtures/recover-conformance/`, `fixtures/conformance/template-output-{xml,json}-simple/`). Reimplement idiomatically (Tier-2); never change Tier-1 (classification, vocabulary, rendered output, wire format). Never edit a fixture to match the port — escalate.
4+
5+
**Goal:** Bring FR-010 to the Python port — the LAST of the five ports. Python ships FR-006 + render + verify + codegen + conformance, but FR-010's recover engine + output-format renderer don't exist yet. Python natively reimplements both under `metaobjects/render/`, then adds the metamodel attrs + serializer fix + codegen. After this, FR-010 ships in all 5 ports.
6+
7+
**Three phases (each a separate merge):**
8+
1. **Recover engine + dirty-input conformance**`metaobjects/render/recover/` reimplemented from Java/C#; a recover-conformance pytest over `fixtures/recover-conformance/` is the gate. (≈ C# Phase 1.)
9+
2. **Output-format renderer**`metaobjects/render/prompt/` (3 comment-free styles × {json,xml}); byte-identical to Java/Kotlin/C#/TS, reusing the `escapers` registry. (≈ C# Phase 2.)
10+
3. **Metamodel attrs + serializer fix + codegen + clean fixtures** — register `@promptStyle`/`@example`/`@instruction`/`@enumAlias`/`@enumDoc`; fix `serializer_json` to sort `properties`-attr map keys (NOT `filter`); extend the output-parser codegen with a tolerant `recover_<name>()`; add an output-prompt generator. (≈ C# Phase 3.)
11+
12+
**Tech stack:** Python 3.11+, `metaobjects` package at `server/python/`, pytest, mypy --strict, ruff. Pydantic is the FR-006 strict tier; recover is the new forgiving tier (no Pydantic). Reuse the `metaobjects.render.escapers` registry for the renderer. Tests under `server/python/tests/`.
13+
14+
## Tier decisions (cross-language-porting)
15+
- **Tier 1 (INVARIANT — must match the corpus):** per-field `FieldRecovery` classification (`RECOVERED|DEFAULTED|LOST_OPTIONAL|LOST_REQUIRED|MALFORMED`) + `empty` flag + canonical normalized value (numeric ±1e-9); pipeline behavior (strip/locate/forgiving-read/malformed-vs-absent/coerce); the rendered fragment (comment-free, valid, 3 styles); `@promptStyle`/`@enumAlias`/`@enumDoc` vocabulary; MALFORMED/TRUNCATED sentinel effect; canonical serialization (`properties` keys sorted, `filter` order preserved).
16+
- **Tier 2 (idiomatic Python):** module layout under `recover/`; `recover(text, schema, opts=None)` free function; enums via `enum.Enum`/`StrEnum` with SCREAMING_SNAKE values matching the corpus; MALFORMED/TRUNCATED as unique sentinel objects; the generated `recover_<name>()` returns `RecoveryResult[<Name>Recovered]` where `<Name>Recovered` is an emitted dataclass with every field `Optional[T]` (parallel to C#/Kotlin/TS nullable mirror — Pydantic payloads can't hold None for a required field). `as_int`/`as_long` both return `Optional[int]` (Python has one int type).
17+
- **Tier 3 (free):** internal mechanism, the conformance runner plumbing, the import-and-call proof.
18+
- **Bounded deferral:** nested-object recover/prompt mapping; array-of-enum (parity with the other ports — though TS implemented engine-level nested; Python may either way as long as the corpus + codegen defer).
19+
20+
---
21+
22+
## PHASE 1 — recover engine + dirty-input conformance (detailed)
23+
24+
Port the engine to `metaobjects/render/recover/` (Python modules), then prove against the corpus. **The corpus is the oracle: when a case is red, ask "should it match?" before "how do I make it match"; never edit a fixture.**
25+
26+
### P1-1: data model (`recover/types.py`)
27+
Port `Format`, `FieldKind`, `FieldRecovery`, `Tolerance`, `Coercion`, `FieldSpec` (+ factories `scalar`/`enum_field`/`range_`/`object_`), `RecoverSchema`, `RecoverOptions` (+ `defaults()`), `RecoverOutcome`, `RecoveryResult`, `RecoveryReport` (mutable: `set`/`add_coercion`/`mark_empty`/`states`/`coercions`/`lost_required`/`malformed`/`has_lost_required`/`is_empty`). Enums with SCREAMING_SNAKE values matching the corpus. **Test:** mirror Java `ModelTest`/`ReportTest`.
28+
29+
### P1-2: `strip` (stage 1) — fence removal (regex, DOTALL). Mirror `StripTest`.
30+
### P1-3: `locate` (stages 2–3) — `locate_json` balanced-brace (string-aware) + `locate_xml`; first-closed-else-first-open; no-throw-on-`</x>`. Mirror `LocateTest`.
31+
### P1-4: `json_forgiving_reader` — tolerant JSON → dict; trailing commas, single/unquoted, **TRUNCATED sentinel**, no-hang on `{"xs":[}`. Port the FIXED version. Mirror `JsonForgivingReaderTest`.
32+
### P1-5: `xml_forgiving_reader` — tolerant XML → dict; unclosed-tag recovery, attr-quote variants, case-folding, no-throw on close-tag-start. Mirror `XmlForgivingReaderTest`.
33+
### P1-6: `coerce` (stage 7) — enum alias/vocab (runtime-alias-wins), numeric range clamp, **non-finite → MALFORMED**, boolean forms, on_field hook, normalizers. **Python numeric note:** use `float(...)`/`int(...)` with `math.isfinite`; reject `0x`/`0b`/`0o` radix prefixes that `int(s,0)` would accept but Java/C# reject (cross-port parity — both C# and TS added this guard); empty-string guard. `MALFORMED` = a unique sentinel object. Mirror `CoerceTest`.
34+
### P1-7: `recover` entry + `recover_map` + classify — assemble strip→locate→read→extract→classify→coerce; array partial-MALFORMED keeps elements; object-given-scalar → MALFORMED; TRUNCATED→MALFORMED; empty-guard. `recover_map` accessors `as_string/as_int/as_long/as_double/as_bool/as_string_list` None-safe. Mirror `RecoverTest`.
35+
### P1-8: dirty-input conformance runner
36+
`tests/conformance/test_recover_conformance.py` (parametrized over `fixtures/recover-conformance/`): parse `schema.json` (map UPPER enum strings), read `input.txt`, run `recover`, assert `expected.json`'s `empty` + `states` (EXHAUSTIVE key-set) + `data` (floats ±1e-9). All 10 cases.
37+
### P1-9: review + simplify + merge. `pytest` + `mypy --strict` + `ruff` green; KNOWN_GAPS note. Merge phase 1.
38+
39+
---
40+
41+
## PHASE 2 — output-format renderer (outline; detailed when phase 1 lands)
42+
Port `metaobjects/render/prompt/{prompt_style,prompt_field,output_format_spec,prompt_overrides,output_format_renderer}.py` — 3 comment-free styles × {json,xml}, reusing `escapers`. Byte-identical fragment (verbatim prose: "Fill in each field as described below:", "Respond exactly like this:", " one of ", " VAL = doc", " e.g. "). Numeric-vs-quoted uses `math.isfinite` + the radix guard (NaN/Infinity/0x.. → quoted). Tests mirror the Java renderer tests (comment-free, valid JSON via `json.loads`, NaN-quoted + finite-unquoted).
43+
44+
## PHASE 3 — metamodel attrs + serializer fix + codegen (outline)
45+
1. **Attrs:** `@promptStyle` (closed enum on template.output via a `_TEMPLATE_OUTPUT_ATTRS` AttrSchema with `allowed_values`, like `@format`); `@example`/`@instruction` (string, common field attrs); `@enumAlias`/`@enumDoc` (`ATTR_SUBTYPE_PROPERTIES`). Add constants.
46+
2. **Serializer fix:** in `serializer_json._normalize` (or the attr-value emit), sort `properties`-attr map keys ordinally while preserving `filter` order — value-shape heuristic (all-scalar values → sort; any dict/list value → preserve), same as C#/TS. Confirm filter desugar runs before serialization. Closes `template-output-{json,xml}-simple` (red in Python today).
47+
3. **Codegen:** extend the output-parser generator to additionally emit a `<Name>Recovered` dataclass (all `Optional`) + `recover_<name>(text, opts=None) -> RecoveryResult[<Name>Recovered]` from a baked RecoverSchema (a recover-schema emitter), for json/xml outputs; a new output-prompt generator emitting `render_<name>_format(overrides=None)` backed by the output-format renderer + an output-format-spec emitter. Reuse `type_map.py`.
48+
4. **Proof:** Python's advantage — `importlib` the emitted module in a test and CALL `recover_<name>()` on dirty input (@enumAlias fold) + `render_<name>_format()` (guide fragment). Plus mypy on emitted source if feasible.
49+
5. Clean `template-output-{json,xml}-simple` fixtures conformance (Python green).
50+
51+
---
52+
53+
## Self-review (phase 1)
54+
- All recover modules ported; corpus 10/10 green; classification + canonical-value asserted; FIXED behaviors carried (no-hang JSON, XML no-throw, non-finite MALFORMED, TRUNCATED→MALFORMED, radix-prefix guard, array partial); never-throws; nested deferred.
55+
- Verify-against-live-code: the Python attr-read API, the pytest parametrize idiom, the `escapers` registry (phase 2), the conformance fixture-walk + canonical-serialize compare (phase 3), the importlib proof.

0 commit comments

Comments
 (0)