traceforge-pcb 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.
- traceforge/__init__.py +3 -0
- traceforge/__main__.py +3 -0
- traceforge/acp_server.py +570 -0
- traceforge/agent.py +680 -0
- traceforge/altium/__init__.py +6 -0
- traceforge/altium/cfb.py +70 -0
- traceforge/altium/cli.py +60 -0
- traceforge/altium/model.py +163 -0
- traceforge/altium/outputs.py +24 -0
- traceforge/altium/pcb.py +301 -0
- traceforge/altium/project.py +328 -0
- traceforge/altium/schematic.py +400 -0
- traceforge/altium/tools.py +166 -0
- traceforge/approval.py +219 -0
- traceforge/attachments.py +346 -0
- traceforge/background.py +253 -0
- traceforge/bench/__init__.py +5 -0
- traceforge/bench/adapters.py +337 -0
- traceforge/bench/checks.py +410 -0
- traceforge/bench/cli.py +121 -0
- traceforge/bench/design.py +439 -0
- traceforge/bench/kicad.py +99 -0
- traceforge/bench/partscheck.py +159 -0
- traceforge/bench/review.py +100 -0
- traceforge/bench/runner.py +226 -0
- traceforge/bench/score.py +204 -0
- traceforge/bench/tasks.py +102 -0
- traceforge/chat.py +1185 -0
- traceforge/checks.py +328 -0
- traceforge/checks_sides.py +118 -0
- traceforge/cli.py +2224 -0
- traceforge/clipboard.py +107 -0
- traceforge/config.py +299 -0
- traceforge/context.py +276 -0
- traceforge/design/__init__.py +1 -0
- traceforge/design/constraints.py +397 -0
- traceforge/design/generate.py +1789 -0
- traceforge/design/outline.py +905 -0
- traceforge/design/schematic_layout.py +932 -0
- traceforge/design/schematic_lint.py +329 -0
- traceforge/design/schematic_route.py +207 -0
- traceforge/design/sheets.py +854 -0
- traceforge/design/sides.py +328 -0
- traceforge/design/spec.py +462 -0
- traceforge/design/specsheet.py +2351 -0
- traceforge/design/specsheet_render.py +183 -0
- traceforge/desktop/__init__.py +109 -0
- traceforge/desktop/keys.py +90 -0
- traceforge/desktop/project.py +443 -0
- traceforge/desktop/review.py +20 -0
- traceforge/desktop/server.py +1277 -0
- traceforge/desktop/static/app.css +223 -0
- traceforge/desktop/static/app.js +683 -0
- traceforge/desktop/static/highlight.js +77 -0
- traceforge/desktop/static/index.html +89 -0
- traceforge/desktop/static/md.js +90 -0
- traceforge/desktop/static/viewer.js +213 -0
- traceforge/desktop/symbols.py +212 -0
- traceforge/desktop/update.py +65 -0
- traceforge/diagnostics.py +67 -0
- traceforge/errata.py +219 -0
- traceforge/fab.py +108 -0
- traceforge/fab_layers.py +44 -0
- traceforge/features/__init__.py +6 -0
- traceforge/features/candidates.py +117 -0
- traceforge/features/cli.py +200 -0
- traceforge/features/package.py +433 -0
- traceforge/features/revdiff.py +224 -0
- traceforge/features/sarif.py +145 -0
- traceforge/features/viewer.py +265 -0
- traceforge/firmware/__init__.py +3 -0
- traceforge/firmware/cli.py +174 -0
- traceforge/firmware/drivers.py +372 -0
- traceforge/firmware/drivers_data.py +256 -0
- traceforge/firmware/mcus.py +177 -0
- traceforge/firmware/netlist.py +55 -0
- traceforge/firmware/pinmap.py +296 -0
- traceforge/firmware/scaffold.py +466 -0
- traceforge/firmware/toolchain.py +262 -0
- traceforge/hooks.py +237 -0
- traceforge/kicad/__init__.py +0 -0
- traceforge/kicad/cli.py +519 -0
- traceforge/kicad/diffpair.py +174 -0
- traceforge/kicad/edgegeom.py +191 -0
- traceforge/kicad/edit.py +405 -0
- traceforge/kicad/escape.py +1410 -0
- traceforge/kicad/formats.py +1073 -0
- traceforge/kicad/impedance.py +320 -0
- traceforge/kicad/install.py +75 -0
- traceforge/kicad/libraries.py +490 -0
- traceforge/kicad/models3d.py +942 -0
- traceforge/kicad/outputs.py +81 -0
- traceforge/kicad/pcb.py +787 -0
- traceforge/kicad/planes.py +1242 -0
- traceforge/kicad/project.py +136 -0
- traceforge/kicad/route.py +2698 -0
- traceforge/kicad/routegeom.py +164 -0
- traceforge/kicad/schematic.py +268 -0
- traceforge/kicad/sides.py +34 -0
- traceforge/kicad/stackup.py +424 -0
- traceforge/kicad_plugin/__init__.py +12 -0
- traceforge/kicad_plugin/actions.py +92 -0
- traceforge/kicad_plugin/cli.py +66 -0
- traceforge/kicad_plugin/client.py +86 -0
- traceforge/kicad_plugin/context.py +257 -0
- traceforge/kicad_plugin/discovery.py +81 -0
- traceforge/kicad_plugin/install.py +203 -0
- traceforge/kicad_plugin/overlay.py +60 -0
- traceforge/kicad_plugin/payload/action_plugin.py +65 -0
- traceforge/kicad_plugin/payload/ipc_main.py +42 -0
- traceforge/kicad_plugin/payload/plugin.json +90 -0
- traceforge/kicad_plugin/payload/requirements.txt +1 -0
- traceforge/kicad_plugin/views.py +202 -0
- traceforge/langchain.py +57 -0
- traceforge/local.py +580 -0
- traceforge/mcp_server.py +304 -0
- traceforge/models.py +310 -0
- traceforge/next_steps.py +110 -0
- traceforge/parts/__init__.py +52 -0
- traceforge/parts/alternates.py +488 -0
- traceforge/parts/bom.py +423 -0
- traceforge/parts/brief.py +705 -0
- traceforge/parts/catalog.py +297 -0
- traceforge/parts/cli_sourcing.py +156 -0
- traceforge/parts/data/duty_rates.yaml +37 -0
- traceforge/parts/data/hs_codes.yaml +103 -0
- traceforge/parts/datasheet.py +278 -0
- traceforge/parts/fx.py +100 -0
- traceforge/parts/model.py +185 -0
- traceforge/parts/net.py +187 -0
- traceforge/parts/offers.py +46 -0
- traceforge/parts/packages.py +397 -0
- traceforge/parts/pinout.py +714 -0
- traceforge/parts/providers/__init__.py +17 -0
- traceforge/parts/providers/base.py +100 -0
- traceforge/parts/providers/jlc.py +364 -0
- traceforge/parts/providers/keyed.py +661 -0
- traceforge/parts/report.py +508 -0
- traceforge/parts/supply.py +330 -0
- traceforge/parts/verify.py +455 -0
- traceforge/parts/web.py +575 -0
- traceforge/plans.py +70 -0
- traceforge/preferences.py +127 -0
- traceforge/prices.py +130 -0
- traceforge/prices.yaml +28 -0
- traceforge/prompts/__init__.py +54 -0
- traceforge/prompts/system.md +103 -0
- traceforge/providers.py +764 -0
- traceforge/remote/__init__.py +33 -0
- traceforge/remote/client.py +422 -0
- traceforge/remote/oidc.py +240 -0
- traceforge/remote/orgs.py +510 -0
- traceforge/remote/server.py +690 -0
- traceforge/remote/store.py +100 -0
- traceforge/review_changes.py +187 -0
- traceforge/routing.py +99 -0
- traceforge/runtime.py +435 -0
- traceforge/sessions.py +324 -0
- traceforge/sexpr.py +279 -0
- traceforge/sim/__init__.py +17 -0
- traceforge/sim/acmath.py +234 -0
- traceforge/sim/circuit.py +499 -0
- traceforge/sim/cli.py +162 -0
- traceforge/sim/engine.py +172 -0
- traceforge/sim/ltspice.py +175 -0
- traceforge/sim/measure.py +123 -0
- traceforge/sim/models.py +76 -0
- traceforge/sim/project.py +218 -0
- traceforge/sim/rawfile.py +123 -0
- traceforge/sim/results.py +298 -0
- traceforge/sim/tune.py +172 -0
- traceforge/skill_commands.py +165 -0
- traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
- traceforge/skill_library/compliance-review/SKILL.md +51 -0
- traceforge/skill_library/design-review/SKILL.md +47 -0
- traceforge/skill_library/errata-hunt/SKILL.md +30 -0
- traceforge/skill_library/firmware/SKILL.md +26 -0
- traceforge/skill_library/power-budget/SKILL.md +27 -0
- traceforge/skill_library/release-package/SKILL.md +29 -0
- traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
- traceforge/skill_library/thermal-review/SKILL.md +15 -0
- traceforge/skills.py +131 -0
- traceforge/thermal.py +557 -0
- traceforge/tools/__init__.py +48 -0
- traceforge/tools/agent_tools.py +213 -0
- traceforge/tools/base.py +303 -0
- traceforge/tools/batch.py +155 -0
- traceforge/tools/connections.py +245 -0
- traceforge/tools/files.py +777 -0
- traceforge/tools/firmware_tools.py +218 -0
- traceforge/tools/kicad_tools.py +1072 -0
- traceforge/tools/models3d_tools.py +141 -0
- traceforge/tools/parts_tools.py +473 -0
- traceforge/tools/processes.py +246 -0
- traceforge/tools/review_tools.py +178 -0
- traceforge/tools/sim_tools.py +230 -0
- traceforge/tui.py +620 -0
- traceforge/updates.py +124 -0
- traceforge/validate/__init__.py +77 -0
- traceforge/validate/builtins.py +1091 -0
- traceforge/validate/circuits.py +1240 -0
- traceforge/validate/config.py +410 -0
- traceforge/validate/default.yaml +113 -0
- traceforge/validate/design.py +191 -0
- traceforge/validate/explain.py +112 -0
- traceforge/validate/grounding.py +169 -0
- traceforge/validate/ipc2221.py +350 -0
- traceforge/validate/multilayer.py +805 -0
- traceforge/validate/netmodel.py +261 -0
- traceforge/validate/partcheck.py +472 -0
- traceforge/validate/report.py +280 -0
- traceforge/validate/review.py +322 -0
- traceforge/validate/runner.py +662 -0
- traceforge/workspace.py +171 -0
- traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
- traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
- traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
- traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
- traceforge_pcb-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""The validation graph: stages, checks, policy profiles, limits and reviewer settings.
|
|
2
|
+
|
|
3
|
+
The packaged ``default.yaml`` is the base. A project layers
|
|
4
|
+
``.traceforge/validate.yaml`` on top, and ``--config FILE`` layers once more.
|
|
5
|
+
Each layer either starts with ``extends: default`` (merge into what is there)
|
|
6
|
+
or replaces the graph outright. Checks merge by id; ``disable: [ids]`` drops
|
|
7
|
+
checks; ``policy``, ``limits`` and ``reviewer`` merge key by key.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
from dataclasses import asdict, dataclass, field
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
SEVERITIES = ("P0", "P1", "P2", "P3")
|
|
21
|
+
CONFIDENCE = {"low": 0, "medium": 1, "high": 2}
|
|
22
|
+
GATES = ("stage", "full", "strict")
|
|
23
|
+
SCOPES = ("project", "per_part", "per_sheet", "per_netclass")
|
|
24
|
+
INPUT_KINDS = ("sch", "pcb", "pro", "libs", "intent", "outputs", "all")
|
|
25
|
+
KINDS = ("builtin", "review")
|
|
26
|
+
PROJECT_CONFIG = Path(".traceforge") / "validate.yaml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigError(ValueError):
|
|
30
|
+
"""The graph is invalid (exit code 2)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Stage:
|
|
35
|
+
id: str
|
|
36
|
+
title: str
|
|
37
|
+
question: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CheckSpec:
|
|
42
|
+
id: str
|
|
43
|
+
stage: str
|
|
44
|
+
kind: str # builtin | review
|
|
45
|
+
title: str
|
|
46
|
+
prompt: str = ""
|
|
47
|
+
needs: list[str] = field(default_factory=list) # hard prerequisites ("requires")
|
|
48
|
+
after: list[str] = field(default_factory=list) # ordering only
|
|
49
|
+
gate: str = "blocking" # blocking | advisory
|
|
50
|
+
builtin: str = "" # builtin implementation name (defaults to the id)
|
|
51
|
+
scope: str = "project"
|
|
52
|
+
batch: int = 4
|
|
53
|
+
repeat: int = 1
|
|
54
|
+
inputs: list[str] = field(default_factory=lambda: ["all"])
|
|
55
|
+
effort: str | None = None
|
|
56
|
+
enabled: bool = True
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def requires(self) -> list[str]:
|
|
60
|
+
return self.needs
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
data = asdict(self)
|
|
64
|
+
data["requires"] = data.pop("needs")
|
|
65
|
+
return {k: v for k, v in data.items() if v not in (None, "", [])}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Profile:
|
|
70
|
+
fail_on: list[str]
|
|
71
|
+
min_confidence: str = "medium"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ReviewerSettings:
|
|
76
|
+
preamble: str = ""
|
|
77
|
+
timeout_s: int = 1800
|
|
78
|
+
max_repairs: int = 2
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ValidationConfig:
|
|
83
|
+
stages: dict[str, str] # id -> title, in order (kept as a dict for compatibility)
|
|
84
|
+
checks: list[CheckSpec]
|
|
85
|
+
profiles: dict[str, dict]
|
|
86
|
+
default_profile: str
|
|
87
|
+
questions: dict[str, str] = field(default_factory=dict)
|
|
88
|
+
error_blocks_dependents: bool = True
|
|
89
|
+
limits: dict[str, Any] = field(default_factory=dict)
|
|
90
|
+
reviewer: ReviewerSettings = field(default_factory=ReviewerSettings)
|
|
91
|
+
|
|
92
|
+
def stage_index(self) -> dict[str, int]:
|
|
93
|
+
return {stage: position for position, stage in enumerate(self.stages)}
|
|
94
|
+
|
|
95
|
+
def check(self, check_id: str) -> CheckSpec:
|
|
96
|
+
for check in self.checks:
|
|
97
|
+
if check.id == check_id:
|
|
98
|
+
return check
|
|
99
|
+
raise KeyError(check_id)
|
|
100
|
+
|
|
101
|
+
def enabled_checks(self) -> list[CheckSpec]:
|
|
102
|
+
return [check for check in self.checks if check.enabled]
|
|
103
|
+
|
|
104
|
+
def profile(self, name: str | None) -> tuple[str, Profile]:
|
|
105
|
+
chosen = name or self.default_profile
|
|
106
|
+
if chosen not in self.profiles:
|
|
107
|
+
raise ConfigError(f"unknown profile {chosen!r}; choose from {sorted(self.profiles)}")
|
|
108
|
+
raw = self.profiles[chosen]
|
|
109
|
+
return chosen, Profile(
|
|
110
|
+
[str(s).upper() for s in raw.get("fail_on", ["P0"])],
|
|
111
|
+
str(raw.get("min_confidence", raw.get("min_confidence_to_fail", "medium"))).lower(),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ----------------------------------------------------------------------- loading
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _read_yaml(path: Path) -> dict[str, Any]:
|
|
119
|
+
try:
|
|
120
|
+
loaded = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
121
|
+
except yaml.YAMLError as exc:
|
|
122
|
+
raise ConfigError(f"{path}: invalid YAML: {exc}") from exc
|
|
123
|
+
except OSError as exc:
|
|
124
|
+
raise ConfigError(f"{path}: {exc}") from exc
|
|
125
|
+
if loaded is None:
|
|
126
|
+
return {}
|
|
127
|
+
if not isinstance(loaded, dict):
|
|
128
|
+
raise ConfigError(f"{path}: the graph must be a mapping at the top level")
|
|
129
|
+
return loaded
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def default_raw() -> dict[str, Any]:
|
|
133
|
+
text = resources.files("traceforge.validate").joinpath("default.yaml").read_text("utf-8")
|
|
134
|
+
return yaml.safe_load(text)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _stage_list(raw: Any) -> list[dict[str, Any]]:
|
|
138
|
+
"""Stages may be a mapping ``{S0: Intake}`` or a list of ``{id, title, question}``."""
|
|
139
|
+
if raw is None:
|
|
140
|
+
return []
|
|
141
|
+
if isinstance(raw, dict):
|
|
142
|
+
return [
|
|
143
|
+
{"id": str(k), **(v if isinstance(v, dict) else {"title": str(v)})}
|
|
144
|
+
for k, v in raw.items()
|
|
145
|
+
]
|
|
146
|
+
if isinstance(raw, list):
|
|
147
|
+
out = []
|
|
148
|
+
for item in raw:
|
|
149
|
+
if not isinstance(item, dict) or not item.get("id"):
|
|
150
|
+
raise ConfigError("every stage entry needs an id")
|
|
151
|
+
out.append(dict(item))
|
|
152
|
+
return out
|
|
153
|
+
raise ConfigError("stages must be a mapping or a list")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _merge_by_id(base: list[dict], overrides: list[Any], label: str) -> list[dict]:
|
|
157
|
+
result = [copy.deepcopy(item) for item in base]
|
|
158
|
+
positions = {item["id"]: index for index, item in enumerate(result)}
|
|
159
|
+
for item in overrides:
|
|
160
|
+
if not isinstance(item, dict) or not item.get("id"):
|
|
161
|
+
raise ConfigError(f"every {label} entry needs an id")
|
|
162
|
+
if item["id"] in positions:
|
|
163
|
+
result[positions[item["id"]]].update(copy.deepcopy(item))
|
|
164
|
+
else:
|
|
165
|
+
positions[item["id"]] = len(result)
|
|
166
|
+
result.append(copy.deepcopy(item))
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _deep_merge(base: dict, overrides: dict) -> dict:
|
|
171
|
+
result = copy.deepcopy(base)
|
|
172
|
+
for key, value in overrides.items():
|
|
173
|
+
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
174
|
+
result[key] = _deep_merge(result[key], value)
|
|
175
|
+
else:
|
|
176
|
+
result[key] = copy.deepcopy(value)
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalise(raw: dict[str, Any]) -> dict[str, Any]:
|
|
181
|
+
raw = dict(raw)
|
|
182
|
+
raw["stages"] = _stage_list(raw.get("stages"))
|
|
183
|
+
raw["checks"] = list(raw.get("checks") or [])
|
|
184
|
+
return raw
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def apply_layer(base: dict[str, Any], layer: dict[str, Any], source: str) -> dict[str, Any]:
|
|
188
|
+
layer = dict(layer)
|
|
189
|
+
extends = layer.pop("extends", None)
|
|
190
|
+
disabled = set(layer.pop("disable", []) or [])
|
|
191
|
+
if extends is None and ("checks" in layer and "stages" in layer):
|
|
192
|
+
result = _normalise(layer) # a complete graph of its own
|
|
193
|
+
else:
|
|
194
|
+
if extends not in (None, "default", "none", False):
|
|
195
|
+
raise ConfigError(f"{source}: extends must be 'default' or 'none', got {extends!r}")
|
|
196
|
+
result = copy.deepcopy(base)
|
|
197
|
+
if extends in ("none", False): # keep stages, policy and limits; start with no checks
|
|
198
|
+
result["checks"] = []
|
|
199
|
+
for key, value in layer.items():
|
|
200
|
+
if key == "checks":
|
|
201
|
+
if not isinstance(value, list):
|
|
202
|
+
raise ConfigError(f"{source}: checks must be a list")
|
|
203
|
+
result["checks"] = _merge_by_id(result["checks"], value, "check")
|
|
204
|
+
elif key == "stages":
|
|
205
|
+
result["stages"] = _merge_by_id(result["stages"], _stage_list(value), "stage")
|
|
206
|
+
elif key in {"policy", "limits", "reviewer"} and isinstance(value, dict):
|
|
207
|
+
result[key] = _deep_merge(result.get(key) or {}, value)
|
|
208
|
+
else:
|
|
209
|
+
result[key] = copy.deepcopy(value)
|
|
210
|
+
if disabled:
|
|
211
|
+
unknown = disabled - {c.get("id") for c in result["checks"]}
|
|
212
|
+
if unknown:
|
|
213
|
+
raise ConfigError(f"{source}: disable names unknown checks {sorted(unknown)}")
|
|
214
|
+
result["checks"] = [c for c in result["checks"] if c.get("id") not in disabled]
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def load_raw(workspace: Path, config_path: Path | None = None) -> dict[str, Any]:
|
|
219
|
+
raw = _normalise(default_raw())
|
|
220
|
+
project = Path(workspace) / PROJECT_CONFIG
|
|
221
|
+
if project.is_file():
|
|
222
|
+
raw = apply_layer(raw, _read_yaml(project), str(project))
|
|
223
|
+
if config_path is not None:
|
|
224
|
+
if not Path(config_path).is_file():
|
|
225
|
+
raise ConfigError(f"config file not found: {config_path}")
|
|
226
|
+
raw = apply_layer(raw, _read_yaml(Path(config_path)), str(config_path))
|
|
227
|
+
return raw
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _as_list(value: Any) -> list[str]:
|
|
231
|
+
if value is None:
|
|
232
|
+
return []
|
|
233
|
+
if isinstance(value, str):
|
|
234
|
+
return [value]
|
|
235
|
+
return [str(v) for v in value]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_config(raw: dict[str, Any]) -> ValidationConfig:
|
|
239
|
+
stages = _stage_list(raw.get("stages"))
|
|
240
|
+
policy = raw.get("policy") or {}
|
|
241
|
+
profiles = policy.get("profiles") or {"dev": {"fail_on": ["P0"]}}
|
|
242
|
+
reviewer_raw = raw.get("reviewer") or {}
|
|
243
|
+
unknown_reviewer = set(reviewer_raw) - {"preamble", "timeout_s", "max_repairs"}
|
|
244
|
+
if unknown_reviewer:
|
|
245
|
+
raise ConfigError(f"reviewer: unknown keys {sorted(unknown_reviewer)}")
|
|
246
|
+
specs: list[CheckSpec] = []
|
|
247
|
+
known = {
|
|
248
|
+
"id",
|
|
249
|
+
"stage",
|
|
250
|
+
"kind",
|
|
251
|
+
"title",
|
|
252
|
+
"prompt",
|
|
253
|
+
"needs",
|
|
254
|
+
"requires",
|
|
255
|
+
"after",
|
|
256
|
+
"gate",
|
|
257
|
+
"builtin",
|
|
258
|
+
"scope",
|
|
259
|
+
"batch",
|
|
260
|
+
"repeat",
|
|
261
|
+
"inputs",
|
|
262
|
+
"effort",
|
|
263
|
+
"enabled",
|
|
264
|
+
}
|
|
265
|
+
for item in raw.get("checks") or []:
|
|
266
|
+
if not isinstance(item, dict) or not item.get("id"):
|
|
267
|
+
raise ConfigError("every check needs an id")
|
|
268
|
+
extra = set(item) - known
|
|
269
|
+
if extra:
|
|
270
|
+
raise ConfigError(f"check {item['id']!r}: unknown keys {sorted(extra)}")
|
|
271
|
+
if "stage" not in item:
|
|
272
|
+
raise ConfigError(f"check {item['id']!r}: needs a stage")
|
|
273
|
+
kind = str(item.get("kind", "review"))
|
|
274
|
+
if kind == "agent": # accept the other common spelling
|
|
275
|
+
kind = "review"
|
|
276
|
+
specs.append(
|
|
277
|
+
CheckSpec(
|
|
278
|
+
id=str(item["id"]),
|
|
279
|
+
stage=str(item["stage"]),
|
|
280
|
+
kind=kind,
|
|
281
|
+
title=str(item.get("title", item["id"])),
|
|
282
|
+
prompt=str(item.get("prompt") or ""),
|
|
283
|
+
needs=_as_list(item.get("needs")) + _as_list(item.get("requires")),
|
|
284
|
+
after=_as_list(item.get("after")),
|
|
285
|
+
gate=str(item.get("gate", "blocking")),
|
|
286
|
+
builtin=str(item.get("builtin") or (item["id"] if kind == "builtin" else "")),
|
|
287
|
+
scope=str(item.get("scope", "project")),
|
|
288
|
+
batch=int(item.get("batch", 4)),
|
|
289
|
+
repeat=int(item.get("repeat", 1)),
|
|
290
|
+
inputs=_as_list(item.get("inputs")) or ["all"],
|
|
291
|
+
effort=item.get("effort"),
|
|
292
|
+
enabled=bool(item.get("enabled", True)),
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
config = ValidationConfig(
|
|
296
|
+
stages={s["id"]: str(s.get("title", s["id"])) for s in stages},
|
|
297
|
+
checks=specs,
|
|
298
|
+
profiles=profiles,
|
|
299
|
+
default_profile=str(policy.get("default_profile", "dev")),
|
|
300
|
+
questions={s["id"]: str(s.get("question", "")) for s in stages},
|
|
301
|
+
error_blocks_dependents=bool(policy.get("error_blocks_dependents", True)),
|
|
302
|
+
limits=dict(raw.get("limits") or {}),
|
|
303
|
+
reviewer=ReviewerSettings(
|
|
304
|
+
preamble=str(reviewer_raw.get("preamble", "")),
|
|
305
|
+
timeout_s=int(reviewer_raw.get("timeout_s", 1800)),
|
|
306
|
+
max_repairs=int(reviewer_raw.get("max_repairs", 2)),
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
validate_graph(config)
|
|
310
|
+
return config
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def load_config(workspace: Path, config_path: Path | None = None) -> ValidationConfig:
|
|
314
|
+
return build_config(load_raw(Path(workspace), config_path))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# -------------------------------------------------------------------- validation
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def validate_graph(config: ValidationConfig) -> None:
|
|
321
|
+
"""Reject graphs that cannot run; every message names the offending check."""
|
|
322
|
+
from traceforge.validate.builtins import BUILTINS
|
|
323
|
+
|
|
324
|
+
order = config.stage_index()
|
|
325
|
+
if not order:
|
|
326
|
+
raise ConfigError("the graph defines no stages")
|
|
327
|
+
if config.default_profile not in config.profiles:
|
|
328
|
+
raise ConfigError(f"default profile {config.default_profile!r} is not defined")
|
|
329
|
+
for name, profile in config.profiles.items():
|
|
330
|
+
bad = [s for s in profile.get("fail_on", []) if str(s).upper() not in SEVERITIES]
|
|
331
|
+
if bad:
|
|
332
|
+
raise ConfigError(f"profile {name!r}: unknown severities {bad}")
|
|
333
|
+
conf = str(profile.get("min_confidence", "medium")).lower()
|
|
334
|
+
if conf not in CONFIDENCE:
|
|
335
|
+
raise ConfigError(f"profile {name!r}: min_confidence must be one of {list(CONFIDENCE)}")
|
|
336
|
+
seen: set[str] = set()
|
|
337
|
+
for check in config.checks:
|
|
338
|
+
if check.id in seen:
|
|
339
|
+
raise ConfigError(f"check {check.id!r}: duplicate id")
|
|
340
|
+
seen.add(check.id)
|
|
341
|
+
by_id = {check.id: check for check in config.checks}
|
|
342
|
+
for check in config.checks:
|
|
343
|
+
where = f"check {check.id!r}"
|
|
344
|
+
if check.stage not in order:
|
|
345
|
+
raise ConfigError(f"{where}: unknown stage {check.stage!r}")
|
|
346
|
+
if check.kind not in KINDS:
|
|
347
|
+
raise ConfigError(f"{where}: kind must be builtin or review")
|
|
348
|
+
if check.gate not in {"blocking", "advisory"}:
|
|
349
|
+
raise ConfigError(f"{where}: gate must be blocking or advisory")
|
|
350
|
+
if check.scope not in SCOPES:
|
|
351
|
+
raise ConfigError(f"{where}: scope must be one of {list(SCOPES)}")
|
|
352
|
+
bad_inputs = [i for i in check.inputs if i not in INPUT_KINDS]
|
|
353
|
+
if bad_inputs:
|
|
354
|
+
raise ConfigError(f"{where}: unknown inputs {bad_inputs}; use {list(INPUT_KINDS)}")
|
|
355
|
+
if check.batch < 1 or check.repeat < 1:
|
|
356
|
+
raise ConfigError(f"{where}: batch and repeat must be at least 1")
|
|
357
|
+
for label, refs in (("needs", check.needs), ("after", check.after)):
|
|
358
|
+
for other in refs:
|
|
359
|
+
if other == check.id:
|
|
360
|
+
raise ConfigError(f"{where}: depends on itself")
|
|
361
|
+
if other not in by_id:
|
|
362
|
+
raise ConfigError(f"{where}: {label} unknown check {other!r}")
|
|
363
|
+
if order[by_id[other].stage] > order[check.stage]:
|
|
364
|
+
raise ConfigError(
|
|
365
|
+
f"{where}: {label} {other!r} runs in the later stage {by_id[other].stage}"
|
|
366
|
+
)
|
|
367
|
+
if check.kind == "review":
|
|
368
|
+
if not check.prompt.strip():
|
|
369
|
+
raise ConfigError(f"{where}: review checks need a prompt")
|
|
370
|
+
if check.scope != "project" and "{items}" not in check.prompt:
|
|
371
|
+
raise ConfigError(f"{where}: scope {check.scope} needs an {{items}} placeholder")
|
|
372
|
+
elif check.builtin not in BUILTINS:
|
|
373
|
+
raise ConfigError(
|
|
374
|
+
f"{where}: unknown builtin {check.builtin!r}; known: {', '.join(sorted(BUILTINS))}"
|
|
375
|
+
)
|
|
376
|
+
_reject_cycles(config)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _reject_cycles(config: ValidationConfig) -> None:
|
|
380
|
+
edges = {check.id: [*check.needs, *check.after] for check in config.checks}
|
|
381
|
+
state: dict[str, int] = {}
|
|
382
|
+
stack: list[str] = []
|
|
383
|
+
|
|
384
|
+
def visit(node: str) -> None:
|
|
385
|
+
state[node] = 1
|
|
386
|
+
stack.append(node)
|
|
387
|
+
for other in edges.get(node, []):
|
|
388
|
+
if state.get(other) == 1:
|
|
389
|
+
cycle = [*stack[stack.index(other) :], other]
|
|
390
|
+
raise ConfigError(f"check {node!r}: dependency cycle {' -> '.join(cycle)}")
|
|
391
|
+
if other not in state:
|
|
392
|
+
visit(other)
|
|
393
|
+
stack.pop()
|
|
394
|
+
state[node] = 2
|
|
395
|
+
|
|
396
|
+
for check_id in edges:
|
|
397
|
+
if check_id not in state:
|
|
398
|
+
visit(check_id)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def prerequisites(config: ValidationConfig, check_ids: set[str]) -> set[str]:
|
|
402
|
+
"""Close ``check_ids`` over hard prerequisites (``needs``), transitively."""
|
|
403
|
+
result = set(check_ids)
|
|
404
|
+
queue = list(check_ids)
|
|
405
|
+
while queue:
|
|
406
|
+
for other in config.check(queue.pop()).needs:
|
|
407
|
+
if other not in result:
|
|
408
|
+
result.add(other)
|
|
409
|
+
queue.append(other)
|
|
410
|
+
return result
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Default traceforge validation graph. A project can extend or override it in
|
|
2
|
+
# .traceforge/validate.yaml:
|
|
3
|
+
# extends: default
|
|
4
|
+
# disable: [dfa.assembly]
|
|
5
|
+
# checks:
|
|
6
|
+
# - {id: custom.usb, stage: S6, kind: review, title: USB 2.0 routing, prompt: "..."}
|
|
7
|
+
version: 1
|
|
8
|
+
|
|
9
|
+
stages:
|
|
10
|
+
S0: Intake
|
|
11
|
+
S1: Architecture
|
|
12
|
+
S2: Parts
|
|
13
|
+
S3: Schematic
|
|
14
|
+
S4: Schematic to board
|
|
15
|
+
S5: Placement
|
|
16
|
+
S6: Routing, power and signal integrity
|
|
17
|
+
S7: Manufacturing and assembly
|
|
18
|
+
S8: Release
|
|
19
|
+
|
|
20
|
+
policy:
|
|
21
|
+
default_profile: dev
|
|
22
|
+
profiles:
|
|
23
|
+
dev: {fail_on: [P0], min_confidence: medium}
|
|
24
|
+
release: {fail_on: [P0, P1], min_confidence: low}
|
|
25
|
+
|
|
26
|
+
checks:
|
|
27
|
+
- {id: intake.tools, stage: S0, kind: builtin, gate: advisory, title: KiCad tooling is available}
|
|
28
|
+
- {id: intake.project, stage: S0, kind: builtin, title: Project files are present and readable, needs: [intake.tools]}
|
|
29
|
+
- {id: intake.intent, stage: S0, kind: builtin, gate: advisory, inputs: [intent, sch, pcb], title: Design intent is recorded and matches the design}
|
|
30
|
+
- id: arch.review
|
|
31
|
+
stage: S1
|
|
32
|
+
kind: review
|
|
33
|
+
title: System architecture
|
|
34
|
+
needs: [intake.project]
|
|
35
|
+
prompt: |
|
|
36
|
+
Review the system architecture. Reconstruct the power tree from the schematic: every input source,
|
|
37
|
+
regulator and rail, with each rail's consumers and an estimate of its current. Check that each
|
|
38
|
+
regulator can supply its load with margin and that its input range, dropout and thermal dissipation
|
|
39
|
+
work at the worst-case input. Check interfaces (USB, UART, I2C, SPI, CAN, debug) for completeness:
|
|
40
|
+
pull-ups, termination, level compatibility between connected parts, ESD protection at connectors.
|
|
41
|
+
Check reset, boot and programming paths of every processor.
|
|
42
|
+
- {id: parts.builtin, stage: S2, kind: builtin, title: "Footprints, values and part numbers", needs: [intake.project], inputs: [sch]}
|
|
43
|
+
- {id: parts.footprints, stage: S2, kind: builtin, title: "Footprint matches symbol, part number and package", needs: [intake.project], inputs: [sch, pcb]}
|
|
44
|
+
- {id: parts.verified, stage: S2, kind: builtin, title: "Every BOM part verified (catalogue, package, pinout)", needs: [intake.project], inputs: [sch, pcb]}
|
|
45
|
+
- {id: parts.lcsc, stage: S2, kind: builtin, gate: advisory, title: "LCSC parts exist, match the MPN and are stocked", needs: [intake.project], inputs: [sch]}
|
|
46
|
+
- id: parts.review
|
|
47
|
+
stage: S2
|
|
48
|
+
kind: review
|
|
49
|
+
title: Part selection and ratings
|
|
50
|
+
needs: [parts.builtin]
|
|
51
|
+
prompt: |
|
|
52
|
+
Review part selection. For each non-trivial part, check that the chosen symbol and footprint match the
|
|
53
|
+
intended package and pinout (compare symbol pin numbers to footprint pad numbers), that voltage,
|
|
54
|
+
current and power ratings have margin (capacitor voltage derating, resistor power, connector current),
|
|
55
|
+
and that the part numbers in LCSC/MPN fields match the values. Flag parts that are obsolete,
|
|
56
|
+
unusual or hard to source when a common equivalent exists. Use part_lookup and datasheet_fetch
|
|
57
|
+
for evidence (package drawing, absolute maximum ratings, pinout) rather than recalling values.
|
|
58
|
+
- {id: sch.erc, stage: S3, kind: builtin, title: KiCad electrical rules check, needs: [intake.project], inputs: [sch, libs]}
|
|
59
|
+
- {id: sch.circuit_rules, stage: S3, kind: builtin, title: "LED, USB-C CC, button, decoupling and regulator rules", needs: [intake.project], inputs: [sch, pcb]}
|
|
60
|
+
- id: sch.circuits
|
|
61
|
+
stage: S3
|
|
62
|
+
kind: review
|
|
63
|
+
title: Circuit correctness
|
|
64
|
+
needs: [intake.project]
|
|
65
|
+
prompt: |
|
|
66
|
+
Check every circuit block against its datasheet's reference design and against first principles:
|
|
67
|
+
decoupling at each IC supply pin, bulk capacitance per rail, feedback dividers and their computed
|
|
68
|
+
output, timing components and their computed frequency, LED and transistor bias currents, pull-ups and
|
|
69
|
+
pull-downs on inputs that could float, polarity of polarized parts and diodes, and enable/reset pins.
|
|
70
|
+
Show the arithmetic for every computed value you rely on.
|
|
71
|
+
- {id: pcb.parity, stage: S4, kind: builtin, title: Schematic and board agree, needs: [intake.project]}
|
|
72
|
+
- {id: pcb.rules, stage: S4, kind: builtin, title: Board rules and net classes meet fab limits, needs: [intake.project], inputs: [pro]}
|
|
73
|
+
- {id: pcb.dfm, stage: S4, kind: builtin, title: "Fab limits, outline, courtyards and routing completeness", needs: [intake.project]}
|
|
74
|
+
- {id: pcb.ipc2221, stage: S4, kind: builtin, gate: advisory, title: "IPC-2221 conductor width and spacing for declared currents and voltages", needs: [intake.project]}
|
|
75
|
+
- {id: pcb.multilayer, stage: S6, kind: builtin, gate: advisory, title: "Layer usage, reference planes, return paths, via stubs, copper balance and fab stackup", needs: [intake.project], inputs: [pcb, pro]}
|
|
76
|
+
- {id: pcb.models3d, stage: S4, kind: builtin, gate: advisory, title: "Every footprint has a resolved 3D model", needs: [intake.project], inputs: [pcb]}
|
|
77
|
+
- {id: pcb.drc, stage: S4, kind: builtin, title: KiCad design rules check, needs: [intake.project]}
|
|
78
|
+
- id: pcb.placement
|
|
79
|
+
stage: S5
|
|
80
|
+
kind: review
|
|
81
|
+
title: Placement
|
|
82
|
+
needs: [pcb.parity]
|
|
83
|
+
prompt: |
|
|
84
|
+
Review component placement using the board coordinates. Decoupling capacitors should sit next to the
|
|
85
|
+
supply pin they serve; crystals next to their oscillator pins; regulators and their capacitors in a
|
|
86
|
+
compact loop; connectors at board edges with sensible orientation; switches, LEDs and test points where
|
|
87
|
+
a user can reach or see them; heat sources away from temperature-sensitive parts. Quote coordinates or
|
|
88
|
+
distances for each finding.
|
|
89
|
+
- id: pcb.routing
|
|
90
|
+
stage: S6
|
|
91
|
+
kind: review
|
|
92
|
+
title: Power and signal routing
|
|
93
|
+
needs: [pcb.dfm]
|
|
94
|
+
prompt: |
|
|
95
|
+
Review routing: track widths on power nets against their expected current (state the current and the
|
|
96
|
+
width you would require), via count on high-current paths, ground return paths (pours, stitching vias,
|
|
97
|
+
splits under signals), loop area of switching regulators, and routing of clocks, USB and other fast or
|
|
98
|
+
sensitive signals (length, layer changes, reference plane). Use kicad_board with pads=true to read
|
|
99
|
+
geometry when needed.
|
|
100
|
+
- id: dfa.assembly
|
|
101
|
+
stage: S7
|
|
102
|
+
kind: review
|
|
103
|
+
title: Assembly readiness
|
|
104
|
+
needs: [parts.builtin, pcb.dfm]
|
|
105
|
+
prompt: |
|
|
106
|
+
Review assembly readiness for the selected fab: every BOM line has an orderable part number, footprint
|
|
107
|
+
rotations and polarity marks are unambiguous on silkscreen, fine-pitch parts have enough clearance for
|
|
108
|
+
rework, fiducials exist when the board will be machine assembled, and through-hole parts are limited to
|
|
109
|
+
what the assembly service supports.
|
|
110
|
+
- {id: pcb.refdes, stage: S7, kind: builtin, title: Reference designators visible on silkscreen, needs: [intake.project], inputs: [pcb]}
|
|
111
|
+
- {id: release.outputs, stage: S8, kind: builtin, title: Fabrication outputs can be generated, needs: [intake.project], after: [pcb.dfm], inputs: [pcb, pro]}
|
|
112
|
+
- {id: release.repo, stage: S8, kind: builtin, title: Repository is portable, needs: [intake.project]}
|
|
113
|
+
- {id: release.cpl, stage: S8, kind: builtin, title: BOM and placement are assembler-ready, needs: [intake.project]}
|