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,784 @@
|
|
|
1
|
+
"""Construction insurance, safety, delay, default, onboarding, and red-flag patterns B114-B123."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import date, timedelta
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Iterable, Mapping
|
|
11
|
+
|
|
12
|
+
from .common import (
|
|
13
|
+
ConstructionIssue,
|
|
14
|
+
Jurisdiction,
|
|
15
|
+
ProjectType,
|
|
16
|
+
ReviewGate,
|
|
17
|
+
RiskLevel,
|
|
18
|
+
SourceRef,
|
|
19
|
+
contains_any,
|
|
20
|
+
decimalize,
|
|
21
|
+
make_issue,
|
|
22
|
+
normalize_label,
|
|
23
|
+
parse_date,
|
|
24
|
+
source_from_mapping,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IndemnityScope(str, Enum):
|
|
29
|
+
OWN_NEGLIGENCE = "own_negligence"
|
|
30
|
+
PARTIAL_NEGLIGENCE = "partial_negligence"
|
|
31
|
+
SOLE_NEGLIGENCE_OF_INDEMNITEE = "sole_negligence_of_indemnitee"
|
|
32
|
+
BROAD_UNKNOWN = "broad_unknown"
|
|
33
|
+
NONE = "none"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class InsuranceRequirement:
|
|
38
|
+
coverage: str
|
|
39
|
+
limit: Decimal | None = None
|
|
40
|
+
required: bool = True
|
|
41
|
+
source_refs: tuple[SourceRef, ...] = ()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class CoverageGap:
|
|
46
|
+
coverage: str
|
|
47
|
+
message: str
|
|
48
|
+
risk_level: RiskLevel = RiskLevel.MEDIUM
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class InsuranceRiskResult:
|
|
53
|
+
pattern_id: str = "B114"
|
|
54
|
+
indemnity_scope: IndemnityScope = IndemnityScope.NONE
|
|
55
|
+
requirements: list[InsuranceRequirement] = field(default_factory=list)
|
|
56
|
+
gaps: list[CoverageGap] = field(default_factory=list)
|
|
57
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
58
|
+
review_gate: ReviewGate = ReviewGate.INSURANCE_REVIEW
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IndemnityInsuranceRiskSplitter:
|
|
62
|
+
"""B114: split indemnity, insurance requirements, COIs, and endorsement gaps."""
|
|
63
|
+
|
|
64
|
+
pattern_id = "B114"
|
|
65
|
+
|
|
66
|
+
def analyze(
|
|
67
|
+
self,
|
|
68
|
+
indemnity_clause: str = "",
|
|
69
|
+
insurance_requirements: Iterable[Mapping[str, Any]] = (),
|
|
70
|
+
evidence: Mapping[str, Any] | None = None,
|
|
71
|
+
) -> InsuranceRiskResult:
|
|
72
|
+
evidence = evidence or {}
|
|
73
|
+
result = InsuranceRiskResult(indemnity_scope=self._scope(indemnity_clause))
|
|
74
|
+
if result.indemnity_scope is IndemnityScope.SOLE_NEGLIGENCE_OF_INDEMNITEE:
|
|
75
|
+
result.issues.append(
|
|
76
|
+
make_issue(
|
|
77
|
+
self.pattern_id,
|
|
78
|
+
"broad indemnity attorney review",
|
|
79
|
+
"Indemnity appears to include the indemnitee's sole negligence; anti-indemnity/enforceability review required.",
|
|
80
|
+
risk_level=RiskLevel.CRITICAL,
|
|
81
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
82
|
+
source_refs=(SourceRef("indemnity_clause", quote=indemnity_clause),),
|
|
83
|
+
tags={"indemnity", "anti_indemnity"},
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
for raw in insurance_requirements:
|
|
87
|
+
req = InsuranceRequirement(
|
|
88
|
+
coverage=str(raw.get("coverage") or raw.get("type") or "coverage"),
|
|
89
|
+
limit=decimalize(raw.get("limit")) if raw.get("limit") is not None else None,
|
|
90
|
+
required=bool(raw.get("required", True)),
|
|
91
|
+
source_refs=source_from_mapping(raw, "insurance_requirement"),
|
|
92
|
+
)
|
|
93
|
+
result.requirements.append(req)
|
|
94
|
+
key = normalize_label(req.coverage)
|
|
95
|
+
supplied = evidence.get(key) or evidence.get(req.coverage)
|
|
96
|
+
if req.required and not supplied:
|
|
97
|
+
gap = CoverageGap(req.coverage, f"Missing evidence for required {req.coverage} coverage.", RiskLevel.HIGH)
|
|
98
|
+
result.gaps.append(gap)
|
|
99
|
+
result.issues.append(
|
|
100
|
+
make_issue(
|
|
101
|
+
self.pattern_id,
|
|
102
|
+
"missing insurance evidence",
|
|
103
|
+
gap.message,
|
|
104
|
+
risk_level=RiskLevel.HIGH,
|
|
105
|
+
review_gate=ReviewGate.INSURANCE_REVIEW,
|
|
106
|
+
source_refs=req.source_refs,
|
|
107
|
+
tags={"insurance", key},
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if evidence.get("coi") and not evidence.get("additional_insured_endorsement"):
|
|
111
|
+
result.gaps.append(CoverageGap("additional insured endorsement", "COI exists but actual additional-insured endorsement was not evidenced.", RiskLevel.HIGH))
|
|
112
|
+
result.issues.append(
|
|
113
|
+
make_issue(
|
|
114
|
+
self.pattern_id,
|
|
115
|
+
"additional insured evidence incomplete",
|
|
116
|
+
"COI alone is not proof of additional-insured status; request endorsement evidence.",
|
|
117
|
+
risk_level=RiskLevel.HIGH,
|
|
118
|
+
review_gate=ReviewGate.INSURANCE_REVIEW,
|
|
119
|
+
tags={"insurance", "additional_insured"},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
if evidence.get("delegated_design") and not (evidence.get("professional_liability") or evidence.get("professional_liability_policy")):
|
|
123
|
+
result.issues.append(
|
|
124
|
+
make_issue(
|
|
125
|
+
self.pattern_id,
|
|
126
|
+
"professional liability gap",
|
|
127
|
+
"Delegated design/design-assist evidence exists but professional liability coverage was not evidenced.",
|
|
128
|
+
risk_level=RiskLevel.HIGH,
|
|
129
|
+
review_gate=ReviewGate.INSURANCE_REVIEW,
|
|
130
|
+
tags={"insurance", "professional_liability", "delegated_design"},
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _scope(text: str) -> IndemnityScope:
|
|
137
|
+
lowered = text.lower()
|
|
138
|
+
if not lowered.strip():
|
|
139
|
+
return IndemnityScope.NONE
|
|
140
|
+
if "sole negligence" in lowered or "regardless of whether caused" in lowered:
|
|
141
|
+
return IndemnityScope.SOLE_NEGLIGENCE_OF_INDEMNITEE
|
|
142
|
+
if "to the extent" in lowered and ("negligence" in lowered or "fault" in lowered):
|
|
143
|
+
return IndemnityScope.PARTIAL_NEGLIGENCE
|
|
144
|
+
if "own negligence" in lowered or "subcontractor's negligence" in lowered or "subcontractor negligence" in lowered:
|
|
145
|
+
return IndemnityScope.OWN_NEGLIGENCE
|
|
146
|
+
if "indemn" in lowered or "hold harmless" in lowered:
|
|
147
|
+
return IndemnityScope.BROAD_UNKNOWN
|
|
148
|
+
return IndemnityScope.NONE
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class OSHAEmployerRole(str, Enum):
|
|
152
|
+
CREATING = "creating"
|
|
153
|
+
EXPOSING = "exposing"
|
|
154
|
+
CORRECTING = "correcting"
|
|
155
|
+
CONTROLLING = "controlling"
|
|
156
|
+
NONE = "none"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True)
|
|
160
|
+
class SafetyEvent:
|
|
161
|
+
id: str
|
|
162
|
+
hazard: str
|
|
163
|
+
roles: tuple[OSHAEmployerRole, ...]
|
|
164
|
+
authority_to_correct: bool = False
|
|
165
|
+
requested_correction: bool = False
|
|
166
|
+
warned_employees: bool = False
|
|
167
|
+
alternative_protection: bool = False
|
|
168
|
+
inspections: bool = False
|
|
169
|
+
correction_system: bool = False
|
|
170
|
+
graduated_enforcement: bool = False
|
|
171
|
+
isolated_hazard: bool = False
|
|
172
|
+
notified_controller: bool = False
|
|
173
|
+
source_refs: tuple[SourceRef, ...] = ()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class SafetyRoleResult:
|
|
178
|
+
event_id: str
|
|
179
|
+
roles: tuple[OSHAEmployerRole, ...]
|
|
180
|
+
required_actions: tuple[str, ...]
|
|
181
|
+
issues: tuple[ConstructionIssue, ...]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class OSHAMultiEmployerClassifier:
|
|
185
|
+
"""B115: classify creating/exposing/correcting/controlling-employer role and actions."""
|
|
186
|
+
|
|
187
|
+
pattern_id = "B115"
|
|
188
|
+
|
|
189
|
+
def classify(self, events: Iterable[SafetyEvent | Mapping[str, Any]]) -> list[SafetyRoleResult]:
|
|
190
|
+
results: list[SafetyRoleResult] = []
|
|
191
|
+
for raw in events:
|
|
192
|
+
event = raw if isinstance(raw, SafetyEvent) else self._coerce_event(raw)
|
|
193
|
+
actions: list[str] = []
|
|
194
|
+
issues: list[ConstructionIssue] = []
|
|
195
|
+
if OSHAEmployerRole.EXPOSING in event.roles and not event.authority_to_correct:
|
|
196
|
+
actions.extend(["request correction", "warn employees", "use feasible alternative protection"])
|
|
197
|
+
if not (event.requested_correction and event.warned_employees and event.alternative_protection):
|
|
198
|
+
issues.append(
|
|
199
|
+
make_issue(
|
|
200
|
+
self.pattern_id,
|
|
201
|
+
"exposing employer action gap",
|
|
202
|
+
"Exposing employer without authority to correct should request correction, warn crew, and use feasible alternative protection.",
|
|
203
|
+
risk_level=RiskLevel.HIGH,
|
|
204
|
+
review_gate=ReviewGate.SAFETY_REVIEW,
|
|
205
|
+
source_refs=event.source_refs,
|
|
206
|
+
tags={"osha", "exposing"},
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
if OSHAEmployerRole.CONTROLLING in event.roles:
|
|
210
|
+
actions.extend(["periodic inspections", "prompt correction system", "graduated enforcement"])
|
|
211
|
+
if not (event.inspections and event.correction_system and event.graduated_enforcement):
|
|
212
|
+
issues.append(
|
|
213
|
+
make_issue(
|
|
214
|
+
self.pattern_id,
|
|
215
|
+
"controlling employer reasonable-care gap",
|
|
216
|
+
"Controlling-employer evidence lacks inspections, correction system, or graduated enforcement.",
|
|
217
|
+
risk_level=RiskLevel.HIGH,
|
|
218
|
+
review_gate=ReviewGate.SAFETY_REVIEW,
|
|
219
|
+
source_refs=event.source_refs,
|
|
220
|
+
tags={"osha", "controlling"},
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
if OSHAEmployerRole.CREATING in event.roles and not (event.isolated_hazard and event.notified_controller):
|
|
224
|
+
actions.extend(["do not create hazard", "isolate hazard", "notify controlling employer"])
|
|
225
|
+
issues.append(
|
|
226
|
+
make_issue(
|
|
227
|
+
self.pattern_id,
|
|
228
|
+
"creating employer hazard response gap",
|
|
229
|
+
"Employer appears to have created a hazard without complete isolation/notification evidence.",
|
|
230
|
+
risk_level=RiskLevel.HIGH,
|
|
231
|
+
review_gate=ReviewGate.SAFETY_REVIEW,
|
|
232
|
+
source_refs=event.source_refs,
|
|
233
|
+
tags={"osha", "creating"},
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
results.append(SafetyRoleResult(event.id, event.roles, tuple(dict.fromkeys(actions)), tuple(issues)))
|
|
237
|
+
return results
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _coerce_event(raw: Mapping[str, Any]) -> SafetyEvent:
|
|
241
|
+
roles: list[OSHAEmployerRole] = []
|
|
242
|
+
for role in raw.get("roles", (raw.get("role"),)):
|
|
243
|
+
if role in OSHAEmployerRole._value2member_map_:
|
|
244
|
+
roles.append(OSHAEmployerRole(str(role)))
|
|
245
|
+
return SafetyEvent(
|
|
246
|
+
id=str(raw.get("id") or raw.get("hazard") or "safety_event"),
|
|
247
|
+
hazard=str(raw.get("hazard") or ""),
|
|
248
|
+
roles=tuple(roles) or (OSHAEmployerRole.NONE,),
|
|
249
|
+
authority_to_correct=bool(raw.get("authority_to_correct")),
|
|
250
|
+
requested_correction=bool(raw.get("requested_correction")),
|
|
251
|
+
warned_employees=bool(raw.get("warned_employees")),
|
|
252
|
+
alternative_protection=bool(raw.get("alternative_protection")),
|
|
253
|
+
inspections=bool(raw.get("inspections")),
|
|
254
|
+
correction_system=bool(raw.get("correction_system")),
|
|
255
|
+
graduated_enforcement=bool(raw.get("graduated_enforcement")),
|
|
256
|
+
isolated_hazard=bool(raw.get("isolated_hazard")),
|
|
257
|
+
notified_controller=bool(raw.get("notified_controller")),
|
|
258
|
+
source_refs=source_from_mapping(raw, "safety_event"),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class DelayEvent:
|
|
264
|
+
id: str
|
|
265
|
+
event_date: date
|
|
266
|
+
days_impacted: int
|
|
267
|
+
notice_sent: bool = False
|
|
268
|
+
baseline_schedule: bool = False
|
|
269
|
+
daily_reports: bool = False
|
|
270
|
+
cost_records: bool = False
|
|
271
|
+
directed_acceleration: bool = False
|
|
272
|
+
source_refs: tuple[SourceRef, ...] = ()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class DelayAnalysis:
|
|
277
|
+
pattern_id: str = "B116"
|
|
278
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
279
|
+
recovery_limited: bool = False
|
|
280
|
+
review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class DelayAccelerationAnalyzer:
|
|
284
|
+
"""B116: surface delay/acceleration/no-damages-for-delay proof gaps."""
|
|
285
|
+
|
|
286
|
+
pattern_id = "B116"
|
|
287
|
+
|
|
288
|
+
def analyze(self, clauses: str, events: Iterable[DelayEvent | Mapping[str, Any]]) -> DelayAnalysis:
|
|
289
|
+
result = DelayAnalysis(recovery_limited=contains_any(clauses, ("no damages for delay", "sole remedy is time", "non-compensable delay")))
|
|
290
|
+
if result.recovery_limited:
|
|
291
|
+
result.issues.append(
|
|
292
|
+
make_issue(
|
|
293
|
+
self.pattern_id,
|
|
294
|
+
"delay recovery limitation",
|
|
295
|
+
"Delay clause may limit monetary recovery; attorney review required.",
|
|
296
|
+
risk_level=RiskLevel.HIGH,
|
|
297
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
298
|
+
source_refs=(SourceRef("delay_clause", quote=clauses),),
|
|
299
|
+
tags={"delay", "recovery_limit"},
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
for raw in events:
|
|
303
|
+
event = raw if isinstance(raw, DelayEvent) else self._coerce_delay_event(raw)
|
|
304
|
+
missing = []
|
|
305
|
+
if not event.notice_sent:
|
|
306
|
+
missing.append("notice")
|
|
307
|
+
if not event.baseline_schedule:
|
|
308
|
+
missing.append("baseline schedule")
|
|
309
|
+
if not event.daily_reports:
|
|
310
|
+
missing.append("daily reports")
|
|
311
|
+
if not event.cost_records:
|
|
312
|
+
missing.append("cost records")
|
|
313
|
+
if missing:
|
|
314
|
+
result.issues.append(
|
|
315
|
+
make_issue(
|
|
316
|
+
self.pattern_id,
|
|
317
|
+
"delay proof incomplete",
|
|
318
|
+
f"Delay event {event.id} is missing: {', '.join(missing)}.",
|
|
319
|
+
risk_level=RiskLevel.HIGH if "notice" in missing or "baseline schedule" in missing else RiskLevel.MEDIUM,
|
|
320
|
+
review_gate=ReviewGate.BUSINESS_REVIEW,
|
|
321
|
+
source_refs=event.source_refs,
|
|
322
|
+
tags={"delay", "proof"},
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
if event.directed_acceleration and not event.notice_sent:
|
|
326
|
+
result.issues.append(
|
|
327
|
+
make_issue(
|
|
328
|
+
self.pattern_id,
|
|
329
|
+
"directed acceleration notice gap",
|
|
330
|
+
f"Directed acceleration event {event.id} needs notice/change documentation.",
|
|
331
|
+
risk_level=RiskLevel.HIGH,
|
|
332
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
333
|
+
source_refs=event.source_refs,
|
|
334
|
+
tags={"delay", "acceleration", "notice"},
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def _coerce_delay_event(raw: Mapping[str, Any]) -> DelayEvent:
|
|
341
|
+
return DelayEvent(
|
|
342
|
+
id=str(raw.get("id") or raw.get("description") or "delay_event"),
|
|
343
|
+
event_date=parse_date(raw.get("event_date") or raw.get("date")) or date.today(),
|
|
344
|
+
days_impacted=int(raw.get("days_impacted") or raw.get("days") or 0),
|
|
345
|
+
notice_sent=bool(raw.get("notice_sent")),
|
|
346
|
+
baseline_schedule=bool(raw.get("baseline_schedule")),
|
|
347
|
+
daily_reports=bool(raw.get("daily_reports")),
|
|
348
|
+
cost_records=bool(raw.get("cost_records")),
|
|
349
|
+
directed_acceleration=bool(raw.get("directed_acceleration")),
|
|
350
|
+
source_refs=source_from_mapping(raw, "delay_event"),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class TerminationMode(str, Enum):
|
|
355
|
+
CONVENIENCE = "convenience"
|
|
356
|
+
CAUSE = "cause"
|
|
357
|
+
NONPAYMENT = "nonpayment"
|
|
358
|
+
SUSPENSION = "suspension"
|
|
359
|
+
SAFETY = "safety"
|
|
360
|
+
UNKNOWN = "unknown"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclass(frozen=True)
|
|
364
|
+
class CureGate:
|
|
365
|
+
mode: TerminationMode
|
|
366
|
+
cure_deadline: date | None
|
|
367
|
+
urgent: bool
|
|
368
|
+
issues: tuple[ConstructionIssue, ...]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class TerminationDefaultGate:
|
|
372
|
+
"""B117: create cure calendar / stop gate for termination and suspension."""
|
|
373
|
+
|
|
374
|
+
pattern_id = "B117"
|
|
375
|
+
|
|
376
|
+
def analyze(self, clause_text: str, *, notice_date: date | str | None = None, mode: str | None = None) -> CureGate:
|
|
377
|
+
notice = parse_date(notice_date)
|
|
378
|
+
mode_enum = self._mode(mode or clause_text)
|
|
379
|
+
cure_days = self._extract_cure_days(clause_text)
|
|
380
|
+
deadline = notice + timedelta(days=cure_days) if notice and cure_days is not None else None
|
|
381
|
+
urgent = deadline is not None and (deadline - date.today()).days <= 2
|
|
382
|
+
issues: list[ConstructionIssue] = []
|
|
383
|
+
if notice:
|
|
384
|
+
issues.append(
|
|
385
|
+
make_issue(
|
|
386
|
+
self.pattern_id,
|
|
387
|
+
"termination/default attorney review",
|
|
388
|
+
"Termination/default/suspension notice requires attorney review and evidence preservation before action.",
|
|
389
|
+
risk_level=RiskLevel.CRITICAL if mode_enum is TerminationMode.CAUSE else RiskLevel.HIGH,
|
|
390
|
+
review_gate=ReviewGate.STOP_EXTERNAL_ACTION,
|
|
391
|
+
source_refs=(SourceRef("termination_clause", quote=clause_text),),
|
|
392
|
+
tags={"termination", mode_enum.value},
|
|
393
|
+
deadline=deadline,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
if mode_enum is TerminationMode.CONVENIENCE and not contains_any(clause_text, ("demobilization", "stored materials", "profit", "unperformed work")):
|
|
397
|
+
issues.append(
|
|
398
|
+
make_issue(
|
|
399
|
+
self.pattern_id,
|
|
400
|
+
"termination-for-convenience pricing gap",
|
|
401
|
+
"Convenience termination clause lacks clear demobilization/stored-material/profit treatment.",
|
|
402
|
+
risk_level=RiskLevel.HIGH,
|
|
403
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
404
|
+
tags={"termination", "pricing"},
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
return CureGate(mode_enum, deadline, urgent, tuple(issues))
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _extract_cure_days(text: str) -> int | None:
|
|
411
|
+
match = re.search(r"(\d+)\s*(?:calendar\s*)?(?:day|days|hour|hours)", text.lower())
|
|
412
|
+
if not match:
|
|
413
|
+
return None
|
|
414
|
+
value = int(match.group(1))
|
|
415
|
+
if "hour" in match.group(0):
|
|
416
|
+
return 1 if value <= 24 else max(1, value // 24)
|
|
417
|
+
return value
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def _mode(text: str) -> TerminationMode:
|
|
421
|
+
lowered = text.lower()
|
|
422
|
+
if "convenience" in lowered:
|
|
423
|
+
return TerminationMode.CONVENIENCE
|
|
424
|
+
if "nonpayment" in lowered or "non-payment" in lowered:
|
|
425
|
+
return TerminationMode.NONPAYMENT
|
|
426
|
+
if "suspend" in lowered:
|
|
427
|
+
return TerminationMode.SUSPENSION
|
|
428
|
+
if "cause" in lowered or "default" in lowered:
|
|
429
|
+
return TerminationMode.CAUSE
|
|
430
|
+
if "safety" in lowered:
|
|
431
|
+
return TerminationMode.SAFETY
|
|
432
|
+
return TerminationMode.UNKNOWN
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True)
|
|
436
|
+
class BackchargeReview:
|
|
437
|
+
clean: bool
|
|
438
|
+
support_score: int
|
|
439
|
+
missing: tuple[str, ...]
|
|
440
|
+
issues: tuple[ConstructionIssue, ...]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class BackchargeDocumentationGate:
|
|
444
|
+
"""B118: score backcharge/setoff proof completeness."""
|
|
445
|
+
|
|
446
|
+
pattern_id = "B118"
|
|
447
|
+
|
|
448
|
+
def review(self, backcharge: Mapping[str, Any]) -> BackchargeReview:
|
|
449
|
+
required = ("notice", "opportunity_to_cure", "photos", "invoices", "causation")
|
|
450
|
+
present = {key for key in required if backcharge.get(key)}
|
|
451
|
+
missing = tuple(key for key in required if key not in present)
|
|
452
|
+
score = len(present)
|
|
453
|
+
issues: list[ConstructionIssue] = []
|
|
454
|
+
if missing:
|
|
455
|
+
issues.append(
|
|
456
|
+
make_issue(
|
|
457
|
+
self.pattern_id,
|
|
458
|
+
"backcharge support incomplete",
|
|
459
|
+
f"Backcharge is missing: {', '.join(missing)}.",
|
|
460
|
+
risk_level=RiskLevel.HIGH if "notice" in missing or "causation" in missing else RiskLevel.MEDIUM,
|
|
461
|
+
review_gate=ReviewGate.BUSINESS_REVIEW,
|
|
462
|
+
source_refs=source_from_mapping(backcharge, "backcharge"),
|
|
463
|
+
tags={"backcharge", "proof"},
|
|
464
|
+
amount=backcharge.get("amount"),
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
if backcharge.get("unrelated_project_setoff"):
|
|
468
|
+
issues.append(
|
|
469
|
+
make_issue(
|
|
470
|
+
self.pattern_id,
|
|
471
|
+
"unrelated project setoff review",
|
|
472
|
+
"Setoff/backcharge against an unrelated project requires attorney review.",
|
|
473
|
+
risk_level=RiskLevel.HIGH,
|
|
474
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
475
|
+
tags={"backcharge", "setoff"},
|
|
476
|
+
amount=backcharge.get("amount"),
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
return BackchargeReview(clean=not issues, support_score=score, missing=missing, issues=tuple(issues))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@dataclass(frozen=True)
|
|
483
|
+
class CloseoutItem:
|
|
484
|
+
name: str
|
|
485
|
+
required: bool = True
|
|
486
|
+
complete: bool = False
|
|
487
|
+
owner: str | None = None
|
|
488
|
+
source_refs: tuple[SourceRef, ...] = ()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclass
|
|
492
|
+
class CloseoutMatrix:
|
|
493
|
+
pattern_id: str = "B119"
|
|
494
|
+
items: list[CloseoutItem] = field(default_factory=list)
|
|
495
|
+
warranty_start: date | None = None
|
|
496
|
+
warranty_uncertain: bool = False
|
|
497
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
498
|
+
review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class WarrantyCloseoutMatrix:
|
|
502
|
+
"""B119: track closeout blockers, warranty start, and final-payment issues."""
|
|
503
|
+
|
|
504
|
+
pattern_id = "B119"
|
|
505
|
+
|
|
506
|
+
def build(self, items: Iterable[CloseoutItem | Mapping[str, Any]], *, warranty_start: date | str | None = None) -> CloseoutMatrix:
|
|
507
|
+
matrix = CloseoutMatrix(warranty_start=parse_date(warranty_start))
|
|
508
|
+
for raw in items:
|
|
509
|
+
item = raw if isinstance(raw, CloseoutItem) else CloseoutItem(
|
|
510
|
+
name=str(raw.get("name") or raw.get("item") or "closeout item"),
|
|
511
|
+
required=bool(raw.get("required", True)),
|
|
512
|
+
complete=bool(raw.get("complete")),
|
|
513
|
+
owner=raw.get("owner"),
|
|
514
|
+
source_refs=source_from_mapping(raw, "closeout"),
|
|
515
|
+
)
|
|
516
|
+
matrix.items.append(item)
|
|
517
|
+
if item.required and not item.complete:
|
|
518
|
+
matrix.issues.append(
|
|
519
|
+
make_issue(
|
|
520
|
+
self.pattern_id,
|
|
521
|
+
"closeout blocker",
|
|
522
|
+
f"Required closeout item '{item.name}' is incomplete.",
|
|
523
|
+
risk_level=RiskLevel.MEDIUM,
|
|
524
|
+
review_gate=ReviewGate.BUSINESS_REVIEW,
|
|
525
|
+
source_refs=item.source_refs,
|
|
526
|
+
tags={"closeout", normalize_label(item.name)},
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
if matrix.warranty_start is None:
|
|
530
|
+
matrix.warranty_uncertain = True
|
|
531
|
+
matrix.issues.append(
|
|
532
|
+
make_issue(
|
|
533
|
+
self.pattern_id,
|
|
534
|
+
"warranty start unclear",
|
|
535
|
+
"Warranty start trigger/date is not documented.",
|
|
536
|
+
risk_level=RiskLevel.MEDIUM,
|
|
537
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
538
|
+
tags={"warranty"},
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
return matrix
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@dataclass(frozen=True)
|
|
545
|
+
class ClassificationFactor:
|
|
546
|
+
name: str
|
|
547
|
+
direction: str
|
|
548
|
+
evidence: str
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@dataclass
|
|
552
|
+
class WorkerClassificationResult:
|
|
553
|
+
pattern_id: str = "B120"
|
|
554
|
+
risk_level: RiskLevel = RiskLevel.MEDIUM
|
|
555
|
+
factors: list[ClassificationFactor] = field(default_factory=list)
|
|
556
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
557
|
+
review_gate: ReviewGate = ReviewGate.TAX_HR_REVIEW
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class IndependentContractorClassifier:
|
|
561
|
+
"""B120: IRS/DOL-style IC classification risk matrix. No final determination."""
|
|
562
|
+
|
|
563
|
+
pattern_id = "B120"
|
|
564
|
+
|
|
565
|
+
def classify(self, facts: Mapping[str, Any]) -> WorkerClassificationResult:
|
|
566
|
+
result = WorkerClassificationResult()
|
|
567
|
+
employee_score = 0
|
|
568
|
+
contractor_score = 0
|
|
569
|
+
def add(name: str, direction: str, evidence: str, points: int = 1) -> None:
|
|
570
|
+
nonlocal employee_score, contractor_score
|
|
571
|
+
result.factors.append(ClassificationFactor(name, direction, evidence))
|
|
572
|
+
if direction == "employee":
|
|
573
|
+
employee_score += points
|
|
574
|
+
elif direction == "contractor":
|
|
575
|
+
contractor_score += points
|
|
576
|
+
|
|
577
|
+
if facts.get("company_controls_schedule") or facts.get("company_controls_methods"):
|
|
578
|
+
add("behavioral control", "employee", "Company controls schedule/methods.", 2)
|
|
579
|
+
if facts.get("company_tools"):
|
|
580
|
+
add("tools/investment", "employee", "Company supplies core tools/equipment.")
|
|
581
|
+
if facts.get("exclusive") or facts.get("ongoing_indefinite"):
|
|
582
|
+
add("permanence/exclusivity", "employee", "Relationship appears exclusive or indefinite.")
|
|
583
|
+
if facts.get("core_business_work"):
|
|
584
|
+
add("integral work", "employee", "Work is core/integral to business.")
|
|
585
|
+
if facts.get("own_business"):
|
|
586
|
+
add("business entity", "contractor", "Worker has separate business.")
|
|
587
|
+
if facts.get("own_insurance") and facts.get("own_tools"):
|
|
588
|
+
add("investment", "contractor", "Worker carries insurance and tools.")
|
|
589
|
+
if facts.get("multiple_clients"):
|
|
590
|
+
add("market availability", "contractor", "Worker markets to multiple clients.")
|
|
591
|
+
if facts.get("negotiated_scope_price"):
|
|
592
|
+
add("profit/loss", "contractor", "Worker can negotiate scope/price and manage profit/loss.")
|
|
593
|
+
if employee_score >= contractor_score + 2:
|
|
594
|
+
result.risk_level = RiskLevel.HIGH
|
|
595
|
+
result.issues.append(
|
|
596
|
+
make_issue(
|
|
597
|
+
self.pattern_id,
|
|
598
|
+
"high-risk independent contractor classification",
|
|
599
|
+
"Facts lean toward employee/economic-dependence treatment despite any IC label; final classification requires CPA/attorney/HR review.",
|
|
600
|
+
risk_level=RiskLevel.HIGH,
|
|
601
|
+
review_gate=ReviewGate.TAX_HR_REVIEW,
|
|
602
|
+
tags={"independent_contractor", "misclassification"},
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
elif contractor_score >= employee_score + 2:
|
|
606
|
+
result.risk_level = RiskLevel.LOW
|
|
607
|
+
else:
|
|
608
|
+
result.risk_level = RiskLevel.MEDIUM
|
|
609
|
+
result.issues.append(
|
|
610
|
+
make_issue(
|
|
611
|
+
self.pattern_id,
|
|
612
|
+
"insufficient or mixed IC evidence",
|
|
613
|
+
"Worker classification evidence is mixed or incomplete; do not make final classification without review.",
|
|
614
|
+
risk_level=RiskLevel.MEDIUM,
|
|
615
|
+
review_gate=ReviewGate.TAX_HR_REVIEW,
|
|
616
|
+
tags={"independent_contractor", "evidence"},
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
return result
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@dataclass(frozen=True)
|
|
623
|
+
class OnboardingRequirement:
|
|
624
|
+
name: str
|
|
625
|
+
status: str
|
|
626
|
+
expires: date | None = None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@dataclass
|
|
630
|
+
class OnboardingPackResult:
|
|
631
|
+
pattern_id: str = "B121"
|
|
632
|
+
requirements: list[OnboardingRequirement] = field(default_factory=list)
|
|
633
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
634
|
+
complete: bool = False
|
|
635
|
+
review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class OnboardingCompliancePack:
|
|
639
|
+
"""B121: verify sub/IC onboarding docs before work/payment."""
|
|
640
|
+
|
|
641
|
+
pattern_id = "B121"
|
|
642
|
+
required_docs = ("signed_agreement", "w9", "coi", "license")
|
|
643
|
+
|
|
644
|
+
def check(self, vendor_file: Mapping[str, Any], *, mobilization_date: date | str | None = None, project_end: date | str | None = None) -> OnboardingPackResult:
|
|
645
|
+
result = OnboardingPackResult()
|
|
646
|
+
mobilize = parse_date(mobilization_date)
|
|
647
|
+
end = parse_date(project_end)
|
|
648
|
+
for doc in self.required_docs:
|
|
649
|
+
value = vendor_file.get(doc)
|
|
650
|
+
expires = parse_date(vendor_file.get(f"{doc}_expires"))
|
|
651
|
+
status = "complete" if value else "missing"
|
|
652
|
+
if expires and end and expires < end:
|
|
653
|
+
status = "expired_before_project_end"
|
|
654
|
+
req = OnboardingRequirement(doc, status, expires)
|
|
655
|
+
result.requirements.append(req)
|
|
656
|
+
if status != "complete":
|
|
657
|
+
result.issues.append(
|
|
658
|
+
make_issue(
|
|
659
|
+
self.pattern_id,
|
|
660
|
+
"onboarding document issue",
|
|
661
|
+
f"{doc} status is {status}.",
|
|
662
|
+
risk_level=RiskLevel.HIGH if doc in {"coi", "license", "signed_agreement"} else RiskLevel.MEDIUM,
|
|
663
|
+
review_gate=ReviewGate.BUSINESS_REVIEW,
|
|
664
|
+
tags={"onboarding", doc},
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
signed_date = parse_date(vendor_file.get("signed_agreement_date"))
|
|
668
|
+
if mobilize and (not signed_date or signed_date > mobilize):
|
|
669
|
+
result.issues.append(
|
|
670
|
+
make_issue(
|
|
671
|
+
self.pattern_id,
|
|
672
|
+
"work before signed agreement",
|
|
673
|
+
"Mobilization appears before signed agreement/work order evidence.",
|
|
674
|
+
risk_level=RiskLevel.HIGH,
|
|
675
|
+
review_gate=ReviewGate.BUSINESS_REVIEW,
|
|
676
|
+
tags={"onboarding", "work_before_contract"},
|
|
677
|
+
deadline=mobilize,
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
result.complete = not result.issues
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@dataclass(frozen=True)
|
|
685
|
+
class OverlayRoute:
|
|
686
|
+
name: str
|
|
687
|
+
source: str
|
|
688
|
+
review_gate: ReviewGate
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@dataclass
|
|
692
|
+
class JurisdictionOverlayResult:
|
|
693
|
+
pattern_id: str = "B122"
|
|
694
|
+
overlays: list[OverlayRoute] = field(default_factory=list)
|
|
695
|
+
issues: list[ConstructionIssue] = field(default_factory=list)
|
|
696
|
+
low_confidence: bool = False
|
|
697
|
+
review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class JurisdictionOverlayRouter:
|
|
701
|
+
"""B122: route state/project-type overlays and block generic legal assumptions."""
|
|
702
|
+
|
|
703
|
+
pattern_id = "B122"
|
|
704
|
+
|
|
705
|
+
def route(self, jurisdiction: Jurisdiction) -> JurisdictionOverlayResult:
|
|
706
|
+
result = JurisdictionOverlayResult()
|
|
707
|
+
if not jurisdiction.state or jurisdiction.project_type is ProjectType.UNKNOWN:
|
|
708
|
+
result.low_confidence = True
|
|
709
|
+
result.issues.append(
|
|
710
|
+
make_issue(
|
|
711
|
+
self.pattern_id,
|
|
712
|
+
"jurisdiction facts missing",
|
|
713
|
+
"Project state and public/private/federal status are required before lien/payment/deadline confidence.",
|
|
714
|
+
risk_level=RiskLevel.HIGH,
|
|
715
|
+
review_gate=ReviewGate.ATTORNEY_REVIEW,
|
|
716
|
+
tags={"jurisdiction"},
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
return result
|
|
720
|
+
if jurisdiction.is_federal:
|
|
721
|
+
result.overlays.append(OverlayRoute("Miller Act payment bond", "40 U.S.C. 3133", ReviewGate.ATTORNEY_REVIEW))
|
|
722
|
+
if jurisdiction.is_kansas_private:
|
|
723
|
+
result.overlays.extend([
|
|
724
|
+
OverlayRoute("Kansas prompt pay", "K.S.A. 16-1803", ReviewGate.ATTORNEY_REVIEW),
|
|
725
|
+
OverlayRoute("Kansas retainage", "K.S.A. 16-1904", ReviewGate.ATTORNEY_REVIEW),
|
|
726
|
+
OverlayRoute("Kansas mechanic lien", "K.S.A. 60-1103", ReviewGate.ATTORNEY_REVIEW),
|
|
727
|
+
])
|
|
728
|
+
if jurisdiction.normalized_state == "MO" and jurisdiction.project_type is ProjectType.PRIVATE:
|
|
729
|
+
result.overlays.append(OverlayRoute("Missouri private lien/payment overlay", "state-specific research required", ReviewGate.ATTORNEY_REVIEW))
|
|
730
|
+
return result
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@dataclass(frozen=True)
|
|
734
|
+
class RedFlag:
|
|
735
|
+
issue: ConstructionIssue
|
|
736
|
+
priority: Decimal
|
|
737
|
+
posture: str
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@dataclass
|
|
741
|
+
class RedFlagQueueResult:
|
|
742
|
+
pattern_id: str = "B123"
|
|
743
|
+
flags: list[RedFlag] = field(default_factory=list)
|
|
744
|
+
review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class ContractRedFlagQueue:
|
|
748
|
+
"""B123: rank construction contract issues into practical negotiation posture."""
|
|
749
|
+
|
|
750
|
+
pattern_id = "B123"
|
|
751
|
+
severity_points = {
|
|
752
|
+
RiskLevel.LOW: Decimal("1"),
|
|
753
|
+
RiskLevel.MEDIUM: Decimal("3"),
|
|
754
|
+
RiskLevel.HIGH: Decimal("7"),
|
|
755
|
+
RiskLevel.CRITICAL: Decimal("12"),
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
def rank(self, issues: Iterable[ConstructionIssue]) -> RedFlagQueueResult:
|
|
759
|
+
flags: list[RedFlag] = []
|
|
760
|
+
for issue in issues:
|
|
761
|
+
priority = self.severity_points[issue.risk_level]
|
|
762
|
+
if issue.amount:
|
|
763
|
+
priority += min(Decimal("10"), abs(issue.amount) / Decimal("10000"))
|
|
764
|
+
if issue.deadline:
|
|
765
|
+
days = (issue.deadline - date.today()).days
|
|
766
|
+
if days <= 2:
|
|
767
|
+
priority += Decimal("5")
|
|
768
|
+
elif days <= 7:
|
|
769
|
+
priority += Decimal("3")
|
|
770
|
+
if issue.review_gate is ReviewGate.STOP_EXTERNAL_ACTION:
|
|
771
|
+
posture = "stop"
|
|
772
|
+
priority += Decimal("10")
|
|
773
|
+
elif issue.review_gate is ReviewGate.ATTORNEY_REVIEW:
|
|
774
|
+
posture = "attorney_review"
|
|
775
|
+
priority += Decimal("4")
|
|
776
|
+
elif issue.risk_level in {RiskLevel.HIGH, RiskLevel.CRITICAL}:
|
|
777
|
+
posture = "negotiate"
|
|
778
|
+
elif "ambiguous" in issue.tags or "scope" in issue.tags:
|
|
779
|
+
posture = "clarify"
|
|
780
|
+
else:
|
|
781
|
+
posture = "accept_or_monitor"
|
|
782
|
+
flags.append(RedFlag(issue, priority, posture))
|
|
783
|
+
flags.sort(key=lambda flag: (flag.priority, flag.issue.title), reverse=True)
|
|
784
|
+
return RedFlagQueueResult(flags=flags)
|