parse-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parse_sdk/__init__.py +57 -0
- parse_sdk/_labels.py +51 -0
- parse_sdk/_oauth.py +301 -0
- parse_sdk/_project.py +184 -0
- parse_sdk/_runtime.py +2142 -0
- parse_sdk/_sanitize.py +15 -0
- parse_sdk/_sync.py +887 -0
- parse_sdk/checks.py +601 -0
- parse_sdk/cli.py +1667 -0
- parse_sdk/cli_help.py +110 -0
- parse_sdk/codegen_reconcile.py +157 -0
- parse_sdk/codegen_v2.py +3361 -0
- parse_sdk/config.py +349 -0
- parse_sdk/docgen.py +587 -0
- parse_sdk/doctor.py +348 -0
- parse_sdk/migrate.py +212 -0
- parse_sdk/preview.py +588 -0
- parse_sdk/py.typed +0 -0
- parse_sdk/resource_surface.py +196 -0
- parse_sdk/sample_gate.py +245 -0
- parse_sdk/scaffold.py +273 -0
- parse_sdk-0.1.0.dist-info/METADATA +177 -0
- parse_sdk-0.1.0.dist-info/RECORD +26 -0
- parse_sdk-0.1.0.dist-info/WHEEL +4 -0
- parse_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- parse_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""The ONE named, stable SDK reachability/entity predicate.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for "what is the emitted client surface
|
|
4
|
+
of a resource". It is imported by THREE consumers that previously each computed
|
|
5
|
+
reachability independently and could silently desync:
|
|
6
|
+
|
|
7
|
+
1. **codegen** (``codegen_v2._ModuleNames``) — gates the ``client.<plural>``
|
|
8
|
+
collection ``@property`` (``emits_client_accessor``);
|
|
9
|
+
2. an external consumer — asserts the
|
|
10
|
+
exact complement (a resource it flags unreachable must NOT emit an accessor),
|
|
11
|
+
and pins lockstep tests against THIS named API so a future SDK refactor can't
|
|
12
|
+
silently break backend validation;
|
|
13
|
+
3. **docgen** (``docgen._reachability_warnings``) — warns on a resource that
|
|
14
|
+
ships as dead code (``not is_class_reachable``).
|
|
15
|
+
|
|
16
|
+
Because consumer 2 lives in a different repository, this is a **stable named
|
|
17
|
+
API** — ``parse_sdk.resource_surface.resource_surface_facts`` — NOT an
|
|
18
|
+
SDK-internal helper. It is additive (a NEW module / the cut's only NEW exported
|
|
19
|
+
symbol); it does not rename or remove any existing symbol.
|
|
20
|
+
|
|
21
|
+
Per-resource facts (each a SEPARATE, independently-meaningful signal):
|
|
22
|
+
|
|
23
|
+
* ``has_root_collection`` — a pinless op with a string endpoint routes to a
|
|
24
|
+
root collection (``_root_collection_emit_ops``). The synthesized
|
|
25
|
+
``fetched_by`` ``.get`` is folded in by ``_normalize_schema``, so this single
|
|
26
|
+
fact already covers the "root-collection emit-ops OR fetched_by" entity arm.
|
|
27
|
+
* ``has_safe_factory`` — ``constructible`` AND an instance-navigable surface (a
|
|
28
|
+
``$self``-pinned op OR a sub-resource) AND a ``str`` ``keyed_by`` AND every
|
|
29
|
+
``$self`` pin uses the key (``_constructible_unsafe_pin`` clean) — the SAME
|
|
30
|
+
gate codegen's factory pass uses. This is the safe-``constructible`` entity arm.
|
|
31
|
+
* ``is_type_referenced`` — the resource's emitted class name is named by some op
|
|
32
|
+
``returns`` (excluding its OWN ops — a resource can't bootstrap itself), a
|
|
33
|
+
field type, a sub-resource ``of``, or a sub-resource op ``returns``.
|
|
34
|
+
* ``is_class_reachable`` = ``has_root_collection or has_safe_factory or
|
|
35
|
+
is_type_referenced`` — the resource's class is genuinely handed to the user
|
|
36
|
+
SOMEHOW; its negation is docgen's "ships as dead code" warning.
|
|
37
|
+
* ``emits_client_accessor`` = ``has_root_collection`` — whether codegen emits a
|
|
38
|
+
``client.<plural>`` collection ``@property`` for this resource. A safe-factory
|
|
39
|
+
resource is reachable but its client surface is the no-network factory
|
|
40
|
+
``@property`` (``_factory_rows``), NOT a ``client.<plural>`` collection; a pure
|
|
41
|
+
value-object (type-referenced only) emits NEITHER. So this fact is the
|
|
42
|
+
collection-accessor gate specifically, and ``is_class_reachable`` is the
|
|
43
|
+
broader "not dead code" question — keeping them SEPARATE is what lets codegen
|
|
44
|
+
suppress dead ``client.<plural>`` while docgen still credits factory/type
|
|
45
|
+
reachability.
|
|
46
|
+
|
|
47
|
+
The facts mirror existing codegen/docgen behavior EXACTLY (this is a
|
|
48
|
+
centralization, not a behavior change): emitting bytes are unchanged — it
|
|
49
|
+
removes the parallel computations so the three consumers can never drift.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from typing import Any, Dict, NamedTuple, Optional, Set
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResourceFacts(NamedTuple):
|
|
58
|
+
"""The reachability/entity surface of a single resource. Field names are the
|
|
59
|
+
a stable public contract — external lockstep tests read them by name."""
|
|
60
|
+
|
|
61
|
+
has_root_collection: bool
|
|
62
|
+
has_safe_factory: bool
|
|
63
|
+
is_type_referenced: bool
|
|
64
|
+
is_class_reachable: bool
|
|
65
|
+
emits_client_accessor: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resource_surface_facts(schema: Dict[str, Any]) -> Dict[str, ResourceFacts]:
|
|
69
|
+
"""``{resource_spec_key -> ResourceFacts}`` for every resource in ``schema``.
|
|
70
|
+
|
|
71
|
+
Computed on the NORMALIZED schema (so a ``fetched_by``-synthesized ``.get``
|
|
72
|
+
counts as a root collection and a normalized-vs-raw caller agree). The key is
|
|
73
|
+
the resource's spec key in the normalized schema; type-reference matching is
|
|
74
|
+
against the ``_camel``-canonical resource universe, mirroring codegen's
|
|
75
|
+
``_allocate_returns`` resolution and the field-resolvability gate.
|
|
76
|
+
|
|
77
|
+
Accepts a raw OR already-normalized schema (it normalizes internally and is
|
|
78
|
+
idempotent under re-normalization), so a cross-repo caller need not import the
|
|
79
|
+
SDK's private normalizer. Returns ``{}`` when the schema declares no resources.
|
|
80
|
+
"""
|
|
81
|
+
# Imported lazily so this module has no import-time cycle with codegen_v2
|
|
82
|
+
# (which is the heavier module and may import this in turn).
|
|
83
|
+
from parse_sdk.codegen_v2 import (
|
|
84
|
+
_camel,
|
|
85
|
+
_is_safe_factory,
|
|
86
|
+
_normalize_schema,
|
|
87
|
+
_referenced_resource_names,
|
|
88
|
+
_root_collection_emit_ops,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
norm = _normalize_schema(schema)
|
|
92
|
+
resources = norm.get("resources")
|
|
93
|
+
if not isinstance(resources, dict) or not resources:
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
# Codegen resolves a return/field leaf to a resource by ``_camel`` identity
|
|
97
|
+
# (``_allocate_returns``), so the resource-name universe is the canonical
|
|
98
|
+
# ``_camel`` form of every resource key.
|
|
99
|
+
camel_names: Set[str] = {
|
|
100
|
+
_camel(rk) for rk, rv in resources.items() if isinstance(rv, dict)
|
|
101
|
+
}
|
|
102
|
+
# Under a ``_camel`` collision (e.g. ``Post`` + ``post``) codegen allocates
|
|
103
|
+
# the FIRST resource the un-suffixed class (``Post``) and the collider a
|
|
104
|
+
# suffix (``Post_`` — see ``_allocate_ident``); a bare-name reference
|
|
105
|
+
# (``featured: "Post"``) resolves to the un-suffixed class only. So a
|
|
106
|
+
# ``_camel`` reference credits ONLY the canonical (first-allocated) resource
|
|
107
|
+
# for that name — else the dead collider inherits the canonical's
|
|
108
|
+
# reachability and the fact API lies to its consumers (codegen/docgen/the
|
|
109
|
+
# external lockstep). ``has_root_collection``/``has_safe_factory`` are
|
|
110
|
+
# per-resource and unaffected; only the type-reference credit needs gating.
|
|
111
|
+
_canonical_rk_for_camel: Dict[str, str] = {}
|
|
112
|
+
for rk, rv in resources.items():
|
|
113
|
+
if isinstance(rv, dict):
|
|
114
|
+
_canonical_rk_for_camel.setdefault(_camel(rk), rk)
|
|
115
|
+
|
|
116
|
+
# --- gather every type label that could name a resource, ONCE -------------
|
|
117
|
+
# (op returns, field types, sub-resource ``of`` + sub-op returns). An own
|
|
118
|
+
# op's ``returns`` must NOT self-credit the owner (a resource can't bootstrap
|
|
119
|
+
# itself; the legit "collection op returns self" case is clause 1's job).
|
|
120
|
+
referenced_names: Set[str] = set()
|
|
121
|
+
|
|
122
|
+
def _credit(label: Any, *, exclude: Optional[str] = None) -> None:
|
|
123
|
+
if not isinstance(label, str):
|
|
124
|
+
return
|
|
125
|
+
names = _referenced_resource_names(label, camel_names)
|
|
126
|
+
if exclude is not None:
|
|
127
|
+
names = names - {exclude}
|
|
128
|
+
referenced_names.update(names)
|
|
129
|
+
|
|
130
|
+
for rk, rspec in resources.items():
|
|
131
|
+
if not isinstance(rspec, dict):
|
|
132
|
+
continue
|
|
133
|
+
own_name = _camel(rk)
|
|
134
|
+
ops = rspec.get("operations")
|
|
135
|
+
ops = ops if isinstance(ops, dict) else {}
|
|
136
|
+
fields = rspec.get("fields")
|
|
137
|
+
fields = fields if isinstance(fields, dict) else {}
|
|
138
|
+
subs = rspec.get("sub_resources")
|
|
139
|
+
subs = subs if isinstance(subs, dict) else {}
|
|
140
|
+
for op in ops.values():
|
|
141
|
+
if isinstance(op, dict):
|
|
142
|
+
_credit(op.get("returns"), exclude=own_name)
|
|
143
|
+
for fval in fields.values():
|
|
144
|
+
if isinstance(fval, str):
|
|
145
|
+
_credit(fval)
|
|
146
|
+
elif isinstance(fval, dict):
|
|
147
|
+
_credit(fval.get("type"))
|
|
148
|
+
for sspec in subs.values():
|
|
149
|
+
if not isinstance(sspec, dict):
|
|
150
|
+
continue
|
|
151
|
+
_credit(sspec.get("of"))
|
|
152
|
+
sops = sspec.get("operations")
|
|
153
|
+
sops = sops if isinstance(sops, dict) else {}
|
|
154
|
+
for sop in sops.values():
|
|
155
|
+
if isinstance(sop, dict):
|
|
156
|
+
_credit(sop.get("returns"))
|
|
157
|
+
|
|
158
|
+
out: Dict[str, ResourceFacts] = {}
|
|
159
|
+
for rk, rspec in resources.items():
|
|
160
|
+
if not isinstance(rspec, dict):
|
|
161
|
+
continue
|
|
162
|
+
ops = rspec.get("operations")
|
|
163
|
+
ops = ops if isinstance(ops, dict) else {}
|
|
164
|
+
subs = rspec.get("sub_resources")
|
|
165
|
+
subs = subs if isinstance(subs, dict) else {}
|
|
166
|
+
|
|
167
|
+
# 1. Root collection (== the client.<plural> collection-accessor gate).
|
|
168
|
+
has_root_collection = bool(_root_collection_emit_ops(ops))
|
|
169
|
+
|
|
170
|
+
# 2. Safe factory — codegen's _factory_gate is the single source (so this
|
|
171
|
+
# reachability fact can never drift from the emitter's factory pass).
|
|
172
|
+
has_safe_factory = _is_safe_factory(rspec, ops, subs)
|
|
173
|
+
|
|
174
|
+
# 3. Referenced as a type (its emitted class is genuinely returned).
|
|
175
|
+
# Only the canonical resource for a colliding ``_camel`` is the target
|
|
176
|
+
# of a bare-name reference (the collider gets a suffixed class no
|
|
177
|
+
# bare label names) — so a non-canonical collider is NOT type-referenced.
|
|
178
|
+
_rk_camel = _camel(rk)
|
|
179
|
+
is_type_referenced = (
|
|
180
|
+
_rk_camel in referenced_names
|
|
181
|
+
and _canonical_rk_for_camel.get(_rk_camel) == rk
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
is_class_reachable = has_root_collection or has_safe_factory or is_type_referenced
|
|
185
|
+
|
|
186
|
+
out[rk] = ResourceFacts(
|
|
187
|
+
has_root_collection=has_root_collection,
|
|
188
|
+
has_safe_factory=has_safe_factory,
|
|
189
|
+
is_type_referenced=is_type_referenced,
|
|
190
|
+
is_class_reachable=is_class_reachable,
|
|
191
|
+
# The client.<plural> collection accessor is gated by the
|
|
192
|
+
# root-collection fact alone (factory/value-object resources do not
|
|
193
|
+
# get a collection accessor) — so codegen suppresses dead client.<plural>.
|
|
194
|
+
emits_client_accessor=has_root_collection,
|
|
195
|
+
)
|
|
196
|
+
return out
|
parse_sdk/sample_gate.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Deterministic field-type ⟷ sample reconciliation.
|
|
2
|
+
|
|
3
|
+
The generated SDKs faithfully try to coerce whatever field type the LLM author
|
|
4
|
+
wrote. When that type is wrong (``release_date: Dict[str, str]`` over a real
|
|
5
|
+
``{"date": str, "coming_soon": bool}`` payload), the SDK can still type-check
|
|
6
|
+
while exposing raw or best-effort-coerced values at runtime. This module is the
|
|
7
|
+
deterministic verifier that catches the mismatch against the captured
|
|
8
|
+
``return_schema.sample``.
|
|
9
|
+
|
|
10
|
+
``gate_field(declared_label, samples, *, is_key=False) -> Verdict`` is pure: it
|
|
11
|
+
reads value-types only, never emits the sample into output, and floors a
|
|
12
|
+
contradicted ``Dict[str, SCALAR]`` / ``List[Dict[str, SCALAR]]`` / ``List[SCALAR]``
|
|
13
|
+
field to the Any-form of the OBSERVED outer shape (``Any`` is sound — raw
|
|
14
|
+
passthrough at runtime). The repair label is always one ``_field_annotation``
|
|
15
|
+
emits verbatim (the resolvability invariant).
|
|
16
|
+
|
|
17
|
+
``samples`` is the list of OBSERVED VALUES for ONE field, already projected by
|
|
18
|
+
The projection (``resource → operation → endpoint → return_schema.sample`` with envelope
|
|
19
|
+
descent). The gate never extracts a wire key itself.
|
|
20
|
+
|
|
21
|
+
Built-in invariants:
|
|
22
|
+
* a declared ``Optional`` is preserved on repair independent of
|
|
23
|
+
whether the sample showed null (dropping it makes ``x = None`` a type error).
|
|
24
|
+
* the ``_KEY_FIELD`` is never rewritten (``is_key=True`` →
|
|
25
|
+
``CONFIRMED``); widening a key to ``Any`` breaks ``__eq__``/``__hash__``/dedup.
|
|
26
|
+
* ``samples`` is guarded: ``None → []``; a non-list (a bare dict/str
|
|
27
|
+
would silently iterate keys/chars) → ``UNDECIDABLE``.
|
|
28
|
+
* an inner null in a ``List``/``Dict`` re-wraps the inner ``Optional``.
|
|
29
|
+
* Repair floors to the **observed** outer shape (fixes outer-container flip:
|
|
30
|
+
``List[str]`` over a dict payload → ``Dict[str, Any]``, not ``List[Any]``).
|
|
31
|
+
* Sanitized placeholder leaves (``REDACTED_*`` etc.) are treated as
|
|
32
|
+
``UNDECIDABLE`` evidence — residual serving-time defense, not the guarantee.
|
|
33
|
+
|
|
34
|
+
Scalar-declared fields (``str``/``int``/...) get a verdict but are never
|
|
35
|
+
repaired here (``repaired_label is None``); scalar mistypes route to the
|
|
36
|
+
runtime backstop. Everything else (``X | str`` open unions, ``Dict[int, X]``,
|
|
37
|
+
nesting beyond one level, resources, ``datetime``/``Decimal``/``Any``) is
|
|
38
|
+
out-of-scope → the declared label is kept (``CONFIRMED``, never touched).
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from typing import Any, List, Optional, Tuple
|
|
44
|
+
|
|
45
|
+
from parse_sdk._labels import split_top as _split_top
|
|
46
|
+
from parse_sdk._labels import strip_wrapper as _inner
|
|
47
|
+
|
|
48
|
+
_SCALARS = ("str", "int", "float", "bool")
|
|
49
|
+
|
|
50
|
+
# Sanitizer placeholder leaves — a fixed allow-list is residual defense only
|
|
51
|
+
# (an LLM sanitizer can emit any string; the build-time persisted repair is the
|
|
52
|
+
# real guarantee). A leaf equal to one of these is "no evidence", not proof.
|
|
53
|
+
_PLACEHOLDER_EXACT = frozenset({
|
|
54
|
+
"CRITICAL_PII_REDACTED", "John Doe", "Jane Doe",
|
|
55
|
+
"user@example.com", "+1 (555) 012-3456", "123 Main St",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Verdict:
|
|
61
|
+
status: str # "CONFIRMED" | "CONTRADICTED" | "UNDECIDABLE"
|
|
62
|
+
repaired_label: Optional[str] # non-None only when a rewrite is warranted
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class _Parsed:
|
|
67
|
+
kind: str # "scalar" | "dict_scalar" | "list_scalar" | "list_dict_scalar" | "opaque"
|
|
68
|
+
scalar: Optional[str]
|
|
69
|
+
optional: bool
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_placeholder(v: Any) -> bool:
|
|
73
|
+
if not isinstance(v, str):
|
|
74
|
+
return False
|
|
75
|
+
return v.startswith("REDACTED_") or v in _PLACEHOLDER_EXACT
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Depth-0 comma split — single source in parse_sdk._labels (the hand-rolled
|
|
79
|
+
# copies here, in codegen_reconcile, and in the runtime drifted historically);
|
|
80
|
+
# imported at the top as ``_split_top``.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_label(label: Any) -> _Parsed:
|
|
84
|
+
"""Classify a declared field label into its reconciliation kind. A non-string
|
|
85
|
+
label (a malformed FieldSpec like ``{"type": 123}``) is out-of-scope/opaque —
|
|
86
|
+
never reconciled, never raised on (the downstream codegen path handles it
|
|
87
|
+
leniently)."""
|
|
88
|
+
if not isinstance(label, str):
|
|
89
|
+
return _Parsed("opaque", None, False)
|
|
90
|
+
s = label.strip()
|
|
91
|
+
optional = False
|
|
92
|
+
inner = _inner(s, "Optional[")
|
|
93
|
+
if inner is not None:
|
|
94
|
+
optional, s = True, inner
|
|
95
|
+
# A top-level union (``X | str``) is an open-enum/leaf form — never touched.
|
|
96
|
+
if "|" in s:
|
|
97
|
+
return _Parsed("opaque", None, optional)
|
|
98
|
+
if s in _SCALARS:
|
|
99
|
+
return _Parsed("scalar", s, optional)
|
|
100
|
+
d = _inner(s, "Dict[")
|
|
101
|
+
if d is not None:
|
|
102
|
+
parts = _split_top(d)
|
|
103
|
+
if len(parts) == 2 and parts[0] == "str" and parts[1] in _SCALARS:
|
|
104
|
+
return _Parsed("dict_scalar", parts[1], optional)
|
|
105
|
+
return _Parsed("opaque", None, optional)
|
|
106
|
+
li = _inner(s, "List[")
|
|
107
|
+
if li is not None:
|
|
108
|
+
if li in _SCALARS:
|
|
109
|
+
return _Parsed("list_scalar", li, optional)
|
|
110
|
+
dd = _inner(li, "Dict[")
|
|
111
|
+
if dd is not None:
|
|
112
|
+
parts = _split_top(dd)
|
|
113
|
+
if len(parts) == 2 and parts[0] == "str" and parts[1] in _SCALARS:
|
|
114
|
+
return _Parsed("list_dict_scalar", parts[1], optional)
|
|
115
|
+
return _Parsed("opaque", None, optional)
|
|
116
|
+
return _Parsed("opaque", None, optional)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_scalar_instance(v: Any, scalar: "Optional[str]") -> bool:
|
|
120
|
+
# ``scalar`` is Optional because ``_Parsed.scalar`` is — a scalar-kinded
|
|
121
|
+
# parse always sets it (invariant by construction), and the str fallthrough
|
|
122
|
+
# is total either way.
|
|
123
|
+
# ``bool`` subclasses ``int`` — check bool first, and exclude bool from
|
|
124
|
+
# int/float so a wire ``True`` never satisfies an ``int``/``float`` field.
|
|
125
|
+
if scalar == "bool":
|
|
126
|
+
return isinstance(v, bool)
|
|
127
|
+
if scalar == "int":
|
|
128
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
129
|
+
if scalar == "float":
|
|
130
|
+
return isinstance(v, float) and not isinstance(v, bool)
|
|
131
|
+
return isinstance(v, str)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _wrap(base: str, optional: bool) -> str:
|
|
135
|
+
return f"Optional[{base}]" if optional else base
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _observed_floor(non_null: List[Any], optional: bool) -> str:
|
|
139
|
+
"""Floor to the Any-form of the OBSERVED outer shape across all samples."""
|
|
140
|
+
saw_dict = any(isinstance(v, dict) for v in non_null)
|
|
141
|
+
lists = [v for v in non_null if isinstance(v, list)]
|
|
142
|
+
saw_list = bool(lists)
|
|
143
|
+
if saw_dict and not saw_list:
|
|
144
|
+
return _wrap("Dict[str, Any]", optional)
|
|
145
|
+
if saw_list and not saw_dict:
|
|
146
|
+
elems = [e for lst in lists for e in lst]
|
|
147
|
+
if elems and all(isinstance(e, dict) for e in elems):
|
|
148
|
+
return _wrap("List[Dict[str, Any]]", optional)
|
|
149
|
+
return _wrap("List[Any]", optional)
|
|
150
|
+
# Scalars-only, or a mixed dict+list across samples — the honest floor is
|
|
151
|
+
# bare Any.
|
|
152
|
+
return _wrap("Any", optional)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _real_leaves(values: List[Any]) -> Tuple[List[Any], bool]:
|
|
156
|
+
"""Drop nulls + sanitized placeholders; report whether a null was present."""
|
|
157
|
+
saw_null = any(v is None for v in values)
|
|
158
|
+
leaves = [v for v in values if v is not None and not _is_placeholder(v)]
|
|
159
|
+
return leaves, saw_null
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def gate_field(declared_label: str, samples: Any, *, is_key: bool = False) -> Verdict:
|
|
163
|
+
# Never rewrite a key field (owned by the original coercer).
|
|
164
|
+
if is_key:
|
|
165
|
+
return Verdict("CONFIRMED", None)
|
|
166
|
+
|
|
167
|
+
parsed = _parse_label(declared_label)
|
|
168
|
+
if parsed.kind == "opaque":
|
|
169
|
+
return Verdict("CONFIRMED", None) # keep the declared label, untouched
|
|
170
|
+
|
|
171
|
+
# Guard the samples arg.
|
|
172
|
+
if samples is None:
|
|
173
|
+
samples = []
|
|
174
|
+
if not isinstance(samples, list):
|
|
175
|
+
return Verdict("UNDECIDABLE", None)
|
|
176
|
+
|
|
177
|
+
non_null = [s for s in samples if s is not None]
|
|
178
|
+
|
|
179
|
+
if parsed.kind == "scalar":
|
|
180
|
+
leaves, _ = _real_leaves(samples)
|
|
181
|
+
if not leaves:
|
|
182
|
+
return Verdict("UNDECIDABLE", None)
|
|
183
|
+
if all(_is_scalar_instance(v, parsed.scalar) for v in leaves):
|
|
184
|
+
return Verdict("CONFIRMED", None)
|
|
185
|
+
# A scalar mismatch is DETECTED but deliberately NOT repaired here
|
|
186
|
+
# (deferred). Runtime coercion is best-effort and keeps
|
|
187
|
+
# unparseable values raw; to close a mismatch deterministically at
|
|
188
|
+
# codegen time, floor to ``Any`` here.
|
|
189
|
+
return Verdict("CONTRADICTED", None)
|
|
190
|
+
|
|
191
|
+
# Repairable container kinds: dict_scalar / list_scalar / list_dict_scalar.
|
|
192
|
+
declared_outer = "dict" if parsed.kind == "dict_scalar" else "list"
|
|
193
|
+
|
|
194
|
+
def _outer(v: Any) -> str:
|
|
195
|
+
if isinstance(v, dict):
|
|
196
|
+
return "dict"
|
|
197
|
+
if isinstance(v, list):
|
|
198
|
+
return "list"
|
|
199
|
+
return "scalar"
|
|
200
|
+
|
|
201
|
+
if non_null and any(_outer(v) != declared_outer for v in non_null):
|
|
202
|
+
# Outer-container flip — floor to the OBSERVED shape, not the declared.
|
|
203
|
+
return Verdict("CONTRADICTED", _observed_floor(non_null, parsed.optional))
|
|
204
|
+
|
|
205
|
+
# Outer shape matches the declaration → inspect inner leaf value-types.
|
|
206
|
+
leaf_values: List[Any] = []
|
|
207
|
+
saw_inner_null = False
|
|
208
|
+
if parsed.kind == "dict_scalar":
|
|
209
|
+
for v in non_null:
|
|
210
|
+
vals = list(v.values())
|
|
211
|
+
leaves, snull = _real_leaves(vals)
|
|
212
|
+
leaf_values.extend(leaves); saw_inner_null |= snull
|
|
213
|
+
elif parsed.kind == "list_scalar":
|
|
214
|
+
for v in non_null:
|
|
215
|
+
leaves, snull = _real_leaves(v)
|
|
216
|
+
leaf_values.extend(leaves); saw_inner_null |= snull
|
|
217
|
+
else: # list_dict_scalar
|
|
218
|
+
for v in non_null:
|
|
219
|
+
for d in v:
|
|
220
|
+
if isinstance(d, dict):
|
|
221
|
+
leaves, snull = _real_leaves(list(d.values()))
|
|
222
|
+
leaf_values.extend(leaves); saw_inner_null |= snull
|
|
223
|
+
elif d is not None:
|
|
224
|
+
# A non-dict element flips the inner shape (the element
|
|
225
|
+
# should be a record, not a scalar) — an outer-shape
|
|
226
|
+
# contradiction; floor to the OBSERVED shape. Do NOT fold it
|
|
227
|
+
# into the scalar leaf check (a str element would falsely
|
|
228
|
+
# CONFIRM a List[Dict[str,str]] over a wire List[str]).
|
|
229
|
+
return Verdict("CONTRADICTED", _observed_floor(non_null, parsed.optional))
|
|
230
|
+
|
|
231
|
+
if not leaf_values and not saw_inner_null:
|
|
232
|
+
return Verdict("UNDECIDABLE", None)
|
|
233
|
+
|
|
234
|
+
contradicted = any(not _is_scalar_instance(v, parsed.scalar) for v in leaf_values)
|
|
235
|
+
if contradicted:
|
|
236
|
+
return Verdict("CONTRADICTED", _observed_floor(non_null, parsed.optional))
|
|
237
|
+
|
|
238
|
+
# Inner nulls observed on an otherwise-CONFIRMED List[SCALAR] re-wrap
|
|
239
|
+
# the inner Optional (latent, non-corrupting).
|
|
240
|
+
if saw_inner_null and parsed.kind == "list_scalar":
|
|
241
|
+
return Verdict("CONTRADICTED",
|
|
242
|
+
_wrap(f"List[Optional[{parsed.scalar}]]", parsed.optional))
|
|
243
|
+
if not leaf_values:
|
|
244
|
+
return Verdict("UNDECIDABLE", None)
|
|
245
|
+
return Verdict("CONFIRMED", None)
|