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,151 @@
1
+ """`GeneratedCheck`: generated files are not edited by hand.
2
+
3
+ Also exports `stamp`/`verify`, the two functions generators use to mark a file as
4
+ generated and later confirm it has not been hand-edited. See DESIGN.md 7.3.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import re
11
+ import time
12
+
13
+ from chipgraph.checks._common import (
14
+ ArgError,
15
+ compute_idempotency_key,
16
+ error_result,
17
+ glob_to_regex,
18
+ iter_repo_files,
19
+ optional_bool,
20
+ require_list_of_str,
21
+ sha256_file,
22
+ )
23
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
24
+ from chipgraph.core.contracts.types import CheckStatus
25
+ from chipgraph.core.plugin_api.types import ToolContext
26
+
27
+ _MARKER_SEARCH_LINES = 20
28
+ _COMMENT_CLOSERS = {"<!--": " -->"}
29
+
30
+ # A marker line: an optional comment prefix, then `chipgraph:generated sha256=<64 hex>`,
31
+ # then an optional closer (for `<!--`-style comments).
32
+ _MARKER_RE = re.compile(
33
+ r"^\s*(?://|#|--|;|<!--)?\s*chipgraph:generated sha256=([0-9a-f]{64})\s*(?:-->)?\s*$"
34
+ )
35
+
36
+
37
+ def stamp(text: str, comment_prefix: str = "//") -> str:
38
+ """Insert or replace the generation marker as the first line of `text`.
39
+
40
+ The marker's hash covers `text` with any existing marker's first line removed, so
41
+ calling `stamp` again on already-stamped, unedited text is a no-op (byte for byte).
42
+ """
43
+ lines = text.splitlines(keepends=True)
44
+ has_marker = bool(lines) and _MARKER_RE.match(_strip_eol(lines[0])) is not None
45
+ base_text = "".join(lines[1:]) if has_marker else text
46
+
47
+ digest = hashlib.sha256(base_text.encode("utf-8")).hexdigest()
48
+ closer = _COMMENT_CLOSERS.get(comment_prefix, "")
49
+ eol = _eol_of(lines[0]) if lines else "\n"
50
+ marker_line = f"{comment_prefix} chipgraph:generated sha256={digest}{closer}{eol}"
51
+ return marker_line + base_text
52
+
53
+
54
+ def verify(text: str) -> bool | None:
55
+ """Check `text`'s generation marker against its content.
56
+
57
+ Returns `True` if the marker matches, `False` if it does not (hand-edited), or
58
+ `None` if no marker is found in the first 20 lines.
59
+ """
60
+ lines = text.splitlines(keepends=True)
61
+ limit = min(len(lines), _MARKER_SEARCH_LINES)
62
+ for i in range(limit):
63
+ m = _MARKER_RE.match(_strip_eol(lines[i]))
64
+ if m is None:
65
+ continue
66
+ stored = m.group(1)
67
+ remainder = "".join(lines[:i] + lines[i + 1 :])
68
+ actual = hashlib.sha256(remainder.encode("utf-8")).hexdigest()
69
+ return actual == stored
70
+ return None
71
+
72
+
73
+ def _strip_eol(line: str) -> str:
74
+ return line.rstrip("\n").rstrip("\r")
75
+
76
+
77
+ def _eol_of(line: str) -> str:
78
+ if line.endswith("\r\n"):
79
+ return "\r\n"
80
+ if line.endswith("\n"):
81
+ return "\n"
82
+ return "\n"
83
+
84
+
85
+ class GeneratedCheck:
86
+ """Checks that generated files still match the marker hash they were stamped with."""
87
+
88
+ id = "generated"
89
+ name = "Generated"
90
+
91
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
92
+ start = time.monotonic()
93
+ args = spec.args
94
+
95
+ try:
96
+ globs = require_list_of_str(args, "files")
97
+ require_marker = optional_bool(args, "require_marker", False)
98
+ regexes = [glob_to_regex(g) for g in globs]
99
+ except ArgError as exc:
100
+ return error_result(spec, str(exc), start)
101
+
102
+ all_files = iter_repo_files(ctx.repo_root)
103
+ matched = [f for f in all_files if any(r.fullmatch(f) for r in regexes)]
104
+
105
+ issues: list[Issue] = []
106
+ read_files: dict[str, str] = {}
107
+ for f in matched:
108
+ full_path = ctx.repo_root / f
109
+ read_files[f] = sha256_file(full_path)
110
+ try:
111
+ text = full_path.read_text()
112
+ except (OSError, UnicodeDecodeError) as exc:
113
+ issues.append(
114
+ Issue(
115
+ file=f,
116
+ rule="generated",
117
+ severity="error",
118
+ msg=f"could not read `{f}`: {exc}",
119
+ )
120
+ )
121
+ continue
122
+
123
+ result = verify(text)
124
+ if result is False:
125
+ issues.append(
126
+ Issue(
127
+ file=f,
128
+ rule="generated",
129
+ severity="error",
130
+ msg=f"`{f}` was edited by hand after it was generated",
131
+ )
132
+ )
133
+ elif result is None:
134
+ issues.append(
135
+ Issue(
136
+ file=f,
137
+ rule="generated",
138
+ severity="error" if require_marker else "warning",
139
+ msg=f"`{f}` has no generation marker",
140
+ )
141
+ )
142
+
143
+ key = compute_idempotency_key(spec.id, args, tuple(read_files.items()))
144
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
145
+ return CheckResult(
146
+ check_id=spec.id,
147
+ status=status,
148
+ issues=tuple(issues),
149
+ duration_s=time.monotonic() - start,
150
+ idempotency_key=key,
151
+ )
@@ -0,0 +1,233 @@
1
+ """`HardcodeCheck`: numbers the contract already names must not be typed by hand in RTL.
2
+
3
+ A layer-1 (structure) cross check (DESIGN.md 7.3). It reads the Design Model
4
+ `chipgraph ingest` wrote for the shared numbers a design must not duplicate, scans the
5
+ integer literals of the RTL files in scope (via the pyslang token stream, so numbers
6
+ inside comments and strings are never seen; sized literals like `32'h8000_4000` are
7
+ decoded), and reports each literal that equals a shared number.
8
+
9
+ Shared numbers come from the model:
10
+
11
+ * every memory region's base, size and end address (`base + size`) whose value is at
12
+ least arg `min_value` (default `0x100`); and
13
+ * every interrupt line number, but only counted when the literal is used as an index on
14
+ the arg `irq_vector` net (small line numbers are otherwise indistinguishable from
15
+ ordinary constants, so they are not flagged elsewhere).
16
+
17
+ A `hardcode.literal` issue is *not* raised when the literal's line carries the arg
18
+ `allow_comment` marker (default `contract:`, matching QSoC's `// contract: <key>` tag) or
19
+ when the file is one of the generated packages listed in arg `generated`.
20
+
21
+ A missing model is a whole-check `error` (run `chipgraph ingest` first).
22
+
23
+ Example profile usage (`.chipgraph.yml`):
24
+
25
+ ```yaml
26
+ adapters:
27
+ hardcode:
28
+ use: hardcode
29
+ scope: ["design/**/rtl/**/*.sv"]
30
+ min_value: 0x100
31
+ generated: ["design/common/rtl/qnsc_pkg.sv"]
32
+ ```
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ import time
39
+ from pathlib import Path
40
+
41
+ from chipgraph.checks._common import (
42
+ ArgError,
43
+ compute_idempotency_key,
44
+ error_result,
45
+ glob_to_regex,
46
+ iter_repo_files,
47
+ optional_list_of_str,
48
+ optional_str,
49
+ require_list_of_str,
50
+ sha256_file,
51
+ )
52
+ from chipgraph.checks._model import ModelUnavailable, load_model
53
+ from chipgraph.checks._xref_syntax import parse_rtl
54
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
55
+ from chipgraph.core.contracts.types import CheckStatus
56
+ from chipgraph.core.model.model import DesignModel
57
+ from chipgraph.core.plugin_api.types import ToolContext
58
+
59
+ _DEFAULT_MIN_VALUE = 0x100
60
+ _DEFAULT_ALLOW_COMMENT = "contract:"
61
+
62
+
63
+ class HardcodeCheck:
64
+ """Checks that no RTL literal duplicates a memory-map or interrupt number."""
65
+
66
+ id = "hardcode"
67
+ name = "Hardcode"
68
+ per_block = False # chip-wide: `chipgraph check` runs it once, not per block
69
+
70
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
71
+ start = time.monotonic()
72
+ args = spec.args
73
+
74
+ try:
75
+ scope = require_list_of_str(args, "scope")
76
+ if not scope:
77
+ raise ArgError("args['scope'] must list at least one glob")
78
+ min_value = _int_arg(args, "min_value", _DEFAULT_MIN_VALUE)
79
+ allow_comment = optional_str(args, "allow_comment", _DEFAULT_ALLOW_COMMENT)
80
+ generated = set(optional_list_of_str(args, "generated"))
81
+ ignore = optional_list_of_str(args, "ignore")
82
+ irq_vector = optional_str(args, "irq_vector", "")
83
+ except ArgError as exc:
84
+ return error_result(spec, str(exc), start)
85
+
86
+ try:
87
+ loaded = load_model(ctx.repo_root)
88
+ except ModelUnavailable as exc:
89
+ return error_result(spec, str(exc), start, rule="model")
90
+
91
+ shared = _shared_numbers(loaded.model, min_value)
92
+ irq_lines = _interrupt_lines(loaded.model)
93
+
94
+ scope_regexes = [glob_to_regex(p) for p in scope]
95
+ ignore_regexes = [glob_to_regex(p) for p in ignore]
96
+ in_scope = [
97
+ f
98
+ for f in iter_repo_files(ctx.repo_root)
99
+ if any(r.fullmatch(f) for r in scope_regexes)
100
+ and not any(r.fullmatch(f) for r in ignore_regexes)
101
+ ]
102
+
103
+ issues: list[Issue] = []
104
+ for rel in in_scope:
105
+ if rel in generated:
106
+ continue
107
+ issues.extend(
108
+ _file_issues(ctx.repo_root / rel, rel, shared, irq_lines, allow_comment, irq_vector)
109
+ )
110
+ issues.sort(key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
111
+
112
+ file_hashes = [(f, sha256_file(ctx.repo_root / f)) for f in in_scope]
113
+ file_hashes.append(("<model>", loaded.build_inputs_hash or "none"))
114
+ key = compute_idempotency_key(spec.id, args, file_hashes)
115
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
116
+ return CheckResult(
117
+ check_id=spec.id,
118
+ status=status,
119
+ issues=tuple(issues),
120
+ duration_s=time.monotonic() - start,
121
+ idempotency_key=key,
122
+ )
123
+
124
+
125
+ def _int_arg(args: object, key: str, default: int) -> int:
126
+ from collections.abc import Mapping
127
+
128
+ if not isinstance(args, Mapping) or key not in args:
129
+ return default
130
+ val = args[key]
131
+ if isinstance(val, bool) or not isinstance(val, int):
132
+ raise ArgError(f"args[{key!r}] must be an integer")
133
+ return val
134
+
135
+
136
+ def _shared_numbers(model: DesignModel, min_value: int) -> dict[int, list[str]]:
137
+ """Map each shared memory-map number (>= `min_value`) to the model keys naming it."""
138
+ out: dict[int, list[str]] = {}
139
+
140
+ def add(value: object, label: str) -> None:
141
+ if isinstance(value, bool) or not isinstance(value, int):
142
+ return
143
+ if value < min_value:
144
+ return
145
+ out.setdefault(value, [])
146
+ if label not in out[value]:
147
+ out[value].append(label)
148
+
149
+ for region in model.by_kind("memory_region"):
150
+ base = getattr(region, "base", None)
151
+ size = getattr(region, "size", None)
152
+ add(base, f"{region.key}.base")
153
+ add(size, f"{region.key}.size")
154
+ if isinstance(base, int) and isinstance(size, int):
155
+ add(base + size, f"{region.key}.end")
156
+ return out
157
+
158
+
159
+ def _interrupt_lines(model: DesignModel) -> dict[int, list[str]]:
160
+ """Map each interrupt line number to the interrupt keys that use it."""
161
+ out: dict[int, list[str]] = {}
162
+ for irq in model.by_kind("interrupt"):
163
+ line = getattr(irq, "line", None)
164
+ if isinstance(line, int) and not isinstance(line, bool):
165
+ out.setdefault(line, [])
166
+ if irq.key not in out[line]:
167
+ out[line].append(irq.key)
168
+ return out
169
+
170
+
171
+ def _file_issues(
172
+ path: Path,
173
+ rel: str,
174
+ shared: dict[int, list[str]],
175
+ irq_lines: dict[int, list[str]],
176
+ allow_comment: str,
177
+ irq_vector: str,
178
+ ) -> list[Issue]:
179
+ parsed = parse_rtl(path)
180
+ issues: list[Issue] = [
181
+ Issue(
182
+ file=rel, line=pf.line, rule="parse", severity="error", msg=f"parse error: {pf.message}"
183
+ )
184
+ for pf in parsed.parse_errors
185
+ ]
186
+ text_lines = path.read_text(errors="ignore").splitlines()
187
+ index_re = re.compile(rf"{re.escape(irq_vector)}\s*\[\s*(\d+)\s*\]") if irq_vector else None
188
+
189
+ for lit in parsed.literals:
190
+ if _has_marker(parsed.line_comments.get(lit.line, ""), allow_comment):
191
+ continue
192
+ keys = shared.get(lit.value)
193
+ if keys:
194
+ issues.append(
195
+ Issue(
196
+ file=rel,
197
+ line=lit.line,
198
+ rule="hardcode.literal",
199
+ severity="error",
200
+ msg=(
201
+ f"literal {lit.raw!r} (= {lit.value}) duplicates the contract number "
202
+ f"{', '.join(sorted(keys))}; take it from the contract, "
203
+ f"or tag the line `// {allow_comment} <key>`"
204
+ ),
205
+ )
206
+ )
207
+ continue
208
+ # An interrupt line number only counts as a shared number when used as an index
209
+ # on the interrupt vector net.
210
+ if index_re is not None and lit.value in irq_lines:
211
+ line_text = text_lines[lit.line - 1] if 0 < lit.line <= len(text_lines) else ""
212
+ if any(int(m.group(1)) == lit.value for m in index_re.finditer(line_text)):
213
+ keys = irq_lines[lit.value]
214
+ issues.append(
215
+ Issue(
216
+ file=rel,
217
+ line=lit.line,
218
+ rule="hardcode.literal",
219
+ severity="error",
220
+ msg=(
221
+ f"literal {lit.raw!r} indexes {irq_vector!r} at bit {lit.value}, the "
222
+ f"contract line of {', '.join(sorted(keys))}; take it from the contract"
223
+ ),
224
+ )
225
+ )
226
+ return issues
227
+
228
+
229
+ def _has_marker(comment: str, allow_comment: str) -> bool:
230
+ return allow_comment in comment
231
+
232
+
233
+ __all__ = ["HardcodeCheck"]
@@ -0,0 +1,155 @@
1
+ """`LayoutCheck`: every file in scope lives where the project's layout templates say.
2
+
3
+ See DESIGN.md 7.3 for the `layout` check and its `.chipgraph.yml` example.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from typing import Any
10
+
11
+ from chipgraph.checks._common import (
12
+ ArgError,
13
+ CompiledTemplate,
14
+ compile_template,
15
+ compute_idempotency_key,
16
+ error_result,
17
+ glob_to_regex,
18
+ iter_repo_files,
19
+ require_dict,
20
+ require_list_of_str,
21
+ sha256_file,
22
+ str_or_list_of_str,
23
+ )
24
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
25
+ from chipgraph.core.contracts.types import CheckStatus
26
+ from chipgraph.core.plugin_api.types import ToolContext
27
+
28
+
29
+ class LayoutCheck:
30
+ """Checks that every in-scope file matches at least one declared layout template."""
31
+
32
+ id = "layout"
33
+ name = "Layout"
34
+
35
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
36
+ start = time.monotonic()
37
+ args = spec.args
38
+
39
+ try:
40
+ templates_by_kind = _flatten_templates(require_dict(args, "templates"))
41
+ scope = require_list_of_str(args, "scope")
42
+ ignore = args.get("ignore", [])
43
+ if not isinstance(ignore, list) or not all(isinstance(v, str) for v in ignore):
44
+ raise ArgError("args['ignore'] must be a list of strings")
45
+ values = _parse_values(args.get("values", {}))
46
+ _check_no_duplicate_templates(templates_by_kind)
47
+ compiled = [(kind, tmpl, compile_template(tmpl)) for kind, tmpl in templates_by_kind]
48
+ except ArgError as exc:
49
+ return error_result(spec, str(exc), start)
50
+
51
+ scope_regexes = [glob_to_regex(p) for p in scope]
52
+ ignore_regexes = [glob_to_regex(p) for p in ignore]
53
+
54
+ all_files = iter_repo_files(ctx.repo_root)
55
+ in_scope = [
56
+ f
57
+ for f in all_files
58
+ if any(r.fullmatch(f) for r in scope_regexes)
59
+ and not any(r.fullmatch(f) for r in ignore_regexes)
60
+ ]
61
+
62
+ issues: list[Issue] = []
63
+ for path in in_scope:
64
+ issues.extend(_check_file(path, compiled, values))
65
+
66
+ file_hashes = tuple((f, sha256_file(ctx.repo_root / f)) for f in in_scope)
67
+ key = compute_idempotency_key(spec.id, args, file_hashes)
68
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
69
+ return CheckResult(
70
+ check_id=spec.id,
71
+ status=status,
72
+ issues=tuple(issues),
73
+ duration_s=time.monotonic() - start,
74
+ idempotency_key=key,
75
+ )
76
+
77
+
78
+ def _flatten_templates(templates: dict[str, Any]) -> list[tuple[str, str]]:
79
+ out: list[tuple[str, str]] = []
80
+ for kind, val in templates.items():
81
+ for tmpl in str_or_list_of_str(val, f"args['templates'][{kind!r}]"):
82
+ out.append((kind, tmpl))
83
+ if not out:
84
+ raise ArgError("args['templates'] must declare at least one template")
85
+ return out
86
+
87
+
88
+ def _check_no_duplicate_templates(templates_by_kind: list[tuple[str, str]]) -> None:
89
+ seen: dict[str, str] = {}
90
+ for kind, tmpl in templates_by_kind:
91
+ if tmpl in seen:
92
+ raise ArgError(f"templates {seen[tmpl]!r} and {kind!r} are identical: {tmpl!r}")
93
+ seen[tmpl] = kind
94
+
95
+
96
+ def _parse_values(raw: Any) -> dict[str, list[str]]:
97
+ if not isinstance(raw, dict):
98
+ raise ArgError("args['values'] must be a mapping")
99
+ out: dict[str, list[str]] = {}
100
+ for name, allowed in raw.items():
101
+ out[name] = str_or_list_of_str(allowed, f"args['values'][{name!r}]")
102
+ return out
103
+
104
+
105
+ def _check_file(
106
+ path: str,
107
+ compiled: list[tuple[str, str, CompiledTemplate]],
108
+ values: dict[str, list[str]],
109
+ ) -> list[Issue]:
110
+ matches: list[tuple[str, dict[str, str]]] = []
111
+ for kind, _tmpl, compiled_tmpl in compiled:
112
+ groups = compiled_tmpl.match(path)
113
+ if groups is not None:
114
+ matches.append((kind, groups))
115
+
116
+ if not matches:
117
+ return [
118
+ Issue(
119
+ file=path,
120
+ rule="layout",
121
+ severity="error",
122
+ msg=f"`{path}` is not in any layout template",
123
+ )
124
+ ]
125
+
126
+ # A file may syntactically match more than one template (e.g. an overly broad and a
127
+ # more specific one). It passes if at least one match has only allowed placeholder
128
+ # values; otherwise we report the value violations of the first match.
129
+ first_violations: list[Issue] | None = None
130
+ for _kind, groups in matches:
131
+ violations = _value_violations(path, groups, values)
132
+ if not violations:
133
+ return []
134
+ if first_violations is None:
135
+ first_violations = violations
136
+ assert first_violations is not None
137
+ return first_violations
138
+
139
+
140
+ def _value_violations(
141
+ path: str, groups: dict[str, str], values: dict[str, list[str]]
142
+ ) -> list[Issue]:
143
+ issues: list[Issue] = []
144
+ for name, val in groups.items():
145
+ allowed = values.get(name)
146
+ if allowed is not None and val not in allowed:
147
+ issues.append(
148
+ Issue(
149
+ file=path,
150
+ rule="layout",
151
+ severity="error",
152
+ msg=f"{name} `{val}` is not one of: {', '.join(allowed)}",
153
+ )
154
+ )
155
+ return issues