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,496 @@
1
+ """Construction document, scope, flowdown, and dispute patterns B106-B108/B124-B126."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Iterable, Mapping
8
+
9
+ from .common import (
10
+ ConstructionIssue,
11
+ ReviewGate,
12
+ RiskLevel,
13
+ SourceRef,
14
+ contains_any,
15
+ make_issue,
16
+ normalize_label,
17
+ normalize_space,
18
+ source_from_mapping,
19
+ )
20
+
21
+
22
+ class ScopeStatus(str, Enum):
23
+ INCLUDED = "included"
24
+ EXCLUDED = "excluded"
25
+ BY_OTHERS = "by_others"
26
+ ALLOWANCE = "allowance"
27
+ UNIT_PRICE = "unit_price"
28
+ AMBIGUOUS = "ambiguous"
29
+ RISK = "scope_risk"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ContractDocument:
34
+ id: str
35
+ name: str
36
+ document_type: str
37
+ text: str = ""
38
+ references: tuple[str, ...] = ()
39
+ precedence: int | None = None
40
+ source_refs: tuple[SourceRef, ...] = ()
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class DocumentEdge:
45
+ source_id: str
46
+ target_id: str
47
+ relation: str
48
+
49
+
50
+ @dataclass
51
+ class ContractDocumentGraph:
52
+ pattern_id: str = "B106"
53
+ documents: dict[str, ContractDocument] = field(default_factory=dict)
54
+ edges: list[DocumentEdge] = field(default_factory=list)
55
+ missing_references: list[str] = field(default_factory=list)
56
+ precedence_map: dict[str, int] = field(default_factory=dict)
57
+ conflicts: list[ConstructionIssue] = field(default_factory=list)
58
+ review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
59
+
60
+ @property
61
+ def requires_human_review(self) -> bool:
62
+ return bool(self.missing_references or self.conflicts)
63
+
64
+
65
+ class ContractDocumentGraphCompiler:
66
+ """B106: build a contract document graph and precedence/conflict map."""
67
+
68
+ pattern_id = "B106"
69
+
70
+ def compile(self, documents: Iterable[ContractDocument | Mapping[str, Any]]) -> ContractDocumentGraph:
71
+ parsed = {doc.id: doc for doc in (self._coerce_document(item) for item in documents)}
72
+ graph = ContractDocumentGraph(documents=parsed)
73
+ for doc in parsed.values():
74
+ if doc.precedence is not None:
75
+ graph.precedence_map[doc.id] = doc.precedence
76
+ for ref in doc.references:
77
+ relation = self._relation_for_reference(ref)
78
+ if ref in parsed:
79
+ graph.edges.append(DocumentEdge(doc.id, ref, relation))
80
+ else:
81
+ graph.missing_references.append(ref)
82
+ graph.conflicts.append(
83
+ make_issue(
84
+ self.pattern_id,
85
+ "missing upstream document",
86
+ f"{doc.name} references {ref}, but that document was not supplied.",
87
+ risk_level=RiskLevel.HIGH,
88
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
89
+ source_refs=doc.source_refs,
90
+ tags={"missing_document", relation},
91
+ )
92
+ )
93
+ if len({value for value in graph.precedence_map.values()}) != len(graph.precedence_map):
94
+ graph.conflicts.append(
95
+ make_issue(
96
+ self.pattern_id,
97
+ "duplicate precedence rank",
98
+ "Multiple contract documents share the same precedence rank; confirm order of precedence.",
99
+ risk_level=RiskLevel.MEDIUM,
100
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
101
+ tags={"precedence"},
102
+ )
103
+ )
104
+ if not graph.precedence_map and len(parsed) > 1:
105
+ graph.conflicts.append(
106
+ make_issue(
107
+ self.pattern_id,
108
+ "precedence unknown",
109
+ "Multiple contract documents were supplied but no order-of-precedence map was found.",
110
+ risk_level=RiskLevel.HIGH,
111
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
112
+ tags={"precedence", "conflict"},
113
+ )
114
+ )
115
+ return graph
116
+
117
+ @staticmethod
118
+ def _coerce_document(item: ContractDocument | Mapping[str, Any]) -> ContractDocument:
119
+ if isinstance(item, ContractDocument):
120
+ return item
121
+ refs = item.get("source_refs") or ()
122
+ source_refs = tuple(ref for ref in refs if isinstance(ref, SourceRef)) or (
123
+ SourceRef(str(item.get("name") or item.get("id") or "document"), quote=item.get("text")),
124
+ )
125
+ return ContractDocument(
126
+ id=str(item.get("id") or normalize_label(str(item.get("name") or "document"))),
127
+ name=str(item.get("name") or item.get("id") or "document"),
128
+ document_type=str(item.get("document_type") or item.get("type") or "unknown"),
129
+ text=str(item.get("text") or ""),
130
+ references=tuple(str(ref) for ref in item.get("references", ())),
131
+ precedence=item.get("precedence"),
132
+ source_refs=source_refs,
133
+ )
134
+
135
+ @staticmethod
136
+ def _relation_for_reference(ref: str) -> str:
137
+ lowered = ref.lower()
138
+ if "prime" in lowered or "owner" in lowered:
139
+ return "flow_down"
140
+ if "addendum" in lowered or "change" in lowered or "mod" in lowered:
141
+ return "modifies"
142
+ return "incorporates_by_reference"
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class ScopeAtom:
147
+ item: str
148
+ status: ScopeStatus
149
+ source_refs: tuple[SourceRef, ...] = ()
150
+ notes: str = ""
151
+
152
+
153
+ @dataclass
154
+ class ScopeMatrix:
155
+ pattern_id: str = "B107"
156
+ atoms: list[ScopeAtom] = field(default_factory=list)
157
+ issues: list[ConstructionIssue] = field(default_factory=list)
158
+ review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
159
+
160
+ @property
161
+ def ambiguous_items(self) -> list[ScopeAtom]:
162
+ return [atom for atom in self.atoms if atom.status in {ScopeStatus.AMBIGUOUS, ScopeStatus.RISK}]
163
+
164
+
165
+ class ScopeMatrixBuilder:
166
+ """B107: classify inclusions/exclusions/by-others/scope-risk atoms."""
167
+
168
+ pattern_id = "B107"
169
+ _status_terms = {
170
+ ScopeStatus.EXCLUDED: ("exclude", "excluded", "not included", "exclusion"),
171
+ ScopeStatus.BY_OTHERS: ("by others", "owner provided", "gc provided", "others to provide"),
172
+ ScopeStatus.ALLOWANCE: ("allowance",),
173
+ ScopeStatus.UNIT_PRICE: ("unit price", "unit-price", "unit pricing"),
174
+ ScopeStatus.INCLUDED: ("include", "included", "provide", "furnish", "install"),
175
+ }
176
+
177
+ def build(
178
+ self,
179
+ scope_items: Iterable[Mapping[str, Any] | ScopeAtom],
180
+ drawing_items: Iterable[str] = (),
181
+ *,
182
+ precedence_supports_exclusion: bool = False,
183
+ ) -> ScopeMatrix:
184
+ matrix = ScopeMatrix()
185
+ seen: set[str] = set()
186
+ for item in scope_items:
187
+ atom = item if isinstance(item, ScopeAtom) else self._coerce_atom(item)
188
+ seen.add(normalize_label(atom.item))
189
+ matrix.atoms.append(atom)
190
+ if atom.status is ScopeStatus.AMBIGUOUS:
191
+ matrix.issues.append(
192
+ make_issue(
193
+ self.pattern_id,
194
+ "ambiguous scope item",
195
+ f"Scope item '{atom.item}' is not clearly included, excluded, or by others.",
196
+ risk_level=RiskLevel.MEDIUM,
197
+ review_gate=ReviewGate.BUSINESS_REVIEW,
198
+ source_refs=atom.source_refs,
199
+ tags={"scope", "ambiguous"},
200
+ )
201
+ )
202
+ excluded = {normalize_label(atom.item) for atom in matrix.atoms if atom.status in {ScopeStatus.EXCLUDED, ScopeStatus.BY_OTHERS}}
203
+ for drawing_item in drawing_items:
204
+ key = normalize_label(drawing_item)
205
+ if key in seen:
206
+ continue
207
+ status = ScopeStatus.EXCLUDED if precedence_supports_exclusion and key in excluded else ScopeStatus.RISK
208
+ atom = ScopeAtom(drawing_item, status, notes="mentioned in drawings/specs but not priced in scope")
209
+ matrix.atoms.append(atom)
210
+ matrix.issues.append(
211
+ make_issue(
212
+ self.pattern_id,
213
+ "scope risk",
214
+ f"'{drawing_item}' appears in drawings/specs but not in the priced scope matrix.",
215
+ risk_level=RiskLevel.HIGH,
216
+ review_gate=ReviewGate.BUSINESS_REVIEW,
217
+ tags={"scope", "drawing_reference"},
218
+ )
219
+ )
220
+ if not any("permit" in atom.item.lower() for atom in matrix.atoms):
221
+ matrix.issues.append(
222
+ make_issue(
223
+ self.pattern_id,
224
+ "permit responsibility missing",
225
+ "Permit responsibility is not clearly assigned in the scope matrix.",
226
+ risk_level=RiskLevel.MEDIUM,
227
+ review_gate=ReviewGate.BUSINESS_REVIEW,
228
+ tags={"scope", "permits"},
229
+ )
230
+ )
231
+ return matrix
232
+
233
+ def _coerce_atom(self, item: Mapping[str, Any]) -> ScopeAtom:
234
+ text = normalize_space(str(item.get("item") or item.get("description") or item.get("text") or ""))
235
+ explicit = item.get("status")
236
+ status = ScopeStatus(str(explicit)) if explicit in ScopeStatus._value2member_map_ else self._infer_status(text)
237
+ return ScopeAtom(text, status, source_refs=source_from_mapping(item), notes=str(item.get("notes") or ""))
238
+
239
+ def _infer_status(self, text: str) -> ScopeStatus:
240
+ lowered = text.lower()
241
+ for status, terms in self._status_terms.items():
242
+ if any(term in lowered for term in terms):
243
+ return status
244
+ return ScopeStatus.AMBIGUOUS
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class FlowDownObligation:
249
+ category: str
250
+ upstream_clause: str
251
+ subcontractor_obligation: str
252
+ source_refs: tuple[SourceRef, ...] = ()
253
+ recovery_limited: bool = False
254
+
255
+
256
+ @dataclass
257
+ class FlowDownResult:
258
+ pattern_id: str = "B108"
259
+ obligations: list[FlowDownObligation] = field(default_factory=list)
260
+ issues: list[ConstructionIssue] = field(default_factory=list)
261
+ missing_upstream_documents: list[str] = field(default_factory=list)
262
+ review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
263
+
264
+
265
+ class FlowDownRiskMapper:
266
+ """B108: map upstream obligations incorporated by flow-down language."""
267
+
268
+ pattern_id = "B108"
269
+ _category_terms = {
270
+ "payment": ("payment", "paid", "retainage"),
271
+ "schedule": ("schedule", "milestone", "delay", "time"),
272
+ "changes": ("change", "extra work", "directive"),
273
+ "claims": ("claim", "notice", "dispute"),
274
+ "insurance": ("insurance", "additional insured", "bond"),
275
+ "safety": ("safety", "osha", "hazard"),
276
+ "indemnity": ("indemn", "hold harmless"),
277
+ }
278
+
279
+ def map(
280
+ self,
281
+ subcontract_text: str,
282
+ upstream_clauses: Iterable[Mapping[str, Any]] | None = None,
283
+ required_upstream_documents: Iterable[str] = (),
284
+ ) -> FlowDownResult:
285
+ result = FlowDownResult()
286
+ has_flowdown = contains_any(subcontract_text, ("flow down", "flow-down", "bound to contractor", "same obligations", "prime contract"))
287
+ clauses = list(upstream_clauses or [])
288
+ if has_flowdown and not clauses:
289
+ result.missing_upstream_documents.extend(required_upstream_documents or ["prime contract"])
290
+ result.issues.append(
291
+ make_issue(
292
+ self.pattern_id,
293
+ "missing upstream document",
294
+ "Subcontract contains flow-down language but upstream contract documents were not supplied.",
295
+ risk_level=RiskLevel.HIGH,
296
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
297
+ tags={"flowdown", "missing_document"},
298
+ )
299
+ )
300
+ return result
301
+ if not has_flowdown:
302
+ return result
303
+ for clause in clauses:
304
+ text = normalize_space(str(clause.get("text") or clause.get("clause") or ""))
305
+ category = self._category(text)
306
+ recovery_limited = contains_any(text, ("condition precedent", "only to the extent", "no damages", "owner pays"))
307
+ obligation = FlowDownObligation(
308
+ category=category,
309
+ upstream_clause=str(clause.get("id") or clause.get("name") or category),
310
+ subcontractor_obligation=text,
311
+ source_refs=source_from_mapping(clause),
312
+ recovery_limited=recovery_limited,
313
+ )
314
+ result.obligations.append(obligation)
315
+ if recovery_limited:
316
+ result.issues.append(
317
+ make_issue(
318
+ self.pattern_id,
319
+ "downstream recovery limit",
320
+ f"Upstream {category} clause may limit downstream recovery; attorney review required.",
321
+ risk_level=RiskLevel.HIGH,
322
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
323
+ source_refs=obligation.source_refs,
324
+ tags={"flowdown", "recovery_limit", category},
325
+ )
326
+ )
327
+ return result
328
+
329
+ def _category(self, text: str) -> str:
330
+ for category, terms in self._category_terms.items():
331
+ if contains_any(text, terms):
332
+ return category
333
+ return "general"
334
+
335
+
336
+ @dataclass(frozen=True)
337
+ class DelegatedDesignItem:
338
+ item: str
339
+ requires_professional_review: bool
340
+ source_refs: tuple[SourceRef, ...] = ()
341
+
342
+
343
+ @dataclass
344
+ class DelegatedDesignResult:
345
+ pattern_id: str = "B124"
346
+ items: list[DelegatedDesignItem] = field(default_factory=list)
347
+ issues: list[ConstructionIssue] = field(default_factory=list)
348
+ review_gate: ReviewGate = ReviewGate.ATTORNEY_REVIEW
349
+
350
+
351
+ class DelegatedDesignGate:
352
+ """B124: detect design-build/delegated design and professional-liability gaps."""
353
+
354
+ pattern_id = "B124"
355
+
356
+ def analyze(self, clauses: Iterable[Mapping[str, Any]], *, has_professional_liability: bool = False) -> DelegatedDesignResult:
357
+ result = DelegatedDesignResult()
358
+ for clause in clauses:
359
+ text = normalize_space(str(clause.get("text") or ""))
360
+ refs = source_from_mapping(clause)
361
+ design_term = contains_any(text, ("delegated design", "shall design", "signed and sealed", "performance requirements", "calculations"))
362
+ if not design_term:
363
+ continue
364
+ item = DelegatedDesignItem(str(clause.get("id") or clause.get("name") or "design obligation"), True, refs)
365
+ result.items.append(item)
366
+ if not has_professional_liability:
367
+ result.issues.append(
368
+ make_issue(
369
+ self.pattern_id,
370
+ "delegated design coverage review",
371
+ "Design/delegated-design language found but professional-liability coverage was not evidenced.",
372
+ risk_level=RiskLevel.HIGH,
373
+ review_gate=ReviewGate.INSURANCE_REVIEW,
374
+ source_refs=refs,
375
+ tags={"delegated_design", "insurance"},
376
+ )
377
+ )
378
+ if "performance requirements" in text.lower() and not contains_any(text, ("criteria", "basis of design", "load", "capacity")):
379
+ result.issues.append(
380
+ make_issue(
381
+ self.pattern_id,
382
+ "performance criteria unclear",
383
+ "Performance-spec language appears without clear measurable criteria; RFI/attorney review required.",
384
+ risk_level=RiskLevel.MEDIUM,
385
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
386
+ source_refs=refs,
387
+ tags={"delegated_design", "rfi"},
388
+ )
389
+ )
390
+ return result
391
+
392
+
393
+ @dataclass(frozen=True)
394
+ class LowerTierGap:
395
+ requirement: str
396
+ upstream_value: str
397
+ lower_tier_value: str | None
398
+ message: str
399
+
400
+
401
+ @dataclass
402
+ class LowerTierFlowdownResult:
403
+ pattern_id: str = "B125"
404
+ gaps: list[LowerTierGap] = field(default_factory=list)
405
+ issues: list[ConstructionIssue] = field(default_factory=list)
406
+ review_gate: ReviewGate = ReviewGate.BUSINESS_REVIEW
407
+
408
+
409
+ class LowerTierFlowdownPack:
410
+ """B125: compare upstream requirements to lower-tier subcontract/supplier terms."""
411
+
412
+ pattern_id = "B125"
413
+
414
+ def compare(self, upstream_requirements: Mapping[str, Any], lower_tier_terms: Mapping[str, Any]) -> LowerTierFlowdownResult:
415
+ result = LowerTierFlowdownResult()
416
+ for key, upstream_value in upstream_requirements.items():
417
+ lower_value = lower_tier_terms.get(key)
418
+ if lower_value == upstream_value:
419
+ continue
420
+ if key.endswith("_deadline_days") and lower_value is not None:
421
+ try:
422
+ if int(lower_value) <= int(upstream_value):
423
+ continue
424
+ except (TypeError, ValueError):
425
+ pass
426
+ gap = LowerTierGap(key, str(upstream_value), None if lower_value is None else str(lower_value), f"Lower-tier {key} does not satisfy upstream requirement.")
427
+ result.gaps.append(gap)
428
+ result.issues.append(
429
+ make_issue(
430
+ self.pattern_id,
431
+ "lower-tier mismatch",
432
+ gap.message,
433
+ risk_level=RiskLevel.HIGH if "insurance" in key or "deadline" in key else RiskLevel.MEDIUM,
434
+ review_gate=ReviewGate.ATTORNEY_REVIEW if "indemn" in key or "venue" in key else ReviewGate.BUSINESS_REVIEW,
435
+ tags={"lower_tier", normalize_label(key)},
436
+ )
437
+ )
438
+ return result
439
+
440
+
441
+ @dataclass(frozen=True)
442
+ class DisputeProtocol:
443
+ path: tuple[str, ...]
444
+ forum: str | None
445
+ notice_recipients: tuple[str, ...]
446
+ email_allowed: bool
447
+ issues: tuple[ConstructionIssue, ...] = ()
448
+
449
+
450
+ class DisputeProtocolExtractor:
451
+ """B126: extract dispute path, forum, and notice protocol."""
452
+
453
+ pattern_id = "B126"
454
+
455
+ def extract(self, dispute_clause: str, notice_clause: str = "") -> DisputeProtocol:
456
+ text = f"{dispute_clause}\n{notice_clause}"
457
+ lowered = text.lower()
458
+ path: list[str] = []
459
+ for step in ("negotiation", "initial decision", "mediation", "arbitration", "litigation"):
460
+ if step in lowered:
461
+ path.append(step)
462
+ if not path and "court" in lowered:
463
+ path.append("litigation")
464
+ forum = None
465
+ if "american arbitration association" in lowered or "aaa" in lowered:
466
+ forum = "AAA"
467
+ elif "district court" in lowered:
468
+ forum = "district court"
469
+ elif "state court" in lowered:
470
+ forum = "state court"
471
+ recipients = tuple(part.strip() for part in notice_clause.split(";") if "@" in part or "attn" in part.lower())
472
+ email_allowed = "email" in lowered or "electronic" in lowered
473
+ issues: list[ConstructionIssue] = []
474
+ if "arbitration" in path and forum is None:
475
+ issues.append(
476
+ make_issue(
477
+ self.pattern_id,
478
+ "arbitration forum missing",
479
+ "Arbitration is selected but no forum/rules were identified.",
480
+ risk_level=RiskLevel.MEDIUM,
481
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
482
+ tags={"dispute", "arbitration"},
483
+ )
484
+ )
485
+ if email_allowed and not recipients:
486
+ issues.append(
487
+ make_issue(
488
+ self.pattern_id,
489
+ "email notice protocol incomplete",
490
+ "Electronic notice appears allowed but recipient/protocol is unclear.",
491
+ risk_level=RiskLevel.MEDIUM,
492
+ review_gate=ReviewGate.ATTORNEY_REVIEW,
493
+ tags={"notice", "email"},
494
+ )
495
+ )
496
+ return DisputeProtocol(tuple(path), forum, recipients, email_allowed, tuple(issues))