Skip to content

Commit ecbfe23

Browse files
dmealingclaude
andcommitted
feat(codegen-python): FindInbound — the ADR-0052 direction rule, in one place
The first piece of the Python port. Mirrors the TS, C#, Java and Kotlin files of the same name: a responding prompt is a template.prompt whose @responseRef resolves, and the gate is @responseRef PRESENCE, never a format value. Resolution goes through resolve_payload_vo — the SAME target rule @payloadRef obeys — rather than an any-object lookup. That is deliberate and load-bearing: C# used the any-object resolver here and shipped a parser bound to a record the payload tier refused to emit (CS0246). Writing the later ports through the value-only resolver is what makes them immune by construction. No generator consumes this yet; the three generators plus the payload tier and api-docs are the remaining work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
1 parent 82f66f2 commit ecbfe23

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""The ADR-0052 direction rule, in ONE place (Python port).
2+
3+
A template subtype's axis is DIRECTION: ``template.output`` renders outbound (a document or
4+
an email) and generates no parser; the inbound half — the response shape, the FR-010
5+
response-format fragment, and the parser-on-receipt — belongs to a ``template.prompt`` that
6+
declares ``@responseRef``.
7+
8+
Every inbound generator calls through here rather than re-deriving "which templates have a
9+
response". Call sites each deciding for themselves is exactly how the pre-ADR-0052 tier
10+
drifted: the parser applied NO format filter to the parser FILE while gating its tolerant
11+
extract on ``@format``, and the fragment emitter applied a different ``@format`` gate — the
12+
format of the OUTBOUND body, which is not the format of the reply.
13+
14+
Mirrors ``codegen-ts/src/templates/find-inbound.ts``, C#'s ``FindInbound.cs``, Java's
15+
``FindInbound.java`` and Kotlin's ``FindInbound.kt``.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from dataclasses import dataclass
21+
22+
from metaobjects.codegen.generators.payload_vo_generator import resolve_payload_vo
23+
from metaobjects.meta.meta_data import MetaData
24+
from metaobjects.meta.template import template_constants as tc
25+
from metaobjects.shared.base_types import TYPE_TEMPLATE
26+
27+
28+
@dataclass(frozen=True)
29+
class InboundShape:
30+
"""What an inbound generator needs about one responding prompt.
31+
32+
``vo`` — the resolved response value-object; the shape a reply is parsed INTO.
33+
``ref`` — the ``@responseRef`` string as authored (bare or fully-qualified).
34+
``format`` — the syntax of the REPLY (ADR-0053), never the template's ``@format``,
35+
which is the syntax of the rendered prompt BODY. The two genuinely differ.
36+
"""
37+
38+
vo: MetaData
39+
ref: str
40+
format: str
41+
42+
43+
def response_ref_of(template: MetaData) -> str | None:
44+
"""The authored ``@responseRef`` of a responding prompt, or ``None``.
45+
46+
ADR-0039: read RESOLVING — a template may inherit ``@responseRef`` through ``extends``,
47+
and shipped fixtures rely on exactly that.
48+
"""
49+
if template.type != TYPE_TEMPLATE or template.sub_type != tc.TEMPLATE_SUBTYPE_PROMPT:
50+
return None
51+
ref = template.get_meta_attr(tc.TEMPLATE_ATTR_RESPONSE_REF)
52+
if not isinstance(ref, str) or not ref:
53+
return None
54+
return ref
55+
56+
57+
def inbound_templates(root: MetaData) -> list[MetaData]:
58+
"""Every ``template.prompt`` that declares a response shape, ordered by name.
59+
60+
The gate is ``@responseRef`` PRESENCE, not a format value: declaring a response shape IS
61+
the request for a parser. Gating on ``@format`` was what let a ``text`` template get a
62+
strict parser but no tolerant extract, and — because ``@format`` defaults to ``text`` —
63+
would silently emit nothing at all after the re-homing.
64+
"""
65+
return sorted(
66+
(c for c in root.children() if response_ref_of(c) is not None),
67+
key=lambda t: t.name,
68+
)
69+
70+
71+
def response_shape(root: MetaData, template: MetaData, referrer_pkg: str) -> InboundShape | None:
72+
"""Resolve a prompt's response value-object and reply syntax, or ``None``.
73+
74+
``None`` when the template declares no ``@responseRef`` or the ref does not resolve —
75+
callers skip rather than raise, matching the pre-ADR-0052 contract for an unresolvable
76+
payload ref.
77+
78+
Resolution goes through ``resolve_payload_vo``, the SAME target rule ``@payloadRef``
79+
obeys, so a parser can never bind a record the payload tier refused to emit. (C# used the
80+
any-object resolver here and shipped exactly that defect: a ``@responseRef`` naming an
81+
``object.entity`` produced a parser returning a type nobody declared.)
82+
"""
83+
ref = response_ref_of(template)
84+
if ref is None:
85+
return None
86+
vo = resolve_payload_vo(root, ref, referrer_pkg)
87+
if vo is None:
88+
return None
89+
return InboundShape(vo=vo, ref=ref, format=response_format_of(template))
90+
91+
92+
def response_format_of(template: MetaData) -> str:
93+
"""The declared reply syntax, defaulted per ADR-0053.
94+
95+
The default is ``json`` because that reproduces the trace helper's pre-ADR-0053 fallback
96+
exactly (anything that was not ``"xml"`` was treated as JSON), which is what makes the
97+
attribute's introduction behaviour-preserving rather than a new policy.
98+
"""
99+
raw = template.get_meta_attr(tc.TEMPLATE_ATTR_RESPONSE_FORMAT)
100+
if isinstance(raw, str) and raw.lower() == tc.RESPONSE_FORMAT_XML:
101+
return tc.RESPONSE_FORMAT_XML
102+
return tc.RESPONSE_FORMAT_DEFAULT
103+
104+
105+
def is_xml(response_format: str) -> bool:
106+
"""True iff the reply is XML.
107+
108+
The strict tier is JSON-ONLY by construction — not because no XML reader exists (the
109+
render package ships a forgiving one) but because strict all-or-nothing semantics layered
110+
over a REPAIRING parser is incoherent: it would raise or accept based on how much repair
111+
happened. So an XML reply gets the tolerant extract and nothing strict.
112+
"""
113+
return response_format.lower() == tc.RESPONSE_FORMAT_XML

0 commit comments

Comments
 (0)