behavior-contracts 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/PKG-INFO +3 -3
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/README.md +2 -2
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/pyproject.toml +1 -1
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/__init__.py +29 -2
- behavior_contracts-0.2.0/src/behavior_contracts/behavior.py +260 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/canonical.py +19 -1
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/codec.py +12 -1
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/conformance.py +186 -2
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/expr_eval.py +17 -1
- behavior_contracts-0.2.0/src/behavior_contracts/fingerprint.py +89 -0
- behavior_contracts-0.2.0/src/behavior_contracts/guard.py +189 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/plan.py +102 -21
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/PKG-INFO +3 -3
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/SOURCES.txt +4 -0
- behavior_contracts-0.2.0/tests/test_codegen_generated.py +126 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/tests/test_conformance.py +2 -2
- behavior_contracts-0.2.0/tests/test_plan_parallel.py +215 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/tests/test_public_api.py +145 -1
- behavior_contracts-0.1.2/src/behavior_contracts/guard.py +0 -54
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/setup.cfg +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/envelope.py +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts/template.py +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/dependency_links.txt +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/entry_points.txt +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/requires.txt +0 -0
- {behavior_contracts-0.1.2 → behavior_contracts-0.2.0}/src/behavior_contracts.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: behavior-contracts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies.
|
|
5
5
|
Author: foo-log
|
|
6
6
|
License: MIT
|
|
@@ -60,12 +60,12 @@ string matching the conformance protocol's failure-code set.
|
|
|
60
60
|
|
|
61
61
|
## Conformance
|
|
62
62
|
|
|
63
|
-
Runs the
|
|
63
|
+
Runs the 7 shared suites (147 vectors) from `../conformance/vectors/*.json`
|
|
64
64
|
through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
|
|
65
65
|
including the pre-flight version fail-closed sweep.
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
python -m behavior_contracts.conformance # → "
|
|
68
|
+
python -m behavior_contracts.conformance # → "147 passed, 0 failed / 147 vectors across 7 suites"
|
|
69
69
|
# or the console script:
|
|
70
70
|
behavior-contracts-conformance
|
|
71
71
|
```
|
|
@@ -48,12 +48,12 @@ string matching the conformance protocol's failure-code set.
|
|
|
48
48
|
|
|
49
49
|
## Conformance
|
|
50
50
|
|
|
51
|
-
Runs the
|
|
51
|
+
Runs the 7 shared suites (147 vectors) from `../conformance/vectors/*.json`
|
|
52
52
|
through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
|
|
53
53
|
including the pre-flight version fail-closed sweep.
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
python -m behavior_contracts.conformance # → "
|
|
56
|
+
python -m behavior_contracts.conformance # → "147 passed, 0 failed / 147 vectors across 7 suites"
|
|
57
57
|
# or the console script:
|
|
58
58
|
behavior-contracts-conformance
|
|
59
59
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "behavior-contracts"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -18,10 +18,13 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
# ── 対応する仕様バージョン(IR/vector の spec version。ライブラリ semver とは別管理)──
|
|
20
20
|
SPEC_VERSIONS = {
|
|
21
|
-
"expression":
|
|
21
|
+
"expression": 2, # v2: obj の __proto__ own key を FORBIDDEN_KEY に(fail-closed)
|
|
22
22
|
"template": 1,
|
|
23
23
|
"plan": 1,
|
|
24
|
-
"canonical":
|
|
24
|
+
"canonical": 2, # v2: obj 値の __proto__ own key を FORBIDDEN_KEY に(fail-closed)
|
|
25
|
+
"behavior": 2, # v2: map.when / map.into / map.batched / handler ctx(bc#22。v1 IR の意味論は不変)
|
|
26
|
+
"guard": 1, # 初版(bc#25: assert_portable_component_graph の accept/reject を vector-pin)
|
|
27
|
+
"c2": 1, # 初版(bc#28: c2-catalog-swap — catalog-swap 実行 + IR 構造一致の 5 言語 pin)
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
# validate_envelope に渡す graphddb 形式の既定 supported 版(``"<major>.<minor>"``)。
|
|
@@ -29,6 +32,7 @@ ENVELOPE_SPEC_VERSION = "1.1"
|
|
|
29
32
|
|
|
30
33
|
# ── expression(evaluate_expression) ──────────────────────────────────────────
|
|
31
34
|
from .expr_eval import ( # noqa: E402
|
|
35
|
+
FORBIDDEN_OBJECT_KEY,
|
|
32
36
|
ExprFailure,
|
|
33
37
|
cmp_code_points,
|
|
34
38
|
encode_value,
|
|
@@ -65,8 +69,16 @@ from .envelope import ( # noqa: E402
|
|
|
65
69
|
|
|
66
70
|
# ── guard(Portability Guard) ────────────────────────────────────────────────
|
|
67
71
|
from .guard import ( # noqa: E402
|
|
72
|
+
PORTABLE_EXPR_OPERATORS,
|
|
68
73
|
PortabilityError,
|
|
69
74
|
assert_portable,
|
|
75
|
+
assert_portable_component_graph,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# ── behavior(component-graph IR + run_behavior) ──────────────────────────────
|
|
79
|
+
from .behavior import ( # noqa: E402
|
|
80
|
+
BehaviorFailure,
|
|
81
|
+
run_behavior,
|
|
70
82
|
)
|
|
71
83
|
|
|
72
84
|
# ── conformance runner adapter 共通部 ─────────────────────────────────────────
|
|
@@ -75,6 +87,12 @@ from .codec import ( # noqa: E402
|
|
|
75
87
|
deep_equals,
|
|
76
88
|
)
|
|
77
89
|
|
|
90
|
+
# ── generator 支援(共通 Generator の生成モジュールが使う fingerprint, bc#13)──
|
|
91
|
+
from .fingerprint import ( # noqa: E402
|
|
92
|
+
FingerprintFailure,
|
|
93
|
+
fingerprint_component_graph,
|
|
94
|
+
)
|
|
95
|
+
|
|
78
96
|
__all__ = [
|
|
79
97
|
"SPEC_VERSIONS",
|
|
80
98
|
"ENVELOPE_SPEC_VERSION",
|
|
@@ -83,6 +101,7 @@ __all__ = [
|
|
|
83
101
|
"encode_value",
|
|
84
102
|
"cmp_code_points",
|
|
85
103
|
"ExprFailure",
|
|
104
|
+
"FORBIDDEN_OBJECT_KEY",
|
|
86
105
|
# template
|
|
87
106
|
"render_template",
|
|
88
107
|
"resolve_partial",
|
|
@@ -101,8 +120,16 @@ __all__ = [
|
|
|
101
120
|
"EnvelopeFailure",
|
|
102
121
|
# guard
|
|
103
122
|
"assert_portable",
|
|
123
|
+
"assert_portable_component_graph",
|
|
124
|
+
"PORTABLE_EXPR_OPERATORS",
|
|
104
125
|
"PortabilityError",
|
|
126
|
+
# behavior
|
|
127
|
+
"run_behavior",
|
|
128
|
+
"BehaviorFailure",
|
|
105
129
|
# codec
|
|
106
130
|
"decode_value",
|
|
107
131
|
"deep_equals",
|
|
132
|
+
# generator 支援(fingerprint)
|
|
133
|
+
"fingerprint_component_graph",
|
|
134
|
+
"FingerprintFailure",
|
|
108
135
|
]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""behavior.py — component-graph IR + run_behavior 統合実行 IF(Python port)。
|
|
2
|
+
|
|
3
|
+
TS 参照実装 ``ts/src/behavior.ts`` の意味論を完全一致で移植(scp-ir-architecture.md §5–§7)。
|
|
4
|
+
|
|
5
|
+
component-graph IR(``components[]{name, inputPorts, body[], output, plan}``)を、既存 COMMON の
|
|
6
|
+
``run_plan``(stage 実行・Skip 伝播・Policy Kind)+ ``evaluate``(Expression IR)の上に実行する。
|
|
7
|
+
専用コンポーネントの実装は handler registry(catalog名 → 実装。境界注入)で名前解決して委譲する。
|
|
8
|
+
|
|
9
|
+
body ノード種:
|
|
10
|
+
- componentRef: ``{id, component, ports, parent?, bindField?, relationKind?, policy?}``
|
|
11
|
+
- map: ``{id, map:{over, as, component, ports, when?, into?, batched?, parent?,
|
|
12
|
+
relationKind?, policy?}}``(when/into/batched は behaviorVersion 2)
|
|
13
|
+
- cond: ``{id, cond:{if, then, else, parent?}}``(純 Expression、handler を呼ばない)
|
|
14
|
+
|
|
15
|
+
behaviorVersion 2(bc#22):
|
|
16
|
+
- ``map.when`` — per-element guard。``{cond:[when,true,false]}`` へ lower して評価
|
|
17
|
+
(strict-bool・非 bool は TYPE_MISMATCH で fail-closed)。false の要素は skip。
|
|
18
|
+
- ``map.into`` — zip-attach。結果は「over の各 guard 通過要素へ into キーで handler 結果を
|
|
19
|
+
書き戻した augment 済みリスト」(over と同じ長さ・順序。skip 要素は無変更で pass through)。
|
|
20
|
+
- ``map.batched``— guard 通過全要素の ports を先に評価し handler を 1 回だけ
|
|
21
|
+
``handler({"items": [...]}, ctx)`` で呼ぶ。結果は items と同じ長さ・順序のリスト契約
|
|
22
|
+
(違反は MAP_BATCH_RESULT_MISMATCH)。通過 0 件なら handler は呼ばれず空リスト。
|
|
23
|
+
- handler ctx — 全 handler 呼び出しに ``{"nodeId", "component"}``(map 非 batched は
|
|
24
|
+
さらに ``"bound"``)を渡す。追加のみ・既存 handler 互換。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import threading
|
|
30
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional
|
|
31
|
+
|
|
32
|
+
from .expr_eval import evaluate as evaluate_expression
|
|
33
|
+
from .plan import run_plan
|
|
34
|
+
|
|
35
|
+
Value = Any
|
|
36
|
+
|
|
37
|
+
# handler(ports: dict, ctx: dict) -> ExecOutcome({"ok": Value} | {"error": str})
|
|
38
|
+
Handler = Callable[[Dict[str, Value], Dict[str, Any]], Mapping[str, Any]]
|
|
39
|
+
Handlers = Mapping[str, Handler]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BehaviorFailure(Exception):
|
|
43
|
+
"""run_behavior の Failure(UNKNOWN_COMPONENT / MAP_OVER_NOT_ARRAY / ...)。"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, code: str, message: str) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.code = code
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _bfail(code: str, message: str) -> "None":
|
|
51
|
+
raise BehaviorFailure(code, message)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _node_kind(n: Mapping[str, Any]) -> str:
|
|
55
|
+
if "map" in n:
|
|
56
|
+
return "map"
|
|
57
|
+
if "cond" in n:
|
|
58
|
+
return "cond"
|
|
59
|
+
if "component" in n:
|
|
60
|
+
return "componentRef"
|
|
61
|
+
_bfail("UNKNOWN_NODE_KIND", f"body node '{n.get('id', '?')}' is not componentRef/map/cond")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _node_parent(n: Mapping[str, Any]) -> Optional[str]:
|
|
65
|
+
if "map" in n:
|
|
66
|
+
return n["map"].get("parent")
|
|
67
|
+
if "cond" in n:
|
|
68
|
+
return n["cond"].get("parent")
|
|
69
|
+
return n.get("parent")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _node_bind_field(n: Mapping[str, Any]) -> Optional[str]:
|
|
73
|
+
if "map" in n or "cond" in n:
|
|
74
|
+
return None
|
|
75
|
+
return n.get("bindField")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _node_policy(n: Mapping[str, Any]) -> Optional[str]:
|
|
79
|
+
if "map" in n:
|
|
80
|
+
return n["map"].get("policy")
|
|
81
|
+
if "cond" in n:
|
|
82
|
+
return None
|
|
83
|
+
return n.get("policy")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _node_relation_kind(n: Mapping[str, Any]) -> Optional[str]:
|
|
87
|
+
if "map" in n:
|
|
88
|
+
return n["map"].get("relationKind")
|
|
89
|
+
if "cond" in n:
|
|
90
|
+
return None
|
|
91
|
+
return n.get("relationKind")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _eval_ports(ports: Mapping[str, Any], scope: Dict[str, Value]) -> Dict[str, Value]:
|
|
95
|
+
# key はソートせず宣言順のまま。FORBIDDEN_KEY 等は evaluate が投げる(fail-closed)。
|
|
96
|
+
return {k: evaluate_expression(v, scope) for k, v in ports.items()}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_behavior(
|
|
100
|
+
ir: Mapping[str, Any],
|
|
101
|
+
handlers: Handlers,
|
|
102
|
+
input: Optional[Mapping[str, Value]] = None,
|
|
103
|
+
entry: Optional[str] = None,
|
|
104
|
+
) -> Value:
|
|
105
|
+
"""component-graph IR の統合実行 IF(scp-ir-architecture.md §7)。
|
|
106
|
+
|
|
107
|
+
:param ir: component-graph 可搬 IR(``{components:[...]}``)。
|
|
108
|
+
:param handlers: 専用コンポーネント handler registry(catalog名 → 実装。境界注入)。
|
|
109
|
+
:param input: エントリ component の inputPorts 束縛(param 値)。
|
|
110
|
+
:param entry: 実行する component 名(省略時は先頭 component)。
|
|
111
|
+
:returns: ``output`` を評価した最終 Value(Φ 合流)。
|
|
112
|
+
"""
|
|
113
|
+
input = dict(input or {})
|
|
114
|
+
components = list(ir.get("components", []))
|
|
115
|
+
comp = None
|
|
116
|
+
if entry is not None:
|
|
117
|
+
comp = next((c for c in components if c.get("name") == entry), None)
|
|
118
|
+
elif components:
|
|
119
|
+
comp = components[0]
|
|
120
|
+
if comp is None:
|
|
121
|
+
_bfail("UNKNOWN_ENTRY", f"component '{entry if entry is not None else '<first>'}' not found in IR")
|
|
122
|
+
|
|
123
|
+
body: List[Mapping[str, Any]] = list(comp["body"])
|
|
124
|
+
id_to_index = {n["id"]: i for i, n in enumerate(body)}
|
|
125
|
+
|
|
126
|
+
ops: List[Dict[str, Any]] = []
|
|
127
|
+
for n in body:
|
|
128
|
+
pid = _node_parent(n)
|
|
129
|
+
parent = id_to_index.get(pid) if pid is not None else None
|
|
130
|
+
op: Dict[str, Any] = {"id": n["id"], "parent": parent}
|
|
131
|
+
bf = _node_bind_field(n)
|
|
132
|
+
if bf is not None:
|
|
133
|
+
op["bindField"] = bf
|
|
134
|
+
rk = _node_relation_kind(n)
|
|
135
|
+
if rk is not None:
|
|
136
|
+
op["relationKind"] = rk
|
|
137
|
+
pol = _node_policy(n)
|
|
138
|
+
if pol is not None:
|
|
139
|
+
op["policy"] = pol
|
|
140
|
+
ops.append(op)
|
|
141
|
+
|
|
142
|
+
# run_plan は concurrency > 1 の stage で exec を worker thread から並行に呼ぶ(bc#23)。
|
|
143
|
+
# results への書き込みは disjoint スロット(自ノード id)だが、base_scope の dict 展開
|
|
144
|
+
# (iteration)と挿入が並行すると RuntimeError になり得るため lock で直列化する。
|
|
145
|
+
# 兄弟は互いのスロットを読まない(§1: stage 内は相互独立)ので結果は決定的。
|
|
146
|
+
results: Dict[str, Value] = {}
|
|
147
|
+
results_lock = threading.Lock()
|
|
148
|
+
|
|
149
|
+
def base_scope() -> Dict[str, Value]:
|
|
150
|
+
with results_lock:
|
|
151
|
+
return {**input, **results}
|
|
152
|
+
|
|
153
|
+
def exec_node(op: Mapping[str, Any], _bound: Optional[Value]) -> Mapping[str, Any]:
|
|
154
|
+
idx = id_to_index[op["id"]]
|
|
155
|
+
node = body[idx]
|
|
156
|
+
kind = _node_kind(node)
|
|
157
|
+
|
|
158
|
+
if kind == "cond":
|
|
159
|
+
c = node["cond"]
|
|
160
|
+
value = evaluate_expression({"cond": [c["if"], c["then"], c["else"]]}, base_scope())
|
|
161
|
+
return {"ok": value}
|
|
162
|
+
|
|
163
|
+
if kind == "map":
|
|
164
|
+
m = node["map"]
|
|
165
|
+
over = evaluate_expression(m["over"], base_scope())
|
|
166
|
+
if not isinstance(over, list):
|
|
167
|
+
_bfail("MAP_OVER_NOT_ARRAY", f"map '{op['id']}': 'over' did not evaluate to an array")
|
|
168
|
+
handler = handlers.get(m["component"])
|
|
169
|
+
if handler is None:
|
|
170
|
+
_bfail("UNKNOWN_COMPONENT", f"component '{m['component']}' has no handler (fail-closed)")
|
|
171
|
+
ctx = {"nodeId": op["id"], "component": m["component"]}
|
|
172
|
+
when = m.get("when")
|
|
173
|
+
|
|
174
|
+
def keep(scope: Dict[str, Value]) -> bool:
|
|
175
|
+
# per-element guard(v2): `{cond:[when,true,false]}` へ lower(strict-bool)。
|
|
176
|
+
if when is None and "when" not in m:
|
|
177
|
+
return True
|
|
178
|
+
return evaluate_expression({"cond": [when, True, False]}, scope) is True
|
|
179
|
+
|
|
180
|
+
kept_idx: List[int] = [] # guard を通過した over 内 index(into の整列用)
|
|
181
|
+
|
|
182
|
+
if m.get("batched") is True:
|
|
183
|
+
# batched(v2): guard 通過全要素の ports を先に評価し、handler を 1 回だけ呼ぶ。
|
|
184
|
+
items: List[Value] = []
|
|
185
|
+
for i, el in enumerate(over):
|
|
186
|
+
scope = {**base_scope(), m["as"]: el}
|
|
187
|
+
if not keep(scope):
|
|
188
|
+
continue
|
|
189
|
+
items.append(_eval_ports(m["ports"], scope))
|
|
190
|
+
kept_idx.append(i)
|
|
191
|
+
if not items:
|
|
192
|
+
collected: List[Value] = [] # guard 全落ち: handler は呼ばれない
|
|
193
|
+
else:
|
|
194
|
+
outcome = handler({"items": items}, ctx)
|
|
195
|
+
if "error" in outcome:
|
|
196
|
+
return outcome
|
|
197
|
+
r = outcome["ok"]
|
|
198
|
+
if not isinstance(r, list) or len(r) != len(items):
|
|
199
|
+
_bfail(
|
|
200
|
+
"MAP_BATCH_RESULT_MISMATCH",
|
|
201
|
+
f"map '{op['id']}': batched handler must return a list aligned to items "
|
|
202
|
+
f"(want {len(items)}, got {len(r) if isinstance(r, list) else type(r).__name__})",
|
|
203
|
+
)
|
|
204
|
+
collected = r
|
|
205
|
+
else:
|
|
206
|
+
collected = []
|
|
207
|
+
for i, el in enumerate(over):
|
|
208
|
+
scope = {**base_scope(), m["as"]: el}
|
|
209
|
+
if not keep(scope):
|
|
210
|
+
continue
|
|
211
|
+
ports = _eval_ports(m["ports"], scope)
|
|
212
|
+
outcome = handler(ports, {**ctx, "bound": el})
|
|
213
|
+
if "error" in outcome:
|
|
214
|
+
return outcome
|
|
215
|
+
collected.append(outcome["ok"])
|
|
216
|
+
kept_idx.append(i)
|
|
217
|
+
|
|
218
|
+
into = m.get("into")
|
|
219
|
+
if into is None and "into" not in m:
|
|
220
|
+
return {"ok": collected}
|
|
221
|
+
|
|
222
|
+
# into(v2): over と同じ長さ・順序の augment 済みリスト(skip 要素は無変更)。
|
|
223
|
+
augmented: List[Value] = []
|
|
224
|
+
k = 0
|
|
225
|
+
for i, el in enumerate(over):
|
|
226
|
+
if k < len(kept_idx) and kept_idx[k] == i:
|
|
227
|
+
if not isinstance(el, dict):
|
|
228
|
+
_bfail(
|
|
229
|
+
"MAP_INTO_ELEMENT_NOT_OBJECT",
|
|
230
|
+
f"map '{op['id']}': 'into' requires object elements (element {i} is not an object)",
|
|
231
|
+
)
|
|
232
|
+
augmented.append({**el, into: collected[k]})
|
|
233
|
+
k += 1
|
|
234
|
+
else:
|
|
235
|
+
augmented.append(el)
|
|
236
|
+
return {"ok": augmented}
|
|
237
|
+
|
|
238
|
+
# componentRef
|
|
239
|
+
handler = handlers.get(node["component"])
|
|
240
|
+
if handler is None:
|
|
241
|
+
_bfail("UNKNOWN_COMPONENT", f"component '{node['component']}' has no handler (fail-closed)")
|
|
242
|
+
ports = _eval_ports(node.get("ports", {}), base_scope())
|
|
243
|
+
return handler(ports, {"nodeId": op["id"], "component": node["component"]})
|
|
244
|
+
|
|
245
|
+
def wrapped_exec(op: Mapping[str, Any], bound: Optional[Value]) -> Mapping[str, Any]:
|
|
246
|
+
outcome = exec_node(op, bound)
|
|
247
|
+
if "ok" in outcome:
|
|
248
|
+
with results_lock:
|
|
249
|
+
results[op["id"]] = outcome["ok"]
|
|
250
|
+
return outcome
|
|
251
|
+
|
|
252
|
+
plan = comp.get("plan")
|
|
253
|
+
run = run_plan(plan, ops, wrapped_exec)
|
|
254
|
+
|
|
255
|
+
for i, s in enumerate(run["states"]):
|
|
256
|
+
if s is not None and s.get("status") == "skipped":
|
|
257
|
+
rk = _node_relation_kind(body[i])
|
|
258
|
+
results[ops[i]["id"]] = {"items": [], "cursor": None} if rk == "connection" else None
|
|
259
|
+
|
|
260
|
+
return evaluate_expression(comp["output"], base_scope())
|
|
@@ -22,9 +22,11 @@ import json
|
|
|
22
22
|
import math
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
+
from .expr_eval import FORBIDDEN_OBJECT_KEY
|
|
26
|
+
|
|
25
27
|
Value = Any
|
|
26
28
|
|
|
27
|
-
_CANONICAL_FAILURE_CODES = frozenset({"NAN_OR_INF", "INVALID_VALUE"})
|
|
29
|
+
_CANONICAL_FAILURE_CODES = frozenset({"NAN_OR_INF", "INVALID_VALUE", "FORBIDDEN_KEY"})
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class CanonicalFailure(Exception):
|
|
@@ -47,6 +49,19 @@ def _is_plain_object(v: Value) -> bool:
|
|
|
47
49
|
return isinstance(v, dict)
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
def _guard_keys(keys) -> None:
|
|
53
|
+
"""obj 値の own key に "__proto__" があれば fail-closed(FORBIDDEN_KEY)。
|
|
54
|
+
|
|
55
|
+
canonical_value/canonical_json/_encode_nested の全経路で適用し、expression の obj ノードと
|
|
56
|
+
同一の禁止キー規律を持つ(expression-ir.md §2.3/§8)。
|
|
57
|
+
"""
|
|
58
|
+
if FORBIDDEN_OBJECT_KEY in keys:
|
|
59
|
+
_fail(
|
|
60
|
+
"FORBIDDEN_KEY",
|
|
61
|
+
f'object key "{FORBIDDEN_OBJECT_KEY}" is forbidden (fail-closed)',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
50
65
|
# ── §4.1 float の正準10進(CPython repr(float))────────────────────────────────
|
|
51
66
|
def py_float_repr(n: float) -> str:
|
|
52
67
|
"""CPython の ``repr(float)`` / ``json.dumps(float)`` と byte 一致する 10 進表現。
|
|
@@ -157,6 +172,7 @@ def canonical_value(v: Value) -> str:
|
|
|
157
172
|
(canonical-serialization.md §2)。
|
|
158
173
|
"""
|
|
159
174
|
if _is_plain_object(v):
|
|
175
|
+
_guard_keys(v.keys())
|
|
160
176
|
keys = sorted(v.keys(), key=_code_point_sort_key)
|
|
161
177
|
parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in keys]
|
|
162
178
|
return "{" + ",".join(parts) + "}"
|
|
@@ -167,6 +183,7 @@ def canonical_value(v: Value) -> str:
|
|
|
167
183
|
def canonical_json(v: Value) -> str:
|
|
168
184
|
"""fingerprint 用。全 object 階層のキーを昇順ソート(配列は順序保持)。"""
|
|
169
185
|
if _is_plain_object(v):
|
|
186
|
+
_guard_keys(v.keys())
|
|
170
187
|
keys = sorted(v.keys(), key=_code_point_sort_key)
|
|
171
188
|
parts = [f"{_json_string(k)}:{canonical_json(v[k])}" for k in keys]
|
|
172
189
|
return "{" + ",".join(parts) + "}"
|
|
@@ -178,6 +195,7 @@ def canonical_json(v: Value) -> str:
|
|
|
178
195
|
# ── 内部: ネスト値の直列化(canonical_value 用。ネスト object は挿入順)──────────────
|
|
179
196
|
def _encode_nested(v: Value) -> str:
|
|
180
197
|
if _is_plain_object(v):
|
|
198
|
+
_guard_keys(v.keys())
|
|
181
199
|
parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in v.keys()]
|
|
182
200
|
return "{" + ",".join(parts) + "}"
|
|
183
201
|
if isinstance(v, list):
|
|
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|
|
16
16
|
import math
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
|
+
from .expr_eval import FORBIDDEN_OBJECT_KEY, ExprFailure
|
|
20
|
+
|
|
19
21
|
Value = Any
|
|
20
22
|
|
|
21
23
|
|
|
@@ -53,7 +55,16 @@ def decode_value(x: Any) -> Value:
|
|
|
53
55
|
return float("nan")
|
|
54
56
|
if k == "inf":
|
|
55
57
|
return float("-inf") if x["inf"] < 0 else float("inf")
|
|
56
|
-
|
|
58
|
+
out = {}
|
|
59
|
+
for k, v in x.items():
|
|
60
|
+
# fail-closed: own key "__proto__" は言語間で発散する(expression-ir.md §2.3/§8)。
|
|
61
|
+
if k == FORBIDDEN_OBJECT_KEY:
|
|
62
|
+
raise ExprFailure(
|
|
63
|
+
"FORBIDDEN_KEY",
|
|
64
|
+
f'object key "{FORBIDDEN_OBJECT_KEY}" is forbidden (fail-closed)',
|
|
65
|
+
)
|
|
66
|
+
out[k] = decode_value(v)
|
|
67
|
+
return out
|
|
57
68
|
raise ValueError(f"cannot decode value: {x!s}")
|
|
58
69
|
|
|
59
70
|
|