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,254 @@
1
+ """B89/B99. Reconciliation triage and bank matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import date, datetime
7
+ from decimal import Decimal
8
+ from itertools import combinations
9
+ from typing import Any, Iterable
10
+
11
+ from .common import RiskLevel, SourceRef, decimalize, stable_exception_id, within_tolerance
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ReconciliationAccount:
16
+ account: str
17
+ balance: Decimal | int | str
18
+ last_reconciled: date | None = None
19
+ unreconciled_items: int = 0
20
+ risk_level: RiskLevel = RiskLevel.MEDIUM
21
+ owner: str | None = None
22
+ source_refs: list[SourceRef] = field(default_factory=list)
23
+
24
+ def __post_init__(self) -> None:
25
+ object.__setattr__(self, "balance", decimalize(self.balance))
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RiskScore:
30
+ account: str
31
+ score: Decimal
32
+ level: RiskLevel
33
+ reasons: list[str]
34
+ source_refs: list[SourceRef] = field(default_factory=list)
35
+
36
+
37
+ class ReconciliationTriage:
38
+ """Rank balance-sheet reconciliations by materiality, age, and risk."""
39
+
40
+ def __init__(self, materiality: Decimal | int | str = Decimal("1000"), as_of: date | None = None) -> None:
41
+ self.materiality = decimalize(materiality)
42
+ self.as_of = as_of or date.today()
43
+
44
+ def score(self, account: ReconciliationAccount) -> RiskScore:
45
+ amount_score = Decimal("0") if self.materiality == 0 else min(abs(account.balance) / self.materiality, Decimal("10")) * 100
46
+ age_score = Decimal("0")
47
+ reasons: list[str] = []
48
+ if account.last_reconciled is None:
49
+ age_score = Decimal("500")
50
+ reasons.append("never reconciled")
51
+ else:
52
+ days = (self.as_of - account.last_reconciled).days
53
+ if days > 60:
54
+ age_score = Decimal("300")
55
+ reasons.append("stale >60 days")
56
+ elif days > 30:
57
+ age_score = Decimal("150")
58
+ reasons.append("stale >30 days")
59
+ item_score = Decimal(account.unreconciled_items) * Decimal("25")
60
+ if account.unreconciled_items:
61
+ reasons.append(f"{account.unreconciled_items} unreconciled items")
62
+ risk_score = Decimal(int(account.risk_level)) * Decimal("250")
63
+ score = amount_score + age_score + item_score + risk_score
64
+ level = RiskLevel.CRITICAL if score >= 1200 else RiskLevel.HIGH if score >= 800 else RiskLevel.MEDIUM if score >= 400 else RiskLevel.LOW
65
+ return RiskScore(account=account.account, score=score, level=level, reasons=reasons, source_refs=list(account.source_refs))
66
+
67
+ def prioritize(self, accounts: Iterable[ReconciliationAccount]) -> list[ReconciliationAccount]:
68
+ return sorted(accounts, key=lambda account: (self.score(account).score, abs(account.balance), account.account), reverse=True)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class BankItem:
73
+ id: str
74
+ amount: Decimal | int | str
75
+ date: date | str
76
+ description: str = ""
77
+ reference: str | None = None
78
+ source_refs: list[SourceRef] = field(default_factory=list)
79
+
80
+ def __post_init__(self) -> None:
81
+ object.__setattr__(self, "amount", decimalize(self.amount))
82
+ if isinstance(self.date, str):
83
+ object.__setattr__(self, "date", datetime.fromisoformat(self.date[:10]).date())
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class BankMatch:
88
+ id: str
89
+ book_ids: list[str]
90
+ bank_ids: list[str]
91
+ amount_difference: Decimal
92
+ date_difference_days: int
93
+ score: Decimal
94
+ match_type: str
95
+ source_refs: list[SourceRef] = field(default_factory=list)
96
+
97
+
98
+ @dataclass
99
+ class MatchResult:
100
+ matches: list[BankMatch]
101
+ unmatched_book: list[BankItem]
102
+ unmatched_bank: list[BankItem]
103
+ tolerance_used: Decimal
104
+ date_window_days: int
105
+
106
+
107
+ class BankMatcher:
108
+ """Deterministic bank reconciliation matcher.
109
+
110
+ Uses exact/reference matches first, greedy one-to-one fuzzy matching second,
111
+ and bounded subset matching for small many-to-one receipt/disbursement groups.
112
+ """
113
+
114
+ def match(
115
+ self,
116
+ book_items: Iterable[BankItem | dict[str, Any]],
117
+ bank_items: Iterable[BankItem | dict[str, Any]],
118
+ tolerance: Decimal | int | str = Decimal("0"),
119
+ date_window_days: int = 3,
120
+ subset_size: int = 3,
121
+ ) -> MatchResult:
122
+ tolerance = decimalize(tolerance)
123
+ book = [_coerce_item(item) for item in book_items]
124
+ bank = [_coerce_item(item) for item in bank_items]
125
+ matched_book: set[str] = set()
126
+ matched_bank: set[str] = set()
127
+ matches: list[BankMatch] = []
128
+
129
+ # Pass 1: exact amount/date/reference.
130
+ for b in book:
131
+ for k in bank:
132
+ if b.id in matched_book or k.id in matched_bank:
133
+ continue
134
+ if _reference_equal(b.reference, k.reference) and b.date == k.date and within_tolerance(b.amount, k.amount, tolerance):
135
+ matches.append(_make_match([b], [k], "exact-reference"))
136
+ matched_book.add(b.id)
137
+ matched_bank.add(k.id)
138
+
139
+ # Pass 2: one-to-one fuzzy amount/date/description.
140
+ candidates: list[tuple[Decimal, BankItem, BankItem]] = []
141
+ for b in book:
142
+ if b.id in matched_book:
143
+ continue
144
+ for k in bank:
145
+ if k.id in matched_bank:
146
+ continue
147
+ days = abs((b.date - k.date).days)
148
+ if days <= date_window_days and within_tolerance(b.amount, k.amount, tolerance):
149
+ amount_diff = abs(b.amount - k.amount)
150
+ score = amount_diff + Decimal(days) / Decimal("100") - (_description_overlap(b.description, k.description) / Decimal("1000"))
151
+ candidates.append((score, b, k))
152
+ for _score, b, k in sorted(candidates, key=lambda row: (row[0], row[1].id, row[2].id)):
153
+ if b.id in matched_book or k.id in matched_bank:
154
+ continue
155
+ matches.append(_make_match([b], [k], "fuzzy-one-to-one"))
156
+ matched_book.add(b.id)
157
+ matched_bank.add(k.id)
158
+
159
+ # Pass 3: bounded many-to-one/one-to-many subset matching.
160
+ remaining_book = [b for b in book if b.id not in matched_book]
161
+ remaining_bank = [k for k in bank if k.id not in matched_bank]
162
+ subset_matches = self._subset_matches(remaining_book, remaining_bank, tolerance, date_window_days, subset_size)
163
+ for match in subset_matches:
164
+ if any(book_id in matched_book for book_id in match.book_ids) or any(bank_id in matched_bank for bank_id in match.bank_ids):
165
+ continue
166
+ matches.append(match)
167
+ matched_book.update(match.book_ids)
168
+ matched_bank.update(match.bank_ids)
169
+
170
+ return MatchResult(
171
+ matches=sorted(matches, key=lambda m: (m.match_type, m.id)),
172
+ unmatched_book=[b for b in book if b.id not in matched_book],
173
+ unmatched_bank=[k for k in bank if k.id not in matched_bank],
174
+ tolerance_used=tolerance,
175
+ date_window_days=date_window_days,
176
+ )
177
+
178
+ def _subset_matches(
179
+ self,
180
+ book: list[BankItem],
181
+ bank: list[BankItem],
182
+ tolerance: Decimal,
183
+ date_window_days: int,
184
+ subset_size: int,
185
+ ) -> list[BankMatch]:
186
+ out: list[BankMatch] = []
187
+ # many book to one bank
188
+ for size in range(2, min(subset_size, len(book)) + 1):
189
+ for group in combinations(book, size):
190
+ amount = sum((item.amount for item in group), Decimal("0"))
191
+ latest = max(item.date for item in group)
192
+ earliest = min(item.date for item in group)
193
+ for bank_item in bank:
194
+ if within_tolerance(amount, bank_item.amount, tolerance) and min(
195
+ abs((latest - bank_item.date).days), abs((earliest - bank_item.date).days)
196
+ ) <= date_window_days:
197
+ out.append(_make_match(list(group), [bank_item], "many-book-to-one-bank"))
198
+ # one book to many bank
199
+ for size in range(2, min(subset_size, len(bank)) + 1):
200
+ for group in combinations(bank, size):
201
+ amount = sum((item.amount for item in group), Decimal("0"))
202
+ latest = max(item.date for item in group)
203
+ earliest = min(item.date for item in group)
204
+ for book_item in book:
205
+ if within_tolerance(book_item.amount, amount, tolerance) and min(
206
+ abs((latest - book_item.date).days), abs((earliest - book_item.date).days)
207
+ ) <= date_window_days:
208
+ out.append(_make_match([book_item], list(group), "one-book-to-many-bank"))
209
+ return sorted(out, key=lambda m: (abs(m.amount_difference), m.date_difference_days, m.id))
210
+
211
+
212
+ def _coerce_item(item: BankItem | dict[str, Any]) -> BankItem:
213
+ if isinstance(item, BankItem):
214
+ return item
215
+ return BankItem(
216
+ id=str(item["id"]),
217
+ amount=item.get("amount", 0),
218
+ date=item.get("date"),
219
+ description=str(item.get("description", "")),
220
+ reference=item.get("reference"),
221
+ source_refs=list(item.get("source_refs", [])),
222
+ )
223
+
224
+
225
+ def _reference_equal(left: str | None, right: str | None) -> bool:
226
+ if not left or not right:
227
+ return False
228
+ return "".join(str(left).lower().split()) == "".join(str(right).lower().split())
229
+
230
+
231
+ def _description_overlap(left: str, right: str) -> Decimal:
232
+ a = {token for token in left.lower().split() if len(token) > 2}
233
+ b = {token for token in right.lower().split() if len(token) > 2}
234
+ if not a or not b:
235
+ return Decimal("0")
236
+ return Decimal(len(a & b)) / Decimal(len(a | b))
237
+
238
+
239
+ def _make_match(book: list[BankItem], bank: list[BankItem], match_type: str) -> BankMatch:
240
+ book_amount = sum((item.amount for item in book), Decimal("0"))
241
+ bank_amount = sum((item.amount for item in bank), Decimal("0"))
242
+ book_date = max(item.date for item in book)
243
+ bank_date = max(item.date for item in bank)
244
+ refs = [ref for item in [*book, *bank] for ref in item.source_refs]
245
+ return BankMatch(
246
+ id=stable_exception_id("B99", [sorted(item.id for item in book), sorted(item.id for item in bank), match_type]),
247
+ book_ids=[item.id for item in book],
248
+ bank_ids=[item.id for item in bank],
249
+ amount_difference=book_amount - bank_amount,
250
+ date_difference_days=abs((book_date - bank_date).days),
251
+ score=abs(book_amount - bank_amount) + Decimal(abs((book_date - bank_date).days)) / Decimal("100"),
252
+ match_type=match_type,
253
+ source_refs=refs,
254
+ )
@@ -0,0 +1,109 @@
1
+ """B95. ASC 606 Revenue Recognition Decision DAG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from decimal import Decimal
7
+ from typing import Any
8
+
9
+ from .common import ReviewGate, SourceRef, decimalize, stable_exception_id
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PerformanceObligation:
14
+ id: str
15
+ description: str
16
+ transaction_price: Decimal | int | str
17
+ satisfied: bool = False
18
+ measure: str | None = None
19
+ source_refs: list[SourceRef] = field(default_factory=list)
20
+
21
+ def __post_init__(self) -> None:
22
+ object.__setattr__(self, "transaction_price", decimalize(self.transaction_price))
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RevenueContract:
27
+ id: str
28
+ customer: str
29
+ approved: bool
30
+ rights_identified: bool
31
+ payment_terms_identified: bool
32
+ commercial_substance: bool
33
+ collectability_probable: bool
34
+ obligations: list[PerformanceObligation]
35
+ source_refs: list[SourceRef] = field(default_factory=list)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RevenueDecision:
40
+ contract_id: str
41
+ recognized_amount: Decimal
42
+ review_required: bool
43
+ reasons: list[str]
44
+ review_gate: ReviewGate
45
+ source_refs: list[SourceRef]
46
+
47
+
48
+ class ASC606DecisionDAG:
49
+ """Five-step ASC 606 decision support, not accounting sign-off."""
50
+
51
+ def evaluate(self, contract: RevenueContract | dict[str, Any]) -> RevenueDecision:
52
+ c = _coerce_contract(contract)
53
+ reasons: list[str] = []
54
+ criteria = {
55
+ "contract not approved": c.approved,
56
+ "rights not identified": c.rights_identified,
57
+ "payment terms not identified": c.payment_terms_identified,
58
+ "commercial substance missing": c.commercial_substance,
59
+ "collectability not probable": c.collectability_probable,
60
+ }
61
+ for reason, ok in criteria.items():
62
+ if not ok:
63
+ reasons.append(reason)
64
+ if not c.obligations:
65
+ reasons.append("missing performance obligations")
66
+ source_refs = list(c.source_refs)
67
+ for obligation in c.obligations:
68
+ source_refs.extend(obligation.source_refs)
69
+ if not obligation.source_refs:
70
+ reasons.append(f"performance obligation {obligation.id} lacks source evidence")
71
+ recognized = sum((ob.transaction_price for ob in c.obligations if ob.satisfied and ob.source_refs), Decimal("0"))
72
+ review_required = bool(reasons)
73
+ return RevenueDecision(
74
+ contract_id=c.id,
75
+ recognized_amount=recognized if not reasons else Decimal("0") if any("contract" in r or "collectability" in r for r in reasons) else recognized,
76
+ review_required=review_required,
77
+ reasons=reasons,
78
+ review_gate=ReviewGate.CPA_REVIEW if review_required else ReviewGate.CONTROLLER_REVIEW,
79
+ source_refs=source_refs,
80
+ )
81
+
82
+
83
+ def _coerce_contract(contract: RevenueContract | dict[str, Any]) -> RevenueContract:
84
+ if isinstance(contract, RevenueContract):
85
+ return contract
86
+ obligations = []
87
+ for row in contract.get("obligations", []):
88
+ if isinstance(row, PerformanceObligation):
89
+ obligations.append(row)
90
+ else:
91
+ obligations.append(PerformanceObligation(
92
+ id=str(row.get("id") or stable_exception_id("B95", [row.get("description"), row.get("transaction_price")])) ,
93
+ description=str(row.get("description", "")),
94
+ transaction_price=row.get("transaction_price", 0),
95
+ satisfied=bool(row.get("satisfied", False)),
96
+ measure=row.get("measure"),
97
+ source_refs=list(row.get("source_refs", [])),
98
+ ))
99
+ return RevenueContract(
100
+ id=str(contract.get("id", "")),
101
+ customer=str(contract.get("customer", "")),
102
+ approved=bool(contract.get("approved", False)),
103
+ rights_identified=bool(contract.get("rights_identified", False)),
104
+ payment_terms_identified=bool(contract.get("payment_terms_identified", False)),
105
+ commercial_substance=bool(contract.get("commercial_substance", False)),
106
+ collectability_probable=bool(contract.get("collectability_probable", False)),
107
+ obligations=obligations,
108
+ source_refs=list(contract.get("source_refs", [])),
109
+ )
@@ -0,0 +1,74 @@
1
+ """B102. Sales/Use Tax and Invoice Taxability Classifier."""
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
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class TaxPolicy:
14
+ id: str
15
+ taxable_categories: set[str]
16
+ rate: Decimal | int | str
17
+ exempt_categories: set[str] = field(default_factory=set)
18
+
19
+ def __post_init__(self) -> None:
20
+ object.__setattr__(self, "rate", decimalize(self.rate))
21
+ object.__setattr__(self, "taxable_categories", {cat.lower() for cat in self.taxable_categories})
22
+ object.__setattr__(self, "exempt_categories", {cat.lower() for cat in self.exempt_categories})
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class TaxLineResult:
27
+ line_id: str
28
+ category: str
29
+ amount: Decimal
30
+ taxable: bool
31
+ tax: Decimal
32
+ reason: str
33
+ source_refs: list[SourceRef]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class TaxClassification:
38
+ policy_id: str
39
+ taxable_amount: Decimal
40
+ tax_amount: Decimal
41
+ line_results: list[TaxLineResult]
42
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
43
+
44
+
45
+ class SalesUseTaxClassifier:
46
+ def classify_invoice(self, lines: Iterable[dict[str, Any]], jurisdiction_policy: TaxPolicy | dict[str, Any]) -> TaxClassification:
47
+ policy = _coerce_policy(jurisdiction_policy)
48
+ results: list[TaxLineResult] = []
49
+ for idx, line in enumerate(lines, start=1):
50
+ category = str(line.get("category", "")).lower()
51
+ amount = decimalize(line.get("amount", 0))
52
+ line_id = str(line.get("id") or stable_exception_id("B102", [idx, category, amount]))
53
+ if category in policy.exempt_categories:
54
+ taxable = False
55
+ reason = "category explicitly exempt"
56
+ else:
57
+ taxable = category in policy.taxable_categories
58
+ reason = "category taxable" if taxable else "category not taxable under policy"
59
+ tax = amount * policy.rate if taxable else Decimal("0")
60
+ results.append(TaxLineResult(line_id, category, amount, taxable, tax, reason, list(line.get("source_refs", []))))
61
+ taxable_amount = sum((row.amount for row in results if row.taxable), Decimal("0"))
62
+ tax_amount = sum((row.tax for row in results), Decimal("0"))
63
+ return TaxClassification(policy.id, taxable_amount, tax_amount, results)
64
+
65
+
66
+ def _coerce_policy(policy: TaxPolicy | dict[str, Any]) -> TaxPolicy:
67
+ if isinstance(policy, TaxPolicy):
68
+ return policy
69
+ return TaxPolicy(
70
+ id=str(policy.get("id", "policy")),
71
+ taxable_categories=set(policy.get("taxable_categories", [])),
72
+ exempt_categories=set(policy.get("exempt_categories", [])),
73
+ rate=policy.get("rate", 0),
74
+ )
@@ -0,0 +1,111 @@
1
+ """B104. CPA Workpaper Tie-Out and Crossfoot Rules."""
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, within_tolerance
10
+
11
+
12
+ @dataclass
13
+ class TieOutResult:
14
+ clean: bool
15
+ check_type: str
16
+ expected: Decimal
17
+ actual: Decimal
18
+ difference: Decimal
19
+ tolerance_used: Decimal
20
+ message: str
21
+ source_refs: list[SourceRef] = field(default_factory=list)
22
+ review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
23
+
24
+
25
+ class WorkpaperTieOut:
26
+ """Foot, crossfoot, trace, and sign-convention checks for schedules."""
27
+
28
+ def foot(
29
+ self,
30
+ table: Iterable[dict[str, Any]],
31
+ columns: Iterable[str],
32
+ expected_totals: dict[str, Any] | None = None,
33
+ tolerance: Any = Decimal("0"),
34
+ ) -> dict[str, TieOutResult]:
35
+ rows = list(table)
36
+ results: dict[str, TieOutResult] = {}
37
+ for column in columns:
38
+ actual = sum((decimalize(row.get(column, 0)) for row in rows), Decimal("0"))
39
+ expected = decimalize((expected_totals or {}).get(column, actual))
40
+ difference = actual - expected
41
+ clean = within_tolerance(actual, expected, tolerance)
42
+ results[column] = TieOutResult(
43
+ clean=clean,
44
+ check_type="foot",
45
+ expected=expected,
46
+ actual=actual,
47
+ difference=difference,
48
+ tolerance_used=decimalize(tolerance),
49
+ message="Foots" if clean else f"Column {column} does not foot",
50
+ )
51
+ return results
52
+
53
+ def crossfoot(
54
+ self,
55
+ table: Iterable[dict[str, Any]],
56
+ row_total_col: str,
57
+ component_cols: Iterable[str],
58
+ tolerance: Any = Decimal("0"),
59
+ ) -> list[TieOutResult]:
60
+ results: list[TieOutResult] = []
61
+ comps = list(component_cols)
62
+ for index, row in enumerate(table, start=1):
63
+ expected = sum((decimalize(row.get(col, 0)) for col in comps), Decimal("0"))
64
+ actual = decimalize(row.get(row_total_col, 0))
65
+ clean = within_tolerance(actual, expected, tolerance)
66
+ results.append(TieOutResult(
67
+ clean=clean,
68
+ check_type="crossfoot",
69
+ expected=expected,
70
+ actual=actual,
71
+ difference=actual - expected,
72
+ tolerance_used=decimalize(tolerance),
73
+ message="Crossfoots" if clean else f"Row {index} does not crossfoot",
74
+ ))
75
+ return results
76
+
77
+ def trace_to_source(
78
+ self,
79
+ schedule_total: Any,
80
+ source_total: Any,
81
+ tolerance: Any = Decimal("0"),
82
+ source_refs: Iterable[SourceRef] = (),
83
+ ) -> TieOutResult:
84
+ actual = decimalize(schedule_total)
85
+ expected = decimalize(source_total)
86
+ clean = within_tolerance(actual, expected, tolerance)
87
+ return TieOutResult(
88
+ clean=clean,
89
+ check_type="trace",
90
+ expected=expected,
91
+ actual=actual,
92
+ difference=actual - expected,
93
+ tolerance_used=decimalize(tolerance),
94
+ message="Ties to source" if clean else "Schedule total does not tie to source",
95
+ source_refs=list(source_refs),
96
+ )
97
+
98
+ def sign_convention_check(self, amount: Any, expected_sign: str, mapped: bool = False) -> TieOutResult:
99
+ actual = decimalize(amount)
100
+ expected_positive = expected_sign.lower() in {"positive", "+", "debit"}
101
+ sign_ok = actual >= 0 if expected_positive else actual <= 0
102
+ clean = sign_ok or mapped
103
+ return TieOutResult(
104
+ clean=clean,
105
+ check_type="sign_convention",
106
+ expected=Decimal("1") if expected_positive else Decimal("-1"),
107
+ actual=Decimal("1") if actual >= 0 else Decimal("-1"),
108
+ difference=Decimal("0") if clean else actual,
109
+ tolerance_used=Decimal("0"),
110
+ message="Sign convention explicit" if clean else "Sign flip requires explicit mapping",
111
+ )
@@ -0,0 +1,120 @@
1
+ """H1 — Algorithm Finding Record.
2
+
3
+ Structured append-only findings with provenance, mined from T3MP3ST Evidence Vault §8.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+
13
+ class FindingStatus(str, Enum):
14
+ PROPOSED = "proposed"
15
+ VALIDATED = "validated"
16
+ RETRACTED = "retracted"
17
+
18
+
19
+ @dataclass
20
+ class Finding:
21
+ """A single algorithm finding with provenance."""
22
+
23
+ id: str
24
+ title: str
25
+ description: str
26
+ source_repo: str
27
+ source_section: str
28
+ status: FindingStatus = FindingStatus.PROPOSED
29
+ created_at: float = field(default_factory=time.time)
30
+ provenance: dict[str, Any] = field(default_factory=dict)
31
+ evidence: list[str] = field(default_factory=list)
32
+
33
+ def to_dict(self) -> dict[str, Any]:
34
+ return {
35
+ "id": self.id,
36
+ "title": self.title,
37
+ "description": self.description,
38
+ "source_repo": self.source_repo,
39
+ "source_section": self.source_section,
40
+ "status": self.status.value,
41
+ "created_at": self.created_at,
42
+ "provenance": dict(self.provenance),
43
+ "evidence": list(self.evidence),
44
+ }
45
+
46
+ @classmethod
47
+ def from_dict(cls, data: dict[str, Any]) -> Finding:
48
+ return cls(
49
+ id=data["id"],
50
+ title=data["title"],
51
+ description=data["description"],
52
+ source_repo=data["source_repo"],
53
+ source_section=data["source_section"],
54
+ status=FindingStatus(data.get("status", "proposed")),
55
+ created_at=data.get("created_at", time.time()),
56
+ provenance=dict(data.get("provenance", {})),
57
+ evidence=list(data.get("evidence", [])),
58
+ )
59
+
60
+
61
+ class FindingRecord:
62
+ """Append-only store for algorithm findings."""
63
+
64
+ def __init__(self) -> None:
65
+ self._findings: list[Finding] = []
66
+
67
+ def create(
68
+ self,
69
+ id: str,
70
+ title: str,
71
+ description: str,
72
+ source_repo: str,
73
+ source_section: str,
74
+ provenance: dict[str, Any] | None = None,
75
+ evidence: list[str] | None = None,
76
+ ) -> Finding:
77
+ if self.get(id) is not None:
78
+ raise ValueError(f"Finding {id!r} already exists — append-only store")
79
+ finding = Finding(
80
+ id=id,
81
+ title=title,
82
+ description=description,
83
+ source_repo=source_repo,
84
+ source_section=source_section,
85
+ provenance=provenance or {},
86
+ evidence=evidence or [],
87
+ )
88
+ self._findings.append(finding)
89
+ return finding
90
+
91
+ def get(self, id: str) -> Finding | None:
92
+ for f in self._findings:
93
+ if f.id == id:
94
+ return f
95
+ return None
96
+
97
+ def query(
98
+ self,
99
+ status: FindingStatus | None = None,
100
+ source_repo: str | None = None,
101
+ ) -> list[Finding]:
102
+ results = self._findings
103
+ if status is not None:
104
+ results = [f for f in results if f.status == status]
105
+ if source_repo is not None:
106
+ results = [f for f in results if f.source_repo == source_repo]
107
+ return list(results)
108
+
109
+ def update_status(self, id: str, status: FindingStatus) -> Finding:
110
+ f = self.get(id)
111
+ if f is None:
112
+ raise KeyError(f"Finding {id!r} not found")
113
+ f.status = status
114
+ return f
115
+
116
+ def all(self) -> list[Finding]:
117
+ return list(self._findings)
118
+
119
+ def count(self) -> int:
120
+ return len(self._findings)