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,523 @@
1
+ """Wire a project's profile to the extractors and run the core ingest (DESIGN.md 4.1-4.5).
2
+
3
+ `run_ingest` reads the resolved profile, builds a `SourcePart` for every input it can
4
+ (the chip-level spec, each block's RTL via the pyslang extractor, each block's MAS via the
5
+ `mas-markdown` extractor), calls `chipgraph.core.model.ingest.ingest` to merge them
6
+ deterministically, and writes the result to the project's `ModelStore` (rebuilt from
7
+ scratch, under `.chipgraph/state/cache/`, so the repo tree stays clean).
8
+
9
+ This layer may import adapters, packs and extractors; the merge logic itself is in
10
+ `core.model.ingest`, which knows nothing about them.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING
20
+
21
+ from chipgraph.adapters.tool.filelist import Filelist, FilelistError, read_filelist
22
+ from chipgraph.adapters.tool.pyslang import ParseDiagnostic, PyslangExtractor
23
+ from chipgraph.app.context import AppContext
24
+ from chipgraph.core.config.loader import ResolvedProfile
25
+ from chipgraph.core.config.models import Profile
26
+ from chipgraph.core.model.ingest import (
27
+ IngestIssue,
28
+ IngestResult,
29
+ InputFile,
30
+ SourcePart,
31
+ ingest,
32
+ )
33
+ from chipgraph.core.model.model import DesignModel
34
+ from chipgraph.core.model.store import ModelStore, default_model_db_path
35
+ from chipgraph.core.plugin_api.registry import PluginError, Registry
36
+ from chipgraph.packs.spec_core.extract.mas import MasDiagnostic, MasExtractor
37
+
38
+ if TYPE_CHECKING:
39
+ from chipgraph.packs.assist.ask.documents import DocumentIndexReport
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class IngestReport:
44
+ """The result of `run_ingest`: what was written, plus the model, stats and issues."""
45
+
46
+ model: DesignModel
47
+ result: IngestResult
48
+ db_path: Path
49
+ documents: DocumentIndexReport | None = None
50
+ """The documents indexed for `/ask` full-text search (and the ones skipped)."""
51
+
52
+ @property
53
+ def issues(self) -> tuple[IngestIssue, ...]:
54
+ return self.result.issues
55
+
56
+ @property
57
+ def has_error(self) -> bool:
58
+ return any(issue.severity == "error" for issue in self.result.issues)
59
+
60
+
61
+ def run_ingest(ctx: AppContext) -> IngestReport:
62
+ """Run every extractor for the project, merge, and write the Design Model store.
63
+
64
+ Raises `AppError` if no profile is loaded. A missing file or an extractor exception
65
+ for one block becomes an `error` `IngestIssue` for that block; the rest of ingest
66
+ still runs.
67
+ """
68
+ resolved = ctx.require_profile()
69
+ root = ctx.root
70
+
71
+ builder = _PartBuilder(root, resolved, ctx.registry)
72
+ builder.build_chip_spec()
73
+ builder.build_blocks()
74
+
75
+ model, result = ingest(
76
+ builder.parts,
77
+ input_files=builder.input_files,
78
+ profile_digest=_profile_digest(resolved.profile),
79
+ ip_blocks={name: override.instances for name, override in resolved.profile.blocks.items()},
80
+ )
81
+ # Fold the builder's own file-level issues (missing files, adapter exceptions) in.
82
+ result = _with_extra_issues(result, builder.issues)
83
+
84
+ db_path = default_model_db_path(root)
85
+ store = ModelStore(db_path)
86
+ store.write(model, build_inputs_hash=result.build_inputs_hash)
87
+ # `write()` rebuilds the database and drops every document: re-index the `/ask`
88
+ # documents (inputs read above plus the project's Markdown docs) right after it.
89
+ # Imported here: the `assist` pack imports this module (its `/audit` runs ingest).
90
+ from chipgraph.packs.assist.ask.documents import index_documents
91
+
92
+ documents = index_documents(
93
+ root, store, [f.rel_path for f in builder.input_files], ctx.store.labels
94
+ )
95
+ result = _with_extra_issues(result, _document_issues(documents))
96
+ return IngestReport(model=model, result=result, db_path=db_path, documents=documents)
97
+
98
+
99
+ def _document_issues(documents: DocumentIndexReport) -> list[IngestIssue]:
100
+ """One `info` issue per `nda` file that was not indexed for `/ask`."""
101
+ return [
102
+ IngestIssue(
103
+ severity="info",
104
+ code="ask.nda_not_indexed",
105
+ message=f"{path} is labelled 'nda': not indexed for /ask (it may use a cloud model)",
106
+ file=path,
107
+ )
108
+ for path in documents.nda_skipped
109
+ ]
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class _PartBuilder:
114
+ """Accumulates `SourcePart`s, the files they were read from, and file-level issues."""
115
+
116
+ root: Path
117
+ resolved: ResolvedProfile
118
+ registry: Registry
119
+ parts: list[SourcePart] = None # type: ignore[assignment]
120
+ input_files: list[InputFile] = None # type: ignore[assignment]
121
+ issues: list[IngestIssue] = None # type: ignore[assignment]
122
+
123
+ def __post_init__(self) -> None:
124
+ self.parts = []
125
+ self.input_files = []
126
+ self.issues = []
127
+
128
+ # -- chip-level spec ------------------------------------------------------------
129
+
130
+ def build_chip_spec(self) -> None:
131
+ chip = self.resolved.profile.spec.chip
132
+ if chip is None:
133
+ return
134
+ path = (self.root / chip.path).resolve()
135
+ # Any `format` adapter in the registry (built-in, entry point or local plugin) that
136
+ # offers `load_model(path, root)` can build the chip-level part.
137
+ try:
138
+ adapter = self.registry.get("format", chip.format)
139
+ except PluginError as exc:
140
+ self.issues.append(
141
+ IngestIssue(
142
+ severity="error",
143
+ code="chip_spec_format",
144
+ message=f"unknown chip spec format {chip.format!r}: {exc}",
145
+ file=chip.path,
146
+ )
147
+ )
148
+ return
149
+ load_model = getattr(adapter, "load_model", None)
150
+ if not callable(load_model):
151
+ self.issues.append(
152
+ IngestIssue(
153
+ severity="error",
154
+ code="chip_spec_format",
155
+ message=f"format adapter {chip.format!r} has no load_model(path, root)",
156
+ file=chip.path,
157
+ )
158
+ )
159
+ return
160
+ if not path.is_file():
161
+ self.issues.append(
162
+ IngestIssue(
163
+ severity="error",
164
+ code="missing_file",
165
+ message=f"chip spec not found: {chip.path}",
166
+ file=chip.path,
167
+ )
168
+ )
169
+ return
170
+ try:
171
+ model, warnings = load_model(path, root=self.root)
172
+ except Exception as exc: # extractor must not crash ingest
173
+ self.issues.append(
174
+ IngestIssue(
175
+ severity="error",
176
+ code="chip_spec_error",
177
+ message=f"failed to read chip spec {chip.path}: {exc}",
178
+ file=chip.path,
179
+ )
180
+ )
181
+ return
182
+ self._record_file(path)
183
+ diagnostics = tuple(
184
+ IngestIssue(severity="warning", code="chip_spec_warning", message=w) for w in warnings
185
+ )
186
+ self.parts.append(
187
+ SourcePart(source="chip", block=None, model=model, diagnostics=diagnostics)
188
+ )
189
+
190
+ # -- per-block RTL and MAS ------------------------------------------------------
191
+
192
+ def build_blocks(self) -> None:
193
+ for block in sorted(self.resolved.profile.blocks):
194
+ block_profile = self.resolved.for_block(block)
195
+ self._build_block_rtl(block, block_profile)
196
+ for template in _layout_templates(block_profile, "spec"):
197
+ self._build_block_mas(block, _fill(template, block))
198
+ self._report_unread_specs()
199
+
200
+ def _report_unread_specs(self) -> None:
201
+ """Warn about spec files that match the project's spec template but no block read.
202
+
203
+ A spec named after a block the profile does not list (or lists under another name)
204
+ would otherwise be skipped silently. Map it with a per-block `layout.spec`, add the
205
+ block, or exempt the file with `paths: {<file>: {checks: {ingest: off}, reason: ...}}`.
206
+ """
207
+ read = {f.rel_path for f in self.input_files}
208
+ patterns: set[str] = set()
209
+ for template in _layout_templates(self.resolved.profile, "spec"):
210
+ if "{block}" in template or "{BLOCK}" in template:
211
+ patterns.add(template.replace("{block}", "*").replace("{BLOCK}", "*"))
212
+ seen: set[str] = set()
213
+ for pattern in sorted(patterns):
214
+ for path in sorted(self.root.glob(pattern)):
215
+ rel = _rel(path, self.root)
216
+ if rel in read or rel in seen or not path.is_file():
217
+ continue
218
+ seen.add(rel)
219
+ if self.resolved.for_path(rel).checks.get("ingest") == "off":
220
+ continue
221
+ self.issues.append(
222
+ IngestIssue(
223
+ severity="warning",
224
+ code="unread_spec",
225
+ message=(
226
+ f"{rel} matches the spec template {pattern!r} but no block reads "
227
+ "it; map it in blocks.<name>.layout.spec, or exempt it in paths:"
228
+ ),
229
+ file=rel,
230
+ )
231
+ )
232
+
233
+ def _build_block_rtl(self, block: str, block_profile: Profile) -> None:
234
+ templates = _layout_templates(block_profile, "filelist")
235
+ if not templates:
236
+ return # no filelist template, or an explicit `filelist: []` for this block
237
+ filelist_path = (self.root / _fill(templates[0], block)).resolve()
238
+ rel = _rel(filelist_path, self.root)
239
+ if not filelist_path.is_file():
240
+ self.issues.append(
241
+ IngestIssue(
242
+ severity="warning",
243
+ code="missing_filelist",
244
+ message=(
245
+ f"block {block!r}: no filelist at {rel}; its RTL is not in the model "
246
+ "(set blocks.<name>.layout.filelist, or [] if it has none)"
247
+ ),
248
+ block=_block_key(block),
249
+ file=rel,
250
+ )
251
+ )
252
+ return
253
+ try:
254
+ filelist = _read_filelist_resolving(filelist_path, self.root)
255
+ except FilelistError as exc:
256
+ self.issues.append(
257
+ IngestIssue(
258
+ severity="error",
259
+ code="filelist_error",
260
+ message=f"cannot read filelist {rel}: {exc}",
261
+ block=_block_key(block),
262
+ file=rel,
263
+ )
264
+ )
265
+ return
266
+ if not filelist.sources:
267
+ self.issues.append(
268
+ IngestIssue(
269
+ severity="info",
270
+ code="empty_filelist",
271
+ message=f"filelist for block {block!r} lists no sources",
272
+ block=_block_key(block),
273
+ file=rel,
274
+ )
275
+ )
276
+ return
277
+
278
+ parse = _parse_options(block_profile)
279
+ try:
280
+ model, diags = PyslangExtractor.extract_model(
281
+ filelist,
282
+ block=None,
283
+ root=self.root,
284
+ tops=parse.tops,
285
+ defines=parse.defines,
286
+ clock_patterns=parse.clock_patterns,
287
+ reset_patterns=parse.reset_patterns,
288
+ )
289
+ except Exception as exc:
290
+ self.issues.append(
291
+ IngestIssue(
292
+ severity="error",
293
+ code="rtl_error",
294
+ message=f"RTL extraction failed for block {block!r}: {exc}",
295
+ block=_block_key(block),
296
+ file=rel,
297
+ )
298
+ )
299
+ return
300
+
301
+ self._record_file(filelist_path)
302
+ for source in filelist.sources:
303
+ self._record_file(source)
304
+ diagnostics = tuple(_parse_diag_to_issue(d, block, self.root) for d in diags)
305
+ self.parts.append(
306
+ SourcePart(source="rtl", block=_block_key(block), model=model, diagnostics=diagnostics)
307
+ )
308
+
309
+ def _build_block_mas(self, block: str, rel_path: str) -> None:
310
+ block_profile = self.resolved.for_block(block)
311
+ mas_path = (self.root / rel_path).resolve()
312
+ rel = _rel(mas_path, self.root)
313
+ if not mas_path.is_file():
314
+ self.issues.append(
315
+ IngestIssue(
316
+ severity="warning",
317
+ code="missing_spec",
318
+ message=(
319
+ f"block {block!r}: no spec at {rel}; its requirements are not in the "
320
+ "model (set blocks.<name>.layout.spec, or [] if it has none)"
321
+ ),
322
+ block=_block_key(block),
323
+ file=rel,
324
+ )
325
+ )
326
+ return
327
+ requirements = block_profile.spec.requirements
328
+ try:
329
+ model, diags = MasExtractor.extract_model(
330
+ mas_path,
331
+ block=block,
332
+ requirements=requirements,
333
+ root=self.root,
334
+ access_modes=block_profile.spec.register_access,
335
+ )
336
+ except Exception as exc:
337
+ self.issues.append(
338
+ IngestIssue(
339
+ severity="error",
340
+ code="mas_error",
341
+ message=f"MAS extraction failed for block {block!r}: {exc}",
342
+ block=_block_key(block),
343
+ file=rel,
344
+ )
345
+ )
346
+ return
347
+
348
+ self._record_file(mas_path)
349
+ diagnostics = tuple(_mas_diag_to_issue(d, block) for d in diags)
350
+ self.parts.append(
351
+ SourcePart(source="mas", block=_block_key(block), model=model, diagnostics=diagnostics)
352
+ )
353
+
354
+ # -- helpers --------------------------------------------------------------------
355
+
356
+ def _record_file(self, path: Path) -> None:
357
+ rel = _rel(path, self.root)
358
+ try:
359
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
360
+ except OSError:
361
+ return
362
+ self.input_files.append(InputFile(rel_path=rel, sha256=digest))
363
+
364
+
365
+ def _read_filelist_resolving(filelist_path: Path, root: Path) -> Filelist:
366
+ """Read a filelist, choosing the path style that actually resolves to real files.
367
+
368
+ QSoC-style filelists resolve source paths relative to the filelist's own directory;
369
+ tinysoc-style filelists resolve them relative to the project root (how its Makefile
370
+ runs Verilator). Try filelist-relative first, and fall back to root-relative when that
371
+ leaves listed sources unresolved, so both conventions ingest without a profile flag.
372
+ """
373
+ filelist = read_filelist(filelist_path, relative_to="filelist", root=root)
374
+ if filelist.sources and all(src.is_file() for src in filelist.sources):
375
+ return filelist
376
+ alt = read_filelist(filelist_path, relative_to="cwd", root=root)
377
+ if alt.sources and sum(src.is_file() for src in alt.sources) > sum(
378
+ src.is_file() for src in filelist.sources
379
+ ):
380
+ return alt
381
+ return filelist
382
+
383
+
384
+ @dataclass(frozen=True, slots=True)
385
+ class _ParseOptions:
386
+ """Typed pyslang options, with the extractor's own defaults when unset."""
387
+
388
+ clock_patterns: tuple[str, ...] = (r"^i_clk", r"^clk")
389
+ reset_patterns: tuple[str, ...] = (r"^i_rst", r"^rst")
390
+ tops: tuple[str, ...] | None = None
391
+ defines: dict[str, str | None] | None = None
392
+
393
+
394
+ def _parse_options(profile: Profile) -> _ParseOptions:
395
+ """The pyslang extractor options from `adapters.parse` (all optional)."""
396
+ cfg = profile.adapters.get("parse")
397
+ if cfg is None:
398
+ return _ParseOptions()
399
+ extra = cfg.model_dump()
400
+ updates: dict[str, object] = {}
401
+ clock = extra.get("clock_patterns")
402
+ reset = extra.get("reset_patterns")
403
+ tops = extra.get("tops")
404
+ defines = extra.get("defines")
405
+ if isinstance(clock, list | tuple):
406
+ updates["clock_patterns"] = tuple(str(p) for p in clock)
407
+ if isinstance(reset, list | tuple):
408
+ updates["reset_patterns"] = tuple(str(p) for p in reset)
409
+ if isinstance(tops, list | tuple):
410
+ updates["tops"] = tuple(str(t) for t in tops)
411
+ if isinstance(defines, dict):
412
+ updates["defines"] = {str(k): (None if v is None else str(v)) for k, v in defines.items()}
413
+ return _ParseOptions(**updates) # type: ignore[arg-type]
414
+
415
+
416
+ def _layout_templates(profile: Profile, key: str) -> tuple[str, ...]:
417
+ """The layout template(s) for `key`: one string, a list of several, or `()` for none.
418
+
419
+ An explicit empty list (`spec: []`) says the block has no such file, so nothing is
420
+ reported missing for it.
421
+ """
422
+ value = profile.layout.get(key)
423
+ if isinstance(value, str):
424
+ return (value,)
425
+ if isinstance(value, tuple):
426
+ return tuple(v for v in value if isinstance(v, str))
427
+ return ()
428
+
429
+
430
+ def _fill(template: str, block: str) -> str:
431
+ return template.replace("{block}", block).replace("{BLOCK}", block.upper())
432
+
433
+
434
+ def _block_key(block: str) -> str:
435
+ return f"block:{block}"
436
+
437
+
438
+ def _rel(path: Path, root: Path) -> str:
439
+ try:
440
+ return path.resolve().relative_to(root.resolve()).as_posix()
441
+ except ValueError:
442
+ return path.as_posix()
443
+
444
+
445
+ def _parse_diag_to_issue(diag: ParseDiagnostic, block: str, root: Path) -> IngestIssue:
446
+ # slang's warnings are lint findings (the `lint` check reports those); for ingest only
447
+ # a parse or elaboration error matters, so warnings are kept as info.
448
+ severity = "error" if diag.severity == "error" else "info"
449
+ file = _rel(Path(diag.file), root) if diag.file else None
450
+ return IngestIssue(
451
+ severity=severity, # type: ignore[arg-type]
452
+ code="rtl_diagnostic",
453
+ message=diag.message,
454
+ block=_block_key(block),
455
+ file=file,
456
+ line=diag.line,
457
+ )
458
+
459
+
460
+ def _mas_diag_to_issue(diag: MasDiagnostic, block: str) -> IngestIssue:
461
+ return IngestIssue(
462
+ severity=diag.severity,
463
+ code=f"mas.{diag.code}",
464
+ message=diag.message,
465
+ block=_block_key(block),
466
+ file=diag.file,
467
+ line=diag.line,
468
+ )
469
+
470
+
471
+ def _with_extra_issues(result: IngestResult, extra: list[IngestIssue]) -> IngestResult:
472
+ if not extra:
473
+ return result
474
+ from chipgraph.core.model.ingest import IngestStats
475
+
476
+ issues = _sort_issues(tuple(result.issues) + tuple(extra))
477
+ by_severity: dict[str, int] = {}
478
+ for issue in issues:
479
+ by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1
480
+ stats = result.stats.model_copy(
481
+ update={"diagnostics_by_severity": dict(sorted(by_severity.items()))}
482
+ )
483
+ assert isinstance(stats, IngestStats)
484
+ return result.model_copy(update={"issues": issues, "stats": stats})
485
+
486
+
487
+ def _sort_issues(issues: tuple[IngestIssue, ...]) -> tuple[IngestIssue, ...]:
488
+ order = {"error": 0, "warning": 1, "info": 2}
489
+ return tuple(
490
+ sorted(
491
+ issues,
492
+ key=lambda i: (
493
+ order.get(i.severity, 3),
494
+ i.code,
495
+ i.key or "",
496
+ i.block or "",
497
+ i.file or "",
498
+ i.line or 0,
499
+ i.message,
500
+ ),
501
+ )
502
+ )
503
+
504
+
505
+ def _profile_digest(profile: Profile) -> str:
506
+ """A stable digest of the ingest-relevant profile sections.
507
+
508
+ Changing any of these (spec source, requirement config, layout templates, blocks or
509
+ the parse adapter) changes the build-inputs hash, so a stale cache is detected.
510
+ """
511
+ relevant = {
512
+ "spec": profile.spec.model_dump(mode="json"),
513
+ "layout": {k: v for k, v in sorted(profile.layout.items())},
514
+ "blocks": {k: v.model_dump(mode="json") for k, v in sorted(profile.blocks.items())},
515
+ "parse": profile.adapters["parse"].model_dump(mode="json")
516
+ if "parse" in profile.adapters
517
+ else None,
518
+ }
519
+ canonical = json.dumps(relevant, sort_keys=True, separators=(",", ":"), default=str)
520
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
521
+
522
+
523
+ __all__ = ["IngestReport", "run_ingest"]
@@ -0,0 +1,52 @@
1
+ """Built-in checks: deterministic, no-external-tool verification of project conventions.
2
+
3
+ See DESIGN.md 7.3. Each check is registered under the `chipgraph.adapters.check` entry
4
+ point group (see `pyproject.toml`) and satisfies `chipgraph.core.plugin_api.Check`.
5
+ """
6
+
7
+ from chipgraph.checks.cdc_struct import CdcStructCheck
8
+ from chipgraph.checks.connect import ConnectCheck
9
+ from chipgraph.checks.cross_chip import CrossChipCheck
10
+ from chipgraph.checks.duplicate import DuplicateCheck
11
+ from chipgraph.checks.filelist import FilelistCheck
12
+ from chipgraph.checks.generated import GeneratedCheck, stamp, verify
13
+ from chipgraph.checks.hardcode import HardcodeCheck
14
+ from chipgraph.checks.layout import LayoutCheck
15
+ from chipgraph.checks.naming import NamingCheck
16
+ from chipgraph.checks.ports_diff import PortsDiffCheck
17
+ from chipgraph.checks.spec_schema import SpecSchemaCheck
18
+ from chipgraph.checks.trace import TraceCheck
19
+
20
+ BUILTIN_CHECKS: dict[str, type] = {
21
+ "layout": LayoutCheck,
22
+ "filelist": FilelistCheck,
23
+ "generated": GeneratedCheck,
24
+ "naming": NamingCheck,
25
+ "spec_schema": SpecSchemaCheck,
26
+ "cross_chip": CrossChipCheck,
27
+ "ports_diff": PortsDiffCheck,
28
+ "duplicate": DuplicateCheck,
29
+ "trace": TraceCheck,
30
+ "connect": ConnectCheck,
31
+ "hardcode": HardcodeCheck,
32
+ "cdc_struct": CdcStructCheck,
33
+ }
34
+ """Maps a check's registered name to its class, for entry-point-free wiring in tests."""
35
+
36
+ __all__ = [
37
+ "BUILTIN_CHECKS",
38
+ "CdcStructCheck",
39
+ "ConnectCheck",
40
+ "CrossChipCheck",
41
+ "DuplicateCheck",
42
+ "FilelistCheck",
43
+ "GeneratedCheck",
44
+ "HardcodeCheck",
45
+ "LayoutCheck",
46
+ "NamingCheck",
47
+ "PortsDiffCheck",
48
+ "SpecSchemaCheck",
49
+ "TraceCheck",
50
+ "stamp",
51
+ "verify",
52
+ ]