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,470 @@
1
+ """Construction payment, lien/bond, waiver, and change-notice patterns B109-B113."""
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
+ Deadline,
15
+ Jurisdiction,
16
+ ReviewGate,
17
+ RiskLevel,
18
+ SourceRef,
19
+ add_business_days,
20
+ add_months_approx,
21
+ decimalize,
22
+ make_issue,
23
+ parse_date,
24
+ source_from_mapping,
25
+ )
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ChangeEvent:
30
+ id: str
31
+ description: str
32
+ event_date: date
33
+ trigger: str = "change"
34
+ written_authorization: bool = False
35
+ work_started: bool = False
36
+ backup_complete: bool = False
37
+ source_refs: tuple[SourceRef, ...] = ()
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ChangeNotice:
42
+ event_id: str
43
+ notice_due: date
44
+ backup_due: date | None
45
+ status: str
46
+ issues: tuple[ConstructionIssue, ...] = ()
47
+
48
+
49
+ @dataclass
50
+ class ChangeNoticeResult:
51
+ pattern_id: str = "B109"
52
+ notices: list[ChangeNotice] = field(default_factory=list)
53
+ issues: list[ConstructionIssue] = field(default_factory=list)
54
+ review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
55
+
56
+
57
+ class ChangeNoticeEngine:
58
+ """B109: compute change-order notice/time-bar review dates."""
59
+
60
+ pattern_id = "B109"
61
+
62
+ def analyze(
63
+ self,
64
+ events: Iterable[ChangeEvent | Mapping[str, Any]],
65
+ clause_text: str = "",
66
+ *,
67
+ default_notice_days: int = 7,
68
+ backup_days: int | None = None,
69
+ ) -> ChangeNoticeResult:
70
+ result = ChangeNoticeResult()
71
+ notice_days = self._extract_days(clause_text, default_notice_days)
72
+ for raw in events:
73
+ event = raw if isinstance(raw, ChangeEvent) else self._coerce_event(raw)
74
+ notice_due = event.event_date + timedelta(days=notice_days)
75
+ backup_due = event.event_date + timedelta(days=backup_days) if backup_days is not None else None
76
+ issues: list[ConstructionIssue] = []
77
+ if event.work_started and not event.written_authorization:
78
+ issues.append(
79
+ make_issue(
80
+ self.pattern_id,
81
+ "verbal or unauthorized extra work",
82
+ f"Event {event.id} appears to have started without written change authorization.",
83
+ risk_level=RiskLevel.HIGH,
84
+ review_gate=ReviewGate.BUSINESS_REVIEW,
85
+ source_refs=event.source_refs,
86
+ tags={"change_order", "written_authorization"},
87
+ deadline=notice_due,
88
+ )
89
+ )
90
+ if not event.backup_complete:
91
+ issues.append(
92
+ make_issue(
93
+ self.pattern_id,
94
+ "change backup incomplete",
95
+ f"Event {event.id} lacks complete cost/schedule backup for change package.",
96
+ risk_level=RiskLevel.MEDIUM,
97
+ review_gate=ReviewGate.BUSINESS_REVIEW,
98
+ source_refs=event.source_refs,
99
+ tags={"change_order", "backup"},
100
+ deadline=backup_due or notice_due,
101
+ )
102
+ )
103
+ status = "late-risk" if date.today() > notice_due else "open"
104
+ notice = ChangeNotice(event.id, notice_due, backup_due, status, tuple(issues))
105
+ result.notices.append(notice)
106
+ result.issues.extend(issues)
107
+ return result
108
+
109
+ @staticmethod
110
+ def _extract_days(text: str, fallback: int) -> int:
111
+ lowered = text.lower()
112
+ if "immediate" in lowered or "immediately" in lowered or "24 hour" in lowered:
113
+ return 1
114
+ match = re.search(r"(\d+)\s*(?:calendar\s*)?days?", lowered)
115
+ return int(match.group(1)) if match else fallback
116
+
117
+ @staticmethod
118
+ def _coerce_event(raw: Mapping[str, Any]) -> ChangeEvent:
119
+ event_date = parse_date(raw.get("event_date") or raw.get("date")) or date.today()
120
+ return ChangeEvent(
121
+ id=str(raw.get("id") or raw.get("description") or "event"),
122
+ description=str(raw.get("description") or raw.get("text") or ""),
123
+ event_date=event_date,
124
+ trigger=str(raw.get("trigger") or "change"),
125
+ written_authorization=bool(raw.get("written_authorization") or raw.get("written_auth")),
126
+ work_started=bool(raw.get("work_started")),
127
+ backup_complete=bool(raw.get("backup_complete")),
128
+ source_refs=source_from_mapping(raw, "change_event"),
129
+ )
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class PayApplication:
134
+ id: str
135
+ amount: Decimal
136
+ submitted_date: date
137
+ proper: bool = True
138
+ disputed_amount: Decimal = Decimal("0")
139
+ owner_payment_date: date | None = None
140
+ contractor_payment_date: date | None = None
141
+ source_refs: tuple[SourceRef, ...] = ()
142
+
143
+ @property
144
+ def undisputed_amount(self) -> Decimal:
145
+ return max(Decimal("0"), self.amount - self.disputed_amount)
146
+
147
+
148
+ @dataclass
149
+ class PaymentAnalysis:
150
+ pattern_id: str = "B110"
151
+ retainage_rate: Decimal | None = None
152
+ deadlines: list[Deadline] = field(default_factory=list)
153
+ issues: list[ConstructionIssue] = field(default_factory=list)
154
+ review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
155
+
156
+
157
+ class PaymentRetainageAnalyzer:
158
+ """B110: extract/validate payment, retainage, and pay-app timing risks."""
159
+
160
+ pattern_id = "B110"
161
+
162
+ def analyze(
163
+ self,
164
+ pay_apps: Iterable[PayApplication | Mapping[str, Any]] = (),
165
+ *,
166
+ retainage_rate: Decimal | str | int | None = None,
167
+ jurisdiction: Jurisdiction | None = None,
168
+ substantial_completion: date | str | None = None,
169
+ ) -> PaymentAnalysis:
170
+ jurisdiction = jurisdiction or Jurisdiction()
171
+ result = PaymentAnalysis(retainage_rate=decimalize(retainage_rate) if retainage_rate is not None else None)
172
+ if jurisdiction.is_kansas_private and result.retainage_rate is not None:
173
+ if result.retainage_rate > Decimal("10"):
174
+ result.issues.append(
175
+ make_issue(
176
+ self.pattern_id,
177
+ "Kansas retainage cap review",
178
+ "Kansas construction retainage appears above 10%; review K.S.A. 16-1904 cap/exception requirements.",
179
+ risk_level=RiskLevel.CRITICAL,
180
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
181
+ tags={"kansas", "retainage", "cap"},
182
+ )
183
+ )
184
+ elif result.retainage_rate > Decimal("5"):
185
+ result.issues.append(
186
+ make_issue(
187
+ self.pattern_id,
188
+ "Kansas retainage above default",
189
+ "Kansas retainage appears above 5%; confirm documented reason and statutory compliance.",
190
+ risk_level=RiskLevel.MEDIUM,
191
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
192
+ tags={"kansas", "retainage"},
193
+ )
194
+ )
195
+ completion = parse_date(substantial_completion)
196
+ if jurisdiction.is_kansas_private and completion:
197
+ result.deadlines.append(
198
+ Deadline(
199
+ "Kansas retainage release review",
200
+ completion + timedelta(days=30),
201
+ "K.S.A. 16-1904 remaining retainage due review after substantial completion on undisputed amounts",
202
+ self.pattern_id,
203
+ ReviewGate.ATTORNEY_REVIEW,
204
+ )
205
+ )
206
+ for raw in pay_apps:
207
+ pay_app = raw if isinstance(raw, PayApplication) else self._coerce_pay_app(raw)
208
+ if not pay_app.proper or pay_app.undisputed_amount <= 0:
209
+ continue
210
+ if jurisdiction.is_kansas_private:
211
+ owner_due = pay_app.submitted_date + timedelta(days=30)
212
+ result.deadlines.append(
213
+ Deadline(f"owner payment review {pay_app.id}", owner_due, "K.S.A. 16-1803 30-day owner payment review", self.pattern_id)
214
+ )
215
+ if pay_app.owner_payment_date:
216
+ sub_due = add_business_days(pay_app.owner_payment_date, 7)
217
+ result.deadlines.append(
218
+ Deadline(f"subcontractor payment review {pay_app.id}", sub_due, "K.S.A. 16-1803 seven-business-day subcontractor payment review", self.pattern_id)
219
+ )
220
+ if pay_app.contractor_payment_date and pay_app.contractor_payment_date > sub_due:
221
+ result.issues.append(
222
+ make_issue(
223
+ self.pattern_id,
224
+ "Kansas subcontractor payment late-risk",
225
+ f"Pay app {pay_app.id} appears paid after the seven-business-day Kansas review window.",
226
+ risk_level=RiskLevel.HIGH,
227
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
228
+ source_refs=pay_app.source_refs,
229
+ tags={"kansas", "prompt_pay"},
230
+ amount=pay_app.undisputed_amount,
231
+ deadline=sub_due,
232
+ )
233
+ )
234
+ return result
235
+
236
+ @staticmethod
237
+ def _coerce_pay_app(raw: Mapping[str, Any]) -> PayApplication:
238
+ return PayApplication(
239
+ id=str(raw.get("id") or raw.get("pay_app") or "pay_app"),
240
+ amount=decimalize(raw.get("amount")),
241
+ submitted_date=parse_date(raw.get("submitted_date") or raw.get("date")) or date.today(),
242
+ proper=bool(raw.get("proper", True)),
243
+ disputed_amount=decimalize(raw.get("disputed_amount")),
244
+ owner_payment_date=parse_date(raw.get("owner_payment_date")),
245
+ contractor_payment_date=parse_date(raw.get("contractor_payment_date") or raw.get("paid_date")),
246
+ source_refs=source_from_mapping(raw, "pay_app"),
247
+ )
248
+
249
+
250
+ @dataclass
251
+ class RightsCalendar:
252
+ pattern_id: str = "B111"
253
+ deadlines: list[Deadline] = field(default_factory=list)
254
+ issues: list[ConstructionIssue] = field(default_factory=list)
255
+ review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
256
+
257
+
258
+ class LienBondDeadlineCalendar:
259
+ """B111: compute review dates for lien/bond rights. Draft calendar only."""
260
+
261
+ pattern_id = "B111"
262
+
263
+ def build(
264
+ self,
265
+ *,
266
+ jurisdiction: Jurisdiction,
267
+ last_work_date: date | str | None,
268
+ unpaid: bool = True,
269
+ second_tier: bool = False,
270
+ ) -> RightsCalendar:
271
+ result = RightsCalendar()
272
+ last_work = parse_date(last_work_date)
273
+ if not last_work:
274
+ result.issues.append(
275
+ make_issue(
276
+ self.pattern_id,
277
+ "missing last-work date",
278
+ "Cannot compute lien/bond review dates without a supported last labor/material date.",
279
+ risk_level=RiskLevel.HIGH,
280
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
281
+ tags={"deadline", "missing_date"},
282
+ )
283
+ )
284
+ return result
285
+ if jurisdiction.is_federal:
286
+ if second_tier:
287
+ result.deadlines.append(
288
+ Deadline("Miller Act 90-day prime notice review", last_work + timedelta(days=90), "40 U.S.C. 3133(b)(2)", self.pattern_id)
289
+ )
290
+ if unpaid:
291
+ result.deadlines.append(
292
+ Deadline("Miller Act unpaid-90-day action review", last_work + timedelta(days=90), "40 U.S.C. 3133(b)(1)", self.pattern_id)
293
+ )
294
+ result.deadlines.append(
295
+ Deadline("Miller Act one-year suit review", last_work + timedelta(days=365), "40 U.S.C. 3133(b)(4)", self.pattern_id)
296
+ )
297
+ elif jurisdiction.is_kansas_private:
298
+ result.deadlines.append(
299
+ Deadline("Kansas lien filing review", add_months_approx(last_work, 3), "K.S.A. 60-1103(a)(1)", self.pattern_id)
300
+ )
301
+ if jurisdiction.project_use.value != "residential":
302
+ result.deadlines.append(
303
+ Deadline("Kansas nonresidential extension notice review", add_months_approx(last_work, 3), "K.S.A. 60-1103(e)", self.pattern_id)
304
+ )
305
+ result.deadlines.append(
306
+ Deadline("Kansas extended lien filing review", add_months_approx(last_work, 5), "K.S.A. 60-1103(e)", self.pattern_id)
307
+ )
308
+ else:
309
+ result.issues.append(
310
+ make_issue(
311
+ self.pattern_id,
312
+ "jurisdiction unknown for rights calendar",
313
+ "Project state/type is not sufficient to compute reliable lien/bond review dates.",
314
+ risk_level=RiskLevel.HIGH,
315
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
316
+ tags={"jurisdiction", "deadline"},
317
+ )
318
+ )
319
+ return result
320
+
321
+
322
+ class WaiverType(str, Enum):
323
+ CONDITIONAL_PROGRESS = "conditional_progress"
324
+ UNCONDITIONAL_PROGRESS = "unconditional_progress"
325
+ CONDITIONAL_FINAL = "conditional_final"
326
+ UNCONDITIONAL_FINAL = "unconditional_final"
327
+ UNKNOWN = "unknown"
328
+
329
+
330
+ @dataclass(frozen=True)
331
+ class LienWaiverReview:
332
+ pattern_id: str
333
+ waiver_type: WaiverType
334
+ covered_amount: Decimal
335
+ stop: bool
336
+ issues: tuple[ConstructionIssue, ...]
337
+ review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
338
+
339
+
340
+ class LienWaiverClassifier:
341
+ """B112: classify lien waivers and stop overbroad/unpaid final waivers."""
342
+
343
+ pattern_id = "B112"
344
+
345
+ def classify(
346
+ self,
347
+ waiver_text: str,
348
+ *,
349
+ covered_amount: Decimal | str | int = Decimal("0"),
350
+ cleared_payment_amount: Decimal | str | int = Decimal("0"),
351
+ open_retainage: Decimal | str | int = Decimal("0"),
352
+ open_claims: Decimal | str | int = Decimal("0"),
353
+ ) -> LienWaiverReview:
354
+ wtype = self._waiver_type(waiver_text)
355
+ covered = decimalize(covered_amount)
356
+ cleared = decimalize(cleared_payment_amount)
357
+ retainage = decimalize(open_retainage)
358
+ claims = decimalize(open_claims)
359
+ issues: list[ConstructionIssue] = []
360
+ stop = False
361
+ if "unconditional" in wtype.value and covered > cleared:
362
+ stop = True
363
+ issues.append(
364
+ make_issue(
365
+ self.pattern_id,
366
+ "unconditional waiver before cleared payment",
367
+ "Unconditional waiver covers more than cleared payment; do not sign/send without attorney/business approval.",
368
+ risk_level=RiskLevel.CRITICAL,
369
+ review_gate=ReviewGate.STOP_EXTERNAL_ACTION,
370
+ tags={"lien_waiver", "unconditional", "payment"},
371
+ amount=covered - cleared,
372
+ )
373
+ )
374
+ if "final" in wtype.value and (retainage > 0 or claims > 0):
375
+ stop = True
376
+ issues.append(
377
+ make_issue(
378
+ self.pattern_id,
379
+ "final waiver with open items",
380
+ "Final waiver appears to coexist with open retainage/claims/change orders.",
381
+ risk_level=RiskLevel.CRITICAL,
382
+ review_gate=ReviewGate.STOP_EXTERNAL_ACTION,
383
+ tags={"lien_waiver", "final", "open_items"},
384
+ amount=retainage + claims,
385
+ )
386
+ )
387
+ if wtype is WaiverType.UNKNOWN:
388
+ issues.append(
389
+ make_issue(
390
+ self.pattern_id,
391
+ "waiver type unknown",
392
+ "Could not classify waiver as conditional/unconditional and progress/final.",
393
+ risk_level=RiskLevel.HIGH,
394
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
395
+ tags={"lien_waiver", "unknown"},
396
+ )
397
+ )
398
+ return LienWaiverReview(self.pattern_id, wtype, covered, stop, tuple(issues))
399
+
400
+ @staticmethod
401
+ def _waiver_type(text: str) -> WaiverType:
402
+ lowered = text.lower()
403
+ conditional = "conditional" in lowered and "unconditional" not in lowered
404
+ unconditional = "unconditional" in lowered
405
+ final = "final" in lowered
406
+ progress = "progress" in lowered or "partial" in lowered
407
+ if conditional and final:
408
+ return WaiverType.CONDITIONAL_FINAL
409
+ if unconditional and final:
410
+ return WaiverType.UNCONDITIONAL_FINAL
411
+ if conditional and progress:
412
+ return WaiverType.CONDITIONAL_PROGRESS
413
+ if unconditional and progress:
414
+ return WaiverType.UNCONDITIONAL_PROGRESS
415
+ if conditional:
416
+ return WaiverType.CONDITIONAL_PROGRESS
417
+ if unconditional:
418
+ return WaiverType.UNCONDITIONAL_PROGRESS
419
+ return WaiverType.UNKNOWN
420
+
421
+
422
+ class ContingentPaymentType(str, Enum):
423
+ PAY_IF_PAID = "pay_if_paid_candidate"
424
+ PAY_WHEN_PAID = "pay_when_paid_candidate"
425
+ ORDINARY = "ordinary_payment_term"
426
+ UNKNOWN = "unknown"
427
+
428
+
429
+ @dataclass(frozen=True)
430
+ class ContingentPaymentReview:
431
+ pattern_id: str
432
+ payment_type: ContingentPaymentType
433
+ notes: tuple[str, ...]
434
+ issues: tuple[ConstructionIssue, ...]
435
+ review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
436
+
437
+
438
+ class ContingentPaymentRiskGate:
439
+ """B113: distinguish pay-if-paid/pay-when-paid candidates with jurisdiction overlay."""
440
+
441
+ pattern_id = "B113"
442
+
443
+ def analyze(self, clause_text: str, *, jurisdiction: Jurisdiction | None = None) -> ContingentPaymentReview:
444
+ jurisdiction = jurisdiction or Jurisdiction()
445
+ lowered = clause_text.lower()
446
+ if "condition precedent" in lowered or "only if" in lowered and "owner" in lowered:
447
+ ptype = ContingentPaymentType.PAY_IF_PAID
448
+ elif "receipt" in lowered and "owner" in lowered or "paid by owner" in lowered:
449
+ ptype = ContingentPaymentType.PAY_WHEN_PAID
450
+ elif "payment" in lowered:
451
+ ptype = ContingentPaymentType.ORDINARY
452
+ else:
453
+ ptype = ContingentPaymentType.UNKNOWN
454
+ notes: list[str] = []
455
+ issues: list[ConstructionIssue] = []
456
+ if ptype in {ContingentPaymentType.PAY_IF_PAID, ContingentPaymentType.PAY_WHEN_PAID}:
457
+ issues.append(
458
+ make_issue(
459
+ self.pattern_id,
460
+ "contingent payment clause",
461
+ f"Clause classified as {ptype.value}; enforceability and payment-right impact require attorney review.",
462
+ risk_level=RiskLevel.HIGH if ptype is ContingentPaymentType.PAY_IF_PAID else RiskLevel.MEDIUM,
463
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
464
+ source_refs=(SourceRef("payment_clause", quote=clause_text),),
465
+ tags={"payment", "contingent", ptype.value},
466
+ )
467
+ )
468
+ if jurisdiction.is_kansas_private and ptype in {ContingentPaymentType.PAY_IF_PAID, ContingentPaymentType.PAY_WHEN_PAID}:
469
+ notes.append("Kansas private construction overlay: K.S.A. 16-1803(c) says contingent payment is no defense to mechanic's lien/bond enforcement.")
470
+ return ContingentPaymentReview(self.pattern_id, ptype, tuple(notes), tuple(issues))