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,447 @@
1
+ """The deterministic triage rules: the first, free tier of `decide()` (DESIGN 5.4).
2
+
3
+ Each rule looks only at *generic* evidence in a parsed log: what the tools and shells
4
+ print when a tool or a file is missing, when time runs out, which kind of check failed,
5
+ and whether every error points into a testbench or into RTL sources. No rule names a
6
+ project, a file or a sample: `tests/packs/assist/test_triage_rules.py` fails if one does.
7
+
8
+ A rule answers only when its evidence is unambiguous and returns `None` otherwise; a
9
+ simulation mismatch never matches a rule (whether the RTL or the testbench is wrong can
10
+ only be told against the spec), so it goes to the model tiers.
11
+
12
+ Order matters: the environment is ruled out first (`infra`), then a failing spec cross
13
+ check (`spec`), then compile/lint errors that all point into testbenches (`tb`) or all
14
+ into RTL sources (`rtl`).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from collections.abc import Callable
21
+ from dataclasses import dataclass
22
+ from fnmatch import fnmatch
23
+
24
+ from chipgraph.core.contracts import Decision, Issue
25
+ from chipgraph.core.engine.decide import Question, Rule, rule_decision
26
+ from chipgraph.packs.assist.triage.contract import TriageLabel
27
+ from chipgraph.packs.assist.triage.parse import CheckRun, ParsedLog
28
+
29
+ SPEC_CROSS_CHECKS = frozenset({"ports_diff", "spec_schema", "trace", "cross_chip"})
30
+ """Checks that compare a spec with its users: when one fails, the spec side is at fault."""
31
+
32
+ HDL_SUFFIXES = (".sv", ".svh", ".v", ".vh", ".vhd", ".vhdl")
33
+ TB_DIRS = frozenset(
34
+ {"tb", "tbs", "dv", "verif", "verification", "testbench", "testbenches", "test", "tests"}
35
+ )
36
+ """Directory names that hold testbenches by common convention."""
37
+
38
+ TB_LAYOUT_KEYS = ("tb", "dv", "tests")
39
+ """Profile `layout` keys whose paths hold testbenches."""
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class TriageFacts:
44
+ """What the rules look at: the parsed log, the failing check, the testbench layout."""
45
+
46
+ parsed: ParsedLog
47
+ check_id: str | None = None
48
+ """The failing check id the caller named (`--check`), if any."""
49
+ check_kinds: tuple[tuple[str, str], ...] = ()
50
+ """`(check id, check kind)` pairs: the kind is the adapter the profile runs it with."""
51
+ tb_patterns: tuple[str, ...] = ()
52
+ """Glob patterns of testbench paths, from the profile's `layout`."""
53
+
54
+ def kind_of(self, check_id: str) -> str:
55
+ """The check's kind (its adapter), or its id when the profile does not say."""
56
+ return dict(self.check_kinds).get(check_id, check_id)
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class RuleHit:
61
+ """A rule's answer: the label, the rule's name, why, and where."""
62
+
63
+ label: TriageLabel
64
+ rule: str
65
+ reason: str
66
+ evidence: tuple[str, ...] = ()
67
+ hint: str = ""
68
+ """A specific next step (e.g. the missing tool), used in the suggestion."""
69
+
70
+
71
+ # --- paths ------------------------------------------------------------------------------
72
+
73
+
74
+ def _norm(path: str) -> str:
75
+ path = path.replace("\\", "/")
76
+ while path.startswith("./"):
77
+ path = path[2:]
78
+ return path
79
+
80
+
81
+ def is_tb_path(path: str, patterns: tuple[str, ...] = ()) -> bool:
82
+ """True for a testbench path: a profile `layout` match, a `tb`/`dv`/... directory,
83
+ or a `tb_`/`test_` prefix or `_tb`/`_test` suffix on the file name."""
84
+ path = _norm(path)
85
+ if any(fnmatch(path, pattern) for pattern in patterns):
86
+ return True
87
+ parts = path.split("/")
88
+ if any(part.lower() in TB_DIRS for part in parts[:-1]):
89
+ return True
90
+ stem = parts[-1].lower().split(".", 1)[0]
91
+ return (
92
+ stem == "tb"
93
+ or stem.startswith(("tb_", "test_"))
94
+ or stem.endswith(("_tb", "_test", "_tests", "_tb_top"))
95
+ )
96
+
97
+
98
+ def is_rtl_path(path: str, patterns: tuple[str, ...] = ()) -> bool:
99
+ """True for an HDL source that is not a testbench."""
100
+ return _norm(path).lower().endswith(HDL_SUFFIXES) and not is_tb_path(path, patterns)
101
+
102
+
103
+ def tb_patterns(layout: dict[str, str | tuple[str, ...]]) -> tuple[str, ...]:
104
+ """Glob patterns for the testbench paths a profile's `layout` names."""
105
+ out: list[str] = []
106
+ for key in TB_LAYOUT_KEYS:
107
+ value = layout.get(key)
108
+ templates = (value,) if isinstance(value, str) else (value or ())
109
+ for template in templates:
110
+ pattern = _norm(re.sub(r"\{[^}]*\}", "*", template)).rstrip("/")
111
+ if pattern:
112
+ out.extend((pattern, f"{pattern}/*"))
113
+ return tuple(out)
114
+
115
+
116
+ def where(issue: Issue) -> str:
117
+ """`file:line` (or just `file`) of an issue."""
118
+ if issue.file is None:
119
+ return ""
120
+ return f"{issue.file}:{issue.line}" if issue.line is not None else issue.file
121
+
122
+
123
+ def _locations(issues: tuple[Issue, ...] | list[Issue], limit: int = 10) -> tuple[str, ...]:
124
+ out: list[str] = []
125
+ for issue in issues:
126
+ loc = where(issue)
127
+ if loc and loc not in out:
128
+ out.append(loc)
129
+ if len(out) >= limit:
130
+ break
131
+ return tuple(out)
132
+
133
+
134
+ # --- infra evidence -----------------------------------------------------------------------
135
+
136
+ _TOOL_MISSING = (
137
+ re.compile(
138
+ r"^(?:\S*/)?(?:ba|z|da|k)?sh(?:\[\d+\])?:\s*(?:line \d+:\s*)?(?:\d+:\s*)?"
139
+ r"(?P<tool>[^\s:]+): (?:command )?not found",
140
+ re.MULTILINE,
141
+ ),
142
+ re.compile(
143
+ r"^(?:\S*/)?g?make(?:\[\d+\])?: (?P<tool>[^\s:*]+): "
144
+ r"(?:No such file or directory|command not found)",
145
+ re.MULTILINE,
146
+ ),
147
+ re.compile(r"\bcommand not found: (?P<tool>\S+)"),
148
+ re.compile(r"(?P<tool>[^\s:]+): command not found"),
149
+ re.compile(r"\] Error 127\b"),
150
+ )
151
+
152
+ _INPUT_MISSING = (
153
+ re.compile(
154
+ r"Cannot find file containing module: "
155
+ r"'(?P<path>[^']*/[^']*|[^']*\.(?:sv|svh|v|vh|vhd|vhdl))'"
156
+ ),
157
+ re.compile(r"\bFile not found: (?P<path>\S+)"),
158
+ re.compile(r"Cannot find include file: ['\"]?(?P<path>[^'\"\s]+)"),
159
+ re.compile(
160
+ r"\b[Cc]an(?:no|')t open (?:file |input file |include file )?['\"]?(?P<path>[^'\"\s:]+)"
161
+ ),
162
+ re.compile(r"(?P<path>[^\s:]+): Permission denied"),
163
+ re.compile(r"\bPermission denied\b"),
164
+ re.compile(r"(?P<path>[^\s:'\"]+): No such file or directory"),
165
+ re.compile(r"\bNo such file or directory\b"),
166
+ )
167
+
168
+ _MAKE_TARGET = re.compile(r"No rule to make target [`'\"]?(?P<target>[^'`\"\s,]+)")
169
+ _NO_TARGETS = re.compile(r"make(?:\[\d+\])?: \*\*\* No targets\b")
170
+
171
+ _TIME_LIMIT = re.compile(
172
+ r"\btimed out\b|\btime ?limit (?:exceeded|reached)\b"
173
+ r"|\btimeout (?:after|expired|exceeded|reached)\b"
174
+ r"|^\s*(?:Killed|Terminated)\b|\bKilled: 9\b|\bSIG(?:KILL|TERM|XCPU)\b"
175
+ r"|\] Error 1(?:24|37)\b",
176
+ re.IGNORECASE | re.MULTILINE,
177
+ )
178
+
179
+ _TOOL_FAILURE = re.compile(
180
+ r"\binternal error\b|\bsegmentation fault\b|\bcore dumped\b|std::bad_alloc"
181
+ r"|\bout of memory\b|\blicen[cs]e\b.*\b(?:fail\w*|unavailable|denied|checkout|"
182
+ r"not available|expired)\b",
183
+ re.IGNORECASE,
184
+ )
185
+
186
+
187
+ def _scan_text(facts: TriageFacts) -> str:
188
+ """The log lines that may carry environment evidence: not a testbench's own failure
189
+ lines (a testbench printing 'timeout waiting for irq' is not an infra problem), plus
190
+ the messages of issues that name no file (a check that could not run)."""
191
+ failures = set(facts.parsed.sim_failures)
192
+ lines = [line for line in facts.parsed.lines if line.strip() not in failures]
193
+ lines.extend(i.msg for i in facts.parsed.issues if i.file is None)
194
+ for check in facts.parsed.failing_checks:
195
+ lines.extend(i.msg for i in check.issues if i.file is None)
196
+ return "\n".join(lines)
197
+
198
+
199
+ def _first_line(text: str, start: int) -> str:
200
+ begin = text.rfind("\n", 0, start) + 1
201
+ end = text.find("\n", start)
202
+ return text[begin : end if end >= 0 else len(text)].strip()
203
+
204
+
205
+ def check_could_not_run(facts: TriageFacts) -> RuleHit | None:
206
+ """A check reported `error` (it could not run: missing model, tool, adapter, time)."""
207
+ failing = facts.parsed.failing_checks
208
+ if not failing or any(c.status != "error" for c in failing):
209
+ return None
210
+ first = failing[0]
211
+ msgs = [i.msg for c in failing for i in c.issues]
212
+ reason = f"check {first.check_id!r} could not run" + (f": {msgs[0]}" if msgs else "")
213
+ return RuleHit("infra", "check_could_not_run", reason, hint=msgs[0] if msgs else "")
214
+
215
+
216
+ def tool_missing(facts: TriageFacts) -> RuleHit | None:
217
+ """A shell or make could not find the program to run (exit 127)."""
218
+ text = _scan_text(facts)
219
+ for pattern in _TOOL_MISSING:
220
+ match = pattern.search(text)
221
+ if match is None:
222
+ continue
223
+ tool = match.groupdict().get("tool")
224
+ hint = (
225
+ f"install `{tool}` or put it on PATH (or load its module: `env.modules`)"
226
+ if tool
227
+ else "install the missing program or put it on PATH"
228
+ )
229
+ return RuleHit(
230
+ "infra",
231
+ "tool_missing",
232
+ f"a program is missing: {_first_line(text, match.start())}",
233
+ hint=hint,
234
+ )
235
+ return None
236
+
237
+
238
+ def input_missing(facts: TriageFacts) -> RuleHit | None:
239
+ """A file the command needs does not exist or cannot be read."""
240
+ text = _scan_text(facts)
241
+ for pattern in _INPUT_MISSING:
242
+ match = pattern.search(text)
243
+ if match is None:
244
+ continue
245
+ path = match.groupdict().get("path")
246
+ line = _first_line(text, match.start())
247
+ if "Permission denied" in line:
248
+ hint = f"make `{path}` readable" if path else "fix the file permissions"
249
+ elif path:
250
+ hint = f"fix the path `{path}` in the filelist or the command, or restore the file"
251
+ else:
252
+ hint = "fix the path in the filelist or the command, or restore the file"
253
+ return RuleHit(
254
+ "infra",
255
+ "input_missing",
256
+ f"an input file is missing: {line}",
257
+ evidence=(path,) if path else (),
258
+ hint=hint,
259
+ )
260
+ return None
261
+
262
+
263
+ def make_target_missing(facts: TriageFacts) -> RuleHit | None:
264
+ """`make` has no rule for the target the flow runs."""
265
+ text = _scan_text(facts)
266
+ match = _MAKE_TARGET.search(text) or _NO_TARGETS.search(text)
267
+ if match is None:
268
+ return None
269
+ target = match.groupdict().get("target")
270
+ hint = (
271
+ f"add the make target `{target}` or fix the command the profile runs"
272
+ if target
273
+ else "add the make target or fix the command the profile runs"
274
+ )
275
+ line = _first_line(text, match.start())
276
+ return RuleHit("infra", "make_target_missing", f"make has no such target: {line}", hint=hint)
277
+
278
+
279
+ def time_limit(facts: TriageFacts) -> RuleHit | None:
280
+ """The command ran out of time or was killed."""
281
+ text = _scan_text(facts)
282
+ match = _TIME_LIMIT.search(text)
283
+ if match is None:
284
+ return None
285
+ line = _first_line(text, match.start())
286
+ return RuleHit(
287
+ "infra",
288
+ "time_limit",
289
+ f"the command ran out of time or was killed: {line}",
290
+ hint="find what it waits on (a licence, a lock, a hung tool) or raise its time limit "
291
+ "(`adapters.<check>.timeout_s`)",
292
+ )
293
+
294
+
295
+ def tool_failure(facts: TriageFacts) -> RuleHit | None:
296
+ """The tool itself crashed, ran out of memory, or found no licence."""
297
+ text = _scan_text(facts)
298
+ match = _TOOL_FAILURE.search(text)
299
+ if match is None:
300
+ return None
301
+ line = _first_line(text, match.start())
302
+ return RuleHit(
303
+ "infra",
304
+ "tool_failure",
305
+ f"the tool failed: {line}",
306
+ hint="check the tool's installation, memory and licence, then rerun",
307
+ )
308
+
309
+
310
+ # --- design evidence ---------------------------------------------------------------------
311
+
312
+
313
+ def _failing_spec_checks(facts: TriageFacts) -> tuple[CheckRun, ...] | None:
314
+ """The failing checks, when every one of them is a spec cross check that ran."""
315
+ failing = facts.parsed.failing_checks
316
+ if failing:
317
+ if all(
318
+ c.status == "fail" and facts.kind_of(c.check_id) in SPEC_CROSS_CHECKS for c in failing
319
+ ):
320
+ return failing
321
+ return None
322
+ if facts.check_id is not None and facts.kind_of(facts.check_id) in SPEC_CROSS_CHECKS:
323
+ issues = facts.parsed.tool_issues
324
+ if issues:
325
+ return (CheckRun(facts.check_id, None, "fail", issues),)
326
+ return None
327
+
328
+
329
+ def spec_cross_check(facts: TriageFacts) -> RuleHit | None:
330
+ """A spec cross check (`ports_diff`, `spec_schema`, `trace`, `cross_chip`) failed."""
331
+ if facts.parsed.sim_ran:
332
+ return None
333
+ checks = _failing_spec_checks(facts)
334
+ if checks is None:
335
+ return None
336
+ issues = [i for c in checks for i in c.issues]
337
+ if not issues or any(i.file is None for i in issues):
338
+ return None
339
+ names = ", ".join(sorted({c.check_id for c in checks}))
340
+ first = issues[0]
341
+ return RuleHit(
342
+ "spec",
343
+ "spec_cross_check",
344
+ f"the spec cross check {names} failed: {where(first)} [{first.rule}] {first.msg}",
345
+ evidence=_locations(issues),
346
+ )
347
+
348
+
349
+ def _design_issues(facts: TriageFacts) -> list[Issue] | None:
350
+ """The compile/lint issues behind the failure, when they say where (else None)."""
351
+ parsed = facts.parsed
352
+ if parsed.sim_ran or parsed.unlocated_errors:
353
+ return None
354
+ if any(c.status == "error" for c in parsed.failing_checks):
355
+ return None
356
+ if any(facts.kind_of(c.check_id) in SPEC_CROSS_CHECKS for c in parsed.failing_checks):
357
+ return None
358
+ issues = [i for i in parsed.tool_issues if i.severity in ("error", "warning")]
359
+ for check in parsed.failing_checks:
360
+ issues.extend(i for i in check.issues if i.severity in ("error", "warning"))
361
+ if not issues or any(i.file is None for i in issues):
362
+ return None
363
+ return issues
364
+
365
+
366
+ def testbench_compile(facts: TriageFacts) -> RuleHit | None:
367
+ """Every compile/lint error points into a testbench, and no simulation ran."""
368
+ issues = _design_issues(facts)
369
+ if issues is None or not all(is_tb_path(i.file or "", facts.tb_patterns) for i in issues):
370
+ return None
371
+ first = issues[0]
372
+ return RuleHit(
373
+ "tb",
374
+ "testbench_compile",
375
+ f"every error is in a testbench: {where(first)} [{first.rule}] {first.msg}",
376
+ evidence=_locations(issues),
377
+ )
378
+
379
+
380
+ def rtl_lint(facts: TriageFacts) -> RuleHit | None:
381
+ """Every compile/lint error points into an RTL source, and no simulation ran."""
382
+ issues = _design_issues(facts)
383
+ if issues is None or not all(is_rtl_path(i.file or "", facts.tb_patterns) for i in issues):
384
+ return None
385
+ first = issues[0]
386
+ return RuleHit(
387
+ "rtl",
388
+ "rtl_lint",
389
+ f"every error is in an RTL source: {where(first)} [{first.rule}] {first.msg}",
390
+ evidence=_locations(issues),
391
+ )
392
+
393
+
394
+ RULES: tuple[Callable[[TriageFacts], RuleHit | None], ...] = (
395
+ check_could_not_run,
396
+ tool_missing,
397
+ input_missing,
398
+ make_target_missing,
399
+ time_limit,
400
+ tool_failure,
401
+ spec_cross_check,
402
+ testbench_compile,
403
+ rtl_lint,
404
+ )
405
+ """Every triage rule, in the order `decide()` tries them."""
406
+
407
+
408
+ def evaluate(facts: TriageFacts) -> RuleHit | None:
409
+ """The first rule that answers, or None: what `decide()`'s rule tier will say."""
410
+ for rule in RULES:
411
+ hit = rule(facts)
412
+ if hit is not None:
413
+ return hit
414
+ return None
415
+
416
+
417
+ def decide_rules(facts: TriageFacts, fired: list[RuleHit]) -> list[Rule]:
418
+ """The rules as `decide()` rules over `facts`; the hit that answered goes to `fired`."""
419
+
420
+ def make(rule: Callable[[TriageFacts], RuleHit | None]) -> Rule:
421
+ def decide_rule(question: Question) -> Decision | None:
422
+ hit = rule(facts)
423
+ if hit is None:
424
+ return None
425
+ fired.append(hit)
426
+ return rule_decision(question, hit.label)
427
+
428
+ decide_rule.__name__ = rule.__name__
429
+ return decide_rule
430
+
431
+ return [make(rule) for rule in RULES]
432
+
433
+
434
+ __all__ = [
435
+ "HDL_SUFFIXES",
436
+ "RULES",
437
+ "SPEC_CROSS_CHECKS",
438
+ "TB_DIRS",
439
+ "RuleHit",
440
+ "TriageFacts",
441
+ "decide_rules",
442
+ "evaluate",
443
+ "is_rtl_path",
444
+ "is_tb_path",
445
+ "tb_patterns",
446
+ "where",
447
+ ]
@@ -0,0 +1,224 @@
1
+ """`run_triage`: classify a failing log with `decide()` and build the `TriageReport`.
2
+
3
+ 1. parse the log (`parse.parse_log`): issues, check results, simulation failures, excerpt;
4
+ 2. ask `decide()` the question `question.build_question` builds, with the deterministic
5
+ `rules` first and the model tiers after them;
6
+ 3. turn the answer into a report: the deterministic summary and suggestion (`report`).
7
+
8
+ The model backend follows the profile's runtime: `claude-code` queues the question for
9
+ the `chipgraph:decider` subagent of the user's Claude Code session (the report is then
10
+ `deferred`: run `/chipgraph:triage`, which runs the decider loop and triages again);
11
+ an API runtime asks the `models.providers` provider through `LlmDecideBackend`. With no
12
+ backend only the rules answer, and a log no rule classifies is `undecided`.
13
+
14
+ A model's label is advice (DESIGN 4.8): it never blocks, and an answer under its tier's
15
+ threshold is marked `low_confidence`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ from pathlib import Path
22
+ from typing import Any, Literal, cast
23
+
24
+ from chipgraph.adapters.llm import LlmError
25
+ from chipgraph.adapters.llm.decide_backend import LlmDecideBackend
26
+ from chipgraph.adapters.runtime.claude_code.decisions import ClaudeCodeDecideBackend
27
+ from chipgraph.app.context import AppContext
28
+ from chipgraph.core.config.models import Profile
29
+ from chipgraph.core.contracts.types import DataLabel
30
+ from chipgraph.core.engine.decide import (
31
+ DecideError,
32
+ DecisionLog,
33
+ Deferred,
34
+ ModelBackend,
35
+ decide,
36
+ low_confidence,
37
+ )
38
+ from chipgraph.packs.assist.ask.answer import make_provider
39
+ from chipgraph.packs.assist.triage.contract import LABELS, SpecLine, TriageLabel, TriageReport
40
+ from chipgraph.packs.assist.triage.parse import ParsedLog, parse_log
41
+ from chipgraph.packs.assist.triage.question import build_question, spec_lines, spec_sources
42
+ from chipgraph.packs.assist.triage.report import evidence, suggestion, summary
43
+ from chipgraph.packs.assist.triage.rules import (
44
+ RuleHit,
45
+ TriageFacts,
46
+ decide_rules,
47
+ is_rtl_path,
48
+ is_tb_path,
49
+ tb_patterns,
50
+ )
51
+
52
+ MAX_REPORT_ISSUES = 50
53
+
54
+ BackendChoice = ModelBackend | None | Literal["auto"]
55
+ """`"auto"`: the profile's runtime decides; `None`: rules only; or a given backend."""
56
+
57
+
58
+ def default_backend(ctx: AppContext) -> ModelBackend | None:
59
+ """The model backend for the profile's runtime, or None when there is none."""
60
+ profile = ctx.require_profile().profile
61
+ if profile.runtime == "claude-code":
62
+ return ClaudeCodeDecideBackend(ctx.layout, profile.models)
63
+ provider = make_provider(profile)
64
+ if provider is None:
65
+ return None
66
+ return LlmDecideBackend(provider, profile.models, data=profile.data)
67
+
68
+
69
+ def _check_kinds(profile: Profile) -> tuple[tuple[str, str], ...]:
70
+ return tuple(sorted((check_id, cfg.use) for check_id, cfg in profile.adapters.items()))
71
+
72
+
73
+ def _check_id(parsed: ParsedLog, given: str | None) -> str | None:
74
+ if given:
75
+ return given
76
+ ids = {c.check_id for c in parsed.failing_checks}
77
+ return ids.pop() if len(ids) == 1 else None
78
+
79
+
80
+ def _source_label(ctx: AppContext, source: str | None) -> DataLabel:
81
+ labels = ctx.store.labels
82
+ if source is None or source == "-":
83
+ return labels.default
84
+ path = Path(source)
85
+ if path.is_absolute():
86
+ try:
87
+ rel = path.resolve().relative_to(ctx.root.resolve()).as_posix()
88
+ except ValueError:
89
+ return labels.default
90
+ else:
91
+ rel = path.as_posix()
92
+ return labels.label_for(rel)
93
+
94
+
95
+ def _logged(ctx: AppContext, question_id: str) -> tuple[str, str | None]:
96
+ """The reason and model of the decision `decide()` logged last for `question_id`."""
97
+ entries = [
98
+ e
99
+ for e in DecisionLog.for_layout(ctx.layout).read()
100
+ if e.question_id == question_id and e.event == "decided"
101
+ ]
102
+ if not entries:
103
+ return "", None
104
+ return entries[-1].reason, entries[-1].model
105
+
106
+
107
+ def _spec_side(ctx: AppContext, parsed: ParsedLog, facts: TriageFacts) -> tuple[SpecLine, ...]:
108
+ """For a `spec` label whose issues all point at RTL or testbenches (a port width the
109
+ spec gets wrong is reported at the RTL port): the spec lines about those issues."""
110
+ for issue in parsed.issues:
111
+ path = issue.file
112
+ if path and not is_rtl_path(path, facts.tb_patterns) and not is_tb_path(path):
113
+ return ()
114
+ query = " ".join(i.msg for i in parsed.issues[:2])
115
+ specs, _ = spec_sources(ctx, query, facts, limit=3)
116
+ return specs
117
+
118
+
119
+ async def triage_log(
120
+ ctx: AppContext,
121
+ log_text: str,
122
+ *,
123
+ source: str | None = None,
124
+ check_id: str | None = None,
125
+ backend: BackendChoice = "auto",
126
+ ) -> TriageReport:
127
+ """Classify `log_text` (a failing lint/sim/check log); see the module docstring."""
128
+ profile = ctx.require_profile().profile
129
+ parsed = parse_log(log_text)
130
+ facts = TriageFacts(
131
+ parsed=parsed,
132
+ check_id=_check_id(parsed, check_id),
133
+ check_kinds=_check_kinds(profile),
134
+ tb_patterns=tb_patterns(profile.layout),
135
+ )
136
+ specs, spec_labels = spec_lines(ctx, parsed, facts)
137
+ question = build_question(
138
+ parsed, facts, specs=specs, labels=(_source_label(ctx, source), *spec_labels)
139
+ )
140
+ chosen = default_backend(ctx) if backend == "auto" else backend
141
+ fired: list[RuleHit] = []
142
+ base: dict[str, Any] = {
143
+ "question_id": question.id,
144
+ "check_id": facts.check_id,
145
+ "source": source,
146
+ "summary": summary(parsed),
147
+ "issues": parsed.issues[:MAX_REPORT_ISSUES],
148
+ "excerpt": parsed.excerpt,
149
+ "spec_lines": specs,
150
+ }
151
+
152
+ try:
153
+ outcome = await decide(
154
+ question,
155
+ rules=decide_rules(facts, fired),
156
+ backend=chosen,
157
+ cfg=profile.decide,
158
+ log=DecisionLog.for_layout(ctx.layout),
159
+ )
160
+ except (DecideError, LlmError) as exc:
161
+ if chosen is None:
162
+ message = (
163
+ "no rule classified this log and no model is configured: set "
164
+ "models.providers (API runtime), or use runtime claude-code and run "
165
+ "/chipgraph:triage in Claude Code"
166
+ )
167
+ else:
168
+ message = f"no model answered: {exc}"
169
+ return TriageReport(status="undecided", message=message, **base)
170
+
171
+ if isinstance(outcome, Deferred):
172
+ where = f" {source}" if source and source != "-" else " <log>"
173
+ message = (
174
+ f"no rule classified this log; it waits for the {outcome.tier} model tier"
175
+ f" ({outcome.model or 'its model'}). In Claude Code run /chipgraph:triage{where}: "
176
+ "it answers the queued question with the decider subagent and triages again."
177
+ )
178
+ return TriageReport(
179
+ status="deferred",
180
+ model=outcome.model,
181
+ message=message,
182
+ **base,
183
+ )
184
+
185
+ label = cast(TriageLabel, outcome.value)
186
+ assert label in LABELS
187
+ if label == "spec" and not specs:
188
+ specs = _spec_side(ctx, parsed, facts)
189
+ base["spec_lines"] = specs
190
+ hit = fired[-1] if outcome.backend == "rule" and fired else None
191
+ if hit is not None:
192
+ reason, model = hit.reason, None
193
+ else:
194
+ reason, model = _logged(ctx, question.id)
195
+ return TriageReport(
196
+ status="decided",
197
+ label=label,
198
+ backend=outcome.backend,
199
+ rule=hit.rule if hit is not None else None,
200
+ confidence=outcome.confidence,
201
+ low_confidence=low_confidence(outcome, profile.decide),
202
+ suggestion=suggestion(label, parsed, facts, hit, specs),
203
+ retry_without_counting=label == "infra",
204
+ ask_person=label == "spec",
205
+ evidence=evidence(label, parsed, facts, hit, specs),
206
+ reason=reason,
207
+ model=model,
208
+ **base,
209
+ )
210
+
211
+
212
+ def run_triage(
213
+ ctx: AppContext,
214
+ log_text: str,
215
+ *,
216
+ source: str | None = None,
217
+ check_id: str | None = None,
218
+ backend: BackendChoice = "auto",
219
+ ) -> TriageReport:
220
+ """`chipgraph triage`: `triage_log` in its own event loop."""
221
+ return asyncio.run(triage_log(ctx, log_text, source=source, check_id=check_id, backend=backend))
222
+
223
+
224
+ __all__ = ["BackendChoice", "default_backend", "run_triage", "triage_log"]
@@ -0,0 +1,19 @@
1
+ """The ``digital-rtl`` pack: rules and skills for digital RTL (DESIGN.md 8.2).
2
+
3
+ - the Critic review of each block's change: the rule ``digital-rtl/review``
4
+ (``rules/review.yml``) and its skill ``review/diff`` (``skills/review-diff.md``);
5
+ - the Planner (M2-03): the rule ``digital-rtl/plan`` (``rules/plan.yml``, skill
6
+ ``plan/modules``, check ``plan_check``), the gate ``plan:{block}`` on the rule
7
+ ``digital-rtl/plan_expand`` (``rules/plan_expand.yml``), and the code behind them in
8
+ :mod:`chipgraph.packs.digital_rtl.plan` (the plan file model, the ``plan.modules``
9
+ resolver, the ``plan_expand`` tool, ``chipgraph plan show`` and the planner context);
10
+ - the RTL of each planned module (M2-04): the rule ``digital-rtl/rtl_module``
11
+ (``rules/rtl_module.yml``, role ``rtl-author``, which never sees a testbench), its
12
+ skills ``lang-sv/rtl`` (``skills/lang-sv-rtl.md``) and ``org/naming``
13
+ (``skills/org-naming.md``), and the context in :mod:`chipgraph.packs.digital_rtl.rtl`.
14
+ Its checks ``format`` and ``style`` are :mod:`chipgraph.checks.format` and
15
+ :mod:`chipgraph.checks.style`.
16
+
17
+ The pack directory is discovered by :func:`chipgraph.app.build.builtin_packs_dir`; a
18
+ project turns it on with ``packs: [digital-rtl]``.
19
+ """