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,214 @@
1
+ """Hyperlink and graph-traversal SQL mixin relocated to Layer 2 Storage (Sprint 387).
2
+
3
+ Part of the CLMRepository MRO.
4
+ Extracted from Layer 0. Provides graph traversal and hyperlink resolution
5
+ over content-addressed MCards and semantic handles.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Any
11
+
12
+ from clm.cross_cutting.clm_logger import get_logger
13
+ from clm.layer0.exceptions import DatabaseError
14
+
15
+ _logger = get_logger("clm.db")
16
+
17
+
18
+ class HyperlinkMixin:
19
+ """Graph traversal and hyperlink resolution methods for CLMRepository.
20
+
21
+ Implements:
22
+ - ``extract_links(content)``: parses [[wikilinks]] and mcard:// URIs.
23
+ - ``get_outgoing_links(hash_val, pillar)``: outgoing references from a card.
24
+ - ``get_backlinks(target_hash_or_handle, pillar)``: cards pointing to a target.
25
+ - ``traverse_graph(start_hash, max_depth, pillar)``: BFS graph traversal.
26
+ - ``find_citing_stages(doc_handle, pillar)``: stage cards citing an arch doc.
27
+ """
28
+
29
+ @staticmethod
30
+ def extract_links(content: str) -> list[tuple[str, str]]:
31
+ """Extract typed hyperlinks from MCard content.
32
+
33
+ Supports:
34
+ - Wikilinks: ``[[target]]`` or ``[[target|label]]``
35
+ - URIs: ``mcard://handle/name``, ``mcard://hash/hex``,
36
+ ``mcard://eval/expr``
37
+
38
+ Returns:
39
+ list of ``(link_type, target)`` tuples where link_type is one of
40
+ ``'wikilink'``, ``'handle'``, ``'hash'``, or ``'eval'``.
41
+ """
42
+ if not content:
43
+ return []
44
+
45
+ links: list[tuple[str, str]] = []
46
+
47
+ # 1. Wikilinks: [[target]] or [[target|label]]
48
+ for match in re.finditer(r"\[\[([^\|\]]+)(?:\|[^\]]+)?\]\]", content):
49
+ target = match.group(1).strip()
50
+ if target:
51
+ links.append(("wikilink", target))
52
+
53
+ # 2. MCard URIs
54
+ for match in re.finditer(r"mcard://(handle|hash|eval)/([^\s\'\"\)\>]+)", content):
55
+ scheme_type = match.group(1)
56
+ target = match.group(2).strip()
57
+ if target:
58
+ links.append((scheme_type, target))
59
+
60
+ return links
61
+
62
+ def get_outgoing_links(self, hash_val: str, pillar: str = "content_memory") -> list[tuple[str, str]]:
63
+ """Return all outgoing hyperlinks extracted from a card's content.
64
+
65
+ Returns:
66
+ list of ``(link_type, target)`` tuples.
67
+ """
68
+ content = self.get_card_content(hash_val, pillar)
69
+ if not content:
70
+ return []
71
+ return self.extract_links(content)
72
+
73
+ def get_backlinks(self, target: str, pillar: str = "content_memory") -> list[str]:
74
+ """Find all card hashes that link to the given target (hash or handle).
75
+
76
+ Scans the card table using SQLite LIKE for candidates, then confirms
77
+ via ``extract_links`` to prevent substring false positives.
78
+ """
79
+ sql = "SELECT hash, content FROM card WHERE content LIKE ?;"
80
+ like_pattern = f"%{target}%"
81
+ try:
82
+ res = self.db.execute(pillar, sql, [like_pattern])
83
+ except Exception as e:
84
+ raise DatabaseError(f"Failed to query backlinks for {target}: {e}") from e
85
+
86
+ if not res or not res.strip():
87
+ return []
88
+
89
+ referring_hashes: list[str] = []
90
+ for line in res.strip().split("\n"):
91
+ line = line.strip()
92
+ if not line:
93
+ continue
94
+ pipe_idx = line.find("|")
95
+ if pipe_idx == -1:
96
+ continue
97
+ h = line[:pipe_idx].strip()
98
+ content = self.get_card_content(h, pillar) or ""
99
+ links = self.extract_links(content)
100
+ if any(t == target for _, t in links):
101
+ referring_hashes.append(h)
102
+
103
+ return referring_hashes
104
+
105
+ def traverse_graph(
106
+ self,
107
+ start_hash: str,
108
+ max_depth: int = 3,
109
+ pillar: str = "content_memory",
110
+ ) -> dict[str, Any]:
111
+ """Breadth-first traversal of the hyperlink graph starting from a card.
112
+
113
+ Returns a dictionary representing the traversed subgraph:
114
+ - ``nodes``: set of visited card hashes
115
+ - ``edges``: list of ``(source_hash, link_type, target)``
116
+ """
117
+ visited: set[str] = set()
118
+ edges: list[tuple[str, str, str]] = []
119
+ queue: list[tuple[str, int]] = [(start_hash, 0)]
120
+
121
+ while queue:
122
+ current_hash, depth = queue.pop(0)
123
+ if current_hash in visited or depth > max_depth:
124
+ continue
125
+
126
+ visited.add(current_hash)
127
+ links = self.get_outgoing_links(current_hash, pillar)
128
+
129
+ for link_type, target in links:
130
+ edges.append((current_hash, link_type, target))
131
+ if depth + 1 <= max_depth:
132
+ next_hash: str | None = None
133
+ if link_type == "hash":
134
+ next_hash = target
135
+ elif link_type in ("handle", "wikilink"):
136
+ resolved = self.resolve_handle_hash(target, pillar)
137
+ if resolved:
138
+ next_hash = resolved
139
+
140
+ if next_hash and next_hash not in visited:
141
+ queue.append((next_hash, depth + 1))
142
+
143
+ return {"nodes": list(visited), "edges": edges}
144
+
145
+ def find_citing_stages(self, doc_handle: str, pillar: str = "content_memory") -> list[str]:
146
+ """Return Abstract-face card hashes whose ``external_references`` cite ``doc_handle``.
147
+
148
+ Implements the contravariant Yoneda functor Hom(-, A) specialised to
149
+ the Sprint 134 provenance graph: given an architecture doc handle A,
150
+ return the hashes of every Abstract-face card whose
151
+ ``context.external_references`` block cites it.
152
+
153
+ Args:
154
+ doc_handle: Architecture document handle name, e.g.
155
+ ``"doc:arch:134-provenance-hyperlinks"``.
156
+ pillar: Database pillar to scan (default ``"content_memory"``).
157
+
158
+ Returns:
159
+ A deduplicated list of card hashes that cite ``doc_handle``.
160
+ Empty list if no card cites it or the handle is unknown.
161
+
162
+ Raises:
163
+ DatabaseError: if the underlying scan query fails.
164
+ """
165
+ if not doc_handle:
166
+ return []
167
+ # Reuse the existing LIKE-pattern backlink scan: any card whose
168
+ # serialized content mentions the handle (as an mcard:// URI or a
169
+ # [[wikilink]]) is a candidate. The filter below confirms the
170
+ # citation via extract_links so partial-string false positives
171
+ # (e.g. a hash that happens to contain the handle as a substring)
172
+ # are rejected.
173
+ sql = "SELECT hash, content FROM card WHERE content LIKE ?;"
174
+ like_pattern = f"%{doc_handle}%"
175
+ try:
176
+ res = self.db.execute(pillar, sql, [like_pattern])
177
+ except Exception as e:
178
+ raise DatabaseError(
179
+ f"Failed to find citing stages for handle {doc_handle}: {e}"
180
+ ) from e
181
+ if not res or not res.strip():
182
+ return []
183
+
184
+ citing: list[str] = []
185
+ seen: set[str] = set()
186
+ # ``db.execute`` returns pipe-separated rows "hash|content" — but the
187
+ # content column itself may contain newlines, so we re-fetch per
188
+ # card via get_card_content to be safe. First pass extracts the
189
+ # candidate hashes only.
190
+ candidate_hashes: list[str] = []
191
+ for line in res.strip().split("\n"):
192
+ line = line.strip()
193
+ if not line:
194
+ continue
195
+ # The first pipe-delimited field is the hash; the rest is the
196
+ # content (which may contain additional pipes).
197
+ pipe_idx = line.find("|")
198
+ if pipe_idx == -1:
199
+ continue
200
+ h = line[:pipe_idx].strip()
201
+ if h and h not in seen:
202
+ seen.add(h)
203
+ candidate_hashes.append(h)
204
+
205
+ # Second pass: confirm via extract_links that the candidate actually
206
+ # cites the handle as a hyperlink (not as incidental prose).
207
+ for h in candidate_hashes:
208
+ content = self.get_card_content(h, pillar) or ""
209
+ links = self.extract_links(content)
210
+ for link_type, target in links:
211
+ if link_type == "handle" and target == doc_handle:
212
+ citing.append(h)
213
+ break
214
+ return citing
@@ -0,0 +1,139 @@
1
+ """High-Concurrency SQLite WAL Connection Pool (Sprint 177 / Sprint 387).
2
+
3
+ Thread-safe SQLite connection pool separating Read and Write channels:
4
+ - Single Writer: Serialized writes using BEGIN IMMEDIATE to avoid busy locks.
5
+ - Multi-Reader Pool: Fixed-size queue of read-only WAL connections.
6
+ - In-Memory Support: Detects ':memory:' and uses shared-cache URI mode.
7
+ - Directory Assurance: Automatically creates parent directories for disk DBs.
8
+
9
+ Canonical Path: clm.layer2.storage.db_pool
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sqlite3
16
+ import threading
17
+ from contextlib import contextmanager
18
+ from queue import Empty, Queue
19
+ from typing import Generator
20
+
21
+
22
+ class ClmDbPool:
23
+ """Thread-safe SQLite connection pool with dedicated writer and reader pool."""
24
+
25
+ def __init__(self, db_path: str, max_readers: int = 8, timeout: float = 5.0) -> None:
26
+ self.raw_path = db_path
27
+ self.is_memory = db_path == ":memory:" or "mode=memory" in db_path
28
+ self.db_path = (
29
+ "file:clm_memdb?mode=memory&cache=shared"
30
+ if db_path == ":memory:"
31
+ else db_path
32
+ )
33
+ self.uri = self.is_memory or db_path.startswith("file:")
34
+ self.max_readers = max_readers
35
+ self.timeout = timeout
36
+ self._write_lock = threading.Lock()
37
+ self._writer_conn: sqlite3.Connection | None = None
38
+ self._reader_queue: Queue[sqlite3.Connection] = Queue(maxsize=max_readers)
39
+ self._closed = False
40
+ self._ensure_dir()
41
+ self._init_db()
42
+
43
+ def _ensure_dir(self) -> None:
44
+ """Create parent directory if database is a disk file."""
45
+ if not self.is_memory and not self.db_path.startswith("file:"):
46
+ dir_path = os.path.dirname(os.path.abspath(self.db_path))
47
+ if dir_path:
48
+ os.makedirs(dir_path, exist_ok=True)
49
+
50
+ def _connect(self) -> sqlite3.Connection:
51
+ """Open a raw connection with timeout and URI configuration."""
52
+ return sqlite3.connect(
53
+ self.db_path,
54
+ timeout=self.timeout,
55
+ uri=self.uri,
56
+ check_same_thread=False,
57
+ )
58
+
59
+ def _init_db(self) -> None:
60
+ """Initialize WAL mode and pragmatic performance settings."""
61
+ conn = self._connect()
62
+ try:
63
+ if not self.is_memory:
64
+ conn.execute("PRAGMA journal_mode=WAL;")
65
+ conn.execute("PRAGMA synchronous=NORMAL;")
66
+ conn.execute("PRAGMA busy_timeout=5000;")
67
+ conn.execute("PRAGMA cache_size=-64000;") # 64MB cache
68
+ conn.commit()
69
+ finally:
70
+ conn.close()
71
+
72
+ @contextmanager
73
+ def read_connection(self) -> Generator[sqlite3.Connection, None, None]:
74
+ """Borrow a read connection from the pool, recycling cleanly on return."""
75
+ conn = None
76
+ try:
77
+ conn = self._reader_queue.get_nowait()
78
+ except Empty:
79
+ conn = self._connect()
80
+ if not self.is_memory:
81
+ conn.execute("PRAGMA query_only=ON;")
82
+ conn.execute("PRAGMA busy_timeout=5000;")
83
+
84
+ try:
85
+ yield conn
86
+ except Exception:
87
+ if conn:
88
+ try:
89
+ conn.rollback()
90
+ except Exception:
91
+ pass
92
+ conn.close()
93
+ conn = None
94
+ raise
95
+ finally:
96
+ if conn is not None and not self._closed and self._reader_queue.qsize() < self.max_readers:
97
+ self._reader_queue.put_nowait(conn)
98
+ elif conn is not None:
99
+ conn.close()
100
+
101
+ @contextmanager
102
+ def write_transaction(self) -> Generator[sqlite3.Connection, None, None]:
103
+ """Acquire exclusive writer connection with automatic rollback and reconnect on error."""
104
+ with self._write_lock:
105
+ if self._writer_conn is None:
106
+ self._writer_conn = self._connect()
107
+ self._writer_conn.execute("PRAGMA busy_timeout=5000;")
108
+ try:
109
+ self._writer_conn.execute("BEGIN IMMEDIATE;")
110
+ yield self._writer_conn
111
+ self._writer_conn.commit()
112
+ except Exception:
113
+ try:
114
+ self._writer_conn.rollback()
115
+ except sqlite3.OperationalError:
116
+ try:
117
+ self._writer_conn.close()
118
+ except Exception:
119
+ pass
120
+ self._writer_conn = None
121
+ raise
122
+
123
+ def close(self) -> None:
124
+ """Close all pooled connections."""
125
+ self._closed = True
126
+ with self._write_lock:
127
+ if self._writer_conn:
128
+ try:
129
+ self._writer_conn.close()
130
+ except Exception:
131
+ pass
132
+ self._writer_conn = None
133
+
134
+ while not self._reader_queue.empty():
135
+ try:
136
+ conn = self._reader_queue.get_nowait()
137
+ conn.close()
138
+ except Empty:
139
+ break
@@ -0,0 +1,229 @@
1
+ """RepositoryBase core CRUD relocated to Layer 2 Storage (Sprint 387).
2
+
3
+ Extracted from Layer 0. The foundational Create/Retrieve/Delete operations
4
+ on content-addressed MCards, semantic handles, and handle history.
5
+ """
6
+ import os
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from clm.cross_cutting.clm_logger import get_logger
11
+ from clm.layer0.exceptions import DatabaseError
12
+ from clm.layer0.utils import clm_hash_string
13
+
14
+ _logger = get_logger("clm.db")
15
+
16
+
17
+ class RepositoryBase:
18
+ """Core CRUD operations for the MCard three-table schema.
19
+
20
+ Implements CRD-only operations (Create, Retrieve, Delete — no Update) on
21
+ content-addressed MCards, semantic handles, and the append-only handle
22
+ history. Handles are the α-conversion layer (mutable pointers over
23
+ immutable content); every handle reassignment is logged immutably in
24
+ handle_history (the η-conversion witness), preserving full temporal
25
+ provenance.
26
+
27
+ The repository enforces the Empty Schema Principle (Kenosis): zero
28
+ domain-specific columns. Domain semantics enter only through the content
29
+ stored in MCards, never through schema structure.
30
+ """
31
+
32
+ def __init__(self, db, clm_root: str):
33
+ self.db = db
34
+ self.clm_root = clm_root
35
+
36
+ def has_card(self, hash_val: str, pillar: str) -> bool:
37
+ """Checks if a card exists in the CAS by hash."""
38
+ sql = "SELECT 1 FROM card WHERE hash = ? LIMIT 1;"
39
+ try:
40
+ res = self.db.execute(pillar, sql, [hash_val])
41
+ return res.strip() == "1"
42
+ except Exception as e:
43
+ raise DatabaseError(f"Failed to check card existence in {pillar}: {e}") from e
44
+
45
+ def get_card_content(self, hash_val: str, pillar: str) -> str | None:
46
+ """Gets the content of a card from the CAS by hash."""
47
+ sql = "SELECT content FROM card WHERE hash = ?;"
48
+ try:
49
+ res = self.db.execute(pillar, sql, [hash_val])
50
+ return res.strip() if res else None
51
+ except Exception as e:
52
+ raise DatabaseError(f"Failed to get card content from {pillar}: {e}") from e
53
+
54
+ def save_card(self, hash_val: str, content: str, pillar: str, timestamp: str | None = None, replace: bool = False) -> None:
55
+ """Saves a card to the CAS."""
56
+ g_time = timestamp or time.strftime("%Y-%m-%dT%H:%M:%S%z")
57
+ verb = "REPLACE" if replace else "IGNORE"
58
+ sql = f"INSERT OR {verb} INTO card (hash, content, g_time) VALUES (?, ?, ?);"
59
+ try:
60
+ self.db.execute(pillar, sql, [hash_val, content, g_time])
61
+ except Exception as e:
62
+ raise DatabaseError(f"Failed to save card to {pillar}: {e}") from e
63
+
64
+ def save_card_and_handle(self, handle_name: str, hash_val: str, content: str, pillar: str, timestamp: str | None = None, replace: bool = True) -> None:
65
+ """Saves a card to the CAS and updates/registers its handle."""
66
+ g_time = timestamp or time.strftime("%Y-%m-%dT%H:%M:%S%z")
67
+ self.save_card(hash_val, content, pillar, timestamp=g_time, replace=replace)
68
+
69
+ existing_hash = self.resolve_handle_hash(handle_name, pillar)
70
+
71
+ try:
72
+ if not existing_hash:
73
+ verb = "REPLACE" if replace else "IGNORE"
74
+ sql = f"INSERT OR {verb} INTO handle_registry (handle, current_hash, created_at, updated_at) VALUES (?, ?, ?, ?);"
75
+ self.db.execute(pillar, sql, [handle_name, hash_val, g_time, g_time])
76
+ elif existing_hash != hash_val:
77
+ self.db.execute(pillar, "INSERT OR IGNORE INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?);", [handle_name, existing_hash, g_time])
78
+ self.db.execute(pillar, "UPDATE handle_registry SET current_hash = ?, updated_at = ? WHERE handle = ?;", [hash_val, g_time, handle_name])
79
+ except Exception as e:
80
+ raise DatabaseError(f"Failed to save handle {handle_name} in {pillar}: {e}") from e
81
+
82
+ def get_handle_by_hash(self, hash_val: str, pillar: str) -> str | None:
83
+ """Gets the handle name associated with a given hash value."""
84
+ sql = "SELECT handle FROM handle_registry WHERE current_hash = ? LIMIT 1;"
85
+ try:
86
+ res = self.db.execute(pillar, sql, [hash_val])
87
+ return res.strip() if res else None
88
+ except Exception as e:
89
+ raise DatabaseError(f"Failed to get handle by hash from {pillar}: {e}") from e
90
+
91
+ def get_all_handles(self, pillar: str) -> list[tuple[str, str]]:
92
+ """Gets all handle-to-hash mappings from the handle registry."""
93
+ sql = "SELECT handle, current_hash FROM handle_registry;"
94
+ try:
95
+ res = self.db.execute(pillar, sql)
96
+ lines = [line.strip() for line in res.strip().split("\n") if line.strip()]
97
+ results = []
98
+ for line in lines:
99
+ parts = line.split("|")
100
+ if len(parts) >= 2:
101
+ results.append((parts[0], parts[1]))
102
+ return results
103
+ except Exception as e:
104
+ raise DatabaseError(f"Failed to get all handles from {pillar}: {e}") from e
105
+
106
+ def get_latest_handle_history(self, handle_name: str, pillar: str) -> str | None:
107
+ """Gets the latest previous hash from the handle history."""
108
+ sql = "SELECT previous_hash FROM handle_history WHERE handle = ? ORDER BY changed_at DESC LIMIT 1;"
109
+ try:
110
+ res = self.db.execute(pillar, sql, [handle_name])
111
+ return res.strip() if res else None
112
+ except Exception as e:
113
+ raise DatabaseError(f"Failed to get latest handle history for {handle_name} from {pillar}: {e}") from e
114
+
115
+ def resolve_handle_hash(self, handle_name: str, pillar: str = "content_memory") -> str:
116
+ sql = "SELECT current_hash FROM handle_registry WHERE handle = ?;"
117
+ res = self.db.execute(pillar, sql, [handle_name])
118
+ return res.strip() if res else ""
119
+
120
+ def _find_script_on_disk(self, handle_name: str) -> str | None:
121
+ if not handle_name.startswith("script:"):
122
+ return None
123
+ script_name = handle_name.split("script:")[1].split("#")[0]
124
+ for ext in (".py", ".sh"):
125
+ for dir_name in ("tests/runners", "scripts", "tests", "computable_knowledge"):
126
+ dir_path = Path(self.clm_root) / dir_name
127
+ if dir_path.exists():
128
+ if dir_name == "tests/runners":
129
+ for sub in ("layers", "sprints", "petri", "benchmarks"):
130
+ cand = dir_path / sub / f"{script_name}{ext}"
131
+ if cand.exists():
132
+ return str(cand)
133
+ for p in dir_path.rglob(f"{script_name}{ext}"):
134
+ return str(p)
135
+ return None
136
+
137
+ def resolve_handle_content(self, handle_name: str, pillar: str = "content_memory") -> str:
138
+ hash_val = self.resolve_handle_hash(handle_name, pillar)
139
+
140
+ if not hash_val and handle_name.startswith("script:"):
141
+ script_path = self._find_script_on_disk(handle_name)
142
+ if script_path:
143
+ self.register_handle(handle_name, script_path)
144
+ hash_val = self.resolve_handle_hash(handle_name, pillar)
145
+
146
+ if hash_val and handle_name.startswith("script:"):
147
+ script_path = self._find_script_on_disk(handle_name)
148
+ if script_path:
149
+ with open(script_path, encoding="utf-8") as f:
150
+ disk_content = f.read()
151
+ norm_content = disk_content.replace("\r\n", "\n").replace("\r", "\n")
152
+ disk_hash = clm_hash_string(norm_content)
153
+ if disk_hash != hash_val:
154
+ _logger.info(f"[PAL:python] Stale handle detected: '{handle_name}' (DB={hash_val}, disk={disk_hash}). Re-reading from file.")
155
+ self.register_handle(handle_name, script_path)
156
+ hash_val = disk_hash
157
+
158
+ if not hash_val:
159
+ return ""
160
+
161
+ sql = "SELECT content FROM card WHERE hash = ?;"
162
+ res = self.db.execute(pillar, sql, [hash_val])
163
+ content_val = res.strip() if res else ""
164
+ if handle_name.startswith("script:") and not content_val:
165
+ script_path = self._find_script_on_disk(handle_name)
166
+ if script_path:
167
+ with open(script_path, encoding="utf-8") as f:
168
+ disk_content = f.read()
169
+ content_val = disk_content.replace("\r\n", "\n").replace("\r", "\n")
170
+
171
+ return content_val
172
+
173
+ def register_handle(self, handle_name: str, content_source: str) -> int:
174
+ import sys
175
+ if content_source == "-":
176
+ content = sys.stdin.read()
177
+ elif os.path.exists(content_source):
178
+ with open(content_source, encoding="utf-8") as f:
179
+ content = f.read()
180
+ else:
181
+ content = content_source
182
+
183
+ if not content:
184
+ _logger.error(f"[REGISTER_HANDLE] Empty content for handle: {handle_name}")
185
+ return 1
186
+
187
+ norm_content = content.replace("\r\n", "\n").replace("\r", "\n")
188
+ content_hash = clm_hash_string(norm_content)
189
+
190
+ existing_hash = self.resolve_handle_hash(handle_name, "content_memory")
191
+ g_time = time.strftime("%Y-%m-%dT%H:%M:%S%z")
192
+
193
+ self.db.execute("content_memory", "INSERT OR IGNORE INTO card (hash, content, g_time) VALUES (?, ?, ?);", [content_hash, content, g_time])
194
+
195
+ now = time.strftime("%Y-%m-%dT%H:%M:%S%z")
196
+ if not existing_hash:
197
+ self.db.execute("content_memory", "INSERT OR REPLACE INTO handle_registry (handle, current_hash, created_at, updated_at) VALUES (?, ?, ?, ?);", [handle_name, content_hash, now, now])
198
+ _logger.info(f"[REGISTER_HANDLE] Created handle: {handle_name}")
199
+ _logger.info(f"[REGISTER_HANDLE] hash: {content_hash}")
200
+ elif existing_hash == content_hash:
201
+ _logger.info(f"[REGISTER_HANDLE] No change for handle: {handle_name}")
202
+ _logger.info(f"[REGISTER_HANDLE] hash: {content_hash}")
203
+ else:
204
+ self.db.execute("content_memory", "INSERT INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?);", [handle_name, existing_hash, now])
205
+ self.db.execute("content_memory", "UPDATE handle_registry SET current_hash = ?, updated_at = ? WHERE handle = ?;", [content_hash, now, handle_name])
206
+ _logger.info(f"[REGISTER_HANDLE] Updated handle: {handle_name}")
207
+ _logger.info(f"[REGISTER_HANDLE] old_hash: {existing_hash}")
208
+ _logger.info(f"[REGISTER_HANDLE] new_hash: {content_hash}")
209
+
210
+ return 0
211
+
212
+ def tombstone_handle(self, handle_name: str, pillar: str = "content_memory") -> bool:
213
+ """Tombstones a handle by dereferencing it and appending a tombstone record to handle_history.
214
+
215
+ Preserves G-Set CRDT monotonicity: the referenced CAS card remains untouched,
216
+ while the mutable handle pointer is removed and an audit tombstone event is logged.
217
+ """
218
+ existing_hash = self.resolve_handle_hash(handle_name, pillar)
219
+ if not existing_hash:
220
+ return False
221
+ now = time.strftime("%Y-%m-%dT%H:%M:%S%z")
222
+ self.db.execute(
223
+ pillar,
224
+ "INSERT INTO handle_history (handle, previous_hash, changed_at, change_type) VALUES (?, ?, ?, 'TOMBSTONE');",
225
+ [handle_name, existing_hash, now],
226
+ )
227
+ self.db.execute(pillar, "DELETE FROM handle_registry WHERE handle = ?;", [handle_name])
228
+ _logger.info(f"[TOMBSTONE_HANDLE] Tombstoned handle '{handle_name}' (was {existing_hash})")
229
+ return True
@@ -0,0 +1,78 @@
1
+ """SQLite Savepoint Transaction Guard & Cordis Disposable List (Sprint 177 / Sprint 387).
2
+
3
+ Implements nested transaction savepoints mirroring Rust Sprint 165 SavepointGuard,
4
+ and DisposableList for strict LIFO rollback and zero-residual-state guarantees.
5
+
6
+ Canonical Path: clm.layer2.storage.savepoint
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sqlite3
12
+ import uuid
13
+ from contextlib import contextmanager
14
+ from typing import Callable, Generator
15
+
16
+
17
+ @contextmanager
18
+ def savepoint(conn: sqlite3.Connection, name: str | None = None) -> Generator[DisposableList, None, None]:
19
+ """Execute within a named SQLite savepoint with automatic LIFO rollback on exception."""
20
+ sp_name = name or f"sp_{uuid.uuid4().hex[:8]}"
21
+ disposables = DisposableList()
22
+ disposables.name = sp_name
23
+ conn.execute(f"SAVEPOINT {sp_name};")
24
+ try:
25
+ yield disposables
26
+ conn.execute(f"RELEASE SAVEPOINT {sp_name};")
27
+ except Exception:
28
+ disposables.dispose()
29
+ conn.execute(f"ROLLBACK TO SAVEPOINT {sp_name};")
30
+ conn.execute(f"RELEASE SAVEPOINT {sp_name};")
31
+ raise
32
+
33
+
34
+ class DisposableList:
35
+ """LIFO stack of reversible effects and cleanup callbacks (Cordis parity)."""
36
+
37
+ def __init__(self) -> None:
38
+ self._disposers: list[Callable[[], None]] = []
39
+
40
+ def push(self, disposer: Callable[[], None]) -> None:
41
+ """Pushes a cleanup function to the top of the LIFO stack."""
42
+ self._disposers.append(disposer)
43
+
44
+ def register(self, disposer: Callable[[], None]) -> None:
45
+ """Alias for push."""
46
+ self.push(disposer)
47
+
48
+ def __len__(self) -> int:
49
+ return len(self._disposers)
50
+
51
+ def is_empty(self) -> bool:
52
+ return len(self._disposers) == 0
53
+
54
+ def dispose(self) -> list[Exception]:
55
+ """Unwinds all registered disposers in strict LIFO order.
56
+
57
+ Executes all disposers even if some raise exceptions, accumulating errors.
58
+ """
59
+ errors: list[Exception] = []
60
+ while self._disposers:
61
+ disposer = self._disposers.pop()
62
+ try:
63
+ disposer()
64
+ except Exception as e:
65
+ errors.append(e)
66
+ return errors
67
+
68
+ def unwind_lifo(self) -> list[Exception]:
69
+ """Alias for dispose() matching Cordis LIFO unwind convention."""
70
+ return self.dispose()
71
+
72
+ def __enter__(self) -> DisposableList:
73
+ return self
74
+
75
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
76
+ errors = self.dispose()
77
+ if errors and exc_type is None:
78
+ raise errors[0]