crprotocol 2.0.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.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Stitch algorithm — echo detection, content-aware stitching, validation (§4.8, §04 §3.4)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContentBoundary(str, Enum):
|
|
13
|
+
"""Content type for boundary-aware stitching."""
|
|
14
|
+
|
|
15
|
+
PROSE = "prose"
|
|
16
|
+
CODE = "code"
|
|
17
|
+
MARKDOWN = "markdown"
|
|
18
|
+
STRUCTURED = "structured"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class StitchResult:
|
|
23
|
+
"""Result of stitching two outputs together."""
|
|
24
|
+
|
|
25
|
+
text: str
|
|
26
|
+
echo_removed: int # chars of echo removed
|
|
27
|
+
boundary_type: ContentBoundary
|
|
28
|
+
bridge_inserted: bool
|
|
29
|
+
trimmed_fragments: list[str] # never silently discard
|
|
30
|
+
validation_warnings: list[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class StitchConfig:
|
|
35
|
+
"""Configuration for the stitch algorithm."""
|
|
36
|
+
|
|
37
|
+
echo_window: int = 2000 # chars to compare for echo
|
|
38
|
+
min_echo_length: int = 20 # minimum echo to detect
|
|
39
|
+
semantic_echo_threshold: float = 0.85
|
|
40
|
+
max_bridge_tokens: int = 50
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── LCS-based echo detection (§04 §3.4) ──────────────────────────
|
|
44
|
+
|
|
45
|
+
def _longest_common_suffix_prefix(prior_tail: str, continuation_head: str) -> str:
|
|
46
|
+
"""Find the longest common substring where prior ends and continuation starts.
|
|
47
|
+
|
|
48
|
+
Uses a suffix-of-prior / prefix-of-continuation match approach.
|
|
49
|
+
"""
|
|
50
|
+
if not prior_tail or not continuation_head:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
best = ""
|
|
54
|
+
# Check progressively longer overlaps
|
|
55
|
+
max_len = min(len(prior_tail), len(continuation_head))
|
|
56
|
+
for length in range(1, max_len + 1):
|
|
57
|
+
if prior_tail[-length:] == continuation_head[:length]:
|
|
58
|
+
best = prior_tail[-length:]
|
|
59
|
+
|
|
60
|
+
return best
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _lcs_dynamic(a: str, b: str) -> str:
|
|
64
|
+
"""Longest Common Substring via dynamic programming.
|
|
65
|
+
|
|
66
|
+
Used for echo detection between last 2000 chars of prior
|
|
67
|
+
and first 2000 chars of continuation.
|
|
68
|
+
"""
|
|
69
|
+
if not a or not b:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
m, n = len(a), len(b)
|
|
73
|
+
# Optimize: only need current and previous row
|
|
74
|
+
prev = [0] * (n + 1)
|
|
75
|
+
curr = [0] * (n + 1)
|
|
76
|
+
longest = 0
|
|
77
|
+
end_pos = 0
|
|
78
|
+
|
|
79
|
+
for i in range(1, m + 1):
|
|
80
|
+
for j in range(1, n + 1):
|
|
81
|
+
if a[i - 1] == b[j - 1]:
|
|
82
|
+
curr[j] = prev[j - 1] + 1
|
|
83
|
+
if curr[j] > longest:
|
|
84
|
+
longest = curr[j]
|
|
85
|
+
end_pos = i
|
|
86
|
+
else:
|
|
87
|
+
curr[j] = 0
|
|
88
|
+
prev, curr = curr, [0] * (n + 1)
|
|
89
|
+
|
|
90
|
+
if longest == 0:
|
|
91
|
+
return ""
|
|
92
|
+
return a[end_pos - longest:end_pos]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def detect_echo(
|
|
96
|
+
prior: str,
|
|
97
|
+
continuation: str,
|
|
98
|
+
config: StitchConfig | None = None,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Detect echoed content at the start of continuation.
|
|
101
|
+
|
|
102
|
+
Strategy:
|
|
103
|
+
1. Suffix-prefix overlap (exact boundary echo)
|
|
104
|
+
2. LCS on last/first 2000 chars (partial echo)
|
|
105
|
+
"""
|
|
106
|
+
cfg = config or StitchConfig()
|
|
107
|
+
window = cfg.echo_window
|
|
108
|
+
|
|
109
|
+
prior_tail = prior[-window:] if len(prior) > window else prior
|
|
110
|
+
cont_head = continuation[:window] if len(continuation) > window else continuation
|
|
111
|
+
|
|
112
|
+
# Strategy 1: exact suffix-prefix overlap
|
|
113
|
+
overlap = _longest_common_suffix_prefix(prior_tail, cont_head)
|
|
114
|
+
if len(overlap) >= cfg.min_echo_length:
|
|
115
|
+
return overlap
|
|
116
|
+
|
|
117
|
+
# Strategy 2: LCS
|
|
118
|
+
lcs = _lcs_dynamic(prior_tail, cont_head)
|
|
119
|
+
if len(lcs) >= cfg.min_echo_length:
|
|
120
|
+
# Only count as echo if it appears at/near the start of continuation
|
|
121
|
+
pos = continuation.find(lcs)
|
|
122
|
+
if pos >= 0 and pos < cfg.echo_window // 2:
|
|
123
|
+
return lcs
|
|
124
|
+
|
|
125
|
+
# Strategy 3: Semantic echo — word-overlap similarity (§5E.5)
|
|
126
|
+
prior_words = set(prior_tail.lower().split())
|
|
127
|
+
cont_words = set(cont_head.lower().split())
|
|
128
|
+
if prior_words and cont_words:
|
|
129
|
+
overlap = len(prior_words & cont_words) / min(len(prior_words), len(cont_words)) # type: ignore[assignment]
|
|
130
|
+
if overlap >= cfg.semantic_echo_threshold: # type: ignore[operator]
|
|
131
|
+
# Find the overlapping segment in continuation
|
|
132
|
+
# Use first sentence that significantly overlaps
|
|
133
|
+
cont_sentences = [s.strip() for s in cont_head.split(".") if len(s.strip()) >= cfg.min_echo_length]
|
|
134
|
+
for sent in cont_sentences[:5]:
|
|
135
|
+
sent_words = set(sent.lower().split())
|
|
136
|
+
if prior_words and sent_words:
|
|
137
|
+
s_overlap = len(prior_words & sent_words) / max(1, len(sent_words))
|
|
138
|
+
if s_overlap >= 0.7:
|
|
139
|
+
return sent
|
|
140
|
+
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── Content-type boundary detection ──────────────────────────────
|
|
145
|
+
|
|
146
|
+
def _detect_boundary_type(text: str) -> ContentBoundary:
|
|
147
|
+
"""Detect content type from the tail of text."""
|
|
148
|
+
tail = text[-500:] if len(text) > 500 else text
|
|
149
|
+
|
|
150
|
+
# Code: triple backticks, indentation patterns
|
|
151
|
+
if "```" in tail or re.search(r"^\s{4,}\S", tail, re.MULTILINE):
|
|
152
|
+
return ContentBoundary.CODE
|
|
153
|
+
|
|
154
|
+
# Markdown: headings, lists, links
|
|
155
|
+
if re.search(r"^#{1,6}\s", tail, re.MULTILINE) or re.search(r"^\s*[-*]\s", tail, re.MULTILINE):
|
|
156
|
+
return ContentBoundary.MARKDOWN
|
|
157
|
+
|
|
158
|
+
# Structured: JSON/YAML-like
|
|
159
|
+
if re.search(r"[{}\[\]]", tail) or re.search(r"^\s+\w+:", tail, re.MULTILINE):
|
|
160
|
+
return ContentBoundary.STRUCTURED
|
|
161
|
+
|
|
162
|
+
return ContentBoundary.PROSE
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _find_clean_boundary(text: str, boundary_type: ContentBoundary) -> int:
|
|
166
|
+
"""Find the best position to end the prior output for clean stitching."""
|
|
167
|
+
if boundary_type == ContentBoundary.PROSE:
|
|
168
|
+
# End at last sentence boundary
|
|
169
|
+
for pattern in [r"[.!?]\s*$", r"[.!?]\s+\S", r"\n\n", r"\n"]:
|
|
170
|
+
match = list(re.finditer(pattern, text[-500:]))
|
|
171
|
+
if match:
|
|
172
|
+
return len(text) - 500 + match[-1].end()
|
|
173
|
+
return len(text)
|
|
174
|
+
|
|
175
|
+
elif boundary_type == ContentBoundary.CODE:
|
|
176
|
+
# End at last complete line or code block boundary
|
|
177
|
+
idx = text.rfind("\n```\n")
|
|
178
|
+
if idx > len(text) - 500:
|
|
179
|
+
return idx + 4
|
|
180
|
+
idx = text.rfind("\n\n")
|
|
181
|
+
if idx > len(text) - 200:
|
|
182
|
+
return idx + 2
|
|
183
|
+
return len(text)
|
|
184
|
+
|
|
185
|
+
elif boundary_type == ContentBoundary.MARKDOWN:
|
|
186
|
+
# End at last double-newline or heading
|
|
187
|
+
idx = text.rfind("\n\n")
|
|
188
|
+
if idx > len(text) - 300:
|
|
189
|
+
return idx + 2
|
|
190
|
+
return len(text)
|
|
191
|
+
|
|
192
|
+
else: # STRUCTURED
|
|
193
|
+
# End at last complete line
|
|
194
|
+
idx = text.rfind("\n")
|
|
195
|
+
if idx > len(text) - 200:
|
|
196
|
+
return idx + 1
|
|
197
|
+
return len(text)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ── Bridge insertion (no-echo fallback) ──────────────────────────
|
|
201
|
+
|
|
202
|
+
def _build_bridge(boundary_type: ContentBoundary, structural_hint: str = "") -> str:
|
|
203
|
+
"""Build a small bridge text for no-echo stitching."""
|
|
204
|
+
if boundary_type == ContentBoundary.CODE:
|
|
205
|
+
return "\n\n"
|
|
206
|
+
elif boundary_type == ContentBoundary.MARKDOWN:
|
|
207
|
+
return "\n\n"
|
|
208
|
+
elif boundary_type == ContentBoundary.STRUCTURED:
|
|
209
|
+
return "\n"
|
|
210
|
+
else:
|
|
211
|
+
return "\n\n"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ── Post-stitch validation (§04 §3.4) ────────────────────────────
|
|
215
|
+
|
|
216
|
+
def _validate_stitch(text: str) -> list[str]:
|
|
217
|
+
"""Post-stitch validation: duplicates, brackets, heading hierarchy."""
|
|
218
|
+
warnings: list[str] = []
|
|
219
|
+
|
|
220
|
+
# Duplicate paragraph detection (exact match of 50+ char paragraphs)
|
|
221
|
+
paragraphs = [p.strip() for p in text.split("\n\n") if len(p.strip()) > 50]
|
|
222
|
+
seen: set[str] = set()
|
|
223
|
+
for p in paragraphs:
|
|
224
|
+
if p in seen:
|
|
225
|
+
warnings.append(f"duplicate_paragraph: {p[:60]}...")
|
|
226
|
+
seen.add(p)
|
|
227
|
+
|
|
228
|
+
# Bracket balance
|
|
229
|
+
for open_c, close_c, name in [("(", ")", "paren"), ("[", "]", "bracket"), ("{", "}", "brace")]:
|
|
230
|
+
balance = text.count(open_c) - text.count(close_c)
|
|
231
|
+
if balance != 0:
|
|
232
|
+
warnings.append(f"unbalanced_{name}: {'+' if balance > 0 else ''}{balance}")
|
|
233
|
+
|
|
234
|
+
# Heading hierarchy (no skipped levels: # → ### without ##)
|
|
235
|
+
headings = re.findall(r"^(#{1,6})\s", text, re.MULTILINE)
|
|
236
|
+
if headings:
|
|
237
|
+
prev_level = 0
|
|
238
|
+
for h in headings:
|
|
239
|
+
level = len(h)
|
|
240
|
+
if prev_level > 0 and level > prev_level + 1:
|
|
241
|
+
warnings.append(f"heading_skip: h{prev_level}→h{level}")
|
|
242
|
+
prev_level = level
|
|
243
|
+
|
|
244
|
+
return warnings
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _dedup_sections(text: str) -> tuple[str, int]:
|
|
248
|
+
"""Remove duplicate sections by heading number (GAP C fix).
|
|
249
|
+
|
|
250
|
+
When the same numbered section (e.g. '## 7. Data Protection') appears
|
|
251
|
+
multiple times, keep the LAST occurrence (most recent/complete) and
|
|
252
|
+
remove earlier duplicates.
|
|
253
|
+
|
|
254
|
+
Returns (deduped_text, sections_removed).
|
|
255
|
+
"""
|
|
256
|
+
# Split text into section blocks keyed by heading number
|
|
257
|
+
section_pattern = re.compile(r"^(#{1,6})\s+(\d{1,3})\.\s+", re.MULTILINE)
|
|
258
|
+
matches = list(section_pattern.finditer(text))
|
|
259
|
+
|
|
260
|
+
if not matches:
|
|
261
|
+
return text, 0
|
|
262
|
+
|
|
263
|
+
# Track section number → list of (start, end) positions
|
|
264
|
+
section_spans: dict[int, list[tuple[int, int]]] = {}
|
|
265
|
+
for i, m in enumerate(matches):
|
|
266
|
+
sec_num = int(m.group(2))
|
|
267
|
+
start = m.start()
|
|
268
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
269
|
+
section_spans.setdefault(sec_num, []).append((start, end))
|
|
270
|
+
|
|
271
|
+
# Find sections with duplicates — remove all but last occurrence
|
|
272
|
+
ranges_to_remove: list[tuple[int, int]] = []
|
|
273
|
+
for sec_num, spans in section_spans.items():
|
|
274
|
+
if len(spans) > 1:
|
|
275
|
+
# Keep last, remove earlier ones
|
|
276
|
+
for start, end in spans[:-1]:
|
|
277
|
+
ranges_to_remove.append((start, end))
|
|
278
|
+
|
|
279
|
+
if not ranges_to_remove:
|
|
280
|
+
return text, 0
|
|
281
|
+
|
|
282
|
+
# Remove in reverse order to preserve positions
|
|
283
|
+
ranges_to_remove.sort(reverse=True)
|
|
284
|
+
result = text
|
|
285
|
+
for start, end in ranges_to_remove:
|
|
286
|
+
result = result[:start] + result[end:]
|
|
287
|
+
|
|
288
|
+
# Clean up multiple blank lines
|
|
289
|
+
result = re.sub(r"\n{3,}", "\n\n", result)
|
|
290
|
+
|
|
291
|
+
return result, len(ranges_to_remove)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── Main stitch function ─────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
def stitch_outputs(
|
|
297
|
+
prior: str,
|
|
298
|
+
continuation: str,
|
|
299
|
+
config: StitchConfig | None = None,
|
|
300
|
+
) -> StitchResult:
|
|
301
|
+
"""Stitch prior output with continuation output (§4.8, §04 §3.4).
|
|
302
|
+
|
|
303
|
+
Algorithm:
|
|
304
|
+
1. Detect echo (LCS on last/first 2000 chars, min 20 chars)
|
|
305
|
+
2. Remove echo from continuation start
|
|
306
|
+
3. Content-type-aware boundary detection
|
|
307
|
+
4. No-echo fallback: structural continuation, bridge insertion
|
|
308
|
+
5. Post-stitch validation
|
|
309
|
+
6. Store any trimmed fragments (never silently discard)
|
|
310
|
+
"""
|
|
311
|
+
cfg = config or StitchConfig()
|
|
312
|
+
trimmed: list[str] = []
|
|
313
|
+
|
|
314
|
+
if not prior:
|
|
315
|
+
return StitchResult(
|
|
316
|
+
text=continuation,
|
|
317
|
+
echo_removed=0,
|
|
318
|
+
boundary_type=ContentBoundary.PROSE,
|
|
319
|
+
bridge_inserted=False,
|
|
320
|
+
trimmed_fragments=trimmed,
|
|
321
|
+
validation_warnings=[],
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if not continuation:
|
|
325
|
+
return StitchResult(
|
|
326
|
+
text=prior,
|
|
327
|
+
echo_removed=0,
|
|
328
|
+
boundary_type=ContentBoundary.PROSE,
|
|
329
|
+
bridge_inserted=False,
|
|
330
|
+
trimmed_fragments=trimmed,
|
|
331
|
+
validation_warnings=[],
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
boundary_type = _detect_boundary_type(prior)
|
|
335
|
+
|
|
336
|
+
# Step 1-2: Echo detection and removal
|
|
337
|
+
echo = detect_echo(prior, continuation, cfg)
|
|
338
|
+
echo_len = len(echo)
|
|
339
|
+
bridge_inserted = False
|
|
340
|
+
|
|
341
|
+
if echo_len >= cfg.min_echo_length:
|
|
342
|
+
# Remove echo from continuation start
|
|
343
|
+
echo_pos = continuation.find(echo)
|
|
344
|
+
if echo_pos >= 0:
|
|
345
|
+
removed = continuation[:echo_pos + echo_len]
|
|
346
|
+
if echo_pos > 0:
|
|
347
|
+
trimmed.append(removed[:echo_pos])
|
|
348
|
+
continuation = continuation[echo_pos + echo_len:]
|
|
349
|
+
else:
|
|
350
|
+
# No-echo fallback: insert bridge
|
|
351
|
+
bridge = _build_bridge(boundary_type)
|
|
352
|
+
prior_trimmed = prior.rstrip()
|
|
353
|
+
continuation = bridge + continuation.lstrip()
|
|
354
|
+
prior = prior_trimmed
|
|
355
|
+
bridge_inserted = True
|
|
356
|
+
|
|
357
|
+
# Step 3: Content-type-aware boundary
|
|
358
|
+
combined = prior + continuation
|
|
359
|
+
|
|
360
|
+
# Step 4b: Section-level deduplication (GAP C fix)
|
|
361
|
+
combined, sections_removed = _dedup_sections(combined)
|
|
362
|
+
if sections_removed:
|
|
363
|
+
trimmed.append(f"[dedup: {sections_removed} duplicate section(s) removed]")
|
|
364
|
+
|
|
365
|
+
# Step 5: Validation
|
|
366
|
+
warnings = _validate_stitch(combined)
|
|
367
|
+
|
|
368
|
+
return StitchResult(
|
|
369
|
+
text=combined,
|
|
370
|
+
echo_removed=echo_len,
|
|
371
|
+
boundary_type=boundary_type,
|
|
372
|
+
bridge_inserted=bridge_inserted,
|
|
373
|
+
trimmed_fragments=trimmed,
|
|
374
|
+
validation_warnings=warnings,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ── N-way iterative stitch (§04 §3.4) ────────────────────────────
|
|
379
|
+
|
|
380
|
+
def stitch_many(
|
|
381
|
+
outputs: list[str],
|
|
382
|
+
config: StitchConfig | None = None,
|
|
383
|
+
) -> StitchResult:
|
|
384
|
+
"""N-way iterative stitch for 50+ windows.
|
|
385
|
+
|
|
386
|
+
Applies pairwise stitch left-to-right, accumulating the result.
|
|
387
|
+
"""
|
|
388
|
+
if not outputs:
|
|
389
|
+
return StitchResult(
|
|
390
|
+
text="", echo_removed=0, boundary_type=ContentBoundary.PROSE,
|
|
391
|
+
bridge_inserted=False, trimmed_fragments=[], validation_warnings=[],
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
cfg = config or StitchConfig()
|
|
395
|
+
accumulated = outputs[0]
|
|
396
|
+
total_echo = 0
|
|
397
|
+
all_trimmed: list[str] = []
|
|
398
|
+
any_bridge = False
|
|
399
|
+
last_boundary = ContentBoundary.PROSE
|
|
400
|
+
|
|
401
|
+
for i in range(1, len(outputs)):
|
|
402
|
+
result = stitch_outputs(accumulated, outputs[i], cfg)
|
|
403
|
+
accumulated = result.text
|
|
404
|
+
total_echo += result.echo_removed
|
|
405
|
+
all_trimmed.extend(result.trimmed_fragments)
|
|
406
|
+
if result.bridge_inserted:
|
|
407
|
+
any_bridge = True
|
|
408
|
+
last_boundary = result.boundary_type
|
|
409
|
+
|
|
410
|
+
final_warnings = _validate_stitch(accumulated)
|
|
411
|
+
|
|
412
|
+
return StitchResult(
|
|
413
|
+
text=accumulated,
|
|
414
|
+
echo_removed=total_echo,
|
|
415
|
+
boundary_type=last_boundary,
|
|
416
|
+
bridge_inserted=any_bridge,
|
|
417
|
+
trimmed_fragments=all_trimmed,
|
|
418
|
+
validation_warnings=final_warnings,
|
|
419
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Continuation trigger — wall detection and continuation conditions (§4.2)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TriggerConfig:
|
|
12
|
+
"""Configuration for continuation triggering."""
|
|
13
|
+
|
|
14
|
+
max_continuations: int = 50
|
|
15
|
+
token_ratio_threshold: float = 0.95
|
|
16
|
+
min_gap_score: float = 0.0
|
|
17
|
+
min_info_flow: float = 0.0
|
|
18
|
+
# When the model stops naturally (no wall hit) but significant
|
|
19
|
+
# unfulfilled requirements remain, override the stop and continue.
|
|
20
|
+
# This handles small models that stop prematurely before completing
|
|
21
|
+
# all requested sections. Set to 0.0 to disable gap override.
|
|
22
|
+
gap_override_threshold: float = 0.3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TriggerResult:
|
|
27
|
+
"""Result of continuation trigger evaluation."""
|
|
28
|
+
|
|
29
|
+
should_continue: bool
|
|
30
|
+
wall_hit: bool
|
|
31
|
+
gap_remaining: float
|
|
32
|
+
info_flow: float
|
|
33
|
+
continuation_count: int
|
|
34
|
+
reason: str
|
|
35
|
+
details: dict[str, object] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_wall_hit(
|
|
39
|
+
finish_reason: str | None,
|
|
40
|
+
output_tokens: int | None = None,
|
|
41
|
+
max_output_tokens: int | None = None,
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""Detect physical context-window wall hit.
|
|
44
|
+
|
|
45
|
+
Primary: finish_reason == "length" (universal across providers).
|
|
46
|
+
Fallback: output_tokens / max_output_tokens >= 0.95 when finish_reason unavailable.
|
|
47
|
+
"""
|
|
48
|
+
if finish_reason is not None:
|
|
49
|
+
return finish_reason.lower() in ("length", "max_tokens")
|
|
50
|
+
|
|
51
|
+
if output_tokens is not None and max_output_tokens is not None and max_output_tokens > 0:
|
|
52
|
+
return output_tokens / max_output_tokens >= 0.95
|
|
53
|
+
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def evaluate_continuation(
|
|
58
|
+
*,
|
|
59
|
+
finish_reason: str | None,
|
|
60
|
+
output_tokens: int | None = None,
|
|
61
|
+
max_output_tokens: int | None = None,
|
|
62
|
+
gap_score: float = 1.0,
|
|
63
|
+
info_flow: float = 1.0,
|
|
64
|
+
continuation_count: int = 0,
|
|
65
|
+
config: TriggerConfig | None = None,
|
|
66
|
+
) -> TriggerResult:
|
|
67
|
+
"""Evaluate whether continuation should proceed.
|
|
68
|
+
|
|
69
|
+
Three conditions must ALL be met (§4.2):
|
|
70
|
+
1. Wall hit detected (physical truncation)
|
|
71
|
+
2. Gap score > min_gap_score (unfulfilled requirements remain)
|
|
72
|
+
3. Info flow > min_info_flow (model still producing useful content)
|
|
73
|
+
4. continuation_count < max_continuations (safety bound)
|
|
74
|
+
"""
|
|
75
|
+
cfg = config or TriggerConfig()
|
|
76
|
+
|
|
77
|
+
wall_hit = detect_wall_hit(finish_reason, output_tokens, max_output_tokens)
|
|
78
|
+
|
|
79
|
+
if not wall_hit:
|
|
80
|
+
# Gap override: if the model stopped naturally but significant
|
|
81
|
+
# requirements remain unfulfilled, continue anyway. This handles
|
|
82
|
+
# small models that stop prematurely before completing all sections.
|
|
83
|
+
# Still respect max_continuations safety bound.
|
|
84
|
+
if (cfg.gap_override_threshold > 0.0
|
|
85
|
+
and gap_score > cfg.gap_override_threshold
|
|
86
|
+
and continuation_count < cfg.max_continuations):
|
|
87
|
+
return TriggerResult(
|
|
88
|
+
should_continue=True,
|
|
89
|
+
wall_hit=False,
|
|
90
|
+
gap_remaining=gap_score,
|
|
91
|
+
info_flow=info_flow,
|
|
92
|
+
continuation_count=continuation_count,
|
|
93
|
+
reason="gap_override",
|
|
94
|
+
details={"gap_threshold": cfg.gap_override_threshold},
|
|
95
|
+
)
|
|
96
|
+
return TriggerResult(
|
|
97
|
+
should_continue=False,
|
|
98
|
+
wall_hit=False,
|
|
99
|
+
gap_remaining=gap_score,
|
|
100
|
+
info_flow=info_flow,
|
|
101
|
+
continuation_count=continuation_count,
|
|
102
|
+
reason="no_wall_hit",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if gap_score <= cfg.min_gap_score:
|
|
106
|
+
return TriggerResult(
|
|
107
|
+
should_continue=False,
|
|
108
|
+
wall_hit=True,
|
|
109
|
+
gap_remaining=gap_score,
|
|
110
|
+
info_flow=info_flow,
|
|
111
|
+
continuation_count=continuation_count,
|
|
112
|
+
reason="gap_fulfilled",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if info_flow <= cfg.min_info_flow:
|
|
116
|
+
return TriggerResult(
|
|
117
|
+
should_continue=False,
|
|
118
|
+
wall_hit=True,
|
|
119
|
+
gap_remaining=gap_score,
|
|
120
|
+
info_flow=info_flow,
|
|
121
|
+
continuation_count=continuation_count,
|
|
122
|
+
reason="info_flow_dead",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if continuation_count >= cfg.max_continuations:
|
|
126
|
+
return TriggerResult(
|
|
127
|
+
should_continue=False,
|
|
128
|
+
wall_hit=True,
|
|
129
|
+
gap_remaining=gap_score,
|
|
130
|
+
info_flow=info_flow,
|
|
131
|
+
continuation_count=continuation_count,
|
|
132
|
+
reason="max_continuations_reached",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return TriggerResult(
|
|
136
|
+
should_continue=True,
|
|
137
|
+
wall_hit=True,
|
|
138
|
+
gap_remaining=gap_score,
|
|
139
|
+
info_flow=info_flow,
|
|
140
|
+
continuation_count=continuation_count,
|
|
141
|
+
reason="continue",
|
|
142
|
+
)
|