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,339 @@
1
+ """Wire `chipgraph baseline` to the graph, git, the review store and the finding store.
2
+
3
+ The generic planning lives in `chipgraph.core.engine.baseline`; this module supplies the
4
+ concrete facts it needs and applies the plan:
5
+
6
+ - it builds the graph for every target, collects each gated instance's gate and the
7
+ file inputs it covers, and asks the `GateEvaluator` whether each gate is already
8
+ decided;
9
+ - it answers `VcsFacts` (content hash, tracked/dirty, last commit) from the git working
10
+ tree and history;
11
+ - on ``--confirm`` it records one `baseline` decision per undecided gate over its clean,
12
+ tracked artifacts (through `core.engine.gate.baseline`) and marks every open finding as
13
+ baselined, and it refuses to run on a branch other than the configured main branch.
14
+
15
+ `baseline` runs after `ingest`; the spec files ingest reads are ordinary file artifacts
16
+ here, gated by whatever rule consumes them.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import subprocess
22
+ from collections.abc import Iterable
23
+ from dataclasses import dataclass
24
+ from datetime import UTC, datetime
25
+ from pathlib import Path
26
+
27
+ from chipgraph.app.build import load_rules
28
+ from chipgraph.app.context import AppContext
29
+ from chipgraph.app.errors import AppError
30
+ from chipgraph.app.findings import finding_current_hashes
31
+ from chipgraph.core.contracts import Approval, ArtifactRef, Finding, RuleInstance
32
+ from chipgraph.core.engine import gate as gate_mod
33
+ from chipgraph.core.engine.baseline import (
34
+ BaselinePlan,
35
+ CommitInfo,
36
+ GateInput,
37
+ clean_refs_for_gate,
38
+ gate_inputs_from_instances,
39
+ plan_baseline,
40
+ )
41
+ from chipgraph.core.engine.graph import GraphError, build_graph
42
+ from chipgraph.core.engine.scheduler import _format_gate
43
+ from chipgraph.core.state.findings import FindingStore, effective_status, waiver_gate_id
44
+
45
+ __all__ = [
46
+ "BaselineOutcome",
47
+ "WrongBranchError",
48
+ "baseline_gate_id",
49
+ "current_branch",
50
+ "main_branch",
51
+ "plan_project_baseline",
52
+ "run_baseline",
53
+ ]
54
+
55
+ _DEFAULT_MAIN_BRANCH = "main"
56
+
57
+
58
+ def baseline_gate_id(finding_id: str) -> str:
59
+ """The `Approval.gate_id` a finding's `baseline` decision is recorded under.
60
+
61
+ Distinct from the waiver gate id (`finding:<id>`), so a baseline decision never
62
+ reads as a waiver: `chipgraph findings` keeps listing the finding as open (recorded,
63
+ non-blocking), which is what a baseline of existing findings means (DESIGN.md 6.4).
64
+ """
65
+ return f"baseline:finding:{finding_id}"
66
+
67
+
68
+ class WrongBranchError(AppError):
69
+ """Raised when `baseline` runs on a branch other than the configured main branch."""
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class BaselineOutcome:
74
+ """What a `--confirm` run recorded: the decisions written and findings baselined."""
75
+
76
+ approvals: tuple[Approval, ...]
77
+ findings_baselined: tuple[str, ...]
78
+
79
+
80
+ # --- git facts ----------------------------------------------------------------------
81
+
82
+
83
+ def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
84
+ """Run a read-only git command under `root`, capturing output (never a shell)."""
85
+ return subprocess.run(
86
+ ["git", "-C", str(root), *args],
87
+ capture_output=True,
88
+ text=True,
89
+ check=False,
90
+ )
91
+
92
+
93
+ def current_branch(root: Path) -> str | None:
94
+ """The current branch name, or `None` when detached / not a git repo."""
95
+ result = _git(root, "rev-parse", "--abbrev-ref", "HEAD")
96
+ if result.returncode != 0:
97
+ return None
98
+ name = result.stdout.strip()
99
+ return None if name in ("", "HEAD") else name
100
+
101
+
102
+ def main_branch(root: Path) -> str:
103
+ """The configured main branch: `origin/HEAD`'s target, falling back to `main`.
104
+
105
+ Read from `git symbolic-ref refs/remotes/origin/HEAD` (e.g. `origin/main` -> `main`);
106
+ if there is no such ref, the default `main` is used (DESIGN.md 6.4).
107
+ """
108
+ result = _git(root, "symbolic-ref", "refs/remotes/origin/HEAD")
109
+ if result.returncode == 0:
110
+ ref = result.stdout.strip()
111
+ prefix = "refs/remotes/origin/"
112
+ if ref.startswith(prefix):
113
+ name = ref[len(prefix) :]
114
+ if name:
115
+ return name
116
+ return _DEFAULT_MAIN_BRANCH
117
+
118
+
119
+ class _GitFacts:
120
+ """`VcsFacts` answered from a git working tree and history under `root`.
121
+
122
+ Tracked/dirty status and content hashes come from the working tree; `last_commit`
123
+ reads `git log`. Results are cached per path so a listing that touches an artifact
124
+ from several gates does not re-shell for it.
125
+ """
126
+
127
+ def __init__(self, ctx: AppContext) -> None:
128
+ self._ctx = ctx
129
+ self._root = ctx.root
130
+ self._sha: dict[str, str | None] = {}
131
+ self._tracked: dict[str, bool] = {}
132
+ self._dirty: dict[str, bool] = {}
133
+ self._commit: dict[str, CommitInfo | None] = {}
134
+ self._changed = self._collect_changed()
135
+
136
+ def _collect_changed(self) -> frozenset[str]:
137
+ result = _git(self._root, "status", "--porcelain=v1", "-z")
138
+ paths: set[str] = set()
139
+ for entry in result.stdout.split("\0"):
140
+ if len(entry) > 3:
141
+ paths.add(entry[3:])
142
+ return frozenset(paths)
143
+
144
+ def sha256(self, path: str) -> str | None:
145
+ if path not in self._sha:
146
+ ref = ArtifactRef(kind="other", path=path)
147
+ hashes = self._ctx.store.current_hashes((ref,))
148
+ self._sha[path] = hashes.get(f"{ref.repo}:{path}")
149
+ return self._sha[path]
150
+
151
+ def is_tracked(self, path: str) -> bool:
152
+ if path not in self._tracked:
153
+ result = _git(self._root, "ls-files", "--error-unmatch", "--", path)
154
+ self._tracked[path] = result.returncode == 0
155
+ return self._tracked[path]
156
+
157
+ def is_dirty(self, path: str) -> bool:
158
+ if path not in self._dirty:
159
+ # Untracked paths appear in `git status` too; only a *tracked* modified path
160
+ # is "dirty" here (an untracked one is reported by `is_tracked`).
161
+ self._dirty[path] = path in self._changed and self.is_tracked(path)
162
+ return self._dirty[path]
163
+
164
+ def last_commit(self, path: str) -> CommitInfo | None:
165
+ if path not in self._commit:
166
+ self._commit[path] = self._read_last_commit(path)
167
+ return self._commit[path]
168
+
169
+ def _read_last_commit(self, path: str) -> CommitInfo | None:
170
+ # %h short sha, %ad author date (short), %s subject, %P parents (to spot merges).
171
+ # %P is last so an empty parent list (a root commit) just yields a trailing empty
172
+ # field; parse with a fixed split count so that empty field is never lost.
173
+ result = _git(
174
+ self._root,
175
+ "log",
176
+ "-1",
177
+ "--date=short",
178
+ "--format=%h%x1f%ad%x1f%s%x1f%P",
179
+ "--",
180
+ path,
181
+ )
182
+ if result.returncode != 0:
183
+ return None
184
+ line = result.stdout.rstrip("\n")
185
+ if not line:
186
+ return None
187
+ parts = line.split("\x1f", 3)
188
+ if len(parts) < 3:
189
+ return None
190
+ short_sha, date, subject = parts[0], parts[1], parts[2]
191
+ parents = parts[3] if len(parts) > 3 else ""
192
+ is_merge = len(parents.split()) > 1
193
+ return CommitInfo(short_sha=short_sha, date=date, subject=subject, is_merge=is_merge)
194
+
195
+
196
+ # --- planning -----------------------------------------------------------------------
197
+
198
+
199
+ def _gated_instances(ctx: AppContext) -> list[tuple[str, RuleInstance, bool]]:
200
+ """Every gated instance across all targets: `(gate_id, instance, already_decided)`.
201
+
202
+ The graph is built once over `"*"` (all targets). A rule with a `gate` yields one
203
+ entry per instance, with the gate id formatted from the instance's params; whether
204
+ the gate is already decided is asked of the project's `GateEvaluator`.
205
+ """
206
+ rules = load_rules(ctx)
207
+ from chipgraph.app.build import _resolver_for
208
+
209
+ try:
210
+ graph = build_graph(rules, _resolver_for(ctx), repo=ctx.store.repo)
211
+ except GraphError as exc:
212
+ raise AppError(str(exc)) from exc
213
+
214
+ gated: list[tuple[str, RuleInstance, bool]] = []
215
+ seen: set[tuple[str, str]] = set()
216
+ for iid in sorted(graph.instances):
217
+ instance = graph.instances[iid]
218
+ rule = graph.rules[instance.rule_id]
219
+ if not rule.gate:
220
+ continue
221
+ gate_id = _format_gate(rule.gate, instance.params)
222
+ key = (gate_id, iid)
223
+ if key in seen:
224
+ continue
225
+ seen.add(key)
226
+ already = ctx.gates.status(gate_id, instance) != "waiting"
227
+ gated.append((gate_id, instance, already))
228
+ return gated
229
+
230
+
231
+ def _refs_by_path(gated: Iterable[tuple[str, RuleInstance, bool]]) -> dict[str, ArtifactRef]:
232
+ """Map each gated instance's file-input path to its `ArtifactRef` (repo/label kept)."""
233
+ by_path: dict[str, ArtifactRef] = {}
234
+ for _gate_id, instance, _decided in gated:
235
+ for ref in instance.inputs:
236
+ if ref.path is not None and ref.path not in by_path:
237
+ by_path[ref.path] = ref
238
+ return by_path
239
+
240
+
241
+ def _open_finding_count(ctx: AppContext) -> int:
242
+ return len(_open_findings(ctx))
243
+
244
+
245
+ def _open_findings(ctx: AppContext) -> list[Finding]:
246
+ store = FindingStore(ctx.layout)
247
+ open_rows: list[Finding] = []
248
+ for finding in store.list():
249
+ waivers = ctx.review.approvals(waiver_gate_id(finding.id))
250
+ current_hashes = finding_current_hashes(ctx.store, finding, waivers)
251
+ if effective_status(finding, waivers, current_hashes) == "open":
252
+ open_rows.append(finding)
253
+ return open_rows
254
+
255
+
256
+ def plan_project_baseline(ctx: AppContext) -> tuple[BaselinePlan, dict[str, ArtifactRef]]:
257
+ """Build the `BaselinePlan` for this project, plus a path -> `ArtifactRef` map.
258
+
259
+ The map lets `run_baseline` pin a decision to the exact refs (with repo and label)
260
+ a gate covers, not just their paths.
261
+ """
262
+ gated = _gated_instances(ctx)
263
+ facts = _GitFacts(ctx)
264
+ gate_inputs: list[GateInput] = gate_inputs_from_instances(gated)
265
+ plan = plan_baseline(gate_inputs, facts, open_findings=_open_finding_count(ctx))
266
+ return plan, _refs_by_path(gated)
267
+
268
+
269
+ # --- confirming ---------------------------------------------------------------------
270
+
271
+
272
+ def run_baseline(
273
+ ctx: AppContext,
274
+ plan: BaselinePlan,
275
+ refs_by_path: dict[str, ArtifactRef],
276
+ *,
277
+ by: str,
278
+ note: str = "",
279
+ ) -> BaselineOutcome:
280
+ """Record the plan: one `baseline` decision per undecided gate, mark findings baselined.
281
+
282
+ Idempotent: a gate that is already decided (skipped) records nothing, so running this
283
+ again after a first confirm writes nothing new. A gate with no clean, tracked artifact
284
+ is skipped too (there is nothing on the main branch to pin). Returns what was written.
285
+ """
286
+ approvals: list[Approval] = []
287
+ for gate in plan.gates:
288
+ if not gate.will_baseline:
289
+ continue
290
+ refs = clean_refs_for_gate(gate, refs_by_path)
291
+ if not refs:
292
+ continue
293
+ try:
294
+ approval = gate_mod.baseline(
295
+ ctx.review, ctx.store, gate.gate_id, refs, by=by, note=note
296
+ )
297
+ except gate_mod.GateError as exc:
298
+ raise AppError(str(exc)) from exc
299
+ approvals.append(approval)
300
+
301
+ baselined = _baseline_open_findings(ctx, by=by, note=note)
302
+ return BaselineOutcome(approvals=tuple(approvals), findings_baselined=tuple(baselined))
303
+
304
+
305
+ def _baseline_open_findings(ctx: AppContext, *, by: str, note: str) -> list[str]:
306
+ """Record a `baseline` decision against every open finding, keyed by its id.
307
+
308
+ Findings never block a build, so "baselined" here means *recorded, non-blocking,
309
+ still listed*: a `baseline` `Approval` bound to the finding's current artifact hashes
310
+ is written through the review store, distinct from a `waive` (so `chipgraph findings`
311
+ still lists the finding as open). Idempotent per (finding, hashes): a finding whose
312
+ artifacts have a current baseline decision is not recorded again.
313
+ """
314
+ recorded: list[str] = []
315
+ now = datetime.now(UTC)
316
+ for finding in _open_findings(ctx):
317
+ gate_id = baseline_gate_id(finding.id)
318
+ hashes = ctx.store.current_hashes(finding.artifacts)
319
+ if not hashes:
320
+ # No artifact to pin (e.g. a whole-check finding); still count it as recorded
321
+ # via its detection-time hashes if it has any, else skip pinning.
322
+ hashes = dict(finding.artifact_hashes)
323
+ if not hashes:
324
+ continue
325
+ existing = ctx.review.approvals(gate_id)
326
+ if any(a.decision == "baseline" and a.is_current(hashes) for a in existing):
327
+ continue
328
+ ctx.review.record(
329
+ Approval(
330
+ gate_id=gate_id,
331
+ by=by,
332
+ at=now,
333
+ artifact_hashes=hashes,
334
+ decision="baseline",
335
+ note=note or "baselined: recorded, non-blocking",
336
+ )
337
+ )
338
+ recorded.append(finding.id)
339
+ return recorded
chipgraph/app/build.py ADDED
@@ -0,0 +1,332 @@
1
+ """Building a `Scheduler` for a target: load rules from the profile's packs, expand
2
+ `foreach` over the profile's blocks, and wire up the executors, check runner and gate
3
+ evaluator (DESIGN.md 3.3, 3.4).
4
+
5
+ The graph also knows the packs' named targets (`targets/*.yml` in a pack, e.g.
6
+ `rtl:<block>`; `chipgraph.core.engine.targets`), so a target may be one everywhere a rule
7
+ selector may. With `review: {use: github}` the PR gates (`pr:<...>`) are read from the
8
+ pull request's reviews (`GitHubGateChecker`); every other gate, and every gate with the
9
+ `file` adapter, from the decision files (`chipgraph approve`).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from collections.abc import Callable, Iterable, Sequence
16
+ from importlib import resources
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from chipgraph.adapters.review.github import GitHubGateChecker
21
+ from chipgraph.adapters.runtime.claude_code import ClaudeCodeExecutor, ClaudeCodeRuntime
22
+ from chipgraph.app.checks import ProfileCheckRunner
23
+ from chipgraph.app.context import AppContext
24
+ from chipgraph.app.errors import AppError
25
+ from chipgraph.app.executors import HumanExecutor, RunExecutor
26
+ from chipgraph.core.contracts import ArtifactRef, RuleInstance, RuleSpec
27
+ from chipgraph.core.engine.agent_rule import AgentRuleExecutor
28
+ from chipgraph.core.engine.graph import (
29
+ BuildGraph,
30
+ ForeachEntry,
31
+ ForeachResolver,
32
+ GraphError,
33
+ build_graph,
34
+ )
35
+ from chipgraph.core.engine.rules import RuleLoadError, load_pack_rules
36
+ from chipgraph.core.engine.scheduler import AgentStub, Executor, GateChecker, Scheduler
37
+ from chipgraph.core.engine.targets import TargetError, TargetRegistry, load_pack_targets
38
+ from chipgraph.core.plugin_api.pack import Pack, discover_packs
39
+ from chipgraph.core.plugin_api.registry import PluginError
40
+ from chipgraph.core.runtime import TaskQueue
41
+ from chipgraph.core.runtime.roles import RoleError, check_agent_rules, task_denied_reads
42
+ from chipgraph.core.state.trace import Tracer
43
+
44
+ PLAN_MODULES = "plan.modules"
45
+ """The `foreach` selector over the modules of every block's approved plan (M2-03)."""
46
+ PLAN_BLOCK = "plan.block"
47
+ """The `foreach` selector over the blocks with an approved plan (M2-11a)."""
48
+ PLAN_BLOCKS = "plan.blocks"
49
+ """The `foreach` selector over every block, after its approved plan's files (M2-11a)."""
50
+ PLAN_SELECTORS = (PLAN_MODULES, PLAN_BLOCK, PLAN_BLOCKS)
51
+
52
+
53
+ def builtin_packs_dir() -> Path | None:
54
+ """The directory of the packs shipped inside the `chipgraph.packs` package (D36).
55
+
56
+ Found through `importlib.resources`, so it is the same whether chipgraph runs from a
57
+ checkout or is installed from a wheel. `None` if the package is not on a real
58
+ filesystem (e.g. imported from a zip), since packs are loaded from directories.
59
+ """
60
+ root = resources.files("chipgraph.packs")
61
+ return root if isinstance(root, Path) and root.is_dir() else None
62
+
63
+
64
+ def pack_search_paths(ctx: AppContext) -> list[Path]:
65
+ """The search paths packs are discovered from, in order (DESIGN.md 3.2, D36).
66
+
67
+ The project's `.chipgraph/packs/`, then the built-in packs, then each entry of
68
+ `$CHIPGRAPH_PACK_PATH`. Two packs with the same name anywhere on the path are an error.
69
+ """
70
+ return pack_search_paths_for(ctx.root)
71
+
72
+
73
+ def pack_search_paths_for(root: Path) -> list[Path]:
74
+ """`pack_search_paths` for the project at `root`."""
75
+ paths = [root / ".chipgraph" / "packs"]
76
+ builtin = builtin_packs_dir()
77
+ if builtin is not None:
78
+ paths.append(builtin)
79
+ env_value = os.environ.get("CHIPGRAPH_PACK_PATH", "")
80
+ paths.extend(Path(p) for p in env_value.split(os.pathsep) if p)
81
+ return paths
82
+
83
+
84
+ def load_rules(ctx: AppContext) -> list[RuleSpec]:
85
+ """Load every rule from the packs named in the profile's `packs`."""
86
+ return rules_for_packs(ctx.root, ctx.require_profile().profile.packs)
87
+
88
+
89
+ def rules_for_packs(root: Path, packs: Iterable[str]) -> list[RuleSpec]:
90
+ """Load every rule of the packs named `packs`, searched for from the project at `root`."""
91
+ rules: list[RuleSpec] = []
92
+ for pack in _packs(root, packs):
93
+ try:
94
+ rules.extend(load_pack_rules(pack))
95
+ except RuleLoadError as exc:
96
+ raise AppError(str(exc)) from exc
97
+ return rules
98
+
99
+
100
+ def _packs(root: Path, packs: Iterable[str]) -> list[Pack]:
101
+ search_paths = pack_search_paths_for(root)
102
+ try:
103
+ available: dict[str, Pack] = discover_packs(search_paths)
104
+ except PluginError as exc:
105
+ raise AppError(str(exc)) from exc
106
+ found: list[Pack] = []
107
+ for name in packs:
108
+ pack = available.get(name)
109
+ if pack is None:
110
+ searched = ", ".join(str(p) for p in search_paths)
111
+ raise AppError(f"pack {name!r} not found; searched: {searched}")
112
+ found.append(pack)
113
+ return found
114
+
115
+
116
+ def targets_for_packs(root: Path, packs: Iterable[str]) -> TargetRegistry:
117
+ """The named targets of the packs named `packs` (`<pack>/targets/*.yml`)."""
118
+ try:
119
+ return TargetRegistry(
120
+ spec for pack in _packs(root, packs) for spec in load_pack_targets(pack)
121
+ )
122
+ except TargetError as exc:
123
+ raise AppError(str(exc)) from exc
124
+
125
+
126
+ def load_targets(ctx: AppContext) -> TargetRegistry:
127
+ """The named targets of the profile's packs."""
128
+ return targets_for_packs(ctx.root, ctx.require_profile().profile.packs)
129
+
130
+
131
+ def _uses_layout(rules: Iterable[RuleSpec]) -> bool:
132
+ return any(
133
+ "{layout." in text
134
+ for rule in rules
135
+ for text in (*rule.outputs, *(i.selector for i in rule.inputs))
136
+ )
137
+
138
+
139
+ class _ProfileForeach:
140
+ """The project's `ForeachResolver`: `blocks`, and the selectors over approved plans.
141
+
142
+ `blocks` expands to one entry `{"block": b}` per key of `profile.blocks`, sorted,
143
+ with the block's layout directories as template values (`{layout.rtl}`,
144
+ `{layout.tb}`, `chipgraph.packs.digital_rtl.layout`) when a rule uses them. From the
145
+ `digital-rtl` pack: `plan.modules` expands to one entry per module of every block's
146
+ approved plan (each with the module's write set, its dependencies as ordering edges,
147
+ the approved plan and the style files as inputs), `plan.block` to one entry per block
148
+ with an approved plan and `plan.blocks` to one per block (both with the plan's files
149
+ and the testbench as inputs, when it is approved). Any other `foreach` expression
150
+ needs the Design Model, which arrives in M1.
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ ctx: AppContext,
156
+ rules: Sequence[RuleSpec],
157
+ plans: Callable[[], Any] | None = None,
158
+ ) -> None:
159
+ self._ctx = ctx
160
+ self._blocks = sorted(ctx.require_profile().profile.blocks)
161
+ self._layout = _uses_layout(rules)
162
+ self._plans_factory = plans
163
+ self._plans: Any = None
164
+
165
+ def _vars(self, block: str) -> dict[str, str]:
166
+ if not self._layout:
167
+ return {}
168
+ from chipgraph.packs.digital_rtl.layout import LayoutError, layout_vars
169
+
170
+ try:
171
+ return layout_vars(self._ctx.require_profile(), block)
172
+ except LayoutError as exc:
173
+ raise AppError(str(exc)) from exc
174
+
175
+ def expand(self, expr: str) -> list[dict[str, str]]:
176
+ return [dict(entry.params) for entry in self.expand_entries(expr)]
177
+
178
+ def expand_entries(self, expr: str) -> list[ForeachEntry]:
179
+ if expr == "blocks":
180
+ return [
181
+ ForeachEntry(params={"block": block}, vars=self._vars(block))
182
+ for block in self._blocks
183
+ ]
184
+ if expr in PLAN_SELECTORS and self._plans_factory is not None:
185
+ if self._plans is None: # once per graph, however many rules use it
186
+ self._plans = self._plans_factory()
187
+ if expr == PLAN_MODULES:
188
+ return list(self._plans.modules())
189
+ if expr == PLAN_BLOCK:
190
+ return list(self._plans.blocks_with_plan())
191
+ return list(self._plans.every_block())
192
+ raise AppError(f"foreach {expr!r} needs the Design Model (M1)")
193
+
194
+
195
+ def _resolver_for(ctx: AppContext, rules: Iterable[RuleSpec] | None = None) -> ForeachResolver:
196
+ """The project's resolver for a graph of `rules` (default: the profile's packs' rules)."""
197
+ rules = load_rules(ctx) if rules is None else list(rules)
198
+ plans: Callable[[], Any] | None = None
199
+ if any(rule.foreach in PLAN_SELECTORS for rule in rules):
200
+ from chipgraph.packs.digital_rtl.plan.resolver import PlanExpansion
201
+
202
+ def _plans() -> PlanExpansion:
203
+ return PlanExpansion(ctx, rules)
204
+
205
+ plans = _plans
206
+ resolver: ForeachResolver = _ProfileForeach(ctx, rules, plans)
207
+ from chipgraph.packs.digital_rtl.wrapper.resolver import SELECTORS, WrapperForeach
208
+
209
+ if any(rule.foreach in SELECTORS for rule in rules): # `wrappers` (M2-12)
210
+ resolver = WrapperForeach(resolver, ctx.require_profile().profile)
211
+ return resolver
212
+
213
+
214
+ def project_graph(ctx: AppContext, rules: Sequence[RuleSpec] | None = None) -> BuildGraph:
215
+ """The project's build graph: the packs' rules, expanded, with the packs' targets."""
216
+ rules = load_rules(ctx) if rules is None else list(rules)
217
+ try:
218
+ return build_graph(
219
+ rules, _resolver_for(ctx, rules), repo=ctx.store.repo, targets=load_targets(ctx)
220
+ )
221
+ except GraphError as exc:
222
+ raise AppError(str(exc)) from exc
223
+
224
+
225
+ def graph_artifacts(graph: BuildGraph) -> list[ArtifactRef]:
226
+ """Every input and output of every instance of `graph`."""
227
+ return [ref for inst in graph.instances.values() for ref in (*inst.inputs, *inst.outputs)]
228
+
229
+
230
+ def denied_reads_for(
231
+ ctx: AppContext, graph: BuildGraph | None, role: str | None, keep: Iterable[str] = ()
232
+ ) -> tuple[str, ...]:
233
+ """What a task of `role` must not read in this project (`task_denied_reads`).
234
+
235
+ The one computation both runtimes use: the in-process loop filters the redo text by
236
+ it, runtime `claude-code` sets it on the task for the guard and `submit`.
237
+ """
238
+ return task_denied_reads(
239
+ role,
240
+ artifacts=graph_artifacts(graph) if graph is not None else (),
241
+ profile=ctx.require_profile().profile,
242
+ keep=keep,
243
+ )
244
+
245
+
246
+ def agent_executor(ctx: AppContext, graph: BuildGraph | None = None) -> Executor:
247
+ """The executor for `kind: agent` rules, chosen by the profile's `runtime` (D35).
248
+
249
+ `claude-code` queues each agent task for the user's Claude Code session (the plugin
250
+ command runs it and hands it back over MCP). Any other runtime runs in-process: when
251
+ an `AgentRuntime` plugin is registered under the profile's runtime name, its agent
252
+ rules run in the engine's loop (`AgentRuleExecutor`: write, check at once, triage,
253
+ retry within the budget, DESIGN.md 5.2). No such plugin ships yet (the API runtimes
254
+ arrive later), so without one agent rules fail with the scheduler's `AgentStub`.
255
+ """
256
+ profile = ctx.require_profile().profile
257
+ if profile.runtime == "claude-code":
258
+ return ClaudeCodeExecutor(ClaudeCodeRuntime(TaskQueue(ctx.layout), ctx.store))
259
+ if profile.runtime in ctx.registry.names("runtime"):
260
+ runtime = ctx.registry.get("runtime", profile.runtime)
261
+
262
+ def denied(rule: RuleSpec, instance: RuleInstance) -> tuple[str, ...]:
263
+ keep = [ref.path for ref in instance.outputs if ref.path is not None]
264
+ return denied_reads_for(ctx, graph, rule.role, keep)
265
+
266
+ return AgentRuleExecutor(runtime, store=ctx.store, layout=ctx.layout, denied_reads=denied)
267
+ return AgentStub()
268
+
269
+
270
+ def gate_checker(ctx: AppContext) -> GateChecker:
271
+ """The gates of this project: PR gates from GitHub reviews with `review: github`.
272
+
273
+ With `review: {use: github}` a gate whose id starts with `pr:` is the pull request's
274
+ review (`GitHubGateChecker`, pinned to the PR's head commit); any other gate, and
275
+ every gate otherwise, is the decision files' (`GateEvaluator`, `chipgraph approve`).
276
+ """
277
+ if ctx.pr_review is not None:
278
+ return GitHubGateChecker(ctx.pr_review, fallback=ctx.gates)
279
+ return ctx.gates
280
+
281
+
282
+ def make_scheduler(
283
+ ctx: AppContext, target: str, *, concurrency: int = 4, tracer: Tracer | None = None
284
+ ) -> Scheduler:
285
+ """Build a `Scheduler` ready to run/resume `target` for this project."""
286
+ graph = project_graph(ctx)
287
+ try:
288
+ graph.select(target)
289
+ except GraphError as exc:
290
+ raise AppError(str(exc)) from exc
291
+ try: # every agent rule must fit its role's tool table (DESIGN.md 5.1)
292
+ check_agent_rules(graph.rules, graph.instances.values())
293
+ except RoleError as exc:
294
+ raise AppError(str(exc)) from exc
295
+
296
+ run_executor = RunExecutor(ctx)
297
+ executors = {
298
+ "gen": run_executor,
299
+ "import": run_executor,
300
+ "human": HumanExecutor(ctx),
301
+ "agent": agent_executor(ctx, graph),
302
+ }
303
+ return Scheduler(
304
+ graph,
305
+ layout=ctx.layout,
306
+ store=ctx.store,
307
+ executors=executors, # type: ignore[arg-type]
308
+ checks=ProfileCheckRunner(ctx),
309
+ gates=gate_checker(ctx),
310
+ concurrency=concurrency,
311
+ tracer=tracer,
312
+ )
313
+
314
+
315
+ __all__ = [
316
+ "PLAN_BLOCK",
317
+ "PLAN_BLOCKS",
318
+ "PLAN_MODULES",
319
+ "PLAN_SELECTORS",
320
+ "agent_executor",
321
+ "denied_reads_for",
322
+ "gate_checker",
323
+ "graph_artifacts",
324
+ "load_rules",
325
+ "load_targets",
326
+ "make_scheduler",
327
+ "pack_search_paths",
328
+ "pack_search_paths_for",
329
+ "project_graph",
330
+ "rules_for_packs",
331
+ "targets_for_packs",
332
+ ]