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