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,583 @@
1
+ """Query API on the Design Model.
2
+
3
+ All query results are frozen pydantic models suitable for JSON serialization.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import difflib
9
+ from collections import defaultdict
10
+ from collections.abc import Iterable
11
+ from fnmatch import fnmatch
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from chipgraph.core.model.entities import EntityBase
17
+ from chipgraph.core.model.model import DesignModel
18
+ from chipgraph.core.model.store import ModelStore
19
+
20
+
21
+ class QueryError(ValueError):
22
+ """Raised when a query cannot be satisfied (unknown key, name, etc.).
23
+
24
+ Message includes close matches (via difflib) to help the user find what they meant.
25
+ """
26
+
27
+
28
+ def _close_matches(name: str, choices: Iterable[str]) -> list[str]:
29
+ """Return the N closest matches from `choices` to `name`, in score descending order."""
30
+ return difflib.get_close_matches(name, list(choices), n=5, cutoff=0.6)
31
+
32
+
33
+ class BlockInfo(BaseModel):
34
+ """Result of `model.block(name)`: the block and what it contains."""
35
+
36
+ model_config = {"frozen": True}
37
+
38
+ key: str = Field(description="Model key of the block")
39
+ name: str = Field(description="Block name")
40
+ owner: str | None = Field(default=None, description="Block owner")
41
+ path: str | None = Field(default=None, description="Repo-relative path")
42
+ modules: list[str] = Field(default_factory=list, description="Model keys of contained modules")
43
+ ports: list[str] = Field(
44
+ default_factory=list, description="Model keys of ports from contained modules"
45
+ )
46
+ registers: list[str] = Field(
47
+ default_factory=list, description="Model keys of contained registers"
48
+ )
49
+ interrupts: list[str] = Field(
50
+ default_factory=list, description="Model keys of contained interrupts"
51
+ )
52
+ memory_regions: list[str] = Field(
53
+ default_factory=list, description="Model keys of memory regions"
54
+ )
55
+ clocks: list[str] = Field(
56
+ default_factory=list, description="Model keys of clocks (from contained modules)"
57
+ )
58
+ resets: list[str] = Field(
59
+ default_factory=list, description="Model keys of resets (from contained modules)"
60
+ )
61
+ requirements: list[str] = Field(
62
+ default_factory=list, description="Model keys of requirements for this block"
63
+ )
64
+ open_items: list[str] = Field(
65
+ default_factory=list, description="Model keys of open items for this block"
66
+ )
67
+
68
+
69
+ class ModuleInfo(BaseModel):
70
+ """Result of `model.module(name)`: a single RTL module and its contents."""
71
+
72
+ model_config = {"frozen": True}
73
+
74
+ key: str = Field(description="Model key of the module")
75
+ name: str = Field(description="Module name")
76
+ file: str | None = Field(default=None, description="Repo-relative file this module is in")
77
+ block: str | None = Field(default=None, description="Model key of the owning block")
78
+ ports: list[str] = Field(default_factory=list, description="Model keys of this module's ports")
79
+ parameters: list[str] = Field(
80
+ default_factory=list, description="Model keys of this module's parameters"
81
+ )
82
+ instances: list[dict[str, str]] = Field(
83
+ default_factory=list,
84
+ description="Child modules: list of {instance_name, module_key} dicts",
85
+ )
86
+ instantiated_by: list[dict[str, str]] = Field(
87
+ default_factory=list,
88
+ description="Parent modules: list of {instance_name, module_key} dicts",
89
+ )
90
+
91
+
92
+ class EntityMatch(BaseModel):
93
+ """A single entity matching a `find` query."""
94
+
95
+ model_config = {"frozen": True}
96
+
97
+ key: str = Field(description="Model key")
98
+ kind: str = Field(description="Entity kind")
99
+ name: str = Field(description="Entity name")
100
+ source: str = Field(description="Source file or description")
101
+
102
+
103
+ class TraceResult(BaseModel):
104
+ """Result of `model.trace(key)`: requirements and links."""
105
+
106
+ model_config = {"frozen": True}
107
+
108
+ key: str = Field(description="The entity key being traced")
109
+ kind: str = Field(description="The entity kind")
110
+ name: str = Field(description="The entity name")
111
+ # For requirement entities:
112
+ implements: list[str] = Field(
113
+ default_factory=list, description="Entities implementing this requirement"
114
+ )
115
+ verifies: list[str] = Field(
116
+ default_factory=list, description="Tests verifying this requirement"
117
+ )
118
+ derives_from: list[str] = Field(
119
+ default_factory=list, description="Requirements this requirement derives from"
120
+ )
121
+ no_test: bool = Field(
122
+ default=False, description="True if this requirement has no verifying test"
123
+ )
124
+ no_implementation: bool = Field(
125
+ default=False, description="True if this requirement has no implementation"
126
+ )
127
+ # For any entity:
128
+ traced_by: list[str] = Field(
129
+ default_factory=list,
130
+ description="Model keys of requirements that trace to this entity",
131
+ )
132
+
133
+
134
+ class ImpactNode(BaseModel):
135
+ """A single entity in an impact graph result."""
136
+
137
+ model_config = {"frozen": True}
138
+
139
+ key: str = Field(description="Model key")
140
+ kind: str = Field(description="Entity kind")
141
+ name: str = Field(description="Entity name")
142
+ path: list[str] = Field(
143
+ description="Path from the root to this node (as relation kinds and entity keys)"
144
+ )
145
+
146
+
147
+ class ImpactResult(BaseModel):
148
+ """Result of `model.impact(key)`: downstream entities grouped by kind."""
149
+
150
+ model_config = {"frozen": True}
151
+
152
+ key: str = Field(description="The change root key")
153
+ max_depth: int = Field(description="Depth limit for the traversal")
154
+ by_kind: dict[str, list[ImpactNode]] = Field(
155
+ default_factory=dict, description="Entities grouped by their kind"
156
+ )
157
+
158
+
159
+ class NeighborResult(BaseModel):
160
+ """Result of `model.neighbors(key)`: adjacent entities."""
161
+
162
+ model_config = {"frozen": True}
163
+
164
+ key: str = Field(description="The entity key")
165
+ incoming: list[dict[str, Any]] = Field(
166
+ default_factory=list,
167
+ description="Relations where this entity is the destination: {src_key, relation_kind}",
168
+ )
169
+ outgoing: list[dict[str, Any]] = Field(
170
+ default_factory=list,
171
+ description="Relations where this entity is the source: {dst_key, relation_kind}",
172
+ )
173
+
174
+
175
+ class SearchResult(BaseModel):
176
+ """A single result from full-text search."""
177
+
178
+ model_config = {"frozen": True}
179
+
180
+ text: str = Field(description="Matched text snippet")
181
+ score: float = Field(description="Relevance score (higher is better)")
182
+ citation: str = Field(description="Model key or file:line")
183
+
184
+
185
+ class ModelQuery:
186
+ """The query API on a `DesignModel`, with optional backing `ModelStore` for search."""
187
+
188
+ def __init__(self, model: DesignModel, store: ModelStore | None = None) -> None:
189
+ """Initialize with a model (required) and optional store (for full-text search)."""
190
+ self.model = model
191
+ self.store = store
192
+
193
+ def block(self, name: str) -> BlockInfo:
194
+ """Return the block and all contained entities."""
195
+ # Find the block by name (exact match via find)
196
+ all_blocks = self.model.by_kind("block")
197
+ block = None
198
+ for b in all_blocks:
199
+ if b.name == name:
200
+ block = b
201
+ break
202
+
203
+ if block is None:
204
+ matches = _close_matches(name, [e.name for e in all_blocks])
205
+ msg = f"block {name!r} not found"
206
+ if matches:
207
+ msg += f"; did you mean: {', '.join(matches)}"
208
+ raise QueryError(msg)
209
+
210
+ # Collect contained entities via relations
211
+ modules: list[EntityBase] = []
212
+ for rel in self.model.relations:
213
+ if rel.src == block.key and rel.kind == "contains":
214
+ m = self.model.get(rel.dst)
215
+ if m and m.kind == "module":
216
+ modules.append(m)
217
+
218
+ module_keys = [m.key for m in modules]
219
+
220
+ # Collect ports and clocks/resets from modules in this block
221
+ ports: list[EntityBase] = []
222
+ clocks: set[str] = set()
223
+ resets: set[str] = set()
224
+ for mod in modules:
225
+ for entity in self.model.entities.values():
226
+ if entity.kind == "port" and getattr(entity, "module", None) == mod.key:
227
+ ports.append(entity)
228
+ # Collect port-level clock and reset
229
+ clock = getattr(entity, "clock", None)
230
+ if isinstance(clock, str):
231
+ clocks.add(clock)
232
+ reset = getattr(entity, "reset", None)
233
+ if isinstance(reset, str):
234
+ resets.add(reset)
235
+
236
+ # Registers, interrupts, memory regions
237
+ registers: list[EntityBase] = []
238
+ interrupts: list[EntityBase] = []
239
+ memory_regions: list[EntityBase] = []
240
+ for entity in self.model.entities.values():
241
+ if entity.kind == "register" and getattr(entity, "block", None) == block.key:
242
+ registers.append(entity)
243
+ elif entity.kind == "interrupt" and getattr(entity, "block", None) == block.key:
244
+ interrupts.append(entity)
245
+ elif entity.kind == "memory_region" and getattr(entity, "block", None) == block.key:
246
+ memory_regions.append(entity)
247
+
248
+ # Find clocks and resets by key
249
+ block_clocks = sorted(list(clocks))
250
+ block_resets = sorted(list(resets))
251
+
252
+ # Requirements: find all requirements that implement/verify entities within this block
253
+ block_entity_keys = {block.key}
254
+ for mod in modules:
255
+ block_entity_keys.add(mod.key)
256
+ for port in ports:
257
+ block_entity_keys.add(port.key)
258
+ for reg in registers:
259
+ block_entity_keys.add(reg.key)
260
+ for inter in interrupts:
261
+ block_entity_keys.add(inter.key)
262
+
263
+ requirements_set: set[str] = set()
264
+ for rel in self.model.relations:
265
+ if rel.dst in block_entity_keys and rel.kind in (
266
+ "implements",
267
+ "verifies",
268
+ "derives_from",
269
+ ):
270
+ # Get the source of the implements relation
271
+ src_entity = self.model.get(rel.src)
272
+ if src_entity and src_entity.kind == "requirement":
273
+ requirements_set.add(rel.src)
274
+ requirements = sorted(list(requirements_set))
275
+
276
+ # Open items: entities with kind "open_item" that relate to this block
277
+ open_items_set: set[str] = set()
278
+ for rel in self.model.relations:
279
+ src_entity = self.model.get(rel.src)
280
+ if src_entity and src_entity.kind == "open_item" and rel.dst in block_entity_keys:
281
+ open_items_set.add(rel.src)
282
+ open_items = sorted(list(open_items_set))
283
+
284
+ return BlockInfo(
285
+ key=block.key,
286
+ name=block.name,
287
+ owner=getattr(block, "owner", None),
288
+ path=getattr(block, "path", None),
289
+ modules=module_keys,
290
+ ports=[p.key for p in ports],
291
+ registers=[r.key for r in registers],
292
+ interrupts=[i.key for i in interrupts],
293
+ memory_regions=[m.key for m in memory_regions],
294
+ clocks=block_clocks,
295
+ resets=block_resets,
296
+ requirements=requirements,
297
+ open_items=open_items,
298
+ )
299
+
300
+ def module(self, name: str) -> ModuleInfo:
301
+ """Return the module, its ports, parameters, instances, and who instantiates it."""
302
+ # Find module by name
303
+ modules = self.model.find(kind="module", name=name)
304
+ if not modules:
305
+ matches = _close_matches(
306
+ name, [e.name for e in self.model.by_kind("module") if isinstance(e.name, str)]
307
+ )
308
+ msg = f"module {name!r} not found"
309
+ if matches:
310
+ msg += f"; did you mean: {', '.join(matches)}"
311
+ raise QueryError(msg)
312
+
313
+ mod = modules[0]
314
+
315
+ # Ports and parameters
316
+ ports = self.model.find(kind="port", module=mod.key)
317
+ parameters = self.model.find(kind="parameter", module=mod.key)
318
+
319
+ # Instances: children (this module instantiates)
320
+ instances: list[dict[str, str]] = []
321
+ for rel in self.model.relations:
322
+ if rel.src == mod.key and rel.kind == "instantiates":
323
+ child = self.model.get(rel.dst)
324
+ if child:
325
+ instance_name = rel.attrs.get("instance_name", "")
326
+ instances.append({"instance_name": str(instance_name), "module_key": rel.dst})
327
+
328
+ # Instantiated by: parents (which modules instantiate this)
329
+ instantiated_by: list[dict[str, str]] = []
330
+ for rel in self.model.relations:
331
+ if rel.dst == mod.key and rel.kind == "instantiates":
332
+ parent = self.model.get(rel.src)
333
+ if parent:
334
+ instance_name = rel.attrs.get("instance_name", "")
335
+ instantiated_by.append(
336
+ {"instance_name": str(instance_name), "module_key": rel.src}
337
+ )
338
+
339
+ return ModuleInfo(
340
+ key=mod.key,
341
+ name=mod.name,
342
+ file=getattr(mod, "file", None),
343
+ block=getattr(mod, "block", None),
344
+ ports=[p.key for p in ports],
345
+ parameters=[p.key for p in parameters],
346
+ instances=instances,
347
+ instantiated_by=instantiated_by,
348
+ )
349
+
350
+ def find(
351
+ self,
352
+ kind: str | None = None,
353
+ name: str | None = None,
354
+ limit: int = 100,
355
+ **attrs: object,
356
+ ) -> list[EntityMatch]:
357
+ """Find entities by kind, name (glob via fnmatch), and/or attributes.
358
+
359
+ Returns up to `limit` results. Raises QueryError if the query is invalid.
360
+ """
361
+ results: list[EntityBase] = []
362
+
363
+ for entity in self.model.entities.values():
364
+ if kind is not None and entity.kind != kind:
365
+ continue
366
+
367
+ if name is not None and not fnmatch(entity.name, name):
368
+ continue
369
+
370
+ # Check typed attributes
371
+ if attrs and not _matches(entity, attrs):
372
+ continue
373
+
374
+ results.append(entity)
375
+
376
+ results = results[:limit]
377
+ return [
378
+ EntityMatch(
379
+ key=e.key,
380
+ kind=e.kind,
381
+ name=e.name,
382
+ source=e.source.file or e.source.extractor or "unknown",
383
+ )
384
+ for e in results
385
+ ]
386
+
387
+ def trace(self, key: str) -> TraceResult:
388
+ """Trace a requirement or any entity: show requirements and links.
389
+
390
+ For requirement entities: shows implements, verifies, derives_from, and flags.
391
+ For other entities: shows requirements that point to them.
392
+ """
393
+ entity = self.model.get(key)
394
+ if entity is None:
395
+ matches = _close_matches(key, list(self.model.entities.keys()))
396
+ msg = f"entity {key!r} not found"
397
+ if matches:
398
+ msg += f"; did you mean: {', '.join(matches)}"
399
+ raise QueryError(msg)
400
+
401
+ implements: list[str] = []
402
+ verifies: list[str] = []
403
+ derives_from: list[str] = []
404
+ no_test = False
405
+ no_implementation = False
406
+ traced_by: list[str] = []
407
+
408
+ # If this is a requirement, trace outward
409
+ if entity.kind == "requirement":
410
+ # implements: relations where this is src and kind is "implements"
411
+ for rel in self.model.relations:
412
+ if rel.src == key and rel.kind == "implements":
413
+ implements.append(rel.dst)
414
+ # verifies: test verifies requirement (relation dst is requirement)
415
+ elif rel.dst == key and rel.kind == "verifies":
416
+ verifies.append(rel.src)
417
+ # derives_from: relations where this is src and kind is "derives_from"
418
+ elif rel.src == key and rel.kind == "derives_from":
419
+ derives_from.append(rel.dst)
420
+
421
+ # Check for no_test and no_implementation
422
+ no_test = len(verifies) == 0
423
+ no_implementation = len(implements) == 0
424
+
425
+ # Any entity: find requirements that trace to it (verifies / implements reverse)
426
+ for rel in self.model.relations:
427
+ if rel.dst == key and rel.kind in ("verifies", "implements"):
428
+ traced_by.append(rel.src)
429
+
430
+ return TraceResult(
431
+ key=key,
432
+ kind=entity.kind,
433
+ name=entity.name,
434
+ implements=implements,
435
+ verifies=verifies,
436
+ derives_from=derives_from,
437
+ no_test=no_test,
438
+ no_implementation=no_implementation,
439
+ traced_by=traced_by,
440
+ )
441
+
442
+ def impact(self, key: str, max_depth: int = 5) -> ImpactResult:
443
+ """Compute downstream impact from a change at `key`.
444
+
445
+ Traverses `contains`, `instantiates`, `connects`, `derives_from`, `implements`,
446
+ `verifies` relations downstream from the root, up to `max_depth` levels.
447
+ Returns entities grouped by kind, with the path to each.
448
+ """
449
+ entity = self.model.get(key)
450
+ if entity is None:
451
+ matches = _close_matches(key, list(self.model.entities.keys()))
452
+ msg = f"entity {key!r} not found"
453
+ if matches:
454
+ msg += f"; did you mean: {', '.join(matches)}"
455
+ raise QueryError(msg)
456
+
457
+ # Downstream relations: those that go "forward" in the design
458
+ downstream_kinds = {
459
+ "contains",
460
+ "instantiates",
461
+ "connects",
462
+ "derives_from",
463
+ "implements",
464
+ "verifies",
465
+ }
466
+
467
+ # BFS with depth tracking
468
+ visited: set[str] = {key}
469
+ by_kind: dict[str, list[ImpactNode]] = defaultdict(list)
470
+
471
+ queue: list[tuple[str, list[str], int]] = [
472
+ (key, [], 0)
473
+ ] # (entity_key, path_to_here, depth)
474
+
475
+ while queue:
476
+ current, path, depth = queue.pop(0)
477
+
478
+ # Find outgoing relations
479
+ for rel in self.model.relations:
480
+ if rel.src == current and rel.kind in downstream_kinds:
481
+ dst = rel.dst
482
+ if dst not in visited and depth < max_depth:
483
+ visited.add(dst)
484
+ # Path includes the relation kind and destination key
485
+ new_path = [*path, rel.kind, dst]
486
+ dst_entity = self.model.get(dst)
487
+ if dst_entity:
488
+ by_kind[dst_entity.kind].append(
489
+ ImpactNode(
490
+ key=dst,
491
+ kind=dst_entity.kind,
492
+ name=dst_entity.name,
493
+ path=new_path,
494
+ )
495
+ )
496
+ # Continue BFS for next level
497
+ queue.append((dst, new_path, depth + 1))
498
+
499
+ # Sort each kind's list by key for determinism
500
+ for kind in by_kind:
501
+ by_kind[kind].sort(key=lambda n: n.key)
502
+
503
+ return ImpactResult(key=key, max_depth=max_depth, by_kind=dict(by_kind))
504
+
505
+ def neighbors(
506
+ self,
507
+ key: str,
508
+ kinds: Iterable[str] | None = None,
509
+ relation: str | None = None,
510
+ direction: str = "out",
511
+ ) -> NeighborResult:
512
+ """Find adjacent entities (one hop) in the specified direction.
513
+
514
+ Args:
515
+ key: The entity key.
516
+ kinds: If given, filter destinations by these entity kinds.
517
+ relation: If given, filter relations by this kind.
518
+ direction: "out" (forward), "in" (backward), or "both" (both).
519
+
520
+ Returns: Lists of incoming and outgoing edges from this entity.
521
+ """
522
+ entity = self.model.get(key)
523
+ if entity is None:
524
+ matches = _close_matches(key, list(self.model.entities.keys()))
525
+ msg = f"entity {key!r} not found"
526
+ if matches:
527
+ msg += f"; did you mean: {', '.join(matches)}"
528
+ raise QueryError(msg)
529
+
530
+ kinds_set = set(kinds) if kinds is not None else None
531
+ incoming: list[dict[str, Any]] = []
532
+ outgoing: list[dict[str, Any]] = []
533
+
534
+ for rel in self.model.relations:
535
+ if relation is not None and rel.kind != relation:
536
+ continue
537
+
538
+ if direction in ("in", "both") and rel.dst == key:
539
+ src_entity = self.model.get(rel.src)
540
+ if src_entity is not None and (kinds_set is None or src_entity.kind in kinds_set):
541
+ incoming.append({"src_key": rel.src, "relation_kind": rel.kind})
542
+
543
+ if direction in ("out", "both") and rel.src == key:
544
+ dst_entity = self.model.get(rel.dst)
545
+ if dst_entity is not None and (kinds_set is None or dst_entity.kind in kinds_set):
546
+ outgoing.append({"dst_key": rel.dst, "relation_kind": rel.kind})
547
+
548
+ return NeighborResult(key=key, incoming=incoming, outgoing=outgoing)
549
+
550
+ def search(
551
+ self,
552
+ text: str,
553
+ kinds: Iterable[str] | None = None,
554
+ limit: int = 20,
555
+ ) -> list[SearchResult]:
556
+ """Full-text search on the backing store.
557
+
558
+ Raises QueryError if no store is available.
559
+ """
560
+ if self.store is None:
561
+ raise QueryError("full-text search requires a ModelStore")
562
+
563
+ kinds_list = list(kinds) if kinds is not None else None
564
+ hits = self.store.search(text, kinds=kinds_list, limit=limit)
565
+
566
+ return [SearchResult(text=hit.text, score=hit.score, citation=hit.citation) for hit in hits]
567
+
568
+
569
+ def _matches(entity: EntityBase, attrs: dict[str, object]) -> bool:
570
+ """Check if entity's typed fields and attrs match the given dict."""
571
+ for key, value in attrs.items():
572
+ # Try typed field first
573
+ if hasattr(entity, key):
574
+ if getattr(entity, key) != value:
575
+ return False
576
+ # Try attrs dict
577
+ elif key in entity.attrs:
578
+ if entity.attrs[key] != value:
579
+ return False
580
+ else:
581
+ # Neither typed field nor attrs: no match
582
+ return False
583
+ return True
@@ -0,0 +1,80 @@
1
+ """`EntityKinds`: the registry mapping an entity `kind` string to its pydantic class.
2
+
3
+ Core kinds (DESIGN.md 4.2) are registered by default. A pack that needs a new kind
4
+ (e.g. an analog pack adding `pin_spec`) registers its own frozen pydantic class that
5
+ subclasses `EntityBase`, without chipgraph.core knowing anything about it. A kind with
6
+ no registered class (because the reader does not have that pack installed, or has not
7
+ called `register` for it) still round-trips losslessly as `ExtEntity`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Mapping
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel
16
+
17
+ from chipgraph.core.model.entities import CORE_ENTITY_CLASSES, EntityBase, ExtEntity
18
+
19
+
20
+ class DuplicateKindError(ValueError):
21
+ """Raised when registering a kind that is already registered to a different class."""
22
+
23
+
24
+ class EntityKinds:
25
+ """A registry of entity `kind` -> pydantic class, seeded with the core kinds."""
26
+
27
+ def __init__(self) -> None:
28
+ self._classes: dict[str, type[EntityBase]] = {}
29
+ for cls in CORE_ENTITY_CLASSES:
30
+ self._classes[_kind_of(cls)] = cls
31
+
32
+ def register(self, cls: type[EntityBase]) -> None:
33
+ """Register a pack-defined entity class, keyed by its `kind` literal default."""
34
+ kind = _kind_of(cls)
35
+ existing = self._classes.get(kind)
36
+ if existing is not None and existing is not cls:
37
+ raise DuplicateKindError(
38
+ f"kind {kind!r} is already registered to {existing.__qualname__}"
39
+ )
40
+ self._classes[kind] = cls
41
+
42
+ def get(self, kind: str) -> type[EntityBase] | None:
43
+ """Return the registered class for `kind`, or None if unregistered."""
44
+ return self._classes.get(kind)
45
+
46
+ def kinds(self) -> tuple[str, ...]:
47
+ """Return every registered kind, sorted."""
48
+ return tuple(sorted(self._classes))
49
+
50
+ def parse(self, data: Mapping[str, Any]) -> EntityBase:
51
+ """Build an entity from a plain dict, using the registered class for its `kind`.
52
+
53
+ Falls back to `ExtEntity` when `kind` has no registered class, so entities from
54
+ packs the reader does not know about still round-trip.
55
+ """
56
+ kind = data.get("kind")
57
+ if not isinstance(kind, str):
58
+ raise ValueError(f"entity data is missing a string 'kind': {data!r}")
59
+ cls = self.get(kind)
60
+ if cls is not None:
61
+ return cls.model_validate(data)
62
+ return ExtEntity.model_validate(data)
63
+
64
+
65
+ def _kind_of(cls: type[EntityBase]) -> str:
66
+ field = cls.model_fields.get("kind")
67
+ if field is None or not isinstance(field.default, str):
68
+ raise TypeError(
69
+ f"{cls.__qualname__} must define a 'kind: Literal[...]' field with a default"
70
+ )
71
+ return field.default
72
+
73
+
74
+ DEFAULT_ENTITY_KINDS = EntityKinds()
75
+ """The process-wide default registry: core kinds only, until packs register more."""
76
+
77
+
78
+ def is_pydantic_entity_class(obj: Any) -> bool:
79
+ """Return True if `obj` looks like a valid entity class to register."""
80
+ return isinstance(obj, type) and issubclass(obj, BaseModel) and issubclass(obj, EntityBase)