odl-kernel 0.9.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.
odl_kernel/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2026 Centillion System, Inc. All rights reserved.
2
+ #
3
+ # This software is licensed under the Business Source License 1.1 (BSL 1.1).
4
+ # For full license terms, see the LICENSE file in the root directory.
5
+ #
6
+ # This software is "Source Available" but contains certain restrictions on
7
+ # commercial use and managed service provision.
8
+ # Usage by organizations with gross revenue exceeding $10M USD requires
9
+ # a commercial license.
10
+
11
+ from .types import (
12
+ JobSnapshot,
13
+ KernelEvent,
14
+ AnalysisResult
15
+ )
16
+ from .engine.analyzer import OdlAnalyzer
17
+ from .engine.logic.introspection import NodeInspector
18
+
19
+ # Main Entry Point
20
+ def analyze(snapshot: JobSnapshot, event: KernelEvent) -> AnalysisResult:
21
+ """
22
+ ODLカーネルのメインエントリポイント。
23
+ 物理法則に基づき、入力されたSnapshotとEventから次のアクションを導出する純粋関数。
24
+
25
+ Usage:
26
+ import odl_kernel
27
+ result = odl_kernel.analyze(snapshot, event)
28
+
29
+ Args:
30
+ snapshot: 現在のジョブの状態 (DBからロード済み)
31
+ event: トリガーとなった事象 (Tick, ActionCompleted等)
32
+
33
+ Returns:
34
+ AnalysisResult: 実行すべきコマンドと、更新されたノード状態
35
+ """
36
+ # Analyzerはステートレスなので、都度インスタンス化して実行する
37
+ # これにより odl.compile(...) と同じような関数的な使い心地を提供する
38
+ return OdlAnalyzer().analyze(snapshot, event)
39
+
40
+ __all__ = [
41
+ "analyze",
42
+ "JobSnapshot",
43
+ "KernelEvent",
44
+ "AnalysisResult",
45
+ "NodeInspector"
46
+ ]
@@ -0,0 +1,474 @@
1
+ # Copyright (c) 2026 Centillion System, Inc. All rights reserved.
2
+ #
3
+ # This software is licensed under the Business Source License 1.1 (BSL 1.1).
4
+ # For full license terms, see the LICENSE file in the root directory.
5
+ #
6
+ # This software is "Source Available" but contains certain restrictions on
7
+ # commercial use and managed service provision.
8
+ # Usage by organizations with gross revenue exceeding $10M USD requires
9
+ # a commercial license.
10
+
11
+ from typing import List, Dict, Any, Set, Optional
12
+ from odl.types import OpCode, NodeType, WiringObject
13
+
14
+ from odl_kernel.types import (
15
+ JobSnapshot, KernelEvent, AnalysisResult,
16
+ RuntimeCommand, CommandType, ProcessNode,
17
+ LifecycleStatus, JobStatus, JobUpdate,
18
+ ContextSchema, KernelEventType, BusinessResult
19
+ )
20
+ from .logic.node_id_generator import NodeIdGenerator
21
+ from .logic.transition_rules import TransitionRules
22
+ from .logic.variable_resolver import VariableResolver
23
+
24
+ # Strategies
25
+ from .logic.expansion.serial import SerialExpansionStrategy
26
+ from .logic.expansion.parallel import ParallelExpansionStrategy
27
+ from .logic.expansion.loop import LoopExpansionStrategy
28
+ from .logic.expansion.iterate import IterateExpansionStrategy
29
+
30
+
31
+ class OdlAnalyzer:
32
+ """
33
+ ODL実行の中核となる純粋関数エンジン (The Physics Engine)。
34
+
35
+ [Architecture: Deep Analyze & Signal/State Separation]
36
+
37
+ 1. Deep Analyze (Fixed-point Iteration):
38
+ 本関数は、入力された状態に対し、物理的に進行可能な限界(Action待ち or 完全停止)まで
39
+ 内部でサイクルを回し続け、収束した結果を一括で返却する。
40
+ これにより、Hostは「過渡状態」を意識することなく、常に安定した状態のみを観測できる。
41
+
42
+ 2. Signal (Command) vs State (UpdatedNodes):
43
+ - updated_nodes: 状態の「真実(Truth)」。Hostはこれを無条件にDB保存する。
44
+ - commands: 状態変化に伴う「シグナル」。Payloadは最小化され、分岐判断やログ通知に使用される。
45
+ """
46
+
47
+ # 無限ループ(物理崩壊)防止用の安全装置
48
+ MAX_ITERATIONS = 100
49
+
50
+ def analyze(self, snapshot: JobSnapshot, event: KernelEvent) -> AnalysisResult:
51
+ """
52
+ Main Entry Point: 物理法則に基づき、入力されたSnapshotとEventから次のアクションを導出する。
53
+ """
54
+ final_result = AnalysisResult()
55
+
56
+ # Working Memory (Deep Copy)
57
+ # 計算中の状態変更を即座に反映させるため、ミュータブルな辞書として展開する
58
+ working_nodes: Dict[str, ProcessNode] = {
59
+ k: v.model_copy(deep=True) for k, v in snapshot.nodes.items()
60
+ }
61
+
62
+ # -----------------------------------------------------------
63
+ # 0. Global Suppression Check
64
+ # -----------------------------------------------------------
65
+ if snapshot.job.status == JobStatus.STOPPING:
66
+ self._evaluate_suppression(snapshot, working_nodes, final_result)
67
+ return final_result
68
+
69
+ # -----------------------------------------------------------
70
+ # 1. Root Bootstrapping (Initial Spawn)
71
+ # -----------------------------------------------------------
72
+ if not working_nodes and snapshot.job.status == JobStatus.RUNNING:
73
+ self._bootstrap_root(snapshot, working_nodes, final_result)
74
+ # Root生成も状態変化の一種なので、ここからメインループに入る
75
+
76
+ # -----------------------------------------------------------
77
+ # 2. Main Deep Loop (Fixed-point Iteration)
78
+ # -----------------------------------------------------------
79
+ # cocrea-2201: Pipelined Analyze (Step A -> B -> C)
80
+ for _ in range(self.MAX_ITERATIONS):
81
+ has_change = False
82
+
83
+ # --- Step A: Transition (State Update) ---
84
+ # 完了判定、タイムアウト、WakeUp(Pending->Running)などを処理
85
+ active_nodes = [
86
+ n for n in working_nodes.values()
87
+ if n.lifecycle_status in (LifecycleStatus.PENDING, LifecycleStatus.RUNNING)
88
+ ]
89
+ for node in active_nodes:
90
+ if self._step_a_transition(node, working_nodes, event, final_result):
91
+ has_change = True
92
+
93
+ # --- Step B: Expansion (Structure Growth) ---
94
+ # Controlノードによる子ノード生成
95
+ control_nodes = [
96
+ n for n in working_nodes.values()
97
+ if n.node_type == NodeType.CONTROL
98
+ and n.lifecycle_status == LifecycleStatus.RUNNING
99
+ ]
100
+ for node in control_nodes:
101
+ if self._step_b_expansion(node, snapshot.job, working_nodes, final_result):
102
+ has_change = True
103
+
104
+ # --- Step C: Dispatch (External Trigger) ---
105
+ # 新規生成されたActionノードの実行開始
106
+ pending_nodes = [
107
+ n for n in working_nodes.values()
108
+ if n.lifecycle_status == LifecycleStatus.PENDING
109
+ ]
110
+ for node in pending_nodes:
111
+ if self._step_c_dispatch(node, working_nodes, final_result):
112
+ has_change = True
113
+
114
+ # --- Convergence Check ---
115
+ # 変化がなくなれば安定状態とみなして終了
116
+ if not has_change:
117
+ break
118
+
119
+ # -----------------------------------------------------------
120
+ # 3. Finalization
121
+ # -----------------------------------------------------------
122
+ self._evaluate_job_status(snapshot.job, working_nodes, final_result)
123
+
124
+ # -----------------------------------------------------------
125
+ # 4. Packaging (Unique Updates)
126
+ # -----------------------------------------------------------
127
+ # working_nodes は常に最新の状態を持っている。
128
+ # final_result.updated_nodes に記録されたID(変更があったもの)について、
129
+ # working_nodes から最新のオブジェクトを取り直して返却する。
130
+ touched_ids = {n.node_id for n in final_result.updated_nodes}
131
+ final_result.updated_nodes = [working_nodes[nid] for nid in touched_ids]
132
+
133
+ return final_result
134
+
135
+ # =========================================================================
136
+ # Internal Steps
137
+ # =========================================================================
138
+
139
+ def _bootstrap_root(self, snapshot: JobSnapshot, nodes: Dict[str, ProcessNode], result: AnalysisResult):
140
+ """Rootノードの初期生成"""
141
+ ir = snapshot.job.ir_root
142
+ id_gen = NodeIdGenerator(snapshot.job.job_id)
143
+ node_id = id_gen.generate(ir.stack_path)
144
+
145
+ # 1. State Update (Truth)
146
+ root_node = self._simulate_spawn(
147
+ node_id=node_id,
148
+ blueprint=ir,
149
+ resolved_path=ir.stack_path,
150
+ context_vars={},
151
+ parent_context=None
152
+ )
153
+ nodes[node_id] = root_node
154
+ result.updated_nodes.append(root_node)
155
+
156
+ # 2. Command (Signal)
157
+ cmd = RuntimeCommand(
158
+ type=CommandType.SPAWN_CHILD,
159
+ target_node_id=snapshot.job.root_node_id or node_id,
160
+ payload={
161
+ "child_node_id": node_id,
162
+ "is_root": True,
163
+ "child_opcode": ir.opcode,
164
+ "parent_opcode": None
165
+ }
166
+ )
167
+ result.commands.append(cmd)
168
+
169
+ def _step_a_transition(self, node: ProcessNode, nodes: Dict[str, ProcessNode], event: KernelEvent, result: AnalysisResult) -> bool:
170
+ """Step A: 状態遷移ロジックの評価と適用"""
171
+
172
+ # 1-A. External Event Handling (Action Completion)
173
+ if event.type == KernelEventType.ACTION_COMPLETED and event.target_node_id == node.node_id:
174
+ # ... (既存コード: ACTION_COMPLETED処理) ...
175
+ if node.lifecycle_status == LifecycleStatus.COMPLETED:
176
+ return False
177
+
178
+ from_status = node.lifecycle_status
179
+
180
+ node.lifecycle_status = LifecycleStatus.COMPLETED
181
+ node.business_result = event.payload.get("result", BusinessResult.NONE)
182
+
183
+ output_data = event.payload.get("output_data")
184
+ if output_data:
185
+ node.runtime_context.output_aggregation.append(output_data)
186
+
187
+ result.updated_nodes.append(node)
188
+
189
+ signal_cmd = RuntimeCommand(
190
+ type=CommandType.FINALIZE,
191
+ target_node_id=node.node_id,
192
+ payload={
193
+ "from_status": from_status,
194
+ "to_status": LifecycleStatus.COMPLETED,
195
+ "result": node.business_result
196
+ }
197
+ )
198
+ result.commands.append(signal_cmd)
199
+ return True
200
+
201
+ # 1-B. External Event Handling (Data Resolved) [UPDATED]
202
+ if event.type == KernelEventType.DATA_RESOLVED and event.target_node_id == node.node_id:
203
+ if node.lifecycle_status == LifecycleStatus.COMPLETED:
204
+ return False
205
+
206
+ # [Physics Fix] Idempotency Check
207
+ if node.runtime_context.output_aggregation:
208
+ pass
209
+ else:
210
+ items = event.payload.get("items")
211
+ resolved_id = event.payload.get("resolved_id")
212
+
213
+ updated = False
214
+ if items is not None:
215
+ node.runtime_context.output_aggregation.append(items)
216
+ updated = True
217
+ elif resolved_id is not None:
218
+ node.runtime_context.output_aggregation.append(resolved_id)
219
+ updated = True
220
+
221
+ if updated:
222
+ # [Fix] データ解決したので、待機フラグを下ろす
223
+ node.runtime_context.system_variables.pop("__waiting_for_data", None)
224
+
225
+ result.updated_nodes.append(node)
226
+ return True
227
+
228
+ # 2. Internal Rule Evaluation
229
+ children = [nodes[cid] for cid in node.runtime_context.children_ids if cid in nodes]
230
+ logic_cmd = TransitionRules.evaluate(node, children, event.occurred_at)
231
+
232
+ if logic_cmd:
233
+ from_status = node.lifecycle_status
234
+
235
+ # State Update Application
236
+ if logic_cmd.type == CommandType.TRANSITION:
237
+ node.lifecycle_status = logic_cmd.payload["to_status"]
238
+ elif logic_cmd.type == CommandType.FINALIZE:
239
+ node.lifecycle_status = LifecycleStatus.COMPLETED
240
+ node.business_result = logic_cmd.payload["result"]
241
+
242
+ elif logic_cmd.type == CommandType.REQUIRE_DATA:
243
+ # [Fix] 要求を出したので、待機フラグを立てる (State Mutation)
244
+ # これにより、次回のEvaluateで二重発行が抑制され、ループが収束する
245
+ node.runtime_context.system_variables["__waiting_for_data"] = True
246
+
247
+ result.updated_nodes.append(node)
248
+
249
+ # Signal
250
+ signal_payload = {
251
+ "from_status": from_status,
252
+ "to_status": node.lifecycle_status
253
+ }
254
+ if logic_cmd.payload:
255
+ signal_payload.update(logic_cmd.payload)
256
+
257
+ signal_cmd = RuntimeCommand(
258
+ type=logic_cmd.type,
259
+ target_node_id=node.node_id,
260
+ payload=signal_payload
261
+ )
262
+ result.commands.append(signal_cmd)
263
+ return True
264
+
265
+ return False
266
+
267
+ def _step_b_expansion(self, node: ProcessNode, job, nodes: Dict[str, ProcessNode], result: AnalysisResult) -> bool:
268
+ """Step B: 子ノードの展開ロジック"""
269
+ strategy = self._select_strategy(node.opcode)
270
+ if not strategy: return False
271
+
272
+ children = [nodes[cid] for cid in node.runtime_context.children_ids if cid in nodes]
273
+ plans = strategy.plan_next_nodes(node, children)
274
+ if not plans: return False
275
+
276
+ id_gen = NodeIdGenerator(job.job_id)
277
+ has_spawn = False
278
+
279
+ for plan in plans:
280
+ new_id = id_gen.generate(plan.resolved_path)
281
+
282
+ # 1. State Update (Truth)
283
+ # Strategyの計画に加え、親のContextを継承・シフトさせて生成する
284
+ # [Update] params_override を渡す
285
+ new_node = self._simulate_spawn(
286
+ node_id=new_id,
287
+ blueprint=plan.blueprint,
288
+ resolved_path=plan.resolved_path,
289
+ context_vars=plan.context_vars,
290
+ parent_context=node.runtime_context,
291
+ params_override=plan.params_override # <--- NEW
292
+ )
293
+
294
+ # Link Parent -> Child
295
+ node.runtime_context.children_ids.append(new_id)
296
+ nodes[new_id] = new_node
297
+
298
+ result.updated_nodes.append(new_node)
299
+ result.updated_nodes.append(node)
300
+
301
+ # 2. Command (Signal)
302
+ cmd = RuntimeCommand(
303
+ type=CommandType.SPAWN_CHILD,
304
+ target_node_id=node.node_id,
305
+ payload={
306
+ "child_node_id": new_id,
307
+ "blueprint_selector": plan.original_index,
308
+ "parent_opcode": node.opcode,
309
+ "child_opcode": plan.blueprint.opcode
310
+ }
311
+ )
312
+ result.commands.append(cmd)
313
+ has_spawn = True
314
+
315
+ return has_spawn
316
+
317
+ def _step_c_dispatch(self, node: ProcessNode, nodes: Dict[str, ProcessNode], result: AnalysisResult) -> bool:
318
+ """Step C: 実行指示ロジック"""
319
+ # PENDING以外は無視
320
+ if node.lifecycle_status != LifecycleStatus.PENDING:
321
+ return False
322
+
323
+ # Control/Logic Nodeは自己駆動するためDispatch対象外
324
+ if node.node_type in (NodeType.CONTROL, NodeType.LOGIC):
325
+ return False
326
+
327
+ # 1. State Update (Truth)
328
+ # DispatchされたノードはHost側で即座にRUNNINGにされる前提
329
+ node.lifecycle_status = LifecycleStatus.RUNNING
330
+ result.updated_nodes.append(node)
331
+
332
+ # 2. Command (Signal)
333
+ cmd = RuntimeCommand(
334
+ type=CommandType.DISPATCH,
335
+ target_node_id=node.node_id,
336
+ payload={}
337
+ )
338
+ result.commands.append(cmd)
339
+
340
+ return True
341
+
342
+ # =========================================================================
343
+ # Helpers
344
+ # =========================================================================
345
+
346
+ def _simulate_spawn(
347
+ self,
348
+ node_id: str,
349
+ blueprint,
350
+ resolved_path: str,
351
+ context_vars: Dict[str, Any] = None,
352
+ parent_context: ContextSchema = None,
353
+ params_override: Dict[str, Any] = None # <--- [NEW] 追加引数
354
+ ) -> ProcessNode:
355
+ """
356
+ [Physics Helper] 完全なProcessNodeオブジェクトをメモリ上に生成する。
357
+
358
+ Physics Rules:
359
+ 1. Context Inheritance & Shifting:
360
+ 親の変数を継承し、ネストされたループ変数 ($LOOP -> $LOOP^1) をシフトする。
361
+ 2. Variable Override:
362
+ Strategyが指定した新しい変数 ($LOOP, $KEY) で上書きする。
363
+ 3. Params Override:
364
+ Strategyが指定した動的パラメータ (items等) で静的定義を上書きする。
365
+ 4. Wiring Resolution:
366
+ コンテキストに基づき、BlueprintのWiring定義内の変数を解決・フィルタリングする。
367
+ """
368
+ # 0. Extract Children Blueprints
369
+ children_bps = []
370
+ if blueprint.children:
371
+ children_bps = blueprint.children
372
+ elif blueprint.contents:
373
+ children_bps = [blueprint.contents]
374
+
375
+ # 1. Inherit & Shift System Variables
376
+ inherited_sys = {}
377
+ inherited_user = {}
378
+
379
+ if parent_context:
380
+ inherited_user = parent_context.user_variables.copy()
381
+
382
+ # [Logic] Context Key Shifting
383
+ # 新しいコンテキスト変数が「$LOOP」や「$KEY」を注入しようとしている場合(= 新しいスコープ)、
384
+ # 親から継承する $LOOP 系変数の深度をシフトする。
385
+ should_shift = context_vars and ("$LOOP" in context_vars or "$KEY" in context_vars)
386
+
387
+ for k, v in parent_context.system_variables.items():
388
+ if should_shift and k.startswith("$LOOP"):
389
+ # $LOOP -> $LOOP^1
390
+ if k == "$LOOP":
391
+ inherited_sys["$LOOP^1"] = v
392
+ else:
393
+ # $LOOP^N -> $LOOP^{N+1}
394
+ try:
395
+ parts = k.split("^")
396
+ if len(parts) == 2:
397
+ depth = int(parts[1])
398
+ inherited_sys[f"$LOOP^{depth+1}"] = v
399
+ except ValueError:
400
+ pass # 形式不正な変数は無視
401
+ else:
402
+ # その他の変数はそのまま継承
403
+ inherited_sys[k] = v
404
+
405
+ # 2. Override with Plan (Strategy) vars
406
+ if context_vars:
407
+ inherited_sys.update(context_vars)
408
+
409
+ # Context初期化
410
+ ctx = ContextSchema(
411
+ system_variables=inherited_sys,
412
+ user_variables=inherited_user
413
+ )
414
+
415
+ # 3. Parameters Merge [NEW]
416
+ # Blueprintの静的パラメータをベースに、Overrideを適用する。
417
+ # Blueprint自体は変更せず、コピーに対して操作する。
418
+ merged_params = blueprint.params.copy()
419
+ if params_override:
420
+ merged_params.update(params_override)
421
+
422
+ # 4. Resolve Wiring
423
+ # 解決済みのコンテキスト(ctx.system_variables)を使ってWiringを解決する
424
+ # これにより {$LOOP-1} 等が物理的な値に置換され、無効な参照は除外される
425
+ raw_wiring = blueprint.wiring or WiringObject()
426
+ resolved_wiring = VariableResolver.resolve_wiring(raw_wiring, ctx.system_variables)
427
+
428
+ return ProcessNode(
429
+ node_id=node_id,
430
+ stack_path=resolved_path,
431
+ node_type=blueprint.node_type,
432
+ opcode=blueprint.opcode,
433
+ wiring=resolved_wiring, # Resolved
434
+ params=merged_params, # <--- [NEW] Merged Paramsを使用
435
+ children_blueprint=children_bps,
436
+ lifecycle_status=LifecycleStatus.PENDING,
437
+ runtime_context=ctx
438
+ )
439
+
440
+ def _select_strategy(self, opcode: OpCode):
441
+ if opcode == OpCode.SERIAL: return SerialExpansionStrategy()
442
+ elif opcode == OpCode.PARALLEL: return ParallelExpansionStrategy()
443
+ elif opcode == OpCode.LOOP: return LoopExpansionStrategy()
444
+ elif opcode == OpCode.ITERATE: return IterateExpansionStrategy()
445
+ return None
446
+
447
+ def _evaluate_suppression(self, snapshot, nodes, result):
448
+ pass # 将来的な実装用
449
+
450
+ def _evaluate_job_status(self, job, nodes, result):
451
+ """ジョブ全体のステータス遷移判定"""
452
+
453
+ # 1. 鎮火判定 (STOPPING -> CANCELLED)
454
+ if job.status == JobStatus.STOPPING:
455
+ running = [n for n in nodes.values() if n.lifecycle_status == LifecycleStatus.RUNNING]
456
+ if not running:
457
+ result.job_update = JobUpdate(status=JobStatus.CANCELLED)
458
+ return
459
+
460
+ # 2. 完了/失敗判定 (RUNNING -> ALL_DONE / FAILED)
461
+ if job.status == JobStatus.RUNNING:
462
+ # Rootノードが特定できない場合(Bootstrap前など)は何もしない
463
+ if not job.root_node_id or job.root_node_id not in nodes:
464
+ return
465
+
466
+ root_node = nodes[job.root_node_id]
467
+
468
+ # Case A: Rootが失敗 -> Jobも失敗
469
+ if root_node.lifecycle_status == LifecycleStatus.FAILED:
470
+ result.job_update = JobUpdate(status=JobStatus.FAILED)
471
+
472
+ # Case B: Rootが正常完了 -> Jobは全工程完了 (ALL_DONE)
473
+ elif root_node.lifecycle_status == LifecycleStatus.COMPLETED:
474
+ result.job_update = JobUpdate(status=JobStatus.ALL_DONE)
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2026 Centillion System, Inc. All rights reserved.
2
+ #
3
+ # This software is licensed under the Business Source License 1.1 (BSL 1.1).
4
+ # For full license terms, see the LICENSE file in the root directory.
5
+ #
6
+ # This software is "Source Available" but contains certain restrictions on
7
+ # commercial use and managed service provision.
8
+ # Usage by organizations with gross revenue exceeding $10M USD requires
9
+ # a commercial license.
10
+
11
+ from .base import (
12
+ ExpansionPlan,
13
+ ExpansionStrategy
14
+ )
15
+
16
+ from .serial import SerialExpansionStrategy
17
+ from .parallel import ParallelExpansionStrategy
18
+ from .iterate import IterateExpansionStrategy
19
+ from .loop import LoopExpansionStrategy
20
+
21
+ __all__ = [
22
+ "ExpansionPlan",
23
+ "ExpansionStrategy",
24
+ "SerialExpansionStrategy",
25
+ "ParallelExpansionStrategy",
26
+ "IterateExpansionStrategy",
27
+ "LoopExpansionStrategy",
28
+ ]
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 Centillion System, Inc. All rights reserved.
2
+ #
3
+ # This software is licensed under the Business Source License 1.1 (BSL 1.1).
4
+ # For full license terms, see the LICENSE file in the root directory.
5
+ #
6
+ # This software is "Source Available" but contains certain restrictions on
7
+ # commercial use and managed service provision.
8
+ # Usage by organizations with gross revenue exceeding $10M USD requires
9
+ # a commercial license.
10
+
11
+ from abc import ABC, abstractmethod
12
+ from typing import List, Dict, Any, Optional
13
+ from dataclasses import dataclass
14
+
15
+ from odl.types import IrComponent
16
+ from odl_kernel.types import ProcessNode
17
+
18
+ @dataclass
19
+ class ExpansionPlan:
20
+ """
21
+ ExpansionStrategyが導出する「次の一手」の定義。
22
+ Analyzeフェーズの出力であり、RuntimeCommand (SPAWN_CHILD) の生成源となる。
23
+ """
24
+ blueprint: IrComponent # 実体化すべきノードの定義 (IR)
25
+ context_vars: Dict[str, Any] # 注入すべきコンテキスト変数 ($LOOP, $PREV 等)
26
+ resolved_path: str # トークン解決済みの物理パス (ID生成の種)
27
+ original_index: int # 親のchildren_blueprintリストにおけるインデックス (Selector用)
28
+
29
+ # 動的に取得したデータ(items等)を注入するために使用する
30
+ params_override: Optional[Dict[str, Any]] = None
31
+
32
+ class ExpansionStrategy(ABC):
33
+ """
34
+ Expansion Logic Interface
35
+
36
+ Architecture:
37
+ L3 Mechanism (Logic) - Pure Domain Logic
38
+
39
+ Responsibility:
40
+ 親ノードの状態と、現在の子ノード群(Current Children)に基づき、
41
+ 「次に生成すべき子ノード」の計画(ExpansionPlan)を立案する。
42
+
43
+ 具体的な展開ロジック(順次、一括、反復等)は、継承先の各Strategyクラスで定義される。
44
+ StrategyはDBへの書き込みや副作用を持たず、純粋に計画オブジェクトを返すことに徹する。
45
+ """
46
+
47
+ @abstractmethod
48
+ def plan_next_nodes(
49
+ self,
50
+ parent: ProcessNode,
51
+ current_children: List[ProcessNode]
52
+ ) -> List[ExpansionPlan]:
53
+ """
54
+ Args:
55
+ parent: 親ノード(Blueprints, Resolved Path保持)
56
+ current_children: 既に生成されている子ノードのリスト
57
+
58
+ Returns:
59
+ List[ExpansionPlan]: 新規に生成すべきノードの計画リスト。
60
+ 生成すべきものがなければ空リストを返す。
61
+ """
62
+ pass
63
+
64
+ def _get_node_name(self, blueprint: IrComponent) -> str:
65
+ """
66
+ Blueprintのパスから、末尾のノード名部分のみを抽出するヘルパーメソッド。
67
+
68
+ Usage:
69
+ Strict Path Resolution (Inheritance) のために使用される。
70
+ Blueprint全体のパス(未解決トークンを含む可能性がある)ではなく、
71
+ このメソッドで得た名前を親パスに結合することで、安全な物理パスを生成する。
72
+
73
+ Example: "root/loop/v{$LOOP}/worker_0" -> "worker_0"
74
+ """
75
+ return blueprint.stack_path.split("/")[-1]