programgarden 2.6.5__tar.gz → 2.6.6__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.
- {programgarden-2.6.5 → programgarden-2.6.6}/PKG-INFO +12 -5
- {programgarden-2.6.5 → programgarden-2.6.6}/README.md +9 -2
- programgarden-2.6.6/programgarden/binding_provenance.py +181 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/context.py +26 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/executor.py +246 -26
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_exchange.py +53 -18
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/node_runner.py +2 -1
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/providers/llm_provider.py +17 -1
- programgarden-2.6.6/programgarden/ranking_nodes.py +478 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resolver.py +45 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/validation_replay.py +9 -2
- {programgarden-2.6.5 → programgarden-2.6.6}/pyproject.toml +3 -3
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/binding_validator.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/client.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/code_worker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/broker_evidence.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/broker_snapshot.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_history.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/incremental_build.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/paper_futures_business_date.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_events.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_external.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_orders.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_sources.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resource/context.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.6}/programgarden/valuation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.6
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -16,8 +16,8 @@ Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
|
16
16
|
Requires-Dist: litellm (>=1.40.0)
|
|
17
17
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
18
18
|
Requires-Dist: programgarden-community (>=2.3.1,<3.0.0)
|
|
19
|
-
Requires-Dist: programgarden-core (>=2.6.
|
|
20
|
-
Requires-Dist: programgarden-finance (>=2.0.
|
|
19
|
+
Requires-Dist: programgarden-core (>=2.6.3,<3.0.0)
|
|
20
|
+
Requires-Dist: programgarden-finance (>=2.0.3,<3.0.0)
|
|
21
21
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
22
22
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
23
23
|
Requires-Dist: pydantic (>=2.0.1,<3.0.0)
|
|
@@ -31,6 +31,13 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
31
31
|
|
|
32
32
|
# ProgramGarden
|
|
33
33
|
|
|
34
|
+
The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
|
|
35
|
+
adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
|
|
36
|
+
expression templates authored in the workflow, routes Gemini and DeepSeek
|
|
37
|
+
credentials to their own APIs, and reads an unresolved futures position
|
|
38
|
+
exchange again at most once per 5 minutes per contract instead of keeping it
|
|
39
|
+
empty for the whole run.
|
|
40
|
+
|
|
34
41
|
The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
35
42
|
reads each futures position's and pending order's exchange from o3105 instead
|
|
36
43
|
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
@@ -307,7 +314,7 @@ job = await pg.run_async(
|
|
|
307
314
|
|
|
308
315
|
## 주요 특징
|
|
309
316
|
|
|
310
|
-
- **노드 기반 DSL**:
|
|
317
|
+
- **노드 기반 DSL**: 74개 내장 노드를 조합하여 코딩 없이 자동매매 전략 구성
|
|
311
318
|
- **실시간 처리**: WebSocket 기반 실시간 시세, 계좌, 체결 이벤트 수신
|
|
312
319
|
- **AI Agent 통합**: LLMModelNode + AIAgentNode로 LLM 기반 분석/의사결정
|
|
313
320
|
- **플러그인 확장**: 67개 내장 전략 플러그인 (RSI, MACD, 볼린저밴드, 이치모쿠, 듀얼모멘텀, 터틀브레이크아웃 등)
|
|
@@ -319,7 +326,7 @@ job = await pg.run_async(
|
|
|
319
326
|
|
|
320
327
|
```
|
|
321
328
|
5-Layer Architecture:
|
|
322
|
-
1. Registry Layer - 노드/플러그인 메타데이터 (
|
|
329
|
+
1. Registry Layer - 노드/플러그인 메타데이터 (76개 노드, 77개 플러그인)
|
|
323
330
|
2. Credential Layer - 인증 정보 관리
|
|
324
331
|
3. Definition Layer - JSON 워크플로우 정의 (노드, 엣지, 크레덴셜)
|
|
325
332
|
4. Job Layer - 상태 유지 실행 인스턴스 (최대 24시간 장기 실행)
|
|
@@ -2,6 +2,13 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
2
2
|
|
|
3
3
|
# ProgramGarden
|
|
4
4
|
|
|
5
|
+
The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
|
|
6
|
+
adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
|
|
7
|
+
expression templates authored in the workflow, routes Gemini and DeepSeek
|
|
8
|
+
credentials to their own APIs, and reads an unresolved futures position
|
|
9
|
+
exchange again at most once per 5 minutes per contract instead of keeping it
|
|
10
|
+
empty for the whole run.
|
|
11
|
+
|
|
5
12
|
The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
6
13
|
reads each futures position's and pending order's exchange from o3105 instead
|
|
7
14
|
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
@@ -278,7 +285,7 @@ job = await pg.run_async(
|
|
|
278
285
|
|
|
279
286
|
## 주요 특징
|
|
280
287
|
|
|
281
|
-
- **노드 기반 DSL**:
|
|
288
|
+
- **노드 기반 DSL**: 74개 내장 노드를 조합하여 코딩 없이 자동매매 전략 구성
|
|
282
289
|
- **실시간 처리**: WebSocket 기반 실시간 시세, 계좌, 체결 이벤트 수신
|
|
283
290
|
- **AI Agent 통합**: LLMModelNode + AIAgentNode로 LLM 기반 분석/의사결정
|
|
284
291
|
- **플러그인 확장**: 67개 내장 전략 플러그인 (RSI, MACD, 볼린저밴드, 이치모쿠, 듀얼모멘텀, 터틀브레이크아웃 등)
|
|
@@ -290,7 +297,7 @@ job = await pg.run_async(
|
|
|
290
297
|
|
|
291
298
|
```
|
|
292
299
|
5-Layer Architecture:
|
|
293
|
-
1. Registry Layer - 노드/플러그인 메타데이터 (
|
|
300
|
+
1. Registry Layer - 노드/플러그인 메타데이터 (76개 노드, 77개 플러그인)
|
|
294
301
|
2. Credential Layer - 인증 정보 관리
|
|
295
302
|
3. Definition Layer - JSON 워크플로우 정의 (노드, 엣지, 크레덴셜)
|
|
296
303
|
4. Job Layer - 상태 유지 실행 인스턴스 (최대 24시간 장기 실행)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Expressions run only from authored templates; runtime values are data.
|
|
2
|
+
|
|
3
|
+
Before 2.6.6 a node's config was evaluated twice: the workflow job resolved
|
|
4
|
+
``{{ … }}`` bindings (``WorkflowJob._resolve_config_expressions``) and the node
|
|
5
|
+
executor then evaluated the already-resolved config again
|
|
6
|
+
(``evaluate_all_bindings``). Any value produced at runtime that contained
|
|
7
|
+
``{{`` — LLM text, HTTP bodies, broker strings, workflow inputs, credentials —
|
|
8
|
+
was evaluated a second time in the workflow expression context.
|
|
9
|
+
|
|
10
|
+
Invariant: an expression is evaluated only if its template text was authored
|
|
11
|
+
in the workflow definition (the node config as stored in the DSL), and at most
|
|
12
|
+
once per execution of that node (per iteration for deferred ``{{ item }}``
|
|
13
|
+
bindings). The check is positional: a string at config path *P* is evaluated
|
|
14
|
+
only while it is still exactly the authored string at *P*. Once a template has
|
|
15
|
+
been resolved, the value at *P* is data and passes byte-identical.
|
|
16
|
+
|
|
17
|
+
Each workflow job registers an authored snapshot per node
|
|
18
|
+
(``ExecutionContext.register_authored_node_configs``). Without a snapshot (a
|
|
19
|
+
node run outside a workflow job, e.g. ``NodeRunner``) the engine keeps the
|
|
20
|
+
legacy behaviour and evaluates every template it is given, because that config
|
|
21
|
+
is the caller's authored input.
|
|
22
|
+
|
|
23
|
+
Source-code fields are never evaluated (``NEVER_EVALUATED_KEYS``).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import copy
|
|
29
|
+
from typing import Any, Dict, Optional
|
|
30
|
+
|
|
31
|
+
# A position the authored config does not have (engine-injected keys, runtime data).
|
|
32
|
+
NOT_AUTHORED = object()
|
|
33
|
+
# No authored snapshot for this node: evaluate every template (pre-2.6.6 behaviour).
|
|
34
|
+
LEGACY = object()
|
|
35
|
+
|
|
36
|
+
# Fields that hold source code, not templates. They are never evaluated, even
|
|
37
|
+
# when the text contains ``{{ … }}``.
|
|
38
|
+
NEVER_EVALUATED_KEYS: Dict[str, frozenset] = {
|
|
39
|
+
"CodeNode": frozenset({"code"}),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def authored_snapshot(node_type: str, config: Optional[Dict[str, Any]], fields: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
44
|
+
"""Deep copy of a node's authored config, as evaluation positions see it.
|
|
45
|
+
|
|
46
|
+
Plugin ``fields`` are exposed under the ``fields`` key (executors receive
|
|
47
|
+
them there). Never-evaluated keys are removed so their positions read as
|
|
48
|
+
not authored.
|
|
49
|
+
"""
|
|
50
|
+
snapshot = copy.deepcopy(dict(config or {}))
|
|
51
|
+
if fields and "fields" not in snapshot:
|
|
52
|
+
snapshot["fields"] = copy.deepcopy(fields)
|
|
53
|
+
for key in NEVER_EVALUATED_KEYS.get(node_type, ()):
|
|
54
|
+
snapshot.pop(key, None)
|
|
55
|
+
return snapshot
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def authored_root(context: Any, node_id: Optional[str]) -> Any:
|
|
59
|
+
"""The authored snapshot of ``node_id``, or ``LEGACY`` when none is registered."""
|
|
60
|
+
if node_id is None:
|
|
61
|
+
return LEGACY
|
|
62
|
+
getter = getattr(context, "authored_node_config", None)
|
|
63
|
+
if not callable(getter):
|
|
64
|
+
return LEGACY
|
|
65
|
+
try:
|
|
66
|
+
snapshot = getter(node_id)
|
|
67
|
+
except Exception: # pragma: no cover - defensive (mock contexts)
|
|
68
|
+
return LEGACY
|
|
69
|
+
return snapshot if isinstance(snapshot, dict) else LEGACY
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def child(authored: Any, key: Any) -> Any:
|
|
73
|
+
"""Authored value at ``key`` of a dict position."""
|
|
74
|
+
if authored is LEGACY:
|
|
75
|
+
return LEGACY
|
|
76
|
+
if isinstance(authored, dict):
|
|
77
|
+
return authored.get(key, NOT_AUTHORED)
|
|
78
|
+
return NOT_AUTHORED
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def element(authored: Any, index: int, length: int) -> Any:
|
|
82
|
+
"""Authored value at ``index`` of a list position of ``length`` items."""
|
|
83
|
+
if authored is LEGACY:
|
|
84
|
+
return LEGACY
|
|
85
|
+
if isinstance(authored, list) and len(authored) == length:
|
|
86
|
+
return authored[index]
|
|
87
|
+
return NOT_AUTHORED
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def authored_at(context: Any, node_id: Optional[str], *path: Any) -> Any:
|
|
91
|
+
"""Authored value at a top-level path of ``node_id`` (``LEGACY`` without a snapshot)."""
|
|
92
|
+
authored = authored_root(context, node_id)
|
|
93
|
+
for key in path:
|
|
94
|
+
authored = child(authored, key)
|
|
95
|
+
return authored
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def may_evaluate(value: Any, authored: Any) -> bool:
|
|
99
|
+
"""True when ``value`` is a template the author wrote at this position, unchanged."""
|
|
100
|
+
if not isinstance(value, str):
|
|
101
|
+
return False
|
|
102
|
+
if authored is LEGACY:
|
|
103
|
+
return True
|
|
104
|
+
return isinstance(authored, str) and value == authored
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def copy_containers(value: Any) -> Any:
|
|
108
|
+
"""Rebuild dicts/lists (leaves shared) without evaluating anything."""
|
|
109
|
+
if isinstance(value, dict):
|
|
110
|
+
return {k: copy_containers(v) for k, v in value.items()}
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
return [copy_containers(v) for v in value]
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _Literal:
|
|
117
|
+
"""Opaque wrapper that hides runtime data from ``ExpressionEvaluator.evaluate_fields``."""
|
|
118
|
+
|
|
119
|
+
__slots__ = ("value",)
|
|
120
|
+
|
|
121
|
+
def __init__(self, value: Any) -> None:
|
|
122
|
+
self.value = value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def shield(value: Any, authored: Any) -> Any:
|
|
126
|
+
"""Wrap every non-authored position so ``evaluate_fields`` leaves it untouched."""
|
|
127
|
+
if authored is LEGACY:
|
|
128
|
+
return value
|
|
129
|
+
if isinstance(value, str):
|
|
130
|
+
if "{{" in value and not may_evaluate(value, authored):
|
|
131
|
+
return _Literal(value)
|
|
132
|
+
return value
|
|
133
|
+
if isinstance(value, dict):
|
|
134
|
+
if isinstance(authored, dict):
|
|
135
|
+
return {k: shield(v, authored.get(k, NOT_AUTHORED)) for k, v in value.items()}
|
|
136
|
+
return _Literal(value)
|
|
137
|
+
if isinstance(value, list):
|
|
138
|
+
if isinstance(authored, list) and len(authored) == len(value):
|
|
139
|
+
return [shield(v, a) for v, a in zip(value, authored)]
|
|
140
|
+
return _Literal(value)
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _rebuild_like_evaluate_fields(value: Any) -> Any:
|
|
145
|
+
# evaluate_fields rebuilds dicts recursively and lists one level deep.
|
|
146
|
+
if isinstance(value, dict):
|
|
147
|
+
return {k: _rebuild_like_evaluate_fields(v) for k, v in value.items()}
|
|
148
|
+
if isinstance(value, list):
|
|
149
|
+
return list(value)
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def unshield(value: Any, authored: Any) -> Any:
|
|
154
|
+
"""Undo ``shield`` along the authored structure (resolved data is not walked)."""
|
|
155
|
+
if isinstance(value, _Literal):
|
|
156
|
+
return _rebuild_like_evaluate_fields(value.value)
|
|
157
|
+
if authored is LEGACY:
|
|
158
|
+
return value
|
|
159
|
+
if isinstance(value, dict) and isinstance(authored, dict):
|
|
160
|
+
return {k: unshield(v, authored.get(k, NOT_AUTHORED)) for k, v in value.items()}
|
|
161
|
+
if isinstance(value, list) and isinstance(authored, list) and len(value) == len(authored):
|
|
162
|
+
return [unshield(v, a) for v, a in zip(value, authored)]
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def shield_config(config: Dict[str, Any], authored: Any) -> Dict[str, Any]:
|
|
167
|
+
return {k: shield(v, child(authored, k)) for k, v in config.items()}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def unshield_config(config: Dict[str, Any], authored: Any) -> Dict[str, Any]:
|
|
171
|
+
return {k: unshield(v, child(authored, k)) for k, v in config.items()}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def evaluate_fields_authored(evaluator: Any, fields: Dict[str, Any], authored: Any, on_error: Any = None) -> Dict[str, Any]:
|
|
175
|
+
"""``ExpressionEvaluator.evaluate_fields`` limited to authored templates."""
|
|
176
|
+
shielded = shield_config(fields, authored)
|
|
177
|
+
if on_error is None:
|
|
178
|
+
resolved = evaluator.evaluate_fields(shielded)
|
|
179
|
+
else:
|
|
180
|
+
resolved = evaluator.evaluate_fields(shielded, on_error=on_error)
|
|
181
|
+
return unshield_config(resolved, authored)
|
|
@@ -331,8 +331,34 @@ class ExecutionContext:
|
|
|
331
331
|
# warn-and-continue behaviour untouched).
|
|
332
332
|
self._deep_unresolved_bindings: List[Dict[str, str]] = []
|
|
333
333
|
|
|
334
|
+
# Authored node configs (deep copies of the workflow definition). Only a
|
|
335
|
+
# template still at its authored position is evaluated; everything a node
|
|
336
|
+
# run produces is data. See programgarden.binding_provenance.
|
|
337
|
+
self._authored_node_configs: Dict[str, Dict[str, Any]] = {}
|
|
338
|
+
|
|
334
339
|
self._build_dag_index(workflow_edges, workflow_nodes)
|
|
335
340
|
|
|
341
|
+
# === Authored config (expression provenance) ===
|
|
342
|
+
|
|
343
|
+
def register_authored_node_configs(self, workflow: Any) -> None:
|
|
344
|
+
"""Snapshot every node's authored config from a resolved workflow."""
|
|
345
|
+
from programgarden.binding_provenance import authored_snapshot
|
|
346
|
+
|
|
347
|
+
nodes = getattr(workflow, "nodes", None)
|
|
348
|
+
if not isinstance(nodes, dict):
|
|
349
|
+
return
|
|
350
|
+
for node_id, node in nodes.items():
|
|
351
|
+
config = getattr(node, "config", None)
|
|
352
|
+
if not isinstance(config, dict):
|
|
353
|
+
continue
|
|
354
|
+
self._authored_node_configs[node_id] = authored_snapshot(
|
|
355
|
+
getattr(node, "node_type", ""), config, getattr(node, "fields", None),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def authored_node_config(self, node_id: str) -> Optional[Dict[str, Any]]:
|
|
359
|
+
"""Authored config snapshot of ``node_id``; None when the node has none."""
|
|
360
|
+
return self._authored_node_configs.get(node_id)
|
|
361
|
+
|
|
336
362
|
# === Storage Directory ===
|
|
337
363
|
|
|
338
364
|
def _resolve_db_path(self, db_filename: str) -> str:
|