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,403 @@
1
+ """Build the four Design Model figures as deterministic :class:`Scene` objects.
2
+
3
+ Each builder reads only from the typed Design Model, sorts everything by model key (or by
4
+ address, for the memory map), and lays elements out on a fixed integer grid so the SVG and
5
+ drawio renderers produce byte-identical output for a given model. Nothing here draws a
6
+ figure by hand or calls an AI: the picture is a function of the model (DESIGN.md 4.6).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from chipgraph.core.model import DesignModel, EntityBase
12
+ from chipgraph.packs.spec_core.gen.diagram._scene import Box, Edge, Scene, escape_id
13
+
14
+ _MARGIN = 20
15
+ _COL_W = 180
16
+ _ROW_H = 44
17
+ _ROW_GAP = 12
18
+ _COL_GAP = 60
19
+ _EMPTY_W = 360
20
+
21
+
22
+ def _empty_scene(title: str, kind_desc: str) -> Scene:
23
+ """A valid figure that says there is no data, for an empty or partial model."""
24
+ box = Box(id="no_data", x=_MARGIN, y=_MARGIN, w=_EMPTY_W, h=_ROW_H, text="no data")
25
+ return Scene(
26
+ title=title,
27
+ desc=f"{kind_desc}: no data in the model.",
28
+ width=_EMPTY_W + 2 * _MARGIN,
29
+ height=_ROW_H + 2 * _MARGIN,
30
+ boxes=(box,),
31
+ )
32
+
33
+
34
+ def _name_of(entity: EntityBase) -> str:
35
+ return entity.name
36
+
37
+
38
+ def _attr_str(entity: EntityBase, key: str) -> str | None:
39
+ value = entity.attrs.get(key)
40
+ return value if isinstance(value, str) else None
41
+
42
+
43
+ def _attr_names(entity: EntityBase, key: str) -> tuple[str, ...]:
44
+ value = entity.attrs.get(key)
45
+ if not isinstance(value, list):
46
+ return ()
47
+ return tuple(str(item) for item in value)
48
+
49
+
50
+ def _block_name(model: DesignModel, key: object) -> str:
51
+ """The name of the block at `key`, or ``-`` when it is missing/unset."""
52
+ if not isinstance(key, str):
53
+ return "-"
54
+ entity = model.get(key)
55
+ return entity.name if entity is not None else "-"
56
+
57
+
58
+ def block_diagram(model: DesignModel) -> Scene:
59
+ """Buses (interfaces) with their masters and slave blocks; lone blocks still shown."""
60
+ title = "Block diagram"
61
+ interfaces = sorted(model.by_kind("interface"), key=lambda e: e.key)
62
+ blocks = {e.key: e for e in model.by_kind("block")}
63
+ if not interfaces and not blocks:
64
+ return _empty_scene(title, "Block diagram")
65
+
66
+ # Which block keys are reached by a bus, so lone blocks can be listed separately.
67
+ connected: set[str] = set()
68
+ boxes: list[Box] = []
69
+ edges: list[Edge] = []
70
+ y = _MARGIN
71
+ bus_x = _MARGIN
72
+ slave_x = _MARGIN + _COL_W + _COL_GAP
73
+ max_x = slave_x + _COL_W
74
+
75
+ for interface in interfaces:
76
+ masters = _attr_names(interface, "masters")
77
+ bus_id = escape_id(interface.key)
78
+ proto_value = getattr(interface, "protocol", None)
79
+ proto = proto_value if isinstance(proto_value, str) else ""
80
+ header = f"bus: {interface.name}"
81
+ if proto:
82
+ header += f"\n{proto}"
83
+ if masters:
84
+ header += "\nmasters: " + ", ".join(sorted(masters))
85
+ bus_y = y
86
+ boxes.append(Box(id=bus_id, x=bus_x, y=bus_y, w=_COL_W, h=_ROW_H, text=header))
87
+ slaves = sorted(
88
+ model.get_relations(src=interface.key, kind="connects"), key=lambda r: r.dst
89
+ )
90
+ row_y = y
91
+ for relation in slaves:
92
+ block = blocks.get(relation.dst)
93
+ if block is None:
94
+ continue
95
+ connected.add(block.key)
96
+ block_id = escape_id(block.key)
97
+ boxes.append(Box(id=block_id, x=slave_x, y=row_y, w=_COL_W, h=_ROW_H, text=block.name))
98
+ edges.append(Edge(id=f"{bus_id}__{block_id}", src=bus_id, dst=block_id))
99
+ row_y += _ROW_H + _ROW_GAP
100
+ y = max(row_y, bus_y + _ROW_H + _ROW_GAP)
101
+
102
+ lone = sorted((b for k, b in blocks.items() if k not in connected), key=lambda e: e.key)
103
+ if lone:
104
+ boxes.append(Box(id="lone_header", x=_MARGIN, y=y, w=_COL_W, h=_ROW_H, text="not on a bus"))
105
+ row_y = y
106
+ for block in lone:
107
+ boxes.append(
108
+ Box(
109
+ id=escape_id(block.key),
110
+ x=slave_x,
111
+ y=row_y,
112
+ w=_COL_W,
113
+ h=_ROW_H,
114
+ text=block.name,
115
+ )
116
+ )
117
+ row_y += _ROW_H + _ROW_GAP
118
+ y = max(row_y, y + _ROW_H + _ROW_GAP)
119
+
120
+ height = max(y, _MARGIN + _ROW_H) + _MARGIN
121
+ return Scene(
122
+ title=title,
123
+ desc=(
124
+ f"Block diagram: {len(interfaces)} bus(es), {len(blocks)} block(s), "
125
+ f"{len(lone)} block(s) not on a bus."
126
+ ),
127
+ width=max_x + _MARGIN,
128
+ height=height,
129
+ boxes=tuple(boxes),
130
+ edges=tuple(edges),
131
+ )
132
+
133
+
134
+ def _region_base(entity: EntityBase) -> int | None:
135
+ base = getattr(entity, "base", None)
136
+ return base if isinstance(base, int) else None
137
+
138
+
139
+ def _region_size(entity: EntityBase) -> int | None:
140
+ size = getattr(entity, "size", None)
141
+ return size if isinstance(size, int) else None
142
+
143
+
144
+ def _hex_or(value: int | None, fallback: object) -> str:
145
+ if value is not None:
146
+ return f"{value:#010x}"
147
+ return "auto" if fallback == "auto" else str(fallback) if fallback is not None else "?"
148
+
149
+
150
+ def _overlapping_keys(regions: list[EntityBase]) -> set[str]:
151
+ """Model keys of regions that overlap another region (both endpoints marked)."""
152
+ placed = [
153
+ (e.key, base, size)
154
+ for e in regions
155
+ if (base := _region_base(e)) is not None and (size := _region_size(e)) is not None
156
+ ]
157
+ placed.sort(key=lambda t: (t[1], t[0]))
158
+ bad: set[str] = set()
159
+ for i, (key_a, base_a, size_a) in enumerate(placed):
160
+ for key_b, base_b, _size_b in placed[i + 1 :]:
161
+ if base_b >= base_a + size_a:
162
+ break
163
+ bad.add(key_a)
164
+ bad.add(key_b)
165
+ return bad
166
+
167
+
168
+ def memory_map(model: DesignModel) -> Scene:
169
+ """Regions sorted by base; base/end/size in hex; overlaps outlined in the marker colour."""
170
+ title = "Memory map"
171
+ regions = model.by_kind("memory_region")
172
+ if not regions:
173
+ return _empty_scene(title, "Memory map")
174
+
175
+ # Sort by base (numeric first, in address order), then by key for stability.
176
+ def sort_key(entity: EntityBase) -> tuple[int, int, str]:
177
+ base = _region_base(entity)
178
+ if base is None:
179
+ return (1, 0, entity.key)
180
+ return (0, base, entity.key)
181
+
182
+ ordered = sorted(regions, key=sort_key)
183
+ bad = _overlapping_keys(list(regions))
184
+
185
+ boxes: list[Box] = []
186
+ width = 520
187
+ y = _MARGIN
188
+ header = Box(
189
+ id="mm_header",
190
+ x=_MARGIN,
191
+ y=y,
192
+ w=width,
193
+ h=_ROW_H,
194
+ text="Memory map (base / end / size / block)",
195
+ )
196
+ boxes.append(header)
197
+ y += _ROW_H + _ROW_GAP
198
+ for entity in ordered:
199
+ base = _region_base(entity)
200
+ size = _region_size(entity)
201
+ end = base + size if base is not None and size is not None else None
202
+ base_s = _hex_or(base, getattr(entity, "base", None))
203
+ end_s = _hex_or(end, None) if end is not None else "?"
204
+ size_s = _hex_or(size, getattr(entity, "size", None))
205
+ block = getattr(entity, "block", None)
206
+ block_name = _block_name(model, block)
207
+ text = f"{entity.name}\n{base_s} .. {end_s} size {size_s} block {block_name}"
208
+ overlap = entity.key in bad
209
+ if overlap:
210
+ text += "\n[OVERLAP]"
211
+ boxes.append(
212
+ Box(
213
+ id=escape_id(entity.key),
214
+ x=_MARGIN,
215
+ y=y,
216
+ w=width,
217
+ h=_ROW_H + (16 if overlap else 0),
218
+ text=text,
219
+ overlap=overlap,
220
+ )
221
+ )
222
+ y += _ROW_H + (16 if overlap else 0) + _ROW_GAP
223
+
224
+ desc = f"Memory map: {len(regions)} region(s)"
225
+ if bad:
226
+ desc += f", {len(bad)} in an overlap (marked)"
227
+ desc += "."
228
+ return Scene(
229
+ title=title,
230
+ desc=desc,
231
+ width=width + 2 * _MARGIN,
232
+ height=y + _MARGIN,
233
+ boxes=tuple(boxes),
234
+ )
235
+
236
+
237
+ def interrupt_map(model: DesignModel) -> Scene:
238
+ """Line number -> source (name, block); interrupts without a line/block are unassigned."""
239
+ title = "Interrupt map"
240
+ interrupts = model.by_kind("interrupt")
241
+ if not interrupts:
242
+ return _empty_scene(title, "Interrupt map")
243
+
244
+ def line_of(entity: EntityBase) -> int | None:
245
+ line = getattr(entity, "line", None)
246
+ return line if isinstance(line, int) else None
247
+
248
+ assigned = sorted(
249
+ (e for e in interrupts if line_of(e) is not None and getattr(e, "block", None)),
250
+ key=lambda e: (line_of(e), e.key),
251
+ )
252
+ unassigned = sorted(
253
+ (e for e in interrupts if line_of(e) is None or not getattr(e, "block", None)),
254
+ key=lambda e: e.key,
255
+ )
256
+
257
+ boxes: list[Box] = []
258
+ width = 460
259
+ y = _MARGIN
260
+ boxes.append(
261
+ Box(
262
+ id="irq_header",
263
+ x=_MARGIN,
264
+ y=y,
265
+ w=width,
266
+ h=_ROW_H,
267
+ text="Interrupt map (line -> source)",
268
+ )
269
+ )
270
+ y += _ROW_H + _ROW_GAP
271
+ # Two sources on one line are bad data: mark them like overlapping memory regions.
272
+ line_counts: dict[int, int] = {}
273
+ for entity in interrupts:
274
+ number = line_of(entity)
275
+ if number is not None:
276
+ line_counts[number] = line_counts.get(number, 0) + 1
277
+ for entity in assigned:
278
+ line = line_of(entity)
279
+ block = getattr(entity, "block", None)
280
+ block_name = _block_name(model, block)
281
+ shared = line is not None and line_counts.get(line, 0) > 1
282
+ text = f"line {line}: {entity.name} (block {block_name})"
283
+ if shared:
284
+ text += " [line shared]"
285
+ boxes.append(
286
+ Box(
287
+ id=escape_id(entity.key),
288
+ x=_MARGIN,
289
+ y=y,
290
+ w=width,
291
+ h=_ROW_H,
292
+ text=text,
293
+ overlap=shared,
294
+ )
295
+ )
296
+ y += _ROW_H + _ROW_GAP
297
+ if unassigned:
298
+ boxes.append(
299
+ Box(
300
+ id="irq_unassigned",
301
+ x=_MARGIN,
302
+ y=y,
303
+ w=width,
304
+ h=_ROW_H,
305
+ text="unassigned (no line, or no block in the model)",
306
+ )
307
+ )
308
+ y += _ROW_H + _ROW_GAP
309
+ for entity in unassigned:
310
+ block = getattr(entity, "block", None)
311
+ block_name = _block_name(model, block)
312
+ line = line_of(entity)
313
+ line_s = str(line) if line is not None else "-"
314
+ shared = line is not None and line_counts.get(line, 0) > 1
315
+ text = f"{entity.name} (line {line_s}, block {block_name})"
316
+ if shared:
317
+ text += " [line shared]"
318
+ boxes.append(
319
+ Box(
320
+ id=escape_id(entity.key),
321
+ x=_MARGIN,
322
+ y=y,
323
+ w=width,
324
+ h=_ROW_H,
325
+ text=text,
326
+ overlap=shared,
327
+ )
328
+ )
329
+ y += _ROW_H + _ROW_GAP
330
+
331
+ return Scene(
332
+ title=title,
333
+ desc=(f"Interrupt map: {len(assigned)} assigned line(s), {len(unassigned)} unassigned."),
334
+ width=width + 2 * _MARGIN,
335
+ height=y + _MARGIN,
336
+ boxes=tuple(boxes),
337
+ )
338
+
339
+
340
+ def _blocks_using(model: DesignModel, node: EntityBase, block_attr: str) -> list[EntityBase]:
341
+ """Blocks that use a clock/reset `node`, via block.attrs[attr] or node.attrs['blocks']."""
342
+ blocks = {e.key: e for e in model.by_kind("block")}
343
+ found: dict[str, EntityBase] = {}
344
+ for block in blocks.values():
345
+ if _attr_str(block, block_attr) == node.name:
346
+ found[block.key] = block
347
+ for name in _attr_names(node, "blocks"):
348
+ for block in blocks.values():
349
+ if block.name == name:
350
+ found[block.key] = block
351
+ return sorted(found.values(), key=lambda e: e.key)
352
+
353
+
354
+ def clock_reset_tree(model: DesignModel) -> Scene:
355
+ """Each clock and reset -> the blocks that use it."""
356
+ title = "Clock/reset tree"
357
+ clocks = sorted(model.by_kind("clock"), key=lambda e: e.key)
358
+ resets = sorted(model.by_kind("reset"), key=lambda e: e.key)
359
+ if not clocks and not resets:
360
+ return _empty_scene(title, "Clock/reset tree")
361
+
362
+ boxes: list[Box] = []
363
+ edges: list[Edge] = []
364
+ node_x = _MARGIN
365
+ block_x = _MARGIN + _COL_W + _COL_GAP
366
+ max_x = block_x + _COL_W
367
+ y = _MARGIN
368
+ n_edges = 0
369
+
370
+ for node, kind_label, attr in (
371
+ *((c, "clock", "clock") for c in clocks),
372
+ *((r, "reset", "reset") for r in resets),
373
+ ):
374
+ node_id = escape_id(node.key)
375
+ node_y = y
376
+ boxes.append(
377
+ Box(
378
+ id=node_id,
379
+ x=node_x,
380
+ y=node_y,
381
+ w=_COL_W,
382
+ h=_ROW_H,
383
+ text=f"{kind_label}: {node.name}",
384
+ )
385
+ )
386
+ users = _blocks_using(model, node, attr)
387
+ row_y = y
388
+ for block in users:
389
+ block_id = f"{node_id}__use__{escape_id(block.key)}"
390
+ boxes.append(Box(id=block_id, x=block_x, y=row_y, w=_COL_W, h=_ROW_H, text=block.name))
391
+ edges.append(Edge(id=f"e{n_edges}", src=node_id, dst=block_id))
392
+ n_edges += 1
393
+ row_y += _ROW_H + _ROW_GAP
394
+ y = max(row_y, node_y + _ROW_H + _ROW_GAP)
395
+
396
+ return Scene(
397
+ title=title,
398
+ desc=f"Clock/reset tree: {len(clocks)} clock(s), {len(resets)} reset(s).",
399
+ width=max_x + _MARGIN,
400
+ height=max(y, _MARGIN + _ROW_H) + _MARGIN,
401
+ boxes=tuple(boxes),
402
+ edges=tuple(edges),
403
+ )
@@ -0,0 +1,163 @@
1
+ """A tiny deterministic scene model and its SVG / drawio renderers.
2
+
3
+ A :class:`Scene` is a title, a description and an ordered list of :class:`Box`,
4
+ :class:`Edge` and :class:`Row` elements laid out on a fixed integer grid. Both renderers
5
+ (:func:`scene_to_svg`, :func:`scene_to_drawio`) walk the same scene, so the two files
6
+ always agree, and both are byte-deterministic: elements keep the order the diagram code
7
+ put them in, ids come from escaped model keys, coordinates are integers, text is XML
8
+ escaped, and there are no timestamps, random ids or attributes that change between runs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+
15
+ FONT_SIZE = 12
16
+ """The single readable font size used for all element text."""
17
+
18
+ _OVERLAP_STROKE = "#cc0000"
19
+ """The one non-monochrome colour, reserved for the memory-map overlap marker."""
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class Box:
24
+ """A rectangle with text, placed by its top-left corner on the grid."""
25
+
26
+ id: str
27
+ x: int
28
+ y: int
29
+ w: int
30
+ h: int
31
+ text: str
32
+ overlap: bool = False
33
+ """When true, the box is outlined in the overlap colour (bad-data marker)."""
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class Edge:
38
+ """A directed connection between two boxes, drawn as an arrow."""
39
+
40
+ id: str
41
+ src: str
42
+ dst: str
43
+ label: str = ""
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Scene:
48
+ """A whole figure: metadata plus its boxes and edges, in draw order."""
49
+
50
+ title: str
51
+ desc: str
52
+ width: int
53
+ height: int
54
+ boxes: tuple[Box, ...] = field(default_factory=tuple)
55
+ edges: tuple[Edge, ...] = field(default_factory=tuple)
56
+
57
+
58
+ def xml_escape(text: str) -> str:
59
+ """Escape a string for use in XML text and double-quoted attribute values."""
60
+ return (
61
+ text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
62
+ )
63
+
64
+
65
+ def escape_id(key: str) -> str:
66
+ """Turn a model key into a stable id token (letters, digits, ``_`` only)."""
67
+ return "".join(c if c.isalnum() else "_" for c in key)
68
+
69
+
70
+ def _box_center(box: Box) -> tuple[int, int]:
71
+ return box.x + box.w // 2, box.y + box.h // 2
72
+
73
+
74
+ def scene_to_svg(scene: Scene) -> bytes:
75
+ """Render `scene` to a self-contained, accessible SVG document (UTF-8, ``\\n``)."""
76
+ lines: list[str] = []
77
+ lines.append('<?xml version="1.0" encoding="UTF-8"?>')
78
+ lines.append(
79
+ f'<svg xmlns="http://www.w3.org/2000/svg" '
80
+ f'width="{scene.width}" height="{scene.height}" '
81
+ f'viewBox="0 0 {scene.width} {scene.height}" '
82
+ f'font-family="sans-serif" font-size="{FONT_SIZE}">'
83
+ )
84
+ lines.append(f" <title>{xml_escape(scene.title)}</title>")
85
+ lines.append(f" <desc>{xml_escape(scene.desc)}</desc>")
86
+ lines.append(
87
+ ' <defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" '
88
+ 'orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#000000"/></marker></defs>'
89
+ )
90
+ by_id = {box.id: box for box in scene.boxes}
91
+ for edge in scene.edges:
92
+ src, dst = by_id.get(edge.src), by_id.get(edge.dst)
93
+ if src is None or dst is None:
94
+ continue
95
+ x1, y1 = _box_center(src)
96
+ x2, y2 = _box_center(dst)
97
+ lines.append(
98
+ f' <line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
99
+ f'stroke="#000000" marker-end="url(#arrow)"/>'
100
+ )
101
+ if edge.label:
102
+ lx, ly = (x1 + x2) // 2, (y1 + y2) // 2
103
+ lines.append(
104
+ f' <text x="{lx}" y="{ly}" text-anchor="middle">{xml_escape(edge.label)}</text>'
105
+ )
106
+ for box in scene.boxes:
107
+ stroke = _OVERLAP_STROKE if box.overlap else "#000000"
108
+ lines.append(
109
+ f' <rect x="{box.x}" y="{box.y}" width="{box.w}" height="{box.h}" '
110
+ f'fill="#ffffff" stroke="{stroke}"/>'
111
+ )
112
+ cx = box.x + box.w // 2
113
+ for i, part in enumerate(box.text.split("\n")):
114
+ ty = box.y + FONT_SIZE + 4 + i * (FONT_SIZE + 4)
115
+ lines.append(
116
+ f' <text x="{cx}" y="{ty}" text-anchor="middle">{xml_escape(part)}</text>'
117
+ )
118
+ lines.append("</svg>")
119
+ return ("\n".join(lines) + "\n").encode("utf-8")
120
+
121
+
122
+ def scene_to_drawio(scene: Scene) -> bytes:
123
+ """Render `scene` to an uncompressed drawio ``<mxfile>`` document (UTF-8, ``\\n``).
124
+
125
+ The ``<mxfile>`` has no ``modified``/``etag``/``agent``/``host`` attributes, so two
126
+ runs of the same scene produce byte-identical files.
127
+ """
128
+ lines: list[str] = []
129
+ lines.append('<?xml version="1.0" encoding="UTF-8"?>')
130
+ lines.append(f'<mxfile><diagram id="0" name="{xml_escape(scene.title)}">')
131
+ lines.append(
132
+ f'<mxGraphModel dx="{scene.width}" dy="{scene.height}" grid="0" '
133
+ f'pageWidth="{scene.width}" pageHeight="{scene.height}" math="0" shadow="0">'
134
+ )
135
+ lines.append("<root>")
136
+ lines.append('<mxCell id="0"/>')
137
+ lines.append('<mxCell id="1" parent="0"/>')
138
+ for box in scene.boxes:
139
+ stroke = _OVERLAP_STROKE if box.overlap else "#000000"
140
+ style = (
141
+ f"rounded=0;whiteSpace=wrap;html=0;fillColor=#ffffff;"
142
+ f"strokeColor={stroke};fontColor=#000000;fontSize={FONT_SIZE}"
143
+ )
144
+ lines.append(
145
+ f'<mxCell id="{box.id}" value="{xml_escape(box.text)}" '
146
+ f'style="{style}" vertex="1" parent="1">'
147
+ )
148
+ lines.append(
149
+ f'<mxGeometry x="{box.x}" y="{box.y}" width="{box.w}" height="{box.h}" as="geometry"/>'
150
+ )
151
+ lines.append("</mxCell>")
152
+ for edge in scene.edges:
153
+ style = "endArrow=classic;html=0;strokeColor=#000000;fontColor=#000000"
154
+ lines.append(
155
+ f'<mxCell id="{edge.id}" value="{xml_escape(edge.label)}" style="{style}" '
156
+ f'edge="1" parent="1" source="{edge.src}" target="{edge.dst}">'
157
+ )
158
+ lines.append('<mxGeometry relative="1" as="geometry"/>')
159
+ lines.append("</mxCell>")
160
+ lines.append("</root>")
161
+ lines.append("</mxGraphModel>")
162
+ lines.append("</diagram></mxfile>")
163
+ return ("\n".join(lines) + "\n").encode("utf-8")
@@ -0,0 +1,6 @@
1
+ schema_version: 1
2
+ name: spec-core
3
+ version: 0.1.0
4
+ description: >-
5
+ Deterministic extractors for text specifications (QNSC-style Micro-Architecture
6
+ Specifications): requirements, interface ports, register maps and open items.
chipgraph/py.typed ADDED
File without changes
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.5
2
+ Name: chipgraph
3
+ Version: 0.1.0
4
+ Summary: AI agent system for chip (IC) design, built on a build graph over a Design Model
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: jinja2>=3.1
9
+ Requires-Dist: mcp<3,>=2.2
10
+ Requires-Dist: opentelemetry-api>=1.45
11
+ Requires-Dist: pydantic>=2.13
12
+ Requires-Dist: pyslang<13,>=12
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: typer>=0.27
15
+ Provides-Extra: evals
16
+ Requires-Dist: inspect-ai==0.3.285; extra == 'evals'
17
+ Provides-Extra: llm
18
+ Requires-Dist: anthropic<3,>=2.3; extra == 'llm'
19
+ Provides-Extra: otel
20
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.45; extra == 'otel'
21
+ Requires-Dist: opentelemetry-sdk>=1.45; extra == 'otel'
22
+ Provides-Extra: sim
23
+ Requires-Dist: cocotb==2.1.0; extra == 'sim'
24
+ Requires-Dist: edalize==0.6.8; extra == 'sim'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # chipgraph
28
+
29
+ chipgraph is an AI agent system for chip (IC) design. It treats a chip project as a
30
+ build graph: specs, RTL and tests are artifacts; rules produce them, and a rule can be
31
+ code, an AI agent or a human; deterministic EDA checks verify every step; humans approve
32
+ at gates. All of this runs over a typed Design Model of the chip.
33
+
34
+ Pre-alpha: the design is agreed (DESIGN.md) and implementation follows
35
+ docs/IMPLEMENTATION_PLAN.md.
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ uv sync
41
+ make check
42
+ ```
43
+
44
+ ## Learn more
45
+
46
+ - [`DESIGN.md`](DESIGN.md) — the design
47
+ - [`docs/DECISIONS.md`](docs/DECISIONS.md) — the reasons behind the design
48
+ - [`docs/IMPLEMENTATION_PLAN.md`](docs/IMPLEMENTATION_PLAN.md) — tasks, dependencies, acceptance criteria
49
+ - [`AGENTS.md`](AGENTS.md) — how to work on this repository
50
+
51
+ ## License
52
+
53
+ Apache-2.0