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,127 @@
|
|
|
1
|
+
"""The `naming` check's rules, as a versioned pydantic data model.
|
|
2
|
+
|
|
3
|
+
A naming rule file (e.g. `src/chipgraph/orgs/qnsc/naming-v1.yml`) declares, as data:
|
|
4
|
+
|
|
5
|
+
- per **object kind** (`module`, `port`, `parameter`, `localparam`, `enum_value`,
|
|
6
|
+
`instance`, `signal`, `memory`, `genvar`, ...) an allowed `pattern`, a `rule` id and a
|
|
7
|
+
`message`;
|
|
8
|
+
- **lexical** rules that apply to every identifier declared in a file (a forbidden
|
|
9
|
+
regex with an optional allow-if escape hatch);
|
|
10
|
+
- a **vocabulary** map of forbidden word -> project term;
|
|
11
|
+
- the `ignore_comment` that exempts a declaration line.
|
|
12
|
+
|
|
13
|
+
The check (`chipgraph.checks.naming`) reads this model; it does not hard-code any
|
|
14
|
+
pattern. Rule ids and messages follow the source document's numbering ("2.1 module",
|
|
15
|
+
"1.5 vocabulary", ...) so a reviewer can compare the file with the document line by line.
|
|
16
|
+
|
|
17
|
+
The JSON Schema of this model is committed at `schemas/formats/naming-rules.schema.json`
|
|
18
|
+
(regenerate with `python -m chipgraph.checks.naming`).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
28
|
+
|
|
29
|
+
# The object kinds a rule file may constrain. These are the classifications the check
|
|
30
|
+
# derives from the pyslang syntax tree, not free-form strings.
|
|
31
|
+
ObjectKind = Literal[
|
|
32
|
+
"module",
|
|
33
|
+
"port",
|
|
34
|
+
"parameter",
|
|
35
|
+
"localparam",
|
|
36
|
+
"enum_value",
|
|
37
|
+
"instance",
|
|
38
|
+
"signal",
|
|
39
|
+
"memory",
|
|
40
|
+
"genvar",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _valid_regex(pattern: str) -> str:
|
|
45
|
+
try:
|
|
46
|
+
re.compile(pattern)
|
|
47
|
+
except re.error as exc: # pragma: no cover - exercised via model validation
|
|
48
|
+
raise ValueError(f"invalid regex {pattern!r}: {exc}") from exc
|
|
49
|
+
return pattern
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _Model(BaseModel):
|
|
53
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class KindRule(_Model):
|
|
57
|
+
"""The allowed-name rule for one object kind: a pattern the whole name must match."""
|
|
58
|
+
|
|
59
|
+
rule: str = Field(description="The rule id, e.g. '2.1 module'.")
|
|
60
|
+
pattern: str = Field(description="A regex the identifier must fully match to pass.")
|
|
61
|
+
message: str = Field(description="Human-readable message shown when the name is rejected.")
|
|
62
|
+
|
|
63
|
+
@field_validator("pattern")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _check_pattern(cls, value: str) -> str:
|
|
66
|
+
return _valid_regex(value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class LexicalRule(_Model):
|
|
70
|
+
"""A lexical rule: a forbidden pattern with an optional allow-if escape hatch."""
|
|
71
|
+
|
|
72
|
+
rule: str = Field(description="The rule id, e.g. '1.1 case'.")
|
|
73
|
+
forbid_pattern: str = Field(description="A regex that, if found in a name, is a violation.")
|
|
74
|
+
allow_if_matches: str | None = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="If set, a name fully matching this regex is exempt from this rule.",
|
|
77
|
+
)
|
|
78
|
+
message: str = Field(description="Human-readable message shown when the rule fires.")
|
|
79
|
+
|
|
80
|
+
@field_validator("forbid_pattern", "allow_if_matches")
|
|
81
|
+
@classmethod
|
|
82
|
+
def _check_pattern(cls, value: str | None) -> str | None:
|
|
83
|
+
return _valid_regex(value) if value is not None else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class VocabularyRule(_Model):
|
|
87
|
+
"""The mandatory-vocabulary rule: each forbidden word maps to the project term."""
|
|
88
|
+
|
|
89
|
+
rule: str = Field(description="The rule id, e.g. '1.5 vocabulary'.")
|
|
90
|
+
replace: dict[str, str] = Field(description="Map of forbidden word -> preferred project term.")
|
|
91
|
+
message: str = Field(description="Human-readable message prefix shown when a word is used.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NamingRules(_Model):
|
|
95
|
+
"""A naming rule file: per-kind patterns, lexical rules, vocabulary and metadata."""
|
|
96
|
+
|
|
97
|
+
schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
|
|
98
|
+
document: str = Field(description="Name of the source naming-rule document.")
|
|
99
|
+
version: str = Field(description="Version of the source document these rules enforce.")
|
|
100
|
+
ignore_comment: str = Field(
|
|
101
|
+
default="naming-check: ignore",
|
|
102
|
+
description="A trailing line comment matching this exempts the declaration line.",
|
|
103
|
+
)
|
|
104
|
+
identifiers: dict[ObjectKind, KindRule] = Field(
|
|
105
|
+
description="Allowed-name rule per object kind (module, port, signal, ...)."
|
|
106
|
+
)
|
|
107
|
+
lexical: tuple[LexicalRule, ...] = Field(
|
|
108
|
+
default=(), description="Lexical rules applied to every identifier declared in a file."
|
|
109
|
+
)
|
|
110
|
+
vocabulary: VocabularyRule | None = Field(
|
|
111
|
+
default=None, description="The mandatory-vocabulary rule, if any."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def schema_json() -> str:
|
|
116
|
+
"""The JSON Schema of a naming rule file, as committed under `schemas/formats/`."""
|
|
117
|
+
return json.dumps(NamingRules.model_json_schema(), indent=2, sort_keys=True) + "\n"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"KindRule",
|
|
122
|
+
"LexicalRule",
|
|
123
|
+
"NamingRules",
|
|
124
|
+
"ObjectKind",
|
|
125
|
+
"VocabularyRule",
|
|
126
|
+
"schema_json",
|
|
127
|
+
]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Syntax-tree helpers shared by the `connect` and `hardcode` cross checks.
|
|
2
|
+
|
|
3
|
+
Both checks look at RTL *structure*, not the elaborated model: `connect` needs the
|
|
4
|
+
hierarchy instances of the top module and their named port connections; `hardcode` needs
|
|
5
|
+
every integer literal a file types, with its decoded value and line, and the comment on
|
|
6
|
+
that line (so a `// contract:` tag can exempt it). Neither elaborates: a single file with
|
|
7
|
+
missing packages still parses (matching `naming`'s single-file, syntax-only parse).
|
|
8
|
+
|
|
9
|
+
Everything here is pure and deterministic and never raises on malformed input: a parse
|
|
10
|
+
error is returned as a `ParseFailure` (via `chipgraph.checks._naming_pyslang.parse_errors`),
|
|
11
|
+
never thrown.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pyslang.syntax import SyntaxKind, SyntaxNode, SyntaxTree
|
|
21
|
+
|
|
22
|
+
from chipgraph.checks._naming_pyslang import ParseFailure, parse_errors, parse_file
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class PortConn:
|
|
27
|
+
"""One named port connection `.port(expr)` of an instance."""
|
|
28
|
+
|
|
29
|
+
port: str
|
|
30
|
+
expr: str
|
|
31
|
+
line: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class Instance:
|
|
36
|
+
"""One hierarchy instance in a module: `module_name inst_name (.p(e), ...)`."""
|
|
37
|
+
|
|
38
|
+
module: str
|
|
39
|
+
name: str
|
|
40
|
+
line: int
|
|
41
|
+
connections: tuple[PortConn, ...] = ()
|
|
42
|
+
|
|
43
|
+
def connection(self, port: str) -> PortConn | None:
|
|
44
|
+
"""The named connection to `port`, or None if it is not connected by name."""
|
|
45
|
+
for conn in self.connections:
|
|
46
|
+
if conn.port == port:
|
|
47
|
+
return conn
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class IntLiteral:
|
|
53
|
+
"""One integer literal token in RTL: its decoded value, line and raw text."""
|
|
54
|
+
|
|
55
|
+
value: int
|
|
56
|
+
line: int
|
|
57
|
+
raw: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(slots=True)
|
|
61
|
+
class ParsedRtl:
|
|
62
|
+
"""The syntax facts one RTL file yields, plus any parse errors."""
|
|
63
|
+
|
|
64
|
+
instances: list[Instance] = field(default_factory=list)
|
|
65
|
+
literals: list[IntLiteral] = field(default_factory=list)
|
|
66
|
+
parse_errors: tuple[ParseFailure, ...] = ()
|
|
67
|
+
line_comments: dict[int, str] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_rtl(path: Path) -> ParsedRtl:
|
|
71
|
+
"""Parse the RTL at `path`, returning its instances, literals and parse errors."""
|
|
72
|
+
tree = parse_file(path)
|
|
73
|
+
text = path.read_text(errors="ignore")
|
|
74
|
+
return ParsedRtl(
|
|
75
|
+
instances=_instances(tree),
|
|
76
|
+
literals=_literals(tree),
|
|
77
|
+
parse_errors=tuple(parse_errors(tree)),
|
|
78
|
+
line_comments=_line_comments(text),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_rtl_text(text: str, name: str = "source") -> ParsedRtl:
|
|
83
|
+
"""Parse RTL `text` (for tests), returning its instances, literals and parse errors."""
|
|
84
|
+
tree = SyntaxTree.fromText(text, name)
|
|
85
|
+
return ParsedRtl(
|
|
86
|
+
instances=_instances(tree),
|
|
87
|
+
literals=_literals(tree),
|
|
88
|
+
parse_errors=tuple(parse_errors(tree)),
|
|
89
|
+
line_comments=_line_comments(text),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------------------
|
|
94
|
+
# instances and their named port connections
|
|
95
|
+
# --------------------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _instances(tree: Any) -> list[Instance]:
|
|
99
|
+
sm = tree.sourceManager
|
|
100
|
+
|
|
101
|
+
def line_of(token: Any) -> int:
|
|
102
|
+
return int(sm.getLineNumber(token.location))
|
|
103
|
+
|
|
104
|
+
out: list[Instance] = []
|
|
105
|
+
for hi in _find(tree.root, SyntaxKind.HierarchyInstantiation):
|
|
106
|
+
module = _text(getattr(hi, "type", None))
|
|
107
|
+
if not module:
|
|
108
|
+
continue
|
|
109
|
+
for inst in _find(hi, SyntaxKind.HierarchicalInstance):
|
|
110
|
+
decl = getattr(inst, "decl", None)
|
|
111
|
+
name = _text(getattr(decl, "name", None)) if decl is not None else ""
|
|
112
|
+
conns: list[PortConn] = []
|
|
113
|
+
for npc in _find(inst, SyntaxKind.NamedPortConnection):
|
|
114
|
+
port = _text(getattr(npc, "name", None))
|
|
115
|
+
if not port:
|
|
116
|
+
continue
|
|
117
|
+
expr = _text(getattr(npc, "expr", None))
|
|
118
|
+
conns.append(PortConn(port=port, expr=expr, line=line_of(npc.getFirstToken())))
|
|
119
|
+
out.append(
|
|
120
|
+
Instance(
|
|
121
|
+
module=module,
|
|
122
|
+
name=name,
|
|
123
|
+
line=line_of(inst.getFirstToken()),
|
|
124
|
+
connections=tuple(conns),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------------------
|
|
131
|
+
# integer literals (tokens, so comments and strings are never scanned)
|
|
132
|
+
# --------------------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
_RADIX = {"b": 2, "o": 8, "d": 10, "h": 16}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _literals(tree: Any) -> list[IntLiteral]:
|
|
138
|
+
"""Every integer literal in `tree`, decoded, in source order.
|
|
139
|
+
|
|
140
|
+
Works on the token stream, so numbers inside comments or strings are never seen. A
|
|
141
|
+
sized literal (`32'h8000_4000`) is three tokens -- the size `IntegerLiteral`, the
|
|
142
|
+
`IntegerBase` (`'h`) and the digits `IntegerLiteral` -- so the size token is skipped
|
|
143
|
+
and the digits are decoded with the base's radix. A bare `IntegerLiteral` with no
|
|
144
|
+
base before or after it is a plain decimal.
|
|
145
|
+
"""
|
|
146
|
+
sm = tree.sourceManager
|
|
147
|
+
tokens = _tokens(tree.root)
|
|
148
|
+
out: list[IntLiteral] = []
|
|
149
|
+
for i, tok in enumerate(tokens):
|
|
150
|
+
kind = str(getattr(tok, "kind", ""))
|
|
151
|
+
if not kind.endswith("IntegerLiteral"):
|
|
152
|
+
continue
|
|
153
|
+
prev = str(getattr(tokens[i - 1], "kind", "")) if i > 0 else ""
|
|
154
|
+
nxt = str(getattr(tokens[i + 1], "kind", "")) if i + 1 < len(tokens) else ""
|
|
155
|
+
raw = str(getattr(tok, "rawText", "") or "")
|
|
156
|
+
line = int(sm.getLineNumber(tok.location))
|
|
157
|
+
if prev.endswith("IntegerBase"):
|
|
158
|
+
base = str(getattr(tokens[i - 1], "rawText", "") or "").lstrip("'").lower()
|
|
159
|
+
value = _decode(raw, _RADIX.get(base[:1], 10))
|
|
160
|
+
elif nxt.endswith("IntegerBase"):
|
|
161
|
+
# A size prefix (`32` in `32'h...`), not a value of its own.
|
|
162
|
+
continue
|
|
163
|
+
else:
|
|
164
|
+
value = _decode(raw, 10)
|
|
165
|
+
if value is not None:
|
|
166
|
+
out.append(IntLiteral(value=value, line=line, raw=raw))
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _decode(digits: str, radix: int) -> int | None:
|
|
171
|
+
"""Decode `digits` (with `_` separators) in `radix`; None if it is not a plain number.
|
|
172
|
+
|
|
173
|
+
A literal with `x`/`z`/`?` bits (`8'bxxxx_xxxx`) has no single value, so it is
|
|
174
|
+
dropped rather than guessed at.
|
|
175
|
+
"""
|
|
176
|
+
cleaned = digits.replace("_", "")
|
|
177
|
+
if not cleaned:
|
|
178
|
+
return None
|
|
179
|
+
try:
|
|
180
|
+
return int(cleaned, radix)
|
|
181
|
+
except ValueError:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --------------------------------------------------------------------------------------
|
|
186
|
+
# shared token/node walking
|
|
187
|
+
# --------------------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _find(node: Any, kind: SyntaxKind) -> list[Any]:
|
|
191
|
+
out: list[Any] = []
|
|
192
|
+
|
|
193
|
+
def walk(n: Any) -> None:
|
|
194
|
+
if n is None:
|
|
195
|
+
return
|
|
196
|
+
if getattr(n, "kind", None) == kind:
|
|
197
|
+
out.append(n)
|
|
198
|
+
if isinstance(n, SyntaxNode):
|
|
199
|
+
for i in range(len(n)):
|
|
200
|
+
try:
|
|
201
|
+
child = n[i]
|
|
202
|
+
except Exception: # pragma: no cover - defensive against odd child slots
|
|
203
|
+
child = None
|
|
204
|
+
walk(child)
|
|
205
|
+
|
|
206
|
+
walk(node)
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _tokens(node: Any) -> list[Any]:
|
|
211
|
+
out: list[Any] = []
|
|
212
|
+
|
|
213
|
+
def walk(n: Any) -> None:
|
|
214
|
+
if n is None:
|
|
215
|
+
return
|
|
216
|
+
if isinstance(n, SyntaxNode):
|
|
217
|
+
for i in range(len(n)):
|
|
218
|
+
try:
|
|
219
|
+
child = n[i]
|
|
220
|
+
except Exception: # pragma: no cover - defensive against odd child slots
|
|
221
|
+
child = None
|
|
222
|
+
walk(child)
|
|
223
|
+
else:
|
|
224
|
+
out.append(n)
|
|
225
|
+
|
|
226
|
+
walk(node)
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _text(node: Any) -> str:
|
|
231
|
+
"""The trimmed source text of `node` (or a token), or '' when it is None/missing."""
|
|
232
|
+
if node is None:
|
|
233
|
+
return ""
|
|
234
|
+
if getattr(node, "isMissing", False):
|
|
235
|
+
return ""
|
|
236
|
+
return str(node).strip()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _line_comments(text: str) -> dict[int, str]:
|
|
240
|
+
"""Map each 1-based line number to its `//` comment text (without the `//`), if any.
|
|
241
|
+
|
|
242
|
+
A best-effort text scan used only to read a `// contract:`-style exemption marker;
|
|
243
|
+
the literals themselves come from the token stream, never from this.
|
|
244
|
+
"""
|
|
245
|
+
out: dict[int, str] = {}
|
|
246
|
+
for i, line in enumerate(text.splitlines(), start=1):
|
|
247
|
+
idx = line.find("//")
|
|
248
|
+
if idx >= 0:
|
|
249
|
+
out[i] = line[idx + 2 :]
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"Instance",
|
|
255
|
+
"IntLiteral",
|
|
256
|
+
"ParsedRtl",
|
|
257
|
+
"PortConn",
|
|
258
|
+
"parse_rtl",
|
|
259
|
+
"parse_rtl_text",
|
|
260
|
+
]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""`CdcStructCheck`: structural clock-domain-crossing check on the Design Model (layer 1).
|
|
2
|
+
|
|
3
|
+
A layer-1 (structure) check (DESIGN.md 4.8): a signal that crosses from one clock domain
|
|
4
|
+
to another must pass through a synchroniser cell declared in the profile (e.g.
|
|
5
|
+
`qnsc_sync`). A crossing that goes straight into a flop of another domain -- directly or
|
|
6
|
+
through plain combinational logic -- is an error.
|
|
7
|
+
|
|
8
|
+
The Design Model does not hold nets or connections (`adapters/tool/pyslang.py`), so this
|
|
9
|
+
check elaborates the block's RTL itself (`_cdc_elaborate`) and analyses clock domains and
|
|
10
|
+
their crossings structurally (`_cdc_analyze`). It is a per-block check: it runs on the RTL
|
|
11
|
+
of the block being checked. Issues cite the destination flop's file and line.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
|
|
15
|
+
* `cdc.unsynchronised` (error): a register samples another domain with no synchroniser.
|
|
16
|
+
* `cdc.unknown_domain` (info): a register samples an input whose clock domain is unknown.
|
|
17
|
+
* `cdc.not_analysed` (info): a path leaves what this structural check resolves.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
|
|
21
|
+
* `sync_cells` (list of str, required): module names that are synchronisers.
|
|
22
|
+
* `scope` (list of glob str, optional): RTL files to analyse; `{block}` is substituted
|
|
23
|
+
from `ctx.params`. Defaults to the files of the RTL modules the model attributes to the
|
|
24
|
+
block, plus the files of every module they instantiate (so a synchroniser or child IP
|
|
25
|
+
the block wires up is elaborated and recognised, not treated as an unknown blackbox).
|
|
26
|
+
* `clock_patterns` (list of str, optional): regexes that recognise a clock port by name
|
|
27
|
+
(default `^i_clk`, `^clk`).
|
|
28
|
+
* `ignore` (list of glob str, optional): files to drop (default: any `vendor` path).
|
|
29
|
+
|
|
30
|
+
Example profile usage (`.chipgraph.yml`):
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
adapters:
|
|
34
|
+
cdc_struct:
|
|
35
|
+
use: cdc_struct
|
|
36
|
+
sync_cells: [qnsc_sync]
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import re
|
|
43
|
+
import time
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
from chipgraph.checks._cdc_analyze import Crossing, Kind, analyse
|
|
47
|
+
from chipgraph.checks._cdc_elaborate import elaborate
|
|
48
|
+
from chipgraph.checks._common import (
|
|
49
|
+
ArgError,
|
|
50
|
+
compute_idempotency_key,
|
|
51
|
+
error_result,
|
|
52
|
+
glob_to_regex,
|
|
53
|
+
iter_repo_files,
|
|
54
|
+
optional_list_of_str,
|
|
55
|
+
require_list_of_str,
|
|
56
|
+
sha256_file,
|
|
57
|
+
)
|
|
58
|
+
from chipgraph.checks._model import ModelUnavailable, block_param, load_model
|
|
59
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
60
|
+
from chipgraph.core.contracts.types import CheckStatus, Severity
|
|
61
|
+
from chipgraph.core.model.entities import ModuleEntity
|
|
62
|
+
from chipgraph.core.model.model import DesignModel
|
|
63
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
64
|
+
|
|
65
|
+
_DEFAULT_CLOCK_PATTERNS = (r"^i_clk", r"^clk")
|
|
66
|
+
_DEFAULT_IGNORE = ("**/vendor/**", "vendor/**")
|
|
67
|
+
|
|
68
|
+
_RULE_BY_KIND: dict[Kind, tuple[str, Severity]] = {
|
|
69
|
+
Kind.UNSYNCHRONISED: ("cdc.unsynchronised", "error"),
|
|
70
|
+
Kind.UNKNOWN_DOMAIN: ("cdc.unknown_domain", "info"),
|
|
71
|
+
Kind.NOT_ANALYSED: ("cdc.not_analysed", "info"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CdcStructCheck:
|
|
76
|
+
"""Checks that every clock-domain crossing goes through a declared synchroniser."""
|
|
77
|
+
|
|
78
|
+
id = "cdc_struct"
|
|
79
|
+
name = "CDC (structural)"
|
|
80
|
+
per_block = True # a block's RTL is analysed at a time
|
|
81
|
+
|
|
82
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
83
|
+
start = time.monotonic()
|
|
84
|
+
args = spec.args
|
|
85
|
+
block = block_param(ctx)
|
|
86
|
+
try:
|
|
87
|
+
sync_cells = frozenset(require_list_of_str(args, "sync_cells"))
|
|
88
|
+
scope = [_substitute(g, block) for g in optional_list_of_str(args, "scope")]
|
|
89
|
+
ignore = optional_list_of_str(args, "ignore") or list(_DEFAULT_IGNORE)
|
|
90
|
+
clock_patterns = optional_list_of_str(args, "clock_patterns") or list(
|
|
91
|
+
_DEFAULT_CLOCK_PATTERNS
|
|
92
|
+
)
|
|
93
|
+
clock_matchers = [re.compile(p) for p in clock_patterns]
|
|
94
|
+
except (ArgError, re.error) as exc:
|
|
95
|
+
return error_result(spec, str(exc), start)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
loaded = load_model(ctx.repo_root)
|
|
99
|
+
except ModelUnavailable as exc:
|
|
100
|
+
return error_result(spec, str(exc), start, rule="model")
|
|
101
|
+
|
|
102
|
+
block_modules = _block_modules(loaded.model, block)
|
|
103
|
+
|
|
104
|
+
scope_files = _scope_files(ctx.repo_root, scope, ignore, loaded.model, block_modules)
|
|
105
|
+
if not scope_files:
|
|
106
|
+
# Nothing to analyse (a block with no RTL yet): a clean pass, not an error.
|
|
107
|
+
return _result(spec, [], scope_files, ctx.repo_root, loaded.build_inputs_hash, start)
|
|
108
|
+
|
|
109
|
+
tops = {m.name for m in block_modules} or None
|
|
110
|
+
elaboration = elaborate(
|
|
111
|
+
(ctx.repo_root / f for f in scope_files),
|
|
112
|
+
sync_cells=sync_cells,
|
|
113
|
+
clock_matchers=clock_matchers,
|
|
114
|
+
repo_root=ctx.repo_root,
|
|
115
|
+
tops=tops,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
issues: list[Issue] = [
|
|
119
|
+
Issue(
|
|
120
|
+
file=diag.file,
|
|
121
|
+
line=diag.line,
|
|
122
|
+
rule="parse",
|
|
123
|
+
severity="error",
|
|
124
|
+
msg=f"elaboration error: {diag.message}",
|
|
125
|
+
)
|
|
126
|
+
for diag in elaboration.diagnostics
|
|
127
|
+
]
|
|
128
|
+
for module in sorted(elaboration.modules):
|
|
129
|
+
for crossing in analyse(elaboration.modules[module]):
|
|
130
|
+
issues.append(_issue(crossing))
|
|
131
|
+
|
|
132
|
+
return _result(spec, issues, scope_files, ctx.repo_root, loaded.build_inputs_hash, start)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _issue(crossing: Crossing) -> Issue:
|
|
136
|
+
rule, severity = _RULE_BY_KIND[crossing.kind]
|
|
137
|
+
return Issue(
|
|
138
|
+
file=crossing.loc.file,
|
|
139
|
+
line=crossing.loc.line,
|
|
140
|
+
rule=rule,
|
|
141
|
+
severity=severity,
|
|
142
|
+
msg=crossing.detail,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _block_modules(model: DesignModel, block: str | None) -> list[ModuleEntity]:
|
|
147
|
+
"""The RTL modules the model attributes to `block` (all modules when no block)."""
|
|
148
|
+
block_key = f"block:{block}" if block else None
|
|
149
|
+
out: list[ModuleEntity] = []
|
|
150
|
+
for entity in model.by_kind("module"):
|
|
151
|
+
assert isinstance(entity, ModuleEntity)
|
|
152
|
+
if block_key is None or entity.block == block_key:
|
|
153
|
+
out.append(entity)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _scope_files(
|
|
158
|
+
repo_root: Path,
|
|
159
|
+
scope: list[str],
|
|
160
|
+
ignore: list[str],
|
|
161
|
+
model: DesignModel,
|
|
162
|
+
block_modules: list[ModuleEntity],
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""The repo-relative RTL files to analyse, deterministically ordered.
|
|
165
|
+
|
|
166
|
+
An explicit `scope` (globs, `{block}` already substituted) wins. Otherwise the files
|
|
167
|
+
of the block's modules, *plus* the files of every module they instantiate (following
|
|
168
|
+
`instantiates` transitively): so a synchroniser or child IP the block wires up is
|
|
169
|
+
elaborated too and can be recognised, instead of being an unknown blackbox. `ignore`
|
|
170
|
+
globs (vendor by default) are dropped either way, so a vendored cell is not analysed
|
|
171
|
+
as a top of its own.
|
|
172
|
+
"""
|
|
173
|
+
ignore_regexes = [glob_to_regex(p) for p in ignore]
|
|
174
|
+
|
|
175
|
+
def kept(rel: str) -> bool:
|
|
176
|
+
return not any(r.fullmatch(rel) for r in ignore_regexes)
|
|
177
|
+
|
|
178
|
+
if scope:
|
|
179
|
+
regexes = [glob_to_regex(g) for g in scope]
|
|
180
|
+
return sorted(
|
|
181
|
+
f
|
|
182
|
+
for f in iter_repo_files(repo_root)
|
|
183
|
+
if any(r.fullmatch(f) for r in regexes) and kept(f)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
files = {m.file for m in _with_instantiated(model, block_modules) if m.file}
|
|
187
|
+
return sorted(f for f in files if kept(f))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _with_instantiated(model: DesignModel, block_modules: list[ModuleEntity]) -> list[ModuleEntity]:
|
|
191
|
+
"""`block_modules` plus every module reachable through `instantiates`, deduped."""
|
|
192
|
+
by_key = {m.key: m for m in model.by_kind("module") if isinstance(m, ModuleEntity)}
|
|
193
|
+
seen: dict[str, ModuleEntity] = {m.key: m for m in block_modules}
|
|
194
|
+
frontier = list(seen)
|
|
195
|
+
while frontier:
|
|
196
|
+
src = frontier.pop()
|
|
197
|
+
for rel in model.get_relations(src=src, kind="instantiates"):
|
|
198
|
+
child = by_key.get(rel.dst)
|
|
199
|
+
if child is not None and child.key not in seen:
|
|
200
|
+
seen[child.key] = child
|
|
201
|
+
frontier.append(child.key)
|
|
202
|
+
return list(seen.values())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _substitute(glob: str, block: str | None) -> str:
|
|
206
|
+
if "{block}" not in glob:
|
|
207
|
+
return glob
|
|
208
|
+
if block is None:
|
|
209
|
+
raise ArgError(f"scope glob {glob!r} uses {{block}} but no block is set in ctx.params")
|
|
210
|
+
return glob.replace("{block}", block)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _result(
|
|
214
|
+
spec: CheckSpec,
|
|
215
|
+
issues: list[Issue],
|
|
216
|
+
scope_files: list[str],
|
|
217
|
+
repo_root: Path,
|
|
218
|
+
build_inputs_hash: str | None,
|
|
219
|
+
start: float,
|
|
220
|
+
) -> CheckResult:
|
|
221
|
+
ordered = sorted(issues, key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
|
|
222
|
+
file_hashes = [(f, sha256_file(repo_root / f)) for f in scope_files]
|
|
223
|
+
file_hashes.append(("<model>", build_inputs_hash or "none"))
|
|
224
|
+
key = compute_idempotency_key(spec.id, spec.args, file_hashes)
|
|
225
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in ordered) else "pass"
|
|
226
|
+
return CheckResult(
|
|
227
|
+
check_id=spec.id,
|
|
228
|
+
status=status,
|
|
229
|
+
issues=tuple(ordered),
|
|
230
|
+
duration_s=time.monotonic() - start,
|
|
231
|
+
idempotency_key=key,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
__all__ = ["CdcStructCheck"]
|