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.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Distil a span's events into the compact context string the titler consumes.
|
|
2
|
+
|
|
3
|
+
This is the **serve-side** half of the title train/serve contract. The titler
|
|
4
|
+
was fine-tuned on ``distilled_context(window)`` strings built from the labelling
|
|
5
|
+
corpus, where each corpus row is a :class:`~traceforge.types.SessionEvent`
|
|
6
|
+
projected through :func:`traceforge.phase.event_rows.event_to_feature_row`. Live
|
|
7
|
+
inference projects the events of a freshly-closed activity/step span through the
|
|
8
|
+
**same** projection and the **same** distiller here, so there is no train/serve
|
|
9
|
+
skew by construction.
|
|
10
|
+
|
|
11
|
+
The distilled package is a handful of highest-signal, source-agnostic slots --
|
|
12
|
+
``intent`` (a stated ``report_intent`` gerund, gold-quality title text when
|
|
13
|
+
present), ``actions`` (tool sequence), ``files`` (touched source files, minus
|
|
14
|
+
learned boilerplate), ``symbols`` (salient code identifiers acted upon) and a
|
|
15
|
+
short free-text ``notes`` tail. Everything is mined from the raw payload /
|
|
16
|
+
classification columns, never from a source-specific schema, so the same code
|
|
17
|
+
runs across agent frameworks.
|
|
18
|
+
|
|
19
|
+
The boilerplate file set is **learned from the full training corpus** (files
|
|
20
|
+
present in the overwhelming majority of a corpus's sessions carry ~zero IDF and
|
|
21
|
+
leak from tool-doc / system-prompt example snippets) and frozen into a packaged
|
|
22
|
+
artifact, so it is applied source-agnostically at inference with no corpus
|
|
23
|
+
dependency. See :data:`_BOILER_FILE`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
#: tool-call ids ("toolu_bdrk_01XW9..."), hex blobs and long high-entropy ids are
|
|
34
|
+
#: pure noise tokens that leak into narration; stripped from notes/symbols.
|
|
35
|
+
_IDJUNK_RE = re.compile(r"\btoolu_\w+|\b[a-fA-F0-9]{16,}\b|\b[A-Za-z0-9]{24,}\b")
|
|
36
|
+
#: web-fetched bundler assets ("monaco-cvufusc8.js", "index-bcttgcnd.css"): a
|
|
37
|
+
#: stem carrying a hash-like segment -> drop as noise, not a real source file.
|
|
38
|
+
_ASSET_RE = re.compile(r"[-_][a-z0-9]*\d[a-z0-9]*\.|[-_][bcdfghjklmnpqrstvwxz]{6,}\.")
|
|
39
|
+
|
|
40
|
+
#: A real filename = a >=2-char stem + a short alphabetic extension drawn from
|
|
41
|
+
#: the concrete code/doc/config extensions seen in agent traces.
|
|
42
|
+
_FILE_EXT = (
|
|
43
|
+
"py|pyi|md|rst|txt|js|jsx|ts|tsx|mjs|cjs|json|jsonl|yaml|yml|toml|ini|cfg|"
|
|
44
|
+
"conf|sh|bash|ps1|bat|sql|html|htm|css|scss|go|rs|java|kt|c|h|cpp|hpp|cc|"
|
|
45
|
+
"rb|php|cs|swift|lock|xml|csv|tsv|env|gitignore|dockerfile|makefile"
|
|
46
|
+
)
|
|
47
|
+
_FILE_RE = re.compile(rf"^[a-z][\w\-]{{1,}}\.({_FILE_EXT})$")
|
|
48
|
+
|
|
49
|
+
#: Absolute/relative path -> trailing basename, so notes read cleanly.
|
|
50
|
+
_ABS_PATH = re.compile(r"(?:[A-Za-z]:\\|/)[\w\\/.\- ]*?([\w.\-]+\.\w{1,5})")
|
|
51
|
+
|
|
52
|
+
#: A code SYMBOL: a concrete identifier the agent acted on (function/class/const/
|
|
53
|
+
#: dotted path), distinct from a bare filename -- carries the segment's subject.
|
|
54
|
+
_SYM_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]+)*")
|
|
55
|
+
_BTICK_RE = re.compile(r"`([^`\n]{2,40})`")
|
|
56
|
+
#: "structured" = snake_case / camelCase / dotted.path -> almost always a real
|
|
57
|
+
#: domain entity; ranked ahead of bare ALL-CAPS keywords (NOT, NULL, WHERE).
|
|
58
|
+
_STRUCT_RE = re.compile(r"_|[a-z][A-Z]|\.[A-Za-z]")
|
|
59
|
+
_ACRONYM_RE = re.compile(r"^[A-Z][A-Z0-9]+$")
|
|
60
|
+
|
|
61
|
+
#: code-shaped entity test for payload-mined objects (dot-ext, snake/camel,
|
|
62
|
+
#: ALLCAPS>=2, digit, slash, class/function keyword).
|
|
63
|
+
_CODESHAPE = re.compile(
|
|
64
|
+
r"(\.\w{1,5}\b)|(_)|([a-z][A-Z])|(\b[A-Z]{2,}\b)|(\d)|(/)|(\bclass\b|\bfunction\b)"
|
|
65
|
+
)
|
|
66
|
+
_IDENT = re.compile(r"[A-Za-z_][\w./-]{2,}")
|
|
67
|
+
|
|
68
|
+
#: Stopwords for object-side filtering (shared with the title composer).
|
|
69
|
+
STOP = set(
|
|
70
|
+
"the a an of to and in for with on is are be this that it we our us you your i "
|
|
71
|
+
"let lets now first then next also will should can use via into from as at".split()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
#: Packaged set of boilerplate files learned from the full training corpus.
|
|
75
|
+
_BOILER_FILE = Path(__file__).resolve().parent / "data" / "boilerplate_files.json"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load_boilerplate() -> frozenset[str]:
|
|
79
|
+
try:
|
|
80
|
+
with open(_BOILER_FILE, encoding="utf-8") as fh:
|
|
81
|
+
return frozenset(str(x).lower() for x in json.load(fh))
|
|
82
|
+
except (OSError, ValueError):
|
|
83
|
+
return frozenset()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_BOILER = _load_boilerplate()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ----------------------------------------------------------------- payload text
|
|
90
|
+
def _payload_obj(row: dict):
|
|
91
|
+
pj = row.get("payload_json")
|
|
92
|
+
if not isinstance(pj, str):
|
|
93
|
+
return None
|
|
94
|
+
try:
|
|
95
|
+
return json.loads(pj)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return pj
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _iter_strings(o):
|
|
101
|
+
if isinstance(o, str):
|
|
102
|
+
yield o
|
|
103
|
+
elif isinstance(o, dict):
|
|
104
|
+
for v in o.values():
|
|
105
|
+
yield from _iter_strings(v)
|
|
106
|
+
elif isinstance(o, (list, tuple)):
|
|
107
|
+
for v in o:
|
|
108
|
+
yield from _iter_strings(v)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def payload_text(row: dict) -> str:
|
|
112
|
+
"""All string leaves of the serialized payload, flattened."""
|
|
113
|
+
o = _payload_obj(row)
|
|
114
|
+
if o is None:
|
|
115
|
+
return ""
|
|
116
|
+
if isinstance(o, str):
|
|
117
|
+
return o
|
|
118
|
+
return " ".join(_iter_strings(o))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def denoise(t: str) -> str:
|
|
122
|
+
if not t:
|
|
123
|
+
return ""
|
|
124
|
+
t = _ABS_PATH.sub(r"\1", t)
|
|
125
|
+
t = re.split(r"\b(?:function|end_of_edit)\b", t, flags=re.I)[0]
|
|
126
|
+
t = re.sub(r"\breport_intent\b", "", t, flags=re.I)
|
|
127
|
+
return re.sub(r"\s+", " ", t).strip()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def sentences(text: str) -> list[str]:
|
|
131
|
+
return [s.strip() for s in re.split(r"(?<=[.!?\n])\s+", (text or "").strip()) if s.strip()]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------- intent
|
|
135
|
+
def extract_intent(row: dict) -> str | None:
|
|
136
|
+
"""The gerund-form ``arguments.intent`` of a ``report_intent`` tool call --
|
|
137
|
+
gold-quality title text when present, else ``None``."""
|
|
138
|
+
if "report_intent" not in str(row.get("tool_name", "")).lower():
|
|
139
|
+
pj = row.get("payload_json")
|
|
140
|
+
if not isinstance(pj, str) or "report_intent" not in pj:
|
|
141
|
+
return None
|
|
142
|
+
o = _payload_obj(row)
|
|
143
|
+
if isinstance(o, dict) and str(o.get("tool_name", "")).lower() == "report_intent":
|
|
144
|
+
intent = (o.get("arguments") or {}).get("intent")
|
|
145
|
+
if isinstance(intent, str) and len(intent.split()) >= 2:
|
|
146
|
+
return intent.strip()
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ----------------------------------------------------------------------- slots
|
|
151
|
+
def tool_seq(rows: list[dict]) -> list[str]:
|
|
152
|
+
seen: set[str] = set()
|
|
153
|
+
out: list[str] = []
|
|
154
|
+
for r in rows:
|
|
155
|
+
tn = r.get("tool_name")
|
|
156
|
+
if tn is None:
|
|
157
|
+
continue
|
|
158
|
+
t = str(tn).lower()
|
|
159
|
+
if t in ("none", "nan", ""):
|
|
160
|
+
continue
|
|
161
|
+
if t not in seen:
|
|
162
|
+
seen.add(t)
|
|
163
|
+
out.append(t)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _payload_entities(row: dict) -> set[str]:
|
|
168
|
+
"""Concrete entities acted UPON: file/binary names, structures, code
|
|
169
|
+
identifiers (not the tool/action verb side)."""
|
|
170
|
+
ents: set[str] = set()
|
|
171
|
+
for col in ("binaries", "structure"):
|
|
172
|
+
v = row.get(col)
|
|
173
|
+
if isinstance(v, (list, tuple)):
|
|
174
|
+
ents |= {str(x).lower() for x in v if x is not None}
|
|
175
|
+
elif v is not None and str(v) != "None":
|
|
176
|
+
ents.add(str(v).lower())
|
|
177
|
+
pj = row.get("payload_json")
|
|
178
|
+
if isinstance(pj, str):
|
|
179
|
+
for m in re.findall(r"[\w.\-]+\.\w{1,5}", pj):
|
|
180
|
+
ents.add(os.path.basename(m).lower())
|
|
181
|
+
for m in _IDENT.findall(pj):
|
|
182
|
+
if _CODESHAPE.search(m):
|
|
183
|
+
ents.add(m.lower())
|
|
184
|
+
return {e for e in ents if len(e) > 1}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def files_touched(rows: list[dict]) -> list[str]:
|
|
188
|
+
seen: set[str] = set()
|
|
189
|
+
out: list[str] = []
|
|
190
|
+
for r in rows:
|
|
191
|
+
for e in _payload_entities(r):
|
|
192
|
+
if _FILE_RE.match(e) and not _ASSET_RE.search(e) and e not in seen:
|
|
193
|
+
seen.add(e)
|
|
194
|
+
out.append(e)
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def clean_notes(narr: list[str]) -> list[str]:
|
|
199
|
+
"""Drop tool-call ids / hex blobs that leak into narration."""
|
|
200
|
+
out: list[str] = []
|
|
201
|
+
for s in narr:
|
|
202
|
+
s = _IDJUNK_RE.sub("", s)
|
|
203
|
+
s = re.sub(r"\s{2,}", " ", s).strip()
|
|
204
|
+
if len(s.split()) >= 3:
|
|
205
|
+
out.append(s)
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def narration(rows: list[dict]) -> list[str]:
|
|
210
|
+
out: list[str] = []
|
|
211
|
+
for r in rows:
|
|
212
|
+
k = str(r.get("kind"))
|
|
213
|
+
if k.startswith("message.assistant") or k.startswith("message.user"):
|
|
214
|
+
for s in sentences(denoise(payload_text(r))):
|
|
215
|
+
if 3 <= len(s.split()) <= 40:
|
|
216
|
+
out.append(s)
|
|
217
|
+
return out[:25]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _sym_ok(m: str, drop: set[str]) -> bool:
|
|
221
|
+
if len(m) < 3 or m.lower() in STOP or m.lower() in drop:
|
|
222
|
+
return False
|
|
223
|
+
if _IDJUNK_RE.search(m) or _ASSET_RE.search(m):
|
|
224
|
+
return False
|
|
225
|
+
return bool(_STRUCT_RE.search(m) or _ACRONYM_RE.match(m))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def salient_symbols(
|
|
229
|
+
rows: list[dict], drop: frozenset[str] = frozenset(), cap: int = 5
|
|
230
|
+
) -> list[str]:
|
|
231
|
+
"""Highest-signal code identifiers acted on in the segment, ranked by
|
|
232
|
+
salience: backtick-quoted > structured (snake/camel/dotted) by frequency >
|
|
233
|
+
bare ALL-CAPS keywords. Source-agnostic (mines raw payload text)."""
|
|
234
|
+
import collections
|
|
235
|
+
|
|
236
|
+
dropl = {d.lower() for d in drop} | {os.path.splitext(d)[0].lower() for d in drop}
|
|
237
|
+
bt: collections.Counter = collections.Counter()
|
|
238
|
+
struct: collections.Counter = collections.Counter()
|
|
239
|
+
acro: collections.Counter = collections.Counter()
|
|
240
|
+
for r in rows:
|
|
241
|
+
txt = payload_text(r)
|
|
242
|
+
if not txt:
|
|
243
|
+
continue
|
|
244
|
+
quoted = {q for span in _BTICK_RE.findall(txt) for q in _SYM_RE.findall(span)}
|
|
245
|
+
for m in _SYM_RE.findall(txt):
|
|
246
|
+
if not _sym_ok(m, dropl):
|
|
247
|
+
continue
|
|
248
|
+
if m in quoted:
|
|
249
|
+
bt[m] += 1
|
|
250
|
+
elif _STRUCT_RE.search(m):
|
|
251
|
+
struct[m] += 1
|
|
252
|
+
else:
|
|
253
|
+
acro[m] += 1
|
|
254
|
+
seen: set[str] = set()
|
|
255
|
+
out: list[str] = []
|
|
256
|
+
for ctr in (bt, struct, acro):
|
|
257
|
+
for w, _ in ctr.most_common():
|
|
258
|
+
k = w.lower()
|
|
259
|
+
if k in seen:
|
|
260
|
+
continue
|
|
261
|
+
seen.add(k)
|
|
262
|
+
out.append(w)
|
|
263
|
+
if len(out) >= cap:
|
|
264
|
+
return out
|
|
265
|
+
return out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------- assemble
|
|
269
|
+
def distilled_context(rows: list[dict]) -> str:
|
|
270
|
+
"""The golden platter: the few highest-signal facts about a span.
|
|
271
|
+
|
|
272
|
+
``rows`` are :func:`event_to_feature_row` projections of the span's events,
|
|
273
|
+
in sequence order. Returns the slot string the titler was trained on, or
|
|
274
|
+
``"(no signal)"`` when nothing useful is extractable.
|
|
275
|
+
"""
|
|
276
|
+
parts: list[str] = []
|
|
277
|
+
intent = next((extract_intent(r) for r in rows if extract_intent(r)), None)
|
|
278
|
+
if intent:
|
|
279
|
+
parts.append(f"intent: {intent}")
|
|
280
|
+
tools = tool_seq(rows)
|
|
281
|
+
if tools:
|
|
282
|
+
parts.append("actions: " + ", ".join(tools[:6]))
|
|
283
|
+
files = [f for f in files_touched(rows) if f not in _BOILER]
|
|
284
|
+
if files:
|
|
285
|
+
parts.append("files: " + ", ".join(files[:5]))
|
|
286
|
+
syms = salient_symbols(rows, drop=frozenset(files) | _BOILER)
|
|
287
|
+
if syms:
|
|
288
|
+
parts.append("symbols: " + ", ".join(syms))
|
|
289
|
+
narr = clean_notes(narration(rows))
|
|
290
|
+
if narr:
|
|
291
|
+
parts.append("notes: " + " ".join(narr[:2])[:240])
|
|
292
|
+
return " | ".join(parts) if parts else "(no signal)"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
__all__ = ["STOP", "distilled_context"]
|