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,283 @@
|
|
|
1
|
+
"""Planning `chipgraph baseline` (DESIGN.md 6.4): what to record for an existing project.
|
|
2
|
+
|
|
3
|
+
A project that already carries reviewed specs, RTL and contracts (merged through PRs)
|
|
4
|
+
has no chipgraph decisions yet, so the very first build would stop at every spec gate.
|
|
5
|
+
`baseline` proposes treating the artifacts on the main branch as *approved at their
|
|
6
|
+
current hash*, records one `baseline` decision per undecided gate, and notes the open
|
|
7
|
+
findings so they are recorded without blocking the build.
|
|
8
|
+
|
|
9
|
+
This module is the generic, side-effect-free half of that command: given the gates in
|
|
10
|
+
the graph, the artifacts each gate covers, and per-artifact version-control facts, it
|
|
11
|
+
produces a deterministic `BaselinePlan`. It knows nothing about git, the review store or
|
|
12
|
+
the finding store (the app layer supplies those facts and applies the plan); it only
|
|
13
|
+
depends on `chipgraph.core.contracts` and is checked under `mypy --strict`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable, Mapping
|
|
19
|
+
from typing import Literal, Protocol
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
22
|
+
|
|
23
|
+
from chipgraph.core.contracts import ArtifactRef, RuleInstance
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ArtifactStatus",
|
|
27
|
+
"BaselineArtifact",
|
|
28
|
+
"BaselineGate",
|
|
29
|
+
"BaselinePlan",
|
|
30
|
+
"CommitInfo",
|
|
31
|
+
"GateInput",
|
|
32
|
+
"VcsFacts",
|
|
33
|
+
"clean_refs_for_gate",
|
|
34
|
+
"gate_inputs_from_instances",
|
|
35
|
+
"plan_baseline",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
ArtifactStatus = Literal["clean", "dirty", "untracked", "missing"]
|
|
39
|
+
"""How an artifact stands relative to the main branch.
|
|
40
|
+
|
|
41
|
+
- ``clean``: tracked and unmodified in the working tree -> can be baselined.
|
|
42
|
+
- ``dirty``: tracked but modified in the working tree -> not baselined.
|
|
43
|
+
- ``untracked``: on disk but not tracked by version control -> not baselined.
|
|
44
|
+
- ``missing``: a declared input that does not exist -> nothing to baseline.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CommitInfo(BaseModel):
|
|
49
|
+
"""The identifying facts of a commit, as shown in a baseline listing."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
52
|
+
|
|
53
|
+
short_sha: str = Field(description="The commit's abbreviated sha.")
|
|
54
|
+
date: str = Field(description="The commit's author date, ISO-8601 (YYYY-MM-DD).")
|
|
55
|
+
subject: str = Field(description="The commit's subject line.")
|
|
56
|
+
is_merge: bool = Field(default=False, description="Whether this is a merge/PR commit.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VcsFacts(Protocol):
|
|
60
|
+
"""The version-control facts `plan_baseline` needs about one repo-relative path.
|
|
61
|
+
|
|
62
|
+
The app layer implements this over the git adapter and the working tree; the core
|
|
63
|
+
only reads it, so it never shells out or imports an adapter.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def sha256(self, path: str) -> str | None:
|
|
67
|
+
"""Current content sha256 (hex) of `path`, or `None` if it is not on disk."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def is_tracked(self, path: str) -> bool:
|
|
71
|
+
"""Whether `path` is tracked by version control on the current branch."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def is_dirty(self, path: str) -> bool:
|
|
75
|
+
"""Whether `path` is modified in the working tree relative to the current commit."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def last_commit(self, path: str) -> CommitInfo | None:
|
|
79
|
+
"""The last commit that touched `path`, or `None` if there is none."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class GateInput(BaseModel):
|
|
84
|
+
"""One gate to plan a baseline for, with the artifacts it covers.
|
|
85
|
+
|
|
86
|
+
`artifacts` are the gated instance's file-based inputs (the things a person
|
|
87
|
+
reviewed); `already_decided` says the gate already has a current decision, so the
|
|
88
|
+
baseline skips it.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
92
|
+
|
|
93
|
+
gate_id: str = Field(description="The formatted gate id, e.g. 'spec:timer'.")
|
|
94
|
+
instance_id: str = Field(description="The rule instance the gate is evaluated for.")
|
|
95
|
+
artifacts: tuple[ArtifactRef, ...] = Field(
|
|
96
|
+
default=(), description="The gate's covered artifacts (the instance's file inputs)."
|
|
97
|
+
)
|
|
98
|
+
already_decided: bool = Field(
|
|
99
|
+
default=False, description="Whether the gate already has a current decision."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class BaselineArtifact(BaseModel):
|
|
104
|
+
"""One artifact covered by a gate, with its version-control facts for the listing."""
|
|
105
|
+
|
|
106
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
107
|
+
|
|
108
|
+
path: str = Field(description="The artifact's repo-relative path.")
|
|
109
|
+
sha256: str | None = Field(
|
|
110
|
+
default=None, description="Current content sha256 (hex), or None if missing on disk."
|
|
111
|
+
)
|
|
112
|
+
status: ArtifactStatus = Field(description="'clean', 'dirty', 'untracked' or 'missing'.")
|
|
113
|
+
last_commit: CommitInfo | None = Field(
|
|
114
|
+
default=None, description="The last commit that touched this artifact, if any."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def short_sha256(self) -> str:
|
|
119
|
+
"""The first 12 chars of the content sha, or '-' when the file is missing."""
|
|
120
|
+
return self.sha256[:12] if self.sha256 else "-"
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def baselineable(self) -> bool:
|
|
124
|
+
"""Whether this artifact is on the main branch and so can be baselined."""
|
|
125
|
+
return self.status == "clean" and self.sha256 is not None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class BaselineGate(BaseModel):
|
|
129
|
+
"""A gate in the plan: its artifacts, whether it is skipped, and what will be baselined."""
|
|
130
|
+
|
|
131
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
132
|
+
|
|
133
|
+
gate_id: str = Field(description="The formatted gate id.")
|
|
134
|
+
instance_id: str = Field(description="The rule instance the gate is evaluated for.")
|
|
135
|
+
already_decided: bool = Field(description="Whether the gate already has a current decision.")
|
|
136
|
+
artifacts: tuple[BaselineArtifact, ...] = Field(
|
|
137
|
+
default=(), description="The gate's covered artifacts, sorted by path."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def clean_artifacts(self) -> tuple[BaselineArtifact, ...]:
|
|
142
|
+
"""The gate's clean, tracked artifacts: what a baseline decision would pin."""
|
|
143
|
+
return tuple(a for a in self.artifacts if a.baselineable)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def will_baseline(self) -> bool:
|
|
147
|
+
"""Whether ``--confirm`` would record a decision for this gate.
|
|
148
|
+
|
|
149
|
+
True only when the gate is undecided and has at least one clean, tracked
|
|
150
|
+
artifact to pin the decision to.
|
|
151
|
+
"""
|
|
152
|
+
return not self.already_decided and bool(self.clean_artifacts)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class BaselinePlan(BaseModel):
|
|
156
|
+
"""The full, deterministic plan a `baseline` run acts on (or just prints, when dry)."""
|
|
157
|
+
|
|
158
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
159
|
+
|
|
160
|
+
gates: tuple[BaselineGate, ...] = Field(
|
|
161
|
+
default=(), description="Every gate found, sorted by gate id then instance id."
|
|
162
|
+
)
|
|
163
|
+
open_findings: int = Field(
|
|
164
|
+
default=0, ge=0, description="Count of currently open findings, recorded at baseline."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def gates_to_baseline(self) -> tuple[BaselineGate, ...]:
|
|
169
|
+
"""The undecided gates with clean artifacts that ``--confirm`` will record."""
|
|
170
|
+
return tuple(g for g in self.gates if g.will_baseline)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def skipped_gates(self) -> tuple[BaselineGate, ...]:
|
|
174
|
+
"""The gates a baseline skips: already decided."""
|
|
175
|
+
return tuple(g for g in self.gates if g.already_decided)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def dirty_artifacts(self) -> tuple[BaselineArtifact, ...]:
|
|
179
|
+
"""Every distinct dirty/untracked artifact across all gates, sorted by path.
|
|
180
|
+
|
|
181
|
+
These are *not* baselined: a baseline is only for what is on the main branch.
|
|
182
|
+
"""
|
|
183
|
+
seen: dict[str, BaselineArtifact] = {}
|
|
184
|
+
for gate in self.gates:
|
|
185
|
+
for artifact in gate.artifacts:
|
|
186
|
+
if artifact.status in ("dirty", "untracked") and artifact.path not in seen:
|
|
187
|
+
seen[artifact.path] = artifact
|
|
188
|
+
return tuple(seen[path] for path in sorted(seen))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _file_inputs(instance: RuleInstance) -> tuple[ArtifactRef, ...]:
|
|
192
|
+
"""The instance's file-based inputs: what a gate over it covers (DESIGN.md 6.1)."""
|
|
193
|
+
return tuple(ref for ref in instance.inputs if ref.path is not None)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def gate_inputs_from_instances(
|
|
197
|
+
gated: Iterable[tuple[str, RuleInstance, bool]],
|
|
198
|
+
) -> list[GateInput]:
|
|
199
|
+
"""Build `GateInput`s from `(gate_id, instance, already_decided)` triples.
|
|
200
|
+
|
|
201
|
+
A small convenience for the app layer: each gate covers its instance's file-based
|
|
202
|
+
inputs. Instances with no file inputs still produce a `GateInput` (with no
|
|
203
|
+
artifacts), so the listing can show a gate that cannot be baselined.
|
|
204
|
+
"""
|
|
205
|
+
return [
|
|
206
|
+
GateInput(
|
|
207
|
+
gate_id=gate_id,
|
|
208
|
+
instance_id=instance.instance_id,
|
|
209
|
+
artifacts=_file_inputs(instance),
|
|
210
|
+
already_decided=already_decided,
|
|
211
|
+
)
|
|
212
|
+
for gate_id, instance, already_decided in gated
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _artifact_status(path: str, facts: VcsFacts) -> ArtifactStatus:
|
|
217
|
+
if facts.sha256(path) is None:
|
|
218
|
+
return "missing"
|
|
219
|
+
if not facts.is_tracked(path):
|
|
220
|
+
return "untracked"
|
|
221
|
+
if facts.is_dirty(path):
|
|
222
|
+
return "dirty"
|
|
223
|
+
return "clean"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def plan_baseline(
|
|
227
|
+
gates: Iterable[GateInput], facts: VcsFacts, *, open_findings: int = 0
|
|
228
|
+
) -> BaselinePlan:
|
|
229
|
+
"""Turn gate inputs and version-control facts into a deterministic `BaselinePlan`.
|
|
230
|
+
|
|
231
|
+
For every gate, each covered artifact is resolved to its current sha256, its status
|
|
232
|
+
('clean'/'dirty'/'untracked') and the last commit that touched it. Gates are sorted
|
|
233
|
+
by `(gate_id, instance_id)` and artifacts by path, so the plan (and any listing or
|
|
234
|
+
JSON built from it) is stable regardless of input order.
|
|
235
|
+
"""
|
|
236
|
+
planned: list[BaselineGate] = []
|
|
237
|
+
for gate in sorted(gates, key=lambda g: (g.gate_id, g.instance_id)):
|
|
238
|
+
artifacts = tuple(
|
|
239
|
+
_plan_artifact(ref, facts)
|
|
240
|
+
for ref in sorted(gate.artifacts, key=_ref_path)
|
|
241
|
+
if ref.path is not None
|
|
242
|
+
)
|
|
243
|
+
planned.append(
|
|
244
|
+
BaselineGate(
|
|
245
|
+
gate_id=gate.gate_id,
|
|
246
|
+
instance_id=gate.instance_id,
|
|
247
|
+
already_decided=gate.already_decided,
|
|
248
|
+
artifacts=artifacts,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
return BaselinePlan(gates=tuple(planned), open_findings=open_findings)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _ref_path(ref: ArtifactRef) -> str:
|
|
255
|
+
return ref.path or ""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _plan_artifact(ref: ArtifactRef, facts: VcsFacts) -> BaselineArtifact:
|
|
259
|
+
assert ref.path is not None # guarded by the caller
|
|
260
|
+
path = ref.path
|
|
261
|
+
status = _artifact_status(path, facts)
|
|
262
|
+
return BaselineArtifact(
|
|
263
|
+
path=path,
|
|
264
|
+
sha256=facts.sha256(path),
|
|
265
|
+
status=status,
|
|
266
|
+
last_commit=facts.last_commit(path),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def clean_refs_for_gate(
|
|
271
|
+
gate: BaselineGate, by_path: Mapping[str, ArtifactRef]
|
|
272
|
+
) -> tuple[ArtifactRef, ...]:
|
|
273
|
+
"""The `ArtifactRef`s of a gate's clean artifacts, looked up in `by_path`.
|
|
274
|
+
|
|
275
|
+
The app layer keeps the original `ArtifactRef`s (with repo and label) keyed by
|
|
276
|
+
path; this returns the ones a baseline decision should pin, in the plan's order.
|
|
277
|
+
"""
|
|
278
|
+
refs: list[ArtifactRef] = []
|
|
279
|
+
for artifact in gate.clean_artifacts:
|
|
280
|
+
ref = by_path.get(artifact.path)
|
|
281
|
+
if ref is not None:
|
|
282
|
+
refs.append(ref)
|
|
283
|
+
return tuple(refs)
|