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,371 @@
1
+ """Read a failing log into structured facts: issues, check results, simulation failures.
2
+
3
+ Three kinds of input are understood, and may be mixed:
4
+
5
+ - a tool log (Verilator and other HDL tools, `make`, a shell): issues come from the
6
+ existing log parsers (`VerilatorParser`, and a `GenericRegexParser` for the common
7
+ `file:line: [error:] message` shape), plus located-less `%Error:` lines;
8
+ - the text output of `chipgraph check` (`FAIL <check> <block> N issues` and indented
9
+ `file:line [rule] message` lines), or its `--json` output (`CheckResult` objects);
10
+ - a simulation run: runtime messages (`[time] %Error`, `Verilog $stop`, `Assertion
11
+ failed`, `UVM_ERROR`, ...), a self-checking testbench's failure lines (`FAIL ...`,
12
+ `... expected X got Y`), and the messages of its failed assertions (a testbench that
13
+ reports with `$error` or `assert` rather than a `FAIL` line).
14
+
15
+ Nothing here decides a class; `rules` does, from these facts.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import re
22
+ from dataclasses import dataclass, field
23
+
24
+ from chipgraph.adapters.parser import GenericRegexParser, VerilatorParser
25
+ from chipgraph.core.contracts import CheckResult, Issue
26
+ from chipgraph.core.contracts.types import CheckStatus
27
+
28
+ MAX_EXCERPT_LINES = 60
29
+ MAX_EXCERPT_CHARS = 4000
30
+ FIRST_ERROR_LINES = 8
31
+
32
+ _ANSI = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
33
+
34
+ _CHECK_LINE = re.compile(
35
+ r"^(?P<status>PASS|FAIL|ERROR|SKIPPED)\s+(?P<check>\S+)\s+(?P<block>\S+)\s+\d+ issues?\s*$"
36
+ )
37
+ _CHECK_ISSUE = re.compile(
38
+ r"^\s{2,}(?P<file>\S+?):(?P<line>\d+|-)\s{2}\[(?P<rule>[^\]]*)\]\s(?P<msg>.*)$"
39
+ )
40
+ _STATUS: dict[str, CheckStatus] = {
41
+ "PASS": "pass",
42
+ "FAIL": "fail",
43
+ "ERROR": "error",
44
+ "SKIPPED": "skipped",
45
+ }
46
+
47
+ _HDL_LINE = (
48
+ r"^(?P<file>[^\s:'\"]+\.(?:sv|svh|v|vh|vhd|vhdl)):(?P<line>\d+):(?:\d+:)?\s*"
49
+ r"(?:(?P<severity>[Ee]rror|[Ww]arning)\s*:?\s*)?(?P<msg>\S.*)$"
50
+ )
51
+ """`file:line[:col]: [error|warning:] message`, as iverilog, slang, verible print it."""
52
+
53
+ _UNLOCATED = re.compile(r"^\s*%(?P<kind>Error|Fatal)(?:-(?P<rule>[A-Za-z0-9_]+))?:\s+(?P<msg>.*)$")
54
+ _LOCATED = re.compile(r"^[^\s:]+:\d+(?::\d+)?:")
55
+ _SUMMARY = re.compile(r"^Exiting due to\b")
56
+
57
+ _RUNTIME = re.compile(
58
+ r"^\s*\[\s*\d+(?:\.\d+)?\s*(?:[a-z]?s)?\]\s*%(?:Error|Fatal|Warning)"
59
+ r"|Verilog \$(?:stop|fatal)\b"
60
+ r"|\bAssertion failed\b"
61
+ r"|^\s*- S i m u l a t i o n\b"
62
+ r"|\bUVM_(?:ERROR|FATAL)\b"
63
+ r"|^\s*\*\* (?:Error|Fatal):.*\bTime:"
64
+ r"|\$finish called\b"
65
+ )
66
+ """Messages only a running simulation prints (not a compiler or a linter)."""
67
+
68
+ _SIM_FAILURE = re.compile(
69
+ r"^\s*(?:\[[^\]]*\]\s*)?(?:FAIL(?:ED|URE)?|MISMATCH)\b"
70
+ r"|(?i:\bexpected\b.*\b(?:got|actual|but (?:got|was|read))\b)"
71
+ r"|\bUVM_(?:ERROR|FATAL)\b(?!\s*:\s*0\b)"
72
+ )
73
+ """A self-checking testbench's failure line."""
74
+
75
+ _ASSERTION_FAILED = re.compile(r"\bAssertion failed in (?P<scope>[\w.$]+)\s*:\s*(?P<msg>.*\S)")
76
+ _RUNTIME_ERROR = re.compile(
77
+ r"^\s*\[[^\]]*\]\s*%(?:Error|Fatal)(?:-[A-Za-z0-9_]+)?:\s*(?:[^\s:]+:\d+(?::\d+)?:\s*)?"
78
+ r"(?P<msg>.*\S)"
79
+ r"|^\s*\*\* (?:Error|Fatal):\s*(?:[^\s:()]+\(\d+\):\s*)?(?P<msg2>.*?\S)\s*(?:\bTime:.*)?$"
80
+ )
81
+ """A running simulation's assertion or `$error`/`$fatal` message: Verilator's `Assertion
82
+ failed in <scope>: <message>`, a time-stamped `%Error:`, a `** Error: ... Time:` (not the
83
+ `Verilog $stop` line after it)."""
84
+
85
+ _NO_FACT = frozenset(
86
+ {"assert", "assertion", "check", "checks", "error", "fail", "failed", "failure", "test"}
87
+ )
88
+ """Words that say a check failed, not what failed."""
89
+
90
+ _WORDS = re.compile(r"\w+")
91
+
92
+
93
+ def _assertion_message(line: str) -> str | None:
94
+ """The message of a simulation assertion line, or None for none or one that only
95
+ says that something failed (`<tb>: 1 check(s) failed`, `'assert' failed`)."""
96
+ if re.search(r"Verilog \$(?:stop|fatal)\b|\$finish called\b", line):
97
+ return None
98
+ scope = ""
99
+ match = _ASSERTION_FAILED.search(line)
100
+ if match is not None:
101
+ scope, msg = match.group("scope"), match.group("msg")
102
+ else:
103
+ match = _RUNTIME_ERROR.search(line)
104
+ if match is None:
105
+ return None
106
+ msg = match.group("msg") or match.group("msg2") or ""
107
+ msg = msg.strip()
108
+ scope_words = {w.lower() for w in _WORDS.findall(scope)}
109
+ facts = [
110
+ w
111
+ for w in _WORDS.findall(msg)
112
+ if len(w) > 1
113
+ and not w.isdigit()
114
+ and w.lower() not in _NO_FACT
115
+ and w.lower() not in scope_words
116
+ ]
117
+ return msg if facts else None
118
+
119
+
120
+ _ERRORISH = re.compile(
121
+ r"%(?:Error|Fatal|Warning)|\berror\b|\bFAIL|\bERROR\b|make: \*\*\*|not found"
122
+ r"|No such file|timed out|Killed|Permission denied|expected\b.*\bgot\b",
123
+ re.IGNORECASE,
124
+ )
125
+
126
+ _NOISE = re.compile(
127
+ r"^\s*(?::\s*)?\.\.\. (?:For (?:warning|error) description see|Use \"/\* verilator"
128
+ r"|See the manual at)"
129
+ )
130
+ """Verilator's hint lines: links and how to waive, never what failed."""
131
+
132
+ _HDL_PARSER = GenericRegexParser(pattern=_HDL_LINE, severity_group="severity")
133
+ _VERILATOR_PARSER = VerilatorParser()
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class CheckRun:
138
+ """One `chipgraph check` result found in the log."""
139
+
140
+ check_id: str
141
+ block: str | None
142
+ status: CheckStatus
143
+ issues: tuple[Issue, ...] = ()
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class ParsedLog:
148
+ """What `parse_log` found in a log."""
149
+
150
+ text: str
151
+ """The log as read (ANSI colours removed)."""
152
+ tool_issues: tuple[Issue, ...] = ()
153
+ """Located compile/lint issues printed by the tools (not by a running simulation)."""
154
+ unlocated_errors: tuple[str, ...] = ()
155
+ """`%Error:` messages that name no file:line (summary lines left out)."""
156
+ runtime_issues: tuple[Issue, ...] = ()
157
+ """Located messages of a running simulation (`Verilog $stop`, assertion failures)."""
158
+ checks: tuple[CheckRun, ...] = ()
159
+ """`chipgraph check` results found in the log (text or JSON)."""
160
+ sim_failures: tuple[str, ...] = ()
161
+ """A self-checking testbench's failure lines."""
162
+ sim_assertions: tuple[str, ...] = ()
163
+ """The messages of a running simulation's failed assertions and `$error`/`$fatal`
164
+ calls (`Assertion failed in <scope>: <message>` gives `<message>`), without the
165
+ ones that only say that something failed."""
166
+ runtime_lines: tuple[str, ...] = ()
167
+ """Lines only a running simulation prints."""
168
+ first_error: str = ""
169
+ """The first error line and the lines after it."""
170
+ excerpt: str = ""
171
+ """The log shortened for a model: hint lines dropped, long logs cut around errors."""
172
+ lines: tuple[str, ...] = field(default=(), repr=False)
173
+
174
+ @property
175
+ def sim_ran(self) -> bool:
176
+ """True when the log shows a simulation that ran (and failed a check)."""
177
+ return bool(self.runtime_lines or self.sim_failures)
178
+
179
+ @property
180
+ def failing_checks(self) -> tuple[CheckRun, ...]:
181
+ """The check results that did not pass: `fail` or `error`."""
182
+ return tuple(c for c in self.checks if c.status in ("fail", "error"))
183
+
184
+ @property
185
+ def issues(self) -> tuple[Issue, ...]:
186
+ """Every located issue: the tools', then the failing checks', then the runtime's."""
187
+ out = list(self.tool_issues)
188
+ for check in self.failing_checks:
189
+ out.extend(check.issues)
190
+ out.extend(self.runtime_issues)
191
+ return tuple(out)
192
+
193
+
194
+ def _json_checks(text: str) -> tuple[tuple[CheckRun, ...], str] | None:
195
+ """`CheckResult` JSON (one object or a list): the runs and their log tails."""
196
+ stripped = text.strip()
197
+ if not stripped or stripped[0] not in "[{":
198
+ return None
199
+ try:
200
+ data = json.loads(stripped)
201
+ except ValueError:
202
+ return None
203
+ # One CheckResult, a list of them (`chipgraph check --json`), or the MCP `check`
204
+ # tool's `{"ok": ..., "results": [...]}`.
205
+ pending: list[object] = list(data) if isinstance(data, list) else [data]
206
+ runs: list[CheckRun] = []
207
+ tails: list[str] = []
208
+ while pending:
209
+ item = pending.pop(0)
210
+ if isinstance(item, dict) and isinstance(item.get("results"), list):
211
+ pending[:0] = item["results"]
212
+ continue
213
+ try:
214
+ result = CheckResult.model_validate(item)
215
+ except ValueError:
216
+ continue
217
+ runs.append(CheckRun(result.check_id, None, result.status, result.issues))
218
+ if result.log_tail:
219
+ tails.append(result.log_tail)
220
+ if not runs:
221
+ return None
222
+ return tuple(runs), "\n".join(tails)
223
+
224
+
225
+ def _check_runs(lines: list[str]) -> tuple[tuple[CheckRun, ...], set[int]]:
226
+ """`chipgraph check` text results, and the indexes of the lines they used."""
227
+ runs: list[CheckRun] = []
228
+ used: set[int] = set()
229
+ current: tuple[str, str | None, CheckStatus] | None = None
230
+ issues: list[Issue] = []
231
+
232
+ def close() -> None:
233
+ if current is not None:
234
+ runs.append(CheckRun(current[0], current[1], current[2], tuple(issues)))
235
+
236
+ for index, line in enumerate(lines):
237
+ head = _CHECK_LINE.match(line)
238
+ if head is not None:
239
+ close()
240
+ block = head.group("block")
241
+ current = (
242
+ head.group("check"),
243
+ None if block == "-" else block,
244
+ _STATUS[head["status"]],
245
+ )
246
+ issues = []
247
+ used.add(index)
248
+ continue
249
+ item = _CHECK_ISSUE.match(line) if current is not None else None
250
+ if item is not None:
251
+ file = item.group("file")
252
+ line_no = item.group("line")
253
+ issues.append(
254
+ Issue(
255
+ file=None if file == "-" else file,
256
+ line=None if line_no == "-" else int(line_no),
257
+ rule=item.group("rule"),
258
+ msg=item.group("msg").strip(),
259
+ )
260
+ )
261
+ used.add(index)
262
+ continue
263
+ if current is not None and line.strip():
264
+ close()
265
+ current = None
266
+ issues = []
267
+ close()
268
+ return tuple(runs), used
269
+
270
+
271
+ def _excerpt(lines: list[str]) -> str:
272
+ kept = [line for line in lines if not _NOISE.match(line)]
273
+ while kept and not kept[-1].strip():
274
+ kept.pop()
275
+ if len(kept) > MAX_EXCERPT_LINES:
276
+ marks = [i for i, line in enumerate(kept) if _ERRORISH.search(line)]
277
+ keep: set[int] = set(range(10)) | set(range(len(kept) - 10, len(kept)))
278
+ for i in marks:
279
+ keep.update(range(max(0, i - 2), min(len(kept), i + 3)))
280
+ out: list[str] = []
281
+ last = -1
282
+ for i in sorted(keep):
283
+ if i != last + 1:
284
+ out.append("…")
285
+ out.append(kept[i])
286
+ last = i
287
+ if len(out) >= MAX_EXCERPT_LINES:
288
+ out.append("…")
289
+ break
290
+ kept = out
291
+ text = "\n".join(kept)
292
+ if len(text) > MAX_EXCERPT_CHARS:
293
+ text = text[: MAX_EXCERPT_CHARS - 1] + "…"
294
+ return text
295
+
296
+
297
+ def _first_error(lines: list[str]) -> str:
298
+ for index, line in enumerate(lines):
299
+ if _NOISE.match(line):
300
+ continue
301
+ if _ERRORISH.search(line) and not line.lstrip().startswith("ok "):
302
+ block = [ln for ln in lines[index : index + FIRST_ERROR_LINES] if not _NOISE.match(ln)]
303
+ return "\n".join(block).rstrip()
304
+ return ""
305
+
306
+
307
+ def _dedupe(issues: list[Issue]) -> tuple[Issue, ...]:
308
+ seen: set[tuple[str | None, int | None, str]] = set()
309
+ out: list[Issue] = []
310
+ for issue in issues:
311
+ key = (issue.file, issue.line, issue.msg)
312
+ if key not in seen:
313
+ seen.add(key)
314
+ out.append(issue)
315
+ return tuple(out)
316
+
317
+
318
+ def parse_log(text: str) -> ParsedLog:
319
+ """Read `text` (a tool log, `chipgraph check` output, or its JSON) into a `ParsedLog`."""
320
+ text = _ANSI.sub("", text).replace("\r\n", "\n")
321
+ json_runs = _json_checks(text)
322
+ checks: tuple[CheckRun, ...] = ()
323
+ if json_runs is not None:
324
+ checks, text = json_runs
325
+ lines = text.split("\n")
326
+ text_runs, used = _check_runs(lines)
327
+ checks = (*checks, *text_runs)
328
+
329
+ tool: list[Issue] = []
330
+ runtime: list[Issue] = []
331
+ runtime_lines: list[str] = []
332
+ sim_failures: list[str] = []
333
+ sim_assertions: list[str] = []
334
+ unlocated: list[str] = []
335
+ for index, line in enumerate(lines):
336
+ if index in used:
337
+ continue
338
+ if _RUNTIME.search(line):
339
+ runtime_lines.append(line.strip())
340
+ message = _assertion_message(line)
341
+ if message is not None and message not in sim_assertions:
342
+ sim_assertions.append(message)
343
+ if _SIM_FAILURE.search(line):
344
+ sim_failures.append(line.strip())
345
+ bare = _UNLOCATED.match(line)
346
+ if bare is not None:
347
+ msg = bare.group("msg").strip()
348
+ if not _LOCATED.match(msg) and not _SUMMARY.match(msg):
349
+ unlocated.append(line.strip())
350
+ for issue in (*_VERILATOR_PARSER.parse(line), *_HDL_PARSER.parse(line)):
351
+ if _RUNTIME.search(line) or _RUNTIME.search(issue.msg):
352
+ runtime.append(issue)
353
+ else:
354
+ tool.append(issue)
355
+
356
+ return ParsedLog(
357
+ text=text,
358
+ tool_issues=_dedupe(tool),
359
+ unlocated_errors=tuple(unlocated),
360
+ runtime_issues=_dedupe(runtime),
361
+ checks=checks,
362
+ sim_failures=tuple(sim_failures),
363
+ sim_assertions=tuple(sim_assertions),
364
+ runtime_lines=tuple(runtime_lines),
365
+ first_error=_first_error(lines),
366
+ excerpt=_excerpt(lines),
367
+ lines=tuple(lines),
368
+ )
369
+
370
+
371
+ __all__ = ["CheckRun", "ParsedLog", "parse_log"]