clm-kernel 0.0.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.
Files changed (333) hide show
  1. clm/__init__.py +230 -0
  2. clm/__main__.py +25 -0
  3. clm/cross_cutting/__init__.py +50 -0
  4. clm/cross_cutting/_test_runner.py +36 -0
  5. clm/cross_cutting/async_utils.py +25 -0
  6. clm/cross_cutting/cel_builder.py +123 -0
  7. clm/cross_cutting/cel_converter.py +697 -0
  8. clm/cross_cutting/cel_engine.py +252 -0
  9. clm/cross_cutting/cel_ops.py +72 -0
  10. clm/cross_cutting/cel_ops_db.py +157 -0
  11. clm/cross_cutting/cel_ops_files.py +99 -0
  12. clm/cross_cutting/cel_ops_state.py +71 -0
  13. clm/cross_cutting/cel_ops_strings.py +64 -0
  14. clm/cross_cutting/cel_resolver.py +63 -0
  15. clm/cross_cutting/cel_slices.py +56 -0
  16. clm/cross_cutting/cel_store.py +330 -0
  17. clm/cross_cutting/clm_logger.py +140 -0
  18. clm/cross_cutting/config/__init__.py +155 -0
  19. clm/cross_cutting/config/config_constants.py +392 -0
  20. clm/cross_cutting/config/env_parameters.py +90 -0
  21. clm/cross_cutting/config/logging.py +378 -0
  22. clm/cross_cutting/config/settings.py +504 -0
  23. clm/cross_cutting/context.py +5 -0
  24. clm/cross_cutting/domain_models.py +55 -0
  25. clm/cross_cutting/domain_types.py +61 -0
  26. clm/cross_cutting/effects.py +67 -0
  27. clm/cross_cutting/errors.py +153 -0
  28. clm/cross_cutting/governance.py +331 -0
  29. clm/cross_cutting/guards.py +41 -0
  30. clm/cross_cutting/mcard.py +321 -0
  31. clm/cross_cutting/native.py +185 -0
  32. clm/cross_cutting/observability.py +289 -0
  33. clm/cross_cutting/pcard.py +434 -0
  34. clm/cross_cutting/registry.py +71 -0
  35. clm/cross_cutting/resources.py +84 -0
  36. clm/cross_cutting/secrets.py +44 -0
  37. clm/cross_cutting/subprocess_utils.py +118 -0
  38. clm/cross_cutting/telemetry.py +164 -0
  39. clm/cross_cutting/testing/__init__.py +8 -0
  40. clm/cross_cutting/testing/comparator.py +187 -0
  41. clm/cross_cutting/testing/datasets.py +87 -0
  42. clm/cross_cutting/timeutil.py +20 -0
  43. clm/cross_cutting/types/new_type_check.py +0 -0
  44. clm/cross_cutting/utils/__init__.py +1 -0
  45. clm/cross_cutting/utils/url_safety.py +50 -0
  46. clm/cross_cutting/vcard.py +560 -0
  47. clm/cross_cutting/writer.py +118 -0
  48. clm/layer0/README.md +50 -0
  49. clm/layer0/__init__.py +223 -0
  50. clm/layer0/algebra.py +60 -0
  51. clm/layer0/algebra_baldwin.py +133 -0
  52. clm/layer0/algebra_baldwin_generators.py +36 -0
  53. clm/layer0/algebra_equivalence.py +262 -0
  54. clm/layer0/algebra_ingest.py +306 -0
  55. clm/layer0/algebra_manifest_io.py +156 -0
  56. clm/layer0/assembler.py +481 -0
  57. clm/layer0/cel/__init__.py +23 -0
  58. clm/layer0/cel_eval.py +491 -0
  59. clm/layer0/codec.py +106 -0
  60. clm/layer0/context.py +122 -0
  61. clm/layer0/db.py +160 -0
  62. clm/layer0/exceptions.py +73 -0
  63. clm/layer0/fibration.py +106 -0
  64. clm/layer0/gates.py +50 -0
  65. clm/layer0/hash.py +80 -0
  66. clm/layer0/loader.py +826 -0
  67. clm/layer0/math_utils.py +163 -0
  68. clm/layer0/mcard.py +96 -0
  69. clm/layer0/model/__init__.py +0 -0
  70. clm/layer0/model/action.py +223 -0
  71. clm/layer0/model/card.py +280 -0
  72. clm/layer0/model/card_triad.py +427 -0
  73. clm/layer0/model/compatibility.py +60 -0
  74. clm/layer0/model/dictionary.py +46 -0
  75. clm/layer0/model/dots.py +331 -0
  76. clm/layer0/model/event_producer.py +88 -0
  77. clm/layer0/model/g_time.py +89 -0
  78. clm/layer0/model/handle.py +163 -0
  79. clm/layer0/model/hash/__init__.py +6 -0
  80. clm/layer0/model/hash/algorithms/__init__.py +3 -0
  81. clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
  82. clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
  83. clm/layer0/model/hash/constants.py +53 -0
  84. clm/layer0/model/hash/enums.py +60 -0
  85. clm/layer0/model/hash/validator.py +307 -0
  86. clm/layer0/model/interpreter.py +275 -0
  87. clm/layer0/model/pagination.py +45 -0
  88. clm/layer0/model/pcard.py +716 -0
  89. clm/layer0/model/schema.py +44 -0
  90. clm/layer0/model/utils/__init__.py +1 -0
  91. clm/layer0/model/utils/content_analyzer.py +152 -0
  92. clm/layer0/model/validators/__init__.py +1 -0
  93. clm/layer0/model/validators/base_validator.py +44 -0
  94. clm/layer0/model/validators/binary_validator.py +66 -0
  95. clm/layer0/model/validators/text_validator.py +82 -0
  96. clm/layer0/model/validators/validation_registry.py +49 -0
  97. clm/layer0/model/vcard.py +823 -0
  98. clm/layer0/model/vcard_ext/__init__.py +39 -0
  99. clm/layer0/model/vcard_ext/core.py +60 -0
  100. clm/layer0/model/vcard_ext/network.py +23 -0
  101. clm/layer0/model/vcard_ext/observability.py +63 -0
  102. clm/layer0/model/vcard_ext/storage.py +60 -0
  103. clm/layer0/model/vcard_ext/vendors.py +211 -0
  104. clm/layer0/model/vcard_sandwich.py +119 -0
  105. clm/layer0/model/vcard_vocabulary.py +463 -0
  106. clm/layer0/model/workflow.py +274 -0
  107. clm/layer0/narrative.py +431 -0
  108. clm/layer0/ontology/collection.py +125 -0
  109. clm/layer0/parser.py +429 -0
  110. clm/layer0/relations.py +73 -0
  111. clm/layer0/rule_evaluator.py +251 -0
  112. clm/layer0/schema/__init__.py +38 -0
  113. clm/layer0/schema/ddl.py +97 -0
  114. clm/layer0/templates/abstract.yaml +99 -0
  115. clm/layer0/templates/balanced.yaml +253 -0
  116. clm/layer0/templates/concrete.yaml +172 -0
  117. clm/layer0/type_registry.py +223 -0
  118. clm/layer0/types.py +204 -0
  119. clm/layer0/utils.py +886 -0
  120. clm/layer0/verifier_core.py +262 -0
  121. clm/layer1/__init__.py +31 -0
  122. clm/layer1/action_dispatcher.py +275 -0
  123. clm/layer1/adapter_registry.py +233 -0
  124. clm/layer1/algebra_baldwin_generators.py +192 -0
  125. clm/layer1/arrow_compiler.py +525 -0
  126. clm/layer1/batch_processor.py +152 -0
  127. clm/layer1/cel/__init__.py +27 -0
  128. clm/layer1/cel/builder.py +173 -0
  129. clm/layer1/engine.py +759 -0
  130. clm/layer1/engine_async.py +89 -0
  131. clm/layer1/eval_callback.py +49 -0
  132. clm/layer1/gate_composer.py +229 -0
  133. clm/layer1/hoare_sandwich.py +298 -0
  134. clm/layer1/identity_guard.py +348 -0
  135. clm/layer1/matrix.py +138 -0
  136. clm/layer1/neural_runtime.py +221 -0
  137. clm/layer1/operations/__init__.py +123 -0
  138. clm/layer1/operations/augment.py +156 -0
  139. clm/layer1/operations/base.py +109 -0
  140. clm/layer1/operations/builtins.py +338 -0
  141. clm/layer1/operations/dispatch.py +144 -0
  142. clm/layer1/operations/exclude.py +107 -0
  143. clm/layer1/operations/handle.py +197 -0
  144. clm/layer1/operations/identity.py +139 -0
  145. clm/layer1/operations/invert.py +66 -0
  146. clm/layer1/operations/loader.py +116 -0
  147. clm/layer1/operations/manipulators/__init__.py +23 -0
  148. clm/layer1/operations/manipulators/document.py +211 -0
  149. clm/layer1/operations/manipulators/media.py +134 -0
  150. clm/layer1/operations/manipulators/structured.py +159 -0
  151. clm/layer1/operations/manipulators/tabular.py +334 -0
  152. clm/layer1/operations/port.py +65 -0
  153. clm/layer1/operations/services.py +529 -0
  154. clm/layer1/operations/split.py +117 -0
  155. clm/layer1/operations/substitute.py +75 -0
  156. clm/layer1/petri.py +1052 -0
  157. clm/layer1/pocketflow_orchestrator.py +408 -0
  158. clm/layer1/pocketflow_scheduler.py +519 -0
  159. clm/layer1/protocols.py +197 -0
  160. clm/layer1/runtime_adapter.py +718 -0
  161. clm/layer1/runtimes/__init__.py +71 -0
  162. clm/layer1/runtimes/_generated_assertions.py +125 -0
  163. clm/layer1/runtimes/_generated_types.py +42 -0
  164. clm/layer1/runtimes/base.py +265 -0
  165. clm/layer1/runtimes/binary.py +101 -0
  166. clm/layer1/runtimes/factory.py +203 -0
  167. clm/layer1/runtimes/javascript.py +321 -0
  168. clm/layer1/runtimes/javascript_runtime.js +33 -0
  169. clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
  170. clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
  171. clm/layer1/runtimes/lambda_calc.py +126 -0
  172. clm/layer1/runtimes/python.py +288 -0
  173. clm/layer1/runtimes/python_runtime.py +41 -0
  174. clm/layer1/runtimes/script.py +114 -0
  175. clm/layer1/sandbox.py +334 -0
  176. clm/layer1/savepoint_guard.py +136 -0
  177. clm/layer1/sparse_net.py +122 -0
  178. clm/layer1/spec_tester.py +165 -0
  179. clm/layer1/store.py +295 -0
  180. clm/layer1/test_runner.py +498 -0
  181. clm/layer1/type_inspector.py +166 -0
  182. clm/layer1/verifier.py +772 -0
  183. clm/layer1/vm.py +91 -0
  184. clm/layer2/__init__.py +78 -0
  185. clm/layer2/card_collection.py +678 -0
  186. clm/layer2/classifier.py +479 -0
  187. clm/layer2/engine/__init__.py +1 -0
  188. clm/layer2/engine/abstract_sql_engine.py +320 -0
  189. clm/layer2/engine/base.py +193 -0
  190. clm/layer2/engine/duckdb_engine.py +576 -0
  191. clm/layer2/engine/sqlite_engine.py +518 -0
  192. clm/layer2/fiber/__init__.py +31 -0
  193. clm/layer2/fiber/context.py +89 -0
  194. clm/layer2/fiber/disposable.py +163 -0
  195. clm/layer2/fiber/fiber.py +125 -0
  196. clm/layer2/fiber/hoare.py +191 -0
  197. clm/layer2/file_io.py +376 -0
  198. clm/layer2/file_registrar.py +77 -0
  199. clm/layer2/ingest.py +202 -0
  200. clm/layer2/lineage.py +322 -0
  201. clm/layer2/mcard_fs.py +262 -0
  202. clm/layer2/merkle.py +11 -0
  203. clm/layer2/mime.py +244 -0
  204. clm/layer2/mime_detector.py +245 -0
  205. clm/layer2/private_collection.py +118 -0
  206. clm/layer2/shared_store.py +11 -0
  207. clm/layer2/storage/__init__.py +190 -0
  208. clm/layer2/storage/db_bitemporal.py +141 -0
  209. clm/layer2/storage/db_driver.py +331 -0
  210. clm/layer2/storage/db_hyperlinks.py +214 -0
  211. clm/layer2/storage/db_pool.py +139 -0
  212. clm/layer2/storage/db_repository.py +229 -0
  213. clm/layer2/storage/savepoint.py +78 -0
  214. clm/layer2/storage/sqlite_wal.py +74 -0
  215. clm/layer2/tridb.py +245 -0
  216. clm/layer2/vcard_receipt.py +71 -0
  217. clm/layer2/vfs.py +17 -0
  218. clm/layer3/__init__.py +141 -0
  219. clm/layer3/agency/__init__.py +19 -0
  220. clm/layer3/agency/config.py +220 -0
  221. clm/layer3/agency/providers/__init__.py +15 -0
  222. clm/layer3/agency/providers/base.py +91 -0
  223. clm/layer3/agency/providers/mlc_llm.py +169 -0
  224. clm/layer3/agency/providers/ollama.py +203 -0
  225. clm/layer3/agency/router.py +71 -0
  226. clm/layer3/agency/runtime.py +524 -0
  227. clm/layer3/gateway.py +353 -0
  228. clm/layer3/open_interpreter_adapter.py +159 -0
  229. clm/layer3/rag/__init__.py +70 -0
  230. clm/layer3/rag/cli.py +228 -0
  231. clm/layer3/rag/config.py +153 -0
  232. clm/layer3/rag/embeddings/__init__.py +16 -0
  233. clm/layer3/rag/embeddings/base.py +79 -0
  234. clm/layer3/rag/embeddings/ollama.py +211 -0
  235. clm/layer3/rag/embeddings/vision.py +290 -0
  236. clm/layer3/rag/engine.py +276 -0
  237. clm/layer3/rag/graph/__init__.py +22 -0
  238. clm/layer3/rag/graph/community.py +198 -0
  239. clm/layer3/rag/graph/engine.py +433 -0
  240. clm/layer3/rag/graph/extractor.py +255 -0
  241. clm/layer3/rag/graph/schema.py +115 -0
  242. clm/layer3/rag/graph/store.py +641 -0
  243. clm/layer3/rag/indexer.py +333 -0
  244. clm/layer3/rag/llm_providers.json +52 -0
  245. clm/layer3/rag/semantic_versioning.py +313 -0
  246. clm/layer3/rag/vector/__init__.py +28 -0
  247. clm/layer3/rag/vector/handle_vector_store.py +749 -0
  248. clm/layer3/rag/vector/schema.py +151 -0
  249. clm/layer3/rag/vector/store.py +632 -0
  250. clm/layer3/reticulum/__init__.py +55 -0
  251. clm/layer3/reticulum/bridge.py +74 -0
  252. clm/layer3/reticulum/daemon.py +88 -0
  253. clm/layer3/reticulum/discovery.py +164 -0
  254. clm/layer3/reticulum/identity.py +107 -0
  255. clm/layer3/reticulum/media.py +148 -0
  256. clm/layer3/reticulum/transport.py +133 -0
  257. clm/layer3/satori/__init__.py +209 -0
  258. clm/layer3/satori/alpha_conversion.py +168 -0
  259. clm/layer3/satori/beta_reduction.py +359 -0
  260. clm/layer3/satori/continuation.py +38 -0
  261. clm/layer3/satori/eta_conversion.py +205 -0
  262. clm/layer3/satori/free_variables.py +163 -0
  263. clm/layer3/satori/io_effects.py +370 -0
  264. clm/layer3/satori/lambda_runtime.py +726 -0
  265. clm/layer3/satori/lambda_term.py +230 -0
  266. clm/layer3/satori/sheaf_audit.py +261 -0
  267. clm/layer3/satori/speech_act.py +279 -0
  268. clm/layer4/README.md +9 -0
  269. clm/layer4/__init__.py +67 -0
  270. clm/layer4/_sovereign_executor.py +580 -0
  271. clm/layer4/action_commands.py +719 -0
  272. clm/layer4/api.py +236 -0
  273. clm/layer4/bootstrap/__init__.py +13 -0
  274. clm/layer4/bootstrap/bootstrapper.py +118 -0
  275. clm/layer4/bootstrap/cleanup.py +44 -0
  276. clm/layer4/bootstrap/clm_hooks.py +49 -0
  277. clm/layer4/bootstrap/genesis.py +77 -0
  278. clm/layer4/bootstrap/injection.py +125 -0
  279. clm/layer4/cli.py +549 -0
  280. clm/layer4/collection_manager.py +553 -0
  281. clm/layer4/commands/__init__.py +81 -0
  282. clm/layer4/commands/action_cmd.py +42 -0
  283. clm/layer4/commands/check_cmd.py +82 -0
  284. clm/layer4/commands/envelope.py +73 -0
  285. clm/layer4/commands/evaluate_cmd.py +224 -0
  286. clm/layer4/commands/mcard_cmd.py +24 -0
  287. clm/layer4/commands/petri_cmd.py +92 -0
  288. clm/layer4/commands/tridb_cmd.py +319 -0
  289. clm/layer4/commands/version_cmd.py +27 -0
  290. clm/layer4/diff_commands.py +167 -0
  291. clm/layer4/eoa_sponsor.py +232 -0
  292. clm/layer4/events.py +69 -0
  293. clm/layer4/flux_gateway.py +133 -0
  294. clm/layer4/gateway.py +91 -0
  295. clm/layer4/loader.py +307 -0
  296. clm/layer4/mcard_commands.py +166 -0
  297. clm/layer4/membrane.py +273 -0
  298. clm/layer4/navigation.py +50 -0
  299. clm/layer4/protocol/protocol_entry_python.py +108 -0
  300. clm/layer4/protocol/python_sync.py +126 -0
  301. clm/layer4/runner.py +150 -0
  302. clm/layer4/satori.py +262 -0
  303. clm/layer4/server/python_sync_client.py +150 -0
  304. clm/layer4/server/python_sync_server.py +139 -0
  305. clm/layer4/server/service_api.py +315 -0
  306. clm/layer4/server/start_servers.py +107 -0
  307. clm/layer4/server/stop_servers.py +24 -0
  308. clm/layer4/simulated_optimization_fix.py +0 -0
  309. clm/layer4/storage.py +463 -0
  310. clm/layer4/tridb_admin.py +268 -0
  311. clm/layer4/websocket/test_ws_client.py +104 -0
  312. clm/layer4/websocket/test_ws_server.py +138 -0
  313. clm/layer4/websocket/ws_server_local.py +160 -0
  314. clm/layer5/__init__.py +51 -0
  315. clm/layer5/async_batch_writer.py +133 -0
  316. clm/layer5/learning.py +276 -0
  317. clm/layer5/telemetry_hook.py +107 -0
  318. clm/layer5/type_lattice.py +238 -0
  319. clm/py.typed +0 -0
  320. clm/schemas/__init__.py +652 -0
  321. clm/schemas/abstract_face_schema.yaml +77 -0
  322. clm/schemas/cel_schema.yaml +416 -0
  323. clm/schemas/lifecycle/__init__.py +45 -0
  324. clm/schemas/lifecycle/models.py +123 -0
  325. clm/schemas/lifecycle/service.py +364 -0
  326. clm/schemas/mcard_schema.sql +192 -0
  327. clm/schemas/mcard_vector_schema.sql +283 -0
  328. clm/schemas/mlp-context-v1.json +91 -0
  329. clm_kernel-0.0.0.dist-info/METADATA +121 -0
  330. clm_kernel-0.0.0.dist-info/RECORD +333 -0
  331. clm_kernel-0.0.0.dist-info/WHEEL +4 -0
  332. clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
  333. clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,560 @@
