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,283 @@
1
+ """Planning `chipgraph baseline` (DESIGN.md 6.4): what to record for an existing project.
2
+
3
+ A project that already carries reviewed specs, RTL and contracts (merged through PRs)
4
+ has no chipgraph decisions yet, so the very first build would stop at every spec gate.
5
+ `baseline` proposes treating the artifacts on the main branch as *approved at their
6
+ current hash*, records one `baseline` decision per undecided gate, and notes the open
7
+ findings so they are recorded without blocking the build.
8
+
9
+ This module is the generic, side-effect-free half of that command: given the gates in
10
+ the graph, the artifacts each gate covers, and per-artifact version-control facts, it
11
+ produces a deterministic `BaselinePlan`. It knows nothing about git, the review store or
12
+ the finding store (the app layer supplies those facts and applies the plan); it only
13
+ depends on `chipgraph.core.contracts` and is checked under `mypy --strict`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterable, Mapping
19
+ from typing import Literal, Protocol
20
+
21
+ from pydantic import BaseModel, ConfigDict, Field
22
+
23
+ from chipgraph.core.contracts import ArtifactRef, RuleInstance
24
+
25
+ __all__ = [
26
+ "ArtifactStatus",
27
+ "BaselineArtifact",
28
+ "BaselineGate",
29
+ "BaselinePlan",
30
+ "CommitInfo",
31
+ "GateInput",
32
+ "VcsFacts",
33
+ "clean_refs_for_gate",
34
+ "gate_inputs_from_instances",
35
+ "plan_baseline",
36
+ ]
37
+
38
+ ArtifactStatus = Literal["clean", "dirty", "untracked", "missing"]
39
+ """How an artifact stands relative to the main branch.
40
+
41
+ - ``clean``: tracked and unmodified in the working tree -> can be baselined.
42
+ - ``dirty``: tracked but modified in the working tree -> not baselined.
43
+ - ``untracked``: on disk but not tracked by version control -> not baselined.
44
+ - ``missing``: a declared input that does not exist -> nothing to baseline.
45
+ """
46
+
47
+
48
+ class CommitInfo(BaseModel):
49
+ """The identifying facts of a commit, as shown in a baseline listing."""
50
+
51
+ model_config = ConfigDict(extra="forbid", frozen=True)
52
+
53
+ short_sha: str = Field(description="The commit's abbreviated sha.")
54
+ date: str = Field(description="The commit's author date, ISO-8601 (YYYY-MM-DD).")
55
+ subject: str = Field(description="The commit's subject line.")
56
+ is_merge: bool = Field(default=False, description="Whether this is a merge/PR commit.")
57
+
58
+
59
+ class VcsFacts(Protocol):
60
+ """The version-control facts `plan_baseline` needs about one repo-relative path.
61
+
62
+ The app layer implements this over the git adapter and the working tree; the core
63
+ only reads it, so it never shells out or imports an adapter.
64
+ """
65
+
66
+ def sha256(self, path: str) -> str | None:
67
+ """Current content sha256 (hex) of `path`, or `None` if it is not on disk."""
68
+ ...
69
+
70
+ def is_tracked(self, path: str) -> bool:
71
+ """Whether `path` is tracked by version control on the current branch."""
72
+ ...
73
+
74
+ def is_dirty(self, path: str) -> bool:
75
+ """Whether `path` is modified in the working tree relative to the current commit."""
76
+ ...
77
+
78
+ def last_commit(self, path: str) -> CommitInfo | None:
79
+ """The last commit that touched `path`, or `None` if there is none."""
80
+ ...
81
+
82
+
83
+ class GateInput(BaseModel):
84
+ """One gate to plan a baseline for, with the artifacts it covers.
85
+
86
+ `artifacts` are the gated instance's file-based inputs (the things a person
87
+ reviewed); `already_decided` says the gate already has a current decision, so the
88
+ baseline skips it.
89
+ """
90
+
91
+ model_config = ConfigDict(extra="forbid", frozen=True)
92
+
93
+ gate_id: str = Field(description="The formatted gate id, e.g. 'spec:timer'.")
94
+ instance_id: str = Field(description="The rule instance the gate is evaluated for.")
95
+ artifacts: tuple[ArtifactRef, ...] = Field(
96
+ default=(), description="The gate's covered artifacts (the instance's file inputs)."
97
+ )
98
+ already_decided: bool = Field(
99
+ default=False, description="Whether the gate already has a current decision."
100
+ )
101
+
102
+
103
+ class BaselineArtifact(BaseModel):
104
+ """One artifact covered by a gate, with its version-control facts for the listing."""
105
+
106
+ model_config = ConfigDict(extra="forbid", frozen=True)
107
+
108
+ path: str = Field(description="The artifact's repo-relative path.")
109
+ sha256: str | None = Field(
110
+ default=None, description="Current content sha256 (hex), or None if missing on disk."
111
+ )
112
+ status: ArtifactStatus = Field(description="'clean', 'dirty', 'untracked' or 'missing'.")
113
+ last_commit: CommitInfo | None = Field(
114
+ default=None, description="The last commit that touched this artifact, if any."
115
+ )
116
+
117
+ @property
118
+ def short_sha256(self) -> str:
119
+ """The first 12 chars of the content sha, or '-' when the file is missing."""
120
+ return self.sha256[:12] if self.sha256 else "-"
121
+
122
+ @property
123
+ def baselineable(self) -> bool:
124
+ """Whether this artifact is on the main branch and so can be baselined."""
125
+ return self.status == "clean" and self.sha256 is not None
126
+
127
+
128
+ class BaselineGate(BaseModel):
129
+ """A gate in the plan: its artifacts, whether it is skipped, and what will be baselined."""
130
+
131
+ model_config = ConfigDict(extra="forbid", frozen=True)
132
+
133
+ gate_id: str = Field(description="The formatted gate id.")
134
+ instance_id: str = Field(description="The rule instance the gate is evaluated for.")
135
+ already_decided: bool = Field(description="Whether the gate already has a current decision.")
136
+ artifacts: tuple[BaselineArtifact, ...] = Field(
137
+ default=(), description="The gate's covered artifacts, sorted by path."
138
+ )
139
+
140
+ @property
141
+ def clean_artifacts(self) -> tuple[BaselineArtifact, ...]:
142
+ """The gate's clean, tracked artifacts: what a baseline decision would pin."""
143
+ return tuple(a for a in self.artifacts if a.baselineable)
144
+
145
+ @property
146
+ def will_baseline(self) -> bool:
147
+ """Whether ``--confirm`` would record a decision for this gate.
148
+
149
+ True only when the gate is undecided and has at least one clean, tracked
150
+ artifact to pin the decision to.
151
+ """
152
+ return not self.already_decided and bool(self.clean_artifacts)
153
+
154
+
155
+ class BaselinePlan(BaseModel):
156
+ """The full, deterministic plan a `baseline` run acts on (or just prints, when dry)."""
157
+
158
+ model_config = ConfigDict(extra="forbid", frozen=True)
159
+
160
+ gates: tuple[BaselineGate, ...] = Field(
161
+ default=(), description="Every gate found, sorted by gate id then instance id."
162
+ )
163
+ open_findings: int = Field(
164
+ default=0, ge=0, description="Count of currently open findings, recorded at baseline."
165
+ )
166
+
167
+ @property
168
+ def gates_to_baseline(self) -> tuple[BaselineGate, ...]:
169
+ """The undecided gates with clean artifacts that ``--confirm`` will record."""
170
+ return tuple(g for g in self.gates if g.will_baseline)
171
+
172
+ @property
173
+ def skipped_gates(self) -> tuple[BaselineGate, ...]:
174
+ """The gates a baseline skips: already decided."""
175
+ return tuple(g for g in self.gates if g.already_decided)
176
+
177
+ @property
178
+ def dirty_artifacts(self) -> tuple[BaselineArtifact, ...]:
179
+ """Every distinct dirty/untracked artifact across all gates, sorted by path.
180
+
181
+ These are *not* baselined: a baseline is only for what is on the main branch.
182
+ """
183
+ seen: dict[str, BaselineArtifact] = {}
184
+ for gate in self.gates:
185
+ for artifact in gate.artifacts:
186
+ if artifact.status in ("dirty", "untracked") and artifact.path not in seen:
187
+ seen[artifact.path] = artifact
188
+ return tuple(seen[path] for path in sorted(seen))
189
+
190
+
191
+ def _file_inputs(instance: RuleInstance) -> tuple[ArtifactRef, ...]:
192
+ """The instance's file-based inputs: what a gate over it covers (DESIGN.md 6.1)."""
193
+ return tuple(ref for ref in instance.inputs if ref.path is not None)
194
+
195
+
196
+ def gate_inputs_from_instances(
197
+ gated: Iterable[tuple[str, RuleInstance, bool]],
198
+ ) -> list[GateInput]:
199
+ """Build `GateInput`s from `(gate_id, instance, already_decided)` triples.
200
+
201
+ A small convenience for the app layer: each gate covers its instance's file-based
202
+ inputs. Instances with no file inputs still produce a `GateInput` (with no
203
+ artifacts), so the listing can show a gate that cannot be baselined.
204
+ """
205
+ return [
206
+ GateInput(
207
+ gate_id=gate_id,
208
+ instance_id=instance.instance_id,
209
+ artifacts=_file_inputs(instance),
210
+ already_decided=already_decided,
211
+ )
212
+ for gate_id, instance, already_decided in gated
213
+ ]
214
+
215
+
216
+ def _artifact_status(path: str, facts: VcsFacts) -> ArtifactStatus:
217
+ if facts.sha256(path) is None:
218
+ return "missing"
219
+ if not facts.is_tracked(path):
220
+ return "untracked"
221
+ if facts.is_dirty(path):
222
+ return "dirty"
223
+ return "clean"
224
+
225
+
226
+ def plan_baseline(
227
+ gates: Iterable[GateInput], facts: VcsFacts, *, open_findings: int = 0
228
+ ) -> BaselinePlan:
229
+ """Turn gate inputs and version-control facts into a deterministic `BaselinePlan`.
230
+
231
+ For every gate, each covered artifact is resolved to its current sha256, its status
232
+ ('clean'/'dirty'/'untracked') and the last commit that touched it. Gates are sorted
233
+ by `(gate_id, instance_id)` and artifacts by path, so the plan (and any listing or
234
+ JSON built from it) is stable regardless of input order.
235
+ """
236
+ planned: list[BaselineGate] = []
237
+ for gate in sorted(gates, key=lambda g: (g.gate_id, g.instance_id)):
238
+ artifacts = tuple(
239
+ _plan_artifact(ref, facts)
240
+ for ref in sorted(gate.artifacts, key=_ref_path)
241
+ if ref.path is not None
242
+ )
243
+ planned.append(
244
+ BaselineGate(
245
+ gate_id=gate.gate_id,
246
+ instance_id=gate.instance_id,
247
+ already_decided=gate.already_decided,
248
+ artifacts=artifacts,
249
+ )
250
+ )
251
+ return BaselinePlan(gates=tuple(planned), open_findings=open_findings)
252
+
253
+
254
+ def _ref_path(ref: ArtifactRef) -> str:
255
+ return ref.path or ""
256
+
257
+
258
+ def _plan_artifact(ref: ArtifactRef, facts: VcsFacts) -> BaselineArtifact:
259
+ assert ref.path is not None # guarded by the caller
260
+ path = ref.path
261
+ status = _artifact_status(path, facts)
262
+ return BaselineArtifact(
263
+ path=path,
264
+ sha256=facts.sha256(path),
265
+ status=status,
266
+ last_commit=facts.last_commit(path),
267
+ )
268
+
269
+
270
+ def clean_refs_for_gate(
271
+ gate: BaselineGate, by_path: Mapping[str, ArtifactRef]
272
+ ) -> tuple[ArtifactRef, ...]:
273
+ """The `ArtifactRef`s of a gate's clean artifacts, looked up in `by_path`.
274
+
275
+ The app layer keeps the original `ArtifactRef`s (with repo and label) keyed by
276
+ path; this returns the ones a baseline decision should pin, in the plan's order.
277
+ """
278
+ refs: list[ArtifactRef] = []
279
+ for artifact in gate.clean_artifacts:
280
+ ref = by_path.get(artifact.path)
281
+ if ref is not None:
282
+ refs.append(ref)
283
+ return tuple(refs)