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,75 @@
|
|
|
1
|
+
"""A configurable regex-based log parser, for tools with no dedicated parser.
|
|
2
|
+
|
|
3
|
+
The pattern must define named groups ``file``, ``line`` and ``msg``; ``rule`` and
|
|
4
|
+
``severity`` are optional. One `Issue` is produced per match, found with
|
|
5
|
+
`re.finditer` over the whole log text (so `pattern` may itself be multi-line).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from chipgraph.core.contracts import Issue
|
|
13
|
+
from chipgraph.core.contracts.types import Severity
|
|
14
|
+
|
|
15
|
+
_REQUIRED_GROUPS = ("file", "line", "msg")
|
|
16
|
+
_SEVERITY_PREFIXES: tuple[tuple[str, Severity], ...] = (
|
|
17
|
+
("err", "error"),
|
|
18
|
+
("warn", "warning"),
|
|
19
|
+
("info", "info"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GenericRegexParser:
|
|
24
|
+
"""A `LogParser` driven entirely by a user-supplied regex pattern."""
|
|
25
|
+
|
|
26
|
+
name = "generic-regex"
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
pattern: str,
|
|
31
|
+
severity_group: str | None = None,
|
|
32
|
+
default_severity: Severity = "error",
|
|
33
|
+
) -> None:
|
|
34
|
+
try:
|
|
35
|
+
self._regex = re.compile(pattern, re.MULTILINE)
|
|
36
|
+
except re.error as exc:
|
|
37
|
+
raise ValueError(f"invalid regex pattern {pattern!r}: {exc}") from exc
|
|
38
|
+
|
|
39
|
+
group_names = set(self._regex.groupindex)
|
|
40
|
+
missing = [name for name in _REQUIRED_GROUPS if name not in group_names]
|
|
41
|
+
if missing:
|
|
42
|
+
raise ValueError(f"pattern {pattern!r} is missing required named group(s): {missing}")
|
|
43
|
+
if severity_group is not None and severity_group not in group_names:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"severity_group {severity_group!r} is not a named group in {pattern!r}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self._severity_group = severity_group
|
|
49
|
+
self._default_severity = default_severity
|
|
50
|
+
|
|
51
|
+
def parse(self, log: str) -> tuple[Issue, ...]:
|
|
52
|
+
issues: list[Issue] = []
|
|
53
|
+
for match in self._regex.finditer(log):
|
|
54
|
+
groups = match.groupdict()
|
|
55
|
+
issues.append(
|
|
56
|
+
Issue(
|
|
57
|
+
file=groups.get("file") or None,
|
|
58
|
+
line=int(groups["line"]) if groups.get("line") else None,
|
|
59
|
+
rule=groups.get("rule") or "",
|
|
60
|
+
severity=self._severity(groups),
|
|
61
|
+
msg=(groups.get("msg") or "").strip(),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return tuple(issues)
|
|
65
|
+
|
|
66
|
+
def _severity(self, groups: dict[str, str | None]) -> Severity:
|
|
67
|
+
group_name = self._severity_group or "severity"
|
|
68
|
+
raw = groups.get(group_name)
|
|
69
|
+
if not raw:
|
|
70
|
+
return self._default_severity
|
|
71
|
+
lowered = raw.strip().lower()
|
|
72
|
+
for prefix, severity in _SEVERITY_PREFIXES:
|
|
73
|
+
if lowered.startswith(prefix):
|
|
74
|
+
return severity
|
|
75
|
+
return self._default_severity
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Log parser for `verible-verilog-lint` output.
|
|
2
|
+
|
|
3
|
+
Handles both the column-range shape (``file:line:col1-col2: msg [rule]``) and the
|
|
4
|
+
single-column shape (``file:line:col: msg [rule]``). A lint finding is always a style
|
|
5
|
+
warning; a `syntax error at token ...` line (verible reports these the same way, with
|
|
6
|
+
no trailing `[rule]` bracket) is treated as an error with rule ``syntax``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from chipgraph.core.contracts import Issue
|
|
14
|
+
from chipgraph.core.contracts.types import Severity
|
|
15
|
+
|
|
16
|
+
_LINE_RE = re.compile(
|
|
17
|
+
r"^(?P<file>[^\s:]+):(?P<line>\d+):(?P<col1>\d+)(-(?P<col2>\d+))?:\s*(?P<msg>.*)$"
|
|
18
|
+
)
|
|
19
|
+
_TRAILING_RULE_RE = re.compile(r"\s*\[(?P<rule>[^\[\]]+)\]\s*$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VeribleParser:
|
|
23
|
+
"""Parses `verible-verilog-lint` logs into `Issue`s."""
|
|
24
|
+
|
|
25
|
+
name = "verible"
|
|
26
|
+
|
|
27
|
+
def parse(self, log: str) -> tuple[Issue, ...]:
|
|
28
|
+
issues: list[Issue] = []
|
|
29
|
+
for raw_line in log.splitlines():
|
|
30
|
+
match = _LINE_RE.match(raw_line)
|
|
31
|
+
if match is None:
|
|
32
|
+
continue
|
|
33
|
+
msg = match.group("msg").strip()
|
|
34
|
+
severity: Severity
|
|
35
|
+
rule_match = _TRAILING_RULE_RE.search(msg)
|
|
36
|
+
if rule_match is not None:
|
|
37
|
+
rule = rule_match.group("rule")
|
|
38
|
+
msg = msg[: rule_match.start()].rstrip()
|
|
39
|
+
severity = "warning"
|
|
40
|
+
elif "syntax error" in msg:
|
|
41
|
+
rule = "syntax"
|
|
42
|
+
severity = "error"
|
|
43
|
+
else:
|
|
44
|
+
rule = ""
|
|
45
|
+
severity = "warning"
|
|
46
|
+
issues.append(
|
|
47
|
+
Issue(
|
|
48
|
+
file=match.group("file"),
|
|
49
|
+
line=int(match.group("line")),
|
|
50
|
+
rule=rule,
|
|
51
|
+
severity=severity,
|
|
52
|
+
msg=msg,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return tuple(issues)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Log parser for Verilator (`--lint-only` and simulation build) output.
|
|
2
|
+
|
|
3
|
+
Handles both the 5.0xx line shape with a column (``%Error-CODE: file:line:col: msg``)
|
|
4
|
+
and the older shape without one (``%Warning-CODE: file:line: msg``), across the
|
|
5
|
+
Verilator versions this project has fixtures for (5.020 and 5.052). Continuation
|
|
6
|
+
lines (indented source context, ``... For warning description ...`` hints) and the
|
|
7
|
+
final summary line (``%Error: Exiting due to N error(s)``, which names no file) are
|
|
8
|
+
ignored.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from chipgraph.core.contracts import Issue
|
|
16
|
+
from chipgraph.core.contracts.types import Severity
|
|
17
|
+
|
|
18
|
+
_LINE_RE = re.compile(
|
|
19
|
+
r"^\s*%(?P<kind>Error|Warning)(-(?P<rule>[A-Za-z0-9_]+))?:\s+"
|
|
20
|
+
r"(?P<file>[^\s:]+):(?P<line>\d+)(:(?P<col>\d+))?:\s*(?P<msg>.*)$"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VerilatorParser:
|
|
25
|
+
"""Parses Verilator lint/build logs into `Issue`s."""
|
|
26
|
+
|
|
27
|
+
name = "verilator"
|
|
28
|
+
|
|
29
|
+
def parse(self, log: str) -> tuple[Issue, ...]:
|
|
30
|
+
issues: list[Issue] = []
|
|
31
|
+
for raw_line in log.splitlines():
|
|
32
|
+
match = _LINE_RE.match(raw_line)
|
|
33
|
+
if match is None:
|
|
34
|
+
continue # continuation line, source context, or summary line
|
|
35
|
+
kind = match.group("kind")
|
|
36
|
+
severity: Severity = "error" if kind == "Error" else "warning"
|
|
37
|
+
issues.append(
|
|
38
|
+
Issue(
|
|
39
|
+
file=match.group("file"),
|
|
40
|
+
line=int(match.group("line")),
|
|
41
|
+
rule=match.group("rule") or "",
|
|
42
|
+
severity=severity,
|
|
43
|
+
msg=match.group("msg").strip(),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return tuple(issues)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""The `file` review adapter: one small YAML decision file per approval.
|
|
2
|
+
|
|
3
|
+
Implements `chipgraph.core.plugin_api.protocols.ReviewAdapter`. Every recorded
|
|
4
|
+
decision (approve, reject, baseline, waive) is written as its own file under a
|
|
5
|
+
`decisions_dir` (typically `.chipgraph/decisions/`), so two people approving at the
|
|
6
|
+
same time never conflict on the same file (DESIGN.md 6.1).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import secrets
|
|
14
|
+
import tempfile
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from chipgraph.core.contracts import Approval
|
|
22
|
+
|
|
23
|
+
_UNSAFE_ID_CHARS = re.compile(r"[^A-Za-z0-9._-]")
|
|
24
|
+
_MAX_NAME_ATTEMPTS = 8
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReviewError(Exception):
|
|
28
|
+
"""Raised when a decision file cannot be recorded or a decision file is malformed."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _safe_id(gate_id: str) -> str:
|
|
32
|
+
"""Replace any character outside `[A-Za-z0-9._-]` in `gate_id` with `_`."""
|
|
33
|
+
return _UNSAFE_ID_CHARS.sub("_", gate_id)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FileReview:
|
|
37
|
+
"""Records approvals as one YAML file per decision under `decisions_dir`."""
|
|
38
|
+
|
|
39
|
+
name = "file"
|
|
40
|
+
|
|
41
|
+
def __init__(self, decisions_dir: Path | None = None) -> None:
|
|
42
|
+
# The registry builds adapters with no arguments; the default is the current
|
|
43
|
+
# project's `.chipgraph/decisions/`, and callers that know the root pass it.
|
|
44
|
+
self.decisions_dir = (
|
|
45
|
+
decisions_dir
|
|
46
|
+
if decisions_dir is not None
|
|
47
|
+
else (Path.cwd() / ".chipgraph" / "decisions")
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def record(self, approval: Approval) -> None:
|
|
51
|
+
"""Write `approval` to a new file in `decisions_dir`.
|
|
52
|
+
|
|
53
|
+
The file name is `<safe gate id>--<UTC YYYYmmddTHHMMSSZ>-<6 hex>.yml`. The
|
|
54
|
+
write is atomic (a temp file is written first, then moved into place with
|
|
55
|
+
`os.replace`), and an existing file is never overwritten: on the vanishingly
|
|
56
|
+
unlikely chance of a name collision, a fresh random suffix is tried again.
|
|
57
|
+
"""
|
|
58
|
+
self.decisions_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
safe_id = _safe_id(approval.gate_id)
|
|
60
|
+
ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
61
|
+
content = yaml.safe_dump(approval.model_dump(mode="json"), sort_keys=True)
|
|
62
|
+
|
|
63
|
+
for _ in range(_MAX_NAME_ATTEMPTS):
|
|
64
|
+
suffix = secrets.token_hex(3)
|
|
65
|
+
target = self.decisions_dir / f"{safe_id}--{ts}-{suffix}.yml"
|
|
66
|
+
if target.exists():
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
70
|
+
dir=self.decisions_dir, prefix=".tmp-decision-", suffix=".yml"
|
|
71
|
+
)
|
|
72
|
+
tmp_path = Path(tmp_name)
|
|
73
|
+
try:
|
|
74
|
+
with os.fdopen(fd, "w") as handle:
|
|
75
|
+
handle.write(content)
|
|
76
|
+
if target.exists():
|
|
77
|
+
tmp_path.unlink(missing_ok=True)
|
|
78
|
+
continue
|
|
79
|
+
os.replace(tmp_path, target)
|
|
80
|
+
except BaseException:
|
|
81
|
+
tmp_path.unlink(missing_ok=True)
|
|
82
|
+
raise
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
raise ReviewError(
|
|
86
|
+
f"could not allocate a unique decision file name for gate {approval.gate_id!r} "
|
|
87
|
+
f"in {self.decisions_dir}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def approvals(self, gate_id: str) -> tuple[Approval, ...]:
|
|
91
|
+
"""Return every approval recorded for `gate_id`, sorted by `at` then file name.
|
|
92
|
+
|
|
93
|
+
Every `*.yml`/`*.yaml` file in `decisions_dir` (except temp files left behind
|
|
94
|
+
by an interrupted write, which start with `.`) is parsed. A file that is not
|
|
95
|
+
valid YAML, not a mapping, or does not validate as an `Approval` raises
|
|
96
|
+
`ReviewError` naming that file; it is never silently skipped.
|
|
97
|
+
"""
|
|
98
|
+
if not self.decisions_dir.is_dir():
|
|
99
|
+
return ()
|
|
100
|
+
|
|
101
|
+
matches: list[tuple[Approval, str]] = []
|
|
102
|
+
paths = sorted({*self.decisions_dir.glob("*.yml"), *self.decisions_dir.glob("*.yaml")})
|
|
103
|
+
for path in paths:
|
|
104
|
+
if path.name.startswith("."):
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
raw = yaml.safe_load(path.read_text())
|
|
108
|
+
except yaml.YAMLError as exc:
|
|
109
|
+
raise ReviewError(f"malformed decision file {path}: invalid YAML: {exc}") from exc
|
|
110
|
+
|
|
111
|
+
if not isinstance(raw, dict):
|
|
112
|
+
raise ReviewError(f"malformed decision file {path}: not a YAML mapping")
|
|
113
|
+
if raw.get("gate_id") != gate_id:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
approval = Approval.model_validate(raw)
|
|
118
|
+
except ValidationError as exc:
|
|
119
|
+
raise ReviewError(f"malformed decision file {path}: {exc}") from exc
|
|
120
|
+
matches.append((approval, path.name))
|
|
121
|
+
|
|
122
|
+
matches.sort(key=lambda pair: (pair[0].at, pair[1]))
|
|
123
|
+
return tuple(approval for approval, _ in matches)
|