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,384 @@
1
+ """The `decide()` question for a failing log: choices `infra|rtl|tb|spec`, and its context.
2
+
3
+ The context is what a model needs and no more (DESIGN 5.4, shortened logs): the failing
4
+ check, the log excerpt, the parsed issues and, for a simulation failure, the spec lines the
5
+ Design Model and the indexed documents hold about what failed (`/ask`'s retrieval), plus
6
+ the chip-level map entries of the registers and blocks the failure names: a simulation
7
+ mismatch is only decidable against the spec (is the testbench's expectation or the
8
+ design's answer the one the spec gives?), and a test of a top-level design drives its
9
+ blocks through the chip's address map.
10
+
11
+ A simulation failure is a self-checking testbench's `FAIL ...` / `expected X got Y` line
12
+ or the message of a failed assertion (`$error`, `assert`): `failure_lines`.
13
+
14
+ Spec lines never come from RTL or testbench files (those are the two sides being judged)
15
+ and never from an 'nda' file. The question id is `triage.<hash of the log and check>`, so
16
+ the same log asks the same question and a recorded answer is picked up again.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import re
23
+ from collections.abc import Iterable
24
+
25
+ from chipgraph.app.context import AppContext
26
+ from chipgraph.app.errors import AppError
27
+ from chipgraph.core.contracts.types import DataLabel
28
+ from chipgraph.core.engine.decide import Question
29
+ from chipgraph.core.model.entities import EntityBase
30
+ from chipgraph.packs.assist.ask._project import AskProject
31
+ from chipgraph.packs.assist.ask.contract import AskSource
32
+ from chipgraph.packs.assist.ask.retrieve import retrieve
33
+ from chipgraph.packs.assist.triage.contract import LABELS, SpecLine
34
+ from chipgraph.packs.assist.triage.parse import ParsedLog
35
+ from chipgraph.packs.assist.triage.rules import TriageFacts, is_rtl_path, is_tb_path, where
36
+
37
+ QUESTION_PREFIX = "triage."
38
+ MAX_SPEC_LINES = 10
39
+ MAX_MAP_LINES = 6
40
+ MAX_SPEC_TEXT = 240
41
+ MAX_ISSUES = 12
42
+ MAX_FAILURES = 6
43
+
44
+ PROMPT = """\
45
+ A chip design run failed. Which side has to change to fix it?
46
+ - infra: the environment or the flow, not the design: a missing tool or file, a wrong \
47
+ path, a missing make target, a time limit, a crash or a licence problem.
48
+ - rtl: the RTL design is wrong: lint or compile errors in RTL sources, or a simulation \
49
+ result that disagrees with the spec.
50
+ - tb: the testbench or test is wrong: it does not compile, drives the design otherwise \
51
+ than the spec describes (a wrong address, port or reset), or expects a value the spec \
52
+ does not give.
53
+ - spec: the specification (MAS, chip contract) is wrong or incomplete, or contradicts the \
54
+ RTL and its users.
55
+ For a simulation mismatch, compare the testbench's stimulus and expected value with the \
56
+ spec lines: if the spec agrees with the expected value, the RTL is wrong; if the spec \
57
+ agrees with what the design returned, or the testbench drove the design in a way the spec \
58
+ does not describe, the testbench is wrong."""
59
+ """The question every triage asks a model (choices: `LABELS`)."""
60
+
61
+ _IDENT = re.compile(r"\w+(?:[-.]\w+)*")
62
+
63
+ _FAIL_WORD = re.compile(r"^\s*(?:\[[^\]]*\]\s*)?(?:FAIL(?:ED|URE)?|MISMATCH)\b[:\s]*")
64
+
65
+
66
+ def question_id(parsed: ParsedLog, check_id: str | None) -> str:
67
+ """`triage.<16 hex>`: a hash of the check id and the log text."""
68
+ digest = hashlib.sha256(f"{check_id or ''}\n{parsed.text}".encode()).hexdigest()
69
+ return QUESTION_PREFIX + digest[:16]
70
+
71
+
72
+ def _not_spec(path: str, facts: TriageFacts) -> bool:
73
+ """RTL, testbench and filelist files are not spec sources."""
74
+ return (
75
+ is_rtl_path(path, facts.tb_patterns)
76
+ or is_tb_path(path, facts.tb_patterns)
77
+ or path.endswith(".f")
78
+ )
79
+
80
+
81
+ def _file_of(citation: str | None) -> str | None:
82
+ if not citation or citation.startswith("model:"):
83
+ return None
84
+ return citation.rsplit(":", 1)[0] if re.search(r":\d+(?:-\d+)?$", citation) else citation
85
+
86
+
87
+ def failure_lines(parsed: ParsedLog) -> tuple[str, ...]:
88
+ """What the simulation says failed: the self-check failure lines, then the messages of
89
+ its failed assertions (a testbench that reports with `$error` or `assert`)."""
90
+ lines = list(parsed.sim_failures)
91
+ for message in parsed.sim_assertions:
92
+ if not any(message in line for line in lines):
93
+ lines.append(message)
94
+ return tuple(lines)
95
+
96
+
97
+ def spec_lines(
98
+ ctx: AppContext, parsed: ParsedLog, facts: TriageFacts
99
+ ) -> tuple[tuple[SpecLine, ...], tuple[DataLabel, ...]]:
100
+ """The spec sources about a simulation failure, and their data labels.
101
+
102
+ The `/ask` retrieval for the failure lines (`failure_lines`), then the chip-level map
103
+ entries of what they name (`chip_map_sources`): the registers, and the address map
104
+ of the blocks and of the buses they sit on. A test of a top-level design drives the
105
+ blocks through that map, so it is only decidable against it.
106
+
107
+ Empty when the log shows no simulation failure, or the project has no Design Model
108
+ yet (`chipgraph ingest`): the question is then asked without them.
109
+ """
110
+ failures = failure_lines(parsed)
111
+ if not failures:
112
+ return (), ()
113
+ try:
114
+ project = AskProject.load(ctx)
115
+ except AppError:
116
+ return (), ()
117
+ texts = [_FAIL_WORD.sub("", line) for line in failures[:3]]
118
+ query = " ".join(t for t in texts if t)
119
+ sources = retrieve(project, query, limit=MAX_SPEC_LINES * 2).sources if query.strip() else ()
120
+ lines: list[SpecLine] = []
121
+ labels: set[DataLabel] = set()
122
+ _collect(project, sources, facts, MAX_SPEC_LINES, lines, labels, set())
123
+ named = "\n".join((*failures, *parsed.runtime_lines))
124
+ _collect(
125
+ project,
126
+ chip_map_sources(project, named),
127
+ facts,
128
+ len(lines) + MAX_MAP_LINES,
129
+ lines,
130
+ labels,
131
+ {line.citation for line in lines},
132
+ by_citation=True,
133
+ )
134
+ return tuple(lines), tuple(sorted(labels))
135
+
136
+
137
+ def spec_sources(
138
+ ctx: AppContext, query: str, facts: TriageFacts, limit: int = MAX_SPEC_LINES
139
+ ) -> tuple[tuple[SpecLine, ...], tuple[DataLabel, ...]]:
140
+ """Spec-side sources for `query` (`/ask` retrieval, no RTL or testbench file)."""
141
+ if not query.strip():
142
+ return (), ()
143
+ try:
144
+ project = AskProject.load(ctx)
145
+ except AppError:
146
+ return (), ()
147
+ context = retrieve(project, query, limit=limit * 2)
148
+ lines: list[SpecLine] = []
149
+ labels: set[DataLabel] = set()
150
+ _collect(project, context.sources, facts, limit, lines, labels, set())
151
+ return tuple(lines), tuple(sorted(labels))
152
+
153
+
154
+ def _collect(
155
+ project: AskProject,
156
+ sources: Iterable[AskSource],
157
+ facts: TriageFacts,
158
+ limit: int,
159
+ lines: list[SpecLine],
160
+ labels: set[DataLabel],
161
+ seen: set[str],
162
+ *,
163
+ by_citation: bool = False,
164
+ ) -> None:
165
+ """Add the spec-side `sources` to `lines` (and their labels), up to `limit` lines.
166
+
167
+ A source whose location is in `seen` already is skipped, or with `by_citation` one
168
+ whose citation is (two entities read from one table row are both kept).
169
+ """
170
+ for source in sources:
171
+ if len(lines) >= limit:
172
+ break
173
+ files = [f for f in (_file_of(source.citation), _file_of(source.defined_at)) if f]
174
+ if any(_not_spec(f, facts) for f in files):
175
+ continue
176
+ # A document line the model entity was read from says the same thing again.
177
+ location = source.defined_at or source.citation
178
+ key = source.citation if by_citation else location
179
+ if key in seen:
180
+ continue
181
+ seen.update((key, location))
182
+ text = " ".join(source.text.split())
183
+ if len(text) > MAX_SPEC_TEXT:
184
+ text = text[: MAX_SPEC_TEXT - 1] + "…"
185
+ defined_at = source.defined_at if source.defined_at != source.citation else None
186
+ lines.append(SpecLine(citation=source.citation, text=text, defined_at=defined_at))
187
+ labels.update(project.labels.label_for(f) for f in files)
188
+
189
+
190
+ # --- the chip-level map of what a failure names -------------------------------------------
191
+
192
+
193
+ def _words(text: str) -> list[str]:
194
+ """The identifiers of `text`, in order, without repeats (`CTRL.EN` is one)."""
195
+ words: list[str] = []
196
+ for match in _IDENT.finditer(text):
197
+ word = match.group(0).strip("-.")
198
+ if word and word not in words:
199
+ words.append(word)
200
+ return words
201
+
202
+
203
+ def _named_blocks(project: AskProject, words: list[str]) -> list[str]:
204
+ """The keys of the blocks the words name, in order: by the name of the block, of one
205
+ of its modules, or of an instance of one.
206
+
207
+ A word that names nothing is split, at `.` and `-` then at `_`, and its parts are
208
+ tried: a top-level port or instance is usually named after its block (`<block>_irq`,
209
+ `u_<block>`, `dut.u_<block>.q`).
210
+ """
211
+ owners: dict[str, list[str]] = {}
212
+
213
+ def own(name: str, block: str | None) -> None:
214
+ if name and block and project.get(block) is not None:
215
+ owners.setdefault(name.lower(), []).append(block)
216
+
217
+ for entity in sorted(project.model.by_kind("block"), key=lambda e: e.key):
218
+ if project.visible(entity):
219
+ own(entity.name, entity.key)
220
+ for module in sorted(project.model.by_kind("module"), key=lambda e: e.key):
221
+ if project.visible(module):
222
+ own(module.name, project.block_of(module))
223
+ for relation in project.model.relations:
224
+ child = project.get(relation.dst) if relation.kind == "instantiates" else None
225
+ if child is not None:
226
+ own(str(relation.attrs.get("instance_name", "")), project.block_of(child))
227
+
228
+ blocks: list[str] = []
229
+
230
+ def named(word: str) -> bool:
231
+ found = owners.get(word.lower(), [])
232
+ blocks.extend(b for b in found if b not in blocks)
233
+ return bool(found)
234
+
235
+ for word in words:
236
+ if named(word):
237
+ continue
238
+ for segment in re.split(r"[.\-]+", word):
239
+ if segment and not named(segment):
240
+ for part in segment.split("_"):
241
+ if part:
242
+ named(part)
243
+ return blocks
244
+
245
+
246
+ def _related_blocks(project: AskProject, blocks: list[str]) -> list[str]:
247
+ """`blocks`, then their IP blocks or instances (`instance_of`), then the other blocks
248
+ on the buses they connect to: the address map a test of the chip addresses them by."""
249
+ related = list(blocks)
250
+
251
+ def add(key: str) -> None:
252
+ if key not in related and project.get(key) is not None:
253
+ related.append(key)
254
+
255
+ for relation in project.model.relations:
256
+ if relation.kind == "instance_of":
257
+ if relation.src in blocks:
258
+ add(relation.dst)
259
+ elif relation.dst in blocks:
260
+ add(relation.src)
261
+ buses = [r.src for r in project.model.relations if r.kind == "connects" and r.dst in related]
262
+ for relation in project.model.relations:
263
+ if relation.kind == "connects" and relation.src in buses:
264
+ add(relation.dst)
265
+ return related
266
+
267
+
268
+ def _named_registers(project: AskProject, words: list[str], blocks: list[str]) -> list[EntityBase]:
269
+ """The registers the words name exactly (`CTRL`, or `CTRL.EN` for a field of it): of
270
+ the named blocks (or their IP blocks) when there are any, else of any block."""
271
+ names = set(words) | {w.split(".")[0] for w in words if "." in w}
272
+ found = [
273
+ r
274
+ for r in sorted(project.model.by_kind("register"), key=lambda e: e.key)
275
+ if r.name in names and project.visible(r)
276
+ ]
277
+ if blocks:
278
+ return [r for r in found if project.block_of(r) in blocks]
279
+ return found
280
+
281
+
282
+ def chip_map_sources(project: AskProject, text: str) -> list[AskSource]:
283
+ """The chip-level map entries of what `text` names, most specific first.
284
+
285
+ The registers it names (of the blocks it names, when it names any); the memory
286
+ regions of the blocks it names (`_named_blocks`) or whose registers it names, of
287
+ their IP blocks or instances (`instance_of`), and of the other blocks on the same
288
+ buses.
289
+ """
290
+ words = _words(text)
291
+ blocks = _named_blocks(project, words)
292
+ registers = _named_registers(project, words, _related_blocks(project, blocks))
293
+ for register in registers:
294
+ block = project.block_of(register)
295
+ if block is not None and block not in blocks:
296
+ blocks.append(block)
297
+ related = _related_blocks(project, blocks)
298
+ order = {key: index for index, key in enumerate(related)}
299
+ regions = sorted(
300
+ (
301
+ r
302
+ for r in project.model.by_kind("memory_region")
303
+ if project.visible(r) and project.block_of(r) in order
304
+ ),
305
+ key=lambda r: (order[project.block_of(r) or ""], r.key),
306
+ )
307
+ return [
308
+ AskSource(
309
+ citation=f"model:{entity.key}",
310
+ kind="model",
311
+ text=project.summary(entity),
312
+ origin="lookup",
313
+ defined_at=project.defined_at(entity),
314
+ )
315
+ for entity in (*registers, *regions)
316
+ ]
317
+
318
+
319
+ def build_context(parsed: ParsedLog, facts: TriageFacts, specs: tuple[SpecLine, ...] = ()) -> str:
320
+ """The text a model reads with the question."""
321
+ parts: list[str] = []
322
+ checks = parsed.failing_checks
323
+ if checks:
324
+ named = ", ".join(
325
+ f"{c.check_id} ({facts.kind_of(c.check_id)}, {c.status})"
326
+ + (f" for block {c.block}" if c.block else "")
327
+ for c in checks
328
+ )
329
+ parts.append(f"Failing check: {named}")
330
+ elif facts.check_id:
331
+ parts.append(f"Failing check: {facts.check_id} ({facts.kind_of(facts.check_id)})")
332
+ parts.append(f"Log excerpt:\n{parsed.excerpt or '(empty log)'}")
333
+ issues = parsed.issues[:MAX_ISSUES]
334
+ if issues:
335
+ rows = [
336
+ f"- {where(i) or '(no file)'} [{i.rule or '-'}] {i.severity}: {i.msg}" for i in issues
337
+ ]
338
+ parts.append("Parsed issues:\n" + "\n".join(rows))
339
+ failures = failure_lines(parsed)
340
+ if failures:
341
+ rows = [f"- {line}" for line in failures[:MAX_FAILURES]]
342
+ parts.append("Simulation self-check failures:\n" + "\n".join(rows))
343
+ if specs:
344
+ rows = [
345
+ f"- {s.citation}" + (f" ({s.defined_at})" if s.defined_at else "") + f": {s.text}"
346
+ for s in specs
347
+ ]
348
+ parts.append(
349
+ "Spec lines (from the project's Design Model and specification documents):\n"
350
+ + "\n".join(rows)
351
+ )
352
+ elif failures:
353
+ parts.append("Spec lines: none found (no Design Model, or nothing matched).")
354
+ return "\n\n".join(parts)
355
+
356
+
357
+ def build_question(
358
+ parsed: ParsedLog,
359
+ facts: TriageFacts,
360
+ *,
361
+ specs: tuple[SpecLine, ...] = (),
362
+ labels: tuple[DataLabel, ...] = (),
363
+ ) -> Question:
364
+ """The `decide()` question for `parsed`."""
365
+ return Question(
366
+ id=question_id(parsed, facts.check_id),
367
+ prompt=PROMPT,
368
+ choices=LABELS,
369
+ context=build_context(parsed, facts, specs),
370
+ labels=tuple(sorted(set(labels))),
371
+ )
372
+
373
+
374
+ __all__ = [
375
+ "PROMPT",
376
+ "QUESTION_PREFIX",
377
+ "build_context",
378
+ "build_question",
379
+ "chip_map_sources",
380
+ "failure_lines",
381
+ "question_id",
382
+ "spec_lines",
383
+ "spec_sources",
384
+ ]
@@ -0,0 +1,160 @@
1
+ """The deterministic parts of a triage report: the summary, the suggestion, the evidence.
2
+
3
+ The summary is a template over the parsed log (what failed, where), never model text. The
4
+ suggestion follows DESIGN 5.2 per label: `infra` -> fix the environment, then retry
5
+ without counting a try; `rtl`/`tb` -> the file:line to fix; `spec` -> the spec line, and
6
+ ask the spec owner (a person decides a spec change).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from chipgraph.core.contracts import Issue
12
+ from chipgraph.packs.assist.triage.contract import SpecLine, TriageLabel
13
+ from chipgraph.packs.assist.triage.parse import ParsedLog
14
+ from chipgraph.packs.assist.triage.rules import (
15
+ RuleHit,
16
+ TriageFacts,
17
+ is_rtl_path,
18
+ is_tb_path,
19
+ where,
20
+ )
21
+
22
+ MAX_EVIDENCE = 10
23
+
24
+
25
+ def _issue_text(issue: Issue) -> str:
26
+ loc = where(issue)
27
+ rule = f" [{issue.rule}]" if issue.rule else ""
28
+ return f"{loc or '(no file)'}{rule} {issue.msg}".strip()
29
+
30
+
31
+ def _count(n: int, word: str) -> str:
32
+ return f"{n} {word}" + ("" if n == 1 else "s")
33
+
34
+
35
+ def summary(parsed: ParsedLog) -> str:
36
+ """What failed and where, in one or two sentences."""
37
+ checks = parsed.failing_checks
38
+ if checks:
39
+ first = checks[0]
40
+ scope = f" for block {first.block}" if first.block else ""
41
+ verb = "could not run" if first.status == "error" else "failed"
42
+ text = f"check {first.check_id}{scope} {verb} with {_count(len(first.issues), 'issue')}"
43
+ if first.issues:
44
+ text += f"; first: {_issue_text(first.issues[0])}"
45
+ if len(checks) > 1:
46
+ text += f" (and {_count(len(checks) - 1, 'other failing check')})"
47
+ return text
48
+ if parsed.sim_failures:
49
+ text = (
50
+ f"simulation failed {_count(len(parsed.sim_failures), 'self-check')}; first: "
51
+ f"{parsed.sim_failures[0]}"
52
+ )
53
+ stop = next((i for i in parsed.runtime_issues if i.file), None)
54
+ if stop is not None:
55
+ text += f" (stopped at {where(stop)})"
56
+ return text
57
+ errors = [i for i in parsed.tool_issues if i.severity == "error"]
58
+ warnings = [i for i in parsed.tool_issues if i.severity == "warning"]
59
+ if parsed.tool_issues:
60
+ top = (errors or warnings or list(parsed.tool_issues))[0]
61
+ return (
62
+ f"{_count(len(errors), 'error')} and {_count(len(warnings), 'warning')} from the "
63
+ f"tool; first: {_issue_text(top)}"
64
+ )
65
+ if parsed.unlocated_errors:
66
+ return f"the tool stopped: {parsed.unlocated_errors[0]}"
67
+ if parsed.first_error:
68
+ return f"the run failed: {parsed.first_error.splitlines()[0].strip()}"
69
+ return "no error message found in the log"
70
+
71
+
72
+ def evidence(
73
+ label: TriageLabel | None,
74
+ parsed: ParsedLog,
75
+ facts: TriageFacts,
76
+ hit: RuleHit | None,
77
+ specs: tuple[SpecLine, ...] = (),
78
+ ) -> tuple[str, ...]:
79
+ """The `file:line` locations behind the label: the rule's, else the parsed ones."""
80
+ out: list[str] = list(hit.evidence) if hit is not None else []
81
+ if not out and label != "infra":
82
+ issues = list(parsed.issues)
83
+ if label == "tb":
84
+ issues = [
85
+ i for i in issues if i.file and is_tb_path(i.file, facts.tb_patterns)
86
+ ] or issues
87
+ elif label == "rtl":
88
+ issues = [
89
+ i for i in issues if i.file and is_rtl_path(i.file, facts.tb_patterns)
90
+ ] or issues
91
+ for issue in issues:
92
+ loc = where(issue)
93
+ if loc and loc not in out:
94
+ out.append(loc)
95
+ if label == "spec" or (label in ("rtl", "tb") and parsed.sim_failures):
96
+ out.extend(s.location for s in specs[:3] if s.location not in out)
97
+ return tuple(out[:MAX_EVIDENCE])
98
+
99
+
100
+ def _first_spec_location(parsed: ParsedLog, facts: TriageFacts, specs: tuple[SpecLine, ...]) -> str:
101
+ for issue in parsed.issues:
102
+ if (
103
+ issue.file
104
+ and not is_rtl_path(issue.file, facts.tb_patterns)
105
+ and not is_tb_path(issue.file, facts.tb_patterns)
106
+ ):
107
+ return where(issue)
108
+ if specs:
109
+ return specs[0].location
110
+ return next((where(i) for i in parsed.issues if i.file), "")
111
+
112
+
113
+ def _first_location(issues: tuple[Issue, ...], wanted: str, facts: TriageFacts) -> str:
114
+ # Errors first: a warning next to an error is rarely what has to change.
115
+ for issue in sorted(issues, key=lambda i: i.severity != "error"):
116
+ if not issue.file:
117
+ continue
118
+ if wanted == "tb" and is_tb_path(issue.file, facts.tb_patterns):
119
+ return where(issue)
120
+ if wanted == "rtl" and is_rtl_path(issue.file, facts.tb_patterns):
121
+ return where(issue)
122
+ return ""
123
+
124
+
125
+ def suggestion(
126
+ label: TriageLabel,
127
+ parsed: ParsedLog,
128
+ facts: TriageFacts,
129
+ hit: RuleHit | None,
130
+ specs: tuple[SpecLine, ...] = (),
131
+ ) -> str:
132
+ """What to do next for `label`."""
133
+ rerun = f"rerun `{facts.check_id}`" if facts.check_id else "rerun it"
134
+ if label == "infra":
135
+ fix = hit.hint if hit is not None and hit.hint else "fix the environment the run needs"
136
+ return (
137
+ f"Not a design problem: {fix}. Then {rerun}; the retry does not count a try "
138
+ "(DESIGN 5.2)."
139
+ )
140
+ if label == "spec":
141
+ loc = _first_spec_location(parsed, facts, specs)
142
+ at = f" at {loc}" if loc else ""
143
+ return (
144
+ f"The spec is the side to change{at}. Ask the spec owner to confirm before "
145
+ "anything is changed: a spec change is a person's decision."
146
+ )
147
+ side = "testbench" if label == "tb" else "RTL"
148
+ if parsed.sim_failures:
149
+ against = f" against {specs[0].location}" if specs else " against the spec"
150
+ return (
151
+ f"Fix the {side} behind the failing self-check ({parsed.sim_failures[0]}); "
152
+ f"check it{against}, then {rerun}."
153
+ )
154
+ loc = _first_location(parsed.issues, label, facts)
155
+ if loc:
156
+ return f"Fix the {side} at {loc}, then {rerun}."
157
+ return f"Fix the {side} named in the log, then {rerun}."
158
+
159
+
160
+ __all__ = ["evidence", "suggestion", "summary"]