1
+ """Cross-Cutting — VCard (Id-type / Application Plane / Arena + Action)
2
+
3
+ Implements the VCard (Verification Card / Id-type) machinery that gates
4
+ every state transition in the CLM system. In the MVP Cards triad:
5
+
6
+ MCard (Σ-type) — the data state (Noun Phrase / Moore Machine)
7
+ PCard (Π-type) — the transformation (Verb Phrase / Mealy Machine)
8
+ VCard (Id-type) — the verification witness (Sentence / Triangle Identity)
9
+
10
+ The VCard Sandwich pattern (Gatekeeper Pattern) enforces the PTR lifecycle:
11
+
12
+ V_pre (precondition) → PCard (execution) → V_post (postcondition)
13
+
14
+ This maps directly to Hoare Logic {P} C {Q}:
15
+ - prep phase: V_pre asserts preconditions (Sum-typed / flexible ingress)
16
+ - exec phase: PCard fires the transition (β-reduction)
17
+ - post phase: V_post verifies postconditions (Product-typed / strict egress)
18
+
19
+ Every VCard is itself stored as an MCard (Empty Schema Principle), making
20
+ the verification trace content-addressable and permanently auditable.
21
+
22
+ Design references:
23
+ - PTR: Polynomial Type Runtime (§Core Lifecycle)
24
+ - PTR Design Patterns (Gatekeeper Pattern)
25
+ - MVP Cards Design Rationale (The Flux Pattern)
26
+ """
27
+
28
+ import os
29
+ from collections.abc import Callable
30
+ from functools import wraps
31
+ from typing import Any
32
+
33
+ from pydantic import BaseModel
34
+
35
+ from clm.cross_cutting.cel_engine import _smart_evaluate # Sprint 132.4: hoisted from 7 inline imports
36
+
37
+ # Sprint 133.3: VCard deprecation alias removed; Card imported lazily where needed.
38
+ from clm.cross_cutting.telemetry import jsonl_emit_typed
39
+ from clm.cross_cutting.timeutil import now_g_time
40
+ from clm.layer0.context import CLMContext
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Sprint 384 (DEL-384-02): Re-export canonical VCard from Layer 0 card_triad
44
+ from clm.layer0.model.card_triad import VCard # noqa: F401
45
+ from clm.layer0.utils import clm_hash_string as hash_string
46
+
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class Marking(BaseModel):
51
+ """Petri Net marking — a boundary state snapshot used by VCard gates."""
52
+ run_id: str
53
+ markings: dict[str, int]
54
+ colors: dict[str, str]
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # VCard Gate
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def vcard_pre_gate(
62
+ ctx: CLMContext,
63
+ pcard_hash: str,
64
+ triggering_dispatch_hash: str,
65
+ input_place_hashes: list[str],
66
+ timestamp: str,
67
+ agent_did: str,
68
+ sponsor_did: str,
69
+ delegation_proof: str,
70
+ execution_locale_hash: str,
71
+ transport_ref: str,
72
+ ) -> str:
73
+ """V_pre — callback gate (prep phase).
74
+
75
+ Sprint 132.4: extracted from ``vcard_gate(mode="callback")``.
76
+ """
77
+ from clm.cross_cutting.mcard import Card
78
+
79
+ card = Card(
80
+ token_type="vcard", gate_type="callback", result="pass",
81
+ pcard_hash=pcard_hash,
82
+ triggering_dispatch_hash=triggering_dispatch_hash,
83
+ timestamp=timestamp, agent_did=agent_did, sponsor_did=sponsor_did,
84
+ delegation_proof=delegation_proof,
85
+ execution_locale_hash=execution_locale_hash,
86
+ transport_ref=transport_ref,
87
+ input_place_hashes=input_place_hashes,
88
+ )
89
+ vcard_yaml = card.to_yaml()
90
+ vcard_hash = hash_string(vcard_yaml)
91
+ handle_name = f"vcard:callback:{vcard_hash}"
92
+ ctx.repo.save_card_and_handle(
93
+ handle_name, vcard_hash, vcard_yaml, "execution_log",
94
+ timestamp=timestamp, replace=False,
95
+ )
96
+ jsonl_emit_typed("vcard_callback", {
97
+ "vcard_hash": vcard_hash,
98
+ "gate_type": "callback",
99
+ "result": "pass",
100
+ "pcard_hash": pcard_hash,
101
+ }, ctx.root)
102
+ return vcard_hash
103
+
104
+
105
+ def vcard_post_gate(
106
+ ctx: CLMContext,
107
+ pcard_hash: str,
108
+ exec_exit_code: str,
109
+ downstream_targets: list[str],
110
+ timestamp: str,
111
+ agent_did: str,
112
+ sponsor_did: str,
113
+ delegation_proof: str,
114
+ execution_locale_hash: str,
115
+ transport_ref: str,
116
+ ) -> str:
117
+ """V_post — dispatch gate (post phase).
118
+
119
+ Sprint 132.4: extracted from ``vcard_gate(mode="dispatch")``.
120
+ """
121
+ from clm.cross_cutting.mcard import Card
122
+
123
+ gate_result = "pass"
124
+ failure_reason = None
125
+ if exec_exit_code != "0":
126
+ gate_result = "fail"
127
+ failure_reason = f"PCard execution failed with exit code {exec_exit_code}"
128
+
129
+ card = Card(
130
+ token_type="vcard", gate_type="dispatch", result=gate_result,
131
+ pcard_hash=pcard_hash,
132
+ timestamp=timestamp, agent_did=agent_did, sponsor_did=sponsor_did,
133
+ delegation_proof=delegation_proof,
134
+ execution_locale_hash=execution_locale_hash,
135
+ transport_ref=transport_ref,
136
+ failure_reason=failure_reason,
137
+ downstream_callback_targets=downstream_targets,
138
+ )
139
+ vcard_yaml = card.to_yaml()
140
+ vcard_hash = hash_string(vcard_yaml)
141
+ handle_name = f"vcard:dispatch:{vcard_hash}"
142
+ ctx.repo.save_card_and_handle(
143
+ handle_name, vcard_hash, vcard_yaml, "execution_log",
144
+ timestamp=timestamp, replace=False,
145
+ )
146
+ jsonl_emit_typed("vcard_dispatch", {
147
+ "vcard_hash": vcard_hash,
148
+ "gate_type": "dispatch",
149
+ "result": gate_result,
150
+ "pcard_hash": pcard_hash,
151
+ }, ctx.root)
152
+ return vcard_hash
153
+
154
+
155
+ def vcard_gate(
156
+ ctx: CLMContext,
157
+ mode: str,
158
+ pcard_hash: str,
159
+ triggering_dispatch_hash_or_exit_code: str,
160
+ input_place_hashes_or_downstream_targets: list[str],
161
+ timestamp: str,
162
+ agent_did: str,
163
+ sponsor_did: str,
164
+ delegation_proof: str,
165
+ execution_locale_hash: str,
166
+ transport_ref: str,
167
+ ) -> str:
168
+ """Generate a VCard gate (pre or post) and persist it to the execution log.
169
+
170
+ In 'callback' mode: generates V_pre (precondition gate / prep phase).
171
+ In 'dispatch' mode: generates V_post (postcondition gate / post phase).
172
+
173
+ .. deprecated:: Sprint 132.4
174
+ Prefer :func:`vcard_pre_gate` / :func:`vcard_post_gate` directly.
175
+ """
176
+ if mode == "callback":
177
+ return vcard_pre_gate(
178
+ ctx, pcard_hash,
179
+ triggering_dispatch_hash_or_exit_code,
180
+ input_place_hashes_or_downstream_targets,
181
+ timestamp, agent_did, sponsor_did, delegation_proof,
182
+ execution_locale_hash, transport_ref,
183
+ )
184
+ return vcard_post_gate(
185
+ ctx, pcard_hash,
186
+ triggering_dispatch_hash_or_exit_code,
187
+ input_place_hashes_or_downstream_targets,
188
+ timestamp, agent_did, sponsor_did, delegation_proof,
189
+ execution_locale_hash, transport_ref,
190
+ )
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Gate Evaluator
195
+ # ---------------------------------------------------------------------------
196
+
197
+ class GateEvaluator:
198
+ """Dispatches VCard gate assertions via an extensible gate registry.
199
+
200
+ Built-in gates:
201
+
202
+ * ``gate:file_exists`` — asserts a filesystem path exists.
203
+ * ``gate:hash_matches`` — asserts expected == actual.
204
+ * ``gate:payload_nonempty`` — asserts a value is non-empty.
205
+ * ``gate:db_accessible`` — asserts a SQLite pillar DB has required tables.
206
+
207
+ Custom gates can be registered at runtime via :meth:`register`.
208
+ """
209
+
210
+ def __init__(self) -> None:
211
+ self._gates: dict[str, Callable] = {
212
+ "gate:file_exists": self._gate_file_exists,
213
+ "gate:hash_matches": self._gate_hash_matches,
214
+ "gate:payload_nonempty": self._gate_payload_nonempty,
215
+ "gate:db_accessible": self._gate_db_accessible,
216
+ "gate:contains": self._gate_contains,
217
+ "gate:true": self._gate_true,
218
+ }
219
+
220
+ def register(self, gate_name: str, handler: Callable) -> None:
221
+ """Register a custom gate handler."""
222
+ self._gates[gate_name] = handler
223
+
224
+ # -- Pre-gate evaluation (Sprint 128.3: unified dict registry) --------
225
+
226
+ def evaluate_pre(self, item: dict, context: dict, suite: Any) -> bool:
227
+ """Evaluate a single ``vcard_pre`` gate item.
228
+
229
+ Returns ``True`` if the gate passed, ``False`` otherwise.
230
+
231
+ Sprint 133 §8.4: unified with ``evaluate_post`` — both call
232
+ ``_evaluate_gate(item, context, suite, phase="pre")``. The gate
233
+ handler decides what to do based on ``phase``.
234
+ """
235
+ return self._evaluate_gate(item, context, suite, phase="pre")
236
+
237
+ # -- Post-gate evaluation ----------------------------------------------
238
+
239
+ def evaluate_post(self, item: dict, context: dict, suite: Any) -> bool:
240
+ """Evaluate a single ``vcard_post`` gate item.
241
+
242
+ Returns ``True`` if the gate passed, ``False`` otherwise.
243
+
244
+ Sprint 133 §8.4: unified with ``evaluate_pre`` — both call
245
+ ``_evaluate_gate(item, context, suite, phase="post")``.
246
+ """
247
+ return self._evaluate_gate(item, context, suite, phase="post")
248
+
249
+ # -- Unified gate dispatcher (Sprint 133 §8.4) -------------------------
250
+
251
+ def _evaluate_gate(self, item: dict, context: dict, suite: Any, *, phase: str) -> bool:
252
+ """Unified gate dispatcher for both pre and post phases.
253
+
254
+ ``phase`` is either ``"pre"`` or ``"post"``. The handler receives
255
+ it as a keyword-only argument and can branch on it.
256
+ """
257
+ test_case = item.get("test_case", "")
258
+ gate = item.get("gate", "")
259
+ value_expr = item.get("value", "")
260
+
261
+ try:
262
+ _val = _smart_evaluate(value_expr, context) # noqa: F841 — pre-eval to catch errors
263
+ except Exception as e:
264
+ label = "Precondition" if phase == "pre" else "Postcondition"
265
+ suite.tu_fail(f"VCard {label} Failed: {test_case} (Evaluation error: {e})")
266
+ return False
267
+
268
+ handler = self._gates.get(gate)
269
+ if handler is None:
270
+ label = "Precondition" if phase == "pre" else "Postcondition"
271
+ suite.tu_fail(f"Unknown VCard {label} gate: {gate}")
272
+ return False
273
+
274
+ try:
275
+ return handler(item, context, suite, test_case, phase=phase)
276
+ except Exception as e:
277
+ suite.tu_fail(f"VCard Postcondition '{test_case}' failed to evaluate: {e}")
278
+ return False
279
+
280
+ # -- Built-in gate handlers (unified pre/post, Sprint 133 §8.4) --------
281
+
282
+ @staticmethod
283
+ def _gate_file_exists(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
284
+ val = _smart_evaluate(item.get("value"), ctx)
285
+ if phase == "pre":
286
+ clm_root = ctx.get("clm_root", "")
287
+ resolved = os.path.join(clm_root, val) if not os.path.isabs(val) else val
288
+ if os.path.isfile(resolved) and os.access(resolved, os.R_OK):
289
+ suite.tu_pass(f"VCard Precondition Passed: {desc} ({val} exists)")
290
+ return True
291
+ suite.tu_fail(f"VCard Precondition Failed: {desc} ({val} not found or not readable)")
292
+ return False
293
+ ok = os.path.isfile(val)
294
+ suite.assert_eq(f"File exists: {val}", True, ok)
295
+ return ok
296
+
297
+ @staticmethod
298
+ def _gate_hash_matches(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
299
+ expected = _smart_evaluate(item.get("expected"), ctx)
300
+ actual = _smart_evaluate(item.get("actual"), ctx)
301
+ suite.assert_eq(desc, expected, actual)
302
+ return str(expected) == str(actual)
303
+
304
+ @staticmethod
305
+ def _gate_payload_nonempty(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
306
+ val = _smart_evaluate(item.get("value"), ctx)
307
+ ok = val is not None and val != "" and val != []
308
+ if phase == "pre":
309
+ if ok:
310
+ suite.tu_pass(f"VCard Precondition Passed: {desc}")
311
+ else:
312
+ suite.tu_fail(f"VCard Precondition Failed: {desc} (payload is empty)")
313
+ else:
314
+ if ok:
315
+ suite.tu_pass(f"Payload non-empty: {desc}")
316
+ else:
317
+ suite.tu_fail(f"Payload is empty: {desc}")
318
+ return ok
319
+
320
+ @staticmethod
321
+ def _gate_db_accessible(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
322
+ import sqlite3
323
+
324
+ args_list = item.get("args") or ([item.get("arg")] if item.get("arg") else [])
325
+ pillar_expr = args_list[0] if args_list and args_list[0] else "content_memory"
326
+ pillar = _smart_evaluate(pillar_expr, ctx) or "content_memory"
327
+ tmp_dir = ctx.get("tmp_dir") or ctx.get("clm_root") or os.getcwd()
328
+ db_file = os.path.join(tmp_dir, "db", f"{pillar}.db")
329
+ if not os.path.isfile(db_file):
330
+ suite.tu_fail(f"gate:db_accessible Failed: DB file {db_file} not found")
331
+ return False
332
+ conn = sqlite3.connect(db_file)
333
+ cursor = conn.cursor()
334
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
335
+ tables = [row[0] for row in cursor.fetchall()]
336
+ conn.close()
337
+ if "card" in tables and "handle_registry" in tables:
338
+ suite.tu_pass(f"gate:db_accessible Passed: tables exist in {pillar}")
339
+ return True
340
+ suite.tu_fail(f"gate:db_accessible Failed: card or handle_registry table missing in {pillar}")
341
+ return False
342
+
343
+ @staticmethod
344
+ def _gate_contains(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
345
+ expected = _smart_evaluate(item.get("expected"), ctx)
346
+ actual = _smart_evaluate(item.get("actual"), ctx)
347
+ ok = str(expected) in str(actual)
348
+ suite.assert_contains(desc, str(expected), str(actual))
349
+ return ok
350
+
351
+ @staticmethod
352
+ def _gate_true(item: dict, ctx: dict, suite: Any, desc: str, *, phase: str = "post") -> bool:
353
+ actual = _smart_evaluate(item.get("actual"), ctx)
354
+ ok = bool(actual)
355
+ suite.assert_eq(desc, True, ok)
356
+ return ok
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # VCard Sandwich (Gatekeeper Pattern)
361
+ # ---------------------------------------------------------------------------
362
+
363
+ class VCardSandwich:
364
+ """Context manager implementing the VCard Sandwich (Gatekeeper Pattern).
365
+
366
+ Wraps every PCard execution in a pre/post verification gate, enforcing
367
+ the PTR prep → exec → post lifecycle as a Hoare triple {P} C {Q}:
368
+
369
+ __enter__ (prep) — Serializes the PCard, fires V_pre callback gate.
370
+ The precondition is Sum-typed: accepts any ONE
371
+ valid credential from alternatives.
372
+ (body) (exec) — User code executes the PCard transition.
373
+ seal() (post) — Fires V_post dispatch gate, emits state token.
374
+ The postcondition is Product-typed: ALL required
375
+ outputs must be present atomically.
376
+
377
+ The sealed VCard is the constructive proof object — a term of the
378
+ Identity type Id(spec, actual) — certifying that the state transition
379
+ satisfies the specification. It is stored as an MCard in the
380
+ execution_log pillar of the Sovereign Triad.
381
+ """
382
+
383
+ def __init__(
384
+ self,
385
+ ctx: CLMContext,
386
+ operation: str,
387
+ runtime: str,
388
+ script_hash: str,
389
+ input_place_hashes: list[str],
390
+ agent_did: str,
391
+ sponsor_did: str,
392
+ ):
393
+ self.ctx = ctx
394
+ self.operation = operation
395
+ self.runtime = runtime
396
+ self.script_hash = script_hash
397
+ self.input_place_hashes = input_place_hashes
398
+ self.agent_did = agent_did
399
+ self.sponsor_did = sponsor_did
400
+ self.pcard_hash = ""
401
+
402
+ def __enter__(self):
403
+ from clm.cross_cutting.pcard import serialize_pcard
404
+ timestamp = now_g_time()
405
+ self.pcard_hash = serialize_pcard(
406
+ self.ctx, self.operation, self.runtime,
407
+ self.script_hash, self.input_place_hashes, timestamp,
408
+ )
409
+ vcard_pre_gate(
410
+ self.ctx, self.pcard_hash, "null", [],
411
+ timestamp, self.agent_did, self.sponsor_did, "self",
412
+ os.environ.get("CLM_EXECUTION_LOCALE_HASH", ""),
413
+ os.environ.get("CLM_TRANSPORT_REF", "local"),
414
+ )
415
+ return self
416
+
417
+ def __exit__(self, exc_type, exc_val, exc_tb):
418
+ # The exit hook just cleans up; seal() must be called explicitly
419
+ # with the execution result before exiting.
420
+ pass
421
+
422
+ def seal(self, exec_exit_code: int, final_content: str, store: Any = None):
423
+ """Seal the sandwich with the execution result.
424
+
425
+ Args:
426
+ exec_exit_code: 0 for success, non-zero for failure.
427
+ final_content: The output content to seal.
428
+ store: Optional CelStore — if provided, flushes the action_log
429
+ to execution_log.db (Sprint 129.2).
430
+ """
431
+ from clm.cross_cutting.mcard import emit_state_failed, emit_state_token
432
+ timestamp = now_g_time()
433
+ dp_hash = vcard_post_gate(
434
+ self.ctx, self.pcard_hash, str(exec_exit_code), [],
435
+ timestamp, self.agent_did, self.sponsor_did, "self",
436
+ os.environ.get("CLM_EXECUTION_LOCALE_HASH", ""),
437
+ os.environ.get("CLM_TRANSPORT_REF", "local"),
438
+ )
439
+ if exec_exit_code == 0:
440
+ emit_state_token(self.ctx, self.pcard_hash, dp_hash, final_content)
441
+ else:
442
+ emit_state_failed(self.ctx, self.pcard_hash, str(exec_exit_code), final_content)
443
+
444
+ # Sprint 129.2: Flush CelStore action_log to execution_log.db
445
+ if store is not None and hasattr(store, "flush_action_log"):
446
+ store.flush_action_log(self.ctx)
447
+
448
+
449
+ # ---------------------------------------------------------------------------
450
+ # VCard Sandwich Decorator
451
+ # ---------------------------------------------------------------------------
452
+
453
+ def vcard_sandwich(
454
+ extract_ctx_func: Callable[..., CLMContext],
455
+ extract_meta_func: Callable[..., dict[str, Any]],
456
+ ):
457
+ """Decorator for wrapping an execution function in a VCard sandwich."""
458
+ def decorator(func):
459
+ @wraps(func)
460
+ def wrapper(*args, **kwargs):
461
+ ctx = extract_ctx_func(*args, **kwargs)
462
+ meta = extract_meta_func(*args, **kwargs)
463
+
464
+ with VCardSandwich(
465
+ ctx,
466
+ meta.get("operation", "unknown"),
467
+ meta.get("runtime", "shell"),
468
+ meta.get("script_hash", ""),
469
+ meta.get("input_place_hashes", []),
470
+ meta.get("agent_did", ""),
471
+ meta.get("sponsor_did", ""),
472
+ ) as sandwich:
473
+ res = func(*args, **kwargs)
474
+ if isinstance(res, tuple):
475
+ exec_exit_code = res[0]
476
+ final_content = res[1] if len(res) > 1 else ""
477
+ else:
478
+ exec_exit_code = res
479
+ final_content = ""
480
+ sandwich.seal(exec_exit_code, final_content)
481
+ return res
482
+ return wrapper
483
+ return decorator
484
+
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # Monadic thunk wrapper (Sprint 129.2 — moved from cel_store.py in Sprint 132.1)
488
+ # ---------------------------------------------------------------------------
489
+
490
+ def thunk(
491
+ store: Any,
492
+ action: dict,
493
+ ctx: CLMContext,
494
+ operation: str,
495
+ agent_did: str = "did:clm:bootstrap",
496
+ dispatch_fn: Any = None,
497
+ post_dispatch: Any = None,
498
+ ) -> Any:
499
+ """Monadic thunk — wrap a dispatch in the VCardSandwich lifecycle.
500
+
501
+ Guarantees that at least one event token is registered to execution_log.db:
502
+ 1. __enter__: serialize_pcard -> MCard in execution_log
503
+ 2. dispatch: CelStore.dispatch(action) -> result + action_log
504
+ 3. seal: vcard_gate -> VCard + state_token -> execution_log
505
+ flush action_log -> execution_log
506
+
507
+ This is the explicit monadic wrapper. Every function evaluation that
508
+ needs auditability goes through this thunk.
509
+
510
+ Sprint 133.4: extended with ``dispatch_fn`` and ``post_dispatch`` parameters
511
+ so ``BalancedRunner._run_case`` can use ``thunk()`` as its canonical
512
+ VCardSandwich lifecycle wrapper. When ``dispatch_fn`` is provided it
513
+ replaces the default ``store.dispatch(action)`` call. When
514
+ ``post_dispatch`` is provided it is called after dispatch but before
515
+ ``seal()``, receiving ``(sandwich, store, result)`` — enabling post-gate
516
+ evaluation. Returns ``(exit_code, result)`` tuple when ``dispatch_fn``
517
+ is used; otherwise returns the dispatch result directly.
518
+
519
+ Moved to vcard.py in Sprint 132.1 — it constructs a VCardSandwich (Id-type
520
+ witness machinery) and belongs next to VCardSandwich, not in cel_store.
521
+ """
522
+ from clm.cross_cutting.clm_logger import get_logger
523
+ from clm.layer0.utils import clm_hash_string
524
+
525
+ _logger = get_logger("clm.thunk")
526
+
527
+ pcard_hash = clm_hash_string(f"pcard:{operation}")
528
+ sandwich = VCardSandwich(
529
+ ctx=ctx, operation=operation, runtime="python",
530
+ script_hash=pcard_hash, input_place_hashes=[],
531
+ agent_did=agent_did, sponsor_did="",
532
+ )
533
+ sandwich.__enter__()
534
+
535
+ # Use dispatch_fn if provided (Sprint 133.4 — BalancedRunner path),
536
+ # otherwise fall back to default store.dispatch(action).
537
+ if dispatch_fn is not None:
538
+ try:
539
+ exit_code, result = dispatch_fn(sandwich, store)
540
+ except Exception as e:
541
+ _logger.error(f"Thunk dispatch_fn failed for operation '{operation}': {e}")
542
+ sandwich.seal(1, f"Exception: {e}", store=store)
543
+ raise
544
+
545
+ if post_dispatch is not None:
546
+ post_dispatch(sandwich, store, result)
547
+
548
+ sandwich.seal(exit_code, str(result) if result else "success", store=store)
549
+ return exit_code, result
550
+
551
+ # Default path: store.dispatch(action)
552
+ try:
553
+ result = store.dispatch(action)
554
+ except Exception as e:
555
+ _logger.error(f"Thunk dispatch failed for operation '{operation}': {e}")
556
+ sandwich.seal(1, f"Exception: {e}", store=store)
557
+ raise
558
+
559
+ sandwich.seal(0, str(result) if result else "success", store=store)
560
+ return result
@@ -0,0 +1,118 @@
1
+ """StoreWriter — the Writer monad for CLM state persistence (Sprint 132.1).
2
+
3
+ Extracted from ``CelStore`` in Sprint 132.1. Per the MVP Cards Design
4
+ Rationale (quoted in Sprint 131 §1.3), ``handle_history`` is the Writer
5
+ monad — the version trajectory. The three methods here are the Writer's
6
+ ``tell`` / ``listen`` / ``flush`` operations:
7
+
8
+ - ``set_persisted`` → ``tell``: commits a new Card version under a handle.
9
+ - ``select_card`` → ``listen``: reads the current (or historical) Card.
10
+ - ``flush_action_log`` → ``flush``: persists the action trajectory.
11
+
12
+ ``CelStore`` holds a ``self.writer`` reference and delegates to it,
13
+ preserving the existing ``store.set_persisted()`` call surface.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from clm.cross_cutting.timeutil import default_agent_did
22
+
23
+ if TYPE_CHECKING:
24
+ from clm.cross_cutting.cel_store import CelStore
25
+
26
+
27
+ class StoreWriter:
28
+ """Writer monad for handle-backed state persistence."""
29
+
30
+ def __init__(self, store: "CelStore") -> None:
31
+ self._store = store
32
+
33
+ @property
34
+ def _state(self) -> Any:
35
+ return self._store.state
36
+
37
+ @property
38
+ def _services(self) -> Any:
39
+ return self._store.services
40
+
41
+ def set_persisted(self, key: str, value: Any, pillar: str = "execution_log") -> Any:
42
+ """Persist ``value`` as a Card under handle ``cel:var:<key>`` (version-tracked).
43
+
44
+ Also keeps the raw value in ``state.vars`` so the existing in-memory
45
+ ``retrieve()``/``select()`` contract is unchanged.
46
+ """
47
+ from clm.cross_cutting.mcard import Card
48
+
49
+ repo = self._services.repo
50
+ if repo is None:
51
+ raise RuntimeError("set_persisted requires a repo in services")
52
+
53
+ card = value if isinstance(value, Card) else Card(
54
+ token_type="mcard", content=str(value),
55
+ agent_did=default_agent_did(),
56
+ )
57
+ handle_name = f"cel:var:{key}"
58
+ repo.save_card_and_handle(
59
+ handle_name, card.compute_hash(), card.to_yaml(), pillar,
60
+ timestamp=card.g_time, replace=True,
61
+ )
62
+ # Sprint 132.6: bind via set_card so card_vars tracks the handle
63
+ self._state.set_card(key, handle_name, value)
64
+ return value
65
+
66
+ def select_card(self, ref: str) -> Any:
67
+ """Like ``CelStore.select``, but resolves handle-backed state to a ``Card``."""
68
+ from clm.cross_cutting.mcard import Card
69
+
70
+ parts = ref.split(".", 2)
71
+ ns = parts[0]
72
+ key = parts[1] if len(parts) > 1 else ""
73
+
74
+ if ns == "state":
75
+ repo = self._services.repo
76
+ if repo is None:
77
+ return None
78
+ # Sprint 132.6: check card_vars first (opt-in Card-backed path)
79
+ handle_name = self._state.card_vars.get(key, f"cel:var:{key}")
80
+ h = repo.resolve_handle_hash(handle_name, "execution_log")
81
+ if h:
82
+ content = repo.get_card_content(h, "execution_log")
83
+ if content:
84
+ try:
85
+ return Card.from_yaml(content)
86
+ except Exception:
87
+ return None
88
+ return None
89
+
90
+ val = self._store.select(ref)
91
+ if val is None:
92
+ return None
93
+ return Card(token_type="mcard", content=str(val))
94
+
95
+ def flush_action_log(self, ctx: Any) -> str | None:
96
+ """Flush the in-memory action_log to execution_log.db."""
97
+ if not self._store.action_log:
98
+ return None
99
+
100
+ run_id = os.environ.get("CLM_RUN_ID", "")
101
+ if not run_id:
102
+ return None
103
+
104
+ from clm.cross_cutting.clm_logger import get_logger
105
+ from clm.cross_cutting.telemetry import jsonl_emit_typed
106
+
107
+ _logger = get_logger("clm.store")
108
+
109
+ for entry in self._store.action_log:
110
+ jsonl_emit_typed("cel_action", {
111
+ "op": entry.get("op", "unknown"),
112
+ "action": entry.get("action", {}),
113
+ "result": entry.get("result", ""),
114
+ "rejected": entry.get("rejected", False),
115
+ }, ctx.root)
116
+
117
+ _logger.debug(f"flush_action_log: {len(self._store.action_log)} entries persisted to run:{run_id}:events")
118
+ return run_id