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
chipgraph/app/checks.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""`ProfileCheckRunner`: maps a check id to a project's `.chipgraph.yml` adapter config
|
|
2
|
+
and runs it, satisfying the scheduler's `CheckRunner` protocol.
|
|
3
|
+
|
|
4
|
+
Backs both `chipgraph check` (run directly, no run id, no caching) and the `checks=`
|
|
5
|
+
argument to `Scheduler` (see `app/build.py`). A run id may be set after construction
|
|
6
|
+
(`chipgraph resume RUN_ID` knows its run id ahead of time); when it is set, results are
|
|
7
|
+
cached in that run's `IdempotencyStore`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from chipgraph.app.context import AppContext
|
|
15
|
+
from chipgraph.app.findings import findings_from_check, layer_for_check
|
|
16
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue, RuleInstance
|
|
17
|
+
from chipgraph.core.plugin_api.protocols import Check, ToolAdapter
|
|
18
|
+
from chipgraph.core.plugin_api.registry import PluginError
|
|
19
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
20
|
+
from chipgraph.core.state.artifacts import hash_inputs
|
|
21
|
+
from chipgraph.core.state.findings import FindingStore
|
|
22
|
+
from chipgraph.core.state.idempotency import IdempotencyStore, make_key
|
|
23
|
+
|
|
24
|
+
_RUNNER_KEYS = frozenset({"use", "per_block", "severity"})
|
|
25
|
+
"""Adapter-config keys the runner itself reads; they are not passed to the plugin."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_SEVERITY_RANK = {"info": 0, "warning": 1, "error": 2}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cap_severity(result: CheckResult, cap: object) -> CheckResult:
|
|
32
|
+
"""Apply a profile's `adapters.<id>.severity` cap to `result` (DESIGN.md 4.8).
|
|
33
|
+
|
|
34
|
+
A project may run a check at a lower severity while it is not yet expected to pass
|
|
35
|
+
(e.g. `trace: { severity: warning }` before DV starts). Every issue that points at a
|
|
36
|
+
file and is above the cap is lowered to it, and a `fail` with no `error` issue left
|
|
37
|
+
becomes `pass`, so the check reports but does not block. An issue with no file says
|
|
38
|
+
the check itself could not run (a missing `make` target, an unreadable input); it is
|
|
39
|
+
not lowered, and neither is a check that returned `error`. A cap that is not one of
|
|
40
|
+
`error`/`warning`/`info` is ignored.
|
|
41
|
+
"""
|
|
42
|
+
if not isinstance(cap, str) or cap not in _SEVERITY_RANK or cap == "error":
|
|
43
|
+
return result
|
|
44
|
+
if result.status == "error":
|
|
45
|
+
return result
|
|
46
|
+
limit = _SEVERITY_RANK[cap]
|
|
47
|
+
issues = tuple(
|
|
48
|
+
issue.model_copy(update={"severity": cap})
|
|
49
|
+
if issue.file is not None and _SEVERITY_RANK[issue.severity] > limit
|
|
50
|
+
else issue
|
|
51
|
+
for issue in result.issues
|
|
52
|
+
)
|
|
53
|
+
status = result.status
|
|
54
|
+
if status == "fail" and not any(i.severity == "error" for i in issues):
|
|
55
|
+
status = "pass"
|
|
56
|
+
return result.model_copy(update={"issues": issues, "status": status})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProfileCheckRunner:
|
|
60
|
+
"""Runs a check id through the adapter `.chipgraph.yml` configures for it."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
ctx: AppContext,
|
|
65
|
+
*,
|
|
66
|
+
run_id: str | None = None,
|
|
67
|
+
root_override: Path | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.ctx = ctx
|
|
70
|
+
self.run_id = run_id
|
|
71
|
+
self.finding_store = FindingStore(ctx.layout)
|
|
72
|
+
self._root = root_override if root_override is not None else ctx.root
|
|
73
|
+
|
|
74
|
+
async def run(self, check_id: str, instance: RuleInstance) -> CheckResult:
|
|
75
|
+
resolved = self.ctx.require_profile()
|
|
76
|
+
block = instance.params.get("block")
|
|
77
|
+
profile = resolved.for_block(block) if block is not None else resolved.profile
|
|
78
|
+
|
|
79
|
+
cfg = profile.adapters.get(check_id)
|
|
80
|
+
if cfg is None:
|
|
81
|
+
return self._record(
|
|
82
|
+
check_id,
|
|
83
|
+
CheckResult(
|
|
84
|
+
check_id=check_id,
|
|
85
|
+
status="error",
|
|
86
|
+
issues=(
|
|
87
|
+
Issue(
|
|
88
|
+
severity="error",
|
|
89
|
+
msg=(
|
|
90
|
+
f"no adapter configured for check {check_id!r}; "
|
|
91
|
+
f"configured adapters: {sorted(profile.adapters)}"
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
duration_s=0.0,
|
|
96
|
+
idempotency_key=make_key({"check_id": check_id, "error": "unknown-check"}),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
plugin = self._resolve_plugin(cfg.use)
|
|
101
|
+
if plugin is None:
|
|
102
|
+
return self._record(
|
|
103
|
+
check_id,
|
|
104
|
+
CheckResult(
|
|
105
|
+
check_id=check_id,
|
|
106
|
+
status="error",
|
|
107
|
+
issues=(
|
|
108
|
+
Issue(
|
|
109
|
+
severity="error",
|
|
110
|
+
msg=(
|
|
111
|
+
f"no check or tool adapter named {cfg.use!r} "
|
|
112
|
+
f"(configured for check {check_id!r})"
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
duration_s=0.0,
|
|
117
|
+
idempotency_key=make_key(
|
|
118
|
+
{"check_id": check_id, "error": "unknown-adapter", "use": cfg.use}
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
args = {k: v for k, v in cfg.model_dump().items() if k not in _RUNNER_KEYS}
|
|
124
|
+
spec = CheckSpec(id=check_id, capability=check_id, adapter=cfg.use, args=args)
|
|
125
|
+
tool_ctx = ToolContext(
|
|
126
|
+
repo_root=self._root,
|
|
127
|
+
runner=self.ctx.registry.get("runner", "local"),
|
|
128
|
+
env=dict(profile.env.vars),
|
|
129
|
+
params=dict(instance.params),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Idempotent resume (DESIGN.md 6.2): a `pass` result is reused when the same
|
|
133
|
+
# check runs again in the same run with the same files. Only plugins with a
|
|
134
|
+
# `key_for(spec, ctx)` (today `CmdTool`) can be keyed before running; the
|
|
135
|
+
# built-in checks fold the files they read into their key inside `run()`, so
|
|
136
|
+
# they always run. The cache key adds the hashes of the instance's inputs and
|
|
137
|
+
# outputs, so editing a file between a kill and `resume` runs the check again.
|
|
138
|
+
# Failures are never reused: a fixed file must be checked again.
|
|
139
|
+
store = IdempotencyStore(self.ctx.layout, self.run_id) if self.run_id is not None else None
|
|
140
|
+
key_for = getattr(plugin, "key_for", None)
|
|
141
|
+
cache_key: str | None = None
|
|
142
|
+
if store is not None and callable(key_for):
|
|
143
|
+
refs = [r for r in (*instance.inputs, *instance.outputs) if r.path is not None]
|
|
144
|
+
files = self.ctx.store.current_hashes(refs)
|
|
145
|
+
cache_key = make_key(
|
|
146
|
+
{"check": str(key_for(spec, tool_ctx)), "files": hash_inputs(files.items())}
|
|
147
|
+
)
|
|
148
|
+
cached = store.get(cache_key)
|
|
149
|
+
if cached is not None and cached.ok:
|
|
150
|
+
return self._record(
|
|
151
|
+
check_id, _cap_severity(cached, cfg.model_dump().get("severity"))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = await plugin.run(spec, tool_ctx)
|
|
155
|
+
|
|
156
|
+
if store is not None and cache_key is not None and result.ok:
|
|
157
|
+
store.put(result.model_copy(update={"idempotency_key": cache_key}))
|
|
158
|
+
|
|
159
|
+
return self._record(check_id, _cap_severity(result, cfg.model_dump().get("severity")))
|
|
160
|
+
|
|
161
|
+
def _record(self, check_id: str, result: CheckResult) -> CheckResult:
|
|
162
|
+
"""Record `result`'s issues as findings, never failing the check for it.
|
|
163
|
+
|
|
164
|
+
Only an `OSError` writing to the finding store is swallowed; anything else
|
|
165
|
+
(e.g. a bug building a `Finding`) is a real error and propagates.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
layer = layer_for_check(check_id, result.check_id)
|
|
169
|
+
findings = findings_from_check(
|
|
170
|
+
result, layer=layer, store=self.ctx.store, run_id=self.run_id
|
|
171
|
+
)
|
|
172
|
+
if findings:
|
|
173
|
+
self.finding_store.upsert(findings)
|
|
174
|
+
except OSError:
|
|
175
|
+
pass
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
def runs_per_block(self, check_id: str) -> bool:
|
|
179
|
+
"""Whether `chipgraph check` runs `check_id` once per block (the default).
|
|
180
|
+
|
|
181
|
+
A profile's `adapters.<id>.per_block` wins; otherwise the plugin's own `per_block`
|
|
182
|
+
attribute (chip-wide checks set it to False); otherwise True.
|
|
183
|
+
"""
|
|
184
|
+
cfg = self.ctx.require_profile().profile.adapters.get(check_id)
|
|
185
|
+
if cfg is None:
|
|
186
|
+
return True
|
|
187
|
+
override = cfg.model_dump().get("per_block")
|
|
188
|
+
if isinstance(override, bool):
|
|
189
|
+
return override
|
|
190
|
+
plugin = self._resolve_plugin(cfg.use)
|
|
191
|
+
return bool(getattr(plugin, "per_block", True))
|
|
192
|
+
|
|
193
|
+
def _resolve_plugin(self, use: str) -> Check | ToolAdapter | None:
|
|
194
|
+
for kind in ("check", "tool"):
|
|
195
|
+
try:
|
|
196
|
+
return self.ctx.registry.get(kind, use) # type: ignore[no-any-return]
|
|
197
|
+
except PluginError:
|
|
198
|
+
continue
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = ["ProfileCheckRunner"]
|
chipgraph/app/context.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""`AppContext`: everything a CLI command needs, wired up from a project's `.chipgraph.yml`.
|
|
2
|
+
|
|
3
|
+
Read-only commands (`status`, `doctor`, `config show`, ...) must work even when no profile
|
|
4
|
+
exists yet (DESIGN.md 8.4/6.1): `AppContext.load` always succeeds, and only writes to disk
|
|
5
|
+
(state directory, git exclude) when a profile was actually found. Commands that need a
|
|
6
|
+
profile call `require_profile()`, which raises a clear `AppError`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from chipgraph.adapters.review.file import FileReview, ReviewError
|
|
17
|
+
from chipgraph.adapters.review.github import GitHubReview
|
|
18
|
+
from chipgraph.app.errors import AppError
|
|
19
|
+
from chipgraph.core.config.errors import ConfigError
|
|
20
|
+
from chipgraph.core.config.loader import ResolvedProfile
|
|
21
|
+
from chipgraph.core.config.loader import load as load_profile
|
|
22
|
+
from chipgraph.core.config.models import DataCfg, Profile
|
|
23
|
+
from chipgraph.core.engine.gate import GateEvaluator
|
|
24
|
+
from chipgraph.core.plugin_api.local import LocalPluginRecord, load_local_plugins
|
|
25
|
+
from chipgraph.core.plugin_api.registry import PluginError, Registry
|
|
26
|
+
from chipgraph.core.state.artifacts import ArtifactStore, LabelRules
|
|
27
|
+
from chipgraph.core.state.backend import LocalBackend
|
|
28
|
+
from chipgraph.core.state.layout import StateLayout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_repo_root(start: Path) -> Path:
|
|
32
|
+
"""The nearest ancestor of `start` containing `.git`, or `start` itself if none.
|
|
33
|
+
|
|
34
|
+
Mirrors `chipgraph.core.config.loader.find_profile`'s own boundary, but always
|
|
35
|
+
returns a directory (never `None`): a project root is needed even when no profile
|
|
36
|
+
exists yet, so read-only commands (and `init`) have somewhere to work from.
|
|
37
|
+
"""
|
|
38
|
+
current = start.resolve()
|
|
39
|
+
if current.is_file():
|
|
40
|
+
current = current.parent
|
|
41
|
+
walker = current
|
|
42
|
+
while True:
|
|
43
|
+
if (walker / ".git").exists():
|
|
44
|
+
return walker
|
|
45
|
+
parent = walker.parent
|
|
46
|
+
if parent == walker:
|
|
47
|
+
return current
|
|
48
|
+
walker = parent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class AppContext:
|
|
53
|
+
"""Everything wired up for one CLI invocation: profile, state, store, registry, ..."""
|
|
54
|
+
|
|
55
|
+
root: Path
|
|
56
|
+
resolved: ResolvedProfile | None
|
|
57
|
+
layout: StateLayout
|
|
58
|
+
store: ArtifactStore
|
|
59
|
+
registry: Registry
|
|
60
|
+
review: FileReview
|
|
61
|
+
gates: GateEvaluator
|
|
62
|
+
backend: LocalBackend | None = None
|
|
63
|
+
local_plugins: tuple[LocalPluginRecord, ...] = ()
|
|
64
|
+
pr_review: GitHubReview | None = None
|
|
65
|
+
"""The PR review adapter when the profile selects `review: {use: github}`; `review`
|
|
66
|
+
stays the `file` adapter, which keeps waivers, baselines and light gates."""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def profile(self) -> Profile | None:
|
|
70
|
+
"""The resolved project profile, or `None` if no `.chipgraph.yml` was found."""
|
|
71
|
+
return self.resolved.profile if self.resolved is not None else None
|
|
72
|
+
|
|
73
|
+
def require_profile(self) -> ResolvedProfile:
|
|
74
|
+
"""Return the resolved profile, or raise `AppError` if there is none."""
|
|
75
|
+
if self.resolved is None:
|
|
76
|
+
raise AppError("no .chipgraph.yml: only read-only commands work; run `chipgraph init`")
|
|
77
|
+
return self.resolved
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def load(cls, start: Path, *, profile_path: Path | None = None) -> AppContext:
|
|
81
|
+
"""Build an `AppContext` for a command starting at `start`.
|
|
82
|
+
|
|
83
|
+
`start` is searched upward for `.chipgraph.yml` (or `profile_path` is used
|
|
84
|
+
directly); the project root is the nearest git root above `start` (or `start`
|
|
85
|
+
itself, if it is not inside a git repo). State is only written to disk (state
|
|
86
|
+
directory created, `.git/info/exclude` updated) when a profile was found.
|
|
87
|
+
"""
|
|
88
|
+
root = find_repo_root(start)
|
|
89
|
+
try:
|
|
90
|
+
resolved = load_profile(start, profile_path=profile_path)
|
|
91
|
+
except ConfigError as exc:
|
|
92
|
+
raise AppError(str(exc)) from exc
|
|
93
|
+
|
|
94
|
+
layout = StateLayout(root)
|
|
95
|
+
backend: LocalBackend | None = None
|
|
96
|
+
if resolved is not None:
|
|
97
|
+
backend = LocalBackend(root)
|
|
98
|
+
backend.prepare()
|
|
99
|
+
|
|
100
|
+
data_cfg = resolved.profile.data if resolved is not None else DataCfg()
|
|
101
|
+
labels = LabelRules(dict.fromkeys(data_cfg.nda_paths, "nda"), default=data_cfg.default)
|
|
102
|
+
store = ArtifactStore(root, labels)
|
|
103
|
+
|
|
104
|
+
registry = Registry()
|
|
105
|
+
try:
|
|
106
|
+
registry.discover()
|
|
107
|
+
except PluginError as exc:
|
|
108
|
+
raise AppError(str(exc)) from exc
|
|
109
|
+
|
|
110
|
+
local_plugins = cls._load_local_plugins(root, resolved, registry)
|
|
111
|
+
|
|
112
|
+
review = FileReview(layout.decisions_dir)
|
|
113
|
+
gates = GateEvaluator(review, store)
|
|
114
|
+
|
|
115
|
+
pr_review: GitHubReview | None = None
|
|
116
|
+
review_cfg = resolved.profile.review if resolved is not None else None
|
|
117
|
+
if review_cfg is not None and review_cfg.use == "github":
|
|
118
|
+
try:
|
|
119
|
+
pr_review = GitHubReview.from_config(review_cfg, state_dir=layout.state_dir)
|
|
120
|
+
except ReviewError as exc:
|
|
121
|
+
raise AppError(f"review: {exc}") from exc
|
|
122
|
+
|
|
123
|
+
return cls(
|
|
124
|
+
root=root,
|
|
125
|
+
resolved=resolved,
|
|
126
|
+
layout=layout,
|
|
127
|
+
store=store,
|
|
128
|
+
registry=registry,
|
|
129
|
+
review=review,
|
|
130
|
+
gates=gates,
|
|
131
|
+
backend=backend,
|
|
132
|
+
local_plugins=local_plugins,
|
|
133
|
+
pr_review=pr_review,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _load_local_plugins(
|
|
138
|
+
root: Path, resolved: ResolvedProfile | None, registry: Registry
|
|
139
|
+
) -> tuple[LocalPluginRecord, ...]:
|
|
140
|
+
"""Load the profile's local plugins into `registry`, honouring policy and env.
|
|
141
|
+
|
|
142
|
+
Disabled either by `policy.local_plugins: deny` (an org may turn it off) or by the
|
|
143
|
+
`CHIPGRAPH_LOCAL_PLUGINS=0` personal safety switch; the env switch can only disable,
|
|
144
|
+
never enable. A `PluginError` while loading becomes an `AppError`.
|
|
145
|
+
"""
|
|
146
|
+
if resolved is None or not resolved.profile.plugins:
|
|
147
|
+
return ()
|
|
148
|
+
|
|
149
|
+
entries = resolved.profile.plugins
|
|
150
|
+
disabled_reason: str | None = None
|
|
151
|
+
if resolved.profile.policy.local_plugins == "deny":
|
|
152
|
+
source = resolved.provenance.get("policy.local_plugins")
|
|
153
|
+
where = source.location if source is not None else "profile"
|
|
154
|
+
disabled_reason = f"disabled by {where}"
|
|
155
|
+
elif os.environ.get("CHIPGRAPH_LOCAL_PLUGINS") == "0":
|
|
156
|
+
disabled_reason = "disabled by CHIPGRAPH_LOCAL_PLUGINS=0"
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
return load_local_plugins(
|
|
160
|
+
root,
|
|
161
|
+
entries,
|
|
162
|
+
registry,
|
|
163
|
+
enabled=disabled_reason is None,
|
|
164
|
+
disabled_reason=disabled_reason,
|
|
165
|
+
)
|
|
166
|
+
except PluginError as exc:
|
|
167
|
+
raise AppError(str(exc)) from exc
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def default_identity() -> str:
|
|
171
|
+
"""The identity to attribute a decision to when `--by` is not given.
|
|
172
|
+
|
|
173
|
+
`git config user.name`, falling back to the `$USER` environment variable, then
|
|
174
|
+
`"unknown"`. Never raises: a missing/broken `git` just falls through.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
result = subprocess.run(
|
|
178
|
+
["git", "config", "user.name"],
|
|
179
|
+
capture_output=True,
|
|
180
|
+
text=True,
|
|
181
|
+
timeout=5,
|
|
182
|
+
check=False,
|
|
183
|
+
)
|
|
184
|
+
except (OSError, subprocess.SubprocessError):
|
|
185
|
+
result = None
|
|
186
|
+
if result is not None and result.returncode == 0 and result.stdout.strip():
|
|
187
|
+
return result.stdout.strip()
|
|
188
|
+
return os.environ.get("USER", "unknown")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["AppContext", "default_identity", "find_repo_root"]
|
chipgraph/app/errors.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""`AppError`: raised for application-layer problems the CLI reports as a usage error."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AppError(Exception):
|
|
7
|
+
"""A problem wiring the engine to a concrete project: bad profile, missing pack,
|
|
8
|
+
missing adapter, no profile for a command that needs one, and so on.
|
|
9
|
+
|
|
10
|
+
The CLI catches this (and `chipgraph.core.config.errors.ConfigError`) and exits 2.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""`Executor`s for the scheduler: one per `RuleKind` (DESIGN.md 3.3, 3.4).
|
|
2
|
+
|
|
3
|
+
`gen` and `import` rules run through a tool adapter (`rule.run`); `human` rules just
|
|
4
|
+
check whether their outputs exist yet; `agent` rules use the scheduler's own
|
|
5
|
+
`AgentStub` (agent rules arrive in M2).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from chipgraph.app.context import AppContext
|
|
11
|
+
from chipgraph.core.contracts import CheckSpec, RuleInstance, RuleSpec
|
|
12
|
+
from chipgraph.core.contracts.types import FailureLabel
|
|
13
|
+
from chipgraph.core.engine.scheduler import ExecOutcome
|
|
14
|
+
from chipgraph.core.plugin_api.registry import PluginError
|
|
15
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RunExecutor:
|
|
19
|
+
"""Runs a `gen`/`import` rule's `run` spec through a tool adapter."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, ctx: AppContext) -> None:
|
|
22
|
+
self.ctx = ctx
|
|
23
|
+
|
|
24
|
+
async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
|
|
25
|
+
if rule.run is None:
|
|
26
|
+
return ExecOutcome(
|
|
27
|
+
ok=False, failure_label="infra", message=f"rule {rule.id!r} has no 'run'"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
tool = self.ctx.registry.get("tool", rule.run.use)
|
|
32
|
+
except PluginError as exc:
|
|
33
|
+
return ExecOutcome(ok=False, failure_label="infra", message=str(exc))
|
|
34
|
+
|
|
35
|
+
spec = CheckSpec(
|
|
36
|
+
id=rule.id, capability=rule.kind, adapter=rule.run.use, args=dict(rule.run.args)
|
|
37
|
+
)
|
|
38
|
+
env = dict(self.ctx.profile.env.vars) if self.ctx.profile is not None else {}
|
|
39
|
+
tool_ctx = ToolContext(
|
|
40
|
+
repo_root=self.ctx.root,
|
|
41
|
+
runner=self.ctx.registry.get("runner", "local"),
|
|
42
|
+
env=env,
|
|
43
|
+
params=dict(instance.params),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result = await tool.run(spec, tool_ctx)
|
|
47
|
+
if result.status != "pass":
|
|
48
|
+
label: FailureLabel = "verification" if result.status == "fail" else "infra"
|
|
49
|
+
message = (
|
|
50
|
+
"; ".join(issue.msg for issue in result.issues)
|
|
51
|
+
or result.log_tail
|
|
52
|
+
or f"tool {rule.run.use!r} did not pass for rule {rule.id!r}"
|
|
53
|
+
)
|
|
54
|
+
return ExecOutcome(ok=False, failure_label=label, message=message)
|
|
55
|
+
return ExecOutcome(ok=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class HumanExecutor:
|
|
59
|
+
"""A `human` rule 'runs' by having a person write its outputs by hand."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, ctx: AppContext) -> None:
|
|
62
|
+
self.ctx = ctx
|
|
63
|
+
|
|
64
|
+
async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
|
|
65
|
+
missing = [
|
|
66
|
+
ref.path
|
|
67
|
+
for ref in instance.outputs
|
|
68
|
+
if ref.path is not None and not self.ctx.store.exists(ref)
|
|
69
|
+
]
|
|
70
|
+
if missing:
|
|
71
|
+
return ExecOutcome(
|
|
72
|
+
ok=False,
|
|
73
|
+
failure_label="planning",
|
|
74
|
+
message=f"waiting for a person to write {', '.join(missing)}",
|
|
75
|
+
)
|
|
76
|
+
return ExecOutcome(ok=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["HumanExecutor", "RunExecutor"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""App-layer glue for findings: `CheckResult` -> `Finding`, and waiving a finding.
|
|
2
|
+
|
|
3
|
+
Core (`chipgraph.core.contracts.finding`, `chipgraph.core.state.findings`) defines what a
|
|
4
|
+
finding and a waiver are; this module wires them to the rest of the app: mapping a check
|
|
5
|
+
id to a DESIGN.md 4.8 layer, turning a check's `Issue`s into `Finding`s, and recording a
|
|
6
|
+
waiver decision through the `ReviewAdapter` (`chipgraph.adapters.review.file.FileReview`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
|
|
15
|
+
from chipgraph.app.errors import AppError
|
|
16
|
+
from chipgraph.core.contracts import Approval, ArtifactRef, CheckResult
|
|
17
|
+
from chipgraph.core.contracts.finding import Evidence, Finding, FindingSeverity
|
|
18
|
+
from chipgraph.core.plugin_api.protocols import ReviewAdapter
|
|
19
|
+
from chipgraph.core.state.artifacts import ArtifactStore
|
|
20
|
+
from chipgraph.core.state.findings import FindingStore, waiver_gate_id
|
|
21
|
+
|
|
22
|
+
# Layer per DESIGN.md 4.8's table, keyed by check id (falling back to capability, then to
|
|
23
|
+
# layer 1, "cấu trúc và số liệu", which is where most cross-artifact checks land). Checks
|
|
24
|
+
# not listed here (including ones not yet built, e.g. M1-07/M1-19) default to layer 1;
|
|
25
|
+
# add them here as they are built if they belong to a different layer.
|
|
26
|
+
LAYER_BY_CHECK: Mapping[str, int] = {
|
|
27
|
+
# layer 1: structural/deterministic cross-checks
|
|
28
|
+
"spec_schema": 1,
|
|
29
|
+
"cross_chip": 1,
|
|
30
|
+
"ports_diff": 1,
|
|
31
|
+
"hardcode": 1,
|
|
32
|
+
"duplicate": 1,
|
|
33
|
+
"connect": 1,
|
|
34
|
+
"cdc_struct": 1,
|
|
35
|
+
# layer 4: lint / synth "smells"
|
|
36
|
+
"lint": 4,
|
|
37
|
+
"layout": 4,
|
|
38
|
+
"synth": 4,
|
|
39
|
+
"naming": 4,
|
|
40
|
+
# layer 5: process (traceability, staleness, gate/approval hygiene)
|
|
41
|
+
"trace": 5,
|
|
42
|
+
}
|
|
43
|
+
_DEFAULT_LAYER = 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def layer_for_check(check_id: str, capability: str = "") -> int:
|
|
47
|
+
"""The DESIGN.md 4.8 layer a check's findings belong to, defaulting to layer 1."""
|
|
48
|
+
if check_id in LAYER_BY_CHECK:
|
|
49
|
+
return LAYER_BY_CHECK[check_id]
|
|
50
|
+
if capability in LAYER_BY_CHECK:
|
|
51
|
+
return LAYER_BY_CHECK[capability]
|
|
52
|
+
return _DEFAULT_LAYER
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _finding_severity(issue_severity: str) -> FindingSeverity:
|
|
56
|
+
# `Issue.severity` is "error" | "warning" | "info"; `Finding.severity` has no "info".
|
|
57
|
+
if issue_severity == "info":
|
|
58
|
+
return "warning"
|
|
59
|
+
if issue_severity == "error":
|
|
60
|
+
return "error"
|
|
61
|
+
return "warning"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def findings_from_check(
|
|
65
|
+
result: CheckResult,
|
|
66
|
+
*,
|
|
67
|
+
layer: int = 1,
|
|
68
|
+
store: ArtifactStore | None = None,
|
|
69
|
+
run_id: str | None = None,
|
|
70
|
+
) -> list[Finding]:
|
|
71
|
+
"""Build one `Finding` per `Issue` in `result`.
|
|
72
|
+
|
|
73
|
+
An issue with a `file` gets that as its evidence's `file`/`line`; if `store` is
|
|
74
|
+
given, the file's current content hash is recorded in `artifacts`/`artifact_hashes`
|
|
75
|
+
(what a waiver against this finding would bind to). An issue with no `file` (e.g. a
|
|
76
|
+
whole-check configuration error) has nowhere else to point at, so its evidence uses
|
|
77
|
+
the check id itself as a Design-Model-style key: `model_key="check:<check_id>"`.
|
|
78
|
+
|
|
79
|
+
All findings from a check are deterministic (`source="check:<check_id>"`), so they
|
|
80
|
+
may carry `severity="error"`.
|
|
81
|
+
"""
|
|
82
|
+
now = datetime.now(UTC)
|
|
83
|
+
findings: list[Finding] = []
|
|
84
|
+
for issue in result.issues:
|
|
85
|
+
if issue.file is not None:
|
|
86
|
+
evidence = Evidence(file=issue.file, line=issue.line, note=issue.rule)
|
|
87
|
+
artifacts: tuple[ArtifactRef, ...] = (ArtifactRef(kind="other", path=issue.file),)
|
|
88
|
+
artifact_hashes = store.current_hashes(artifacts) if store is not None else {}
|
|
89
|
+
else:
|
|
90
|
+
evidence = Evidence(model_key=f"check:{result.check_id}", note=issue.rule)
|
|
91
|
+
artifacts = ()
|
|
92
|
+
artifact_hashes = {}
|
|
93
|
+
|
|
94
|
+
findings.append(
|
|
95
|
+
Finding(
|
|
96
|
+
layer=layer,
|
|
97
|
+
severity=_finding_severity(issue.severity),
|
|
98
|
+
source=f"check:{result.check_id}",
|
|
99
|
+
evidence=(evidence,),
|
|
100
|
+
claim=issue.msg,
|
|
101
|
+
artifacts=artifacts,
|
|
102
|
+
artifact_hashes=artifact_hashes,
|
|
103
|
+
first_seen=now,
|
|
104
|
+
last_seen=now,
|
|
105
|
+
run_id=run_id,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return findings
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
ABSENT_HASH = hashlib.sha256(b"chipgraph:absent-file").hexdigest()
|
|
112
|
+
"""The hash a waiver records for a bound file that does not exist yet.
|
|
113
|
+
|
|
114
|
+
When the file appears, its real hash differs, so the waiver stops being current."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bound_hashes(store: ArtifactStore, paths: Iterable[str]) -> dict[str, str]:
|
|
118
|
+
"""`{"<repo>:<path>": hash}` for `paths`, with `ABSENT_HASH` for a missing file."""
|
|
119
|
+
refs = [ArtifactRef(kind="other", path=path) for path in paths]
|
|
120
|
+
present = store.current_hashes(refs)
|
|
121
|
+
return {
|
|
122
|
+
f"{ref.repo}:{ref.path}": present.get(f"{ref.repo}:{ref.path}", ABSENT_HASH) for ref in refs
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def finding_current_hashes(
|
|
127
|
+
store: ArtifactStore, finding: Finding, waivers: Iterable[Approval]
|
|
128
|
+
) -> dict[str, str]:
|
|
129
|
+
"""The current hashes to judge `finding`'s waivers against.
|
|
130
|
+
|
|
131
|
+
The finding's own artifacts, plus every file a waiver was bound to with `--bind`
|
|
132
|
+
(a missing one hashes to `ABSENT_HASH`), so a waiver on a whole-check finding
|
|
133
|
+
expires when a bound file appears or changes.
|
|
134
|
+
"""
|
|
135
|
+
current = store.current_hashes(finding.artifacts)
|
|
136
|
+
extra = sorted(
|
|
137
|
+
key.partition(":")[2]
|
|
138
|
+
for waiver in waivers
|
|
139
|
+
for key in waiver.artifact_hashes
|
|
140
|
+
if key not in current and key.partition(":")[2]
|
|
141
|
+
)
|
|
142
|
+
current.update(bound_hashes(store, extra))
|
|
143
|
+
return current
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def waive(
|
|
147
|
+
finding: Finding,
|
|
148
|
+
*,
|
|
149
|
+
by: str,
|
|
150
|
+
reason: str,
|
|
151
|
+
store: FindingStore,
|
|
152
|
+
review: ReviewAdapter,
|
|
153
|
+
current_hashes: Mapping[str, str] | None = None,
|
|
154
|
+
) -> Finding:
|
|
155
|
+
"""Waive `finding`: record a `waive` decision through `review`, bound to its artifacts.
|
|
156
|
+
|
|
157
|
+
`reason` is required (an empty reason is refused) and is stored as the decision's
|
|
158
|
+
`note`. The waiver's `artifact_hashes` come from `finding.artifact_hashes` (its
|
|
159
|
+
hashes at detection time), unless `current_hashes` is given, in which case the
|
|
160
|
+
waiver binds to `finding.artifacts`' *current* hashes instead (waiving as of now,
|
|
161
|
+
not as of whenever the finding was first recorded). A finding with no file (a
|
|
162
|
+
whole-check error) is bound with `current_hashes` from `bound_hashes` instead (the
|
|
163
|
+
CLI's `--bind`). Raises `ValueError` if `reason` is blank, or `AppError` if there is
|
|
164
|
+
no artifact hash to bind a waiver to.
|
|
165
|
+
|
|
166
|
+
Also updates the finding's stored `status` to `"waived"` in `store`, for display
|
|
167
|
+
convenience; `chipgraph.core.state.findings.effective_status` remains the source of
|
|
168
|
+
truth for whether the waiver is still current.
|
|
169
|
+
"""
|
|
170
|
+
if not reason.strip():
|
|
171
|
+
raise ValueError("a waiver requires a non-empty reason")
|
|
172
|
+
|
|
173
|
+
hashes = dict(current_hashes) if current_hashes else dict(finding.artifact_hashes)
|
|
174
|
+
if not hashes:
|
|
175
|
+
raise AppError(
|
|
176
|
+
f"cannot waive finding {finding.id!r}: it has no file to bind the waiver to; "
|
|
177
|
+
"name the files whose change should end the waiver with --bind PATH"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
approval = Approval(
|
|
181
|
+
gate_id=waiver_gate_id(finding.id),
|
|
182
|
+
by=by,
|
|
183
|
+
at=datetime.now(UTC),
|
|
184
|
+
artifact_hashes=hashes,
|
|
185
|
+
decision="waive",
|
|
186
|
+
note=reason,
|
|
187
|
+
)
|
|
188
|
+
review.record(approval)
|
|
189
|
+
|
|
190
|
+
updated = finding.model_copy(update={"status": "waived"})
|
|
191
|
+
(stored,) = store.upsert((updated,))
|
|
192
|
+
return stored
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"ABSENT_HASH",
|
|
197
|
+
"LAYER_BY_CHECK",
|
|
198
|
+
"bound_hashes",
|
|
199
|
+
"finding_current_hashes",
|
|
200
|
+
"findings_from_check",
|
|
201
|
+
"layer_for_check",
|
|
202
|
+
"waive",
|
|
203
|
+
]
|