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,178 @@
|
|
|
1
|
+
"""H20 — Falsification Suite.
|
|
2
|
+
|
|
3
|
+
Multiple independent attack vectors on a claim.
|
|
4
|
+
Mined from GLOSSOPETRAE experiments/falsify/ (S1-S4 probes).
|
|
5
|
+
|
|
6
|
+
Each probe attacks the claim from a different angle. If any probe
|
|
7
|
+
succeeds in falsifying the claim, the claim is rejected.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FalsificationProbe:
|
|
17
|
+
"""A single falsification probe."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
attack_fn: Callable[[dict[str, Any]], bool] # Returns True if falsified
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ProbeResult:
|
|
26
|
+
"""Result of a single probe."""
|
|
27
|
+
|
|
28
|
+
probe_name: str
|
|
29
|
+
falsified: bool
|
|
30
|
+
evidence: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FalsificationResult:
|
|
35
|
+
"""Combined result of all probes."""
|
|
36
|
+
|
|
37
|
+
entry_id: str
|
|
38
|
+
probe_results: list[ProbeResult] = field(default_factory=list)
|
|
39
|
+
any_falsified: bool = False
|
|
40
|
+
falsified_by: list[str] = field(default_factory=list)
|
|
41
|
+
survived_count: int = 0
|
|
42
|
+
total_count: int = 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Built-in probes
|
|
46
|
+
def probe_missing_evidence(entry_id: str, claims: dict[str, Any]) -> ProbeResult:
|
|
47
|
+
"""S1: Check if claims lack evidence."""
|
|
48
|
+
falsified = False
|
|
49
|
+
evidence_parts: list[str] = []
|
|
50
|
+
|
|
51
|
+
for key, value in claims.items():
|
|
52
|
+
if value is None:
|
|
53
|
+
falsified = True
|
|
54
|
+
evidence_parts.append(f"{key} is None")
|
|
55
|
+
elif isinstance(value, (list, dict)) and len(value) == 0:
|
|
56
|
+
falsified = True
|
|
57
|
+
evidence_parts.append(f"{key} is empty")
|
|
58
|
+
|
|
59
|
+
return ProbeResult(
|
|
60
|
+
probe_name="missing-evidence",
|
|
61
|
+
falsified=falsified,
|
|
62
|
+
evidence="; ".join(evidence_parts) if evidence_parts else "All claims have values",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def probe_contradictory_claims(entry_id: str, claims: dict[str, Any]) -> ProbeResult:
|
|
67
|
+
"""S2: Check for internal contradictions."""
|
|
68
|
+
falsified = False
|
|
69
|
+
evidence_parts: list[str] = []
|
|
70
|
+
|
|
71
|
+
# Check status vs tests
|
|
72
|
+
status = claims.get("status", "")
|
|
73
|
+
tests = claims.get("tests", [])
|
|
74
|
+
if status == "implemented" and not tests:
|
|
75
|
+
falsified = True
|
|
76
|
+
evidence_parts.append("Status 'implemented' but no tests")
|
|
77
|
+
|
|
78
|
+
# Check score vs status
|
|
79
|
+
score = claims.get("score", 0.0)
|
|
80
|
+
if status == "implemented" and isinstance(score, (int, float)) and score < 0.5:
|
|
81
|
+
falsified = True
|
|
82
|
+
evidence_parts.append(f"Status 'implemented' but score={score}")
|
|
83
|
+
|
|
84
|
+
return ProbeResult(
|
|
85
|
+
probe_name="contradictory-claims",
|
|
86
|
+
falsified=falsified,
|
|
87
|
+
evidence="; ".join(evidence_parts) if evidence_parts else "No contradictions found",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def probe_boundary_values(entry_id: str, claims: dict[str, Any]) -> ProbeResult:
|
|
92
|
+
"""S3: Check for impossible boundary values."""
|
|
93
|
+
falsified = False
|
|
94
|
+
evidence_parts: list[str] = []
|
|
95
|
+
|
|
96
|
+
for key, value in claims.items():
|
|
97
|
+
if isinstance(value, (int, float)):
|
|
98
|
+
if value < 0.0 and "count" in key.lower():
|
|
99
|
+
falsified = True
|
|
100
|
+
evidence_parts.append(f"{key}={value} is negative")
|
|
101
|
+
if value > 1.0 and "rate" in key.lower():
|
|
102
|
+
falsified = True
|
|
103
|
+
evidence_parts.append(f"{key}={value} exceeds 1.0")
|
|
104
|
+
|
|
105
|
+
return ProbeResult(
|
|
106
|
+
probe_name="boundary-values",
|
|
107
|
+
falsified=falsified,
|
|
108
|
+
evidence="; ".join(evidence_parts) if evidence_parts else "All values in bounds",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def probe_unverifiable_sources(entry_id: str, claims: dict[str, Any]) -> ProbeResult:
|
|
113
|
+
"""S4: Check if sources are unverifiable."""
|
|
114
|
+
falsified = False
|
|
115
|
+
evidence_parts: list[str] = []
|
|
116
|
+
|
|
117
|
+
source = claims.get("source", "")
|
|
118
|
+
if source is None:
|
|
119
|
+
falsified = True
|
|
120
|
+
evidence_parts.append("Source is None")
|
|
121
|
+
elif not isinstance(source, str):
|
|
122
|
+
falsified = True
|
|
123
|
+
evidence_parts.append("Source is not a string")
|
|
124
|
+
elif not source.strip():
|
|
125
|
+
falsified = True
|
|
126
|
+
evidence_parts.append("Source is empty string")
|
|
127
|
+
|
|
128
|
+
return ProbeResult(
|
|
129
|
+
probe_name="unverifiable-sources",
|
|
130
|
+
falsified=falsified,
|
|
131
|
+
evidence="; ".join(evidence_parts) if evidence_parts else "Sources verifiable",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Default probe set
|
|
136
|
+
DEFAULT_PROBES: list[tuple[str, Callable[[str, dict], ProbeResult]]] = [
|
|
137
|
+
("missing-evidence", probe_missing_evidence),
|
|
138
|
+
("contradictory-claims", probe_contradictory_claims),
|
|
139
|
+
("boundary-values", probe_boundary_values),
|
|
140
|
+
("unverifiable-sources", probe_unverifiable_sources),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run_falsification(
|
|
145
|
+
entry_id: str,
|
|
146
|
+
claims: dict[str, Any],
|
|
147
|
+
probes: list[tuple[str, Callable[[str, dict], ProbeResult]]] | None = None,
|
|
148
|
+
model_client: Any | None = None,
|
|
149
|
+
) -> FalsificationResult:
|
|
150
|
+
"""Run a falsification suite against a claim set.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
entry_id: The entry being tested.
|
|
154
|
+
claims: The claims to falsify.
|
|
155
|
+
probes: List of (name, probe_fn) tuples. Defaults to built-in probes.
|
|
156
|
+
model_client: Optional LLM for deeper probes.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
FalsificationResult with all probe results.
|
|
160
|
+
"""
|
|
161
|
+
probe_list = probes or DEFAULT_PROBES
|
|
162
|
+
results: list[ProbeResult] = []
|
|
163
|
+
|
|
164
|
+
for name, fn in probe_list:
|
|
165
|
+
result = fn(entry_id, claims)
|
|
166
|
+
results.append(result)
|
|
167
|
+
|
|
168
|
+
falsified_by = [r.probe_name for r in results if r.falsified]
|
|
169
|
+
survived = sum(1 for r in results if not r.falsified)
|
|
170
|
+
|
|
171
|
+
return FalsificationResult(
|
|
172
|
+
entry_id=entry_id,
|
|
173
|
+
probe_results=results,
|
|
174
|
+
any_falsified=len(falsified_by) > 0,
|
|
175
|
+
falsified_by=falsified_by,
|
|
176
|
+
survived_count=survived,
|
|
177
|
+
total_count=len(results),
|
|
178
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Finance / CPA / Controller intelligence patterns B87-B105."""
|
|
2
|
+
|
|
3
|
+
from .anomalies import JournalEntry, JournalEntryAnomalyScorer, JournalEntryScore
|
|
4
|
+
from .ap_ar import (
|
|
5
|
+
APDuplicateDetector,
|
|
6
|
+
APInvoice,
|
|
7
|
+
APPayment,
|
|
8
|
+
ARAgingPrioritizer,
|
|
9
|
+
ARInvoice,
|
|
10
|
+
AgingBucket,
|
|
11
|
+
AgingReport,
|
|
12
|
+
CollectionAction,
|
|
13
|
+
DuplicateCandidate,
|
|
14
|
+
SoDConflict,
|
|
15
|
+
ThreeWayMatchResult,
|
|
16
|
+
Vendor,
|
|
17
|
+
VendorChangeEvent,
|
|
18
|
+
VendorRisk,
|
|
19
|
+
VendorRiskAnalyzer,
|
|
20
|
+
)
|
|
21
|
+
from .cash import CashFlowItem, CashForecast, CashForecastWeek, ForecastVariance, ForecastVarianceReport, ThirteenWeekCashForecast
|
|
22
|
+
from .close import (
|
|
23
|
+
AccrualEngine,
|
|
24
|
+
AccrualPolicy,
|
|
25
|
+
AccrualProposal,
|
|
26
|
+
BlockedTask,
|
|
27
|
+
CloseDAG,
|
|
28
|
+
ClosePackageAnalyzer,
|
|
29
|
+
ClosePackageInputs,
|
|
30
|
+
ClosePackageResult,
|
|
31
|
+
CloseTask,
|
|
32
|
+
ReversalProposal,
|
|
33
|
+
Variance,
|
|
34
|
+
VarianceExplanation,
|
|
35
|
+
VarianceExplanationMiner,
|
|
36
|
+
)
|
|
37
|
+
from .common import (
|
|
38
|
+
EvidenceItem,
|
|
39
|
+
EvidenceStatus,
|
|
40
|
+
FinanceException,
|
|
41
|
+
MaterialityPolicy,
|
|
42
|
+
Money,
|
|
43
|
+
ReviewGate,
|
|
44
|
+
RiskLevel,
|
|
45
|
+
Severity,
|
|
46
|
+
SourceRef,
|
|
47
|
+
decimalize,
|
|
48
|
+
hash_source_bytes,
|
|
49
|
+
month_bucket,
|
|
50
|
+
normalize_invoice_number,
|
|
51
|
+
normalize_vendor_name,
|
|
52
|
+
stable_exception_id,
|
|
53
|
+
within_tolerance,
|
|
54
|
+
)
|
|
55
|
+
from .construction import (
|
|
56
|
+
ConstructionWIPController,
|
|
57
|
+
FixedAssetRollForward,
|
|
58
|
+
JobCost,
|
|
59
|
+
PayrollReconResult,
|
|
60
|
+
PayrollReconciler,
|
|
61
|
+
RollForwardResult,
|
|
62
|
+
WIPException,
|
|
63
|
+
WIPReport,
|
|
64
|
+
WIPRow,
|
|
65
|
+
)
|
|
66
|
+
from .controls import Control, ControlGap, FinanceProcess, RCMRow, Risk, RiskControlMatrix, RiskControlMatrixCompiler
|
|
67
|
+
from .evidence import EvidenceBinder, EvidenceReport, EvidenceRequest
|
|
68
|
+
from .exceptions import ControllerExceptionQueue, ExceptionCluster, QueueSummary, WaiverResult, make_exception
|
|
69
|
+
from .reconciliations import BankItem, BankMatch, BankMatcher, MatchResult, ReconciliationAccount, ReconciliationTriage, RiskScore
|
|
70
|
+
from .revenue import ASC606DecisionDAG, PerformanceObligation, RevenueContract, RevenueDecision
|
|
71
|
+
from .tax import SalesUseTaxClassifier, TaxClassification, TaxLineResult, TaxPolicy
|
|
72
|
+
from .workpapers import TieOutResult, WorkpaperTieOut
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"JournalEntry",
|
|
76
|
+
"JournalEntryAnomalyScorer",
|
|
77
|
+
"JournalEntryScore",
|
|
78
|
+
"APDuplicateDetector",
|
|
79
|
+
"APInvoice",
|
|
80
|
+
"APPayment",
|
|
81
|
+
"ARAgingPrioritizer",
|
|
82
|
+
"ARInvoice",
|
|
83
|
+
"AgingBucket",
|
|
84
|
+
"AgingReport",
|
|
85
|
+
"CollectionAction",
|
|
86
|
+
"DuplicateCandidate",
|
|
87
|
+
"SoDConflict",
|
|
88
|
+
"ThreeWayMatchResult",
|
|
89
|
+
"Vendor",
|
|
90
|
+
"VendorChangeEvent",
|
|
91
|
+
"VendorRisk",
|
|
92
|
+
"VendorRiskAnalyzer",
|
|
93
|
+
"CashFlowItem",
|
|
94
|
+
"CashForecast",
|
|
95
|
+
"CashForecastWeek",
|
|
96
|
+
"ForecastVariance",
|
|
97
|
+
"ForecastVarianceReport",
|
|
98
|
+
"ThirteenWeekCashForecast",
|
|
99
|
+
"AccrualEngine",
|
|
100
|
+
"AccrualPolicy",
|
|
101
|
+
"AccrualProposal",
|
|
102
|
+
"BlockedTask",
|
|
103
|
+
"CloseDAG",
|
|
104
|
+
"ClosePackageAnalyzer",
|
|
105
|
+
"ClosePackageInputs",
|
|
106
|
+
"ClosePackageResult",
|
|
107
|
+
"CloseTask",
|
|
108
|
+
"ReversalProposal",
|
|
109
|
+
"Variance",
|
|
110
|
+
"VarianceExplanation",
|
|
111
|
+
"VarianceExplanationMiner",
|
|
112
|
+
"EvidenceItem",
|
|
113
|
+
"EvidenceStatus",
|
|
114
|
+
"FinanceException",
|
|
115
|
+
"MaterialityPolicy",
|
|
116
|
+
"Money",
|
|
117
|
+
"ReviewGate",
|
|
118
|
+
"RiskLevel",
|
|
119
|
+
"Severity",
|
|
120
|
+
"SourceRef",
|
|
121
|
+
"decimalize",
|
|
122
|
+
"hash_source_bytes",
|
|
123
|
+
"month_bucket",
|
|
124
|
+
"normalize_invoice_number",
|
|
125
|
+
"normalize_vendor_name",
|
|
126
|
+
"stable_exception_id",
|
|
127
|
+
"within_tolerance",
|
|
128
|
+
"ConstructionWIPController",
|
|
129
|
+
"FixedAssetRollForward",
|
|
130
|
+
"JobCost",
|
|
131
|
+
"PayrollReconResult",
|
|
132
|
+
"PayrollReconciler",
|
|
133
|
+
"RollForwardResult",
|
|
134
|
+
"WIPException",
|
|
135
|
+
"WIPReport",
|
|
136
|
+
"WIPRow",
|
|
137
|
+
"Control",
|
|
138
|
+
"ControlGap",
|
|
139
|
+
"FinanceProcess",
|
|
140
|
+
"RCMRow",
|
|
141
|
+
"Risk",
|
|
142
|
+
"RiskControlMatrix",
|
|
143
|
+
"RiskControlMatrixCompiler",
|
|
144
|
+
"EvidenceBinder",
|
|
145
|
+
"EvidenceReport",
|
|
146
|
+
"EvidenceRequest",
|
|
147
|
+
"ControllerExceptionQueue",
|
|
148
|
+
"ExceptionCluster",
|
|
149
|
+
"QueueSummary",
|
|
150
|
+
"WaiverResult",
|
|
151
|
+
"make_exception",
|
|
152
|
+
"BankItem",
|
|
153
|
+
"BankMatch",
|
|
154
|
+
"BankMatcher",
|
|
155
|
+
"MatchResult",
|
|
156
|
+
"ReconciliationAccount",
|
|
157
|
+
"ReconciliationTriage",
|
|
158
|
+
"RiskScore",
|
|
159
|
+
"ASC606DecisionDAG",
|
|
160
|
+
"PerformanceObligation",
|
|
161
|
+
"RevenueContract",
|
|
162
|
+
"RevenueDecision",
|
|
163
|
+
"SalesUseTaxClassifier",
|
|
164
|
+
"TaxClassification",
|
|
165
|
+
"TaxLineResult",
|
|
166
|
+
"TaxPolicy",
|
|
167
|
+
"TieOutResult",
|
|
168
|
+
"WorkpaperTieOut",
|
|
169
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""B90. Journal Entry Anomaly Scoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import date, datetime, time
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from statistics import median
|
|
9
|
+
from typing import Any, Iterable
|
|
10
|
+
|
|
11
|
+
from .common import ReviewGate, RiskLevel, SourceRef, decimalize
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class JournalEntry:
|
|
16
|
+
id: str
|
|
17
|
+
account: str
|
|
18
|
+
amount: Decimal | int | str
|
|
19
|
+
entry_date: date | str
|
|
20
|
+
user: str
|
|
21
|
+
description: str = ""
|
|
22
|
+
manual: bool = True
|
|
23
|
+
approved_by: str | None = None
|
|
24
|
+
posted_at: datetime | str | None = None
|
|
25
|
+
source_refs: list[SourceRef] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
object.__setattr__(self, "amount", decimalize(self.amount))
|
|
29
|
+
if isinstance(self.entry_date, str):
|
|
30
|
+
object.__setattr__(self, "entry_date", datetime.fromisoformat(self.entry_date[:10]).date())
|
|
31
|
+
if isinstance(self.posted_at, str):
|
|
32
|
+
object.__setattr__(self, "posted_at", datetime.fromisoformat(self.posted_at))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class JournalEntryScore:
|
|
37
|
+
entry: JournalEntry
|
|
38
|
+
score: Decimal
|
|
39
|
+
level: RiskLevel
|
|
40
|
+
reasons: list[str]
|
|
41
|
+
source_refs: list[SourceRef]
|
|
42
|
+
review_gate: ReviewGate = ReviewGate.CONTROLLER_REVIEW
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JournalEntryAnomalyScorer:
|
|
46
|
+
"""Weighted JE risk scoring for controller review, not auto-rejection."""
|
|
47
|
+
|
|
48
|
+
def score(
|
|
49
|
+
self,
|
|
50
|
+
entries: Iterable[JournalEntry | dict[str, Any]],
|
|
51
|
+
history: Iterable[JournalEntry | dict[str, Any]] | None = None,
|
|
52
|
+
) -> list[JournalEntryScore]:
|
|
53
|
+
rows = [_coerce_entry(entry) for entry in entries]
|
|
54
|
+
hist = [_coerce_entry(entry) for entry in (history or [])]
|
|
55
|
+
account_medians = _median_amount_by_account(hist)
|
|
56
|
+
user_accounts = _accounts_by_user(hist)
|
|
57
|
+
scored = [self._score_one(entry, account_medians, user_accounts) for entry in rows]
|
|
58
|
+
return sorted(scored, key=lambda row: (row.score, abs(row.entry.amount), row.entry.id), reverse=True)
|
|
59
|
+
|
|
60
|
+
def _score_one(
|
|
61
|
+
self,
|
|
62
|
+
entry: JournalEntry,
|
|
63
|
+
account_medians: dict[str, Decimal],
|
|
64
|
+
user_accounts: dict[str, set[str]],
|
|
65
|
+
) -> JournalEntryScore:
|
|
66
|
+
score = Decimal("0")
|
|
67
|
+
reasons: list[str] = []
|
|
68
|
+
amount = abs(entry.amount)
|
|
69
|
+
if entry.manual:
|
|
70
|
+
score += Decimal("100")
|
|
71
|
+
reasons.append("manual entry")
|
|
72
|
+
if not entry.approved_by:
|
|
73
|
+
score += Decimal("300")
|
|
74
|
+
reasons.append("missing approval")
|
|
75
|
+
if _is_round_amount(amount):
|
|
76
|
+
score += Decimal("125")
|
|
77
|
+
reasons.append("round-dollar amount")
|
|
78
|
+
if entry.posted_at and _outside_business_hours(entry.posted_at):
|
|
79
|
+
score += Decimal("175")
|
|
80
|
+
reasons.append("posted outside business hours")
|
|
81
|
+
if entry.entry_date.day >= 28:
|
|
82
|
+
score += Decimal("100")
|
|
83
|
+
reasons.append("period-end posting")
|
|
84
|
+
if not entry.description.strip():
|
|
85
|
+
score += Decimal("150")
|
|
86
|
+
reasons.append("blank description")
|
|
87
|
+
median_amount = account_medians.get(entry.account)
|
|
88
|
+
if median_amount and median_amount > 0 and amount > median_amount * Decimal("3"):
|
|
89
|
+
score += Decimal("250")
|
|
90
|
+
reasons.append("amount exceeds 3x historical median")
|
|
91
|
+
known_accounts = user_accounts.get(entry.user, set())
|
|
92
|
+
if known_accounts and entry.account not in known_accounts:
|
|
93
|
+
score += Decimal("200")
|
|
94
|
+
reasons.append("user unusual for account")
|
|
95
|
+
level = RiskLevel.CRITICAL if score >= 900 else RiskLevel.HIGH if score >= 600 else RiskLevel.MEDIUM if score >= 250 else RiskLevel.LOW
|
|
96
|
+
return JournalEntryScore(entry=entry, score=score, level=level, reasons=reasons, source_refs=list(entry.source_refs))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _coerce_entry(entry: JournalEntry | dict[str, Any]) -> JournalEntry:
|
|
100
|
+
if isinstance(entry, JournalEntry):
|
|
101
|
+
return entry
|
|
102
|
+
return JournalEntry(
|
|
103
|
+
id=str(entry["id"]),
|
|
104
|
+
account=str(entry.get("account", "")),
|
|
105
|
+
amount=entry.get("amount", 0),
|
|
106
|
+
entry_date=entry.get("entry_date") or entry.get("date"),
|
|
107
|
+
user=str(entry.get("user", "")),
|
|
108
|
+
description=str(entry.get("description", "")),
|
|
109
|
+
manual=bool(entry.get("manual", True)),
|
|
110
|
+
approved_by=entry.get("approved_by"),
|
|
111
|
+
posted_at=entry.get("posted_at"),
|
|
112
|
+
source_refs=list(entry.get("source_refs", [])),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _median_amount_by_account(entries: list[JournalEntry]) -> dict[str, Decimal]:
|
|
117
|
+
grouped: dict[str, list[Decimal]] = {}
|
|
118
|
+
for entry in entries:
|
|
119
|
+
grouped.setdefault(entry.account, []).append(abs(entry.amount))
|
|
120
|
+
return {account: decimalize(median(values)) for account, values in grouped.items() if values}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _accounts_by_user(entries: list[JournalEntry]) -> dict[str, set[str]]:
|
|
124
|
+
out: dict[str, set[str]] = {}
|
|
125
|
+
for entry in entries:
|
|
126
|
+
out.setdefault(entry.user, set()).add(entry.account)
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _is_round_amount(amount: Decimal) -> bool:
|
|
131
|
+
return amount != 0 and amount % Decimal("1000") == 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _outside_business_hours(value: datetime) -> bool:
|
|
135
|
+
return value.weekday() >= 5 or value.time() < time(6, 0) or value.time() > time(20, 0)
|