algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,92 @@
1
+ """H4 — Harness Discovery Event Log.
2
+
3
+ Structured events for the full discovery lifecycle.
4
+ Mined from T3MP3ST EventEmitter (finding:discovered, finding:validated, finding:retracted).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Callable
11
+
12
+
13
+ class EventType:
14
+ DISCOVERED = "discovered"
15
+ VALIDATED = "validated"
16
+ RETRACTED = "retracted"
17
+ PROPOSED = "proposed"
18
+ ENRICHED = "enriched"
19
+ PROPAGATED = "propagated"
20
+
21
+
22
+ @dataclass
23
+ class DiscoveryEvent:
24
+ """A single lifecycle event."""
25
+
26
+ event_type: str
27
+ target_id: str
28
+ timestamp: float = field(default_factory=time.time)
29
+ source: str = ""
30
+ metadata: dict[str, Any] = field(default_factory=dict)
31
+
32
+ def to_dict(self) -> dict[str, Any]:
33
+ return {
34
+ "event_type": self.event_type,
35
+ "target_id": self.target_id,
36
+ "timestamp": self.timestamp,
37
+ "source": self.source,
38
+ "metadata": dict(self.metadata),
39
+ }
40
+
41
+
42
+ class DiscoveryEventLog:
43
+ """Append-only event log with listener support."""
44
+
45
+ def __init__(self, max_events: int = 10_000) -> None:
46
+ self._events: list[DiscoveryEvent] = []
47
+ self._listeners: list[Callable[[DiscoveryEvent], None]] = []
48
+ self._max_events = max_events
49
+
50
+ def emit(
51
+ self,
52
+ event_type: str,
53
+ target_id: str,
54
+ source: str = "",
55
+ metadata: dict[str, Any] | None = None,
56
+ ) -> DiscoveryEvent:
57
+ event = DiscoveryEvent(
58
+ event_type=event_type,
59
+ target_id=target_id,
60
+ source=source,
61
+ metadata=metadata or {},
62
+ )
63
+ self._events.append(event)
64
+ if len(self._events) > self._max_events:
65
+ self._events = self._events[-self._max_events:]
66
+ for listener in self._listeners:
67
+ listener(event)
68
+ return event
69
+
70
+ def on(self, listener: Callable[[DiscoveryEvent], None]) -> None:
71
+ self._listeners.append(listener)
72
+
73
+ def query(
74
+ self,
75
+ event_type: str | None = None,
76
+ target_id: str | None = None,
77
+ ) -> list[DiscoveryEvent]:
78
+ results = self._events
79
+ if event_type is not None:
80
+ results = [e for e in results if e.event_type == event_type]
81
+ if target_id is not None:
82
+ results = [e for e in results if e.target_id == target_id]
83
+ return list(results)
84
+
85
+ def all(self) -> list[DiscoveryEvent]:
86
+ return list(self._events)
87
+
88
+ def count(self) -> int:
89
+ return len(self._events)
90
+
91
+ def clear_listeners(self) -> None:
92
+ self._listeners.clear()
@@ -0,0 +1,298 @@
1
+ """B34. Converter Pipeline + Markdown Normalization (MarkItDown Pattern).
2
+
3
+ Converts arbitrary file formats into token-efficient Markdown before RAG or
4
+ agent analysis. Uses a registry of format-specific converters with narrow
5
+ readers (local file, stream, URL) and security boundaries.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import io
12
+ import json
13
+ import os
14
+ import re
15
+ import zipfile
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any, Iterable
19
+ from xml.etree import ElementTree
20
+
21
+ # ── data types ────────────────────────────────────────────────────────
22
+
23
+
24
+ @dataclass
25
+ class ConversionResult:
26
+ """Outcome of a single conversion."""
27
+
28
+ markdown: str
29
+ metadata: dict[str, Any] = field(default_factory=dict)
30
+ warnings: list[str] = field(default_factory=list)
31
+
32
+
33
+ class UnsupportedFormat(Exception):
34
+ """No registered converter can handle the given file."""
35
+
36
+
37
+ # ── converter base ────────────────────────────────────────────────────
38
+
39
+
40
+ class Converter:
41
+ """Base converter — override ``supports`` and ``convert_local``."""
42
+
43
+ def supports(self, path: Path) -> bool:
44
+ raise NotImplementedError
45
+
46
+ def convert_local(self, path: Path) -> ConversionResult:
47
+ raise NotImplementedError
48
+
49
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
50
+ raise NotImplementedError
51
+
52
+
53
+ # ── built-in converters ───────────────────────────────────────────────
54
+
55
+
56
+ class CSVConverter(Converter):
57
+ """CSV → Markdown table."""
58
+
59
+ def supports(self, path: Path) -> bool:
60
+ return path.suffix.lower() in {".csv", ".tsv"}
61
+
62
+ def convert_local(self, path: Path) -> ConversionResult:
63
+ delim = "\t" if path.suffix.lower() == ".tsv" else ","
64
+ with open(path, encoding="utf-8", newline="") as fh:
65
+ return self._from_reader(csv.reader(fh, delimiter=delim), path)
66
+
67
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
68
+ text = data.decode("utf-8", errors="replace")
69
+ return self._from_reader(csv.reader(io.StringIO(text)), Path(name))
70
+
71
+ @staticmethod
72
+ def _from_reader(rows: Iterable[list[str]], path: Path) -> ConversionResult:
73
+ lines = list(rows)
74
+ if not lines:
75
+ return ConversionResult(markdown="", metadata={"source": str(path)})
76
+ header = lines[0]
77
+ md = "| " + " | ".join(header) + " |\n"
78
+ md += "|" + "---|" * len(header) + "\n"
79
+ for row in lines[1:]:
80
+ # pad / truncate to header length
81
+ cells = list(row) + [""] * (len(header) - len(row))
82
+ md += "| " + " | ".join(cells[: len(header)]) + " |\n"
83
+ return ConversionResult(
84
+ markdown=md,
85
+ metadata={"source": str(path), "rows": len(lines) - 1, "columns": len(header)},
86
+ )
87
+
88
+
89
+ class JSONConverter(Converter):
90
+ """JSON → fenced code block."""
91
+
92
+ def supports(self, path: Path) -> bool:
93
+ return path.suffix.lower() == ".json"
94
+
95
+ def convert_local(self, path: Path) -> ConversionResult:
96
+ with open(path, encoding="utf-8") as fh:
97
+ data = json.load(fh)
98
+ return self._from_data(data, path)
99
+
100
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
101
+ return self._from_data(json.loads(data), Path(name))
102
+
103
+ @staticmethod
104
+ def _from_data(data: Any, path: Path) -> ConversionResult:
105
+ md = f"```json\n{json.dumps(data, indent=2)}\n```\n"
106
+ return ConversionResult(
107
+ markdown=md,
108
+ metadata={"source": str(path), "type": type(data).__name__},
109
+ )
110
+
111
+
112
+ class XMLConverter(Converter):
113
+ """XML → indented text inside a code fence."""
114
+
115
+ def supports(self, path: Path) -> bool:
116
+ return path.suffix.lower() in {".xml", ".svg", ".xsd", ".xsl"}
117
+
118
+ def convert_local(self, path: Path) -> ConversionResult:
119
+ tree = ElementTree.parse(path)
120
+ return self._from_tree(tree, path)
121
+
122
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
123
+ return self._from_tree(ElementTree.fromstring(data), Path(name))
124
+
125
+ @staticmethod
126
+ def _from_tree(tree: Any, path: Path) -> ConversionResult:
127
+ if isinstance(tree, ElementTree.ElementTree):
128
+ root = tree.getroot()
129
+ else:
130
+ root = tree
131
+ text = ElementTree.tostring(root, encoding="unicode")
132
+ md = f"```xml\n{text}\n```\n"
133
+ return ConversionResult(
134
+ markdown=md,
135
+ metadata={"source": str(path), "root_tag": root.tag},
136
+ )
137
+
138
+
139
+ class HTMLConverter(Converter):
140
+ """Strip HTML tags → plain text with basic structure preservation."""
141
+
142
+ _TAG_RE = re.compile(r"<[^>]+>")
143
+ _BLOCK_RE = re.compile(r"</(p|div|h[1-6]|li|tr|br)\s*>", re.I)
144
+
145
+ def supports(self, path: Path) -> bool:
146
+ return path.suffix.lower() in {".html", ".htm"}
147
+
148
+ def convert_local(self, path: Path) -> ConversionResult:
149
+ html = path.read_text(encoding="utf-8", errors="replace")
150
+ return self._from_html(html, path)
151
+
152
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
153
+ return self._from_html(data.decode("utf-8", errors="replace"), Path(name))
154
+
155
+ def _from_html(self, html: str, path: Path) -> ConversionResult:
156
+ # insert newlines after block-level closes
157
+ text = self._BLOCK_RE.sub("\n", html)
158
+ text = self._TAG_RE.sub("", text)
159
+ # collapse excessive blank lines
160
+ text = re.sub(r"\n{3,}", "\n\n", text).strip()
161
+ md = f"{text}\n"
162
+ return ConversionResult(
163
+ markdown=md,
164
+ metadata={"source": str(path), "chars": len(text)},
165
+ )
166
+
167
+
168
+ class TextConverter(Converter):
169
+ """Plain text / Markdown passthrough."""
170
+
171
+ def supports(self, path: Path) -> bool:
172
+ return path.suffix.lower() in {".txt", ".md", ".markdown", ".rst", ".log"}
173
+
174
+ def convert_local(self, path: Path) -> ConversionResult:
175
+ text = path.read_text(encoding="utf-8", errors="replace")
176
+ return ConversionResult(
177
+ markdown=text,
178
+ metadata={"source": str(path), "chars": len(text)},
179
+ )
180
+
181
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
182
+ text = data.decode("utf-8", errors="replace")
183
+ return ConversionResult(
184
+ markdown=text,
185
+ metadata={"source": name, "chars": len(text)},
186
+ )
187
+
188
+
189
+ class ZipConverter(Converter):
190
+ """Traverse ZIP archives and convert each entry with a sub-registry."""
191
+
192
+ def __init__(self, sub_registry: "ConverterRegistry", max_files: int = 50, max_bytes: int = 5_000_000):
193
+ self.sub_registry = sub_registry
194
+ self.max_files = max_files
195
+ self.max_bytes = max_bytes
196
+
197
+ def supports(self, path: Path) -> bool:
198
+ return path.suffix.lower() == ".zip"
199
+
200
+ def convert_local(self, path: Path) -> ConversionResult:
201
+ parts: list[str] = []
202
+ warnings: list[str] = []
203
+ total_bytes = 0
204
+ file_count = 0
205
+ with zipfile.ZipFile(path) as zf:
206
+ for info in zf.infolist():
207
+ if info.is_dir():
208
+ continue
209
+ if file_count >= self.max_files:
210
+ warnings.append(f"max_files={self.max_files} reached, remaining entries skipped")
211
+ break
212
+ if total_bytes + info.file_size > self.max_bytes:
213
+ warnings.append(f"max_bytes={self.max_bytes} reached, remaining entries skipped")
214
+ break
215
+ data = zf.read(info)
216
+ total_bytes += len(data)
217
+ file_count += 1
218
+ inner_path = Path(info.filename)
219
+ try:
220
+ sub = self.sub_registry.convert_local(inner_path, _data_override=data)
221
+ except UnsupportedFormat:
222
+ parts.append(f"## {info.filename}\n\n*(binary or unsupported)*\n")
223
+ continue
224
+ parts.append(f"## {info.filename}\n\n{sub.markdown}")
225
+ return ConversionResult(
226
+ markdown="\n\n".join(parts),
227
+ metadata={"source": str(path), "entries": file_count, "total_bytes": total_bytes},
228
+ warnings=warnings,
229
+ )
230
+
231
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
232
+ # write to temp, delegate
233
+ import tempfile
234
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
235
+ tf.write(data)
236
+ tf_path = Path(tf.name)
237
+ try:
238
+ return self.convert_local(tf_path)
239
+ finally:
240
+ os.unlink(tf_path)
241
+
242
+
243
+ # ── registry ──────────────────────────────────────────────────────────
244
+
245
+
246
+ class ConverterRegistry:
247
+ """Ordered registry of converters with narrow reader methods."""
248
+
249
+ def __init__(self) -> None:
250
+ self._converters: list[Converter] = []
251
+
252
+ def register(self, converter: Converter) -> None:
253
+ self._converters.append(converter)
254
+
255
+ def _find(self, path: Path) -> Converter:
256
+ for c in self._converters:
257
+ if c.supports(path):
258
+ return c
259
+ raise UnsupportedFormat(f"No converter for {path.suffix}")
260
+
261
+ def convert_local(self, path: Path, _data_override: bytes | None = None) -> ConversionResult:
262
+ converter = self._find(path)
263
+ if _data_override is not None and hasattr(converter, "convert_stream"):
264
+ return converter.convert_stream(_data_override, str(path))
265
+ return converter.convert_local(path)
266
+
267
+ def convert_stream(self, data: bytes, name: str = "") -> ConversionResult:
268
+ path = Path(name or "stream")
269
+ converter = self._find(path)
270
+ return converter.convert_stream(data, name)
271
+
272
+ def supported_suffixes(self) -> set[str]:
273
+ suffixes: set[str] = set()
274
+ for c in self._converters:
275
+ # probe common suffixes
276
+ for suf in [".csv", ".tsv", ".json", ".xml", ".html", ".htm", ".txt", ".md", ".zip"]:
277
+ if c.supports(Path(f"file{suf}")):
278
+ suffixes.add(suf)
279
+ return suffixes
280
+
281
+
282
+ def default_registry() -> ConverterRegistry:
283
+ """Build a registry with all built-in converters."""
284
+ reg = ConverterRegistry()
285
+ reg.register(CSVConverter())
286
+ reg.register(JSONConverter())
287
+ reg.register(XMLConverter())
288
+ reg.register(HTMLConverter())
289
+ reg.register(TextConverter())
290
+ # zip needs a sub-registry without itself to avoid recursion
291
+ sub = ConverterRegistry()
292
+ sub.register(CSVConverter())
293
+ sub.register(JSONConverter())
294
+ sub.register(XMLConverter())
295
+ sub.register(HTMLConverter())
296
+ sub.register(TextConverter())
297
+ reg.register(ZipConverter(sub))
298
+ return reg
@@ -0,0 +1,151 @@
1
+ """H18 — Dual-Layer Validation.
2
+
3
+ Validator probes check claims, then an independent skeptic attempts to refute.
4
+ Mined from GLOSSOPETRAE VALIDATION.md: six validators + independent skeptic.
5
+
6
+ LLM integration: optionally uses an LLM as the skeptic. Falls back to
7
+ rule-based refutation when no model is available.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Callable
13
+
14
+
15
+ @dataclass
16
+ class ValidationResult:
17
+ """Result of a single validator probe."""
18
+
19
+ validator_name: str
20
+ passed: bool
21
+ message: str = ""
22
+ evidence: list[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass
26
+ class DualLayerResult:
27
+ """Combined result of validator + skeptic layers."""
28
+
29
+ entry_id: str
30
+ validator_results: list[ValidationResult] = field(default_factory=list)
31
+ skeptic_result: ValidationResult | None = None
32
+ final_verdict: bool = False
33
+ confidence: float = 0.0
34
+
35
+
36
+ def run_validators(
37
+ entry_id: str,
38
+ claims: dict[str, Any],
39
+ validators: list[tuple[str, Callable[[str, dict], ValidationResult]]],
40
+ ) -> list[ValidationResult]:
41
+ """Run multiple validator probes on a claim set.
42
+
43
+ Args:
44
+ entry_id: The entry being validated.
45
+ claims: The claims to validate.
46
+ validators: List of (name, validator_fn) tuples.
47
+
48
+ Returns:
49
+ List of ValidationResult from each validator.
50
+ """
51
+ results: list[ValidationResult] = []
52
+ for name, fn in validators:
53
+ result = fn(entry_id, claims)
54
+ if not isinstance(result, ValidationResult):
55
+ result = ValidationResult(
56
+ validator_name=name,
57
+ passed=bool(result),
58
+ )
59
+ results.append(result)
60
+ return results
61
+
62
+
63
+ def run_skeptic(
64
+ entry_id: str,
65
+ claims: dict[str, Any],
66
+ validator_results: list[ValidationResult],
67
+ model_client: Any | None = None,
68
+ ) -> ValidationResult:
69
+ """Run an independent skeptic that attempts to refute the validators.
70
+
71
+ Args:
72
+ entry_id: The entry being scrutinized.
73
+ claims: The claims to attack.
74
+ validator_results: Results from the validator layer.
75
+ model_client: Optional LLM for deeper refutation.
76
+
77
+ Returns:
78
+ ValidationResult from the skeptic's perspective.
79
+ """
80
+ # Rule-based skeptic: find any validator that failed
81
+ failed_validators = [r for r in validator_results if not r.passed]
82
+ if failed_validators:
83
+ return ValidationResult(
84
+ validator_name="skeptic",
85
+ passed=False,
86
+ message=f"Skeptic confirms failures from: {[r.validator_name for r in failed_validators]}",
87
+ evidence=[r.message for r in failed_validators],
88
+ )
89
+
90
+ # Rule-based skeptic: check for unsupported claims
91
+ unsupported = []
92
+ for key, value in claims.items():
93
+ if value is None:
94
+ unsupported.append(f"{key} is None")
95
+ elif isinstance(value, str) and not value.strip():
96
+ unsupported.append(f"{key} is empty")
97
+
98
+ if unsupported:
99
+ return ValidationResult(
100
+ validator_name="skeptic",
101
+ passed=False,
102
+ message="Skeptic found unsupported claims",
103
+ evidence=unsupported,
104
+ )
105
+
106
+ return ValidationResult(
107
+ validator_name="skeptic",
108
+ passed=True,
109
+ message="Skeptic could not refute validators",
110
+ )
111
+
112
+
113
+ def dual_layer_validate(
114
+ entry_id: str,
115
+ claims: dict[str, Any],
116
+ validators: list[tuple[str, Callable[[str, dict], ValidationResult]]],
117
+ model_client: Any | None = None,
118
+ ) -> DualLayerResult:
119
+ """Run full dual-layer validation.
120
+
121
+ Args:
122
+ entry_id: The entry being validated.
123
+ claims: The claims to validate.
124
+ validators: List of (name, validator_fn) tuples.
125
+ model_client: Optional LLM for skeptic.
126
+
127
+ Returns:
128
+ DualLayerResult with both layers and final verdict.
129
+ """
130
+ validator_results = run_validators(entry_id, claims, validators)
131
+ skeptic_result = run_skeptic(entry_id, claims, validator_results, model_client)
132
+
133
+ # Final verdict: all validators must pass AND skeptic must pass
134
+ all_validators_pass = all(r.passed for r in validator_results)
135
+ final_verdict = all_validators_pass and skeptic_result.passed
136
+
137
+ # Confidence: fraction of validators that passed
138
+ if validator_results:
139
+ validator_pass_rate = sum(1 for r in validator_results if r.passed) / len(validator_results)
140
+ else:
141
+ validator_pass_rate = 0.0
142
+
143
+ confidence = validator_pass_rate * (1.0 if skeptic_result.passed else 0.5)
144
+
145
+ return DualLayerResult(
146
+ entry_id=entry_id,
147
+ validator_results=validator_results,
148
+ skeptic_result=skeptic_result,
149
+ final_verdict=final_verdict,
150
+ confidence=confidence,
151
+ )
@@ -0,0 +1,73 @@
1
+ """H7 — Echo-Fidelity Guard.
2
+
3
+ Distinguishes ``None`` (unmeasured) from ``0.0`` (measured failure) from a
4
+ valid float. Prevents fabrications where a missing measurement is silently
5
+ treated as a zero (or vice-versa).
6
+
7
+ Source: GLOSSOPETRAE experiments/lib — null vs 0 distinction.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Optional
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class GuardResult:
17
+ """Outcome of an echo-fidelity check."""
18
+
19
+ value: Optional[float]
20
+ status: str # "unmeasured" | "measured" | "invalid"
21
+ message: str
22
+
23
+ @property
24
+ def is_measured(self) -> bool:
25
+ return self.status == "measured"
26
+
27
+
28
+ class EchoFidelityGuard:
29
+ """Guard that distinguishes unmeasured from measured-zero values."""
30
+
31
+ def check(self, value: object) -> GuardResult:
32
+ """Check a value and return its fidelity status.
33
+
34
+ - ``None`` → unmeasured (no data collected)
35
+ - ``float`` or ``int`` in valid range → measured
36
+ - ``float`` or ``int`` outside [lo, hi] → invalid
37
+ """
38
+ if value is None:
39
+ return GuardResult(
40
+ value=None,
41
+ status="unmeasured",
42
+ message="No measurement was taken (value is None).",
43
+ )
44
+ if isinstance(value, bool):
45
+ return GuardResult(
46
+ value=None,
47
+ status="invalid",
48
+ message="Boolean is not a valid measurement.",
49
+ )
50
+ if isinstance(value, (int, float)):
51
+ v = float(value)
52
+ return GuardResult(
53
+ value=v,
54
+ status="measured",
55
+ message=f"Measurement recorded: {v}",
56
+ )
57
+ return GuardResult(
58
+ value=None,
59
+ status="invalid",
60
+ message=f"Expected numeric or None, got {type(value).__name__}.",
61
+ )
62
+
63
+ def check_many(self, values: list[object]) -> list[GuardResult]:
64
+ """Check a batch of values."""
65
+ return [self.check(v) for v in values]
66
+
67
+ def measured_values(self, values: list[object]) -> list[float]:
68
+ """Return only the measured float values, skipping unmeasured and invalid."""
69
+ return [
70
+ r.value
71
+ for r in self.check_many(values)
72
+ if r.is_measured and r.value is not None
73
+ ]