chipgraph 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 (252) hide show
  1. chipgraph/__init__.py +7 -0
  2. chipgraph/adapters/__init__.py +1 -0
  3. chipgraph/adapters/format/__init__.py +6 -0
  4. chipgraph/adapters/format/__main__.py +16 -0
  5. chipgraph/adapters/format/_yaml_lines.py +54 -0
  6. chipgraph/adapters/format/chip_yaml.py +323 -0
  7. chipgraph/adapters/format/qsoc_contract.py +246 -0
  8. chipgraph/adapters/llm/__init__.py +58 -0
  9. chipgraph/adapters/llm/_budget.py +173 -0
  10. chipgraph/adapters/llm/_errors.py +50 -0
  11. chipgraph/adapters/llm/_nda.py +30 -0
  12. chipgraph/adapters/llm/_retry.py +85 -0
  13. chipgraph/adapters/llm/anthropic.py +247 -0
  14. chipgraph/adapters/llm/decide_backend.py +179 -0
  15. chipgraph/adapters/llm/fake.py +87 -0
  16. chipgraph/adapters/llm/guard.py +68 -0
  17. chipgraph/adapters/parser/__init__.py +22 -0
  18. chipgraph/adapters/parser/cocotb.py +304 -0
  19. chipgraph/adapters/parser/generic_regex.py +75 -0
  20. chipgraph/adapters/parser/verible.py +55 -0
  21. chipgraph/adapters/parser/verilator.py +46 -0
  22. chipgraph/adapters/review/__init__.py +7 -0
  23. chipgraph/adapters/review/file.py +123 -0
  24. chipgraph/adapters/review/github.py +886 -0
  25. chipgraph/adapters/runner/__init__.py +7 -0
  26. chipgraph/adapters/runner/local.py +122 -0
  27. chipgraph/adapters/runtime/__init__.py +5 -0
  28. chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
  29. chipgraph/adapters/runtime/claude_code/agents.py +158 -0
  30. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  31. chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
  32. chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
  33. chipgraph/adapters/runtime/claude_code/loop.py +342 -0
  34. chipgraph/adapters/runtime/claude_code/previous.py +419 -0
  35. chipgraph/adapters/runtime/claude_code/review.py +492 -0
  36. chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
  37. chipgraph/adapters/runtime/claude_code/service.py +2285 -0
  38. chipgraph/adapters/tool/__init__.py +7 -0
  39. chipgraph/adapters/tool/cmd.py +229 -0
  40. chipgraph/adapters/tool/edalize.py +773 -0
  41. chipgraph/adapters/tool/filelist.py +259 -0
  42. chipgraph/adapters/tool/pyslang.py +485 -0
  43. chipgraph/adapters/vcs/__init__.py +1 -0
  44. chipgraph/adapters/vcs/git.py +521 -0
  45. chipgraph/app/__init__.py +10 -0
  46. chipgraph/app/baseline.py +339 -0
  47. chipgraph/app/build.py +332 -0
  48. chipgraph/app/checks.py +202 -0
  49. chipgraph/app/context.py +191 -0
  50. chipgraph/app/errors.py +11 -0
  51. chipgraph/app/executors.py +79 -0
  52. chipgraph/app/findings.py +203 -0
  53. chipgraph/app/ingest.py +523 -0
  54. chipgraph/checks/__init__.py +52 -0
  55. chipgraph/checks/_cdc_analyze.py +272 -0
  56. chipgraph/checks/_cdc_elaborate.py +455 -0
  57. chipgraph/checks/_common.py +268 -0
  58. chipgraph/checks/_cross.py +96 -0
  59. chipgraph/checks/_model.py +64 -0
  60. chipgraph/checks/_naming_pyslang.py +235 -0
  61. chipgraph/checks/_naming_rules.py +127 -0
  62. chipgraph/checks/_xref_syntax.py +260 -0
  63. chipgraph/checks/cdc_struct.py +235 -0
  64. chipgraph/checks/connect.py +353 -0
  65. chipgraph/checks/cross_chip.py +454 -0
  66. chipgraph/checks/duplicate.py +153 -0
  67. chipgraph/checks/filelist.py +323 -0
  68. chipgraph/checks/format.py +268 -0
  69. chipgraph/checks/generated.py +151 -0
  70. chipgraph/checks/hardcode.py +233 -0
  71. chipgraph/checks/layout.py +155 -0
  72. chipgraph/checks/naming.py +311 -0
  73. chipgraph/checks/plan_check.py +901 -0
  74. chipgraph/checks/ports_diff.py +203 -0
  75. chipgraph/checks/spec_schema.py +277 -0
  76. chipgraph/checks/style.py +910 -0
  77. chipgraph/checks/tb_static.py +599 -0
  78. chipgraph/checks/trace.py +224 -0
  79. chipgraph/checks/vendor_guard.py +369 -0
  80. chipgraph/checks/wrap_check.py +486 -0
  81. chipgraph/cli/__init__.py +1688 -0
  82. chipgraph/cli/next_steps.py +137 -0
  83. chipgraph/core/__init__.py +1 -0
  84. chipgraph/core/config/__init__.py +93 -0
  85. chipgraph/core/config/defaults.py +14 -0
  86. chipgraph/core/config/errors.py +32 -0
  87. chipgraph/core/config/loader.py +766 -0
  88. chipgraph/core/config/models.py +845 -0
  89. chipgraph/core/config/templates.py +340 -0
  90. chipgraph/core/contracts/__init__.py +75 -0
  91. chipgraph/core/contracts/_paths.py +26 -0
  92. chipgraph/core/contracts/agent.py +49 -0
  93. chipgraph/core/contracts/artifact.py +68 -0
  94. chipgraph/core/contracts/check.py +76 -0
  95. chipgraph/core/contracts/event.py +70 -0
  96. chipgraph/core/contracts/export.py +128 -0
  97. chipgraph/core/contracts/finding.py +171 -0
  98. chipgraph/core/contracts/gate.py +50 -0
  99. chipgraph/core/contracts/rule.py +177 -0
  100. chipgraph/core/contracts/types.py +45 -0
  101. chipgraph/core/engine/__init__.py +70 -0
  102. chipgraph/core/engine/agent_rule.py +1362 -0
  103. chipgraph/core/engine/baseline.py +283 -0
  104. chipgraph/core/engine/decide.py +462 -0
  105. chipgraph/core/engine/fanout.py +1358 -0
  106. chipgraph/core/engine/gate.py +170 -0
  107. chipgraph/core/engine/graph.py +647 -0
  108. chipgraph/core/engine/records.py +60 -0
  109. chipgraph/core/engine/rules.py +92 -0
  110. chipgraph/core/engine/scheduler.py +637 -0
  111. chipgraph/core/engine/select.py +1239 -0
  112. chipgraph/core/engine/targets.py +342 -0
  113. chipgraph/core/model/__init__.py +98 -0
  114. chipgraph/core/model/entities.py +244 -0
  115. chipgraph/core/model/export.py +16 -0
  116. chipgraph/core/model/ingest.py +693 -0
  117. chipgraph/core/model/json_value.py +4 -0
  118. chipgraph/core/model/keys.py +95 -0
  119. chipgraph/core/model/model.py +153 -0
  120. chipgraph/core/model/provenance.py +27 -0
  121. chipgraph/core/model/query.py +583 -0
  122. chipgraph/core/model/registry.py +80 -0
  123. chipgraph/core/model/relations.py +61 -0
  124. chipgraph/core/model/store.py +279 -0
  125. chipgraph/core/plugin_api/__init__.py +81 -0
  126. chipgraph/core/plugin_api/local.py +167 -0
  127. chipgraph/core/plugin_api/pack.py +159 -0
  128. chipgraph/core/plugin_api/protocols.py +177 -0
  129. chipgraph/core/plugin_api/registry.py +111 -0
  130. chipgraph/core/plugin_api/types.py +94 -0
  131. chipgraph/core/runtime/__init__.py +41 -0
  132. chipgraph/core/runtime/executor.py +79 -0
  133. chipgraph/core/runtime/queue.py +565 -0
  134. chipgraph/core/runtime/roles/__init__.py +91 -0
  135. chipgraph/core/runtime/roles/data/author.md +41 -0
  136. chipgraph/core/runtime/roles/data/critic.md +56 -0
  137. chipgraph/core/runtime/roles/data/planner.md +38 -0
  138. chipgraph/core/runtime/roles/data/researcher.md +37 -0
  139. chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
  140. chipgraph/core/runtime/roles/data/tb-author.md +48 -0
  141. chipgraph/core/runtime/roles/data/triage.md +34 -0
  142. chipgraph/core/runtime/roles/policy.py +299 -0
  143. chipgraph/core/runtime/roles/registry.py +52 -0
  144. chipgraph/core/runtime/roles/review.py +275 -0
  145. chipgraph/core/runtime/roles/schema.py +82 -0
  146. chipgraph/core/runtime/roles/skills.py +134 -0
  147. chipgraph/core/runtime/roles/spec.py +232 -0
  148. chipgraph/core/state/__init__.py +1 -0
  149. chipgraph/core/state/artifacts.py +156 -0
  150. chipgraph/core/state/backend.py +117 -0
  151. chipgraph/core/state/findings.py +151 -0
  152. chipgraph/core/state/handoff.py +411 -0
  153. chipgraph/core/state/idempotency.py +59 -0
  154. chipgraph/core/state/journal.py +238 -0
  155. chipgraph/core/state/layout.py +82 -0
  156. chipgraph/core/state/lock.py +195 -0
  157. chipgraph/core/state/trace.py +225 -0
  158. chipgraph/learn/__init__.py +96 -0
  159. chipgraph/learn/draft.py +187 -0
  160. chipgraph/learn/infer.py +580 -0
  161. chipgraph/learn/models.py +145 -0
  162. chipgraph/learn/try_run.py +228 -0
  163. chipgraph/mcp/__init__.py +12 -0
  164. chipgraph/mcp/model_tools.py +167 -0
  165. chipgraph/mcp/server.py +787 -0
  166. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  167. chipgraph/orgs/qnsc/profile.yml +5 -0
  168. chipgraph/packs/__init__.py +8 -0
  169. chipgraph/packs/assist/__init__.py +16 -0
  170. chipgraph/packs/assist/ask/__init__.py +58 -0
  171. chipgraph/packs/assist/ask/_db.py +57 -0
  172. chipgraph/packs/assist/ask/_project.py +230 -0
  173. chipgraph/packs/assist/ask/answer.py +266 -0
  174. chipgraph/packs/assist/ask/check.py +182 -0
  175. chipgraph/packs/assist/ask/contract.py +115 -0
  176. chipgraph/packs/assist/ask/documents.py +157 -0
  177. chipgraph/packs/assist/ask/fts.py +80 -0
  178. chipgraph/packs/assist/ask/retrieve.py +331 -0
  179. chipgraph/packs/assist/audit/__init__.py +383 -0
  180. chipgraph/packs/assist/pack.yml +13 -0
  181. chipgraph/packs/assist/triage/__init__.py +64 -0
  182. chipgraph/packs/assist/triage/contract.py +94 -0
  183. chipgraph/packs/assist/triage/parse.py +371 -0
  184. chipgraph/packs/assist/triage/question.py +384 -0
  185. chipgraph/packs/assist/triage/report.py +160 -0
  186. chipgraph/packs/assist/triage/rules.py +447 -0
  187. chipgraph/packs/assist/triage/run.py +224 -0
  188. chipgraph/packs/digital_rtl/__init__.py +19 -0
  189. chipgraph/packs/digital_rtl/layout.py +144 -0
  190. chipgraph/packs/digital_rtl/pack.yml +25 -0
  191. chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
  192. chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
  193. chipgraph/packs/digital_rtl/plan/context.py +82 -0
  194. chipgraph/packs/digital_rtl/plan/expand.py +94 -0
  195. chipgraph/packs/digital_rtl/plan/model.py +150 -0
  196. chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
  197. chipgraph/packs/digital_rtl/plan/show.py +176 -0
  198. chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
  199. chipgraph/packs/digital_rtl/pr/body.py +165 -0
  200. chipgraph/packs/digital_rtl/pr/gate.py +110 -0
  201. chipgraph/packs/digital_rtl/pr/open.py +397 -0
  202. chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
  203. chipgraph/packs/digital_rtl/rtl/context.py +272 -0
  204. chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
  205. chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
  206. chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
  207. chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
  208. chipgraph/packs/digital_rtl/rules/review.yml +31 -0
  209. chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
  210. chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
  211. chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
  212. chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
  213. chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
  214. chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
  215. chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
  216. chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
  217. chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
  218. chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
  219. chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
  220. chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
  221. chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
  222. chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
  223. chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
  224. chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
  225. chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
  226. chipgraph/packs/dv/__init__.py +15 -0
  227. chipgraph/packs/dv/declaration.py +392 -0
  228. chipgraph/packs/dv/interface.py +596 -0
  229. chipgraph/packs/dv/pack.yml +14 -0
  230. chipgraph/packs/dv/rules/tb_module.yml +42 -0
  231. chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
  232. chipgraph/packs/spec_core/__init__.py +9 -0
  233. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  234. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  235. chipgraph/packs/spec_core/extract/_text.py +116 -0
  236. chipgraph/packs/spec_core/extract/mas.py +975 -0
  237. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  238. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  239. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  240. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  241. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  242. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  243. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  244. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  245. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  246. chipgraph/packs/spec_core/pack.yml +6 -0
  247. chipgraph/py.typed +0 -0
  248. chipgraph-0.1.0.dist-info/METADATA +53 -0
  249. chipgraph-0.1.0.dist-info/RECORD +252 -0
  250. chipgraph-0.1.0.dist-info/WHEEL +4 -0
  251. chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
  252. chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,323 @@
