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