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,239 @@
|
|
|
1
|
+
"""Config loading with hierarchical precedence.
|
|
2
|
+
|
|
3
|
+
Precedence (highest → lowest):
|
|
4
|
+
1. Explicit kwargs passed to load_config()
|
|
5
|
+
2. TRACEFORGE_* environment variables
|
|
6
|
+
3. TRACEFORGE_CONFIG env var (explicit path override)
|
|
7
|
+
4. Project-local: ./traceforge.yaml
|
|
8
|
+
5. User-global: ~/.traceforge/config.yaml
|
|
9
|
+
6. Built-in defaults
|
|
10
|
+
|
|
11
|
+
On first config access, ~/.traceforge/ is auto-created with a default config
|
|
12
|
+
if it does not already exist. This is the post-install bootstrap — no separate
|
|
13
|
+
init command needed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
from traceforge.config.defaults import DEFAULT_CONFIG_YAML
|
|
26
|
+
from traceforge.config.models import TraceforgeConfig
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Module-level singleton (lazily loaded)
|
|
31
|
+
_config: TraceforgeConfig | None = None
|
|
32
|
+
_bootstrapped: bool = False
|
|
33
|
+
|
|
34
|
+
# Well-known paths
|
|
35
|
+
_USER_CONFIG_DIR = Path.home() / ".traceforge"
|
|
36
|
+
_USER_MAPPINGS_DIR = _USER_CONFIG_DIR / "mappings"
|
|
37
|
+
_USER_CONFIG_FILE = _USER_CONFIG_DIR / "config.yaml"
|
|
38
|
+
_PROJECT_CONFIG_FILE = Path("traceforge.yaml")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ensure_user_dir() -> None:
|
|
42
|
+
"""Create ~/.traceforge/ with default config if it doesn't exist.
|
|
43
|
+
|
|
44
|
+
Called once on first config access. Idempotent — safe to call multiple times.
|
|
45
|
+
This is the post-install bootstrap: no separate `traceforge init` needed.
|
|
46
|
+
"""
|
|
47
|
+
global _bootstrapped
|
|
48
|
+
if _bootstrapped:
|
|
49
|
+
return
|
|
50
|
+
_bootstrapped = True
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
_USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
_USER_MAPPINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
if not _USER_CONFIG_FILE.exists():
|
|
57
|
+
_USER_CONFIG_FILE.write_text(DEFAULT_CONFIG_YAML, encoding="utf-8")
|
|
58
|
+
logger.info("Created default config at %s", _USER_CONFIG_FILE)
|
|
59
|
+
except OSError as exc:
|
|
60
|
+
# Non-fatal: read-only filesystem, containerized env, etc.
|
|
61
|
+
logger.debug("Could not create ~/.traceforge/: %s", exc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_config_files() -> list[Path]:
|
|
65
|
+
"""Discover config files in precedence order (first = highest priority).
|
|
66
|
+
|
|
67
|
+
Returns paths that exist on disk. Higher-priority files override lower.
|
|
68
|
+
"""
|
|
69
|
+
files: list[Path] = []
|
|
70
|
+
|
|
71
|
+
# Explicit override via env var
|
|
72
|
+
env_path = os.environ.get("TRACEFORGE_CONFIG")
|
|
73
|
+
if env_path:
|
|
74
|
+
p = Path(env_path).expanduser().resolve()
|
|
75
|
+
if p.is_file():
|
|
76
|
+
files.append(p)
|
|
77
|
+
return files # explicit override skips discovery
|
|
78
|
+
else:
|
|
79
|
+
logger.warning("TRACEFORGE_CONFIG=%s does not exist, falling back to discovery", p)
|
|
80
|
+
|
|
81
|
+
# Project-local
|
|
82
|
+
project_file = _PROJECT_CONFIG_FILE.resolve()
|
|
83
|
+
if project_file.is_file():
|
|
84
|
+
files.append(project_file)
|
|
85
|
+
|
|
86
|
+
# User-global
|
|
87
|
+
if _USER_CONFIG_FILE.is_file():
|
|
88
|
+
files.append(_USER_CONFIG_FILE)
|
|
89
|
+
|
|
90
|
+
return files
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _load_yaml_file(path: Path) -> dict[str, Any]:
|
|
94
|
+
"""Load a YAML file, returning empty dict on parse errors."""
|
|
95
|
+
try:
|
|
96
|
+
with open(path) as f:
|
|
97
|
+
data = yaml.safe_load(f)
|
|
98
|
+
return data if isinstance(data, dict) else {}
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
logger.warning("Failed to load config from %s: %s", path, exc)
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _merge_dicts(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
"""Deep-merge override into base. Lists are replaced, not appended."""
|
|
106
|
+
merged = base.copy()
|
|
107
|
+
for key, value in override.items():
|
|
108
|
+
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
109
|
+
merged[key] = _merge_dicts(merged[key], value)
|
|
110
|
+
else:
|
|
111
|
+
merged[key] = value
|
|
112
|
+
return merged
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _apply_env_overrides(data: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
"""Apply TRACEFORGE_* environment variables as scalar overrides.
|
|
117
|
+
|
|
118
|
+
Supports flat keys only: TRACEFORGE_LOG_LEVEL → log_level.
|
|
119
|
+
Nested keys use double-underscore: TRACEFORGE_SDK__BATCH_SIZE → sdk.batch_size.
|
|
120
|
+
"""
|
|
121
|
+
prefix = "TRACEFORGE_"
|
|
122
|
+
skip = {"TRACEFORGE_CONFIG"} # meta env var, not a config field
|
|
123
|
+
|
|
124
|
+
for key, value in os.environ.items():
|
|
125
|
+
if not key.startswith(prefix) or key in skip:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Convert TRACEFORGE_SDK__BATCH_SIZE → ["sdk", "batch_size"]
|
|
129
|
+
parts = key[len(prefix) :].lower().split("__")
|
|
130
|
+
|
|
131
|
+
# Navigate to the right nesting level
|
|
132
|
+
target = data
|
|
133
|
+
for part in parts[:-1]:
|
|
134
|
+
if part not in target or not isinstance(target[part], dict):
|
|
135
|
+
target[part] = {}
|
|
136
|
+
target = target[part]
|
|
137
|
+
|
|
138
|
+
# Set the leaf value (attempt int/float coercion for numeric fields)
|
|
139
|
+
leaf = parts[-1]
|
|
140
|
+
target[leaf] = _coerce_env_value(value)
|
|
141
|
+
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _coerce_env_value(value: str) -> str | int | float | bool:
|
|
146
|
+
"""Best-effort coercion of env var string to Python scalar."""
|
|
147
|
+
if value.lower() in ("true", "1", "yes"):
|
|
148
|
+
return True
|
|
149
|
+
if value.lower() in ("false", "0", "no"):
|
|
150
|
+
return False
|
|
151
|
+
try:
|
|
152
|
+
return int(value)
|
|
153
|
+
except ValueError:
|
|
154
|
+
pass
|
|
155
|
+
try:
|
|
156
|
+
return float(value)
|
|
157
|
+
except ValueError:
|
|
158
|
+
pass
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_config(**overrides: Any) -> TraceforgeConfig:
|
|
163
|
+
"""Load traceforge configuration with full precedence chain.
|
|
164
|
+
|
|
165
|
+
On first call, ensures ~/.traceforge/ exists with a default config.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
**overrides: Explicit field values (highest precedence).
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Fully validated TraceforgeConfig instance.
|
|
172
|
+
"""
|
|
173
|
+
global _config
|
|
174
|
+
|
|
175
|
+
# Bootstrap user directory (idempotent, once per process)
|
|
176
|
+
_ensure_user_dir()
|
|
177
|
+
|
|
178
|
+
# Start with defaults (empty dict — Pydantic fills defaults)
|
|
179
|
+
merged: dict[str, Any] = {}
|
|
180
|
+
|
|
181
|
+
# Layer config files (lowest priority first)
|
|
182
|
+
config_files = _find_config_files()
|
|
183
|
+
for path in reversed(config_files): # reversed so highest-priority applied last
|
|
184
|
+
file_data = _load_yaml_file(path)
|
|
185
|
+
merged = _merge_dicts(merged, file_data)
|
|
186
|
+
logger.debug("Loaded config from %s", path)
|
|
187
|
+
|
|
188
|
+
# Layer env vars
|
|
189
|
+
merged = _apply_env_overrides(merged)
|
|
190
|
+
|
|
191
|
+
# Layer explicit overrides
|
|
192
|
+
if overrides:
|
|
193
|
+
merged = _merge_dicts(merged, overrides)
|
|
194
|
+
|
|
195
|
+
# Validate and construct
|
|
196
|
+
_config = TraceforgeConfig.model_validate(merged)
|
|
197
|
+
return _config
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_config_from_path(path: Any = None) -> TraceforgeConfig:
|
|
201
|
+
"""Resolve a full ``TraceforgeConfig``, optionally forcing a specific file.
|
|
202
|
+
|
|
203
|
+
When ``path`` is given it becomes the sole config source (applied via the
|
|
204
|
+
``TRACEFORGE_CONFIG`` override, then restored); when ``None`` the standard
|
|
205
|
+
discovery chain runs (``TRACEFORGE_CONFIG`` env, ``./traceforge.yaml``,
|
|
206
|
+
``~/.traceforge/config.yaml``).
|
|
207
|
+
|
|
208
|
+
This is the single ``path -> config`` resolution shared by the SDK
|
|
209
|
+
(:meth:`traceforge.sdk.Pipeline.from_config`) and governance
|
|
210
|
+
(:meth:`traceforge.governance.pipeline.GovernancePipeline.from_config`)
|
|
211
|
+
entry points, so both hydrate from the same resolved config.
|
|
212
|
+
"""
|
|
213
|
+
if path is None:
|
|
214
|
+
return load_config()
|
|
215
|
+
|
|
216
|
+
old_env = os.environ.get("TRACEFORGE_CONFIG")
|
|
217
|
+
os.environ["TRACEFORGE_CONFIG"] = str(path)
|
|
218
|
+
try:
|
|
219
|
+
return load_config()
|
|
220
|
+
finally:
|
|
221
|
+
if old_env is None:
|
|
222
|
+
os.environ.pop("TRACEFORGE_CONFIG", None)
|
|
223
|
+
else:
|
|
224
|
+
os.environ["TRACEFORGE_CONFIG"] = old_env
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_config() -> TraceforgeConfig:
|
|
228
|
+
"""Get the current config singleton, loading defaults if needed."""
|
|
229
|
+
global _config
|
|
230
|
+
if _config is None:
|
|
231
|
+
_config = load_config()
|
|
232
|
+
return _config
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def reset_config() -> None:
|
|
236
|
+
"""Reset the config singleton (for testing)."""
|
|
237
|
+
global _config, _bootstrapped
|
|
238
|
+
_config = None
|
|
239
|
+
_bootstrapped = False
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Mapping resolver — finds YAML framework mappings across all search paths.
|
|
2
|
+
|
|
3
|
+
Search order:
|
|
4
|
+
1. User mappings dirs (from config.mappings_dirs) — allows overrides
|
|
5
|
+
2. ~/.traceforge/mappings/ (default user dir)
|
|
6
|
+
3. Bundled mappings (src/traceforge/mappings/) — shipped with package
|
|
7
|
+
|
|
8
|
+
First match wins. User mappings override bundled ones with the same name.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.resources
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Bundled mappings location (inside the installed package)
|
|
20
|
+
_BUNDLED_MAPPINGS_DIR: Path | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_bundled_dir() -> Path:
|
|
24
|
+
"""Resolve the bundled mappings directory from the installed package."""
|
|
25
|
+
global _BUNDLED_MAPPINGS_DIR
|
|
26
|
+
if _BUNDLED_MAPPINGS_DIR is None:
|
|
27
|
+
ref = importlib.resources.files("traceforge") / "mappings"
|
|
28
|
+
# Traversable → Path (works for both installed and editable installs)
|
|
29
|
+
_BUNDLED_MAPPINGS_DIR = Path(str(ref))
|
|
30
|
+
return _BUNDLED_MAPPINGS_DIR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_mapping_path(name: str, extra_dirs: list[Path] | None = None) -> Path | None:
|
|
34
|
+
"""Find a YAML mapping file by framework name.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
name: Framework name (e.g. "copilot", "claude"). Resolved as {name}.yaml.
|
|
38
|
+
extra_dirs: Additional directories to search (from config.mappings_dirs).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to the YAML file, or None if not found.
|
|
42
|
+
"""
|
|
43
|
+
filename = f"{name}.yaml"
|
|
44
|
+
search_dirs: list[Path] = []
|
|
45
|
+
|
|
46
|
+
# User-specified dirs (highest priority)
|
|
47
|
+
if extra_dirs:
|
|
48
|
+
for d in extra_dirs:
|
|
49
|
+
resolved = Path(d).expanduser().resolve()
|
|
50
|
+
if resolved.is_dir():
|
|
51
|
+
search_dirs.append(resolved)
|
|
52
|
+
|
|
53
|
+
# Default user mappings dir
|
|
54
|
+
user_mappings = Path.home() / ".traceforge" / "mappings"
|
|
55
|
+
if user_mappings.is_dir() and user_mappings not in search_dirs:
|
|
56
|
+
search_dirs.append(user_mappings)
|
|
57
|
+
|
|
58
|
+
# Bundled mappings (lowest priority)
|
|
59
|
+
bundled = _get_bundled_dir()
|
|
60
|
+
if bundled.is_dir():
|
|
61
|
+
search_dirs.append(bundled)
|
|
62
|
+
|
|
63
|
+
# First match wins
|
|
64
|
+
for directory in search_dirs:
|
|
65
|
+
candidate = directory / filename
|
|
66
|
+
if candidate.is_file():
|
|
67
|
+
return candidate
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_available_mappings(extra_dirs: list[Path] | None = None) -> dict[str, Path]:
|
|
73
|
+
"""List all available mapping names and their resolved paths.
|
|
74
|
+
|
|
75
|
+
Later entries do not override earlier ones (first-found wins).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict of framework_name → path.
|
|
79
|
+
"""
|
|
80
|
+
mappings: dict[str, Path] = {}
|
|
81
|
+
search_dirs: list[Path] = []
|
|
82
|
+
|
|
83
|
+
# Build search order (same as resolve_mapping_path)
|
|
84
|
+
if extra_dirs:
|
|
85
|
+
for d in extra_dirs:
|
|
86
|
+
resolved = Path(d).expanduser().resolve()
|
|
87
|
+
if resolved.is_dir():
|
|
88
|
+
search_dirs.append(resolved)
|
|
89
|
+
|
|
90
|
+
user_mappings = Path.home() / ".traceforge" / "mappings"
|
|
91
|
+
if user_mappings.is_dir() and user_mappings not in search_dirs:
|
|
92
|
+
search_dirs.append(user_mappings)
|
|
93
|
+
|
|
94
|
+
bundled = _get_bundled_dir()
|
|
95
|
+
if bundled.is_dir():
|
|
96
|
+
search_dirs.append(bundled)
|
|
97
|
+
|
|
98
|
+
for directory in search_dirs:
|
|
99
|
+
for yaml_file in sorted(directory.glob("*.yaml")):
|
|
100
|
+
name = yaml_file.stem
|
|
101
|
+
if name not in mappings: # first-found wins (user overrides bundled)
|
|
102
|
+
mappings[name] = yaml_file
|
|
103
|
+
|
|
104
|
+
return mappings
|