1
+ """`FilelistCheck`: every RTL file is listed, and every listed file exists.
2
+
3
+ See DESIGN.md 7.3 for the `filelist` check.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import re
10
+ import time
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ from chipgraph.checks._common import (
16
+ ArgError,
17
+ compile_template,
18
+ compute_idempotency_key,
19
+ error_result,
20
+ glob_to_regex,
21
+ iter_repo_files,
22
+ optional_list_of_str,
23
+ optional_str,
24
+ require_str,
25
+ sha256_file,
26
+ to_repo_rel,
27
+ )
28
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
29
+ from chipgraph.core.contracts.types import CheckStatus
30
+ from chipgraph.core.plugin_api.types import ToolContext
31
+
32
+ _ENV_VAR = re.compile(r"\$\{(\w+)\}|\$(\w+)")
33
+
34
+
35
+ class FilelistCheck:
36
+ """Checks a project's per-block RTL filelists against the RTL files actually on disk."""
37
+
38
+ id = "filelist"
39
+ name = "Filelist"
40
+
41
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
42
+ start = time.monotonic()
43
+ args = spec.args
44
+
45
+ try:
46
+ filelist_tmpl_str = require_str(args, "filelist")
47
+ sources_tmpl_str = require_str(args, "sources")
48
+ package_suffix = optional_str(args, "package_suffix", "_pkg.sv")
49
+ ignore = optional_list_of_str(args, "ignore")
50
+ filelist_tmpl = compile_template(filelist_tmpl_str)
51
+ sources_tmpl = compile_template(sources_tmpl_str)
52
+ if set(filelist_tmpl.names) != {"block"}:
53
+ raise ArgError("args['filelist'] must use exactly the `{block}` placeholder")
54
+ if set(sources_tmpl.names) != {"block"}:
55
+ raise ArgError("args['sources'] must use exactly the `{block}` placeholder")
56
+ except ArgError as exc:
57
+ return error_result(spec, str(exc), start)
58
+
59
+ ignore_regexes = [glob_to_regex(p) for p in ignore]
60
+ all_files = [
61
+ f
62
+ for f in iter_repo_files(ctx.repo_root)
63
+ if not any(r.fullmatch(f) for r in ignore_regexes)
64
+ ]
65
+
66
+ blocks: set[str] = set()
67
+ sources_by_block: dict[str, list[str]] = {}
68
+ filelist_path_by_block: dict[str, str] = {}
69
+ for f in all_files:
70
+ src_match = sources_tmpl.match(f)
71
+ if src_match is not None:
72
+ block = src_match["block"]
73
+ blocks.add(block)
74
+ sources_by_block.setdefault(block, []).append(f)
75
+ fl_match = filelist_tmpl.match(f)
76
+ if fl_match is not None:
77
+ block = fl_match["block"]
78
+ blocks.add(block)
79
+ filelist_path_by_block[block] = f
80
+
81
+ issues: list[Issue] = []
82
+ read_files: dict[str, str] = {}
83
+
84
+ for block in sorted(blocks):
85
+ expected_filelist = filelist_tmpl_str.replace("{block}", block)
86
+ block_sources = sorted(sources_by_block.get(block, []))
87
+ for f in block_sources:
88
+ read_files[f] = sha256_file(ctx.repo_root / f)
89
+
90
+ if block not in filelist_path_by_block:
91
+ if block_sources:
92
+ issues.append(
93
+ Issue(
94
+ file=expected_filelist,
95
+ rule="filelist",
96
+ severity="error",
97
+ msg=f"block `{block}` has RTL sources but no filelist",
98
+ )
99
+ )
100
+ continue
101
+
102
+ visited: set[Path] = set()
103
+ entries, parse_issues = _parse_filelist(
104
+ ctx.repo_root / expected_filelist, ctx.repo_root, ctx.env, visited
105
+ )
106
+ issues.extend(parse_issues)
107
+ for visited_path in visited:
108
+ rel = to_repo_rel(visited_path, ctx.repo_root)
109
+ if rel is not None:
110
+ read_files[rel] = sha256_file(visited_path)
111
+
112
+ listed_issues, listed_repo_rel = _check_entries(entries, package_suffix, ctx.repo_root)
113
+ issues.extend(listed_issues)
114
+
115
+ for f in block_sources:
116
+ if f not in listed_repo_rel:
117
+ issues.append(
118
+ Issue(
119
+ file=f,
120
+ rule="filelist",
121
+ severity="error",
122
+ msg=f"`{f}` is not in `{expected_filelist}`",
123
+ )
124
+ )
125
+
126
+ key = compute_idempotency_key(spec.id, args, tuple(read_files.items()))
127
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
128
+ return CheckResult(
129
+ check_id=spec.id,
130
+ status=status,
131
+ issues=tuple(issues),
132
+ duration_s=time.monotonic() - start,
133
+ idempotency_key=key,
134
+ )
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class _Entry:
139
+ raw: str
140
+ line: int
141
+ filelist_rel: str
142
+ resolved: Path
143
+ is_absolute: bool
144
+
145
+
146
+ def _expand_env(text: str, env: Mapping[str, str]) -> str | None:
147
+ """Expand `$VAR`/`${VAR}` in `text` from `env`; `None` if any variable is unknown."""
148
+ ok = True
149
+
150
+ def _sub(m: re.Match[str]) -> str:
151
+ nonlocal ok
152
+ name = m.group(1) or m.group(2)
153
+ if name in env:
154
+ return env[name]
155
+ ok = False
156
+ return m.group(0)
157
+
158
+ result = _ENV_VAR.sub(_sub, text)
159
+ return result if ok else None
160
+
161
+
162
+ def _strip_comment(line: str) -> str:
163
+ idx = len(line)
164
+ for token in ("//", "#"):
165
+ pos = line.find(token)
166
+ if pos != -1:
167
+ idx = min(idx, pos)
168
+ return line[:idx]
169
+
170
+
171
+ def _parse_filelist(
172
+ path: Path, root: Path, env: Mapping[str, str], visited: set[Path]
173
+ ) -> tuple[list[_Entry], list[Issue]]:
174
+ entries: list[_Entry] = []
175
+ issues: list[Issue] = []
176
+ _parse_filelist_into(path, root, env, visited, entries, issues)
177
+ return entries, issues
178
+
179
+
180
+ def _parse_filelist_into(
181
+ path: Path,
182
+ root: Path,
183
+ env: Mapping[str, str],
184
+ visited: set[Path],
185
+ entries: list[_Entry],
186
+ issues: list[Issue],
187
+ ) -> None:
188
+ key = Path(os.path.normpath(str(path)))
189
+ if key in visited:
190
+ return
191
+ visited.add(key)
192
+
193
+ filelist_rel = to_repo_rel(path, root) or str(path)
194
+ try:
195
+ text = path.read_text()
196
+ except OSError:
197
+ return
198
+
199
+ for lineno, raw_line in enumerate(text.splitlines(), start=1):
200
+ stripped = _strip_comment(raw_line).strip()
201
+ if not stripped:
202
+ continue
203
+ if stripped.startswith("+"):
204
+ continue
205
+
206
+ tokens = stripped.split(None, 1)
207
+ head = tokens[0]
208
+
209
+ if head in ("-f", "-F") and len(tokens) > 1:
210
+ target_raw = tokens[1].strip()
211
+ expanded = _expand_env(target_raw, env)
212
+ if expanded is None:
213
+ issues.append(
214
+ Issue(
215
+ file=filelist_rel,
216
+ line=lineno,
217
+ severity="warning",
218
+ msg=f"unknown environment variable in `{target_raw}`",
219
+ )
220
+ )
221
+ continue
222
+ target_path = Path(expanded)
223
+ if not target_path.is_absolute():
224
+ target_path = path.parent / target_path
225
+ _parse_filelist_into(target_path, root, env, visited, entries, issues)
226
+ continue
227
+
228
+ if head.startswith("-"):
229
+ continue
230
+
231
+ raw_entry = stripped
232
+ expanded = _expand_env(raw_entry, env)
233
+ if expanded is None:
234
+ issues.append(
235
+ Issue(
236
+ file=filelist_rel,
237
+ line=lineno,
238
+ severity="warning",
239
+ msg=f"unknown environment variable in `{raw_entry}`",
240
+ )
241
+ )
242
+ continue
243
+
244
+ entry_path = Path(expanded)
245
+ is_absolute = entry_path.is_absolute()
246
+ if is_absolute:
247
+ issues.append(
248
+ Issue(
249
+ file=filelist_rel,
250
+ line=lineno,
251
+ severity="warning",
252
+ msg=f"`{raw_entry}` is an absolute path",
253
+ )
254
+ )
255
+ resolved = entry_path
256
+ else:
257
+ resolved = path.parent / entry_path
258
+
259
+ entries.append(
260
+ _Entry(
261
+ raw=raw_entry,
262
+ line=lineno,
263
+ filelist_rel=filelist_rel,
264
+ resolved=resolved,
265
+ is_absolute=is_absolute,
266
+ )
267
+ )
268
+
269
+
270
+ def _check_entries(
271
+ entries: list[_Entry], package_suffix: str, root: Path
272
+ ) -> tuple[list[Issue], set[str]]:
273
+ issues: list[Issue] = []
274
+ seen: set[str] = set()
275
+ listed_repo_rel: set[str] = set()
276
+ seen_non_pkg_name: str | None = None
277
+
278
+ for entry in entries:
279
+ if not entry.resolved.is_file():
280
+ issues.append(
281
+ Issue(
282
+ file=entry.filelist_rel,
283
+ line=entry.line,
284
+ severity="error",
285
+ msg=f"`{entry.raw}` is listed but missing",
286
+ )
287
+ )
288
+ continue
289
+
290
+ key = os.path.normpath(str(entry.resolved))
291
+ name = Path(key).name
292
+
293
+ if key in seen:
294
+ issues.append(
295
+ Issue(
296
+ file=entry.filelist_rel,
297
+ line=entry.line,
298
+ severity="warning",
299
+ msg=f"`{entry.raw}` is listed twice",
300
+ )
301
+ )
302
+ else:
303
+ seen.add(key)
304
+ rel = to_repo_rel(entry.resolved, root)
305
+ if rel is not None:
306
+ listed_repo_rel.add(rel)
307
+
308
+ is_pkg = name.endswith(package_suffix)
309
+ if is_pkg:
310
+ if seen_non_pkg_name is not None:
311
+ issues.append(
312
+ Issue(
313
+ file=entry.filelist_rel,
314
+ line=entry.line,
315
+ rule="order",
316
+ severity="error",
317
+ msg=f"package `{name}` must come before `{seen_non_pkg_name}`",
318
+ )
319
+ )
320
+ elif seen_non_pkg_name is None:
321
+ seen_non_pkg_name = name
322
+
323
+ return issues, listed_repo_rel
@@ -0,0 +1,268 @@
1
+ """`FormatCheck`: the project's formatter, run by the engine on an agent task's outputs.
2
+
3
+ The first check of `digital-rtl/rtl_module` (DESIGN.md 8.6 V3, task M2-04). The engine,
4
+ never the agent, runs it once the agent has finished and before the other checks, so
5
+ lint, naming and style see the formatted file, and the output hash the engine records
6
+ (the task's accepted outputs, the build record) is the formatted file's. It works the
7
+ same in both runtimes: `submit` (claude-code) and `AgentRuleExecutor` (in process) both
8
+ run a rule's checks after the attempt and hash the outputs after the checks.
9
+
10
+ Which files: `target_files` (shared with the `style` check):
11
+
12
+ - arg `files`: path templates (`{block}`, `{BLOCK}`, `{module}` filled from the task's
13
+ params);
14
+ - else, for a task with params `block` and `module` (a `foreach: plan.modules` rule), the
15
+ module's write set in the block's approved plan (arg `plan`, default
16
+ `plan/{block}.approved.json`), its HDL files only (`.sv`, `.svh`, `.v`, `.vh`). The
17
+ write set bounds the task's outputs, so the check writes only what the task may write.
18
+
19
+ Without either (e.g. `chipgraph check` over a block) it is `skipped`: it never formats a
20
+ person's files outside a task.
21
+
22
+ Which formatter: arg `formatter` (an inline adapter config), else the profile's
23
+ `style.formatter`, e.g.::
24
+
25
+ style:
26
+ formatter: { use: cmd, cmd: ["verible-verilog-format", "--inplace", "{file}"] }
27
+
28
+ It runs once per file, with the task's params plus `file` (the repo-relative path) and
29
+ `file_abs`; it must rewrite the file in place. With no formatter configured,
30
+ `verible-verilog-format --inplace` is used when it is on PATH; otherwise the check is
31
+ `skipped` with a note. A configured formatter that fails, or cannot be started, makes
32
+ the check `error` with the formatter's message (an infra failure: no try is used); the
33
+ file is then put back as it was.
34
+
35
+ Profile example (`.chipgraph.yml`)::
36
+
37
+ adapters:
38
+ format: { use: format }
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import shutil
45
+ import time
46
+ from collections.abc import Mapping
47
+ from pathlib import Path
48
+ from typing import Any
49
+
50
+ from chipgraph.checks._common import compute_idempotency_key, sha256_file
51
+ from chipgraph.core.config.errors import ConfigError
52
+ from chipgraph.core.config.loader import load as load_profile
53
+ from chipgraph.core.config.models import AdapterCfg
54
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
55
+ from chipgraph.core.contracts.types import CheckStatus
56
+ from chipgraph.core.plugin_api.registry import PluginError, Registry
57
+ from chipgraph.core.plugin_api.types import ToolContext
58
+
59
+ DEFAULT_FORMATTER = "verible-verilog-format"
60
+ """Used, with `--inplace`, when the profile names no formatter and it is on PATH."""
61
+ DEFAULT_APPROVED_PLAN = "plan/{block}.approved.json"
62
+ """Where a block's approved plan is (the `plan_expand` tool's default output)."""
63
+ HDL_SUFFIXES = (".sv", ".svh", ".v", ".vh")
64
+ """The files of a module's write set the per-task checks format and measure."""
65
+
66
+
67
+ class TargetError(Exception):
68
+ """The files of a per-task check cannot be named (bad args, an unreadable plan)."""
69
+
70
+
71
+ def _fill(template: str, params: Mapping[str, str]) -> str | None:
72
+ """`template` with `{key}`/`{KEY}` from `params`; `None` if a key is not a param."""
73
+ out = template
74
+ for key, value in params.items():
75
+ out = out.replace("{" + key + "}", value).replace("{" + key.upper() + "}", value.upper())
76
+ return None if "{" in out else out
77
+
78
+
79
+ def _relative(path: str) -> str:
80
+ text = path.replace("\\", "/").strip()
81
+ parts = [p for p in text.split("/") if p not in ("", ".")]
82
+ if text.startswith("/") or ".." in parts or not parts:
83
+ raise TargetError(f"{path!r} is not a repo-relative path")
84
+ return "/".join(parts)
85
+
86
+
87
+ def target_files(
88
+ args: Mapping[str, Any], params: Mapping[str, str], root: Path
89
+ ) -> list[str] | None:
90
+ """The repo-relative files a per-task check works on, or `None` for none (see above).
91
+
92
+ Raises `TargetError` for a malformed `files`/`plan` arg or an approved plan that
93
+ cannot be read.
94
+ """
95
+ files = args.get("files")
96
+ if files is not None:
97
+ templates = [files] if isinstance(files, str) else files
98
+ if not isinstance(templates, list) or not all(isinstance(t, str) for t in templates):
99
+ raise TargetError("args['files'] must be a path template or a list of them")
100
+ filled = [_fill(t, params) for t in templates]
101
+ if any(f is None for f in filled):
102
+ return None
103
+ return list(dict.fromkeys(_relative(f) for f in filled if f is not None))
104
+ block, module = params.get("block"), params.get("module")
105
+ if not block or not module:
106
+ return None
107
+ plan_t = args.get("plan", DEFAULT_APPROVED_PLAN)
108
+ if not isinstance(plan_t, str):
109
+ raise TargetError("args['plan'] must be a path template")
110
+ plan_rel = _fill(plan_t, params)
111
+ if plan_rel is None:
112
+ return None
113
+ path = root / _relative(plan_rel)
114
+ if not path.is_file():
115
+ return None
116
+ try:
117
+ data = json.loads(path.read_text(encoding="utf-8"))
118
+ entry = next(m for m in data.get("modules", []) if m.get("name") == module)
119
+ writes = [str(w) for w in entry.get("writes", [])]
120
+ except (OSError, ValueError, AttributeError, StopIteration) as exc:
121
+ raise TargetError(f"cannot read module {module!r} from {plan_rel}: {exc!r}") from exc
122
+ return [_relative(w) for w in writes if w.endswith(HDL_SUFFIXES)]
123
+
124
+
125
+ def formatter_config(args: Mapping[str, Any], root: Path) -> dict[str, Any] | None:
126
+ """The formatter's adapter config: arg `formatter`, else the profile's `style.formatter`."""
127
+ inline = args.get("formatter")
128
+ if inline is not None:
129
+ return AdapterCfg.model_validate(inline).model_dump()
130
+ resolved = load_profile(root)
131
+ if resolved is None or resolved.profile.style.formatter is None:
132
+ return None
133
+ return resolved.profile.style.formatter.model_dump()
134
+
135
+
136
+ def _result(
137
+ spec: CheckSpec,
138
+ status: CheckStatus,
139
+ issues: list[Issue],
140
+ start: float,
141
+ hashes: list[tuple[str, str]],
142
+ log_tail: str = "",
143
+ ) -> CheckResult:
144
+ return CheckResult(
145
+ check_id=spec.id,
146
+ status=status,
147
+ issues=tuple(issues),
148
+ log_tail=log_tail[-4000:],
149
+ duration_s=time.monotonic() - start,
150
+ idempotency_key=compute_idempotency_key(spec.id, spec.args, hashes),
151
+ )
152
+
153
+
154
+ def _plugin(use: str) -> Any:
155
+ registry = Registry()
156
+ registry.discover()
157
+ for kind in ("tool", "check"):
158
+ try:
159
+ return registry.get(kind, use)
160
+ except PluginError:
161
+ continue
162
+ raise PluginError(f"no tool or check adapter named {use!r} (the formatter)")
163
+
164
+
165
+ def _message(result: CheckResult) -> str:
166
+ msgs = "; ".join(i.msg for i in result.issues if i.severity == "error")
167
+ return msgs or result.log_tail.strip()[-400:] or f"the formatter returned {result.status}"
168
+
169
+
170
+ class FormatCheck:
171
+ """Runs the project's formatter in place on a task's output files."""
172
+
173
+ id = "format"
174
+ name = "Format"
175
+
176
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
177
+ start = time.monotonic()
178
+ root = ctx.repo_root
179
+ try:
180
+ files = target_files(spec.args, ctx.params, root)
181
+ cfg = formatter_config(spec.args, root)
182
+ except (TargetError, ValueError, ConfigError) as exc:
183
+ issue = Issue(rule="format.args", msg=str(exc))
184
+ return _result(spec, "error", [issue], start, [])
185
+ if files is None:
186
+ note = Issue(
187
+ severity="info",
188
+ rule="format.no_files",
189
+ msg=(
190
+ "nothing to format: the format check runs on an agent task's outputs "
191
+ "(args 'files', or params block and module with an approved plan)"
192
+ ),
193
+ )
194
+ return _result(spec, "skipped", [note], start, [])
195
+ if cfg is None:
196
+ found = shutil.which(DEFAULT_FORMATTER)
197
+ if found is None:
198
+ note = Issue(
199
+ severity="info",
200
+ rule="format.no_formatter",
201
+ msg=(
202
+ f"no formatter: the profile sets no style.formatter and "
203
+ f"{DEFAULT_FORMATTER} is not on PATH; the files are left as written"
204
+ ),
205
+ )
206
+ return _result(spec, "skipped", [note], start, [])
207
+ cfg = {"use": "cmd", "cmd": [found, "--inplace", "{file}"]}
208
+ use = str(cfg.pop("use"))
209
+ try:
210
+ plugin = _plugin(use)
211
+ except PluginError as exc:
212
+ return _result(spec, "error", [Issue(rule="format.adapter", msg=str(exc))], start, [])
213
+
214
+ issues: list[Issue] = []
215
+ logs: list[str] = []
216
+ hashes: list[tuple[str, str]] = []
217
+ failed = False
218
+ for rel in files:
219
+ path = root / rel
220
+ if not path.is_file():
221
+ continue # the engine's own output check reports a missing output
222
+ before = path.read_bytes()
223
+ params = {**ctx.params, "file": rel, "file_abs": str(path.resolve())}
224
+ sub_ctx = ctx.model_copy(update={"params": params})
225
+ sub_spec = CheckSpec(
226
+ id=f"{spec.id}.formatter", capability="format", adapter=use, args=cfg
227
+ )
228
+ try:
229
+ result: CheckResult = await plugin.run(sub_spec, sub_ctx)
230
+ except Exception as exc: # a broken formatter adapter: infra, never the agent's
231
+ result = CheckResult(
232
+ check_id=sub_spec.id,
233
+ status="error",
234
+ issues=(Issue(msg=f"{type(exc).__name__}: {exc}"),),
235
+ duration_s=0.0,
236
+ idempotency_key="format:raised",
237
+ )
238
+ if result.log_tail:
239
+ logs.append(result.log_tail)
240
+ if result.status != "pass" or not path.is_file():
241
+ failed = True
242
+ path.write_bytes(before) # leave the agent's file as it was
243
+ issues.append(
244
+ Issue(
245
+ file=rel,
246
+ rule="format.failed",
247
+ msg=f"the formatter ({use}) failed on {rel}: {_message(result)}",
248
+ )
249
+ )
250
+ continue
251
+ if path.read_bytes() != before:
252
+ issues.append(
253
+ Issue(severity="info", file=rel, rule="format.changed", msg=f"formatted {rel}")
254
+ )
255
+ hashes.append((rel, sha256_file(path)))
256
+ status: CheckStatus = "error" if failed else "pass"
257
+ return _result(spec, status, issues, start, hashes, "\n".join(logs))
258
+
259
+
260
+ __all__ = [
261
+ "DEFAULT_APPROVED_PLAN",
262
+ "DEFAULT_FORMATTER",
263
+ "HDL_SUFFIXES",
264
+ "FormatCheck",
265
+ "TargetError",
266
+ "formatter_config",
267
+ "target_files",
268
+ ]