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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|