claude-toolstack-cli 1.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.
- claude_toolstack_cli-1.0.0.dist-info/METADATA +354 -0
- claude_toolstack_cli-1.0.0.dist-info/RECORD +48 -0
- claude_toolstack_cli-1.0.0.dist-info/WHEEL +5 -0
- claude_toolstack_cli-1.0.0.dist-info/entry_points.txt +2 -0
- claude_toolstack_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- claude_toolstack_cli-1.0.0.dist-info/top_level.txt +1 -0
- cts/__init__.py +3 -0
- cts/__main__.py +5 -0
- cts/autopilot.py +633 -0
- cts/bundle.py +958 -0
- cts/cli.py +2858 -0
- cts/confidence.py +218 -0
- cts/config.py +19 -0
- cts/corpus/__init__.py +139 -0
- cts/corpus/apply.py +305 -0
- cts/corpus/archive.py +309 -0
- cts/corpus/baseline.py +294 -0
- cts/corpus/evaluate.py +409 -0
- cts/corpus/experiment_eval.py +585 -0
- cts/corpus/experiment_schema.py +380 -0
- cts/corpus/extract.py +353 -0
- cts/corpus/load.py +44 -0
- cts/corpus/model.py +114 -0
- cts/corpus/patch.py +467 -0
- cts/corpus/registry.py +420 -0
- cts/corpus/report.py +745 -0
- cts/corpus/scan.py +87 -0
- cts/corpus/store.py +63 -0
- cts/corpus/trends.py +478 -0
- cts/corpus/tuning_schema.py +313 -0
- cts/corpus/variants.py +335 -0
- cts/ctags.py +133 -0
- cts/diff_context.py +92 -0
- cts/errors.py +109 -0
- cts/http.py +89 -0
- cts/ranking.py +466 -0
- cts/render.py +388 -0
- cts/schema.py +96 -0
- cts/semantic/__init__.py +47 -0
- cts/semantic/candidates.py +150 -0
- cts/semantic/chunker.py +184 -0
- cts/semantic/config.py +120 -0
- cts/semantic/embedder.py +151 -0
- cts/semantic/indexer.py +159 -0
- cts/semantic/search.py +252 -0
- cts/semantic/store.py +330 -0
- cts/sidecar.py +431 -0
- cts/structural.py +305 -0
cts/corpus/extract.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Extract normalized metrics from sidecar artifacts.
|
|
2
|
+
|
|
3
|
+
Defensive parsing: missing fields are tracked in ``missing_fields``
|
|
4
|
+
rather than raising exceptions. This ensures partial ingestion is
|
|
5
|
+
always possible even as the sidecar schema evolves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from cts.confidence import bundle_confidence
|
|
14
|
+
from cts.corpus.model import CorpusRecord, PassRecord
|
|
15
|
+
|
|
16
|
+
# Sections whose byte sizes are measured in the final bundle
|
|
17
|
+
_SECTION_KEYS = [
|
|
18
|
+
"ranked_sources",
|
|
19
|
+
"matches",
|
|
20
|
+
"slices",
|
|
21
|
+
"symbols",
|
|
22
|
+
"diff",
|
|
23
|
+
"suggested_commands",
|
|
24
|
+
"notes",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _json_bytes(obj: Any) -> int:
|
|
34
|
+
"""Compute the JSON-encoded byte size of *obj*."""
|
|
35
|
+
try:
|
|
36
|
+
return len(json.dumps(obj, default=str).encode("utf-8"))
|
|
37
|
+
except (TypeError, ValueError):
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _extract_section_bytes(final: Dict[str, Any]) -> Dict[str, int]:
|
|
42
|
+
"""Compute byte sizes for each section in the final bundle."""
|
|
43
|
+
result: Dict[str, int] = {}
|
|
44
|
+
for key in _SECTION_KEYS:
|
|
45
|
+
if key in final:
|
|
46
|
+
result[key] = _json_bytes(final[key])
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _extract_truncation_flags(final: Dict[str, Any]) -> Dict[str, bool]:
|
|
51
|
+
"""Extract truncation flags from the final bundle."""
|
|
52
|
+
flags: Dict[str, bool] = {}
|
|
53
|
+
if "truncated" in final:
|
|
54
|
+
flags["truncated"] = bool(final["truncated"])
|
|
55
|
+
return flags
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_timings(final: Dict[str, Any]) -> Dict[str, float]:
|
|
59
|
+
"""Extract timing data from ``_debug.timings`` if present."""
|
|
60
|
+
debug = final.get("_debug")
|
|
61
|
+
if not isinstance(debug, dict):
|
|
62
|
+
return {}
|
|
63
|
+
timings = debug.get("timings")
|
|
64
|
+
if not isinstance(timings, dict):
|
|
65
|
+
return {}
|
|
66
|
+
result: Dict[str, float] = {}
|
|
67
|
+
for key, val in timings.items():
|
|
68
|
+
if isinstance(val, (int, float)):
|
|
69
|
+
result[key] = float(val)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_actions(passes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
74
|
+
"""Build an ordered action list from all pass records.
|
|
75
|
+
|
|
76
|
+
Includes trigger reasons and summary counts but never raw file
|
|
77
|
+
content or diff text (safe for corpus output).
|
|
78
|
+
"""
|
|
79
|
+
actions: List[Dict[str, Any]] = []
|
|
80
|
+
_COUNT_KEYS = (
|
|
81
|
+
"trace_targets_count",
|
|
82
|
+
"def_targets_count",
|
|
83
|
+
"caller_targets_count",
|
|
84
|
+
"changed_targets_count",
|
|
85
|
+
"ident_count",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for p in passes:
|
|
89
|
+
details = p.get("action_details", [])
|
|
90
|
+
action_names = p.get("actions", [])
|
|
91
|
+
|
|
92
|
+
if details:
|
|
93
|
+
for detail in details:
|
|
94
|
+
record: Dict[str, Any] = {"name": detail.get("name", "")}
|
|
95
|
+
if "trigger_reason" in detail:
|
|
96
|
+
record["trigger_reason"] = detail["trigger_reason"]
|
|
97
|
+
for key in _COUNT_KEYS:
|
|
98
|
+
if key in detail:
|
|
99
|
+
record[key] = detail[key]
|
|
100
|
+
actions.append(record)
|
|
101
|
+
elif action_names:
|
|
102
|
+
# Fallback: just action names without details
|
|
103
|
+
for name in action_names:
|
|
104
|
+
actions.append({"name": name})
|
|
105
|
+
|
|
106
|
+
return actions
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Public API
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_record(
|
|
115
|
+
sidecar: Dict[str, Any],
|
|
116
|
+
*,
|
|
117
|
+
source_path: str = "",
|
|
118
|
+
) -> CorpusRecord:
|
|
119
|
+
"""Extract a normalized :class:`CorpusRecord` from a sidecar dict.
|
|
120
|
+
|
|
121
|
+
Missing or unparseable fields are tracked in ``missing_fields``
|
|
122
|
+
rather than raising. This allows partial ingestion of artifacts
|
|
123
|
+
whose ``_debug`` section is absent or whose schema is slightly
|
|
124
|
+
ahead of the extractor.
|
|
125
|
+
"""
|
|
126
|
+
missing: List[str] = []
|
|
127
|
+
|
|
128
|
+
# --- Identity ---
|
|
129
|
+
schema_version = sidecar.get("bundle_schema_version", 0)
|
|
130
|
+
if not schema_version:
|
|
131
|
+
missing.append("bundle_schema_version")
|
|
132
|
+
|
|
133
|
+
repo = sidecar.get("repo", "")
|
|
134
|
+
if not repo:
|
|
135
|
+
missing.append("repo")
|
|
136
|
+
|
|
137
|
+
mode = sidecar.get("mode", "")
|
|
138
|
+
if not mode:
|
|
139
|
+
missing.append("mode")
|
|
140
|
+
|
|
141
|
+
created_at = sidecar.get("created_at", 0.0)
|
|
142
|
+
if not created_at:
|
|
143
|
+
missing.append("created_at")
|
|
144
|
+
|
|
145
|
+
request_id = sidecar.get("request_id", "")
|
|
146
|
+
if not request_id:
|
|
147
|
+
missing.append("request_id")
|
|
148
|
+
|
|
149
|
+
# --- Passes ---
|
|
150
|
+
passes = sidecar.get("passes", [])
|
|
151
|
+
passes_count = len(passes)
|
|
152
|
+
|
|
153
|
+
# --- Final bundle ---
|
|
154
|
+
final = sidecar.get("final", {})
|
|
155
|
+
if not final:
|
|
156
|
+
missing.append("final")
|
|
157
|
+
|
|
158
|
+
# --- Confidence ---
|
|
159
|
+
confidence_pass1: Optional[float] = None
|
|
160
|
+
confidence_final: Optional[float] = None
|
|
161
|
+
confidence_delta: Optional[float] = None
|
|
162
|
+
|
|
163
|
+
# Score cards from _debug (if available)
|
|
164
|
+
score_cards = None
|
|
165
|
+
debug_data = final.get("_debug") if final else None
|
|
166
|
+
if isinstance(debug_data, dict):
|
|
167
|
+
score_cards = debug_data.get("score_cards")
|
|
168
|
+
else:
|
|
169
|
+
missing.append("_debug")
|
|
170
|
+
|
|
171
|
+
if passes:
|
|
172
|
+
# confidence_pass1 from first pass record
|
|
173
|
+
c1 = passes[0].get("confidence_before")
|
|
174
|
+
if isinstance(c1, (int, float)):
|
|
175
|
+
confidence_pass1 = float(c1)
|
|
176
|
+
else:
|
|
177
|
+
missing.append("confidence_pass1")
|
|
178
|
+
|
|
179
|
+
# confidence_final: recompute from final bundle
|
|
180
|
+
if final:
|
|
181
|
+
try:
|
|
182
|
+
conf = bundle_confidence(final, score_cards=score_cards)
|
|
183
|
+
confidence_final = conf["score"]
|
|
184
|
+
except Exception:
|
|
185
|
+
missing.append("confidence_final")
|
|
186
|
+
|
|
187
|
+
# When no passes ran, initial IS final
|
|
188
|
+
if confidence_pass1 is None and confidence_final is not None:
|
|
189
|
+
confidence_pass1 = confidence_final
|
|
190
|
+
|
|
191
|
+
if confidence_pass1 is not None and confidence_final is not None:
|
|
192
|
+
confidence_delta = round(confidence_final - confidence_pass1, 4)
|
|
193
|
+
|
|
194
|
+
# --- Actions ---
|
|
195
|
+
actions = _extract_actions(passes)
|
|
196
|
+
|
|
197
|
+
# --- Size metrics ---
|
|
198
|
+
bundle_bytes_final = _json_bytes(final) if final else 0
|
|
199
|
+
section_bytes = _extract_section_bytes(final)
|
|
200
|
+
|
|
201
|
+
# --- Truncation ---
|
|
202
|
+
truncation_flags = _extract_truncation_flags(final)
|
|
203
|
+
|
|
204
|
+
# --- Timings ---
|
|
205
|
+
timings_ms = _extract_timings(final)
|
|
206
|
+
if not timings_ms and isinstance(debug_data, dict):
|
|
207
|
+
# _debug present but no timings sub-key
|
|
208
|
+
missing.append("timings")
|
|
209
|
+
|
|
210
|
+
# --- Semantic augmentation (Phase 4) ---
|
|
211
|
+
semantic_invoked = False
|
|
212
|
+
semantic_time_ms: Optional[float] = None
|
|
213
|
+
semantic_hit_count = 0
|
|
214
|
+
semantic_action_fired = False
|
|
215
|
+
semantic_lift: Optional[float] = None
|
|
216
|
+
|
|
217
|
+
# Check for semantic data in _debug.semantic or final.semantic
|
|
218
|
+
semantic_data = None
|
|
219
|
+
if isinstance(debug_data, dict):
|
|
220
|
+
semantic_data = debug_data.get("semantic")
|
|
221
|
+
if semantic_data is None and final:
|
|
222
|
+
semantic_data = final.get("semantic")
|
|
223
|
+
|
|
224
|
+
if isinstance(semantic_data, dict):
|
|
225
|
+
semantic_invoked = bool(semantic_data.get("invoked", False))
|
|
226
|
+
st = semantic_data.get("time_ms")
|
|
227
|
+
if isinstance(st, (int, float)):
|
|
228
|
+
semantic_time_ms = float(st)
|
|
229
|
+
semantic_hit_count = int(semantic_data.get("hit_count", 0))
|
|
230
|
+
|
|
231
|
+
# Check if autopilot fired the semantic_fallback action
|
|
232
|
+
for a in actions:
|
|
233
|
+
if a.get("name") == "semantic_fallback":
|
|
234
|
+
semantic_action_fired = True
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
# Compute semantic lift: confidence delta attributable to the pass
|
|
238
|
+
# that included semantic_fallback
|
|
239
|
+
if semantic_action_fired and passes:
|
|
240
|
+
for idx, p in enumerate(passes):
|
|
241
|
+
p_actions = p.get("actions", [])
|
|
242
|
+
if "semantic_fallback" in p_actions:
|
|
243
|
+
conf_before = p.get("confidence_before")
|
|
244
|
+
# Use next pass's confidence_before, or final confidence
|
|
245
|
+
if idx + 1 < len(passes):
|
|
246
|
+
conf_after = passes[idx + 1].get("confidence_before")
|
|
247
|
+
elif confidence_final is not None:
|
|
248
|
+
conf_after = confidence_final
|
|
249
|
+
else:
|
|
250
|
+
conf_after = None
|
|
251
|
+
if isinstance(conf_before, (int, float)) and isinstance(
|
|
252
|
+
conf_after, (int, float)
|
|
253
|
+
):
|
|
254
|
+
semantic_lift = round(conf_after - conf_before, 4)
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# --- Candidate narrowing (Phase 4.2) ---
|
|
258
|
+
semantic_candidate_strategy = ""
|
|
259
|
+
semantic_candidate_files = 0
|
|
260
|
+
semantic_candidate_chunks = 0
|
|
261
|
+
semantic_candidate_fallback_used = False
|
|
262
|
+
|
|
263
|
+
# Look in _debug.semantic_candidates or _debug.semantic.candidate_selection
|
|
264
|
+
cand_data = None
|
|
265
|
+
if isinstance(debug_data, dict):
|
|
266
|
+
cand_data = debug_data.get("semantic_candidates")
|
|
267
|
+
if cand_data is None and isinstance(semantic_data, dict):
|
|
268
|
+
cand_data = semantic_data.get("candidate_selection")
|
|
269
|
+
|
|
270
|
+
if isinstance(cand_data, dict):
|
|
271
|
+
semantic_candidate_strategy = str(cand_data.get("strategy", ""))
|
|
272
|
+
semantic_candidate_files = int(cand_data.get("candidate_files", 0))
|
|
273
|
+
semantic_candidate_chunks = int(cand_data.get("candidate_chunks_considered", 0))
|
|
274
|
+
semantic_candidate_fallback_used = bool(cand_data.get("fallback_used", False))
|
|
275
|
+
|
|
276
|
+
return CorpusRecord(
|
|
277
|
+
schema_version=schema_version,
|
|
278
|
+
repo=repo,
|
|
279
|
+
mode=mode,
|
|
280
|
+
created_at=created_at,
|
|
281
|
+
request_id=request_id,
|
|
282
|
+
source_path=source_path,
|
|
283
|
+
passes_count=passes_count,
|
|
284
|
+
confidence_pass1=confidence_pass1,
|
|
285
|
+
confidence_final=confidence_final,
|
|
286
|
+
confidence_delta=confidence_delta,
|
|
287
|
+
actions=actions,
|
|
288
|
+
bundle_bytes_final=bundle_bytes_final,
|
|
289
|
+
section_bytes=section_bytes,
|
|
290
|
+
truncation_flags=truncation_flags,
|
|
291
|
+
timings_ms=timings_ms,
|
|
292
|
+
missing_fields=missing,
|
|
293
|
+
semantic_invoked=semantic_invoked,
|
|
294
|
+
semantic_time_ms=semantic_time_ms,
|
|
295
|
+
semantic_hit_count=semantic_hit_count,
|
|
296
|
+
semantic_action_fired=semantic_action_fired,
|
|
297
|
+
semantic_lift=semantic_lift,
|
|
298
|
+
semantic_candidate_strategy=semantic_candidate_strategy,
|
|
299
|
+
semantic_candidate_files=semantic_candidate_files,
|
|
300
|
+
semantic_candidate_chunks=semantic_candidate_chunks,
|
|
301
|
+
semantic_candidate_fallback_used=semantic_candidate_fallback_used,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def extract_passes(
|
|
306
|
+
sidecar: Dict[str, Any],
|
|
307
|
+
) -> List[PassRecord]:
|
|
308
|
+
"""Extract per-pass records from a sidecar artifact.
|
|
309
|
+
|
|
310
|
+
Returns one :class:`PassRecord` per refinement pass. Sensitive
|
|
311
|
+
content (file paths in target lists) is stripped — only counts
|
|
312
|
+
and trigger reasons are kept.
|
|
313
|
+
"""
|
|
314
|
+
request_id = sidecar.get("request_id", "")
|
|
315
|
+
passes = sidecar.get("passes", [])
|
|
316
|
+
records: List[PassRecord] = []
|
|
317
|
+
|
|
318
|
+
_COUNT_KEYS = (
|
|
319
|
+
"trace_targets_count",
|
|
320
|
+
"def_targets_count",
|
|
321
|
+
"caller_targets_count",
|
|
322
|
+
"changed_targets_count",
|
|
323
|
+
"ident_count",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
for i, p in enumerate(passes):
|
|
327
|
+
action_names = p.get("actions", [])
|
|
328
|
+
details = p.get("action_details", [])
|
|
329
|
+
|
|
330
|
+
# Sanitize action details (counts + reasons only)
|
|
331
|
+
clean_details: List[Dict[str, Any]] = []
|
|
332
|
+
for d in details:
|
|
333
|
+
clean: Dict[str, Any] = {"name": d.get("name", "")}
|
|
334
|
+
if "trigger_reason" in d:
|
|
335
|
+
clean["trigger_reason"] = d["trigger_reason"]
|
|
336
|
+
for key in _COUNT_KEYS:
|
|
337
|
+
if key in d:
|
|
338
|
+
clean[key] = d[key]
|
|
339
|
+
clean_details.append(clean)
|
|
340
|
+
|
|
341
|
+
records.append(
|
|
342
|
+
PassRecord(
|
|
343
|
+
request_id=request_id,
|
|
344
|
+
pass_index=i,
|
|
345
|
+
confidence=p.get("confidence_before"),
|
|
346
|
+
actions_this_pass=action_names,
|
|
347
|
+
action_details=clean_details,
|
|
348
|
+
status=p.get("status", ""),
|
|
349
|
+
elapsed_ms=p.get("elapsed_ms", 0.0),
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return records
|
cts/corpus/load.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Load and validate sidecar artifacts for corpus ingestion.
|
|
2
|
+
|
|
3
|
+
Wraps ``cts.sidecar.validate_envelope`` to provide a load-or-fail
|
|
4
|
+
interface suitable for batch processing: returns ``(data, errors)``
|
|
5
|
+
so callers decide whether to skip or abort on invalid files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from cts.sidecar import validate_envelope
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_artifact(
|
|
17
|
+
path: str,
|
|
18
|
+
) -> Tuple[Optional[Dict[str, Any]], List[str]]:
|
|
19
|
+
"""Load a sidecar JSON file and validate its envelope.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path: Filesystem path to the sidecar JSON file.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
``(sidecar_dict, errors)``. When *errors* is non-empty the
|
|
26
|
+
dict may be ``None`` (parse failure) or partially populated
|
|
27
|
+
(validation failure).
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
with open(path, encoding="utf-8") as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
33
|
+
return None, [f"parse error: {exc}"]
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
return None, [f"read error: {exc}"]
|
|
36
|
+
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
return None, [f"expected JSON object, got {type(data).__name__}"]
|
|
39
|
+
|
|
40
|
+
errors = validate_envelope(data)
|
|
41
|
+
if errors:
|
|
42
|
+
return data, errors
|
|
43
|
+
|
|
44
|
+
return data, []
|
cts/corpus/model.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Dataclasses for corpus metrics extracted from sidecar artifacts.
|
|
2
|
+
|
|
3
|
+
Each ingested sidecar produces one :class:`CorpusRecord` and,
|
|
4
|
+
optionally, one :class:`PassRecord` per refinement pass. Both
|
|
5
|
+
carry a ``to_dict()`` method for JSONL serialization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CorpusRecord:
|
|
16
|
+
"""One record per ingested sidecar artifact."""
|
|
17
|
+
|
|
18
|
+
# Identity
|
|
19
|
+
schema_version: int = 0
|
|
20
|
+
repo: str = ""
|
|
21
|
+
mode: str = ""
|
|
22
|
+
created_at: float = 0.0
|
|
23
|
+
request_id: str = ""
|
|
24
|
+
source_path: str = ""
|
|
25
|
+
|
|
26
|
+
# Pass metrics
|
|
27
|
+
passes_count: int = 0
|
|
28
|
+
confidence_pass1: Optional[float] = None
|
|
29
|
+
confidence_final: Optional[float] = None
|
|
30
|
+
confidence_delta: Optional[float] = None
|
|
31
|
+
|
|
32
|
+
# Actions across all passes (ordered)
|
|
33
|
+
actions: List[Dict[str, Any]] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
# Size metrics (bytes)
|
|
36
|
+
bundle_bytes_final: int = 0
|
|
37
|
+
section_bytes: Dict[str, int] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
# Truncation
|
|
40
|
+
truncation_flags: Dict[str, bool] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
# Timings (from _debug)
|
|
43
|
+
timings_ms: Dict[str, float] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
# Quality metadata
|
|
46
|
+
missing_fields: List[str] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
# Semantic augmentation metrics (Phase 4)
|
|
49
|
+
semantic_invoked: bool = False
|
|
50
|
+
semantic_time_ms: Optional[float] = None
|
|
51
|
+
semantic_hit_count: int = 0
|
|
52
|
+
semantic_action_fired: bool = False
|
|
53
|
+
semantic_lift: Optional[float] = None
|
|
54
|
+
|
|
55
|
+
# Candidate narrowing metrics (Phase 4.2)
|
|
56
|
+
semantic_candidate_strategy: str = ""
|
|
57
|
+
semantic_candidate_files: int = 0
|
|
58
|
+
semantic_candidate_chunks: int = 0
|
|
59
|
+
semantic_candidate_fallback_used: bool = False
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
62
|
+
"""Convert to a JSON-serializable dict for JSONL output."""
|
|
63
|
+
return {
|
|
64
|
+
"schema_version": self.schema_version,
|
|
65
|
+
"repo": self.repo,
|
|
66
|
+
"mode": self.mode,
|
|
67
|
+
"created_at": self.created_at,
|
|
68
|
+
"request_id": self.request_id,
|
|
69
|
+
"source_path": self.source_path,
|
|
70
|
+
"passes_count": self.passes_count,
|
|
71
|
+
"confidence_pass1": self.confidence_pass1,
|
|
72
|
+
"confidence_final": self.confidence_final,
|
|
73
|
+
"confidence_delta": self.confidence_delta,
|
|
74
|
+
"actions": self.actions,
|
|
75
|
+
"bundle_bytes_final": self.bundle_bytes_final,
|
|
76
|
+
"section_bytes": self.section_bytes,
|
|
77
|
+
"truncation_flags": self.truncation_flags,
|
|
78
|
+
"timings_ms": self.timings_ms,
|
|
79
|
+
"missing_fields": self.missing_fields,
|
|
80
|
+
"semantic_invoked": self.semantic_invoked,
|
|
81
|
+
"semantic_time_ms": self.semantic_time_ms,
|
|
82
|
+
"semantic_hit_count": self.semantic_hit_count,
|
|
83
|
+
"semantic_action_fired": self.semantic_action_fired,
|
|
84
|
+
"semantic_lift": self.semantic_lift,
|
|
85
|
+
"semantic_candidate_strategy": self.semantic_candidate_strategy,
|
|
86
|
+
"semantic_candidate_files": self.semantic_candidate_files,
|
|
87
|
+
"semantic_candidate_chunks": self.semantic_candidate_chunks,
|
|
88
|
+
"semantic_candidate_fallback_used": (self.semantic_candidate_fallback_used),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class PassRecord:
|
|
94
|
+
"""One record per refinement pass within a sidecar artifact."""
|
|
95
|
+
|
|
96
|
+
request_id: str = ""
|
|
97
|
+
pass_index: int = 0
|
|
98
|
+
confidence: Optional[float] = None
|
|
99
|
+
actions_this_pass: List[str] = field(default_factory=list)
|
|
100
|
+
action_details: List[Dict[str, Any]] = field(default_factory=list)
|
|
101
|
+
status: str = ""
|
|
102
|
+
elapsed_ms: float = 0.0
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
105
|
+
"""Convert to a JSON-serializable dict for JSONL output."""
|
|
106
|
+
return {
|
|
107
|
+
"request_id": self.request_id,
|
|
108
|
+
"pass_index": self.pass_index,
|
|
109
|
+
"confidence": self.confidence,
|
|
110
|
+
"actions_this_pass": self.actions_this_pass,
|
|
111
|
+
"action_details": self.action_details,
|
|
112
|
+
"status": self.status,
|
|
113
|
+
"elapsed_ms": self.elapsed_ms,
|
|
114
|
+
}
|