traceforge-toolkit 0.1.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 (205) hide show
  1. traceforge/__init__.py +72 -0
  2. traceforge/__main__.py +5 -0
  3. traceforge/_generated.py +254 -0
  4. traceforge/adapters/__init__.py +12 -0
  5. traceforge/adapters/base.py +82 -0
  6. traceforge/adapters/genai_otel.py +164 -0
  7. traceforge/adapters/mapped_json.py +297 -0
  8. traceforge/adapters/otel.py +220 -0
  9. traceforge/boundary/__init__.py +46 -0
  10. traceforge/boundary/data/boundary-model.joblib +0 -0
  11. traceforge/boundary/decode.py +87 -0
  12. traceforge/boundary/features.py +137 -0
  13. traceforge/boundary/inference.py +267 -0
  14. traceforge/boundary/inferencer.py +146 -0
  15. traceforge/classify/__init__.py +95 -0
  16. traceforge/classify/cmd.py +109 -0
  17. traceforge/classify/coding.py +213 -0
  18. traceforge/classify/config.py +554 -0
  19. traceforge/classify/core.py +266 -0
  20. traceforge/classify/data/binary_info.yaml +358 -0
  21. traceforge/classify/data/canonical_tools.yaml +251 -0
  22. traceforge/classify/data/effect_overrides.yaml +480 -0
  23. traceforge/classify/data/mcp_profiles.yaml +711 -0
  24. traceforge/classify/data/recommendation_rules.yaml +111 -0
  25. traceforge/classify/data/risk.yaml +534 -0
  26. traceforge/classify/data/shell_defaults.yaml +95 -0
  27. traceforge/classify/data/shell_rules.yaml +1192 -0
  28. traceforge/classify/data/tool_classifications.yaml +215 -0
  29. traceforge/classify/data/verb_inference.yaml +218 -0
  30. traceforge/classify/mcp.py +181 -0
  31. traceforge/classify/phases.py +75 -0
  32. traceforge/classify/powershell.py +93 -0
  33. traceforge/classify/registry.py +138 -0
  34. traceforge/classify/risk.py +553 -0
  35. traceforge/classify/rules.py +215 -0
  36. traceforge/classify/schema.yaml +282 -0
  37. traceforge/classify/shell.py +503 -0
  38. traceforge/classify/tools.py +91 -0
  39. traceforge/classify/workflow.py +32 -0
  40. traceforge/cli/__init__.py +42 -0
  41. traceforge/cli/config_cmd.py +130 -0
  42. traceforge/cli/detect.py +46 -0
  43. traceforge/cli/download_cmd.py +74 -0
  44. traceforge/cli/factory.py +60 -0
  45. traceforge/cli/gate_cmd.py +30 -0
  46. traceforge/cli/init_cmd.py +86 -0
  47. traceforge/cli/replay.py +130 -0
  48. traceforge/cli/runner.py +181 -0
  49. traceforge/cli/score.py +217 -0
  50. traceforge/cli/status.py +65 -0
  51. traceforge/cli/watch.py +297 -0
  52. traceforge/config/__init__.py +80 -0
  53. traceforge/config/defaults.py +149 -0
  54. traceforge/config/loader.py +239 -0
  55. traceforge/config/mappings.py +104 -0
  56. traceforge/config/models.py +579 -0
  57. traceforge/enricher.py +576 -0
  58. traceforge/formatting/__init__.py +12 -0
  59. traceforge/formatting/budget.py +92 -0
  60. traceforge/formatting/density.py +154 -0
  61. traceforge/gate/__init__.py +17 -0
  62. traceforge/gate/client.py +145 -0
  63. traceforge/gate/external.py +305 -0
  64. traceforge/gate/registry.py +108 -0
  65. traceforge/gate/server.py +174 -0
  66. traceforge/gates/__init__.py +8 -0
  67. traceforge/gates/pii.py +352 -0
  68. traceforge/gates/pii_patterns.yaml +228 -0
  69. traceforge/governance/__init__.py +121 -0
  70. traceforge/governance/assessor.py +441 -0
  71. traceforge/governance/budget.py +59 -0
  72. traceforge/governance/canonical.py +56 -0
  73. traceforge/governance/codec.py +414 -0
  74. traceforge/governance/context.py +249 -0
  75. traceforge/governance/drift.py +196 -0
  76. traceforge/governance/emitter.py +234 -0
  77. traceforge/governance/envelope.py +138 -0
  78. traceforge/governance/ifc.py +364 -0
  79. traceforge/governance/integrity.py +162 -0
  80. traceforge/governance/labeler.py +250 -0
  81. traceforge/governance/mcp_drift.py +328 -0
  82. traceforge/governance/monitor.py +539 -0
  83. traceforge/governance/observer.py +276 -0
  84. traceforge/governance/persistence.py +401 -0
  85. traceforge/governance/phase1.py +77 -0
  86. traceforge/governance/pii.py +276 -0
  87. traceforge/governance/pipeline.py +840 -0
  88. traceforge/governance/registry.py +110 -0
  89. traceforge/governance/results.py +183 -0
  90. traceforge/governance/risk_wrapper.py +113 -0
  91. traceforge/governance/rules.py +368 -0
  92. traceforge/governance/scorer.py +262 -0
  93. traceforge/governance/shield.py +318 -0
  94. traceforge/governance/state.py +466 -0
  95. traceforge/governance/types.py +176 -0
  96. traceforge/mappings/__init__.py +1 -0
  97. traceforge/mappings/aider.yaml +216 -0
  98. traceforge/mappings/aider_markdown.yaml +113 -0
  99. traceforge/mappings/amazonq.yaml +56 -0
  100. traceforge/mappings/antigravity.yaml +88 -0
  101. traceforge/mappings/claude.yaml +93 -0
  102. traceforge/mappings/cline.yaml +286 -0
  103. traceforge/mappings/codex.yaml +158 -0
  104. traceforge/mappings/continue_dev.yaml +57 -0
  105. traceforge/mappings/copilot.yaml +266 -0
  106. traceforge/mappings/copilot_markdown.yaml +72 -0
  107. traceforge/mappings/copilot_vscode.yaml +181 -0
  108. traceforge/mappings/crewai.yaml +592 -0
  109. traceforge/mappings/goose.yaml +128 -0
  110. traceforge/mappings/langgraph.yaml +165 -0
  111. traceforge/mappings/maf.yaml +90 -0
  112. traceforge/mappings/maf_transcript.yaml +99 -0
  113. traceforge/mappings/openai_agents.yaml +144 -0
  114. traceforge/mappings/opencode.yaml +473 -0
  115. traceforge/mappings/openhands.yaml +320 -0
  116. traceforge/mappings/pydantic_ai.yaml +183 -0
  117. traceforge/mappings/smolagents.yaml +89 -0
  118. traceforge/mappings/sweagent.yaml +82 -0
  119. traceforge/migrations/__init__.py +1 -0
  120. traceforge/migrations/env.py +60 -0
  121. traceforge/migrations/models.py +145 -0
  122. traceforge/migrations/runner.py +44 -0
  123. traceforge/migrations/script.py.mako +25 -0
  124. traceforge/migrations/versions/0001_initial.py +176 -0
  125. traceforge/migrations/versions/__init__.py +4 -0
  126. traceforge/models.py +29 -0
  127. traceforge/parsers/__init__.py +21 -0
  128. traceforge/parsers/aider.py +416 -0
  129. traceforge/parsers/base.py +226 -0
  130. traceforge/parsers/copilot.py +314 -0
  131. traceforge/phase/__init__.py +50 -0
  132. traceforge/phase/data/phase-model.joblib +0 -0
  133. traceforge/phase/data/potion-base-8M/README.md +99 -0
  134. traceforge/phase/data/potion-base-8M/config.json +13 -0
  135. traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
  136. traceforge/phase/data/potion-base-8M/modules.json +14 -0
  137. traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
  138. traceforge/phase/event_rows.py +107 -0
  139. traceforge/phase/features.py +468 -0
  140. traceforge/phase/inference.py +279 -0
  141. traceforge/phase/inferencer.py +171 -0
  142. traceforge/phase/segmentation.py +258 -0
  143. traceforge/pipeline.py +891 -0
  144. traceforge/preprocessors/__init__.py +34 -0
  145. traceforge/preprocessors/amazonq.py +224 -0
  146. traceforge/preprocessors/antigravity.py +116 -0
  147. traceforge/preprocessors/claude.py +95 -0
  148. traceforge/preprocessors/cline.py +36 -0
  149. traceforge/preprocessors/codex.py +311 -0
  150. traceforge/preprocessors/continue_dev.py +119 -0
  151. traceforge/preprocessors/copilot_vscode.py +171 -0
  152. traceforge/preprocessors/goose.py +156 -0
  153. traceforge/preprocessors/maf_transcript.py +84 -0
  154. traceforge/preprocessors/openai_agents.py +86 -0
  155. traceforge/preprocessors/opencode.py +85 -0
  156. traceforge/preprocessors/openhands.py +36 -0
  157. traceforge/preprocessors/pydantic_ai.py +62 -0
  158. traceforge/preprocessors/registry.py +24 -0
  159. traceforge/preprocessors/smolagents.py +90 -0
  160. traceforge/py.typed +0 -0
  161. traceforge/sdk/__init__.py +59 -0
  162. traceforge/sdk/gate_policy.py +63 -0
  163. traceforge/sdk/gate_types.py +140 -0
  164. traceforge/sdk/pipeline.py +265 -0
  165. traceforge/sdk/verdict.py +81 -0
  166. traceforge/sinks/__init__.py +23 -0
  167. traceforge/sinks/base.py +95 -0
  168. traceforge/sinks/callback.py +132 -0
  169. traceforge/sinks/console.py +112 -0
  170. traceforge/sinks/factory.py +94 -0
  171. traceforge/sinks/jsonl.py +125 -0
  172. traceforge/sinks/otel_exporter.py +212 -0
  173. traceforge/sinks/parquet.py +260 -0
  174. traceforge/sinks/s3.py +206 -0
  175. traceforge/sinks/sqlite_output.py +234 -0
  176. traceforge/sinks/webhook.py +136 -0
  177. traceforge/sources/__init__.py +18 -0
  178. traceforge/sources/auto_detect.py +173 -0
  179. traceforge/sources/base.py +45 -0
  180. traceforge/sources/file_poll.py +136 -0
  181. traceforge/sources/file_watch.py +221 -0
  182. traceforge/sources/http_poll.py +141 -0
  183. traceforge/sources/replay.py +63 -0
  184. traceforge/sources/sqlite.py +187 -0
  185. traceforge/sources/sse.py +198 -0
  186. traceforge/telemetry/__init__.py +192 -0
  187. traceforge/title/__init__.py +23 -0
  188. traceforge/title/_resolve.py +79 -0
  189. traceforge/title/context.py +295 -0
  190. traceforge/title/data/boilerplate_files.json +14 -0
  191. traceforge/title/heuristics.py +429 -0
  192. traceforge/title/hygiene.py +90 -0
  193. traceforge/title/inference.py +314 -0
  194. traceforge/title/inferencer.py +477 -0
  195. traceforge/title/naming.py +398 -0
  196. traceforge/trace.py +291 -0
  197. traceforge/tracking/__init__.py +30 -0
  198. traceforge/tracking/models.py +115 -0
  199. traceforge/tracking/phase_tracker.py +288 -0
  200. traceforge/types.py +315 -0
  201. traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
  202. traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
  203. traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
  204. traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  205. traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,258 @@
