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,383 @@
1
+ """`/audit` (task M1-20): run every deterministic check over the whole project.
2
+
3
+ `run_audit` runs, once, every check the project's profile configures -- the same
4
+ per-block / chip-wide split as `chipgraph check` -- through the shared
5
+ `chipgraph.app.checks.ProfileCheckRunner`, so their findings land in the finding store
6
+ exactly as any other check run would. It then reads the store back and builds an
7
+ `AuditReport` that groups every finding by DESIGN.md 4.8 layer and severity, records each
8
+ check's status (so a check that *errored*, e.g. a missing tool, is visible, not silent),
9
+ and folds in the ingest issues it ran first (the model checks need a model).
10
+
11
+ The report is pure data (pydantic, JSON-dumpable, deterministic order) and never touches
12
+ the repo tree: `run_audit` writes only machine-local state (the finding store and the
13
+ model cache). `chipgraph audit` prints it and the `audit` MCP tool returns it; `chipgraph
14
+ try` (M1-21) reuses `run_audit` as its read-only whole-project pass.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ from typing import Literal
21
+
22
+ from pydantic import BaseModel, ConfigDict, Field
23
+
24
+ from chipgraph.app.baseline import baseline_gate_id
25
+ from chipgraph.app.checks import ProfileCheckRunner
26
+ from chipgraph.app.context import AppContext
27
+ from chipgraph.app.findings import finding_current_hashes, layer_for_check
28
+ from chipgraph.app.ingest import run_ingest
29
+ from chipgraph.core.contracts import ArtifactRef, CheckResult, RuleInstance
30
+ from chipgraph.core.contracts.finding import Finding, FindingSeverity
31
+ from chipgraph.core.state.findings import FindingStore, effective_status, waiver_gate_id
32
+
33
+ # The layers whose *open* errors block a strict audit (DESIGN.md 4.8: only deterministic
34
+ # layers may report `error`; a strict `/audit` treats layers 1 and 5 as hard, and layer 4
35
+ # only for lint errors -- a design "smell" that is a real error, not a warning).
36
+ _HARD_LAYERS = frozenset({1, 5})
37
+ _LINT_LAYER = 4
38
+ # A lint error is reported by the `lint` check (`check:lint`) or, in principle, directly
39
+ # by the pyslang extractor (`lint`); either counts as a blocking layer-4 error.
40
+ _LINT_SOURCES = frozenset({"lint", "check:lint"})
41
+
42
+ _SEVERITY_ORDER = {"error": 0, "warning": 1, "question": 2}
43
+ _STATUS_ORDER = {"open": 0, "waived": 1, "baselined": 2, "fixed": 3}
44
+
45
+ # A finding's audit status is its effective status, plus `"baselined"`: an open finding
46
+ # with a current `baseline` decision (DESIGN.md 6.4 -- recorded, listed, never blocking).
47
+ AuditFindingStatus = Literal["open", "waived", "baselined", "fixed"]
48
+
49
+
50
+ class AuditFinding(BaseModel):
51
+ """One finding as the audit reports it: its layer, severity, source and status."""
52
+
53
+ model_config = ConfigDict(extra="forbid", frozen=True)
54
+
55
+ id: str = Field(description="The finding id, 'F-<8 hex>'.")
56
+ layer: int = Field(ge=1, le=5, description="DESIGN.md 4.8 layer (1..5).")
57
+ severity: FindingSeverity = Field(description="How serious the finding is.")
58
+ source: str = Field(description="Where it came from, e.g. 'check:cross_chip'.")
59
+ status: AuditFindingStatus = Field(description="Audit status (adds 'baselined').")
60
+ evidence: str = Field(description="First evidence location: 'file:line' or 'model:<key>'.")
61
+ claim: str = Field(description="What is wrong, in plain language.")
62
+ blocking: bool = Field(description="Whether this finding blocks a strict audit.")
63
+
64
+
65
+ class LayerReport(BaseModel):
66
+ """Every finding at one layer, with counts by severity and status."""
67
+
68
+ model_config = ConfigDict(extra="forbid", frozen=True)
69
+
70
+ layer: int = Field(ge=1, le=5, description="The DESIGN.md 4.8 layer.")
71
+ by_severity: dict[str, int] = Field(
72
+ default_factory=dict, description="Finding count keyed by severity."
73
+ )
74
+ by_status: dict[str, int] = Field(
75
+ default_factory=dict, description="Finding count keyed by audit status."
76
+ )
77
+ open_errors: int = Field(default=0, description="Open (not waived/baselined) errors here.")
78
+ blocking: int = Field(
79
+ default=0, description="Findings at this layer that block a strict audit."
80
+ )
81
+ findings: tuple[AuditFinding, ...] = Field(
82
+ default=(), description="The findings at this layer, ordered."
83
+ )
84
+
85
+
86
+ class CheckReport(BaseModel):
87
+ """One entry per check that was run: its status and how many issues it raised.
88
+
89
+ A check that errored (`status='error'`, e.g. a missing tool or a missing model) is
90
+ listed here so it is visible rather than silently contributing no findings.
91
+ """
92
+
93
+ model_config = ConfigDict(extra="forbid", frozen=True)
94
+
95
+ check_id: str = Field(description="The configured check id.")
96
+ block: str | None = Field(
97
+ default=None, description="The block it ran for, or None (chip-wide)."
98
+ )
99
+ status: Literal["pass", "fail", "error", "skipped"] = Field(description="The check's outcome.")
100
+ layer: int = Field(ge=1, le=5, description="The layer its findings belong to.")
101
+ issues: int = Field(ge=0, description="How many issues the check raised.")
102
+
103
+
104
+ class IngestSection(BaseModel):
105
+ """The ingest run's own issues (the model checks need a model): counts and errors."""
106
+
107
+ model_config = ConfigDict(extra="forbid", frozen=True)
108
+
109
+ ran: bool = Field(description="Whether ingest was run at all.")
110
+ by_severity: dict[str, int] = Field(
111
+ default_factory=dict, description="Ingest issue count keyed by severity."
112
+ )
113
+ errors: tuple[str, ...] = Field(
114
+ default=(), description="The error-severity ingest issues, as messages."
115
+ )
116
+
117
+
118
+ class AuditReport(BaseModel):
119
+ """The whole-project audit: findings grouped by layer, per-check status, ingest section."""
120
+
121
+ model_config = ConfigDict(extra="forbid", frozen=True)
122
+
123
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
124
+ ingest: IngestSection = Field(description="The ingest run's issues (run first).")
125
+ checks: tuple[CheckReport, ...] = Field(
126
+ default=(), description="Per-check status, in run order."
127
+ )
128
+ layers: tuple[LayerReport, ...] = Field(
129
+ default=(), description="One entry per non-empty layer, ordered 1..5."
130
+ )
131
+ open_errors_by_layer: dict[str, int] = Field(
132
+ default_factory=dict, description="Open error count keyed by layer number (as a string)."
133
+ )
134
+ blocking: int = Field(default=0, description="Total findings that block a strict audit.")
135
+
136
+ @property
137
+ def has_blocking(self) -> bool:
138
+ """Whether any finding blocks a strict audit (open errors in layers 1/5, lint in 4)."""
139
+ return self.blocking > 0
140
+
141
+ def summary_line(self) -> str:
142
+ """A one-line summary: open errors per layer and the blocking count."""
143
+ if self.open_errors_by_layer:
144
+ parts = ", ".join(
145
+ f"L{layer}={self.open_errors_by_layer[layer]}"
146
+ for layer in sorted(self.open_errors_by_layer, key=int)
147
+ )
148
+ errors = f"open errors: {parts}"
149
+ else:
150
+ errors = "open errors: none"
151
+ return f"{errors} blocking: {self.blocking}"
152
+
153
+
154
+ def _check_combos(
155
+ runner: ProfileCheckRunner, ctx: AppContext, blocks: list[str] | None
156
+ ) -> list[tuple[str, str | None]]:
157
+ """The (check_id, block) combos to run, mirroring `chipgraph check`'s own rule.
158
+
159
+ A chip-wide check (`per_block=False`) runs once for the whole project, unless the
160
+ caller scoped the run to specific `blocks`; running it per block would repeat its
161
+ issues and drop those that belong to no block.
162
+ """
163
+ resolved = ctx.require_profile()
164
+ check_ids = sorted(resolved.profile.adapters)
165
+ scoped = blocks is not None
166
+ raw_blocks = list(blocks) if blocks is not None else sorted(resolved.profile.blocks)
167
+ block_list: list[str | None] = list(raw_blocks) if raw_blocks else [None]
168
+ return [
169
+ (cid, b)
170
+ for cid in check_ids
171
+ for b in (block_list if scoped or runner.runs_per_block(cid) else [None])
172
+ ]
173
+
174
+
175
+ def _fake_instance(check_id: str, block: str | None) -> RuleInstance:
176
+ """A throwaway `RuleInstance` carrying just the params `ProfileCheckRunner` reads.
177
+
178
+ Identical to `chipgraph.cli._fake_instance` (private there); `/audit` runs checks
179
+ directly, outside the build graph, so there is no real rule instance to pass.
180
+ """
181
+ params = {"block": block} if block is not None else {}
182
+ rule_id = "chipgraph/audit"
183
+ safe = "".join(c if c.isalnum() else "_" for c in check_id)
184
+ output_path = f".chipgraph/tmp/audit-{safe}-{block or 'all'}.json"
185
+ return RuleInstance(
186
+ rule_id=rule_id,
187
+ params=params,
188
+ outputs=(ArtifactRef(kind="report", path=output_path),),
189
+ instance_id=RuleInstance.make_id(rule_id, params),
190
+ )
191
+
192
+
193
+ async def _run_check_combos(
194
+ runner: ProfileCheckRunner, combos: list[tuple[str, str | None]]
195
+ ) -> list[CheckReport]:
196
+ reports: list[CheckReport] = []
197
+ for check_id, block in combos:
198
+ instance = _fake_instance(check_id, block)
199
+ result: CheckResult = await runner.run(check_id, instance)
200
+ reports.append(
201
+ CheckReport(
202
+ check_id=check_id,
203
+ block=block,
204
+ status=result.status,
205
+ layer=layer_for_check(check_id, result.check_id),
206
+ issues=len(result.issues),
207
+ )
208
+ )
209
+ return reports
210
+
211
+
212
+ def _audit_status(finding: Finding, ctx: AppContext) -> tuple[AuditFindingStatus, bool]:
213
+ """A finding's audit status and whether it blocks a strict audit.
214
+
215
+ Its effective status (`open`/`waived`/`fixed`) is computed against its current
216
+ artifact hashes; an open finding is reported `"baselined"` instead when it has a
217
+ current `baseline` decision (DESIGN.md 6.4). Only an `open` (not waived, not
218
+ baselined) `error` in a hard layer -- layers 1 and 5, or a lint error in layer 4 --
219
+ is blocking.
220
+ """
221
+ waivers = ctx.review.approvals(waiver_gate_id(finding.id))
222
+ current_hashes = finding_current_hashes(ctx.store, finding, waivers)
223
+ status: AuditFindingStatus = effective_status(finding, waivers, current_hashes)
224
+ if status == "open" and _is_baselined(finding, ctx, current_hashes):
225
+ status = "baselined"
226
+ blocking = (
227
+ status == "open"
228
+ and finding.severity == "error"
229
+ and (
230
+ finding.layer in _HARD_LAYERS
231
+ or (finding.layer == _LINT_LAYER and finding.source in _LINT_SOURCES)
232
+ )
233
+ )
234
+ return status, blocking
235
+
236
+
237
+ def _is_baselined(finding: Finding, ctx: AppContext, current_hashes: dict[str, str]) -> bool:
238
+ """Whether `finding` has a current `baseline` decision bound to its current hashes.
239
+
240
+ Mirrors `chipgraph.app.baseline._baseline_open_findings`: a whole-check finding with
241
+ no artifacts is baselined against its detection-time hashes instead.
242
+ """
243
+ hashes = dict(current_hashes) or dict(finding.artifact_hashes)
244
+ if not hashes:
245
+ return False
246
+ decisions = ctx.review.approvals(baseline_gate_id(finding.id))
247
+ return any(a.decision == "baseline" and a.is_current(hashes) for a in decisions)
248
+
249
+
250
+ def _first_evidence(finding: Finding) -> str:
251
+ ev = finding.evidence[0]
252
+ if ev.file is not None:
253
+ return f"{ev.file}:{ev.line}" if ev.line is not None else ev.file
254
+ return f"model:{ev.model_key}"
255
+
256
+
257
+ def _finding_sort_key(af: AuditFinding) -> tuple[int, int, str, str]:
258
+ return (
259
+ _SEVERITY_ORDER.get(af.severity, 9),
260
+ _STATUS_ORDER.get(af.status, 9),
261
+ af.source,
262
+ af.id,
263
+ )
264
+
265
+
266
+ def _build_layers(ctx: AppContext) -> tuple[list[LayerReport], dict[str, int], int]:
267
+ """Read the finding store and group every finding by layer, then severity/status."""
268
+ store = FindingStore(ctx.layout)
269
+ by_layer: dict[int, list[AuditFinding]] = {}
270
+ for finding in store.list():
271
+ status, blocking = _audit_status(finding, ctx)
272
+ af = AuditFinding(
273
+ id=finding.id,
274
+ layer=finding.layer,
275
+ severity=finding.severity,
276
+ source=finding.source,
277
+ status=status,
278
+ evidence=_first_evidence(finding),
279
+ claim=finding.claim,
280
+ blocking=blocking,
281
+ )
282
+ by_layer.setdefault(finding.layer, []).append(af)
283
+
284
+ layers: list[LayerReport] = []
285
+ open_errors_by_layer: dict[str, int] = {}
286
+ total_blocking = 0
287
+ for layer in sorted(by_layer):
288
+ items = sorted(by_layer[layer], key=_finding_sort_key)
289
+ by_severity: dict[str, int] = {}
290
+ by_status: dict[str, int] = {}
291
+ open_errors = 0
292
+ layer_blocking = 0
293
+ for af in items:
294
+ by_severity[af.severity] = by_severity.get(af.severity, 0) + 1
295
+ by_status[af.status] = by_status.get(af.status, 0) + 1
296
+ if af.status == "open" and af.severity == "error":
297
+ open_errors += 1
298
+ if af.blocking:
299
+ layer_blocking += 1
300
+ if open_errors:
301
+ open_errors_by_layer[str(layer)] = open_errors
302
+ total_blocking += layer_blocking
303
+ layers.append(
304
+ LayerReport(
305
+ layer=layer,
306
+ by_severity=dict(sorted(by_severity.items())),
307
+ by_status=dict(sorted(by_status.items())),
308
+ open_errors=open_errors,
309
+ blocking=layer_blocking,
310
+ findings=tuple(items),
311
+ )
312
+ )
313
+ return layers, open_errors_by_layer, total_blocking
314
+
315
+
316
+ def run_audit(
317
+ ctx: AppContext, *, ingest: bool = True, blocks: list[str] | None = None
318
+ ) -> AuditReport:
319
+ """Run every deterministic check over the whole project and group the findings.
320
+
321
+ Steps (DESIGN.md 4.8, 6.4):
322
+
323
+ 1. If `ingest`, run `run_ingest(ctx)` first (the model checks need a model); its
324
+ issues are kept as their own report section (counts by severity + the errors).
325
+ 2. Run every check the profile's `adapters` configures over the whole project -- the
326
+ same per-block / chip-wide split as `chipgraph check` -- through
327
+ `ProfileCheckRunner`, so findings are recorded in the store as usual. When
328
+ `blocks` is given, only those blocks are run (a chip-wide check runs once per
329
+ requested block, matching `chipgraph check --block`).
330
+ 3. Build the report from the finding store: every finding with its layer, severity,
331
+ source, evidence, claim and audit status, grouped by layer then severity, plus
332
+ per-check status so a check that errored is visible.
333
+ 4. A finding is "blocking" when it is an open (not waived, not baselined) error in a
334
+ hard layer (1 or 5) or an open lint error in layer 4. Baselined findings are
335
+ listed but never blocking (DESIGN.md 6.4).
336
+
337
+ Requires a loaded profile (raises `AppError` otherwise). Writes only machine-local
338
+ state (the finding store and the model cache), never files in the repo tree.
339
+ """
340
+ ctx.require_profile()
341
+
342
+ ingest_section = _run_ingest_section(ctx) if ingest else IngestSection(ran=False)
343
+
344
+ runner = ProfileCheckRunner(ctx)
345
+ combos = _check_combos(runner, ctx, blocks)
346
+ check_reports = asyncio.run(_run_check_combos(runner, combos))
347
+
348
+ layers, open_errors_by_layer, total_blocking = _build_layers(ctx)
349
+
350
+ return AuditReport(
351
+ ingest=ingest_section,
352
+ checks=tuple(check_reports),
353
+ layers=tuple(layers),
354
+ open_errors_by_layer=open_errors_by_layer,
355
+ blocking=total_blocking,
356
+ )
357
+
358
+
359
+ def _run_ingest_section(ctx: AppContext) -> IngestSection:
360
+ report = run_ingest(ctx)
361
+ by_severity: dict[str, int] = {}
362
+ errors: list[str] = []
363
+ for issue in report.issues:
364
+ by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1
365
+ if issue.severity == "error":
366
+ loc = f"{issue.file}:{issue.line}" if issue.file else (issue.block or "")
367
+ errors.append(f"{loc} {issue.message}".strip() if loc else issue.message)
368
+ return IngestSection(
369
+ ran=True,
370
+ by_severity=dict(sorted(by_severity.items())),
371
+ errors=tuple(errors),
372
+ )
373
+
374
+
375
+ __all__ = [
376
+ "AuditFinding",
377
+ "AuditFindingStatus",
378
+ "AuditReport",
379
+ "CheckReport",
380
+ "IngestSection",
381
+ "LayerReport",
382
+ "run_audit",
383
+ ]
@@ -0,0 +1,13 @@
1
+ schema_version: 1
2
+ name: assist
3
+ version: 0.1.0
4
+ description: >-
5
+ Whole-project assistance commands. Ships `/audit`: run every deterministic check
6
+ over the whole project and group the findings by layer (DESIGN.md 4.8) and severity;
7
+ `/ask`: answer a question from the Design Model and the project's documents, only
8
+ with checked citations; and `/triage`: classify a failing log as infra, rtl, tb or spec.
9
+ provides:
10
+ commands:
11
+ - /audit
12
+ - /ask
13
+ - /triage
@@ -0,0 +1,64 @@
1
+ """`/triage` (task M1-14): classify a failing lint/sim/check log as infra, rtl, tb or spec.
2
+
3
+ - `parse` reads the log with the existing log parsers: issues (file, line, rule, message),
4
+ `chipgraph check` results, a simulation's self-check failures, the first error, an
5
+ excerpt.
6
+ - `rules` are the deterministic first tier of `decide()`: generic evidence only (a missing
7
+ tool or file, a time limit, a failing spec cross check, errors that all point into
8
+ testbenches or all into RTL). A simulation mismatch never matches a rule.
9
+ - `question` builds the `decide()` question (choices `infra|rtl|tb|spec`) with the
10
+ excerpt, the issues and, for a simulation failure, the spec lines `/ask` retrieves.
11
+ - `report` writes the deterministic summary, the suggestion per label and the evidence.
12
+ - `run` (`run_triage`, `triage_log`) ties them together behind `chipgraph triage`, the
13
+ `triage` MCP tool and `/chipgraph:triage`.
14
+ """
15
+
16
+ from chipgraph.packs.assist.triage.contract import (
17
+ LABELS,
18
+ SpecLine,
19
+ TriageLabel,
20
+ TriageReport,
21
+ TriageStatus,
22
+ )
23
+ from chipgraph.packs.assist.triage.parse import CheckRun, ParsedLog, parse_log
24
+ from chipgraph.packs.assist.triage.question import PROMPT, build_question, question_id
25
+ from chipgraph.packs.assist.triage.rules import (
26
+ RULES,
27
+ SPEC_CROSS_CHECKS,
28
+ RuleHit,
29
+ TriageFacts,
30
+ evaluate,
31
+ is_rtl_path,
32
+ is_tb_path,
33
+ )
34
+ from chipgraph.packs.assist.triage.run import (
35
+ BackendChoice,
36
+ default_backend,
37
+ run_triage,
38
+ triage_log,
39
+ )
40
+
41
+ __all__ = [
42
+ "LABELS",
43
+ "PROMPT",
44
+ "RULES",
45
+ "SPEC_CROSS_CHECKS",
46
+ "BackendChoice",
47
+ "CheckRun",
48
+ "ParsedLog",
49
+ "RuleHit",
50
+ "SpecLine",
51
+ "TriageFacts",
52
+ "TriageLabel",
53
+ "TriageReport",
54
+ "TriageStatus",
55
+ "build_question",
56
+ "default_backend",
57
+ "evaluate",
58
+ "is_rtl_path",
59
+ "is_tb_path",
60
+ "parse_log",
61
+ "question_id",
62
+ "run_triage",
63
+ "triage_log",
64
+ ]
@@ -0,0 +1,94 @@
1
+ """The `/triage` data contract: the classes a failing log is sorted into, and the report.
2
+
3
+ A `TriageReport` says which side has to change to fix a failing lint, simulation or check
4
+ run (`label`: `infra`, `rtl`, `tb` or `spec`), who decided it (`backend`: a deterministic
5
+ rule, or the small or large model tier of `decide()`), how sure it is, a short summary of
6
+ what failed and where, and what to do next (DESIGN 5.2: infra -> fix the environment and
7
+ retry without counting a try; rtl/tb -> fix that file; spec -> ask the spec owner).
8
+
9
+ A model's label is advice: it never blocks a build (DESIGN 4.8), and `low_confidence`
10
+ marks an answer under its tier's threshold.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Literal
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ from chipgraph.core.contracts import Issue
20
+
21
+ TriageLabel = Literal["infra", "rtl", "tb", "spec"]
22
+ """Which side has to change: the environment, the RTL, the testbench, or the spec."""
23
+
24
+ LABELS: tuple[TriageLabel, ...] = ("infra", "rtl", "tb", "spec")
25
+ """The choices of every triage question, in this order."""
26
+
27
+ TriageStatus = Literal["decided", "deferred", "undecided"]
28
+ """`decided`: a label; `deferred`: a model in the user's Claude Code session will answer
29
+ (run the decider loop, then triage again); `undecided`: no rule and no model answered."""
30
+
31
+
32
+ class SpecLine(BaseModel):
33
+ """A spec source the question showed the model: a document line or a model entity."""
34
+
35
+ model_config = ConfigDict(extra="forbid", frozen=True)
36
+
37
+ citation: str = Field(description="'path:line' or 'model:<key>'.")
38
+ text: str = Field(description="The line, or the entity's facts.")
39
+ defined_at: str | None = Field(
40
+ default=None, description="For a model entity: the 'path:line' it was read from."
41
+ )
42
+
43
+ @property
44
+ def location(self) -> str:
45
+ """The file location when there is one, else the citation."""
46
+ return self.defined_at or self.citation
47
+
48
+
49
+ class TriageReport(BaseModel):
50
+ """What `/triage` says about one failing log."""
51
+
52
+ model_config = ConfigDict(extra="forbid", frozen=True)
53
+
54
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
55
+ status: TriageStatus = Field(description="Whether a label was decided.")
56
+ label: TriageLabel | None = Field(default=None, description="The class, when decided.")
57
+ backend: Literal["rule", "small", "large"] | None = Field(
58
+ default=None, description="Who decided: a rule, or a model tier."
59
+ )
60
+ rule: str | None = Field(default=None, description="The rule that decided, if one did.")
61
+ confidence: float = Field(default=0.0, ge=0, le=1, description="Confidence of the label.")
62
+ low_confidence: bool = Field(
63
+ default=False, description="A model's label under its tier's threshold: advice only."
64
+ )
65
+ question_id: str = Field(description="The decide() question id ('triage.<hash>').")
66
+ check_id: str | None = Field(default=None, description="The failing check, if known.")
67
+ source: str | None = Field(default=None, description="Where the log was read from.")
68
+ summary: str = Field(description="What failed and where, from the parsed log.")
69
+ suggestion: str = Field(default="", description="What to do next, for the label.")
70
+ retry_without_counting: bool = Field(
71
+ default=False,
72
+ description="infra: fix the environment and retry; the retry does not count a try.",
73
+ )
74
+ ask_person: bool = Field(
75
+ default=False, description="spec: the spec owner decides; ask them before changing it."
76
+ )
77
+ evidence: tuple[str, ...] = Field(
78
+ default=(), description="'file:line' locations (and spec citations) behind the label."
79
+ )
80
+ issues: tuple[Issue, ...] = Field(default=(), description="Issues parsed from the log.")
81
+ excerpt: str = Field(default="", description="The shortened log the question showed.")
82
+ spec_lines: tuple[SpecLine, ...] = Field(
83
+ default=(),
84
+ description=(
85
+ "Spec sources: those shown to the model with a simulation failure, or the spec "
86
+ "lines a 'spec' label points at."
87
+ ),
88
+ )
89
+ reason: str = Field(default="", description="Why: the rule's evidence or the model's reason.")
90
+ model: str | None = Field(default=None, description="The model that answered, if any.")
91
+ message: str = Field(default="", description="What to do next when not decided.")
92
+
93
+
94
+ __all__ = ["LABELS", "SpecLine", "TriageLabel", "TriageReport", "TriageStatus"]