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,268 @@
1
+ """Shared helpers for built-in checks: template/glob matching, file walking, hashing, timing.
2
+
3
+ Nothing here knows about a specific check; `layout.py`, `filelist.py` and `generated.py`
4
+ build on top of this module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import re
13
+ import time
14
+ from collections.abc import Iterable, Mapping
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Argument validation
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ class ArgError(Exception):
27
+ """Raised for invalid `CheckSpec.args`; the caller turns this into an `error` result."""
28
+
29
+
30
+ def require_str(args: Mapping[str, Any], key: str) -> str:
31
+ """Return `args[key]` as a string, or raise `ArgError`."""
32
+ if key not in args:
33
+ raise ArgError(f"args[{key!r}] is required")
34
+ val = args[key]
35
+ if not isinstance(val, str):
36
+ raise ArgError(f"args[{key!r}] must be a string")
37
+ return val
38
+
39
+
40
+ def require_dict(args: Mapping[str, Any], key: str) -> dict[str, Any]:
41
+ """Return `args[key]` as a dict, or raise `ArgError`."""
42
+ if key not in args:
43
+ raise ArgError(f"args[{key!r}] is required")
44
+ val = args[key]
45
+ if not isinstance(val, dict):
46
+ raise ArgError(f"args[{key!r}] must be a mapping")
47
+ return val
48
+
49
+
50
+ def optional_bool(args: Mapping[str, Any], key: str, default: bool) -> bool:
51
+ """Return `args[key]` as a bool, defaulting to `default`, or raise `ArgError`."""
52
+ val = args.get(key, default)
53
+ if not isinstance(val, bool):
54
+ raise ArgError(f"args[{key!r}] must be a boolean")
55
+ return val
56
+
57
+
58
+ def optional_str(args: Mapping[str, Any], key: str, default: str) -> str:
59
+ """Return `args[key]` as a string, defaulting to `default`, or raise `ArgError`."""
60
+ val = args.get(key, default)
61
+ if not isinstance(val, str):
62
+ raise ArgError(f"args[{key!r}] must be a string")
63
+ return val
64
+
65
+
66
+ def require_list_of_str(args: Mapping[str, Any], key: str) -> list[str]:
67
+ """Return `args[key]` as a list of strings, or raise `ArgError`."""
68
+ if key not in args:
69
+ raise ArgError(f"args[{key!r}] is required")
70
+ return _as_list_of_str(args[key], key)
71
+
72
+
73
+ def optional_list_of_str(args: Mapping[str, Any], key: str) -> list[str]:
74
+ """Return `args[key]` as a list of strings, defaulting to `[]`, or raise `ArgError`."""
75
+ if key not in args:
76
+ return []
77
+ return _as_list_of_str(args[key], key)
78
+
79
+
80
+ def _as_list_of_str(val: Any, key: str) -> list[str]:
81
+ if not isinstance(val, list) or not all(isinstance(v, str) for v in val):
82
+ raise ArgError(f"args[{key!r}] must be a list of strings")
83
+ return list(val)
84
+
85
+
86
+ def str_or_list_of_str(val: Any, where: str) -> list[str]:
87
+ """Normalize a `str | list[str]` value into a list of strings, or raise `ArgError`."""
88
+ if isinstance(val, str):
89
+ return [val]
90
+ if isinstance(val, list) and all(isinstance(v, str) for v in val):
91
+ return list(val)
92
+ raise ArgError(f"{where} must be a string or a list of strings")
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Template and glob matching
97
+ #
98
+ # A template is a repo-relative path pattern with two kinds of wildcards:
99
+ # - `{name}` matches exactly one path segment (or part of one); repeating the same
100
+ # name later in the template requires the same value (a backreference).
101
+ # - `**` (as a whole path segment, e.g. `a/**/b.sv` or `vendor/**`) matches any depth,
102
+ # including zero.
103
+ # - `*` and `?` behave as usual within a single path segment.
104
+ # ---------------------------------------------------------------------------
105
+
106
+ _PLACEHOLDER_TOKEN = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")
107
+ _GLOB_SPECIAL = re.compile(r"(\*\*/|/\*\*|\*\*|\*|\?)")
108
+
109
+
110
+ def _sanitize(name: str) -> str:
111
+ """A valid, collision-resistant Python regex group name for placeholder `name`."""
112
+ return "p_" + name.replace(".", "_")
113
+
114
+
115
+ def _glob_literal_to_regex(text: str) -> str:
116
+ """Translate glob wildcards (`**`, `*`, `?`) in a literal chunk of text to a regex body."""
117
+ parts = _GLOB_SPECIAL.split(text)
118
+ out: list[str] = []
119
+ for part in parts:
120
+ if part == "**/":
121
+ out.append("(?:.*/)?")
122
+ elif part == "/**":
123
+ out.append("(?:/.*)?")
124
+ elif part == "**":
125
+ out.append(".*")
126
+ elif part == "*":
127
+ out.append("[^/]*")
128
+ elif part == "?":
129
+ out.append("[^/]")
130
+ else:
131
+ out.append(re.escape(part))
132
+ return "".join(out)
133
+
134
+
135
+ def glob_to_regex(pattern: str) -> re.Pattern[str]:
136
+ """Compile a glob pattern (`**`, `*`, `?`) into a regex matching a whole repo-relative path."""
137
+ return re.compile("^" + _glob_literal_to_regex(pattern) + "$")
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class CompiledTemplate:
142
+ """A path template compiled to a regex, with its placeholder names in first-seen order."""
143
+
144
+ regex: re.Pattern[str]
145
+ names: tuple[str, ...]
146
+
147
+ def match(self, path: str) -> dict[str, str] | None:
148
+ """Match `path` (a repo-relative POSIX path) against this template.
149
+
150
+ Returns a mapping of placeholder name to captured value, or `None` if `path`
151
+ does not match.
152
+ """
153
+ m = self.regex.fullmatch(path)
154
+ if m is None:
155
+ return None
156
+ groups = m.groupdict()
157
+ return {name: groups[_sanitize(name)] for name in self.names}
158
+
159
+
160
+ def compile_template(template: str) -> CompiledTemplate:
161
+ """Compile a path template (placeholders, `**`, `*`, `?`) into a `CompiledTemplate`."""
162
+ tokens = _PLACEHOLDER_TOKEN.split(template)
163
+ seen: set[str] = set()
164
+ order: list[str] = []
165
+ parts: list[str] = []
166
+ is_placeholder = False
167
+ for tok in tokens:
168
+ if is_placeholder:
169
+ sanitized = _sanitize(tok)
170
+ if tok in seen:
171
+ parts.append(f"(?P={sanitized})")
172
+ else:
173
+ seen.add(tok)
174
+ order.append(tok)
175
+ parts.append(f"(?P<{sanitized}>[^/]+)")
176
+ else:
177
+ parts.append(_glob_literal_to_regex(tok))
178
+ is_placeholder = not is_placeholder
179
+ regex = re.compile("^" + "".join(parts) + "$")
180
+ return CompiledTemplate(regex=regex, names=tuple(order))
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Repo file walking
185
+ # ---------------------------------------------------------------------------
186
+
187
+ _ALWAYS_SKIP_DIR = ".git"
188
+ _STATE_DIR = (".chipgraph", "state")
189
+
190
+
191
+ def iter_repo_files(root: Path) -> list[str]:
192
+ """List every file under `root` as a repo-relative POSIX path, sorted.
193
+
194
+ Always skips `.git` and `.chipgraph/state`, regardless of check configuration.
195
+ """
196
+ out: list[str] = []
197
+ for dirpath, dirnames, filenames in os.walk(root):
198
+ rel_dir = Path(dirpath).relative_to(root)
199
+ rel_parts = () if rel_dir == Path() else rel_dir.parts
200
+ dirnames[:] = [
201
+ d for d in dirnames if d != _ALWAYS_SKIP_DIR and (*rel_parts, d) != _STATE_DIR
202
+ ]
203
+ for filename in filenames:
204
+ rel = "/".join((*rel_parts, filename)) if rel_parts else filename
205
+ out.append(rel)
206
+ return sorted(out)
207
+
208
+
209
+ def norm_path(path: Path) -> Path:
210
+ """Normalize `path` (collapse `..`/`.`) without resolving symlinks."""
211
+ return Path(os.path.normpath(str(path)))
212
+
213
+
214
+ def to_repo_rel(path: Path, root: Path) -> str | None:
215
+ """Return `path` as a repo-relative POSIX string, or `None` if it is outside `root`."""
216
+ try:
217
+ return norm_path(path).relative_to(norm_path(root)).as_posix()
218
+ except ValueError:
219
+ return None
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Hashing and idempotency
224
+ # ---------------------------------------------------------------------------
225
+
226
+
227
+ def sha256_bytes(data: bytes) -> str:
228
+ """Return the hex sha256 digest of `data`."""
229
+ return hashlib.sha256(data).hexdigest()
230
+
231
+
232
+ def sha256_file(path: Path) -> str:
233
+ """Return the hex sha256 digest of the content of the file at `path`."""
234
+ return sha256_bytes(path.read_bytes())
235
+
236
+
237
+ def canonical_json(obj: Any) -> str:
238
+ """Render `obj` as canonical JSON (sorted keys, no extra whitespace)."""
239
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), default=str)
240
+
241
+
242
+ def compute_idempotency_key(
243
+ check_id: str,
244
+ args: Mapping[str, Any],
245
+ file_hashes: Iterable[tuple[str, str]],
246
+ ) -> str:
247
+ """A stable idempotency key over the check id, its args, and the files it read.
248
+
249
+ `file_hashes` is an iterable of `(repo_relative_path, content_sha256)` pairs; it is
250
+ sorted here so caller order does not matter.
251
+ """
252
+ payload = {
253
+ "check_id": check_id,
254
+ "args": args,
255
+ "files": sorted(file_hashes),
256
+ }
257
+ return sha256_bytes(canonical_json(payload).encode("utf-8"))
258
+
259
+
260
+ def error_result(spec: CheckSpec, msg: str, start: float, *, rule: str = "args") -> CheckResult:
261
+ """Build an `error` `CheckResult` for a bad-args failure, timed from `start`."""
262
+ return CheckResult(
263
+ check_id=spec.id,
264
+ status="error",
265
+ issues=(Issue(msg=msg, severity="error", rule=rule),),
266
+ duration_s=time.monotonic() - start,
267
+ idempotency_key=compute_idempotency_key(spec.id, spec.args, ()),
268
+ )
@@ -0,0 +1,96 @@
1
+ """Shared helpers for the model-based cross checks (M1-07 part A, DESIGN.md 4.4/4.8).
2
+
3
+ Everything a `spec_schema`/`cross_chip`/`ports_diff` check needs beyond `_model.py`:
4
+ building the `error` result when the model is missing, turning a loaded model plus a
5
+ list of `Issue`s into a `pass`/`fail` `CheckResult`, and small model helpers (the block
6
+ key a scope maps to, the entities owned by a block).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ from chipgraph.checks._common import compute_idempotency_key, error_result
14
+ from chipgraph.checks._model import LoadedModel, ModelUnavailable, block_param, load_model
15
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
16
+ from chipgraph.core.contracts.types import CheckStatus
17
+ from chipgraph.core.model.model import DesignModel
18
+ from chipgraph.core.plugin_api.types import ToolContext
19
+
20
+
21
+ def load_model_or_skip(
22
+ spec: CheckSpec, ctx: ToolContext, start: float
23
+ ) -> LoadedModel | CheckResult:
24
+ """Load the Design Model, or return an `error` `CheckResult` when it is missing.
25
+
26
+ The cross checks never build the model themselves (`_model.py` docstring); a missing
27
+ store means `chipgraph ingest` has not run yet, and the message says so. The same
28
+ rule holds for every model-based check (`trace`, `connect`, `hardcode` too).
29
+ """
30
+ try:
31
+ return load_model(ctx.repo_root)
32
+ except ModelUnavailable as exc:
33
+ return error_result(spec, str(exc), start, rule="model")
34
+
35
+
36
+ def result_from_issues(
37
+ spec: CheckSpec,
38
+ issues: list[Issue],
39
+ loaded: LoadedModel,
40
+ start: float,
41
+ ) -> CheckResult:
42
+ """Build a deterministic `pass`/`fail` `CheckResult` from model-derived issues.
43
+
44
+ Issues are sorted by (file, line, rule, msg) so the same model always yields the same
45
+ order. The idempotency key is over the check id, args and the model's build-inputs
46
+ hash: the same ingested model gives the same key, and re-ingesting after an edit
47
+ changes it. A result is `fail` when any issue is an `error`, else `pass` (an `info`
48
+ issue does not fail a check).
49
+ """
50
+ ordered = sort_issues(issues)
51
+ key = compute_idempotency_key(spec.id, spec.args, [("<model>", loaded.build_inputs_hash or "")])
52
+ status: CheckStatus = "fail" if any(i.severity == "error" for i in ordered) else "pass"
53
+ return CheckResult(
54
+ check_id=spec.id,
55
+ status=status,
56
+ issues=tuple(ordered),
57
+ duration_s=time.monotonic() - start,
58
+ idempotency_key=key,
59
+ )
60
+
61
+
62
+ def sort_issues(issues: list[Issue]) -> list[Issue]:
63
+ """Return `issues` in a stable, deterministic order."""
64
+ return sorted(issues, key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
65
+
66
+
67
+ def block_key(name: str) -> str:
68
+ """The model key of the block named `name` (`block:<name>`)."""
69
+ return f"block:{name}"
70
+
71
+
72
+ def scope_block_keys(model: DesignModel, block: str) -> set[str]:
73
+ """The block key `block` scopes to, plus its instances (`instance_of` -> it).
74
+
75
+ A `--block timer` run covers `block:timer` and every `block:<instance>` that is an
76
+ `instance_of` it (D38: `timer_0`, `timer_1`), so an issue about an instance of the
77
+ scoped IP is still reported.
78
+ """
79
+ ip_key = block_key(block)
80
+ keys = {ip_key}
81
+ for rel in model.get_relations(dst=ip_key, kind="instance_of"):
82
+ keys.add(rel.src)
83
+ return keys
84
+
85
+
86
+ __all__ = [
87
+ "LoadedModel",
88
+ "ModelUnavailable",
89
+ "block_key",
90
+ "block_param",
91
+ "load_model",
92
+ "load_model_or_skip",
93
+ "result_from_issues",
94
+ "scope_block_keys",
95
+ "sort_issues",
96
+ ]
@@ -0,0 +1,64 @@
1
+ """Shared helpers for the model-based cross checks (M1-07, DESIGN.md 4.4).
2
+
3
+ Cross checks read the Design Model that `chipgraph ingest` wrote to the project's
4
+ `ModelStore`; they never run extractors themselves. A missing store is a check `error`
5
+ telling the user to run `chipgraph ingest` first.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from chipgraph.core.model.model import DesignModel
15
+ from chipgraph.core.model.store import ModelStore, default_model_db_path
16
+ from chipgraph.core.plugin_api.types import ToolContext
17
+
18
+
19
+ class ModelUnavailable(Exception):
20
+ """Raised when there is no Design Model to check (ingest has not run)."""
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class LoadedModel:
25
+ """The stored model, plus the hash of the inputs it was built from."""
26
+
27
+ model: DesignModel
28
+ build_inputs_hash: str | None
29
+ db_path: Path
30
+
31
+
32
+ def load_model(repo_root: Path) -> LoadedModel:
33
+ """Read the model `chipgraph ingest` wrote for `repo_root`.
34
+
35
+ Raises `ModelUnavailable` with a message naming the command to run when the store
36
+ does not exist yet.
37
+ """
38
+ db_path = default_model_db_path(repo_root)
39
+ if not db_path.is_file():
40
+ raise ModelUnavailable(
41
+ f"no Design Model at {db_path}; run `chipgraph ingest` before the cross checks"
42
+ )
43
+ model = ModelStore(db_path).read()
44
+ return LoadedModel(model=model, build_inputs_hash=_build_inputs_hash(db_path), db_path=db_path)
45
+
46
+
47
+ def block_param(ctx: ToolContext) -> str | None:
48
+ """The block this check run is scoped to (`chipgraph check --block`), if any."""
49
+ value = ctx.params.get("block")
50
+ return value if isinstance(value, str) and value else None
51
+
52
+
53
+ def _build_inputs_hash(db_path: Path) -> str | None:
54
+ conn = sqlite3.connect(db_path)
55
+ try:
56
+ row = conn.execute("SELECT value FROM meta WHERE key = 'build_inputs_hash'").fetchone()
57
+ except sqlite3.Error:
58
+ return None
59
+ finally:
60
+ conn.close()
61
+ return str(row[0]) if row else None
62
+
63
+
64
+ __all__ = ["LoadedModel", "ModelUnavailable", "block_param", "load_model"]
@@ -0,0 +1,235 @@
1
+ """Classify the identifiers a SystemVerilog file *declares*, using pyslang's syntax tree.
2
+
3
+ This is the point of the `naming` check: pyslang knows whether a name is a module, a
4
+ port, a parameter, a signal or a memory; a regex over text only guesses. We parse a file
5
+ into a syntax tree (no elaboration, so a single file with missing packages still parses)
6
+ and walk it, yielding one `Decl` per declared identifier with its object kind and line.
7
+
8
+ Only *declarations* are yielded. A reference like `.clk_i(...)` (a connection to a
9
+ vendored module's port), `pkg::Member`, `$clog2` or `` `MACRO`` names something the file
10
+ does not own, so it is never classified here -- matching the reference checker's rule
11
+ that we only name what we declare.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Iterator
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from pyslang.syntax import SyntaxKind, SyntaxNode, SyntaxTree
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class Decl:
26
+ """One declared identifier: its object kind, its name, and its 1-based line."""
27
+
28
+ kind: str
29
+ name: str
30
+ line: int
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ParseFailure:
35
+ """A parse error, with a 1-based line when the parser could locate one."""
36
+
37
+ line: int | None
38
+ message: str
39
+
40
+
41
+ def parse_text(text: str, name: str = "source") -> Any:
42
+ """Parse `text` into a pyslang syntax tree (syntax only, no elaboration)."""
43
+ return SyntaxTree.fromText(text, name)
44
+
45
+
46
+ def parse_file(path: Path) -> Any:
47
+ """Parse the file at `path` into a pyslang syntax tree (syntax only, no elaboration)."""
48
+ return SyntaxTree.fromText(path.read_text(errors="ignore"), path.as_posix())
49
+
50
+
51
+ def parse_errors(tree: Any) -> list[ParseFailure]:
52
+ """Real syntax errors of `tree`, as `ParseFailure`s (never raised).
53
+
54
+ Only `Parser`- and `Lexer`-subsystem error diagnostics count: those are genuine
55
+ malformed SystemVerilog. `Preprocessor` diagnostics (a `` `include`` we cannot open,
56
+ an `` `UNKNOWN_MACRO`` directive defined in a package we were not given) are expected
57
+ when a single file is parsed without its include paths -- the whole point of a
58
+ syntax-only, single-file parse -- and are ignored, matching the reference checker,
59
+ which does no preprocessing at all.
60
+ """
61
+ sm = tree.sourceManager
62
+ out: list[ParseFailure] = []
63
+ for diag in tree.diagnostics:
64
+ if not diag.isError():
65
+ continue
66
+ subsystem = str(diag.code.getSubsystem()).rsplit(".", 1)[-1]
67
+ if subsystem not in ("Parser", "Lexer"):
68
+ continue
69
+ loc = getattr(diag, "location", None)
70
+ line = sm.getLineNumber(loc) if loc else None
71
+ out.append(ParseFailure(line=line, message=str(diag.code)))
72
+ return out
73
+
74
+
75
+ def declared_identifiers(tree: Any) -> list[Decl]:
76
+ """Every identifier declared in `tree`, classified by object kind, sorted by line.
77
+
78
+ The walk dispatches on syntax node kind. It handles: module/interface/program/package
79
+ headers; ANSI and non-ANSI port declarations; parameter vs localparam; enum members;
80
+ variable/net declarations (a declarator with an unpacked dimension is a `memory`, else
81
+ a `signal`); genvars; and hierarchy instantiations.
82
+ """
83
+ sm = tree.sourceManager
84
+
85
+ def line_of(token: Any) -> int:
86
+ return int(sm.getLineNumber(token.location))
87
+
88
+ out: list[Decl] = []
89
+
90
+ def emit(kind: str, token: Any) -> None:
91
+ name = token.valueText
92
+ if name:
93
+ out.append(Decl(kind=kind, name=name, line=line_of(token)))
94
+
95
+ K = SyntaxKind
96
+ # Only a `module` declaration is checked for the module-name rule. Interface, program
97
+ # and package names have no rule in the source document and are not checked by the
98
+ # reference, so we do not classify them here.
99
+ header_kinds = {K.ModuleHeader}
100
+
101
+ def visit(node: Any) -> None:
102
+ kind = getattr(node, "kind", None)
103
+ if kind is None:
104
+ return
105
+
106
+ if kind in header_kinds:
107
+ _emit_header_name(node, "module", emit)
108
+ elif kind == K.ImplicitAnsiPort or kind == K.ExplicitAnsiPort:
109
+ _emit_declarator(node, "port", emit)
110
+ elif kind == K.PortDeclaration:
111
+ _emit_declarators(node, "port", emit)
112
+ elif kind == K.ParameterDeclaration:
113
+ _emit_parameter(node, emit)
114
+ elif kind == K.EnumType:
115
+ _emit_enum_members(node, emit)
116
+ elif kind == K.DataDeclaration:
117
+ _emit_signal_declarators(node, emit, is_net=False)
118
+ elif kind == K.NetDeclaration:
119
+ _emit_signal_declarators(node, emit, is_net=True)
120
+ elif kind == K.GenvarDeclaration:
121
+ _emit_genvars(node, emit)
122
+ elif kind == K.HierarchicalInstance:
123
+ _emit_instance(node, emit)
124
+
125
+ _walk(tree.root, visit)
126
+ out.sort(key=lambda d: (d.line, d.kind, d.name))
127
+ return out
128
+
129
+
130
+ # --------------------------------------------------------------------------------------
131
+ # per-kind extraction helpers
132
+ # --------------------------------------------------------------------------------------
133
+
134
+
135
+ def _walk(node: Any, visit: Any) -> None:
136
+ """Depth-first walk, calling `visit(node)` on every syntax node."""
137
+ if node is None:
138
+ return
139
+ visit(node)
140
+ if isinstance(node, SyntaxNode):
141
+ for i in range(len(node)):
142
+ try:
143
+ child = node[i]
144
+ except Exception: # pragma: no cover - defensive against odd child slots
145
+ child = None
146
+ _walk(child, visit)
147
+
148
+
149
+ def _emit_header_name(node: Any, kind: str, emit: Any) -> None:
150
+ name = getattr(node, "name", None)
151
+ if name is not None and not name.isMissing:
152
+ emit(kind, name)
153
+
154
+
155
+ def _iter_declarators(node: Any, attr: str) -> Iterator[Any]:
156
+ """Yield the `Declarator` items of `node.<attr>`, skipping the separator commas.
157
+
158
+ A pyslang separated list (`declarators`, `members`) iterates its comma tokens too;
159
+ we keep only the `Declarator` nodes.
160
+ """
161
+ for item in getattr(node, attr, []):
162
+ if getattr(item, "kind", None) == SyntaxKind.Declarator and not item.name.isMissing:
163
+ yield item
164
+
165
+
166
+ def _emit_declarator(node: Any, kind: str, emit: Any) -> None:
167
+ dec = getattr(node, "declarator", None)
168
+ if dec is not None and not dec.name.isMissing:
169
+ emit(kind, dec.name)
170
+
171
+
172
+ def _emit_declarators(node: Any, kind: str, emit: Any) -> None:
173
+ for dec in _iter_declarators(node, "declarators"):
174
+ emit(kind, dec.name)
175
+
176
+
177
+ def _emit_parameter(node: Any, emit: Any) -> None:
178
+ keyword = getattr(node, "keyword", None)
179
+ is_local = keyword is not None and str(keyword.kind).endswith("LocalParamKeyword")
180
+ kind = "localparam" if is_local else "parameter"
181
+ for dec in _iter_declarators(node, "declarators"):
182
+ emit(kind, dec.name)
183
+
184
+
185
+ def _emit_enum_members(node: Any, emit: Any) -> None:
186
+ for dec in _iter_declarators(node, "members"):
187
+ emit("enum_value", dec.name)
188
+
189
+
190
+ _SIGNAL_TYPE_KINDS = frozenset({"LogicType", "RegType"})
191
+ """DataDeclaration types the check treats as signals: `logic` and `reg`. A declaration of
192
+ a user-defined type (`my_struct_t x;`) or an integer type (`int i;`) is not a signal, so
193
+ it is left alone -- matching the reference, which only matches `reg`/`wire`/`logic`."""
194
+
195
+
196
+ def _emit_signal_declarators(node: Any, emit: Any, *, is_net: bool) -> None:
197
+ if not is_net:
198
+ # A DataDeclaration is only a signal when its type is `logic` or `reg`. Anything
199
+ # else (a named type, `int`, a struct) is not what this rule governs.
200
+ type_kind = str(getattr(getattr(node, "type", None), "kind", "")).rsplit(".", 1)[-1]
201
+ if type_kind not in _SIGNAL_TYPE_KINDS:
202
+ return
203
+ for dec in _iter_declarators(node, "declarators"):
204
+ # An unpacked dimension on the declarator (e.g. `logic [7:0] mem [0:15]`) makes
205
+ # this a memory; a plain declarator is a signal.
206
+ kind = "memory" if len(getattr(dec, "dimensions", [])) > 0 else "signal"
207
+ emit(kind, dec.name)
208
+
209
+
210
+ def _emit_genvars(node: Any, emit: Any) -> None:
211
+ for ident in getattr(node, "identifiers", []):
212
+ if getattr(ident, "kind", None) != SyntaxKind.IdentifierName:
213
+ continue
214
+ token = getattr(ident, "identifier", None)
215
+ if token is not None and not token.isMissing:
216
+ emit("genvar", token)
217
+
218
+
219
+ def _emit_instance(node: Any, emit: Any) -> None:
220
+ decl = getattr(node, "decl", None)
221
+ if decl is None:
222
+ return
223
+ name = getattr(decl, "name", None)
224
+ if name is not None and not name.isMissing:
225
+ emit("instance", name)
226
+
227
+
228
+ __all__ = [
229
+ "Decl",
230
+ "ParseFailure",
231
+ "declared_identifiers",
232
+ "parse_errors",
233
+ "parse_file",
234
+ "parse_text",
235
+ ]