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,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))
|