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.
Files changed (48) hide show
  1. claude_toolstack_cli-1.0.0.dist-info/METADATA +354 -0
  2. claude_toolstack_cli-1.0.0.dist-info/RECORD +48 -0
  3. claude_toolstack_cli-1.0.0.dist-info/WHEEL +5 -0
  4. claude_toolstack_cli-1.0.0.dist-info/entry_points.txt +2 -0
  5. claude_toolstack_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. claude_toolstack_cli-1.0.0.dist-info/top_level.txt +1 -0
  7. cts/__init__.py +3 -0
  8. cts/__main__.py +5 -0
  9. cts/autopilot.py +633 -0
  10. cts/bundle.py +958 -0
  11. cts/cli.py +2858 -0
  12. cts/confidence.py +218 -0
  13. cts/config.py +19 -0
  14. cts/corpus/__init__.py +139 -0
  15. cts/corpus/apply.py +305 -0
  16. cts/corpus/archive.py +309 -0
  17. cts/corpus/baseline.py +294 -0
  18. cts/corpus/evaluate.py +409 -0
  19. cts/corpus/experiment_eval.py +585 -0
  20. cts/corpus/experiment_schema.py +380 -0
  21. cts/corpus/extract.py +353 -0
  22. cts/corpus/load.py +44 -0
  23. cts/corpus/model.py +114 -0
  24. cts/corpus/patch.py +467 -0
  25. cts/corpus/registry.py +420 -0
  26. cts/corpus/report.py +745 -0
  27. cts/corpus/scan.py +87 -0
  28. cts/corpus/store.py +63 -0
  29. cts/corpus/trends.py +478 -0
  30. cts/corpus/tuning_schema.py +313 -0
  31. cts/corpus/variants.py +335 -0
  32. cts/ctags.py +133 -0
  33. cts/diff_context.py +92 -0
  34. cts/errors.py +109 -0
  35. cts/http.py +89 -0
  36. cts/ranking.py +466 -0
  37. cts/render.py +388 -0
  38. cts/schema.py +96 -0
  39. cts/semantic/__init__.py +47 -0
  40. cts/semantic/candidates.py +150 -0
  41. cts/semantic/chunker.py +184 -0
  42. cts/semantic/config.py +120 -0
  43. cts/semantic/embedder.py +151 -0
  44. cts/semantic/indexer.py +159 -0
  45. cts/semantic/search.py +252 -0
  46. cts/semantic/store.py +330 -0
  47. cts/sidecar.py +431 -0
  48. 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
+ }