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