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,332 @@
1
+ """B88/B97/B98. Close DAG, variance miner, accrual/reversal engine."""
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 Any, Iterable
9
+
10
+ from .common import FinanceException, ReviewGate, Severity, SourceRef, decimalize, stable_exception_id
11
+ from .exceptions import ControllerExceptionQueue
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CloseTask:
16
+ id: str
17
+ name: str
18
+ duration_hours: Decimal | int | str = Decimal("1")
19
+ dependencies: set[str] = field(default_factory=set)
20
+ owner: str | None = None
21
+ due_day: int | None = None
22
+ completed: bool = False
23
+
24
+ def __post_init__(self) -> None:
25
+ object.__setattr__(self, "duration_hours", decimalize(self.duration_hours))
26
+ object.__setattr__(self, "dependencies", set(self.dependencies))
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class BlockedTask:
31
+ task: CloseTask
32
+ missing_dependencies: set[str]
33
+
34
+
35
+ class CloseDAG:
36
+ """Month-end close task DAG with ready/blocked/critical-path analysis."""
37
+
38
+ def __init__(self) -> None:
39
+ self.tasks: dict[str, CloseTask] = {}
40
+
41
+ def add_task(self, task: CloseTask) -> None:
42
+ self.tasks[task.id] = task
43
+
44
+ def ready_tasks(self, completed: set[str] | None = None) -> list[CloseTask]:
45
+ completed = set(completed or ()) | {task.id for task in self.tasks.values() if task.completed}
46
+ return sorted(
47
+ [
48
+ task for task in self.tasks.values()
49
+ if task.id not in completed and task.dependencies <= completed
50
+ ],
51
+ key=lambda task: (task.due_day if task.due_day is not None else 99, task.id),
52
+ )
53
+
54
+ def blocked_tasks(self, completed: set[str] | None = None) -> list[BlockedTask]:
55
+ completed = set(completed or ()) | {task.id for task in self.tasks.values() if task.completed}
56
+ blocked: list[BlockedTask] = []
57
+ for task in self.tasks.values():
58
+ missing = task.dependencies - completed
59
+ if task.id not in completed and missing:
60
+ blocked.append(BlockedTask(task=task, missing_dependencies=missing))
61
+ return sorted(blocked, key=lambda row: row.task.id)
62
+
63
+ def topological_order(self) -> list[str]:
64
+ indegree = {task_id: 0 for task_id in self.tasks}
65
+ children: dict[str, list[str]] = {task_id: [] for task_id in self.tasks}
66
+ for task in self.tasks.values():
67
+ for dep in task.dependencies:
68
+ if dep not in self.tasks:
69
+ raise ValueError(f"Unknown dependency {dep!r} for close task {task.id!r}")
70
+ indegree[task.id] += 1
71
+ children[dep].append(task.id)
72
+ ready = sorted([task_id for task_id, degree in indegree.items() if degree == 0])
73
+ order: list[str] = []
74
+ while ready:
75
+ current = ready.pop(0)
76
+ order.append(current)
77
+ for child in sorted(children[current]):
78
+ indegree[child] -= 1
79
+ if indegree[child] == 0:
80
+ ready.append(child)
81
+ ready.sort()
82
+ if len(order) != len(self.tasks):
83
+ raise ValueError("Close DAG contains a cycle")
84
+ return order
85
+
86
+ def critical_path(self) -> list[CloseTask]:
87
+ order = self.topological_order()
88
+ distance: dict[str, Decimal] = {}
89
+ predecessor: dict[str, str | None] = {}
90
+ for task_id in order:
91
+ task = self.tasks[task_id]
92
+ best_dep = None
93
+ best_distance = Decimal("0")
94
+ for dep in task.dependencies:
95
+ if distance[dep] > best_distance:
96
+ best_dep = dep
97
+ best_distance = distance[dep]
98
+ distance[task_id] = best_distance + task.duration_hours
99
+ predecessor[task_id] = best_dep
100
+ if not distance:
101
+ return []
102
+ end = max(distance, key=lambda task_id: (distance[task_id], task_id))
103
+ path_ids: list[str] = []
104
+ while end is not None:
105
+ path_ids.append(end)
106
+ end = predecessor[end]
107
+ path_ids.reverse()
108
+ return [self.tasks[task_id] for task_id in path_ids]
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class AccrualPolicy:
113
+ id: str = "default-accrual"
114
+ minimum_amount: Decimal = Decimal("100")
115
+ require_reversal_date: bool = True
116
+
117
+ def __post_init__(self) -> None:
118
+ object.__setattr__(self, "minimum_amount", decimalize(self.minimum_amount))
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class AccrualProposal:
123
+ id: str
124
+ vendor: str
125
+ amount: Decimal
126
+ basis: str
127
+ reversal_date: date | None
128
+ source_refs: list[SourceRef]
129
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class ReversalProposal:
134
+ id: str
135
+ accrual_id: str
136
+ amount: Decimal
137
+ reversal_date: date
138
+ source_refs: list[SourceRef]
139
+
140
+
141
+ class AccrualEngine:
142
+ """Propose accruals and reversals; never posts journal entries."""
143
+
144
+ def propose_accruals(
145
+ self,
146
+ expenses: Iterable[dict[str, Any]],
147
+ receipts: Iterable[dict[str, Any]],
148
+ invoices: Iterable[dict[str, Any]],
149
+ policy: AccrualPolicy | None = None,
150
+ ) -> list[AccrualProposal]:
151
+ policy = policy or AccrualPolicy()
152
+ invoiced_keys = {_accrual_key(row) for row in invoices}
153
+ proposals: list[AccrualProposal] = []
154
+ for row in list(expenses) + list(receipts):
155
+ amount = abs(decimalize(row.get("amount", 0)))
156
+ if amount < policy.minimum_amount:
157
+ continue
158
+ key = _accrual_key(row)
159
+ if key in invoiced_keys:
160
+ continue
161
+ reversal_date = row.get("reversal_date")
162
+ if isinstance(reversal_date, str):
163
+ reversal_date = date.fromisoformat(reversal_date[:10])
164
+ source_refs = list(row.get("source_refs", []))
165
+ if not row.get("basis") or (policy.require_reversal_date and reversal_date is None) or not source_refs:
166
+ continue
167
+ proposal_id = stable_exception_id("B98", [key, amount, reversal_date])
168
+ proposals.append(AccrualProposal(
169
+ id=proposal_id,
170
+ vendor=str(row.get("vendor", "")),
171
+ amount=amount,
172
+ basis=str(row.get("basis")),
173
+ reversal_date=reversal_date,
174
+ source_refs=source_refs,
175
+ ))
176
+ return proposals
177
+
178
+ def propose_reversals(self, prior_accruals: Iterable[AccrualProposal], current_period: str) -> list[ReversalProposal]:
179
+ reversals: list[ReversalProposal] = []
180
+ for accrual in prior_accruals:
181
+ if not accrual.reversal_date:
182
+ continue
183
+ if f"{accrual.reversal_date.year:04d}-{accrual.reversal_date.month:02d}" == current_period:
184
+ reversals.append(ReversalProposal(
185
+ id=stable_exception_id("B98R", [accrual.id, current_period]),
186
+ accrual_id=accrual.id,
187
+ amount=-accrual.amount,
188
+ reversal_date=accrual.reversal_date,
189
+ source_refs=list(accrual.source_refs),
190
+ ))
191
+ return reversals
192
+
193
+
194
+ @dataclass(frozen=True)
195
+ class Variance:
196
+ account: str
197
+ current: Decimal
198
+ comparison: Decimal
199
+ difference: Decimal
200
+ percent_change: Decimal | None
201
+ source_refs: list[SourceRef] = field(default_factory=list)
202
+
203
+
204
+ @dataclass(frozen=True)
205
+ class VarianceExplanation:
206
+ variance: Variance
207
+ driver: str | None
208
+ explanation: str
209
+ supported: bool
210
+ source_refs: list[SourceRef]
211
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
212
+
213
+
214
+ class VarianceExplanationMiner:
215
+ """Detect and explain material flux without accepting tautologies."""
216
+
217
+ def detect_variances(
218
+ self,
219
+ current: dict[str, Any],
220
+ prior: dict[str, Any],
221
+ budget: dict[str, Any] | None = None,
222
+ materiality: Any = Decimal("1000"),
223
+ ) -> list[Variance]:
224
+ baseline = budget or prior
225
+ threshold = abs(decimalize(materiality))
226
+ variances: list[Variance] = []
227
+ for account, current_value in current.items():
228
+ current_amount = decimalize(current_value)
229
+ comparison = decimalize(baseline.get(account, 0))
230
+ difference = current_amount - comparison
231
+ if abs(difference) < threshold:
232
+ continue
233
+ percent = None if comparison == 0 else difference / abs(comparison)
234
+ variances.append(Variance(
235
+ account=account,
236
+ current=current_amount,
237
+ comparison=comparison,
238
+ difference=difference,
239
+ percent_change=percent,
240
+ ))
241
+ return sorted(variances, key=lambda row: (abs(row.difference), row.account), reverse=True)
242
+
243
+ def explain(self, variance: Variance, activity_sources: Iterable[dict[str, Any]]) -> VarianceExplanation:
244
+ allowed = {
245
+ "volume",
246
+ "price",
247
+ "rate",
248
+ "mix",
249
+ "timing",
250
+ "correction",
251
+ "new-vendor",
252
+ "new-customer",
253
+ "job-status",
254
+ "headcount",
255
+ "tax-rate",
256
+ "accrual",
257
+ "reversal",
258
+ "reclass",
259
+ }
260
+ for source in activity_sources:
261
+ if str(source.get("account", "")).lower() != variance.account.lower():
262
+ continue
263
+ driver = str(source.get("driver", "")).lower()
264
+ explanation = str(source.get("explanation", "")).strip()
265
+ source_refs = list(source.get("source_refs", []))
266
+ if driver in allowed and source_refs and not _is_tautology(variance.account, explanation):
267
+ return VarianceExplanation(
268
+ variance=variance,
269
+ driver=driver,
270
+ explanation=explanation,
271
+ supported=True,
272
+ source_refs=source_refs,
273
+ review_gate=ReviewGate.DRAFT,
274
+ )
275
+ return VarianceExplanation(
276
+ variance=variance,
277
+ driver=None,
278
+ explanation="driver unclear — flag for controller",
279
+ supported=False,
280
+ source_refs=[],
281
+ )
282
+
283
+
284
+ @dataclass
285
+ class ClosePackageInputs:
286
+ current_balances: dict[str, Any] = field(default_factory=dict)
287
+ prior_balances: dict[str, Any] = field(default_factory=dict)
288
+ activity_sources: list[dict[str, Any]] = field(default_factory=list)
289
+ materiality: Decimal = Decimal("1000")
290
+
291
+
292
+ @dataclass
293
+ class ClosePackageResult:
294
+ variances: list[Variance]
295
+ explanations: list[VarianceExplanation]
296
+ exception_queue: ControllerExceptionQueue
297
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
298
+
299
+
300
+ class ClosePackageAnalyzer:
301
+ """Compose close analysis and route issues to a controller queue."""
302
+
303
+ def run(self, inputs: ClosePackageInputs) -> ClosePackageResult:
304
+ miner = VarianceExplanationMiner()
305
+ variances = miner.detect_variances(inputs.current_balances, inputs.prior_balances, materiality=inputs.materiality)
306
+ explanations = [miner.explain(variance, inputs.activity_sources) for variance in variances]
307
+ queue = ControllerExceptionQueue()
308
+ for explanation in explanations:
309
+ if not explanation.supported:
310
+ queue.add(FinanceException(
311
+ id=stable_exception_id("B97", [explanation.variance.account, explanation.variance.difference]),
312
+ pattern_id="B97",
313
+ severity=Severity.MEDIUM,
314
+ amount=abs(explanation.variance.difference),
315
+ message=f"Unsupported variance explanation for {explanation.variance.account}",
316
+ source_refs=list(explanation.variance.source_refs),
317
+ tags={"variance", f"account:{explanation.variance.account.lower()}"},
318
+ ))
319
+ return ClosePackageResult(variances=variances, explanations=explanations, exception_queue=queue)
320
+
321
+
322
+ def _accrual_key(row: dict[str, Any]) -> tuple[str, str, str]:
323
+ return (str(row.get("vendor", "")).lower(), str(row.get("description", "")).lower(), str(row.get("period", "")))
324
+
325
+
326
+ def _is_tautology(account: str, explanation: str) -> bool:
327
+ low = explanation.lower()
328
+ if not low:
329
+ return True
330
+ tokens = [token for token in account.lower().replace("-", " ").split() if len(token) > 3]
331
+ tautology_phrases = ("increased due to higher", "decreased due to lower", "changed due to change")
332
+ return any(phrase in low for phrase in tautology_phrases) or (tokens and all(token in low for token in tokens) and len(low.split()) < 8)
@@ -0,0 +1,244 @@
1
+ """Shared finance/controller data contracts.
2
+
3
+ These primitives back Track C patterns B87-B105. They are deliberately
4
+ local-only and deterministic: engines produce draft analysis, provenance, and
5
+ review gates; they never post JEs, approve payments, file taxes, or distribute
6
+ external work product.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from datetime import date, datetime
13
+ from decimal import Decimal, InvalidOperation
14
+ from enum import Enum, IntEnum
15
+ import hashlib
16
+ import json
17
+ import re
18
+ from typing import Any, Iterable
19
+
20
+
21
+ class Severity(IntEnum):
22
+ INFO = 0
23
+ LOW = 1
24
+ MEDIUM = 2
25
+ HIGH = 3
26
+ CRITICAL = 4
27
+
28
+
29
+ class RiskLevel(IntEnum):
30
+ LOW = 1
31
+ MEDIUM = 2
32
+ HIGH = 3
33
+ CRITICAL = 4
34
+
35
+
36
+ class EvidenceStatus(Enum):
37
+ OPEN = "open"
38
+ REQUESTED = "requested"
39
+ RECEIVED = "received"
40
+ REVIEWED = "reviewed"
41
+ STALE = "stale"
42
+ WAIVED = "waived"
43
+
44
+
45
+ class ReviewGate(Enum):
46
+ DRAFT = "draft"
47
+ CONTROLLER_REVIEW = "controller_review"
48
+ CPA_REVIEW = "cpa_review"
49
+ EXTERNAL_APPROVAL_REQUIRED = "external_approval_required"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Money:
54
+ amount: Decimal
55
+ currency: str = "USD"
56
+
57
+ def __post_init__(self) -> None:
58
+ object.__setattr__(self, "amount", decimalize(self.amount))
59
+ object.__setattr__(self, "currency", self.currency.upper())
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class SourceRef:
64
+ source_id: str
65
+ path: str | None = None
66
+ row: int | None = None
67
+ column: str | None = None
68
+ document_hash: str | None = None
69
+ note: str | None = None
70
+
71
+
72
+ @dataclass
73
+ class EvidenceItem:
74
+ id: str
75
+ description: str
76
+ source_refs: list[SourceRef] = field(default_factory=list)
77
+ prepared_by: str | None = None
78
+ reviewed_by: str | None = None
79
+ status: EvidenceStatus = EvidenceStatus.OPEN
80
+
81
+ @property
82
+ def has_support(self) -> bool:
83
+ return bool(self.source_refs)
84
+
85
+
86
+ @dataclass
87
+ class FinanceException:
88
+ id: str
89
+ pattern_id: str
90
+ severity: Severity
91
+ amount: Decimal | None
92
+ message: str
93
+ source_refs: list[SourceRef] = field(default_factory=list)
94
+ owner: str | None = None
95
+ due_date: date | None = None
96
+ tags: set[str] = field(default_factory=set)
97
+ requires_human_review: bool = True
98
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
99
+ tolerance_used: Decimal | None = None
100
+ materiality_policy_id: str | None = None
101
+ waived: bool = False
102
+ waiver_rationale: str | None = None
103
+ waiver_approver: str | None = None
104
+ waiver_expires: date | None = None
105
+
106
+ def __post_init__(self) -> None:
107
+ if self.amount is not None:
108
+ self.amount = decimalize(self.amount)
109
+ if not isinstance(self.severity, Severity):
110
+ self.severity = Severity[self.severity]
111
+ self.tags = {str(tag).lower() for tag in self.tags}
112
+
113
+
114
+ @dataclass
115
+ class MaterialityPolicy:
116
+ """Small deterministic scoring policy for controller exception queues."""
117
+
118
+ id: str = "default"
119
+ quantitative_threshold: Decimal = Decimal("1000")
120
+ qualitative_tags: set[str] = field(default_factory=lambda: {
121
+ "fraud",
122
+ "sod",
123
+ "segregation-of-duties",
124
+ "tax",
125
+ "covenant",
126
+ "related-party",
127
+ "external-reporting",
128
+ "executive-override",
129
+ })
130
+ severity_weight: Decimal = Decimal("1000")
131
+ qualitative_weight: Decimal = Decimal("10000")
132
+ deadline_weight: Decimal = Decimal("100")
133
+ recurrence_weight: Decimal = Decimal("250")
134
+
135
+ def __post_init__(self) -> None:
136
+ self.quantitative_threshold = decimalize(self.quantitative_threshold)
137
+ self.qualitative_tags = {tag.lower() for tag in self.qualitative_tags}
138
+
139
+ def quantitative_score(self, amount: Decimal | int | float | str | None) -> Decimal:
140
+ if amount is None:
141
+ return Decimal("0")
142
+ absolute = abs(decimalize(amount))
143
+ if self.quantitative_threshold <= 0:
144
+ return absolute
145
+ return min(absolute / self.quantitative_threshold, Decimal("10")) * Decimal("100")
146
+
147
+ def qualitative_score(self, tags: Iterable[str]) -> Decimal:
148
+ normalized = {str(tag).lower() for tag in tags}
149
+ hits = normalized & self.qualitative_tags
150
+ return self.qualitative_weight * len(hits)
151
+
152
+ def score_exception(self, exception: FinanceException, as_of: date | None = None) -> Decimal:
153
+ score = Decimal(int(exception.severity)) * self.severity_weight
154
+ score += self.quantitative_score(exception.amount)
155
+ score += self.qualitative_score(exception.tags)
156
+ recurrence = _tag_int(exception.tags, "recurrence:")
157
+ score += Decimal(recurrence) * self.recurrence_weight
158
+ if exception.due_date and as_of:
159
+ days = (exception.due_date - as_of).days
160
+ if days <= 0:
161
+ score += self.deadline_weight * Decimal("10")
162
+ elif days <= 5:
163
+ score += self.deadline_weight * Decimal(6 - days)
164
+ return score
165
+
166
+
167
+ def decimalize(value: Decimal | int | float | str | None) -> Decimal:
168
+ """Convert common finance values to Decimal without binary-float surprises."""
169
+ if value is None:
170
+ return Decimal("0")
171
+ if isinstance(value, Decimal):
172
+ return value
173
+ if isinstance(value, bool):
174
+ return Decimal(int(value))
175
+ text = str(value).strip()
176
+ if not text:
177
+ return Decimal("0")
178
+ negative = text.startswith("(") and text.endswith(")")
179
+ text = text.strip("()")
180
+ text = text.replace("$", "").replace(",", "").replace("%", "")
181
+ try:
182
+ amount = Decimal(text)
183
+ except InvalidOperation as exc:
184
+ raise ValueError(f"Cannot convert to Decimal: {value!r}") from exc
185
+ return -amount if negative else amount
186
+
187
+
188
+ def within_tolerance(left: Any, right: Any, tolerance: Any = Decimal("0")) -> bool:
189
+ return abs(decimalize(left) - decimalize(right)) <= abs(decimalize(tolerance))
190
+
191
+
192
+ def stable_exception_id(pattern_id: str, key_fields: Iterable[Any]) -> str:
193
+ payload = json.dumps([pattern_id, list(key_fields)], sort_keys=True, default=str)
194
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
195
+
196
+
197
+ def hash_source_bytes(data: bytes) -> str:
198
+ return hashlib.sha256(data).hexdigest()
199
+
200
+
201
+ _VENDOR_SUFFIXES = {
202
+ "inc",
203
+ "incorporated",
204
+ "llc",
205
+ "l.l.c",
206
+ "ltd",
207
+ "co",
208
+ "company",
209
+ "corp",
210
+ "corporation",
211
+ }
212
+
213
+
214
+ def normalize_vendor_name(name: str | None) -> str:
215
+ text = (name or "").lower()
216
+ text = re.sub(r"[^a-z0-9\s]", " ", text)
217
+ words = [word for word in text.split() if word not in _VENDOR_SUFFIXES]
218
+ return " ".join(words)
219
+
220
+
221
+ def normalize_invoice_number(number: str | int | None) -> str:
222
+ text = str(number or "").upper().strip()
223
+ text = re.sub(r"[^A-Z0-9]", "", text)
224
+ # Keep a fully-zero number as "0", but remove padding for common invoice keys.
225
+ stripped = text.lstrip("0")
226
+ return stripped or ("0" if text else "")
227
+
228
+
229
+ def month_bucket(value: date | datetime | str) -> str:
230
+ if isinstance(value, datetime):
231
+ value = value.date()
232
+ elif isinstance(value, str):
233
+ value = datetime.fromisoformat(value[:10]).date()
234
+ return f"{value.year:04d}-{value.month:02d}"
235
+
236
+
237
+ def _tag_int(tags: Iterable[str], prefix: str) -> int:
238
+ for tag in tags:
239
+ if tag.startswith(prefix):
240
+ try:
241
+ return int(tag.split(":", 1)[1])
242
+ except (IndexError, ValueError):
243
+ return 0
244
+ return 0