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,323 @@
|
|
|
1
|
+
"""`FilelistCheck`: every RTL file is listed, and every listed file exists.
|
|
2
|
+
|
|
3
|
+
See DESIGN.md 7.3 for the `filelist` check.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from chipgraph.checks._common import (
|
|
16
|
+
ArgError,
|
|
17
|
+
compile_template,
|
|
18
|
+
compute_idempotency_key,
|
|
19
|
+
error_result,
|
|
20
|
+
glob_to_regex,
|
|
21
|
+
iter_repo_files,
|
|
22
|
+
optional_list_of_str,
|
|
23
|
+
optional_str,
|
|
24
|
+
require_str,
|
|
25
|
+
sha256_file,
|
|
26
|
+
to_repo_rel,
|
|
27
|
+
)
|
|
28
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
29
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
30
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
31
|
+
|
|
32
|
+
_ENV_VAR = re.compile(r"\$\{(\w+)\}|\$(\w+)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FilelistCheck:
|
|
36
|
+
"""Checks a project's per-block RTL filelists against the RTL files actually on disk."""
|
|
37
|
+
|
|
38
|
+
id = "filelist"
|
|
39
|
+
name = "Filelist"
|
|
40
|
+
|
|
41
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
42
|
+
start = time.monotonic()
|
|
43
|
+
args = spec.args
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
filelist_tmpl_str = require_str(args, "filelist")
|
|
47
|
+
sources_tmpl_str = require_str(args, "sources")
|
|
48
|
+
package_suffix = optional_str(args, "package_suffix", "_pkg.sv")
|
|
49
|
+
ignore = optional_list_of_str(args, "ignore")
|
|
50
|
+
filelist_tmpl = compile_template(filelist_tmpl_str)
|
|
51
|
+
sources_tmpl = compile_template(sources_tmpl_str)
|
|
52
|
+
if set(filelist_tmpl.names) != {"block"}:
|
|
53
|
+
raise ArgError("args['filelist'] must use exactly the `{block}` placeholder")
|
|
54
|
+
if set(sources_tmpl.names) != {"block"}:
|
|
55
|
+
raise ArgError("args['sources'] must use exactly the `{block}` placeholder")
|
|
56
|
+
except ArgError as exc:
|
|
57
|
+
return error_result(spec, str(exc), start)
|
|
58
|
+
|
|
59
|
+
ignore_regexes = [glob_to_regex(p) for p in ignore]
|
|
60
|
+
all_files = [
|
|
61
|
+
f
|
|
62
|
+
for f in iter_repo_files(ctx.repo_root)
|
|
63
|
+
if not any(r.fullmatch(f) for r in ignore_regexes)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
blocks: set[str] = set()
|
|
67
|
+
sources_by_block: dict[str, list[str]] = {}
|
|
68
|
+
filelist_path_by_block: dict[str, str] = {}
|
|
69
|
+
for f in all_files:
|
|
70
|
+
src_match = sources_tmpl.match(f)
|
|
71
|
+
if src_match is not None:
|
|
72
|
+
block = src_match["block"]
|
|
73
|
+
blocks.add(block)
|
|
74
|
+
sources_by_block.setdefault(block, []).append(f)
|
|
75
|
+
fl_match = filelist_tmpl.match(f)
|
|
76
|
+
if fl_match is not None:
|
|
77
|
+
block = fl_match["block"]
|
|
78
|
+
blocks.add(block)
|
|
79
|
+
filelist_path_by_block[block] = f
|
|
80
|
+
|
|
81
|
+
issues: list[Issue] = []
|
|
82
|
+
read_files: dict[str, str] = {}
|
|
83
|
+
|
|
84
|
+
for block in sorted(blocks):
|
|
85
|
+
expected_filelist = filelist_tmpl_str.replace("{block}", block)
|
|
86
|
+
block_sources = sorted(sources_by_block.get(block, []))
|
|
87
|
+
for f in block_sources:
|
|
88
|
+
read_files[f] = sha256_file(ctx.repo_root / f)
|
|
89
|
+
|
|
90
|
+
if block not in filelist_path_by_block:
|
|
91
|
+
if block_sources:
|
|
92
|
+
issues.append(
|
|
93
|
+
Issue(
|
|
94
|
+
file=expected_filelist,
|
|
95
|
+
rule="filelist",
|
|
96
|
+
severity="error",
|
|
97
|
+
msg=f"block `{block}` has RTL sources but no filelist",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
visited: set[Path] = set()
|
|
103
|
+
entries, parse_issues = _parse_filelist(
|
|
104
|
+
ctx.repo_root / expected_filelist, ctx.repo_root, ctx.env, visited
|
|
105
|
+
)
|
|
106
|
+
issues.extend(parse_issues)
|
|
107
|
+
for visited_path in visited:
|
|
108
|
+
rel = to_repo_rel(visited_path, ctx.repo_root)
|
|
109
|
+
if rel is not None:
|
|
110
|
+
read_files[rel] = sha256_file(visited_path)
|
|
111
|
+
|
|
112
|
+
listed_issues, listed_repo_rel = _check_entries(entries, package_suffix, ctx.repo_root)
|
|
113
|
+
issues.extend(listed_issues)
|
|
114
|
+
|
|
115
|
+
for f in block_sources:
|
|
116
|
+
if f not in listed_repo_rel:
|
|
117
|
+
issues.append(
|
|
118
|
+
Issue(
|
|
119
|
+
file=f,
|
|
120
|
+
rule="filelist",
|
|
121
|
+
severity="error",
|
|
122
|
+
msg=f"`{f}` is not in `{expected_filelist}`",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
key = compute_idempotency_key(spec.id, args, tuple(read_files.items()))
|
|
127
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
128
|
+
return CheckResult(
|
|
129
|
+
check_id=spec.id,
|
|
130
|
+
status=status,
|
|
131
|
+
issues=tuple(issues),
|
|
132
|
+
duration_s=time.monotonic() - start,
|
|
133
|
+
idempotency_key=key,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class _Entry:
|
|
139
|
+
raw: str
|
|
140
|
+
line: int
|
|
141
|
+
filelist_rel: str
|
|
142
|
+
resolved: Path
|
|
143
|
+
is_absolute: bool
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _expand_env(text: str, env: Mapping[str, str]) -> str | None:
|
|
147
|
+
"""Expand `$VAR`/`${VAR}` in `text` from `env`; `None` if any variable is unknown."""
|
|
148
|
+
ok = True
|
|
149
|
+
|
|
150
|
+
def _sub(m: re.Match[str]) -> str:
|
|
151
|
+
nonlocal ok
|
|
152
|
+
name = m.group(1) or m.group(2)
|
|
153
|
+
if name in env:
|
|
154
|
+
return env[name]
|
|
155
|
+
ok = False
|
|
156
|
+
return m.group(0)
|
|
157
|
+
|
|
158
|
+
result = _ENV_VAR.sub(_sub, text)
|
|
159
|
+
return result if ok else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _strip_comment(line: str) -> str:
|
|
163
|
+
idx = len(line)
|
|
164
|
+
for token in ("//", "#"):
|
|
165
|
+
pos = line.find(token)
|
|
166
|
+
if pos != -1:
|
|
167
|
+
idx = min(idx, pos)
|
|
168
|
+
return line[:idx]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _parse_filelist(
|
|
172
|
+
path: Path, root: Path, env: Mapping[str, str], visited: set[Path]
|
|
173
|
+
) -> tuple[list[_Entry], list[Issue]]:
|
|
174
|
+
entries: list[_Entry] = []
|
|
175
|
+
issues: list[Issue] = []
|
|
176
|
+
_parse_filelist_into(path, root, env, visited, entries, issues)
|
|
177
|
+
return entries, issues
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _parse_filelist_into(
|
|
181
|
+
path: Path,
|
|
182
|
+
root: Path,
|
|
183
|
+
env: Mapping[str, str],
|
|
184
|
+
visited: set[Path],
|
|
185
|
+
entries: list[_Entry],
|
|
186
|
+
issues: list[Issue],
|
|
187
|
+
) -> None:
|
|
188
|
+
key = Path(os.path.normpath(str(path)))
|
|
189
|
+
if key in visited:
|
|
190
|
+
return
|
|
191
|
+
visited.add(key)
|
|
192
|
+
|
|
193
|
+
filelist_rel = to_repo_rel(path, root) or str(path)
|
|
194
|
+
try:
|
|
195
|
+
text = path.read_text()
|
|
196
|
+
except OSError:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
for lineno, raw_line in enumerate(text.splitlines(), start=1):
|
|
200
|
+
stripped = _strip_comment(raw_line).strip()
|
|
201
|
+
if not stripped:
|
|
202
|
+
continue
|
|
203
|
+
if stripped.startswith("+"):
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
tokens = stripped.split(None, 1)
|
|
207
|
+
head = tokens[0]
|
|
208
|
+
|
|
209
|
+
if head in ("-f", "-F") and len(tokens) > 1:
|
|
210
|
+
target_raw = tokens[1].strip()
|
|
211
|
+
expanded = _expand_env(target_raw, env)
|
|
212
|
+
if expanded is None:
|
|
213
|
+
issues.append(
|
|
214
|
+
Issue(
|
|
215
|
+
file=filelist_rel,
|
|
216
|
+
line=lineno,
|
|
217
|
+
severity="warning",
|
|
218
|
+
msg=f"unknown environment variable in `{target_raw}`",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
target_path = Path(expanded)
|
|
223
|
+
if not target_path.is_absolute():
|
|
224
|
+
target_path = path.parent / target_path
|
|
225
|
+
_parse_filelist_into(target_path, root, env, visited, entries, issues)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if head.startswith("-"):
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
raw_entry = stripped
|
|
232
|
+
expanded = _expand_env(raw_entry, env)
|
|
233
|
+
if expanded is None:
|
|
234
|
+
issues.append(
|
|
235
|
+
Issue(
|
|
236
|
+
file=filelist_rel,
|
|
237
|
+
line=lineno,
|
|
238
|
+
severity="warning",
|
|
239
|
+
msg=f"unknown environment variable in `{raw_entry}`",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
entry_path = Path(expanded)
|
|
245
|
+
is_absolute = entry_path.is_absolute()
|
|
246
|
+
if is_absolute:
|
|
247
|
+
issues.append(
|
|
248
|
+
Issue(
|
|
249
|
+
file=filelist_rel,
|
|
250
|
+
line=lineno,
|
|
251
|
+
severity="warning",
|
|
252
|
+
msg=f"`{raw_entry}` is an absolute path",
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
resolved = entry_path
|
|
256
|
+
else:
|
|
257
|
+
resolved = path.parent / entry_path
|
|
258
|
+
|
|
259
|
+
entries.append(
|
|
260
|
+
_Entry(
|
|
261
|
+
raw=raw_entry,
|
|
262
|
+
line=lineno,
|
|
263
|
+
filelist_rel=filelist_rel,
|
|
264
|
+
resolved=resolved,
|
|
265
|
+
is_absolute=is_absolute,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _check_entries(
|
|
271
|
+
entries: list[_Entry], package_suffix: str, root: Path
|
|
272
|
+
) -> tuple[list[Issue], set[str]]:
|
|
273
|
+
issues: list[Issue] = []
|
|
274
|
+
seen: set[str] = set()
|
|
275
|
+
listed_repo_rel: set[str] = set()
|
|
276
|
+
seen_non_pkg_name: str | None = None
|
|
277
|
+
|
|
278
|
+
for entry in entries:
|
|
279
|
+
if not entry.resolved.is_file():
|
|
280
|
+
issues.append(
|
|
281
|
+
Issue(
|
|
282
|
+
file=entry.filelist_rel,
|
|
283
|
+
line=entry.line,
|
|
284
|
+
severity="error",
|
|
285
|
+
msg=f"`{entry.raw}` is listed but missing",
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
key = os.path.normpath(str(entry.resolved))
|
|
291
|
+
name = Path(key).name
|
|
292
|
+
|
|
293
|
+
if key in seen:
|
|
294
|
+
issues.append(
|
|
295
|
+
Issue(
|
|
296
|
+
file=entry.filelist_rel,
|
|
297
|
+
line=entry.line,
|
|
298
|
+
severity="warning",
|
|
299
|
+
msg=f"`{entry.raw}` is listed twice",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
seen.add(key)
|
|
304
|
+
rel = to_repo_rel(entry.resolved, root)
|
|
305
|
+
if rel is not None:
|
|
306
|
+
listed_repo_rel.add(rel)
|
|
307
|
+
|
|
308
|
+
is_pkg = name.endswith(package_suffix)
|
|
309
|
+
if is_pkg:
|
|
310
|
+
if seen_non_pkg_name is not None:
|
|
311
|
+
issues.append(
|
|
312
|
+
Issue(
|
|
313
|
+
file=entry.filelist_rel,
|
|
314
|
+
line=entry.line,
|
|
315
|
+
rule="order",
|
|
316
|
+
severity="error",
|
|
317
|
+
msg=f"package `{name}` must come before `{seen_non_pkg_name}`",
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
elif seen_non_pkg_name is None:
|
|
321
|
+
seen_non_pkg_name = name
|
|
322
|
+
|
|
323
|
+
return issues, listed_repo_rel
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""`FormatCheck`: the project's formatter, run by the engine on an agent task's outputs.
|
|
2
|
+
|
|
3
|
+
The first check of `digital-rtl/rtl_module` (DESIGN.md 8.6 V3, task M2-04). The engine,
|
|
4
|
+
never the agent, runs it once the agent has finished and before the other checks, so
|
|
5
|
+
lint, naming and style see the formatted file, and the output hash the engine records
|
|
6
|
+
(the task's accepted outputs, the build record) is the formatted file's. It works the
|
|
7
|
+
same in both runtimes: `submit` (claude-code) and `AgentRuleExecutor` (in process) both
|
|
8
|
+
run a rule's checks after the attempt and hash the outputs after the checks.
|
|
9
|
+
|
|
10
|
+
Which files: `target_files` (shared with the `style` check):
|
|
11
|
+
|
|
12
|
+
- arg `files`: path templates (`{block}`, `{BLOCK}`, `{module}` filled from the task's
|
|
13
|
+
params);
|
|
14
|
+
- else, for a task with params `block` and `module` (a `foreach: plan.modules` rule), the
|
|
15
|
+
module's write set in the block's approved plan (arg `plan`, default
|
|
16
|
+
`plan/{block}.approved.json`), its HDL files only (`.sv`, `.svh`, `.v`, `.vh`). The
|
|
17
|
+
write set bounds the task's outputs, so the check writes only what the task may write.
|
|
18
|
+
|
|
19
|
+
Without either (e.g. `chipgraph check` over a block) it is `skipped`: it never formats a
|
|
20
|
+
person's files outside a task.
|
|
21
|
+
|
|
22
|
+
Which formatter: arg `formatter` (an inline adapter config), else the profile's
|
|
23
|
+
`style.formatter`, e.g.::
|
|
24
|
+
|
|
25
|
+
style:
|
|
26
|
+
formatter: { use: cmd, cmd: ["verible-verilog-format", "--inplace", "{file}"] }
|
|
27
|
+
|
|
28
|
+
It runs once per file, with the task's params plus `file` (the repo-relative path) and
|
|
29
|
+
`file_abs`; it must rewrite the file in place. With no formatter configured,
|
|
30
|
+
`verible-verilog-format --inplace` is used when it is on PATH; otherwise the check is
|
|
31
|
+
`skipped` with a note. A configured formatter that fails, or cannot be started, makes
|
|
32
|
+
the check `error` with the formatter's message (an infra failure: no try is used); the
|
|
33
|
+
file is then put back as it was.
|
|
34
|
+
|
|
35
|
+
Profile example (`.chipgraph.yml`)::
|
|
36
|
+
|
|
37
|
+
adapters:
|
|
38
|
+
format: { use: format }
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import json
|
|
44
|
+
import shutil
|
|
45
|
+
import time
|
|
46
|
+
from collections.abc import Mapping
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from chipgraph.checks._common import compute_idempotency_key, sha256_file
|
|
51
|
+
from chipgraph.core.config.errors import ConfigError
|
|
52
|
+
from chipgraph.core.config.loader import load as load_profile
|
|
53
|
+
from chipgraph.core.config.models import AdapterCfg
|
|
54
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
55
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
56
|
+
from chipgraph.core.plugin_api.registry import PluginError, Registry
|
|
57
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
58
|
+
|
|
59
|
+
DEFAULT_FORMATTER = "verible-verilog-format"
|
|
60
|
+
"""Used, with `--inplace`, when the profile names no formatter and it is on PATH."""
|
|
61
|
+
DEFAULT_APPROVED_PLAN = "plan/{block}.approved.json"
|
|
62
|
+
"""Where a block's approved plan is (the `plan_expand` tool's default output)."""
|
|
63
|
+
HDL_SUFFIXES = (".sv", ".svh", ".v", ".vh")
|
|
64
|
+
"""The files of a module's write set the per-task checks format and measure."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TargetError(Exception):
|
|
68
|
+
"""The files of a per-task check cannot be named (bad args, an unreadable plan)."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _fill(template: str, params: Mapping[str, str]) -> str | None:
|
|
72
|
+
"""`template` with `{key}`/`{KEY}` from `params`; `None` if a key is not a param."""
|
|
73
|
+
out = template
|
|
74
|
+
for key, value in params.items():
|
|
75
|
+
out = out.replace("{" + key + "}", value).replace("{" + key.upper() + "}", value.upper())
|
|
76
|
+
return None if "{" in out else out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _relative(path: str) -> str:
|
|
80
|
+
text = path.replace("\\", "/").strip()
|
|
81
|
+
parts = [p for p in text.split("/") if p not in ("", ".")]
|
|
82
|
+
if text.startswith("/") or ".." in parts or not parts:
|
|
83
|
+
raise TargetError(f"{path!r} is not a repo-relative path")
|
|
84
|
+
return "/".join(parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def target_files(
|
|
88
|
+
args: Mapping[str, Any], params: Mapping[str, str], root: Path
|
|
89
|
+
) -> list[str] | None:
|
|
90
|
+
"""The repo-relative files a per-task check works on, or `None` for none (see above).
|
|
91
|
+
|
|
92
|
+
Raises `TargetError` for a malformed `files`/`plan` arg or an approved plan that
|
|
93
|
+
cannot be read.
|
|
94
|
+
"""
|
|
95
|
+
files = args.get("files")
|
|
96
|
+
if files is not None:
|
|
97
|
+
templates = [files] if isinstance(files, str) else files
|
|
98
|
+
if not isinstance(templates, list) or not all(isinstance(t, str) for t in templates):
|
|
99
|
+
raise TargetError("args['files'] must be a path template or a list of them")
|
|
100
|
+
filled = [_fill(t, params) for t in templates]
|
|
101
|
+
if any(f is None for f in filled):
|
|
102
|
+
return None
|
|
103
|
+
return list(dict.fromkeys(_relative(f) for f in filled if f is not None))
|
|
104
|
+
block, module = params.get("block"), params.get("module")
|
|
105
|
+
if not block or not module:
|
|
106
|
+
return None
|
|
107
|
+
plan_t = args.get("plan", DEFAULT_APPROVED_PLAN)
|
|
108
|
+
if not isinstance(plan_t, str):
|
|
109
|
+
raise TargetError("args['plan'] must be a path template")
|
|
110
|
+
plan_rel = _fill(plan_t, params)
|
|
111
|
+
if plan_rel is None:
|
|
112
|
+
return None
|
|
113
|
+
path = root / _relative(plan_rel)
|
|
114
|
+
if not path.is_file():
|
|
115
|
+
return None
|
|
116
|
+
try:
|
|
117
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
118
|
+
entry = next(m for m in data.get("modules", []) if m.get("name") == module)
|
|
119
|
+
writes = [str(w) for w in entry.get("writes", [])]
|
|
120
|
+
except (OSError, ValueError, AttributeError, StopIteration) as exc:
|
|
121
|
+
raise TargetError(f"cannot read module {module!r} from {plan_rel}: {exc!r}") from exc
|
|
122
|
+
return [_relative(w) for w in writes if w.endswith(HDL_SUFFIXES)]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def formatter_config(args: Mapping[str, Any], root: Path) -> dict[str, Any] | None:
|
|
126
|
+
"""The formatter's adapter config: arg `formatter`, else the profile's `style.formatter`."""
|
|
127
|
+
inline = args.get("formatter")
|
|
128
|
+
if inline is not None:
|
|
129
|
+
return AdapterCfg.model_validate(inline).model_dump()
|
|
130
|
+
resolved = load_profile(root)
|
|
131
|
+
if resolved is None or resolved.profile.style.formatter is None:
|
|
132
|
+
return None
|
|
133
|
+
return resolved.profile.style.formatter.model_dump()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _result(
|
|
137
|
+
spec: CheckSpec,
|
|
138
|
+
status: CheckStatus,
|
|
139
|
+
issues: list[Issue],
|
|
140
|
+
start: float,
|
|
141
|
+
hashes: list[tuple[str, str]],
|
|
142
|
+
log_tail: str = "",
|
|
143
|
+
) -> CheckResult:
|
|
144
|
+
return CheckResult(
|
|
145
|
+
check_id=spec.id,
|
|
146
|
+
status=status,
|
|
147
|
+
issues=tuple(issues),
|
|
148
|
+
log_tail=log_tail[-4000:],
|
|
149
|
+
duration_s=time.monotonic() - start,
|
|
150
|
+
idempotency_key=compute_idempotency_key(spec.id, spec.args, hashes),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _plugin(use: str) -> Any:
|
|
155
|
+
registry = Registry()
|
|
156
|
+
registry.discover()
|
|
157
|
+
for kind in ("tool", "check"):
|
|
158
|
+
try:
|
|
159
|
+
return registry.get(kind, use)
|
|
160
|
+
except PluginError:
|
|
161
|
+
continue
|
|
162
|
+
raise PluginError(f"no tool or check adapter named {use!r} (the formatter)")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _message(result: CheckResult) -> str:
|
|
166
|
+
msgs = "; ".join(i.msg for i in result.issues if i.severity == "error")
|
|
167
|
+
return msgs or result.log_tail.strip()[-400:] or f"the formatter returned {result.status}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class FormatCheck:
|
|
171
|
+
"""Runs the project's formatter in place on a task's output files."""
|
|
172
|
+
|
|
173
|
+
id = "format"
|
|
174
|
+
name = "Format"
|
|
175
|
+
|
|
176
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
177
|
+
start = time.monotonic()
|
|
178
|
+
root = ctx.repo_root
|
|
179
|
+
try:
|
|
180
|
+
files = target_files(spec.args, ctx.params, root)
|
|
181
|
+
cfg = formatter_config(spec.args, root)
|
|
182
|
+
except (TargetError, ValueError, ConfigError) as exc:
|
|
183
|
+
issue = Issue(rule="format.args", msg=str(exc))
|
|
184
|
+
return _result(spec, "error", [issue], start, [])
|
|
185
|
+
if files is None:
|
|
186
|
+
note = Issue(
|
|
187
|
+
severity="info",
|
|
188
|
+
rule="format.no_files",
|
|
189
|
+
msg=(
|
|
190
|
+
"nothing to format: the format check runs on an agent task's outputs "
|
|
191
|
+
"(args 'files', or params block and module with an approved plan)"
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
return _result(spec, "skipped", [note], start, [])
|
|
195
|
+
if cfg is None:
|
|
196
|
+
found = shutil.which(DEFAULT_FORMATTER)
|
|
197
|
+
if found is None:
|
|
198
|
+
note = Issue(
|
|
199
|
+
severity="info",
|
|
200
|
+
rule="format.no_formatter",
|
|
201
|
+
msg=(
|
|
202
|
+
f"no formatter: the profile sets no style.formatter and "
|
|
203
|
+
f"{DEFAULT_FORMATTER} is not on PATH; the files are left as written"
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
return _result(spec, "skipped", [note], start, [])
|
|
207
|
+
cfg = {"use": "cmd", "cmd": [found, "--inplace", "{file}"]}
|
|
208
|
+
use = str(cfg.pop("use"))
|
|
209
|
+
try:
|
|
210
|
+
plugin = _plugin(use)
|
|
211
|
+
except PluginError as exc:
|
|
212
|
+
return _result(spec, "error", [Issue(rule="format.adapter", msg=str(exc))], start, [])
|
|
213
|
+
|
|
214
|
+
issues: list[Issue] = []
|
|
215
|
+
logs: list[str] = []
|
|
216
|
+
hashes: list[tuple[str, str]] = []
|
|
217
|
+
failed = False
|
|
218
|
+
for rel in files:
|
|
219
|
+
path = root / rel
|
|
220
|
+
if not path.is_file():
|
|
221
|
+
continue # the engine's own output check reports a missing output
|
|
222
|
+
before = path.read_bytes()
|
|
223
|
+
params = {**ctx.params, "file": rel, "file_abs": str(path.resolve())}
|
|
224
|
+
sub_ctx = ctx.model_copy(update={"params": params})
|
|
225
|
+
sub_spec = CheckSpec(
|
|
226
|
+
id=f"{spec.id}.formatter", capability="format", adapter=use, args=cfg
|
|
227
|
+
)
|
|
228
|
+
try:
|
|
229
|
+
result: CheckResult = await plugin.run(sub_spec, sub_ctx)
|
|
230
|
+
except Exception as exc: # a broken formatter adapter: infra, never the agent's
|
|
231
|
+
result = CheckResult(
|
|
232
|
+
check_id=sub_spec.id,
|
|
233
|
+
status="error",
|
|
234
|
+
issues=(Issue(msg=f"{type(exc).__name__}: {exc}"),),
|
|
235
|
+
duration_s=0.0,
|
|
236
|
+
idempotency_key="format:raised",
|
|
237
|
+
)
|
|
238
|
+
if result.log_tail:
|
|
239
|
+
logs.append(result.log_tail)
|
|
240
|
+
if result.status != "pass" or not path.is_file():
|
|
241
|
+
failed = True
|
|
242
|
+
path.write_bytes(before) # leave the agent's file as it was
|
|
243
|
+
issues.append(
|
|
244
|
+
Issue(
|
|
245
|
+
file=rel,
|
|
246
|
+
rule="format.failed",
|
|
247
|
+
msg=f"the formatter ({use}) failed on {rel}: {_message(result)}",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
if path.read_bytes() != before:
|
|
252
|
+
issues.append(
|
|
253
|
+
Issue(severity="info", file=rel, rule="format.changed", msg=f"formatted {rel}")
|
|
254
|
+
)
|
|
255
|
+
hashes.append((rel, sha256_file(path)))
|
|
256
|
+
status: CheckStatus = "error" if failed else "pass"
|
|
257
|
+
return _result(spec, status, issues, start, hashes, "\n".join(logs))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
__all__ = [
|
|
261
|
+
"DEFAULT_APPROVED_PLAN",
|
|
262
|
+
"DEFAULT_FORMATTER",
|
|
263
|
+
"HDL_SUFFIXES",
|
|
264
|
+
"FormatCheck",
|
|
265
|
+
"TargetError",
|
|
266
|
+
"formatter_config",
|
|
267
|
+
"target_files",
|
|
268
|
+
]
|