1
+ """Classical segmentation detectors as features (research/docs/03 §6).
2
+
3
+ We don't commit to one segmentation algorithm. We run several cheap detectors
4
+ over the per-event *phase stream* of a session and feed their outputs as
5
+ features; the classifier learns when to trust each. Every input here is a
6
+ canonical phase enum, so the features are portable by construction.
7
+
8
+ This is the per-event ``phase_signals`` stream — i.e. the enricher's own
9
+ ``metadata.phases`` estimate. Feeding its segmentation outputs back in as
10
+ features is exactly how the deterministic majority-vote "voting algo" survives
11
+ inside the learned classifier rather than being the final decision.
12
+
13
+ Detectors:
14
+
15
+ * ``bocpd`` — a categorical Bayesian Online Changepoint Detection
16
+ (Adams & MacKay 2007) with a Dirichlet-multinomial predictive and a constant
17
+ hazard. Emits the posterior changepoint probability and the (normalised)
18
+ expected run length at each step. Online / causal by construction.
19
+ * ``majority_vote`` — for each window ``w``, did the windowed (trailing)
20
+ majority phase change at this step, and how many steps since the last change.
21
+ * ``phase_entropy`` — Shannon entropy of the phase distribution in a trailing
22
+ window (high entropy ⇒ unstable region ⇒ boundary-likely).
23
+
24
+ All numeric knobs (window sizes, hazard's expected run length, Dirichlet
25
+ concentration) are passed in via :class:`SegmentationParams`, never baked in.
26
+ Every detector reads only the trailing prefix of the stream, so the features
27
+ are causal and computable online.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import math
33
+ from collections import deque
34
+ from dataclasses import dataclass
35
+
36
+ import numpy as np
37
+
38
+ #: Canonical phase vocabulary for the stream, plus a sentinel for "no signal".
39
+ PHASE_VOCAB: tuple[str, ...] = (
40
+ "planning",
41
+ "implementation",
42
+ "verification",
43
+ "exploration",
44
+ "review",
45
+ "none",
46
+ )
47
+ _VOCAB_INDEX = {p: i for i, p in enumerate(PHASE_VOCAB)}
48
+ _K = len(PHASE_VOCAB)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class SegmentationParams:
53
+ """Hyperparameters for the segmentation detectors (sourced from YAML)."""
54
+
55
+ windows: tuple[int, ...]
56
+ entropy_window: int
57
+ bocpd_expected_run_length: float
58
+ bocpd_alpha: float
59
+ bocpd_r_max: int
60
+
61
+ @property
62
+ def hazard(self) -> float:
63
+ return 1.0 / float(self.bocpd_expected_run_length)
64
+
65
+
66
+ def phase_of(phase_signals) -> str:
67
+ """Primary phase of an event from its phase_signals list."""
68
+
69
+ for s in phase_signals or ():
70
+ if s in _VOCAB_INDEX:
71
+ return s
72
+ return "none"
73
+
74
+
75
+ def _bocpd(
76
+ cats: list[int], hazard: float, alpha: float, r_max: int
77
+ ) -> tuple[list[float], list[float]]:
78
+ """Categorical BOCPD. Returns (changepoint_prob[t], expected_runlength[t])."""
79
+
80
+ cp_score: list[float] = []
81
+ exp_run: list[float] = []
82
+ run_post = np.zeros(r_max + 2)
83
+ run_post[0] = 1.0
84
+ recent: list[int] = []
85
+
86
+ for x in cats:
87
+ maxr = min(len(recent), r_max)
88
+ # Dirichlet-multinomial predictive of x under each run-length hypothesis.
89
+ pred = np.empty(maxr + 1)
90
+ pred[0] = (alpha) / (_K * alpha) # empty run → prior
91
+ cnt = np.zeros(_K)
92
+ n = 0
93
+ for r in range(1, maxr + 1):
94
+ cnt[recent[-r]] += 1
95
+ n += 1
96
+ pred[r] = (cnt[x] + alpha) / (n + _K * alpha)
97
+
98
+ cur = run_post[: maxr + 1].copy()
99
+ s = cur.sum()
100
+ if s == 0:
101
+ cur[0] = 1.0
102
+ else:
103
+ cur /= s
104
+ evidence = cur * pred
105
+ new = np.zeros(r_max + 2)
106
+ new[1 : maxr + 2] = evidence * (1.0 - hazard)
107
+ new[0] = evidence.sum() * hazard
108
+ total = new.sum()
109
+ if total > 0:
110
+ new /= total
111
+ cp_score.append(float(new[0]))
112
+ exp_run.append(float(np.dot(np.arange(r_max + 2), new) / r_max))
113
+ run_post = new
114
+ recent.append(x)
115
+
116
+ return cp_score, exp_run
117
+
118
+
119
+ def _windowed(
120
+ cats: list[str], windows: tuple[int, ...], entropy_window: int
121
+ ) -> list[dict[str, float]]:
122
+ """Majority-vote-change, steps-since-change, and entropy features per step."""
123
+
124
+ feats: list[dict[str, float]] = [dict() for _ in cats]
125
+ for w in windows:
126
+ last_majority: str | None = None
127
+ last_change = 0
128
+ for t in range(len(cats)):
129
+ window = cats[max(0, t - w + 1) : t + 1]
130
+ maj = max(set(window), key=window.count)
131
+ changed = last_majority is not None and maj != last_majority
132
+ feats[t][f"seg_majchange_w{w}"] = 1.0 if changed else 0.0
133
+ if changed:
134
+ last_change = t
135
+ feats[t][f"seg_since_majchange_w{w}"] = float(t - last_change)
136
+ last_majority = maj
137
+
138
+ for t in range(len(cats)):
139
+ window = cats[max(0, t - entropy_window + 1) : t + 1]
140
+ counts: dict[str, int] = {}
141
+ for c in window:
142
+ counts[c] = counts.get(c, 0) + 1
143
+ total = len(window)
144
+ ent = -sum((v / total) * math.log2(v / total) for v in counts.values())
145
+ feats[t][f"seg_phase_entropy_w{entropy_window}"] = float(ent)
146
+ return feats
147
+
148
+
149
+ def session_segmentation_features(
150
+ ordered_phase_signals: list,
151
+ ordered_event_ids: list[str],
152
+ params: SegmentationParams,
153
+ ) -> dict[str, dict[str, float]]:
154
+ """Compute per-event segmentation features for one session.
155
+
156
+ ``ordered_phase_signals`` and ``ordered_event_ids`` must be aligned and in
157
+ session sequence order. Returns ``event_id -> feature dict``.
158
+ """
159
+
160
+ cats = [phase_of(ps) for ps in ordered_phase_signals]
161
+ cat_ids = [_VOCAB_INDEX[c] for c in cats]
162
+ cp_score, exp_run = _bocpd(cat_ids, params.hazard, params.bocpd_alpha, params.bocpd_r_max)
163
+ windowed = _windowed(cats, params.windows, params.entropy_window)
164
+
165
+ out: dict[str, dict[str, float]] = {}
166
+ for i, eid in enumerate(ordered_event_ids):
167
+ d = dict(windowed[i])
168
+ d["seg_bocpd_changepoint"] = cp_score[i]
169
+ d["seg_bocpd_runlength"] = exp_run[i]
170
+ out[eid] = d
171
+ return out
172
+
173
+
174
+ class IncrementalSegmentation:
175
+ """Online, exactly-equivalent streaming form of the segmentation detectors.
176
+
177
+ Feeding events one at a time through :meth:`push` yields, for each event,
178
+ the *identical* feature dict :func:`session_segmentation_features` produces
179
+ for that event over the full prefix — because every detector here is causal
180
+ and carries only bounded state forward (BOCPD run-length posterior + the
181
+ last ``r_max`` categories; per-window majority + trailing category buffers).
182
+ This is what lets the pipeline stamp phases live without re-scanning the
183
+ session or resetting BOCPD on a moving window.
184
+ """
185
+
186
+ def __init__(self, params: SegmentationParams) -> None:
187
+ self.p = params
188
+ r_max = params.bocpd_r_max
189
+ self._run_post = np.zeros(r_max + 2)
190
+ self._run_post[0] = 1.0
191
+ self._recent: deque[int] = deque(maxlen=r_max)
192
+ self._t = 0
193
+ cat_span = max(max(params.windows, default=1), params.entropy_window)
194
+ self._cats: deque[str] = deque(maxlen=cat_span)
195
+ self._last_majority: dict[int, str | None] = {w: None for w in params.windows}
196
+ self._last_change: dict[int, int] = {w: 0 for w in params.windows}
197
+
198
+ def push(self, phase_signals) -> dict[str, float]:
199
+ c = phase_of(phase_signals)
200
+ x = _VOCAB_INDEX[c]
201
+ d: dict[str, float] = {}
202
+
203
+ # --- BOCPD step (mirror of _bocpd's per-observation update) ---
204
+ p = self.p
205
+ r_max = p.bocpd_r_max
206
+ hazard = p.hazard
207
+ alpha = p.bocpd_alpha
208
+ maxr = min(len(self._recent), r_max)
209
+ pred = np.empty(maxr + 1)
210
+ pred[0] = alpha / (_K * alpha)
211
+ cnt = np.zeros(_K)
212
+ n = 0
213
+ for r in range(1, maxr + 1):
214
+ cnt[self._recent[-r]] += 1
215
+ n += 1
216
+ pred[r] = (cnt[x] + alpha) / (n + _K * alpha)
217
+ cur = self._run_post[: maxr + 1].copy()
218
+ s = cur.sum()
219
+ if s == 0:
220
+ cur[0] = 1.0
221
+ else:
222
+ cur /= s
223
+ evidence = cur * pred
224
+ new = np.zeros(r_max + 2)
225
+ new[1 : maxr + 2] = evidence * (1.0 - hazard)
226
+ new[0] = evidence.sum() * hazard
227
+ total = new.sum()
228
+ if total > 0:
229
+ new /= total
230
+ d["seg_bocpd_changepoint"] = float(new[0])
231
+ d["seg_bocpd_runlength"] = float(np.dot(np.arange(r_max + 2), new) / r_max)
232
+ self._run_post = new
233
+ self._recent.append(x)
234
+
235
+ # --- windowed majority-change / steps-since-change / entropy ---
236
+ self._cats.append(c)
237
+ buf = list(self._cats)
238
+ t = self._t
239
+ for w in p.windows:
240
+ window = buf[-w:]
241
+ maj = max(set(window), key=window.count)
242
+ changed = self._last_majority[w] is not None and maj != self._last_majority[w]
243
+ d[f"seg_majchange_w{w}"] = 1.0 if changed else 0.0
244
+ if changed:
245
+ self._last_change[w] = t
246
+ d[f"seg_since_majchange_w{w}"] = float(t - self._last_change[w])
247
+ self._last_majority[w] = maj
248
+ ew = p.entropy_window
249
+ window = buf[-ew:]
250
+ counts: dict[str, int] = {}
251
+ for cc in window:
252
+ counts[cc] = counts.get(cc, 0) + 1
253
+ tot = len(window)
254
+ d[f"seg_phase_entropy_w{ew}"] = float(
255
+ -sum((v / tot) * math.log2(v / tot) for v in counts.values())
256
+ )
257
+ self._t += 1
258
+ return d