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,224 @@
|
|
|
1
|
+
"""`TraceCheck`: every requirement must be referenced by at least one test (layer 5).
|
|
2
|
+
|
|
3
|
+
A layer-5 (process) cross check (DESIGN.md 4.8, DECISIONS D37). It reads the Design Model
|
|
4
|
+
`chipgraph ingest` wrote and the test files under the `tests` globs, and reports:
|
|
5
|
+
|
|
6
|
+
* `req.no_test` -- a *declared* requirement whose ID appears in no test file.
|
|
7
|
+
* `test.unknown_req` -- a test mentions a string that matches the profile's requirement
|
|
8
|
+
`id_pattern` for some block but is not a requirement in the model (a stale or mistyped
|
|
9
|
+
REQ-ID).
|
|
10
|
+
* `req.inferred_untraceable` -- an `info` per block with requirements that have no
|
|
11
|
+
declared ID: *inferred* ones (D37) and Verification items *missing* their ID in a file
|
|
12
|
+
that declares IDs (`spec_schema` reports each of those as `requirement.missing_id`).
|
|
13
|
+
They have no stable ID to grep for, so instead of one failure each, one `info` says how
|
|
14
|
+
many a block has, of which kind, and that they cannot be traced by ID.
|
|
15
|
+
|
|
16
|
+
A missing model is a whole-check `error` (run `chipgraph ingest` first). With a `block`
|
|
17
|
+
param (`chipgraph check --block`), only that block's requirements and its inferred summary
|
|
18
|
+
are considered; `test.unknown_req` still uses every block's pattern, so a test naming
|
|
19
|
+
another block's stale ID is still caught.
|
|
20
|
+
|
|
21
|
+
Example profile usage (`.chipgraph.yml`):
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
adapters:
|
|
25
|
+
trace: { use: trace, tests: ["dv/**/*.py", "dv/**/*.sv"] }
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from chipgraph.checks._common import (
|
|
36
|
+
ArgError,
|
|
37
|
+
compute_idempotency_key,
|
|
38
|
+
error_result,
|
|
39
|
+
glob_to_regex,
|
|
40
|
+
iter_repo_files,
|
|
41
|
+
require_list_of_str,
|
|
42
|
+
sha256_file,
|
|
43
|
+
)
|
|
44
|
+
from chipgraph.checks._model import ModelUnavailable, block_param, load_model
|
|
45
|
+
from chipgraph.core.config.models import RequirementsCfg
|
|
46
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
47
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
48
|
+
from chipgraph.core.model.model import DesignModel
|
|
49
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TraceCheck:
|
|
53
|
+
"""Checks that every declared requirement is referenced by at least one test."""
|
|
54
|
+
|
|
55
|
+
id = "trace"
|
|
56
|
+
name = "Trace"
|
|
57
|
+
|
|
58
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
59
|
+
start = time.monotonic()
|
|
60
|
+
args = spec.args
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
test_globs = require_list_of_str(args, "tests")
|
|
64
|
+
if not test_globs:
|
|
65
|
+
raise ArgError("args['tests'] must list at least one glob")
|
|
66
|
+
id_pattern = args.get("id_pattern")
|
|
67
|
+
if id_pattern is not None and not isinstance(id_pattern, str):
|
|
68
|
+
raise ArgError("args['id_pattern'] must be a string")
|
|
69
|
+
except ArgError as exc:
|
|
70
|
+
return error_result(spec, str(exc), start)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
loaded = load_model(ctx.repo_root)
|
|
74
|
+
except ModelUnavailable as exc:
|
|
75
|
+
return error_result(spec, str(exc), start, rule="model")
|
|
76
|
+
|
|
77
|
+
block = block_param(ctx)
|
|
78
|
+
req_cfg = RequirementsCfg(id_pattern=id_pattern) if id_pattern else RequirementsCfg()
|
|
79
|
+
|
|
80
|
+
test_files = _match_files(ctx.repo_root, test_globs)
|
|
81
|
+
texts = {rel: (ctx.repo_root / rel).read_text(errors="ignore") for rel in test_files}
|
|
82
|
+
|
|
83
|
+
issues = _trace_issues(loaded.model, texts, req_cfg, block)
|
|
84
|
+
issues.sort(key=lambda i: (i.file or "", i.line or 0, i.rule, i.msg))
|
|
85
|
+
|
|
86
|
+
file_hashes = [(rel, sha256_file(ctx.repo_root / rel)) for rel in test_files]
|
|
87
|
+
file_hashes.append(("<model>", loaded.build_inputs_hash or "none"))
|
|
88
|
+
key = compute_idempotency_key(spec.id, args, file_hashes)
|
|
89
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
90
|
+
return CheckResult(
|
|
91
|
+
check_id=spec.id,
|
|
92
|
+
status=status,
|
|
93
|
+
issues=tuple(issues),
|
|
94
|
+
duration_s=time.monotonic() - start,
|
|
95
|
+
idempotency_key=key,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _match_files(root: Path, globs: list[str]) -> list[str]:
|
|
100
|
+
regexes = [glob_to_regex(g) for g in globs]
|
|
101
|
+
return [f for f in iter_repo_files(root) if any(r.fullmatch(f) for r in regexes)]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _trace_issues(
|
|
105
|
+
model: DesignModel,
|
|
106
|
+
texts: dict[str, str],
|
|
107
|
+
req_cfg: RequirementsCfg,
|
|
108
|
+
block: str | None,
|
|
109
|
+
) -> list[Issue]:
|
|
110
|
+
issues: list[Issue] = []
|
|
111
|
+
block_key = f"block:{block}" if block else None
|
|
112
|
+
|
|
113
|
+
requirements = model.by_kind("requirement")
|
|
114
|
+
declared = [
|
|
115
|
+
r
|
|
116
|
+
for r in requirements
|
|
117
|
+
if r.attrs.get("id_source") == "declared"
|
|
118
|
+
and (block_key is None or r.attrs.get("block") == block_key)
|
|
119
|
+
]
|
|
120
|
+
# No declared ID: inferred (D37) or missing in a file that declares IDs.
|
|
121
|
+
untraceable = [
|
|
122
|
+
r
|
|
123
|
+
for r in requirements
|
|
124
|
+
if r.attrs.get("id_source") in ("inferred", "missing")
|
|
125
|
+
and (block_key is None or r.attrs.get("block") == block_key)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# req.no_test: a declared ID mentioned in no test (word-boundary search on its name).
|
|
129
|
+
for req in sorted(declared, key=lambda r: r.key):
|
|
130
|
+
pattern = re.compile(rf"(?<![\w-]){re.escape(req.name)}(?![\w-])")
|
|
131
|
+
if not any(pattern.search(text) for text in texts.values()):
|
|
132
|
+
issues.append(
|
|
133
|
+
Issue(
|
|
134
|
+
file=req.source.file,
|
|
135
|
+
line=req.source.line,
|
|
136
|
+
rule="req.no_test",
|
|
137
|
+
severity="error",
|
|
138
|
+
msg=f"requirement {req.name!r} is referenced by no test under `tests`",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# test.unknown_req: a test names a REQ-ID-shaped string that is no requirement.
|
|
143
|
+
known_names = {r.name for r in requirements}
|
|
144
|
+
id_regexes = _block_id_regexes(model, req_cfg)
|
|
145
|
+
for rel in sorted(texts):
|
|
146
|
+
for line_no, token in _req_like_tokens(texts[rel], id_regexes):
|
|
147
|
+
if token not in known_names:
|
|
148
|
+
issues.append(
|
|
149
|
+
Issue(
|
|
150
|
+
file=rel,
|
|
151
|
+
line=line_no,
|
|
152
|
+
rule="test.unknown_req",
|
|
153
|
+
severity="error",
|
|
154
|
+
msg=(
|
|
155
|
+
f"test names {token!r}, which matches a requirement id pattern "
|
|
156
|
+
"but is not a requirement in the model"
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# No declared ID: one info per block, not one failure per requirement (D37).
|
|
162
|
+
by_block: dict[str, dict[str, int]] = {}
|
|
163
|
+
for req in untraceable:
|
|
164
|
+
b = str(req.attrs.get("block") or "")
|
|
165
|
+
counts = by_block.setdefault(b, {"inferred": 0, "missing": 0})
|
|
166
|
+
counts[str(req.attrs.get("id_source"))] += 1
|
|
167
|
+
for b in sorted(by_block):
|
|
168
|
+
issues.append(
|
|
169
|
+
Issue(
|
|
170
|
+
rule="req.inferred_untraceable",
|
|
171
|
+
severity="info",
|
|
172
|
+
msg=_untraceable_msg(b or "(no block)", by_block[b]),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return issues
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _untraceable_msg(block: str, counts: dict[str, int]) -> str:
|
|
180
|
+
"""The `req.inferred_untraceable` message for one block's requirements with no ID."""
|
|
181
|
+
inferred, missing = counts["inferred"], counts["missing"]
|
|
182
|
+
if not missing:
|
|
183
|
+
return (
|
|
184
|
+
f"{inferred} inferred requirement(s) in {block} cannot be traced by ID "
|
|
185
|
+
"(they are inferred, not declared REQ-IDs)"
|
|
186
|
+
)
|
|
187
|
+
parts = [f"{inferred} inferred"] if inferred else []
|
|
188
|
+
parts.append(f"{missing} Verification item(s) missing an ID (requirement.missing_id)")
|
|
189
|
+
return (
|
|
190
|
+
f"{inferred + missing} requirement(s) in {block} cannot be traced by ID: "
|
|
191
|
+
f"{', '.join(parts)}; none of them is a declared REQ-ID"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _block_id_regexes(model: DesignModel, req_cfg: RequirementsCfg) -> list[re.Pattern[str]]:
|
|
196
|
+
"""A requirement-id regex per block name in the model (plus a block-less default).
|
|
197
|
+
|
|
198
|
+
The `id_pattern` may use `{block}`/`{BLOCK}`; compiled once per block so a test naming
|
|
199
|
+
another block's stale ID is still recognised as REQ-ID-shaped.
|
|
200
|
+
"""
|
|
201
|
+
uses_block = "{block}" in req_cfg.id_pattern or "{BLOCK}" in req_cfg.id_pattern
|
|
202
|
+
if not uses_block:
|
|
203
|
+
return [req_cfg.id_regex()]
|
|
204
|
+
regexes: list[re.Pattern[str]] = []
|
|
205
|
+
for blk in model.by_kind("block"):
|
|
206
|
+
regexes.append(req_cfg.id_regex(blk.name))
|
|
207
|
+
return regexes or [req_cfg.id_regex("x")]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
_TOKEN = re.compile(r"[A-Za-z0-9_-]+")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _req_like_tokens(text: str, id_regexes: list[re.Pattern[str]]) -> list[tuple[int, str]]:
|
|
214
|
+
"""Every (line, token) in `text` whose token fully matches a requirement id regex."""
|
|
215
|
+
out: list[tuple[int, str]] = []
|
|
216
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
217
|
+
for m in _TOKEN.finditer(line):
|
|
218
|
+
token = m.group(0)
|
|
219
|
+
if any(rx.fullmatch(token) for rx in id_regexes):
|
|
220
|
+
out.append((line_no, token))
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = ["TraceCheck"]
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""`VendorGuardCheck` (`vendor_guard`): vendored IP is never edited in place.
|
|
2
|
+
|
|
3
|
+
Third-party IP is copied into the repository so that a tape-out has a frozen, auditable
|
|
4
|
+
source; that holds only while the files match what was recorded. The check reads a
|
|
5
|
+
manifest in `sha256sum` format (`<sha256> <path>` per line, paths relative to the
|
|
6
|
+
manifest's directory, `#` comments), so `shasum -a 256 -c MANIFEST.sha256` run in that
|
|
7
|
+
directory verifies the same thing, and fails when, under the manifest's directory:
|
|
8
|
+
|
|
9
|
+
- a listed file's content differs from its recorded hash (`vendor_guard.modified`);
|
|
10
|
+
- a listed file is gone, or is not a regular file (`vendor_guard.missing`);
|
|
11
|
+
- a file is not listed (`vendor_guard.unlisted`), except the manifest itself and the
|
|
12
|
+
`allow` globs (e.g. a `patches/**` directory for reviewed changes kept apart);
|
|
13
|
+
- a file or directory is a symbolic link, or a listed path resolves outside the
|
|
14
|
+
manifest's directory (`vendor_guard.symlink`): a link would let the guarded tree
|
|
15
|
+
point at files the manifest does not cover. The manifest itself, and every directory
|
|
16
|
+
on the way to it, must not be a link either.
|
|
17
|
+
|
|
18
|
+
It never passes on what it could not read: an unreadable file or directory is an
|
|
19
|
+
`error` (`vendor_guard.unreadable`). Only the `.git` directory directly under the
|
|
20
|
+
manifest's directory is skipped; one deeper is walked like any other directory. Two
|
|
21
|
+
manifest entries that differ only in case (one file on a case-insensitive file system)
|
|
22
|
+
are refused.
|
|
23
|
+
|
|
24
|
+
An IP update is then a deliberate change: new files and a new manifest in the same
|
|
25
|
+
commit, reviewed as such. A project change to how an IP is used goes in its wrapper.
|
|
26
|
+
|
|
27
|
+
Args (all optional): `manifest` (default `vendor/MANIFEST.sha256`; a repo-relative
|
|
28
|
+
path, no `..`, inside the repository), `allow` (globs, relative to the manifest's
|
|
29
|
+
directory). Chip-wide: it runs once, not per block.
|
|
30
|
+
|
|
31
|
+
Profile example (`.chipgraph.yml`)::
|
|
32
|
+
|
|
33
|
+
adapters:
|
|
34
|
+
vendor_guard: { use: vendor_guard }
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import hashlib
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
import stat
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
from chipgraph.checks._common import (
|
|
48
|
+
ArgError,
|
|
49
|
+
compute_idempotency_key,
|
|
50
|
+
error_result,
|
|
51
|
+
glob_to_regex,
|
|
52
|
+
optional_list_of_str,
|
|
53
|
+
optional_str,
|
|
54
|
+
)
|
|
55
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
56
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
57
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
58
|
+
|
|
59
|
+
DEFAULT_MANIFEST = "vendor/MANIFEST.sha256"
|
|
60
|
+
_LINE_RE = re.compile(r"^([0-9a-fA-F]{64}) [ *](.+)$")
|
|
61
|
+
_OPEN_FLAGS = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
62
|
+
_CHUNK = 1 << 20
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ManifestError(Exception):
|
|
66
|
+
"""The manifest cannot be read."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_manifest(path: Path) -> dict[str, str]:
|
|
70
|
+
"""The manifest's entries: path (relative to its directory) -> sha256.
|
|
71
|
+
|
|
72
|
+
Errors name the line number only, never quote the line."""
|
|
73
|
+
return parse_manifest(_read_regular(path).decode("utf-8"), path.name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_manifest(text: str, name: str) -> dict[str, str]:
|
|
77
|
+
"""The entries of the manifest text `text` (`name` is its file name, for errors)."""
|
|
78
|
+
entries: dict[str, str] = {}
|
|
79
|
+
folded: dict[str, str] = {}
|
|
80
|
+
for number, raw in enumerate(text.splitlines(), start=1):
|
|
81
|
+
line = raw.strip()
|
|
82
|
+
if not line or line.startswith("#"):
|
|
83
|
+
continue
|
|
84
|
+
match = _LINE_RE.match(line)
|
|
85
|
+
if match is None:
|
|
86
|
+
raise ManifestError(f"{name}:{number}: not `<sha256> <path>`")
|
|
87
|
+
rel = match.group(2).strip().removeprefix("./")
|
|
88
|
+
if rel.startswith("/") or ".." in rel.split("/"):
|
|
89
|
+
raise ManifestError(f"{name}:{number}: the path leaves the manifest's directory")
|
|
90
|
+
if "\x00" in rel:
|
|
91
|
+
raise ManifestError(f"{name}:{number}: the path holds a NUL byte")
|
|
92
|
+
if rel in entries:
|
|
93
|
+
raise ManifestError(f"{name}:{number}: {rel!r} is listed twice")
|
|
94
|
+
other = folded.get(rel.casefold())
|
|
95
|
+
if other is not None:
|
|
96
|
+
raise ManifestError(
|
|
97
|
+
f"{name}:{number}: {rel!r} and {other!r} differ only in case (one file on a "
|
|
98
|
+
"case-insensitive file system): list it once"
|
|
99
|
+
)
|
|
100
|
+
entries[rel] = match.group(1).lower()
|
|
101
|
+
folded[rel.casefold()] = rel
|
|
102
|
+
return entries
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _why(exc: OSError) -> str:
|
|
106
|
+
return exc.strerror or str(exc)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_manifest(directory: Path, manifest: str = "MANIFEST.sha256") -> str:
|
|
110
|
+
"""The manifest text for every file under `directory` (for a person updating an IP).
|
|
111
|
+
|
|
112
|
+
Raises `ManifestError` when the tree holds a link or something unreadable."""
|
|
113
|
+
tree = _walk(directory)
|
|
114
|
+
if tree.links or tree.unreadable:
|
|
115
|
+
bad = ", ".join([*tree.links, *tree.unreadable])
|
|
116
|
+
raise ManifestError(f"cannot record {directory}: links or unreadable entries: {bad}")
|
|
117
|
+
lines = [f"{_sha256_regular(directory / rel)} {rel}" for rel in tree.files if rel != manifest]
|
|
118
|
+
return "\n".join(lines) + "\n"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class _Tree:
|
|
123
|
+
files: list[str] = field(default_factory=list)
|
|
124
|
+
"""Regular files (and other non-directory, non-link entries), relative, sorted."""
|
|
125
|
+
links: list[str] = field(default_factory=list)
|
|
126
|
+
"""Symbolic links, to files or directories (never followed)."""
|
|
127
|
+
unreadable: list[str] = field(default_factory=list)
|
|
128
|
+
"""Directories that could not be listed."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _walk(directory: Path) -> _Tree:
|
|
132
|
+
tree = _Tree()
|
|
133
|
+
|
|
134
|
+
def _error(exc: OSError) -> None:
|
|
135
|
+
where = Path(exc.filename) if exc.filename else directory
|
|
136
|
+
try:
|
|
137
|
+
rel = where.relative_to(directory).as_posix()
|
|
138
|
+
except ValueError:
|
|
139
|
+
rel = where.as_posix()
|
|
140
|
+
tree.unreadable.append(rel or ".")
|
|
141
|
+
|
|
142
|
+
for dirpath, dirnames, filenames in os.walk(directory, onerror=_error):
|
|
143
|
+
here = Path(dirpath)
|
|
144
|
+
keep = []
|
|
145
|
+
for name in sorted(dirnames):
|
|
146
|
+
if here == directory and name == ".git":
|
|
147
|
+
continue
|
|
148
|
+
rel = (here / name).relative_to(directory).as_posix()
|
|
149
|
+
if (here / name).is_symlink():
|
|
150
|
+
tree.links.append(rel)
|
|
151
|
+
else:
|
|
152
|
+
keep.append(name)
|
|
153
|
+
dirnames[:] = keep
|
|
154
|
+
for name in filenames:
|
|
155
|
+
rel = (here / name).relative_to(directory).as_posix()
|
|
156
|
+
(tree.links if (here / name).is_symlink() else tree.files).append(rel)
|
|
157
|
+
tree.files.sort()
|
|
158
|
+
tree.links.sort()
|
|
159
|
+
tree.unreadable.sort()
|
|
160
|
+
return tree
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _read_regular(path: Path) -> bytes:
|
|
164
|
+
"""The bytes of the regular file `path`, never through a link (`OSError` otherwise)."""
|
|
165
|
+
fd = os.open(path, _OPEN_FLAGS)
|
|
166
|
+
try:
|
|
167
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
168
|
+
raise OSError(f"{path.name} is not a regular file")
|
|
169
|
+
chunks = []
|
|
170
|
+
while chunk := os.read(fd, _CHUNK):
|
|
171
|
+
chunks.append(chunk)
|
|
172
|
+
return b"".join(chunks)
|
|
173
|
+
finally:
|
|
174
|
+
os.close(fd)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _sha256_regular(path: Path) -> str:
|
|
178
|
+
return hashlib.sha256(_read_regular(path)).hexdigest()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _manifest_rel(value: str) -> str:
|
|
182
|
+
"""`value` as a normalised repo-relative POSIX path (`ArgError` if it is not one)."""
|
|
183
|
+
text = value.replace("\\", "/").strip()
|
|
184
|
+
parts = [p for p in text.split("/") if p not in ("", ".")]
|
|
185
|
+
if not parts or text.startswith("/") or ".." in parts or (len(text) > 1 and text[1] == ":"):
|
|
186
|
+
raise ArgError("args['manifest'] must be a repo-relative path without '..'")
|
|
187
|
+
return "/".join(parts)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _linked_part(root: Path, rel: str) -> str | None:
|
|
191
|
+
"""The first component of `rel` under `root` that is a symbolic link, if any."""
|
|
192
|
+
current = root
|
|
193
|
+
for part in rel.split("/"):
|
|
194
|
+
current = current / part
|
|
195
|
+
if current.is_symlink():
|
|
196
|
+
return current.relative_to(root).as_posix()
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class VendorGuardCheck:
|
|
201
|
+
"""Fails when a file under the vendor manifest's directory differs from the manifest."""
|
|
202
|
+
|
|
203
|
+
id = "vendor_guard"
|
|
204
|
+
name = "Vendor guard"
|
|
205
|
+
per_block = False
|
|
206
|
+
|
|
207
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
208
|
+
start = time.monotonic()
|
|
209
|
+
try:
|
|
210
|
+
manifest_rel = _manifest_rel(optional_str(spec.args, "manifest", DEFAULT_MANIFEST))
|
|
211
|
+
allow = [glob_to_regex(g) for g in optional_list_of_str(spec.args, "allow")]
|
|
212
|
+
except ArgError as exc:
|
|
213
|
+
return error_result(spec, str(exc), start)
|
|
214
|
+
root = ctx.repo_root
|
|
215
|
+
manifest = root / manifest_rel
|
|
216
|
+
base = manifest.parent
|
|
217
|
+
base_rel = Path(manifest_rel).parent.as_posix()
|
|
218
|
+
prefix = "" if base_rel in ("", ".") else base_rel + "/"
|
|
219
|
+
linked = _linked_part(root, manifest_rel)
|
|
220
|
+
if linked is not None:
|
|
221
|
+
issue = Issue(
|
|
222
|
+
file=linked,
|
|
223
|
+
rule="vendor_guard.symlink",
|
|
224
|
+
msg=f"{linked} is a symbolic link: the manifest and its directory must be real",
|
|
225
|
+
)
|
|
226
|
+
return self._result(spec, "error", [issue], start, [])
|
|
227
|
+
if not Path(os.path.realpath(manifest)).is_relative_to(Path(os.path.realpath(root))):
|
|
228
|
+
issue = Issue(rule="vendor_guard.manifest", msg="the manifest is outside the repo")
|
|
229
|
+
return self._result(spec, "error", [issue], start, [])
|
|
230
|
+
if not manifest.is_file():
|
|
231
|
+
issue = Issue(
|
|
232
|
+
file=manifest_rel,
|
|
233
|
+
rule="vendor_guard.no_manifest",
|
|
234
|
+
msg=f"no vendor manifest at {manifest_rel}",
|
|
235
|
+
)
|
|
236
|
+
return self._result(spec, "error", [issue], start, [])
|
|
237
|
+
try:
|
|
238
|
+
manifest_bytes = _read_regular(manifest)
|
|
239
|
+
entries = parse_manifest(manifest_bytes.decode("utf-8"), manifest.name)
|
|
240
|
+
except ManifestError as exc:
|
|
241
|
+
issue = Issue(file=manifest_rel, rule="vendor_guard.manifest", msg=str(exc))
|
|
242
|
+
return self._result(spec, "error", [issue], start, [])
|
|
243
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
244
|
+
why = "not UTF-8" if isinstance(exc, UnicodeDecodeError) else _why(exc)
|
|
245
|
+
issue = Issue(
|
|
246
|
+
file=manifest_rel,
|
|
247
|
+
rule="vendor_guard.manifest",
|
|
248
|
+
msg=f"cannot read {manifest_rel}: {why}",
|
|
249
|
+
)
|
|
250
|
+
return self._result(spec, "error", [issue], start, [])
|
|
251
|
+
|
|
252
|
+
tree = _walk(base)
|
|
253
|
+
issues: list[Issue] = []
|
|
254
|
+
infra = False
|
|
255
|
+
hashes: list[tuple[str, str]] = [(manifest_rel, hashlib.sha256(manifest_bytes).hexdigest())]
|
|
256
|
+
for rel in tree.links:
|
|
257
|
+
hashes.append((prefix + rel, "symlink"))
|
|
258
|
+
issues.append(
|
|
259
|
+
Issue(
|
|
260
|
+
file=prefix + rel,
|
|
261
|
+
rule="vendor_guard.symlink",
|
|
262
|
+
msg=(
|
|
263
|
+
f"{prefix + rel} is a symbolic link: vendored IP is real files, so "
|
|
264
|
+
"the manifest covers what is built; replace the link by the files"
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
for rel in tree.unreadable:
|
|
269
|
+
infra = True
|
|
270
|
+
hashes.append((prefix + rel, "unreadable"))
|
|
271
|
+
issues.append(
|
|
272
|
+
Issue(
|
|
273
|
+
file=prefix + rel,
|
|
274
|
+
rule="vendor_guard.unreadable",
|
|
275
|
+
msg=f"cannot list {prefix + rel}: its files cannot be verified",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
real_base = Path(os.path.realpath(base))
|
|
279
|
+
for rel, recorded in sorted(entries.items()):
|
|
280
|
+
path = base / rel
|
|
281
|
+
link = _linked_part(base, rel)
|
|
282
|
+
if link is not None or not Path(os.path.realpath(path)).is_relative_to(real_base):
|
|
283
|
+
if link is None or link not in tree.links:
|
|
284
|
+
issues.append(
|
|
285
|
+
Issue(
|
|
286
|
+
file=prefix + rel,
|
|
287
|
+
rule="vendor_guard.symlink",
|
|
288
|
+
msg=f"{prefix + rel} resolves outside {base_rel or '.'}",
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
try:
|
|
293
|
+
if not stat.S_ISREG(path.lstat().st_mode):
|
|
294
|
+
raise FileNotFoundError(rel) # a FIFO, a directory...: not the file
|
|
295
|
+
actual = _sha256_regular(path)
|
|
296
|
+
except (FileNotFoundError, ValueError):
|
|
297
|
+
issues.append(
|
|
298
|
+
Issue(
|
|
299
|
+
file=prefix + rel,
|
|
300
|
+
rule="vendor_guard.missing",
|
|
301
|
+
msg=f"{prefix + rel} is in {manifest_rel} but does not exist",
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
continue
|
|
305
|
+
except OSError as exc:
|
|
306
|
+
infra = True
|
|
307
|
+
hashes.append((prefix + rel, "unreadable"))
|
|
308
|
+
issues.append(
|
|
309
|
+
Issue(
|
|
310
|
+
file=prefix + rel,
|
|
311
|
+
rule="vendor_guard.unreadable",
|
|
312
|
+
msg=f"cannot read {prefix + rel} ({_why(exc)}): it cannot be verified",
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
continue
|
|
316
|
+
hashes.append((prefix + rel, actual))
|
|
317
|
+
if actual != recorded:
|
|
318
|
+
issues.append(
|
|
319
|
+
Issue(
|
|
320
|
+
file=prefix + rel,
|
|
321
|
+
rule="vendor_guard.modified",
|
|
322
|
+
msg=(
|
|
323
|
+
f"{prefix + rel} differs from {manifest_rel}: vendored IP is never "
|
|
324
|
+
"edited in place. Revert it, and adapt the IP in its wrapper; an "
|
|
325
|
+
"IP update replaces its files and the manifest in one reviewed "
|
|
326
|
+
"commit"
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
name = manifest.name
|
|
331
|
+
for rel in tree.files:
|
|
332
|
+
if rel == name or rel in entries or any(r.fullmatch(rel) for r in allow):
|
|
333
|
+
continue
|
|
334
|
+
hashes.append((prefix + rel, "unlisted"))
|
|
335
|
+
issues.append(
|
|
336
|
+
Issue(
|
|
337
|
+
file=prefix + rel,
|
|
338
|
+
rule="vendor_guard.unlisted",
|
|
339
|
+
msg=f"{prefix + rel} is not in {manifest_rel}: record it, or remove it",
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
status: CheckStatus = "error" if infra else "fail" if issues else "pass"
|
|
343
|
+
return self._result(spec, status, issues, start, hashes)
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def _result(
|
|
347
|
+
spec: CheckSpec,
|
|
348
|
+
status: CheckStatus,
|
|
349
|
+
issues: list[Issue],
|
|
350
|
+
start: float,
|
|
351
|
+
hashes: list[tuple[str, str]],
|
|
352
|
+
) -> CheckResult:
|
|
353
|
+
return CheckResult(
|
|
354
|
+
check_id=spec.id,
|
|
355
|
+
status=status,
|
|
356
|
+
issues=tuple(issues),
|
|
357
|
+
duration_s=time.monotonic() - start,
|
|
358
|
+
idempotency_key=compute_idempotency_key(spec.id, spec.args, hashes),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
__all__ = [
|
|
363
|
+
"DEFAULT_MANIFEST",
|
|
364
|
+
"ManifestError",
|
|
365
|
+
"VendorGuardCheck",
|
|
366
|
+
"parse_manifest",
|
|
367
|
+
"read_manifest",
|
|
368
|
+
"write_manifest",
|
|
369
|
+
]
|