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,400 @@
1
+ """`foreach: plan.modules`: one rule instance per module of an approved plan (DESIGN.md 5.3).
2
+
3
+ The gate `plan:<block>` hangs on the `gen` rule `digital-rtl/plan_expand`, whose only
4
+ file input is the plan: approving it pins the plan file's hash, and editing the plan
5
+ afterwards puts the gate back to waiting (the existing gate behaviour, `gate.py`).
6
+
7
+ `plan_modules` is what the project's resolver calls for `plan.modules`. For each block
8
+ it reads the plan only when that gate is approved at the plan's current hash, and only
9
+ when the plan still passes `plan_check` (limits included: defence in depth). Anything
10
+ else (no plan, not approved, edited since, invalid, over a limit) expands to nothing.
11
+ The graph is rebuilt on every build and `next_task`, so a plan that changes simply
12
+ yields no instances until it is approved again. Each module's entry carries:
13
+
14
+ - params `block` and `module`;
15
+ - the module's write set (`writes`): the instance's outputs must stay inside it;
16
+ - its `depends_on` as ordering edges to the same rule's instances of those modules;
17
+ - the approved plan artifact (`plan_expand`'s output) as an input, so every expanded
18
+ instance runs after the gate and the expansion;
19
+ - the profile's `style.exemplars` and naming rules file as inputs (M2-11a), so editing
20
+ one makes the module stale and it is written again. An exemplar an approved plan
21
+ writes, or one a role of a `plan.modules` agent rule must not read, is left out;
22
+ - the block's layout directories as template values (`{layout.rtl}`, `{layout.tb}`,
23
+ `chipgraph.packs.digital_rtl.layout`).
24
+
25
+ Two more selectors over the same approved plans (M2-11a), whose entries take every
26
+ module's write set and the block's testbench (the outputs of the `tb-author` rules over
27
+ `blocks`) as inputs, so their instances run after all of them:
28
+
29
+ - `plan.block`: one entry per block with an approved, valid plan (`digital-rtl/open_pr`,
30
+ `digital-rtl/pr_gate`); none for the others;
31
+ - `plan.blocks`: one entry per block of the profile; a block without an approved plan
32
+ gets a plain entry, with no extra input (`digital-rtl/review`, which also reviews a
33
+ change no plan made).
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from collections.abc import Iterable, Mapping, Sequence
39
+ from dataclasses import dataclass, field
40
+ from pathlib import Path
41
+
42
+ from chipgraph.app.context import AppContext
43
+ from chipgraph.app.errors import AppError
44
+ from chipgraph.checks.plan_check import (
45
+ DEFAULT_PLAN_PATH,
46
+ PLAN_MODULES,
47
+ PlanEnvError,
48
+ PlanReport,
49
+ check_plan_file,
50
+ dependency_depth,
51
+ topo_order,
52
+ )
53
+ from chipgraph.core.config.loader import ResolvedProfile
54
+ from chipgraph.core.config.models import PlanCfg
55
+ from chipgraph.core.contracts import RuleInstance, RuleSpec
56
+ from chipgraph.core.engine.gate import GateStatus, gate_id_for
57
+ from chipgraph.core.engine.graph import ForeachEntry, GraphError, build_graph, kind_for
58
+ from chipgraph.core.runtime.roles import denied_kind, find_role
59
+ from chipgraph.packs.digital_rtl.layout import LayoutError, fill_vars, layout_vars
60
+ from chipgraph.packs.digital_rtl.plan.model import Plan
61
+
62
+ PLAN_RULE = "digital-rtl/plan"
63
+ """The agent rule that writes a block's plan."""
64
+ EXPAND_RULE = "digital-rtl/plan_expand"
65
+ """The `gen` rule behind the gate `plan:<block>`; it writes the approved plan artifact."""
66
+ PLAN_BLOCK = "plan.block"
67
+ """The selector over the blocks with an approved, valid plan."""
68
+ PLAN_BLOCKS = "plan.blocks"
69
+ """The selector over every block, with the plan's files as inputs when it is approved."""
70
+ TB_ROLE = "tb-author"
71
+ """The role of the rules whose outputs are a block's testbench."""
72
+
73
+
74
+ class _Blocks:
75
+ def __init__(self, blocks: Iterable[str]) -> None:
76
+ self._blocks = sorted(blocks)
77
+
78
+ def expand(self, expr: str) -> list[dict[str, str]]:
79
+ if expr != "blocks":
80
+ raise GraphError(f"rule {EXPAND_RULE!r} must be 'foreach: blocks', not {expr!r}")
81
+ return [{"block": b} for b in self._blocks]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class PlanGate:
86
+ """A block's plan gate: its id, the instance it is evaluated on, and the paths."""
87
+
88
+ block: str
89
+ gate_id: str
90
+ instance: RuleInstance
91
+ plan_path: str
92
+ approved_path: str
93
+
94
+
95
+ def plan_gates(
96
+ rules: Iterable[RuleSpec], blocks: Iterable[str], *, repo: str = "."
97
+ ) -> dict[str, PlanGate]:
98
+ """Every block's `PlanGate`, from the loaded `digital-rtl/plan_expand` rule.
99
+
100
+ Raises `AppError` when the rule is not loaded (the `digital-rtl` pack is off) or has
101
+ no gate.
102
+ """
103
+ rule = next((r for r in rules if r.id == EXPAND_RULE), None)
104
+ if rule is None:
105
+ raise AppError(
106
+ f"foreach 'plan.modules' needs rule {EXPAND_RULE!r}: add the 'digital-rtl' pack "
107
+ "to the profile's packs"
108
+ )
109
+ if rule.gate is None:
110
+ raise AppError(f"rule {EXPAND_RULE!r} must have a gate (plan:{{block}})")
111
+ try:
112
+ graph = build_graph([rule], _Blocks(blocks), repo=repo)
113
+ except GraphError as exc:
114
+ raise AppError(str(exc)) from exc
115
+ gates: dict[str, PlanGate] = {}
116
+ for instance in graph.instances.values():
117
+ block = instance.params["block"]
118
+ plan_path = next(
119
+ (ref.path for ref in instance.inputs if ref.path is not None),
120
+ DEFAULT_PLAN_PATH.format(block=block),
121
+ )
122
+ approved = instance.outputs[0].path or ""
123
+ gates[block] = PlanGate(
124
+ block=block,
125
+ gate_id=gate_id_for(rule.gate, instance.params),
126
+ instance=instance,
127
+ plan_path=plan_path,
128
+ approved_path=approved,
129
+ )
130
+ return gates
131
+
132
+
133
+ def gate_status(ctx: AppContext, gate: PlanGate) -> GateStatus:
134
+ """The status of a block's plan gate, at the plan's current hash."""
135
+ return ctx.gates.status(gate.gate_id, gate.instance)
136
+
137
+
138
+ def within_limits(report: PlanReport, limits: PlanCfg) -> bool:
139
+ """The plan is inside the profile's limits (checked again, apart from `plan_check`)."""
140
+ plan = report.plan
141
+ if plan is None:
142
+ return False
143
+ return (
144
+ len(plan.modules) <= limits.max_modules
145
+ and dependency_depth(plan) <= limits.max_depth
146
+ and sum(m.budget.tries for m in plan.modules) <= limits.max_total_tries
147
+ )
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class ApprovedPlan:
152
+ """A block's plan, approved at its current hash and still valid."""
153
+
154
+ gate: PlanGate
155
+ plan: Plan
156
+ vars: Mapping[str, str] = field(default_factory=dict)
157
+
158
+ @property
159
+ def block(self) -> str:
160
+ return self.gate.block
161
+
162
+ def module_writes(self) -> tuple[str, ...]:
163
+ """Every file of the plan's write sets, in build order."""
164
+ return tuple(
165
+ path
166
+ for name in topo_order(self.plan)
167
+ if (module := self.plan.module(name)) is not None
168
+ for path in module.write_paths
169
+ )
170
+
171
+
172
+ def _vars(resolved: ResolvedProfile, block: str) -> dict[str, str]:
173
+ try:
174
+ return layout_vars(resolved, block)
175
+ except LayoutError as exc:
176
+ raise AppError(str(exc)) from exc
177
+
178
+
179
+ def approved_plan(
180
+ ctx: AppContext, rules: Sequence[RuleSpec], gate: PlanGate
181
+ ) -> ApprovedPlan | None:
182
+ """The block's plan when it is approved at its current hash and valid, else None."""
183
+ if gate_status(ctx, gate) != "approved":
184
+ return None
185
+ resolved = ctx.require_profile()
186
+ try:
187
+ report = check_plan_file(
188
+ ctx.root,
189
+ gate.block,
190
+ plan_path=gate.plan_path,
191
+ resolved=resolved,
192
+ rules=rules,
193
+ check_existing=False, # checked before approval; now the files are the plan's
194
+ )
195
+ except PlanEnvError:
196
+ return None
197
+ if not report.ok or report.plan is None or not within_limits(report, resolved.profile.plan):
198
+ return None
199
+ return ApprovedPlan(gate=gate, plan=report.plan, vars=_vars(resolved, gate.block))
200
+
201
+
202
+ def approved_plans(ctx: AppContext, rules: Sequence[RuleSpec]) -> dict[str, ApprovedPlan]:
203
+ """Every block's `ApprovedPlan`, for the blocks that have one."""
204
+ resolved = ctx.require_profile()
205
+ gates = plan_gates(rules, resolved.profile.blocks, repo=ctx.store.repo)
206
+ found: dict[str, ApprovedPlan] = {}
207
+ for block in sorted(gates):
208
+ approved = approved_plan(ctx, rules, gates[block])
209
+ if approved is not None:
210
+ found[block] = approved
211
+ return found
212
+
213
+
214
+ def _naming_file(root: Path, resolved: ResolvedProfile, block: str) -> str | None:
215
+ """The naming rules file the profile names, repo-relative, when it is in the repo."""
216
+ from chipgraph.checks.naming import _resolve_rules_path, _RulesError
217
+ from chipgraph.packs.digital_rtl.rtl.context import _naming_ref
218
+
219
+ ref = _naming_ref(resolved, block)
220
+ if ref is None:
221
+ return None
222
+ try:
223
+ path = _resolve_rules_path(ref, root)
224
+ except _RulesError:
225
+ return None
226
+ try:
227
+ rel = path.resolve().relative_to(root.resolve()).as_posix()
228
+ except ValueError:
229
+ return None
230
+ return rel if path.is_file() else None
231
+
232
+
233
+ def style_inputs(
234
+ ctx: AppContext,
235
+ rules: Sequence[RuleSpec],
236
+ block: str,
237
+ *,
238
+ planned: Iterable[str] = (),
239
+ ) -> tuple[str, ...]:
240
+ """The style files a planned module is written after: exemplars and naming rules.
241
+
242
+ Only files that exist; never one in `planned` (a file an approved plan writes:
243
+ it would be an input of the module that writes it), and never one a role of an
244
+ agent rule over `plan.modules` must not read.
245
+ """
246
+ resolved = ctx.require_profile()
247
+ roles = [
248
+ role
249
+ for rule in rules
250
+ if rule.foreach == PLAN_MODULES and rule.kind == "agent" and rule.role is not None
251
+ if (role := find_role(rule.role)) is not None
252
+ ]
253
+ skip = set(planned)
254
+ candidates = [*resolved.for_block(block).style.exemplars]
255
+ naming = _naming_file(ctx.root, resolved, block)
256
+ if naming is not None:
257
+ candidates.append(naming)
258
+ out: list[str] = []
259
+ for rel in dict.fromkeys(candidates):
260
+ if rel in skip or not (ctx.root / rel).is_file():
261
+ continue
262
+ if any(denied_kind(role, kind_for(rel), rel) is not None for role in roles):
263
+ continue
264
+ out.append(rel)
265
+ return tuple(out)
266
+
267
+
268
+ def module_entries(
269
+ ctx: AppContext,
270
+ rules: Sequence[RuleSpec],
271
+ approved: ApprovedPlan,
272
+ *,
273
+ planned: Iterable[str] = (),
274
+ ) -> list[ForeachEntry]:
275
+ """The `plan.modules` entries of one block's approved plan."""
276
+ plan = approved.plan
277
+ style = style_inputs(ctx, rules, approved.block, planned=planned)
278
+ entries: list[ForeachEntry] = []
279
+ for name in topo_order(plan):
280
+ module = plan.module(name)
281
+ assert module is not None
282
+ entries.append(
283
+ ForeachEntry(
284
+ params={"block": approved.block, "module": module.name},
285
+ inputs=(approved.gate.approved_path, *style),
286
+ after=tuple(
287
+ {"block": approved.block, "module": dep}
288
+ for dep in dict.fromkeys(module.depends_on)
289
+ ),
290
+ writes=frozenset(module.write_paths),
291
+ vars=dict(approved.vars),
292
+ )
293
+ )
294
+ return entries
295
+
296
+
297
+ def block_entries(ctx: AppContext, rules: Sequence[RuleSpec], gate: PlanGate) -> list[ForeachEntry]:
298
+ """The `plan.modules` entries of one block: none unless its plan is approved and valid."""
299
+ approved = approved_plan(ctx, rules, gate)
300
+ if approved is None:
301
+ return []
302
+ return module_entries(ctx, rules, approved, planned=approved.module_writes())
303
+
304
+
305
+ def testbench_files(
306
+ rules: Sequence[RuleSpec], block: str, values: Mapping[str, str]
307
+ ) -> tuple[str, ...]:
308
+ """The block's testbench: the outputs of the `tb-author` rules over `blocks`."""
309
+ out: list[str] = []
310
+ for rule in sorted(rules, key=lambda r: r.id):
311
+ if rule.kind == "agent" and rule.role == TB_ROLE and rule.foreach == "blocks":
312
+ out.extend(fill_vars(t, {**values, "block": block}) for t in rule.outputs)
313
+ return tuple(dict.fromkeys(out))
314
+
315
+
316
+ def block_files(
317
+ rules: Sequence[RuleSpec], approved: ApprovedPlan
318
+ ) -> tuple[tuple[str, ...], tuple[str, ...]]:
319
+ """The block's RTL (every module's write set) and testbench files."""
320
+ return approved.module_writes(), testbench_files(rules, approved.block, approved.vars)
321
+
322
+
323
+ def plan_block_entry(rules: Sequence[RuleSpec], approved: ApprovedPlan) -> ForeachEntry:
324
+ """The `plan.block` / `plan.blocks` entry of a block with an approved plan."""
325
+ rtl, tb = block_files(rules, approved)
326
+ return ForeachEntry(
327
+ params={"block": approved.block},
328
+ inputs=tuple(dict.fromkeys((approved.gate.approved_path, *rtl, *tb))),
329
+ vars=dict(approved.vars),
330
+ )
331
+
332
+
333
+ @dataclass
334
+ class PlanExpansion:
335
+ """The approved plans of a project, read once per graph, and the entries they give."""
336
+
337
+ ctx: AppContext
338
+ rules: Sequence[RuleSpec]
339
+ _approved: dict[str, ApprovedPlan] | None = None
340
+
341
+ @property
342
+ def approved(self) -> dict[str, ApprovedPlan]:
343
+ if self._approved is None:
344
+ self._approved = approved_plans(self.ctx, self.rules)
345
+ return self._approved
346
+
347
+ def modules(self) -> list[ForeachEntry]:
348
+ """`plan.modules`: every module of every approved plan."""
349
+ planned = {p for a in self.approved.values() for p in a.module_writes()}
350
+ entries: list[ForeachEntry] = []
351
+ for block in sorted(self.approved):
352
+ entries.extend(
353
+ module_entries(self.ctx, self.rules, self.approved[block], planned=planned)
354
+ )
355
+ return entries
356
+
357
+ def blocks_with_plan(self) -> list[ForeachEntry]:
358
+ """`plan.block`: one entry per block with an approved plan."""
359
+ return [plan_block_entry(self.rules, self.approved[b]) for b in sorted(self.approved)]
360
+
361
+ def every_block(self) -> list[ForeachEntry]:
362
+ """`plan.blocks`: every block; an approved plan's files as inputs when it has one."""
363
+ resolved = self.ctx.require_profile()
364
+ entries: list[ForeachEntry] = []
365
+ for block in sorted(resolved.profile.blocks):
366
+ approved = self.approved.get(block)
367
+ if approved is not None:
368
+ entries.append(plan_block_entry(self.rules, approved))
369
+ else:
370
+ entries.append(ForeachEntry(params={"block": block}, vars=_vars(resolved, block)))
371
+ return entries
372
+
373
+
374
+ def plan_modules(ctx: AppContext, rules: Sequence[RuleSpec]) -> list[ForeachEntry]:
375
+ """`foreach: plan.modules`: every module of every block's approved, valid plan."""
376
+ return PlanExpansion(ctx, rules).modules()
377
+
378
+
379
+ __all__ = [
380
+ "EXPAND_RULE",
381
+ "PLAN_BLOCK",
382
+ "PLAN_BLOCKS",
383
+ "PLAN_RULE",
384
+ "TB_ROLE",
385
+ "ApprovedPlan",
386
+ "PlanExpansion",
387
+ "PlanGate",
388
+ "approved_plan",
389
+ "approved_plans",
390
+ "block_entries",
391
+ "block_files",
392
+ "gate_status",
393
+ "module_entries",
394
+ "plan_block_entry",
395
+ "plan_gates",
396
+ "plan_modules",
397
+ "style_inputs",
398
+ "testbench_files",
399
+ "within_limits",
400
+ ]
@@ -0,0 +1,176 @@
1
+ """`chipgraph plan show <block>`: what the approver of gate `plan:<block>` reads.
2
+
3
+ The summary: the gate's status (and the command that approves it), the check's errors
4
+ and warnings, the modules (summary, requirements, dependencies, write sets, ports,
5
+ budget) in build order, requirement coverage, the limits used, the assumptions and the
6
+ open questions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from chipgraph.app.build import load_rules
14
+ from chipgraph.app.context import AppContext
15
+ from chipgraph.app.errors import AppError
16
+ from chipgraph.checks.plan_check import (
17
+ PlanEnvError,
18
+ check_plan_file,
19
+ dependency_depth,
20
+ topo_order,
21
+ )
22
+ from chipgraph.packs.digital_rtl.plan.resolver import gate_status, plan_gates
23
+
24
+
25
+ def plan_summary(ctx: AppContext, block: str) -> dict[str, Any]:
26
+ """The summary of `block`'s plan, as plain data (`--json` prints it as is)."""
27
+ resolved = ctx.require_profile()
28
+ if block not in resolved.profile.blocks:
29
+ raise AppError(
30
+ f"unknown block {block!r}; blocks: {', '.join(sorted(resolved.profile.blocks))}"
31
+ )
32
+ rules = load_rules(ctx)
33
+ gate = plan_gates(rules, resolved.profile.blocks, repo=ctx.store.repo)[block]
34
+ try:
35
+ report = check_plan_file(
36
+ ctx.root, block, plan_path=gate.plan_path, resolved=resolved, rules=rules
37
+ )
38
+ except PlanEnvError as exc:
39
+ raise AppError(str(exc)) from exc
40
+ status = gate_status(ctx, gate)
41
+ summary: dict[str, Any] = {
42
+ "block": block,
43
+ "path": report.path,
44
+ "exists": report.exists,
45
+ "sha256": report.sha256,
46
+ "gate": {
47
+ "id": gate.gate_id,
48
+ "status": status,
49
+ "instance": gate.instance.instance_id,
50
+ "approve": (
51
+ f"chipgraph approve {gate.gate_id} --instance '{gate.instance.instance_id}'"
52
+ ),
53
+ },
54
+ "ok": report.ok,
55
+ "errors": [_issue(i) for i in report.issues if i.severity == "error"],
56
+ "warnings": [_issue(i) for i in report.issues if i.severity == "warning"],
57
+ }
58
+ plan = report.plan
59
+ if plan is None:
60
+ return summary
61
+ limits = resolved.profile.plan
62
+ order = topo_order(plan)
63
+ summary.update(
64
+ {
65
+ "top": plan.top,
66
+ "order": order,
67
+ "modules": [
68
+ {
69
+ "name": m.name,
70
+ "summary": m.summary,
71
+ "reqs": list(m.reqs),
72
+ "depends_on": list(m.depends_on),
73
+ "writes": [w.model_dump(mode="json") for w in m.writes],
74
+ "ports": [p.model_dump(mode="json") for p in m.interface.ports],
75
+ "budget": m.budget.model_dump(mode="json"),
76
+ }
77
+ for name in order
78
+ if (m := plan.module(name)) is not None
79
+ ],
80
+ "coverage": _coverage(report),
81
+ "limits": {
82
+ "modules": [len(plan.modules), limits.max_modules],
83
+ "depth": [dependency_depth(plan), limits.max_depth],
84
+ "tries": [sum(m.budget.tries for m in plan.modules), limits.max_total_tries],
85
+ },
86
+ "assumptions": list(plan.assumptions),
87
+ "open_questions": list(plan.open_questions),
88
+ }
89
+ )
90
+ return summary
91
+
92
+
93
+ def _issue(issue: Any) -> str:
94
+ where = f"{issue.file}:{issue.line}" if issue.line else str(issue.file)
95
+ return f"{where} [{issue.rule}] {issue.msg}"
96
+
97
+
98
+ def _coverage(report: Any) -> list[dict[str, Any]]:
99
+ """Each block requirement: the modules covering it, or why it is unassigned, or missing."""
100
+ plan = report.plan
101
+ env = report.env
102
+ if plan is None or env is None:
103
+ return []
104
+ rows: list[dict[str, Any]] = []
105
+ for req in env.requirements:
106
+ modules = [m.name for m in plan.modules if req.refs & set(m.reqs)]
107
+ reasons = [u.reason for u in plan.unassigned_reqs if u.req in req.refs]
108
+ rows.append(
109
+ {
110
+ "req": req.ref,
111
+ "modules": modules,
112
+ "unassigned": reasons[0] if reasons and not modules else None,
113
+ }
114
+ )
115
+ return rows
116
+
117
+
118
+ def render_text(summary: dict[str, Any]) -> str:
119
+ """The summary as text, for a person."""
120
+ lines = [f"plan for block {summary['block']}: {summary['path']}"]
121
+ gate = summary["gate"]
122
+ lines.append(f"gate {gate['id']}: {gate['status']}")
123
+ if not summary["exists"]:
124
+ lines.append("no plan yet: build rule digital-rtl/plan for this block first")
125
+ return "\n".join(lines) + "\n"
126
+ lines.append(f"check: {'ok' if summary['ok'] else 'FAILS'}")
127
+ for error in summary["errors"]:
128
+ lines.append(f" error {error}")
129
+ for warning in summary["warnings"]:
130
+ lines.append(f" warning {warning}")
131
+ if "modules" not in summary:
132
+ return "\n".join(lines) + "\n"
133
+
134
+ limits = summary["limits"]
135
+ lines.append(
136
+ f"top: {summary['top']} modules {limits['modules'][0]}/{limits['modules'][1]}, "
137
+ f"depth {limits['depth'][0]}/{limits['depth'][1]}, "
138
+ f"tries {limits['tries'][0]}/{limits['tries'][1]}"
139
+ )
140
+ lines.append("")
141
+ lines.append("modules (build order):")
142
+ for m in summary["modules"]:
143
+ lines.append(f" {m['name']}: {m['summary']}")
144
+ lines.append(f" reqs: {', '.join(m['reqs']) or '-'}")
145
+ lines.append(f" depends on: {', '.join(m['depends_on']) or '-'}")
146
+ writes = [w["path"] + (" (replaces)" if w["replaces"] else "") for w in m["writes"]]
147
+ lines.append(f" writes: {', '.join(writes)}")
148
+ ports = [
149
+ f"{p['name']}:{p['direction']}[{p['width']}]" + (" internal" if p["internal"] else "")
150
+ for p in m["ports"]
151
+ ]
152
+ lines.append(f" ports: {', '.join(ports) or '-'}")
153
+ lines.append(f" budget: {m['budget']['tries']} tries, tier {m['budget']['tier']}")
154
+ lines.append("")
155
+ lines.append("requirement coverage:")
156
+ for row in summary["coverage"]:
157
+ if row["modules"]:
158
+ lines.append(f" {row['req']}: {', '.join(row['modules'])}")
159
+ elif row["unassigned"]:
160
+ lines.append(f" {row['req']}: unassigned ({row['unassigned']})")
161
+ else:
162
+ lines.append(f" {row['req']}: NOT COVERED")
163
+ if not summary["coverage"]:
164
+ lines.append(" (the block has no requirements in the Design Model)")
165
+ lines.append("")
166
+ lines.append("assumptions:")
167
+ lines.extend(f" - {a}" for a in summary["assumptions"] or ["none"])
168
+ lines.append("open questions (read before approving):")
169
+ lines.extend(f" - {q}" for q in summary["open_questions"] or ["none"])
170
+ if gate["status"] != "approved":
171
+ lines.append("")
172
+ lines.append(f"approve: {gate['approve']}")
173
+ return "\n".join(lines) + "\n"
174
+
175
+
176
+ __all__ = ["plan_summary", "render_text"]
@@ -0,0 +1,11 @@
1
+ """The pull request of a block's planned RTL (M2-11a): its branch, its body, its gate.
2
+
3
+ - `open`: the `open_pr` tool of rule `digital-rtl/open_pr`: builds the branch
4
+ `chipgraph/<run id>/<block>` from the block's RTL and testbench files without touching
5
+ the user's checkout, pushes it, opens the PR and writes `pr/<block>.json`;
6
+ - `gate`: the `pr_gate` tool of rule `digital-rtl/pr_gate`, gated by `pr:<block>`;
7
+ - `body`: the PR body: the plan, requirement coverage, check results and the Critic's
8
+ verdict, factual and short.
9
+
10
+ Nothing here merges a pull request, or asks GitHub to.
11
+ """