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,203 @@
1
+ """`PortsDiffCheck`: an IP's spec ports against the RTL ports of its top module (layer 1).
2
+
3
+ For each IP block, compares the spec's declared ports (`port:spec.<block>.*`, D36/M1-05)
4
+ to the ports of the block's top RTL module (DESIGN.md 4.4 "Spec và RTL", 4.8 layer 1):
5
+
6
+ - the top module is the one module owned by the block (`ModuleEntity.block`) that no
7
+ other module owned by the block instantiates; if there is not exactly one, a name
8
+ template (arg `top`, e.g. `tiny_{block}`) picks it; if none can be found, one `info`
9
+ issue is emitted for the block and it is not failed;
10
+ - reports a port missing in RTL, a port missing in the spec, a direction mismatch, and a
11
+ width mismatch (integer widths only; a string width is skipped).
12
+
13
+ With a `--block`, only that IP (or an instance's IP) is compared.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import time
19
+
20
+ from chipgraph.checks._common import ArgError, error_result, optional_str
21
+ from chipgraph.checks._cross import (
22
+ block_param,
23
+ load_model_or_skip,
24
+ result_from_issues,
25
+ )
26
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
27
+ from chipgraph.core.model.entities import ModuleEntity, PortEntity
28
+ from chipgraph.core.model.model import DesignModel
29
+ from chipgraph.core.plugin_api.types import ToolContext
30
+
31
+
32
+ class PortsDiffCheck:
33
+ """Checks an IP's spec ports against the ports of its top RTL module."""
34
+
35
+ id = "ports_diff"
36
+ name = "Ports diff"
37
+
38
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
39
+ start = time.monotonic()
40
+ try:
41
+ top_template = optional_str(spec.args, "top", "")
42
+ except ArgError as exc:
43
+ return error_result(spec, str(exc), start)
44
+
45
+ loaded = load_model_or_skip(spec, ctx, start)
46
+ if isinstance(loaded, CheckResult):
47
+ return loaded
48
+ model = loaded.model
49
+ block = block_param(ctx)
50
+
51
+ issues: list[Issue] = []
52
+ for ip in _ips_with_spec_ports(model):
53
+ if block is not None and ip != block:
54
+ continue
55
+ issues.extend(_diff_block(model, ip, top_template))
56
+ return result_from_issues(spec, issues, loaded, start)
57
+
58
+
59
+ def _ips_with_spec_ports(model: DesignModel) -> list[str]:
60
+ """Every IP block name that has at least one spec port, sorted."""
61
+ ips: set[str] = set()
62
+ for port in model.by_kind("port"):
63
+ assert isinstance(port, PortEntity)
64
+ ip = _spec_port_block(port)
65
+ if ip is not None:
66
+ ips.add(ip)
67
+ return sorted(ips)
68
+
69
+
70
+ def _spec_port_block(port: PortEntity) -> str | None:
71
+ _, _, rest = port.key.partition(":")
72
+ parts = rest.split(".")
73
+ if len(parts) >= 3 and parts[0] == "spec":
74
+ return parts[1]
75
+ return None
76
+
77
+
78
+ def _spec_port_name(port: PortEntity) -> str:
79
+ _, _, rest = port.key.partition(":")
80
+ return rest.split(".", 2)[2]
81
+
82
+
83
+ def _diff_block(model: DesignModel, ip: str, top_template: str) -> list[Issue]:
84
+ top = _top_module(model, ip, top_template)
85
+ if top is None:
86
+ return [
87
+ Issue(
88
+ file=None,
89
+ line=None,
90
+ rule="ports_diff.no_top",
91
+ severity="info",
92
+ msg=(
93
+ f"no single top RTL module for IP {ip!r}; set the check's `top` template "
94
+ "(e.g. 'tiny_{block}') to compare its spec ports to RTL"
95
+ ),
96
+ )
97
+ ]
98
+
99
+ spec_ports = {
100
+ _spec_port_name(p): p
101
+ for p in model.by_kind("port")
102
+ if isinstance(p, PortEntity) and _spec_port_block(p) == ip
103
+ }
104
+ rtl_ports = {
105
+ p.name: p
106
+ for p in model.by_kind("port")
107
+ if isinstance(p, PortEntity) and getattr(p, "module", None) == top.key
108
+ }
109
+
110
+ issues: list[Issue] = []
111
+ for name in sorted(set(spec_ports) | set(rtl_ports)):
112
+ spec_port = spec_ports.get(name)
113
+ rtl_port = rtl_ports.get(name)
114
+ if spec_port is not None and rtl_port is None:
115
+ issues.append(
116
+ Issue(
117
+ file=spec_port.source.file,
118
+ line=spec_port.source.line,
119
+ rule="ports_diff.missing_in_rtl",
120
+ severity="error",
121
+ msg=f"spec port {name!r} of IP {ip!r} is missing from RTL module {top.name!r}",
122
+ )
123
+ )
124
+ continue
125
+ if rtl_port is not None and spec_port is None:
126
+ issues.append(
127
+ Issue(
128
+ file=rtl_port.source.file,
129
+ line=rtl_port.source.line,
130
+ rule="ports_diff.missing_in_spec",
131
+ severity="error",
132
+ msg=(
133
+ f"RTL port {name!r} of module {top.name!r} is missing from IP {ip!r}'s spec"
134
+ ),
135
+ )
136
+ )
137
+ continue
138
+ assert spec_port is not None and rtl_port is not None
139
+ if (
140
+ spec_port.direction is not None
141
+ and rtl_port.direction is not None
142
+ and spec_port.direction != rtl_port.direction
143
+ ):
144
+ issues.append(
145
+ Issue(
146
+ file=rtl_port.source.file,
147
+ line=rtl_port.source.line,
148
+ rule="ports_diff.direction",
149
+ severity="error",
150
+ msg=(
151
+ f"port {name!r} of IP {ip!r}: spec direction {spec_port.direction!r} "
152
+ f"but RTL {rtl_port.direction!r}"
153
+ ),
154
+ )
155
+ )
156
+ if (
157
+ isinstance(spec_port.width, int)
158
+ and isinstance(rtl_port.width, int)
159
+ and spec_port.width != rtl_port.width
160
+ ):
161
+ issues.append(
162
+ Issue(
163
+ file=rtl_port.source.file,
164
+ line=rtl_port.source.line,
165
+ rule="ports_diff.width",
166
+ severity="error",
167
+ msg=(
168
+ f"port {name!r} of IP {ip!r}: spec width {spec_port.width} "
169
+ f"but RTL width {rtl_port.width}"
170
+ ),
171
+ )
172
+ )
173
+ return issues
174
+
175
+
176
+ def _top_module(model: DesignModel, ip: str, top_template: str) -> ModuleEntity | None:
177
+ """The block's top RTL module, or None when it cannot be identified.
178
+
179
+ The top is the one module owned by the block that no other module owned by the block
180
+ instantiates. If there is not exactly one such module, a `top` name template (with
181
+ `{block}` substituted) picks a module by name.
182
+ """
183
+ ip_key = f"block:{ip}"
184
+ owned = [
185
+ m for m in model.by_kind("module") if isinstance(m, ModuleEntity) and m.block == ip_key
186
+ ]
187
+ if owned:
188
+ instantiated = {
189
+ r.dst for m in owned for r in model.get_relations(src=m.key, kind="instantiates")
190
+ }
191
+ roots = [m for m in owned if m.key not in instantiated]
192
+ if len(roots) == 1:
193
+ return roots[0]
194
+
195
+ if top_template:
196
+ want = top_template.replace("{block}", ip)
197
+ for module in model.by_kind("module"):
198
+ if isinstance(module, ModuleEntity) and module.name == want:
199
+ return module
200
+ return None
201
+
202
+
203
+ __all__ = ["PortsDiffCheck"]
@@ -0,0 +1,277 @@
1
+ """`SpecSchemaCheck`: per-entity sanity of the spec side of the Design Model (layer 1).
2
+
3
+ Deterministic schema checks on what a spec declared, run over the model `chipgraph
4
+ ingest` built (DESIGN.md 4.4 "Schema", 4.8 layer 1):
5
+
6
+ - a register with no access, or no reset value;
7
+ - a field whose bits fall outside its register's width (32 when the width is unknown),
8
+ or overlap another field of the same register, or have `msb < lsb`;
9
+ - a spec port with no direction, or no width;
10
+ - a requirement with empty text;
11
+ - `requirement.missing_id`: in a MAS file that declares at least one requirement ID, a
12
+ top-level numbered item of its Verification section (`requirements.infer_heading`) that
13
+ carries no ID (the extractor records it with `attrs.id_source = "missing"`). The issue
14
+ points at the item's line and, when one can be proposed, names the next free ID of the
15
+ file (`attrs.suggested_id`). See docs/REQUIREMENT_IDS.md.
16
+
17
+ Scope: with a `--block` (D38 IP block), only entities of that block (or its instances)
18
+ are checked; without one, the whole model is checked.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import time
24
+
25
+ from chipgraph.checks._cross import (
26
+ block_param,
27
+ load_model_or_skip,
28
+ result_from_issues,
29
+ scope_block_keys,
30
+ )
31
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
32
+ from chipgraph.core.model.entities import (
33
+ EntityBase,
34
+ FieldEntity,
35
+ PortEntity,
36
+ RegisterEntity,
37
+ RequirementEntity,
38
+ )
39
+ from chipgraph.core.model.model import DesignModel
40
+ from chipgraph.core.plugin_api.types import ToolContext
41
+
42
+ _DEFAULT_REGISTER_WIDTH = 32
43
+ """The register width assumed for field-bounds checks when the register does not state
44
+ one (a common case: MAS register tables give field bits, not a register width)."""
45
+
46
+
47
+ class SpecSchemaCheck:
48
+ """Checks the spec side of the Design Model for missing or inconsistent fields."""
49
+
50
+ id = "spec_schema"
51
+ name = "Spec schema"
52
+
53
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
54
+ start = time.monotonic()
55
+ loaded = load_model_or_skip(spec, ctx, start)
56
+ if isinstance(loaded, CheckResult):
57
+ return loaded
58
+ model = loaded.model
59
+ scope = _scope(model, block_param(ctx))
60
+
61
+ issues: list[Issue] = []
62
+ issues.extend(_register_issues(model, scope))
63
+ issues.extend(_field_issues(model, scope))
64
+ issues.extend(_port_issues(model, scope))
65
+ issues.extend(_requirement_issues(model, scope))
66
+ return result_from_issues(spec, issues, loaded, start)
67
+
68
+
69
+ def _scope(model: DesignModel, block: str | None) -> set[str] | None:
70
+ """The set of block keys to restrict to, or None for the whole model."""
71
+ if block is None:
72
+ return None
73
+ return scope_block_keys(model, block)
74
+
75
+
76
+ def _entity_block(entity: EntityBase) -> str | None:
77
+ """The block key an entity belongs to, from its typed field or `attrs.block`."""
78
+ block = getattr(entity, "block", None)
79
+ if isinstance(block, str) and block:
80
+ return block
81
+ attr_block = entity.attrs.get("block")
82
+ if isinstance(attr_block, str) and attr_block:
83
+ return attr_block
84
+ return None
85
+
86
+
87
+ def _prov(entity: EntityBase) -> tuple[str | None, int | None]:
88
+ return entity.source.file, entity.source.line
89
+
90
+
91
+ def _register_issues(model: DesignModel, scope: set[str] | None) -> list[Issue]:
92
+ issues: list[Issue] = []
93
+ for reg in model.by_kind("register"):
94
+ assert isinstance(reg, RegisterEntity)
95
+ if scope is not None and _entity_block(reg) not in scope:
96
+ continue
97
+ file, line = _prov(reg)
98
+ if not reg.access:
99
+ issues.append(
100
+ Issue(
101
+ file=file,
102
+ line=line,
103
+ rule="register.no_access",
104
+ severity="error",
105
+ msg=f"register {reg.key!r} has no access mode",
106
+ )
107
+ )
108
+ if reg.reset_value is None:
109
+ issues.append(
110
+ Issue(
111
+ file=file,
112
+ line=line,
113
+ rule="register.no_reset",
114
+ severity="error",
115
+ msg=f"register {reg.key!r} has no reset value",
116
+ )
117
+ )
118
+ return issues
119
+
120
+
121
+ def _field_issues(model: DesignModel, scope: set[str] | None) -> list[Issue]:
122
+ issues: list[Issue] = []
123
+ fields_by_register: dict[str, list[FieldEntity]] = {}
124
+ for field in model.by_kind("field"):
125
+ assert isinstance(field, FieldEntity)
126
+ if field.register is None:
127
+ continue
128
+ fields_by_register.setdefault(field.register, []).append(field)
129
+
130
+ for reg_key, fields in fields_by_register.items():
131
+ reg = model.get(reg_key)
132
+ if scope is not None and (reg is None or _entity_block(reg) not in scope):
133
+ continue
134
+ width = reg.width if isinstance(reg, RegisterEntity) and reg.width else None
135
+ limit = width if width is not None else _DEFAULT_REGISTER_WIDTH
136
+ placed: list[tuple[int, int, str]] = [] # (lsb, msb, key), for overlap
137
+ for field in sorted(fields, key=lambda f: f.key):
138
+ file, line = _prov(field)
139
+ if field.msb is None or field.lsb is None:
140
+ # A field with no bit range cannot be range-checked; skip silently: a
141
+ # missing width is a different concern from an out-of-range field.
142
+ continue
143
+ if field.msb < field.lsb:
144
+ issues.append(
145
+ Issue(
146
+ file=file,
147
+ line=line,
148
+ rule="field.msb_lt_lsb",
149
+ severity="error",
150
+ msg=f"field {field.key!r} has msb {field.msb} < lsb {field.lsb}",
151
+ )
152
+ )
153
+ continue
154
+ if field.msb >= limit:
155
+ issues.append(
156
+ Issue(
157
+ file=file,
158
+ line=line,
159
+ rule="field.out_of_range",
160
+ severity="error",
161
+ msg=(
162
+ f"field {field.key!r} bits [{field.msb}:{field.lsb}] fall outside "
163
+ f"register {reg_key!r} width {limit}"
164
+ ),
165
+ )
166
+ )
167
+ continue
168
+ for lsb2, msb2, key2 in placed:
169
+ if field.lsb <= msb2 and lsb2 <= field.msb:
170
+ issues.append(
171
+ Issue(
172
+ file=file,
173
+ line=line,
174
+ rule="field.overlap",
175
+ severity="error",
176
+ msg=(
177
+ f"field {field.key!r} bits [{field.msb}:{field.lsb}] overlap "
178
+ f"field {key2!r} bits [{msb2}:{lsb2}]"
179
+ ),
180
+ )
181
+ )
182
+ break
183
+ placed.append((field.lsb, field.msb, field.key))
184
+ return issues
185
+
186
+
187
+ def _port_issues(model: DesignModel, scope: set[str] | None) -> list[Issue]:
188
+ issues: list[Issue] = []
189
+ for port in model.by_kind("port"):
190
+ assert isinstance(port, PortEntity)
191
+ block = _spec_port_block(port)
192
+ if block is None:
193
+ continue # not a spec port (`port:spec.<block>.*`)
194
+ if scope is not None and f"block:{block}" not in scope:
195
+ continue
196
+ file, line = _prov(port)
197
+ if port.direction is None:
198
+ issues.append(
199
+ Issue(
200
+ file=file,
201
+ line=line,
202
+ rule="port.no_direction",
203
+ severity="error",
204
+ msg=f"spec port {port.key!r} has no direction",
205
+ )
206
+ )
207
+ if port.width is None:
208
+ issues.append(
209
+ Issue(
210
+ file=file,
211
+ line=line,
212
+ rule="port.no_width",
213
+ severity="error",
214
+ msg=f"spec port {port.key!r} has no width",
215
+ )
216
+ )
217
+ return issues
218
+
219
+
220
+ def _spec_port_block(port: PortEntity) -> str | None:
221
+ """The block name of a spec port key (`port:spec.<block>.<name>`), else None."""
222
+ _, _, rest = port.key.partition(":")
223
+ parts = rest.split(".")
224
+ if len(parts) >= 3 and parts[0] == "spec":
225
+ return parts[1]
226
+ return None
227
+
228
+
229
+ def _requirement_issues(model: DesignModel, scope: set[str] | None) -> list[Issue]:
230
+ issues: list[Issue] = []
231
+ for req in model.by_kind("requirement"):
232
+ assert isinstance(req, RequirementEntity)
233
+ if scope is not None and not _requirement_in_scope(req, scope):
234
+ continue
235
+ if req.attrs.get("id_source") == "missing":
236
+ file, line = _prov(req)
237
+ suggested = req.attrs.get("suggested_id")
238
+ hint = f" Suggested next ID: {suggested}" if isinstance(suggested, str) else ""
239
+ issues.append(
240
+ Issue(
241
+ file=file,
242
+ line=line,
243
+ rule="requirement.missing_id",
244
+ severity="error",
245
+ msg=(
246
+ f"Verification item has no ID; the other items of this file have one.{hint}"
247
+ ),
248
+ )
249
+ )
250
+ if req.text is None or not req.text.strip():
251
+ file, line = _prov(req)
252
+ issues.append(
253
+ Issue(
254
+ file=file,
255
+ line=line,
256
+ rule="requirement.empty_text",
257
+ severity="error",
258
+ msg=f"requirement {req.key!r} has empty text",
259
+ )
260
+ )
261
+ return issues
262
+
263
+
264
+ def _requirement_in_scope(req: RequirementEntity, scope: set[str]) -> bool:
265
+ """Whether a requirement belongs to a scoped block, by its `block`/key prefix."""
266
+ block = _entity_block(req)
267
+ if block is not None:
268
+ return block in scope
269
+ # A requirement key is `requirement:<block>.h<hash>` (inferred or missing, D37) or
270
+ # `requirement:<ID>` (declared). Only the hashed form carries a block; a declared
271
+ # ID that names no block is left to a whole-model run.
272
+ _, _, rest = req.key.partition(":")
273
+ head = rest.split(".", 1)[0]
274
+ return f"block:{head}" in scope
275
+
276
+
277
+ __all__ = ["SpecSchemaCheck"]