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,339 @@
|
|
|
1
|
+
"""Wire `chipgraph baseline` to the graph, git, the review store and the finding store.
|
|
2
|
+
|
|
3
|
+
The generic planning lives in `chipgraph.core.engine.baseline`; this module supplies the
|
|
4
|
+
concrete facts it needs and applies the plan:
|
|
5
|
+
|
|
6
|
+
- it builds the graph for every target, collects each gated instance's gate and the
|
|
7
|
+
file inputs it covers, and asks the `GateEvaluator` whether each gate is already
|
|
8
|
+
decided;
|
|
9
|
+
- it answers `VcsFacts` (content hash, tracked/dirty, last commit) from the git working
|
|
10
|
+
tree and history;
|
|
11
|
+
- on ``--confirm`` it records one `baseline` decision per undecided gate over its clean,
|
|
12
|
+
tracked artifacts (through `core.engine.gate.baseline`) and marks every open finding as
|
|
13
|
+
baselined, and it refuses to run on a branch other than the configured main branch.
|
|
14
|
+
|
|
15
|
+
`baseline` runs after `ingest`; the spec files ingest reads are ordinary file artifacts
|
|
16
|
+
here, gated by whatever rule consumes them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from datetime import UTC, datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from chipgraph.app.build import load_rules
|
|
28
|
+
from chipgraph.app.context import AppContext
|
|
29
|
+
from chipgraph.app.errors import AppError
|
|
30
|
+
from chipgraph.app.findings import finding_current_hashes
|
|
31
|
+
from chipgraph.core.contracts import Approval, ArtifactRef, Finding, RuleInstance
|
|
32
|
+
from chipgraph.core.engine import gate as gate_mod
|
|
33
|
+
from chipgraph.core.engine.baseline import (
|
|
34
|
+
BaselinePlan,
|
|
35
|
+
CommitInfo,
|
|
36
|
+
GateInput,
|
|
37
|
+
clean_refs_for_gate,
|
|
38
|
+
gate_inputs_from_instances,
|
|
39
|
+
plan_baseline,
|
|
40
|
+
)
|
|
41
|
+
from chipgraph.core.engine.graph import GraphError, build_graph
|
|
42
|
+
from chipgraph.core.engine.scheduler import _format_gate
|
|
43
|
+
from chipgraph.core.state.findings import FindingStore, effective_status, waiver_gate_id
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"BaselineOutcome",
|
|
47
|
+
"WrongBranchError",
|
|
48
|
+
"baseline_gate_id",
|
|
49
|
+
"current_branch",
|
|
50
|
+
"main_branch",
|
|
51
|
+
"plan_project_baseline",
|
|
52
|
+
"run_baseline",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
_DEFAULT_MAIN_BRANCH = "main"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def baseline_gate_id(finding_id: str) -> str:
|
|
59
|
+
"""The `Approval.gate_id` a finding's `baseline` decision is recorded under.
|
|
60
|
+
|
|
61
|
+
Distinct from the waiver gate id (`finding:<id>`), so a baseline decision never
|
|
62
|
+
reads as a waiver: `chipgraph findings` keeps listing the finding as open (recorded,
|
|
63
|
+
non-blocking), which is what a baseline of existing findings means (DESIGN.md 6.4).
|
|
64
|
+
"""
|
|
65
|
+
return f"baseline:finding:{finding_id}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WrongBranchError(AppError):
|
|
69
|
+
"""Raised when `baseline` runs on a branch other than the configured main branch."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class BaselineOutcome:
|
|
74
|
+
"""What a `--confirm` run recorded: the decisions written and findings baselined."""
|
|
75
|
+
|
|
76
|
+
approvals: tuple[Approval, ...]
|
|
77
|
+
findings_baselined: tuple[str, ...]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- git facts ----------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
84
|
+
"""Run a read-only git command under `root`, capturing output (never a shell)."""
|
|
85
|
+
return subprocess.run(
|
|
86
|
+
["git", "-C", str(root), *args],
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
check=False,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def current_branch(root: Path) -> str | None:
|
|
94
|
+
"""The current branch name, or `None` when detached / not a git repo."""
|
|
95
|
+
result = _git(root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
96
|
+
if result.returncode != 0:
|
|
97
|
+
return None
|
|
98
|
+
name = result.stdout.strip()
|
|
99
|
+
return None if name in ("", "HEAD") else name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main_branch(root: Path) -> str:
|
|
103
|
+
"""The configured main branch: `origin/HEAD`'s target, falling back to `main`.
|
|
104
|
+
|
|
105
|
+
Read from `git symbolic-ref refs/remotes/origin/HEAD` (e.g. `origin/main` -> `main`);
|
|
106
|
+
if there is no such ref, the default `main` is used (DESIGN.md 6.4).
|
|
107
|
+
"""
|
|
108
|
+
result = _git(root, "symbolic-ref", "refs/remotes/origin/HEAD")
|
|
109
|
+
if result.returncode == 0:
|
|
110
|
+
ref = result.stdout.strip()
|
|
111
|
+
prefix = "refs/remotes/origin/"
|
|
112
|
+
if ref.startswith(prefix):
|
|
113
|
+
name = ref[len(prefix) :]
|
|
114
|
+
if name:
|
|
115
|
+
return name
|
|
116
|
+
return _DEFAULT_MAIN_BRANCH
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _GitFacts:
|
|
120
|
+
"""`VcsFacts` answered from a git working tree and history under `root`.
|
|
121
|
+
|
|
122
|
+
Tracked/dirty status and content hashes come from the working tree; `last_commit`
|
|
123
|
+
reads `git log`. Results are cached per path so a listing that touches an artifact
|
|
124
|
+
from several gates does not re-shell for it.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, ctx: AppContext) -> None:
|
|
128
|
+
self._ctx = ctx
|
|
129
|
+
self._root = ctx.root
|
|
130
|
+
self._sha: dict[str, str | None] = {}
|
|
131
|
+
self._tracked: dict[str, bool] = {}
|
|
132
|
+
self._dirty: dict[str, bool] = {}
|
|
133
|
+
self._commit: dict[str, CommitInfo | None] = {}
|
|
134
|
+
self._changed = self._collect_changed()
|
|
135
|
+
|
|
136
|
+
def _collect_changed(self) -> frozenset[str]:
|
|
137
|
+
result = _git(self._root, "status", "--porcelain=v1", "-z")
|
|
138
|
+
paths: set[str] = set()
|
|
139
|
+
for entry in result.stdout.split("\0"):
|
|
140
|
+
if len(entry) > 3:
|
|
141
|
+
paths.add(entry[3:])
|
|
142
|
+
return frozenset(paths)
|
|
143
|
+
|
|
144
|
+
def sha256(self, path: str) -> str | None:
|
|
145
|
+
if path not in self._sha:
|
|
146
|
+
ref = ArtifactRef(kind="other", path=path)
|
|
147
|
+
hashes = self._ctx.store.current_hashes((ref,))
|
|
148
|
+
self._sha[path] = hashes.get(f"{ref.repo}:{path}")
|
|
149
|
+
return self._sha[path]
|
|
150
|
+
|
|
151
|
+
def is_tracked(self, path: str) -> bool:
|
|
152
|
+
if path not in self._tracked:
|
|
153
|
+
result = _git(self._root, "ls-files", "--error-unmatch", "--", path)
|
|
154
|
+
self._tracked[path] = result.returncode == 0
|
|
155
|
+
return self._tracked[path]
|
|
156
|
+
|
|
157
|
+
def is_dirty(self, path: str) -> bool:
|
|
158
|
+
if path not in self._dirty:
|
|
159
|
+
# Untracked paths appear in `git status` too; only a *tracked* modified path
|
|
160
|
+
# is "dirty" here (an untracked one is reported by `is_tracked`).
|
|
161
|
+
self._dirty[path] = path in self._changed and self.is_tracked(path)
|
|
162
|
+
return self._dirty[path]
|
|
163
|
+
|
|
164
|
+
def last_commit(self, path: str) -> CommitInfo | None:
|
|
165
|
+
if path not in self._commit:
|
|
166
|
+
self._commit[path] = self._read_last_commit(path)
|
|
167
|
+
return self._commit[path]
|
|
168
|
+
|
|
169
|
+
def _read_last_commit(self, path: str) -> CommitInfo | None:
|
|
170
|
+
# %h short sha, %ad author date (short), %s subject, %P parents (to spot merges).
|
|
171
|
+
# %P is last so an empty parent list (a root commit) just yields a trailing empty
|
|
172
|
+
# field; parse with a fixed split count so that empty field is never lost.
|
|
173
|
+
result = _git(
|
|
174
|
+
self._root,
|
|
175
|
+
"log",
|
|
176
|
+
"-1",
|
|
177
|
+
"--date=short",
|
|
178
|
+
"--format=%h%x1f%ad%x1f%s%x1f%P",
|
|
179
|
+
"--",
|
|
180
|
+
path,
|
|
181
|
+
)
|
|
182
|
+
if result.returncode != 0:
|
|
183
|
+
return None
|
|
184
|
+
line = result.stdout.rstrip("\n")
|
|
185
|
+
if not line:
|
|
186
|
+
return None
|
|
187
|
+
parts = line.split("\x1f", 3)
|
|
188
|
+
if len(parts) < 3:
|
|
189
|
+
return None
|
|
190
|
+
short_sha, date, subject = parts[0], parts[1], parts[2]
|
|
191
|
+
parents = parts[3] if len(parts) > 3 else ""
|
|
192
|
+
is_merge = len(parents.split()) > 1
|
|
193
|
+
return CommitInfo(short_sha=short_sha, date=date, subject=subject, is_merge=is_merge)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --- planning -----------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _gated_instances(ctx: AppContext) -> list[tuple[str, RuleInstance, bool]]:
|
|
200
|
+
"""Every gated instance across all targets: `(gate_id, instance, already_decided)`.
|
|
201
|
+
|
|
202
|
+
The graph is built once over `"*"` (all targets). A rule with a `gate` yields one
|
|
203
|
+
entry per instance, with the gate id formatted from the instance's params; whether
|
|
204
|
+
the gate is already decided is asked of the project's `GateEvaluator`.
|
|
205
|
+
"""
|
|
206
|
+
rules = load_rules(ctx)
|
|
207
|
+
from chipgraph.app.build import _resolver_for
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
graph = build_graph(rules, _resolver_for(ctx), repo=ctx.store.repo)
|
|
211
|
+
except GraphError as exc:
|
|
212
|
+
raise AppError(str(exc)) from exc
|
|
213
|
+
|
|
214
|
+
gated: list[tuple[str, RuleInstance, bool]] = []
|
|
215
|
+
seen: set[tuple[str, str]] = set()
|
|
216
|
+
for iid in sorted(graph.instances):
|
|
217
|
+
instance = graph.instances[iid]
|
|
218
|
+
rule = graph.rules[instance.rule_id]
|
|
219
|
+
if not rule.gate:
|
|
220
|
+
continue
|
|
221
|
+
gate_id = _format_gate(rule.gate, instance.params)
|
|
222
|
+
key = (gate_id, iid)
|
|
223
|
+
if key in seen:
|
|
224
|
+
continue
|
|
225
|
+
seen.add(key)
|
|
226
|
+
already = ctx.gates.status(gate_id, instance) != "waiting"
|
|
227
|
+
gated.append((gate_id, instance, already))
|
|
228
|
+
return gated
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _refs_by_path(gated: Iterable[tuple[str, RuleInstance, bool]]) -> dict[str, ArtifactRef]:
|
|
232
|
+
"""Map each gated instance's file-input path to its `ArtifactRef` (repo/label kept)."""
|
|
233
|
+
by_path: dict[str, ArtifactRef] = {}
|
|
234
|
+
for _gate_id, instance, _decided in gated:
|
|
235
|
+
for ref in instance.inputs:
|
|
236
|
+
if ref.path is not None and ref.path not in by_path:
|
|
237
|
+
by_path[ref.path] = ref
|
|
238
|
+
return by_path
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _open_finding_count(ctx: AppContext) -> int:
|
|
242
|
+
return len(_open_findings(ctx))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _open_findings(ctx: AppContext) -> list[Finding]:
|
|
246
|
+
store = FindingStore(ctx.layout)
|
|
247
|
+
open_rows: list[Finding] = []
|
|
248
|
+
for finding in store.list():
|
|
249
|
+
waivers = ctx.review.approvals(waiver_gate_id(finding.id))
|
|
250
|
+
current_hashes = finding_current_hashes(ctx.store, finding, waivers)
|
|
251
|
+
if effective_status(finding, waivers, current_hashes) == "open":
|
|
252
|
+
open_rows.append(finding)
|
|
253
|
+
return open_rows
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def plan_project_baseline(ctx: AppContext) -> tuple[BaselinePlan, dict[str, ArtifactRef]]:
|
|
257
|
+
"""Build the `BaselinePlan` for this project, plus a path -> `ArtifactRef` map.
|
|
258
|
+
|
|
259
|
+
The map lets `run_baseline` pin a decision to the exact refs (with repo and label)
|
|
260
|
+
a gate covers, not just their paths.
|
|
261
|
+
"""
|
|
262
|
+
gated = _gated_instances(ctx)
|
|
263
|
+
facts = _GitFacts(ctx)
|
|
264
|
+
gate_inputs: list[GateInput] = gate_inputs_from_instances(gated)
|
|
265
|
+
plan = plan_baseline(gate_inputs, facts, open_findings=_open_finding_count(ctx))
|
|
266
|
+
return plan, _refs_by_path(gated)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# --- confirming ---------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def run_baseline(
|
|
273
|
+
ctx: AppContext,
|
|
274
|
+
plan: BaselinePlan,
|
|
275
|
+
refs_by_path: dict[str, ArtifactRef],
|
|
276
|
+
*,
|
|
277
|
+
by: str,
|
|
278
|
+
note: str = "",
|
|
279
|
+
) -> BaselineOutcome:
|
|
280
|
+
"""Record the plan: one `baseline` decision per undecided gate, mark findings baselined.
|
|
281
|
+
|
|
282
|
+
Idempotent: a gate that is already decided (skipped) records nothing, so running this
|
|
283
|
+
again after a first confirm writes nothing new. A gate with no clean, tracked artifact
|
|
284
|
+
is skipped too (there is nothing on the main branch to pin). Returns what was written.
|
|
285
|
+
"""
|
|
286
|
+
approvals: list[Approval] = []
|
|
287
|
+
for gate in plan.gates:
|
|
288
|
+
if not gate.will_baseline:
|
|
289
|
+
continue
|
|
290
|
+
refs = clean_refs_for_gate(gate, refs_by_path)
|
|
291
|
+
if not refs:
|
|
292
|
+
continue
|
|
293
|
+
try:
|
|
294
|
+
approval = gate_mod.baseline(
|
|
295
|
+
ctx.review, ctx.store, gate.gate_id, refs, by=by, note=note
|
|
296
|
+
)
|
|
297
|
+
except gate_mod.GateError as exc:
|
|
298
|
+
raise AppError(str(exc)) from exc
|
|
299
|
+
approvals.append(approval)
|
|
300
|
+
|
|
301
|
+
baselined = _baseline_open_findings(ctx, by=by, note=note)
|
|
302
|
+
return BaselineOutcome(approvals=tuple(approvals), findings_baselined=tuple(baselined))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _baseline_open_findings(ctx: AppContext, *, by: str, note: str) -> list[str]:
|
|
306
|
+
"""Record a `baseline` decision against every open finding, keyed by its id.
|
|
307
|
+
|
|
308
|
+
Findings never block a build, so "baselined" here means *recorded, non-blocking,
|
|
309
|
+
still listed*: a `baseline` `Approval` bound to the finding's current artifact hashes
|
|
310
|
+
is written through the review store, distinct from a `waive` (so `chipgraph findings`
|
|
311
|
+
still lists the finding as open). Idempotent per (finding, hashes): a finding whose
|
|
312
|
+
artifacts have a current baseline decision is not recorded again.
|
|
313
|
+
"""
|
|
314
|
+
recorded: list[str] = []
|
|
315
|
+
now = datetime.now(UTC)
|
|
316
|
+
for finding in _open_findings(ctx):
|
|
317
|
+
gate_id = baseline_gate_id(finding.id)
|
|
318
|
+
hashes = ctx.store.current_hashes(finding.artifacts)
|
|
319
|
+
if not hashes:
|
|
320
|
+
# No artifact to pin (e.g. a whole-check finding); still count it as recorded
|
|
321
|
+
# via its detection-time hashes if it has any, else skip pinning.
|
|
322
|
+
hashes = dict(finding.artifact_hashes)
|
|
323
|
+
if not hashes:
|
|
324
|
+
continue
|
|
325
|
+
existing = ctx.review.approvals(gate_id)
|
|
326
|
+
if any(a.decision == "baseline" and a.is_current(hashes) for a in existing):
|
|
327
|
+
continue
|
|
328
|
+
ctx.review.record(
|
|
329
|
+
Approval(
|
|
330
|
+
gate_id=gate_id,
|
|
331
|
+
by=by,
|
|
332
|
+
at=now,
|
|
333
|
+
artifact_hashes=hashes,
|
|
334
|
+
decision="baseline",
|
|
335
|
+
note=note or "baselined: recorded, non-blocking",
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
recorded.append(finding.id)
|
|
339
|
+
return recorded
|
chipgraph/app/build.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Building a `Scheduler` for a target: load rules from the profile's packs, expand
|
|
2
|
+
`foreach` over the profile's blocks, and wire up the executors, check runner and gate
|
|
3
|
+
evaluator (DESIGN.md 3.3, 3.4).
|
|
4
|
+
|
|
5
|
+
The graph also knows the packs' named targets (`targets/*.yml` in a pack, e.g.
|
|
6
|
+
`rtl:<block>`; `chipgraph.core.engine.targets`), so a target may be one everywhere a rule
|
|
7
|
+
selector may. With `review: {use: github}` the PR gates (`pr:<...>`) are read from the
|
|
8
|
+
pull request's reviews (`GitHubGateChecker`); every other gate, and every gate with the
|
|
9
|
+
`file` adapter, from the decision files (`chipgraph approve`).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
16
|
+
from importlib import resources
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from chipgraph.adapters.review.github import GitHubGateChecker
|
|
21
|
+
from chipgraph.adapters.runtime.claude_code import ClaudeCodeExecutor, ClaudeCodeRuntime
|
|
22
|
+
from chipgraph.app.checks import ProfileCheckRunner
|
|
23
|
+
from chipgraph.app.context import AppContext
|
|
24
|
+
from chipgraph.app.errors import AppError
|
|
25
|
+
from chipgraph.app.executors import HumanExecutor, RunExecutor
|
|
26
|
+
from chipgraph.core.contracts import ArtifactRef, RuleInstance, RuleSpec
|
|
27
|
+
from chipgraph.core.engine.agent_rule import AgentRuleExecutor
|
|
28
|
+
from chipgraph.core.engine.graph import (
|
|
29
|
+
BuildGraph,
|
|
30
|
+
ForeachEntry,
|
|
31
|
+
ForeachResolver,
|
|
32
|
+
GraphError,
|
|
33
|
+
build_graph,
|
|
34
|
+
)
|
|
35
|
+
from chipgraph.core.engine.rules import RuleLoadError, load_pack_rules
|
|
36
|
+
from chipgraph.core.engine.scheduler import AgentStub, Executor, GateChecker, Scheduler
|
|
37
|
+
from chipgraph.core.engine.targets import TargetError, TargetRegistry, load_pack_targets
|
|
38
|
+
from chipgraph.core.plugin_api.pack import Pack, discover_packs
|
|
39
|
+
from chipgraph.core.plugin_api.registry import PluginError
|
|
40
|
+
from chipgraph.core.runtime import TaskQueue
|
|
41
|
+
from chipgraph.core.runtime.roles import RoleError, check_agent_rules, task_denied_reads
|
|
42
|
+
from chipgraph.core.state.trace import Tracer
|
|
43
|
+
|
|
44
|
+
PLAN_MODULES = "plan.modules"
|
|
45
|
+
"""The `foreach` selector over the modules of every block's approved plan (M2-03)."""
|
|
46
|
+
PLAN_BLOCK = "plan.block"
|
|
47
|
+
"""The `foreach` selector over the blocks with an approved plan (M2-11a)."""
|
|
48
|
+
PLAN_BLOCKS = "plan.blocks"
|
|
49
|
+
"""The `foreach` selector over every block, after its approved plan's files (M2-11a)."""
|
|
50
|
+
PLAN_SELECTORS = (PLAN_MODULES, PLAN_BLOCK, PLAN_BLOCKS)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def builtin_packs_dir() -> Path | None:
|
|
54
|
+
"""The directory of the packs shipped inside the `chipgraph.packs` package (D36).
|
|
55
|
+
|
|
56
|
+
Found through `importlib.resources`, so it is the same whether chipgraph runs from a
|
|
57
|
+
checkout or is installed from a wheel. `None` if the package is not on a real
|
|
58
|
+
filesystem (e.g. imported from a zip), since packs are loaded from directories.
|
|
59
|
+
"""
|
|
60
|
+
root = resources.files("chipgraph.packs")
|
|
61
|
+
return root if isinstance(root, Path) and root.is_dir() else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def pack_search_paths(ctx: AppContext) -> list[Path]:
|
|
65
|
+
"""The search paths packs are discovered from, in order (DESIGN.md 3.2, D36).
|
|
66
|
+
|
|
67
|
+
The project's `.chipgraph/packs/`, then the built-in packs, then each entry of
|
|
68
|
+
`$CHIPGRAPH_PACK_PATH`. Two packs with the same name anywhere on the path are an error.
|
|
69
|
+
"""
|
|
70
|
+
return pack_search_paths_for(ctx.root)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def pack_search_paths_for(root: Path) -> list[Path]:
|
|
74
|
+
"""`pack_search_paths` for the project at `root`."""
|
|
75
|
+
paths = [root / ".chipgraph" / "packs"]
|
|
76
|
+
builtin = builtin_packs_dir()
|
|
77
|
+
if builtin is not None:
|
|
78
|
+
paths.append(builtin)
|
|
79
|
+
env_value = os.environ.get("CHIPGRAPH_PACK_PATH", "")
|
|
80
|
+
paths.extend(Path(p) for p in env_value.split(os.pathsep) if p)
|
|
81
|
+
return paths
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_rules(ctx: AppContext) -> list[RuleSpec]:
|
|
85
|
+
"""Load every rule from the packs named in the profile's `packs`."""
|
|
86
|
+
return rules_for_packs(ctx.root, ctx.require_profile().profile.packs)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def rules_for_packs(root: Path, packs: Iterable[str]) -> list[RuleSpec]:
|
|
90
|
+
"""Load every rule of the packs named `packs`, searched for from the project at `root`."""
|
|
91
|
+
rules: list[RuleSpec] = []
|
|
92
|
+
for pack in _packs(root, packs):
|
|
93
|
+
try:
|
|
94
|
+
rules.extend(load_pack_rules(pack))
|
|
95
|
+
except RuleLoadError as exc:
|
|
96
|
+
raise AppError(str(exc)) from exc
|
|
97
|
+
return rules
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _packs(root: Path, packs: Iterable[str]) -> list[Pack]:
|
|
101
|
+
search_paths = pack_search_paths_for(root)
|
|
102
|
+
try:
|
|
103
|
+
available: dict[str, Pack] = discover_packs(search_paths)
|
|
104
|
+
except PluginError as exc:
|
|
105
|
+
raise AppError(str(exc)) from exc
|
|
106
|
+
found: list[Pack] = []
|
|
107
|
+
for name in packs:
|
|
108
|
+
pack = available.get(name)
|
|
109
|
+
if pack is None:
|
|
110
|
+
searched = ", ".join(str(p) for p in search_paths)
|
|
111
|
+
raise AppError(f"pack {name!r} not found; searched: {searched}")
|
|
112
|
+
found.append(pack)
|
|
113
|
+
return found
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def targets_for_packs(root: Path, packs: Iterable[str]) -> TargetRegistry:
|
|
117
|
+
"""The named targets of the packs named `packs` (`<pack>/targets/*.yml`)."""
|
|
118
|
+
try:
|
|
119
|
+
return TargetRegistry(
|
|
120
|
+
spec for pack in _packs(root, packs) for spec in load_pack_targets(pack)
|
|
121
|
+
)
|
|
122
|
+
except TargetError as exc:
|
|
123
|
+
raise AppError(str(exc)) from exc
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_targets(ctx: AppContext) -> TargetRegistry:
|
|
127
|
+
"""The named targets of the profile's packs."""
|
|
128
|
+
return targets_for_packs(ctx.root, ctx.require_profile().profile.packs)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _uses_layout(rules: Iterable[RuleSpec]) -> bool:
|
|
132
|
+
return any(
|
|
133
|
+
"{layout." in text
|
|
134
|
+
for rule in rules
|
|
135
|
+
for text in (*rule.outputs, *(i.selector for i in rule.inputs))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _ProfileForeach:
|
|
140
|
+
"""The project's `ForeachResolver`: `blocks`, and the selectors over approved plans.
|
|
141
|
+
|
|
142
|
+
`blocks` expands to one entry `{"block": b}` per key of `profile.blocks`, sorted,
|
|
143
|
+
with the block's layout directories as template values (`{layout.rtl}`,
|
|
144
|
+
`{layout.tb}`, `chipgraph.packs.digital_rtl.layout`) when a rule uses them. From the
|
|
145
|
+
`digital-rtl` pack: `plan.modules` expands to one entry per module of every block's
|
|
146
|
+
approved plan (each with the module's write set, its dependencies as ordering edges,
|
|
147
|
+
the approved plan and the style files as inputs), `plan.block` to one entry per block
|
|
148
|
+
with an approved plan and `plan.blocks` to one per block (both with the plan's files
|
|
149
|
+
and the testbench as inputs, when it is approved). Any other `foreach` expression
|
|
150
|
+
needs the Design Model, which arrives in M1.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
ctx: AppContext,
|
|
156
|
+
rules: Sequence[RuleSpec],
|
|
157
|
+
plans: Callable[[], Any] | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
self._ctx = ctx
|
|
160
|
+
self._blocks = sorted(ctx.require_profile().profile.blocks)
|
|
161
|
+
self._layout = _uses_layout(rules)
|
|
162
|
+
self._plans_factory = plans
|
|
163
|
+
self._plans: Any = None
|
|
164
|
+
|
|
165
|
+
def _vars(self, block: str) -> dict[str, str]:
|
|
166
|
+
if not self._layout:
|
|
167
|
+
return {}
|
|
168
|
+
from chipgraph.packs.digital_rtl.layout import LayoutError, layout_vars
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
return layout_vars(self._ctx.require_profile(), block)
|
|
172
|
+
except LayoutError as exc:
|
|
173
|
+
raise AppError(str(exc)) from exc
|
|
174
|
+
|
|
175
|
+
def expand(self, expr: str) -> list[dict[str, str]]:
|
|
176
|
+
return [dict(entry.params) for entry in self.expand_entries(expr)]
|
|
177
|
+
|
|
178
|
+
def expand_entries(self, expr: str) -> list[ForeachEntry]:
|
|
179
|
+
if expr == "blocks":
|
|
180
|
+
return [
|
|
181
|
+
ForeachEntry(params={"block": block}, vars=self._vars(block))
|
|
182
|
+
for block in self._blocks
|
|
183
|
+
]
|
|
184
|
+
if expr in PLAN_SELECTORS and self._plans_factory is not None:
|
|
185
|
+
if self._plans is None: # once per graph, however many rules use it
|
|
186
|
+
self._plans = self._plans_factory()
|
|
187
|
+
if expr == PLAN_MODULES:
|
|
188
|
+
return list(self._plans.modules())
|
|
189
|
+
if expr == PLAN_BLOCK:
|
|
190
|
+
return list(self._plans.blocks_with_plan())
|
|
191
|
+
return list(self._plans.every_block())
|
|
192
|
+
raise AppError(f"foreach {expr!r} needs the Design Model (M1)")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _resolver_for(ctx: AppContext, rules: Iterable[RuleSpec] | None = None) -> ForeachResolver:
|
|
196
|
+
"""The project's resolver for a graph of `rules` (default: the profile's packs' rules)."""
|
|
197
|
+
rules = load_rules(ctx) if rules is None else list(rules)
|
|
198
|
+
plans: Callable[[], Any] | None = None
|
|
199
|
+
if any(rule.foreach in PLAN_SELECTORS for rule in rules):
|
|
200
|
+
from chipgraph.packs.digital_rtl.plan.resolver import PlanExpansion
|
|
201
|
+
|
|
202
|
+
def _plans() -> PlanExpansion:
|
|
203
|
+
return PlanExpansion(ctx, rules)
|
|
204
|
+
|
|
205
|
+
plans = _plans
|
|
206
|
+
resolver: ForeachResolver = _ProfileForeach(ctx, rules, plans)
|
|
207
|
+
from chipgraph.packs.digital_rtl.wrapper.resolver import SELECTORS, WrapperForeach
|
|
208
|
+
|
|
209
|
+
if any(rule.foreach in SELECTORS for rule in rules): # `wrappers` (M2-12)
|
|
210
|
+
resolver = WrapperForeach(resolver, ctx.require_profile().profile)
|
|
211
|
+
return resolver
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def project_graph(ctx: AppContext, rules: Sequence[RuleSpec] | None = None) -> BuildGraph:
|
|
215
|
+
"""The project's build graph: the packs' rules, expanded, with the packs' targets."""
|
|
216
|
+
rules = load_rules(ctx) if rules is None else list(rules)
|
|
217
|
+
try:
|
|
218
|
+
return build_graph(
|
|
219
|
+
rules, _resolver_for(ctx, rules), repo=ctx.store.repo, targets=load_targets(ctx)
|
|
220
|
+
)
|
|
221
|
+
except GraphError as exc:
|
|
222
|
+
raise AppError(str(exc)) from exc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def graph_artifacts(graph: BuildGraph) -> list[ArtifactRef]:
|
|
226
|
+
"""Every input and output of every instance of `graph`."""
|
|
227
|
+
return [ref for inst in graph.instances.values() for ref in (*inst.inputs, *inst.outputs)]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def denied_reads_for(
|
|
231
|
+
ctx: AppContext, graph: BuildGraph | None, role: str | None, keep: Iterable[str] = ()
|
|
232
|
+
) -> tuple[str, ...]:
|
|
233
|
+
"""What a task of `role` must not read in this project (`task_denied_reads`).
|
|
234
|
+
|
|
235
|
+
The one computation both runtimes use: the in-process loop filters the redo text by
|
|
236
|
+
it, runtime `claude-code` sets it on the task for the guard and `submit`.
|
|
237
|
+
"""
|
|
238
|
+
return task_denied_reads(
|
|
239
|
+
role,
|
|
240
|
+
artifacts=graph_artifacts(graph) if graph is not None else (),
|
|
241
|
+
profile=ctx.require_profile().profile,
|
|
242
|
+
keep=keep,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def agent_executor(ctx: AppContext, graph: BuildGraph | None = None) -> Executor:
|
|
247
|
+
"""The executor for `kind: agent` rules, chosen by the profile's `runtime` (D35).
|
|
248
|
+
|
|
249
|
+
`claude-code` queues each agent task for the user's Claude Code session (the plugin
|
|
250
|
+
command runs it and hands it back over MCP). Any other runtime runs in-process: when
|
|
251
|
+
an `AgentRuntime` plugin is registered under the profile's runtime name, its agent
|
|
252
|
+
rules run in the engine's loop (`AgentRuleExecutor`: write, check at once, triage,
|
|
253
|
+
retry within the budget, DESIGN.md 5.2). No such plugin ships yet (the API runtimes
|
|
254
|
+
arrive later), so without one agent rules fail with the scheduler's `AgentStub`.
|
|
255
|
+
"""
|
|
256
|
+
profile = ctx.require_profile().profile
|
|
257
|
+
if profile.runtime == "claude-code":
|
|
258
|
+
return ClaudeCodeExecutor(ClaudeCodeRuntime(TaskQueue(ctx.layout), ctx.store))
|
|
259
|
+
if profile.runtime in ctx.registry.names("runtime"):
|
|
260
|
+
runtime = ctx.registry.get("runtime", profile.runtime)
|
|
261
|
+
|
|
262
|
+
def denied(rule: RuleSpec, instance: RuleInstance) -> tuple[str, ...]:
|
|
263
|
+
keep = [ref.path for ref in instance.outputs if ref.path is not None]
|
|
264
|
+
return denied_reads_for(ctx, graph, rule.role, keep)
|
|
265
|
+
|
|
266
|
+
return AgentRuleExecutor(runtime, store=ctx.store, layout=ctx.layout, denied_reads=denied)
|
|
267
|
+
return AgentStub()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def gate_checker(ctx: AppContext) -> GateChecker:
|
|
271
|
+
"""The gates of this project: PR gates from GitHub reviews with `review: github`.
|
|
272
|
+
|
|
273
|
+
With `review: {use: github}` a gate whose id starts with `pr:` is the pull request's
|
|
274
|
+
review (`GitHubGateChecker`, pinned to the PR's head commit); any other gate, and
|
|
275
|
+
every gate otherwise, is the decision files' (`GateEvaluator`, `chipgraph approve`).
|
|
276
|
+
"""
|
|
277
|
+
if ctx.pr_review is not None:
|
|
278
|
+
return GitHubGateChecker(ctx.pr_review, fallback=ctx.gates)
|
|
279
|
+
return ctx.gates
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def make_scheduler(
|
|
283
|
+
ctx: AppContext, target: str, *, concurrency: int = 4, tracer: Tracer | None = None
|
|
284
|
+
) -> Scheduler:
|
|
285
|
+
"""Build a `Scheduler` ready to run/resume `target` for this project."""
|
|
286
|
+
graph = project_graph(ctx)
|
|
287
|
+
try:
|
|
288
|
+
graph.select(target)
|
|
289
|
+
except GraphError as exc:
|
|
290
|
+
raise AppError(str(exc)) from exc
|
|
291
|
+
try: # every agent rule must fit its role's tool table (DESIGN.md 5.1)
|
|
292
|
+
check_agent_rules(graph.rules, graph.instances.values())
|
|
293
|
+
except RoleError as exc:
|
|
294
|
+
raise AppError(str(exc)) from exc
|
|
295
|
+
|
|
296
|
+
run_executor = RunExecutor(ctx)
|
|
297
|
+
executors = {
|
|
298
|
+
"gen": run_executor,
|
|
299
|
+
"import": run_executor,
|
|
300
|
+
"human": HumanExecutor(ctx),
|
|
301
|
+
"agent": agent_executor(ctx, graph),
|
|
302
|
+
}
|
|
303
|
+
return Scheduler(
|
|
304
|
+
graph,
|
|
305
|
+
layout=ctx.layout,
|
|
306
|
+
store=ctx.store,
|
|
307
|
+
executors=executors, # type: ignore[arg-type]
|
|
308
|
+
checks=ProfileCheckRunner(ctx),
|
|
309
|
+
gates=gate_checker(ctx),
|
|
310
|
+
concurrency=concurrency,
|
|
311
|
+
tracer=tracer,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
__all__ = [
|
|
316
|
+
"PLAN_BLOCK",
|
|
317
|
+
"PLAN_BLOCKS",
|
|
318
|
+
"PLAN_MODULES",
|
|
319
|
+
"PLAN_SELECTORS",
|
|
320
|
+
"agent_executor",
|
|
321
|
+
"denied_reads_for",
|
|
322
|
+
"gate_checker",
|
|
323
|
+
"graph_artifacts",
|
|
324
|
+
"load_rules",
|
|
325
|
+
"load_targets",
|
|
326
|
+
"make_scheduler",
|
|
327
|
+
"pack_search_paths",
|
|
328
|
+
"pack_search_paths_for",
|
|
329
|
+
"project_graph",
|
|
330
|
+
"rules_for_packs",
|
|
331
|
+
"targets_for_packs",
|
|
332
|
+
]
|