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.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- 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
|
+
"""
|