algo-cli-runtime 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,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)