programgarden 2.6.5__tar.gz → 2.6.7__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.7}/PKG-INFO +24 -6
- {programgarden-2.6.5 → programgarden-2.6.7}/README.md +21 -3
- programgarden-2.6.7/programgarden/binding_provenance.py +181 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/context.py +26 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/executor.py +384 -74
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_exchange.py +53 -18
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/node_runner.py +2 -1
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/providers/llm_provider.py +17 -1
- programgarden-2.6.7/programgarden/ranking_nodes.py +478 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resolver.py +45 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/validation_replay.py +9 -2
- {programgarden-2.6.5 → programgarden-2.6.7}/pyproject.toml +3 -3
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/binding_validator.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/client.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/code_worker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/broker_evidence.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/broker_snapshot.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_history.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/incremental_build.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/paper_futures_business_date.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_events.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_external.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_orders.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_sources.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resource/context.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.6.5 → programgarden-2.6.7}/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.7
|
|
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.5,<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,20 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
31
31
|
|
|
32
32
|
# ProgramGarden
|
|
33
33
|
|
|
34
|
+
The 2.6.7 release [futures TC2 confirmation handling](../../docs/release-2.6.7-validation.md)
|
|
35
|
+
requires programgarden-finance 2.0.5, which reads overseas futures modify and
|
|
36
|
+
cancel confirmations (TC2) as LS sends them and re-registers account events
|
|
37
|
+
after a reconnect. The engine finds the workflow's order through
|
|
38
|
+
`orgn_ordr_no`, uses the guide's side codes and skips frames the SDK could
|
|
39
|
+
not read.
|
|
40
|
+
|
|
41
|
+
The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
|
|
42
|
+
adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
|
|
43
|
+
expression templates authored in the workflow, routes Gemini and DeepSeek
|
|
44
|
+
credentials to their own APIs, and reads an unresolved futures position
|
|
45
|
+
exchange again at most once per 5 minutes per contract instead of keeping it
|
|
46
|
+
empty for the whole run.
|
|
47
|
+
|
|
34
48
|
The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
35
49
|
reads each futures position's and pending order's exchange from o3105 instead
|
|
36
50
|
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
@@ -198,7 +212,11 @@ exchange execution numbers and broker execution numbers are not interchangeable.
|
|
|
198
212
|
This ledger API alone does not reconcile finite workflows after their shutdown.
|
|
199
213
|
|
|
200
214
|
Standalone futures TC3 callbacks interpret `s_b_ccd="1"` as sell and `"2"` as buy,
|
|
201
|
-
matching the SDK contract.
|
|
215
|
+
matching the SDK contract. TC2 confirmations use the same codes and find the
|
|
216
|
+
workflow's order through `orgn_ordr_no`: a futures modify or cancel gets its own
|
|
217
|
+
new order number (LS paper, 2026-10-15), so the cancel confirmation cancels the
|
|
218
|
+
original order in the ledger and a modify confirmation updates the successor row
|
|
219
|
+
recorded by ModifyOrderNodeExecutor. Unknown or blank side codes are logged and skipped
|
|
202
220
|
before any inventory write is scheduled. With an app order lifecycle handler,
|
|
203
221
|
TC3 still bypasses this legacy fill path: canonical REST reconciliation remains
|
|
204
222
|
the sole writer. This side correction does not establish an alias between TC3
|
|
@@ -307,7 +325,7 @@ job = await pg.run_async(
|
|
|
307
325
|
|
|
308
326
|
## 주요 특징
|
|
309
327
|
|
|
310
|
-
- **노드 기반 DSL**:
|
|
328
|
+
- **노드 기반 DSL**: 74개 내장 노드를 조합하여 코딩 없이 자동매매 전략 구성
|
|
311
329
|
- **실시간 처리**: WebSocket 기반 실시간 시세, 계좌, 체결 이벤트 수신
|
|
312
330
|
- **AI Agent 통합**: LLMModelNode + AIAgentNode로 LLM 기반 분석/의사결정
|
|
313
331
|
- **플러그인 확장**: 67개 내장 전략 플러그인 (RSI, MACD, 볼린저밴드, 이치모쿠, 듀얼모멘텀, 터틀브레이크아웃 등)
|
|
@@ -319,7 +337,7 @@ job = await pg.run_async(
|
|
|
319
337
|
|
|
320
338
|
```
|
|
321
339
|
5-Layer Architecture:
|
|
322
|
-
1. Registry Layer - 노드/플러그인 메타데이터 (
|
|
340
|
+
1. Registry Layer - 노드/플러그인 메타데이터 (76개 노드, 77개 플러그인)
|
|
323
341
|
2. Credential Layer - 인증 정보 관리
|
|
324
342
|
3. Definition Layer - JSON 워크플로우 정의 (노드, 엣지, 크레덴셜)
|
|
325
343
|
4. Job Layer - 상태 유지 실행 인스턴스 (최대 24시간 장기 실행)
|
|
@@ -2,6 +2,20 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
|
|
|
2
2
|
|
|
3
3
|
# ProgramGarden
|
|
4
4
|
|
|
5
|
+
The 2.6.7 release [futures TC2 confirmation handling](../../docs/release-2.6.7-validation.md)
|
|
6
|
+
requires programgarden-finance 2.0.5, which reads overseas futures modify and
|
|
7
|
+
cancel confirmations (TC2) as LS sends them and re-registers account events
|
|
8
|
+
after a reconnect. The engine finds the workflow's order through
|
|
9
|
+
`orgn_ordr_no`, uses the guide's side codes and skips frames the SDK could
|
|
10
|
+
not read.
|
|
11
|
+
|
|
12
|
+
The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
|
|
13
|
+
adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
|
|
14
|
+
expression templates authored in the workflow, routes Gemini and DeepSeek
|
|
15
|
+
credentials to their own APIs, and reads an unresolved futures position
|
|
16
|
+
exchange again at most once per 5 minutes per contract instead of keeping it
|
|
17
|
+
empty for the whole run.
|
|
18
|
+
|
|
5
19
|
The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
|
|
6
20
|
reads each futures position's and pending order's exchange from o3105 instead
|
|
7
21
|
of a fixed `HKEX`; an unconfirmed exchange stays empty.
|
|
@@ -169,7 +183,11 @@ exchange execution numbers and broker execution numbers are not interchangeable.
|
|
|
169
183
|
This ledger API alone does not reconcile finite workflows after their shutdown.
|
|
170
184
|
|
|
171
185
|
Standalone futures TC3 callbacks interpret `s_b_ccd="1"` as sell and `"2"` as buy,
|
|
172
|
-
matching the SDK contract.
|
|
186
|
+
matching the SDK contract. TC2 confirmations use the same codes and find the
|
|
187
|
+
workflow's order through `orgn_ordr_no`: a futures modify or cancel gets its own
|
|
188
|
+
new order number (LS paper, 2026-10-15), so the cancel confirmation cancels the
|
|
189
|
+
original order in the ledger and a modify confirmation updates the successor row
|
|
190
|
+
recorded by ModifyOrderNodeExecutor. Unknown or blank side codes are logged and skipped
|
|
173
191
|
before any inventory write is scheduled. With an app order lifecycle handler,
|
|
174
192
|
TC3 still bypasses this legacy fill path: canonical REST reconciliation remains
|
|
175
193
|
the sole writer. This side correction does not establish an alias between TC3
|
|
@@ -278,7 +296,7 @@ job = await pg.run_async(
|
|
|
278
296
|
|
|
279
297
|
## 주요 특징
|
|
280
298
|
|
|
281
|
-
- **노드 기반 DSL**:
|
|
299
|
+
- **노드 기반 DSL**: 74개 내장 노드를 조합하여 코딩 없이 자동매매 전략 구성
|
|
282
300
|
- **실시간 처리**: WebSocket 기반 실시간 시세, 계좌, 체결 이벤트 수신
|
|
283
301
|
- **AI Agent 통합**: LLMModelNode + AIAgentNode로 LLM 기반 분석/의사결정
|
|
284
302
|
- **플러그인 확장**: 67개 내장 전략 플러그인 (RSI, MACD, 볼린저밴드, 이치모쿠, 듀얼모멘텀, 터틀브레이크아웃 등)
|
|
@@ -290,7 +308,7 @@ job = await pg.run_async(
|
|
|
290
308
|
|
|
291
309
|
```
|
|
292
310
|
5-Layer Architecture:
|
|
293
|
-
1. Registry Layer - 노드/플러그인 메타데이터 (
|
|
311
|
+
1. Registry Layer - 노드/플러그인 메타데이터 (76개 노드, 77개 플러그인)
|
|
294
312
|
2. Credential Layer - 인증 정보 관리
|
|
295
313
|
3. Definition Layer - JSON 워크플로우 정의 (노드, 엣지, 크레덴셜)
|
|
296
314
|
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:
|