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,135 @@
1
+ """B96/B100/B101. Construction WIP, fixed assets, payroll reconciliations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from decimal import Decimal
7
+ from typing import Any, Iterable
8
+
9
+ from .common import ReviewGate, SourceRef, decimalize, stable_exception_id, within_tolerance
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class JobCost:
14
+ id: str
15
+ contract_value: Decimal | int | str
16
+ costs_to_date: Decimal | int | str
17
+ estimated_total_cost: Decimal | int | str
18
+ billed_to_date: Decimal | int | str
19
+ source_refs: list[SourceRef] = field(default_factory=list)
20
+
21
+ def __post_init__(self) -> None:
22
+ for attr in ("contract_value", "costs_to_date", "estimated_total_cost", "billed_to_date"):
23
+ object.__setattr__(self, attr, decimalize(getattr(self, attr)))
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class WIPRow:
28
+ job_id: str
29
+ percent_complete: Decimal
30
+ earned_revenue: Decimal
31
+ billed_to_date: Decimal
32
+ over_under_billing: Decimal
33
+ source_refs: list[SourceRef]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class WIPException:
38
+ id: str
39
+ job_id: str
40
+ amount: Decimal
41
+ message: str
42
+ source_refs: list[SourceRef]
43
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class WIPReport:
48
+ rows: list[WIPRow]
49
+ total_over_under: Decimal
50
+ exceptions: list[WIPException]
51
+
52
+
53
+ class ConstructionWIPController:
54
+ def compute_wip(self, jobs: Iterable[JobCost | dict[str, Any]], materiality: Decimal | int | str = Decimal("1000")) -> WIPReport:
55
+ rows: list[WIPRow] = []
56
+ exceptions: list[WIPException] = []
57
+ threshold = decimalize(materiality)
58
+ for job in [_coerce_job(row) for row in jobs]:
59
+ pct = Decimal("0") if job.estimated_total_cost == 0 else min(job.costs_to_date / job.estimated_total_cost, Decimal("1"))
60
+ earned = job.contract_value * pct
61
+ over_under = job.billed_to_date - earned
62
+ row = WIPRow(job.id, pct, earned, job.billed_to_date, over_under, list(job.source_refs))
63
+ rows.append(row)
64
+ if abs(over_under) >= threshold:
65
+ message = "overbilling" if over_under > 0 else "underbilling"
66
+ exceptions.append(WIPException(stable_exception_id("B96", [job.id, over_under]), job.id, over_under, message, list(job.source_refs)))
67
+ return WIPReport(rows=rows, total_over_under=sum((row.over_under_billing for row in rows), Decimal("0")), exceptions=exceptions)
68
+
69
+ def flag_over_under_billing(self, jobs: Iterable[JobCost | dict[str, Any]], materiality: Decimal | int | str = Decimal("1000")) -> list[WIPException]:
70
+ return self.compute_wip(jobs, materiality=materiality).exceptions
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class RollForwardResult:
75
+ clean: bool
76
+ beginning: Decimal
77
+ additions: Decimal
78
+ disposals: Decimal
79
+ depreciation: Decimal
80
+ ending: Decimal
81
+ expected_ending: Decimal
82
+ difference: Decimal
83
+ source_refs: list[SourceRef] = field(default_factory=list)
84
+
85
+
86
+ class FixedAssetRollForward:
87
+ def roll_forward(
88
+ self,
89
+ assets: Iterable[dict[str, Any]],
90
+ additions: Iterable[dict[str, Any]],
91
+ disposals: Iterable[dict[str, Any]],
92
+ depreciation: Iterable[dict[str, Any]],
93
+ tolerance: Decimal | int | str = Decimal("0"),
94
+ ) -> RollForwardResult:
95
+ beginning = sum((decimalize(row.get("beginning_nbv", row.get("amount", 0))) for row in assets), Decimal("0"))
96
+ ending = sum((decimalize(row.get("ending_nbv", row.get("ending", row.get("amount", 0)))) for row in assets), Decimal("0"))
97
+ add = sum((decimalize(row.get("amount", 0)) for row in additions), Decimal("0"))
98
+ disp = sum((decimalize(row.get("amount", 0)) for row in disposals), Decimal("0"))
99
+ dep = sum((decimalize(row.get("amount", 0)) for row in depreciation), Decimal("0"))
100
+ expected = beginning + add - disp - dep
101
+ refs = [ref for row in [*assets, *additions, *disposals, *depreciation] for ref in row.get("source_refs", [])]
102
+ return RollForwardResult(within_tolerance(ending, expected, tolerance), beginning, add, disp, dep, ending, expected, ending - expected, refs)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class PayrollReconResult:
107
+ clean: bool
108
+ register_total: Decimal
109
+ gl_total: Decimal
110
+ tax_liability_total: Decimal
111
+ differences: dict[str, Decimal]
112
+ source_refs: list[SourceRef]
113
+
114
+
115
+ class PayrollReconciler:
116
+ def reconcile(
117
+ self,
118
+ payroll_register: Iterable[dict[str, Any]],
119
+ gl: Iterable[dict[str, Any]],
120
+ tax_liabilities: Iterable[dict[str, Any]],
121
+ tolerance: Decimal | int | str = Decimal("0"),
122
+ ) -> PayrollReconResult:
123
+ register_total = sum((decimalize(row.get("gross_pay", row.get("amount", 0))) for row in payroll_register), Decimal("0"))
124
+ gl_total = sum((decimalize(row.get("amount", 0)) for row in gl), Decimal("0"))
125
+ tax_total = sum((decimalize(row.get("amount", 0)) for row in tax_liabilities), Decimal("0"))
126
+ differences = {"register_to_gl": register_total - gl_total, "tax_liability": tax_total}
127
+ refs = [ref for row in [*payroll_register, *gl, *tax_liabilities] for ref in row.get("source_refs", [])]
128
+ clean = within_tolerance(register_total, gl_total, tolerance) and bool(tax_liabilities)
129
+ return PayrollReconResult(clean, register_total, gl_total, tax_total, differences, refs)
130
+
131
+
132
+ def _coerce_job(row: JobCost | dict[str, Any]) -> JobCost:
133
+ if isinstance(row, JobCost):
134
+ return row
135
+ return JobCost(str(row["id"]), row.get("contract_value", 0), row.get("costs_to_date", 0), row.get("estimated_total_cost", 0), row.get("billed_to_date", 0), list(row.get("source_refs", [])))
@@ -0,0 +1,172 @@
1
+ """B87. COSO Risk-Control Matrix Compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from decimal import Decimal
7
+ from typing import Iterable
8
+
9
+ from .common import RiskLevel, Severity, SourceRef, stable_exception_id
10
+ from .exceptions import make_exception
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FinanceProcess:
15
+ id: str
16
+ name: str
17
+ owner: str | None = None
18
+ assertions: set[str] = field(default_factory=set)
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Risk:
23
+ id: str
24
+ process_id: str
25
+ description: str
26
+ level: RiskLevel = RiskLevel.MEDIUM
27
+ assertion: str | None = None
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Control:
32
+ id: str
33
+ process_id: str
34
+ description: str
35
+ covers_risk_ids: set[str] = field(default_factory=set)
36
+ owner: str | None = None
37
+ control_type: str = "detective" # preventive, detective, corrective
38
+ frequency: str | None = None
39
+ evidence_required: str | None = None
40
+ automated: bool = False
41
+ key_control: bool = False
42
+ source_refs: list[SourceRef] = field(default_factory=list)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class RCMRow:
47
+ process: FinanceProcess
48
+ risk: Risk
49
+ controls: list[Control]
50
+ residual_level: RiskLevel
51
+ assertions: set[str]
52
+
53
+
54
+ @dataclass
55
+ class RiskControlMatrix:
56
+ rows: list[RCMRow]
57
+ control_count: int
58
+ risk_count: int
59
+
60
+ @property
61
+ def coverage_ratio(self) -> Decimal:
62
+ if not self.risk_count:
63
+ return Decimal("1")
64
+ covered = sum(1 for row in self.rows if row.controls)
65
+ return Decimal(covered) / Decimal(self.risk_count)
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ControlGap:
70
+ id: str
71
+ risk_id: str | None
72
+ control_id: str | None
73
+ severity: Severity
74
+ message: str
75
+ source_refs: list[SourceRef] = field(default_factory=list)
76
+
77
+
78
+ class RiskControlMatrixCompiler:
79
+ """Compile process/risk/control metadata into a controller RCM."""
80
+
81
+ def compile(
82
+ self,
83
+ processes: Iterable[FinanceProcess],
84
+ risks: Iterable[Risk],
85
+ controls: Iterable[Control],
86
+ ) -> RiskControlMatrix:
87
+ process_by_id = {process.id: process for process in processes}
88
+ controls_by_risk: dict[str, list[Control]] = {}
89
+ control_rows = list(controls)
90
+ for control in control_rows:
91
+ for risk_id in control.covers_risk_ids:
92
+ controls_by_risk.setdefault(risk_id, []).append(control)
93
+
94
+ rows: list[RCMRow] = []
95
+ risk_rows = list(risks)
96
+ for risk in risk_rows:
97
+ process = process_by_id.get(risk.process_id) or FinanceProcess(id=risk.process_id, name=risk.process_id)
98
+ covered_controls = sorted(controls_by_risk.get(risk.id, []), key=lambda c: c.id)
99
+ residual = _residual_level(risk.level, covered_controls)
100
+ assertions = set(process.assertions)
101
+ if risk.assertion:
102
+ assertions.add(risk.assertion)
103
+ rows.append(RCMRow(
104
+ process=process,
105
+ risk=risk,
106
+ controls=covered_controls,
107
+ residual_level=residual,
108
+ assertions=assertions,
109
+ ))
110
+ return RiskControlMatrix(rows=rows, control_count=len(control_rows), risk_count=len(risk_rows))
111
+
112
+ def find_gaps(self, matrix: RiskControlMatrix) -> list[ControlGap]:
113
+ gaps: list[ControlGap] = []
114
+ for row in matrix.rows:
115
+ if not row.controls:
116
+ gaps.append(ControlGap(
117
+ id=stable_exception_id("B87", [row.risk.id, "no-control"]),
118
+ risk_id=row.risk.id,
119
+ control_id=None,
120
+ severity=Severity.HIGH if row.risk.level >= RiskLevel.HIGH else Severity.MEDIUM,
121
+ message=f"Risk {row.risk.id} has no mapped control",
122
+ ))
123
+ continue
124
+ if not any(control.control_type.lower() == "preventive" for control in row.controls):
125
+ gaps.append(ControlGap(
126
+ id=stable_exception_id("B87", [row.risk.id, "no-preventive"]),
127
+ risk_id=row.risk.id,
128
+ control_id=None,
129
+ severity=Severity.LOW,
130
+ message=f"Risk {row.risk.id} has no preventive control",
131
+ ))
132
+ for control in row.controls:
133
+ if not control.owner:
134
+ gaps.append(ControlGap(
135
+ id=stable_exception_id("B87", [row.risk.id, control.id, "owner"]),
136
+ risk_id=row.risk.id,
137
+ control_id=control.id,
138
+ severity=Severity.MEDIUM,
139
+ message=f"Control {control.id} lacks an owner",
140
+ source_refs=list(control.source_refs),
141
+ ))
142
+ if control.key_control and not control.evidence_required:
143
+ gaps.append(ControlGap(
144
+ id=stable_exception_id("B87", [row.risk.id, control.id, "evidence"]),
145
+ risk_id=row.risk.id,
146
+ control_id=control.id,
147
+ severity=Severity.HIGH,
148
+ message=f"Key control {control.id} lacks evidence requirement",
149
+ source_refs=list(control.source_refs),
150
+ ))
151
+ return gaps
152
+
153
+ def exceptions_for_gaps(self, gaps: Iterable[ControlGap]):
154
+ return [
155
+ make_exception(
156
+ "B87",
157
+ gap.message,
158
+ severity=gap.severity,
159
+ tags={"control-gap", f"risk:{gap.risk_id}"} if gap.risk_id else {"control-gap"},
160
+ source_refs=gap.source_refs,
161
+ )
162
+ for gap in gaps
163
+ ]
164
+
165
+
166
+ def _residual_level(level: RiskLevel, controls: list[Control]) -> RiskLevel:
167
+ if not controls:
168
+ return level
169
+ reduction = 1 if any(control.key_control for control in controls) else 0
170
+ if any(control.automated for control in controls):
171
+ reduction += 1
172
+ return RiskLevel(max(int(RiskLevel.LOW), int(level) - reduction))
@@ -0,0 +1,119 @@
1
+ """B103. Evidence Binder / PBC Indexer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Iterable
8
+
9
+ from .common import EvidenceItem, EvidenceStatus, SourceRef
10
+
11
+
12
+ @dataclass
13
+ class EvidenceRequest:
14
+ id: str
15
+ description: str
16
+ required: bool = True
17
+ evidence: list[EvidenceItem] = field(default_factory=list)
18
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
19
+ reviewed_at: datetime | None = None
20
+ expected_hashes: set[str] = field(default_factory=set)
21
+
22
+
23
+ @dataclass
24
+ class EvidenceReport:
25
+ total_requests: int
26
+ complete_count: int
27
+ missing_count: int
28
+ stale_count: int
29
+ open_ids: list[str]
30
+ stale_ids: list[str]
31
+
32
+ @property
33
+ def complete(self) -> bool:
34
+ return self.missing_count == 0 and self.stale_count == 0
35
+
36
+
37
+ class EvidenceBinder:
38
+ """Indexes support for workpapers/PBC requests with source hashes."""
39
+
40
+ def __init__(self) -> None:
41
+ self.requests: dict[str, EvidenceRequest] = {}
42
+
43
+ def add_request(self, request_id: str, description: str, required: bool = True) -> EvidenceRequest:
44
+ request = EvidenceRequest(id=request_id, description=description, required=required)
45
+ self.requests[request_id] = request
46
+ return request
47
+
48
+ def attach_evidence(
49
+ self,
50
+ request_id: str,
51
+ item_id: str,
52
+ description: str,
53
+ source_refs: Iterable[SourceRef],
54
+ prepared_by: str | None = None,
55
+ ) -> EvidenceItem:
56
+ request = self.requests[request_id]
57
+ item = EvidenceItem(
58
+ id=item_id,
59
+ description=description,
60
+ source_refs=list(source_refs),
61
+ prepared_by=prepared_by,
62
+ status=EvidenceStatus.RECEIVED,
63
+ )
64
+ request.evidence.append(item)
65
+ return item
66
+
67
+ def mark_reviewed(self, request_id: str, reviewer: str) -> None:
68
+ request = self.requests[request_id]
69
+ request.expected_hashes = {
70
+ ref.document_hash
71
+ for item in request.evidence
72
+ for ref in item.source_refs
73
+ if ref.document_hash
74
+ }
75
+ for item in request.evidence:
76
+ item.reviewed_by = reviewer
77
+ item.status = EvidenceStatus.REVIEWED
78
+ request.reviewed_at = datetime.now(timezone.utc)
79
+
80
+ def refresh_stale_status(self) -> list[str]:
81
+ stale_ids: list[str] = []
82
+ for request in self.requests.values():
83
+ if not request.expected_hashes:
84
+ continue
85
+ current_hashes = {
86
+ ref.document_hash
87
+ for item in request.evidence
88
+ for ref in item.source_refs
89
+ if ref.document_hash
90
+ }
91
+ if current_hashes != request.expected_hashes:
92
+ stale_ids.append(request.id)
93
+ for item in request.evidence:
94
+ item.status = EvidenceStatus.STALE
95
+ return stale_ids
96
+
97
+ def completeness_report(self) -> EvidenceReport:
98
+ stale_ids = self.refresh_stale_status()
99
+ open_ids: list[str] = []
100
+ complete_count = 0
101
+ missing_count = 0
102
+ for request in self.requests.values():
103
+ has_support = any(item.has_support for item in request.evidence)
104
+ reviewed_or_received = any(
105
+ item.status in (EvidenceStatus.RECEIVED, EvidenceStatus.REVIEWED) for item in request.evidence
106
+ )
107
+ if request.required and not (has_support and reviewed_or_received):
108
+ missing_count += 1
109
+ open_ids.append(request.id)
110
+ else:
111
+ complete_count += 1
112
+ return EvidenceReport(
113
+ total_requests=len(self.requests),
114
+ complete_count=complete_count,
115
+ missing_count=missing_count,
116
+ stale_count=len(stale_ids),
117
+ open_ids=open_ids,
118
+ stale_ids=stale_ids,
119
+ )
@@ -0,0 +1,157 @@
1
+ """B105. Controller Exception Queue with Materiality Gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import date
7
+ from decimal import Decimal
8
+ from typing import Iterable
9
+
10
+ from .common import FinanceException, MaterialityPolicy, ReviewGate, Severity, stable_exception_id
11
+
12
+
13
+ @dataclass
14
+ class ExceptionCluster:
15
+ key: str
16
+ exceptions: list[FinanceException]
17
+ highest_severity: Severity
18
+ total_amount: Decimal
19
+ representative: FinanceException
20
+
21
+
22
+ @dataclass
23
+ class WaiverResult:
24
+ approved: bool
25
+ exception: FinanceException
26
+ message: str
27
+
28
+
29
+ @dataclass
30
+ class QueueSummary:
31
+ total: int
32
+ open_count: int
33
+ waived_count: int
34
+ requires_review_count: int
35
+ by_pattern: dict[str, int] = field(default_factory=dict)
36
+
37
+
38
+ class ControllerExceptionQueue:
39
+ """Finite controller queue that ranks exceptions by materiality and risk.
40
+
41
+ This queue is intentionally non-mutating with respect to accounting systems:
42
+ it routes review work and waiver metadata only. It never posts entries,
43
+ approves payments, or sends external communications.
44
+ """
45
+
46
+ def __init__(self) -> None:
47
+ self._exceptions: dict[str, FinanceException] = {}
48
+
49
+ def add(self, exception: FinanceException) -> None:
50
+ self._exceptions[exception.id] = exception
51
+
52
+ def extend(self, exceptions: Iterable[FinanceException]) -> None:
53
+ for exception in exceptions:
54
+ self.add(exception)
55
+
56
+ def all(self) -> list[FinanceException]:
57
+ return list(self._exceptions.values())
58
+
59
+ def rank(
60
+ self,
61
+ materiality_policy: MaterialityPolicy | None = None,
62
+ as_of: date | None = None,
63
+ include_waived: bool = False,
64
+ ) -> list[FinanceException]:
65
+ policy = materiality_policy or MaterialityPolicy()
66
+ rows = [ex for ex in self._exceptions.values() if include_waived or not ex.waived]
67
+ return sorted(
68
+ rows,
69
+ key=lambda ex: (
70
+ policy.score_exception(ex, as_of=as_of),
71
+ int(ex.severity),
72
+ abs(ex.amount or Decimal("0")),
73
+ ex.id,
74
+ ),
75
+ reverse=True,
76
+ )
77
+
78
+ def cluster_duplicates(self) -> list[ExceptionCluster]:
79
+ clusters: dict[str, list[FinanceException]] = {}
80
+ for exception in self._exceptions.values():
81
+ key = _cluster_key(exception)
82
+ clusters.setdefault(key, []).append(exception)
83
+
84
+ out: list[ExceptionCluster] = []
85
+ for key, exceptions in clusters.items():
86
+ total = sum((abs(ex.amount or Decimal("0")) for ex in exceptions), Decimal("0"))
87
+ representative = sorted(
88
+ exceptions,
89
+ key=lambda ex: (int(ex.severity), abs(ex.amount or Decimal("0")), ex.id),
90
+ reverse=True,
91
+ )[0]
92
+ out.append(ExceptionCluster(
93
+ key=key,
94
+ exceptions=exceptions,
95
+ highest_severity=max((ex.severity for ex in exceptions), default=Severity.INFO),
96
+ total_amount=total,
97
+ representative=representative,
98
+ ))
99
+ return sorted(out, key=lambda c: (len(c.exceptions), int(c.highest_severity), c.total_amount), reverse=True)
100
+
101
+ def require_approval_for_waiver(
102
+ self,
103
+ exception_id: str,
104
+ rationale: str | None,
105
+ approver: str | None,
106
+ expires: date | None,
107
+ ) -> WaiverResult:
108
+ exception = self._exceptions[exception_id]
109
+ if not rationale or not approver or expires is None:
110
+ return WaiverResult(False, exception, "Waiver requires rationale, approver, and expiration period.")
111
+ exception.waived = True
112
+ exception.waiver_rationale = rationale
113
+ exception.waiver_approver = approver
114
+ exception.waiver_expires = expires
115
+ exception.review_gate = ReviewGate.CONTROLLER_REVIEW
116
+ return WaiverResult(True, exception, "Waiver recorded for controller review trail.")
117
+
118
+ def summary(self) -> QueueSummary:
119
+ by_pattern: dict[str, int] = {}
120
+ for exception in self._exceptions.values():
121
+ by_pattern[exception.pattern_id] = by_pattern.get(exception.pattern_id, 0) + 1
122
+ return QueueSummary(
123
+ total=len(self._exceptions),
124
+ open_count=sum(1 for ex in self._exceptions.values() if not ex.waived),
125
+ waived_count=sum(1 for ex in self._exceptions.values() if ex.waived),
126
+ requires_review_count=sum(1 for ex in self._exceptions.values() if ex.requires_human_review),
127
+ by_pattern=by_pattern,
128
+ )
129
+
130
+
131
+ def make_exception(
132
+ pattern_id: str,
133
+ message: str,
134
+ severity: Severity = Severity.MEDIUM,
135
+ amount: Decimal | int | str | None = None,
136
+ tags: Iterable[str] = (),
137
+ source_refs=None,
138
+ ) -> FinanceException:
139
+ key = [pattern_id, message, str(amount), sorted(str(tag).lower() for tag in tags)]
140
+ return FinanceException(
141
+ id=stable_exception_id(pattern_id, key),
142
+ pattern_id=pattern_id,
143
+ severity=severity,
144
+ amount=amount,
145
+ message=message,
146
+ source_refs=list(source_refs or []),
147
+ tags=set(tags),
148
+ )
149
+
150
+
151
+ def _cluster_key(exception: FinanceException) -> str:
152
+ root_tags = sorted(tag for tag in exception.tags if tag.startswith("root:") or tag.startswith("account:"))
153
+ if root_tags:
154
+ return "|".join([exception.pattern_id, *root_tags])
155
+ source_ids = sorted(ref.source_id for ref in exception.source_refs)
156
+ normalized_message = " ".join(exception.message.lower().split()[:8])
157
+ return "|".join([exception.pattern_id, normalized_message, *source_ids])