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,901 @@
|
|
|
1
|
+
"""`PlanCheck`: a block's plan is valid before a person approves it (DESIGN.md 5.1, 5.3).
|
|
2
|
+
|
|
3
|
+
A built-in check, deterministic and fast: a pure function (`validate_plan`) over the
|
|
4
|
+
plan file (`chipgraph.packs.digital_rtl.plan.Plan`), the block's slice of the Design
|
|
5
|
+
Model, the profile and the build graph's other rules (`PlanEnv`). The Planner gets its
|
|
6
|
+
issues as the redo text of the normal agent loop. It rejects:
|
|
7
|
+
|
|
8
|
+
- write sets: two modules writing the same file (F2), a write outside the block's
|
|
9
|
+
`rtl` and `tb` layout paths (`layout.rtl`, `layout.tb`, per block under
|
|
10
|
+
`blocks.<b>.layout`; a kind with no layout key allows its default directory, `rtl/`
|
|
11
|
+
or `dv/<block>/`), a write to a path another rule produces (a file
|
|
12
|
+
a `gen`/`import`/`human` rule writes is the engine's or a person's, never a plan's),
|
|
13
|
+
an existing file the plan does not say it replaces (`replaces: true`), and a module
|
|
14
|
+
whose write set misses a file a `foreach: plan.modules` rule will write for it;
|
|
15
|
+
- dependencies: an unknown module, a module depending on itself, a cycle;
|
|
16
|
+
- requirements: an id that is not one of the block's requirements in the model, and a
|
|
17
|
+
block requirement neither assigned to a module nor listed in `unassigned_reqs`;
|
|
18
|
+
- interfaces (F1): a port that is not on the block's interface in the model, or whose
|
|
19
|
+
direction or width differs from it; `internal: true` helper ports only off the top;
|
|
20
|
+
- limits (profile `plan:`): modules, dependency depth, total tries;
|
|
21
|
+
- module names against the project's naming rule, when one is configured.
|
|
22
|
+
|
|
23
|
+
`open_questions` are a **warning**: they do not block, but the approver must read
|
|
24
|
+
them (`chipgraph plan show`).
|
|
25
|
+
|
|
26
|
+
An existing file counts as the plan's own once a chipgraph build of this block's plan
|
|
27
|
+
nodes wrote it (its hash is a production record's): re-planning does not have to mark
|
|
28
|
+
every file it wrote last time `replaces: true`.
|
|
29
|
+
|
|
30
|
+
Example profile usage (`.chipgraph.yml`):
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
adapters:
|
|
34
|
+
plan_check: { use: plan_check } # optional arg: plan: "plan/{block}.plan.yml"
|
|
35
|
+
plan: { max_modules: 12, max_depth: 4, max_total_tries: 36 }
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import re
|
|
42
|
+
import time
|
|
43
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
import yaml
|
|
49
|
+
from pydantic import ValidationError
|
|
50
|
+
|
|
51
|
+
from chipgraph.checks._common import (
|
|
52
|
+
compile_template,
|
|
53
|
+
compute_idempotency_key,
|
|
54
|
+
error_result,
|
|
55
|
+
sha256_bytes,
|
|
56
|
+
sha256_file,
|
|
57
|
+
)
|
|
58
|
+
from chipgraph.checks._naming_pyslang import Decl
|
|
59
|
+
from chipgraph.checks.naming import (
|
|
60
|
+
_check_decl,
|
|
61
|
+
_CompiledRules,
|
|
62
|
+
_load_rules,
|
|
63
|
+
_resolve_rules_path,
|
|
64
|
+
_RulesError,
|
|
65
|
+
)
|
|
66
|
+
from chipgraph.core.config.loader import ResolvedProfile
|
|
67
|
+
from chipgraph.core.config.loader import load as load_profile
|
|
68
|
+
from chipgraph.core.config.models import PlanCfg, Profile
|
|
69
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue, RuleSpec
|
|
70
|
+
from chipgraph.core.contracts.types import CheckStatus
|
|
71
|
+
from chipgraph.core.engine.graph import ForeachEntry, GraphError, build_graph
|
|
72
|
+
from chipgraph.core.engine.records import RecordStore
|
|
73
|
+
from chipgraph.core.model.model import DesignModel
|
|
74
|
+
from chipgraph.core.model.store import ModelStore, default_model_db_path
|
|
75
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
76
|
+
from chipgraph.core.state.layout import StateLayout
|
|
77
|
+
from chipgraph.packs.digital_rtl.layout import LayoutError, fill_vars, layout_vars, write_globs
|
|
78
|
+
from chipgraph.packs.digital_rtl.plan.model import Plan, PlanModule
|
|
79
|
+
|
|
80
|
+
DEFAULT_PLAN_PATH = "plan/{block}.plan.yml"
|
|
81
|
+
"""Where a block's plan lives (the output of rule `digital-rtl/plan`)."""
|
|
82
|
+
|
|
83
|
+
PLAN_MODULES = "plan.modules"
|
|
84
|
+
"""The `foreach` selector that expands to one instance per module of an approved plan."""
|
|
85
|
+
|
|
86
|
+
_PLACEHOLDER_RE = re.compile(r"\{([^{}]+)\}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PlanEnvError(Exception):
|
|
90
|
+
"""There is nothing to check a plan against: no profile, or no Design Model."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- the environment a plan is checked against ----------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ReqInfo:
|
|
98
|
+
"""One requirement of the Design Model."""
|
|
99
|
+
|
|
100
|
+
key: str
|
|
101
|
+
name: str
|
|
102
|
+
text: str
|
|
103
|
+
block: str | None
|
|
104
|
+
"""The owning block's name, if the model says."""
|
|
105
|
+
declared: bool = True
|
|
106
|
+
"""The spec declares its id; an inferred one (D37) has only its model key."""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def ref(self) -> str:
|
|
110
|
+
"""How a plan names it: the declared REQ id, else the inferred key (`timer.h1a2b`)."""
|
|
111
|
+
return self.name if self.declared else self.key.removeprefix("requirement:")
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def refs(self) -> frozenset[str]:
|
|
115
|
+
"""The strings a plan may use for it: its id, its key, the key without the kind."""
|
|
116
|
+
refs = {self.key, self.key.removeprefix("requirement:")}
|
|
117
|
+
if self.declared:
|
|
118
|
+
refs.add(self.name)
|
|
119
|
+
return frozenset(refs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class PortInfo:
|
|
124
|
+
"""One port: name, direction and width as the Design Model knows them."""
|
|
125
|
+
|
|
126
|
+
name: str
|
|
127
|
+
direction: str | None
|
|
128
|
+
width: int | str | None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class ModuleInfo:
|
|
133
|
+
"""An existing module of the block in the Design Model."""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
file: str | None
|
|
137
|
+
ports: tuple[PortInfo, ...]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class PlanEnv:
|
|
142
|
+
"""Everything `validate_plan` checks a plan against, for one block."""
|
|
143
|
+
|
|
144
|
+
block: str
|
|
145
|
+
requirements: tuple[ReqInfo, ...] = ()
|
|
146
|
+
"""The block's requirements."""
|
|
147
|
+
other_requirements: tuple[ReqInfo, ...] = ()
|
|
148
|
+
"""Every other requirement of the model (so a wrong block is named as such)."""
|
|
149
|
+
interface: tuple[PortInfo, ...] = ()
|
|
150
|
+
"""The block's interface ports, from the spec (else the top RTL module)."""
|
|
151
|
+
interface_source: str = "none"
|
|
152
|
+
"""Where `interface` came from: `spec`, `rtl:<module>` or `none`."""
|
|
153
|
+
existing_modules: tuple[ModuleInfo, ...] = ()
|
|
154
|
+
layout: tuple[str, ...] = ()
|
|
155
|
+
"""Where the block's modules may write: its `rtl` and `tb` layout templates,
|
|
156
|
+
`{block}`/`{BLOCK}` filled in (`chipgraph.packs.digital_rtl.layout.write_globs`)."""
|
|
157
|
+
engine_outputs: Mapping[str, str] = field(default_factory=dict)
|
|
158
|
+
"""Path -> instance id, for every output of a non-agent rule."""
|
|
159
|
+
agent_outputs: Mapping[str, str] = field(default_factory=dict)
|
|
160
|
+
"""Path -> instance id, for every output of an agent rule not expanded from a plan."""
|
|
161
|
+
module_outputs: tuple[tuple[str, str], ...] = ()
|
|
162
|
+
"""(rule id, output template) of every rule with `foreach: plan.modules`, the
|
|
163
|
+
block's layout directories (`{layout.rtl}`, `{layout.tb}`) filled in."""
|
|
164
|
+
existing: Mapping[str, str] = field(default_factory=dict)
|
|
165
|
+
"""Path -> content hash, for the plan's write paths that exist on disk."""
|
|
166
|
+
built: Mapping[str, frozenset[str]] = field(default_factory=dict)
|
|
167
|
+
"""Path -> hashes a build of this block's plan nodes recorded for it."""
|
|
168
|
+
limits: PlanCfg = field(default_factory=PlanCfg)
|
|
169
|
+
naming: _CompiledRules | None = None
|
|
170
|
+
naming_ref: str | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --- validation (pure) ----------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _source_lines(source: str | None) -> tuple[dict[str, int], dict[int, int]]:
|
|
177
|
+
"""Line numbers of the plan's top-level keys and of each `modules[i]` item."""
|
|
178
|
+
if not source:
|
|
179
|
+
return {}, {}
|
|
180
|
+
try:
|
|
181
|
+
node = yaml.compose(source)
|
|
182
|
+
except yaml.YAMLError:
|
|
183
|
+
return {}, {}
|
|
184
|
+
keys: dict[str, int] = {}
|
|
185
|
+
modules: dict[int, int] = {}
|
|
186
|
+
if not isinstance(node, yaml.MappingNode):
|
|
187
|
+
return keys, modules
|
|
188
|
+
for key_node, value_node in node.value:
|
|
189
|
+
name = str(key_node.value)
|
|
190
|
+
keys[name] = key_node.start_mark.line + 1
|
|
191
|
+
if name == "modules" and isinstance(value_node, yaml.SequenceNode):
|
|
192
|
+
for index, item in enumerate(value_node.value):
|
|
193
|
+
modules[index] = item.start_mark.line + 1
|
|
194
|
+
return keys, modules
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _fill(template: str, params: Mapping[str, str]) -> str | None:
|
|
198
|
+
"""`template` with `{key}` from `params` (`{KEY}` upper-cased); `None` if one is unknown."""
|
|
199
|
+
missing = False
|
|
200
|
+
|
|
201
|
+
def _replace(match: re.Match[str]) -> str:
|
|
202
|
+
nonlocal missing
|
|
203
|
+
key = match.group(1)
|
|
204
|
+
if key in params:
|
|
205
|
+
return params[key]
|
|
206
|
+
if key.isupper() and key.lower() in params:
|
|
207
|
+
return params[key.lower()].upper()
|
|
208
|
+
missing = True
|
|
209
|
+
return match.group(0)
|
|
210
|
+
|
|
211
|
+
filled = _PLACEHOLDER_RE.sub(_replace, template)
|
|
212
|
+
return None if missing else filled
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def dependency_depth(plan: Plan) -> int:
|
|
216
|
+
"""The longest chain of dependencies, counted in modules (no cycle assumed)."""
|
|
217
|
+
deps = {m.name: [d for d in m.depends_on if d != m.name] for m in plan.modules}
|
|
218
|
+
memo: dict[str, int] = {}
|
|
219
|
+
|
|
220
|
+
def depth(name: str, seen: frozenset[str]) -> int:
|
|
221
|
+
if name in memo:
|
|
222
|
+
return memo[name]
|
|
223
|
+
children = [d for d in deps.get(name, []) if d in deps and d not in seen]
|
|
224
|
+
value = 1 + max((depth(d, seen | {d}) for d in children), default=0)
|
|
225
|
+
memo[name] = value
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
return max((depth(m.name, frozenset({m.name})) for m in plan.modules), default=0)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _find_cycle(plan: Plan) -> list[str] | None:
|
|
232
|
+
"""One dependency cycle among known modules (as a path ending where it starts), or None."""
|
|
233
|
+
deps = {m.name: [d for d in m.depends_on if d != m.name] for m in plan.modules}
|
|
234
|
+
white, gray, black = 0, 1, 2
|
|
235
|
+
color = dict.fromkeys(deps, white)
|
|
236
|
+
stack: list[str] = []
|
|
237
|
+
|
|
238
|
+
def visit(name: str) -> list[str] | None:
|
|
239
|
+
color[name] = gray
|
|
240
|
+
stack.append(name)
|
|
241
|
+
for dep in deps[name]:
|
|
242
|
+
if dep not in deps:
|
|
243
|
+
continue
|
|
244
|
+
if color[dep] == gray:
|
|
245
|
+
return [*stack[stack.index(dep) :], dep]
|
|
246
|
+
if color[dep] == white:
|
|
247
|
+
found = visit(dep)
|
|
248
|
+
if found is not None:
|
|
249
|
+
return found
|
|
250
|
+
stack.pop()
|
|
251
|
+
color[name] = black
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
for name in deps:
|
|
255
|
+
if color[name] == white:
|
|
256
|
+
found = visit(name)
|
|
257
|
+
if found is not None:
|
|
258
|
+
return found
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def topo_order(plan: Plan) -> list[str]:
|
|
263
|
+
"""The modules in build order: dependencies first, ties by plan order (no cycle)."""
|
|
264
|
+
order: list[str] = []
|
|
265
|
+
done: set[str] = set()
|
|
266
|
+
pending = [m.name for m in plan.modules]
|
|
267
|
+
known = set(pending)
|
|
268
|
+
while pending:
|
|
269
|
+
progressed = False
|
|
270
|
+
for name in list(pending):
|
|
271
|
+
module = plan.module(name)
|
|
272
|
+
assert module is not None
|
|
273
|
+
if all(d in done or d not in known or d == name for d in module.depends_on):
|
|
274
|
+
order.append(name)
|
|
275
|
+
done.add(name)
|
|
276
|
+
pending.remove(name)
|
|
277
|
+
progressed = True
|
|
278
|
+
if not progressed: # a cycle: keep plan order for the rest
|
|
279
|
+
order.extend(pending)
|
|
280
|
+
break
|
|
281
|
+
return order
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def validate_plan(
|
|
285
|
+
plan: Plan,
|
|
286
|
+
env: PlanEnv,
|
|
287
|
+
*,
|
|
288
|
+
plan_path: str,
|
|
289
|
+
source: str | None = None,
|
|
290
|
+
check_existing: bool = True,
|
|
291
|
+
) -> list[Issue]:
|
|
292
|
+
"""Every problem with `plan` for `env.block`, as `Issue`s on `plan_path`, sorted by line.
|
|
293
|
+
|
|
294
|
+
Pure: reads nothing but its arguments. `source` (the file's text) only gives the
|
|
295
|
+
issues line numbers. With `check_existing=False` the existing-file rule is skipped:
|
|
296
|
+
once a person approved the plan, the files it writes exist by design.
|
|
297
|
+
"""
|
|
298
|
+
key_lines, module_lines = _source_lines(source)
|
|
299
|
+
issues: list[Issue] = []
|
|
300
|
+
|
|
301
|
+
def add(rule: str, msg: str, *, line: int | None = None, severity: str = "error") -> None:
|
|
302
|
+
issues.append(
|
|
303
|
+
Issue(file=plan_path, line=line, rule=rule, severity=severity, msg=msg) # type: ignore[arg-type]
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
index_of = {id(m): i for i, m in enumerate(plan.modules)}
|
|
307
|
+
|
|
308
|
+
def mline(module: PlanModule) -> int | None:
|
|
309
|
+
return module_lines.get(index_of[id(module)])
|
|
310
|
+
|
|
311
|
+
# --- structure
|
|
312
|
+
if plan.block != env.block:
|
|
313
|
+
add(
|
|
314
|
+
"plan.block",
|
|
315
|
+
f"the plan is for block {plan.block!r}, but this is block {env.block!r}",
|
|
316
|
+
line=key_lines.get("block"),
|
|
317
|
+
)
|
|
318
|
+
names: dict[str, PlanModule] = {}
|
|
319
|
+
for module in plan.modules:
|
|
320
|
+
if module.name in names:
|
|
321
|
+
add(
|
|
322
|
+
"plan.duplicate_module",
|
|
323
|
+
f"module {module.name!r} is listed twice",
|
|
324
|
+
line=mline(module),
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
names[module.name] = module
|
|
328
|
+
top = names.get(plan.top)
|
|
329
|
+
if top is None:
|
|
330
|
+
add(
|
|
331
|
+
"plan.top",
|
|
332
|
+
f"top {plan.top!r} is not one of the plan's modules: {', '.join(names)}",
|
|
333
|
+
line=key_lines.get("top"),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# --- limits
|
|
337
|
+
limits = env.limits
|
|
338
|
+
if len(plan.modules) > limits.max_modules:
|
|
339
|
+
add(
|
|
340
|
+
"plan.limit.modules",
|
|
341
|
+
f"the plan has {len(plan.modules)} modules; the limit is {limits.max_modules} "
|
|
342
|
+
"(profile plan.max_modules)",
|
|
343
|
+
line=key_lines.get("modules"),
|
|
344
|
+
)
|
|
345
|
+
total_tries = sum(m.budget.tries for m in plan.modules)
|
|
346
|
+
if total_tries > limits.max_total_tries:
|
|
347
|
+
add(
|
|
348
|
+
"plan.limit.tries",
|
|
349
|
+
f"the modules' budgets add up to {total_tries} tries; the limit is "
|
|
350
|
+
f"{limits.max_total_tries} (profile plan.max_total_tries)",
|
|
351
|
+
line=key_lines.get("modules"),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# --- dependencies
|
|
355
|
+
for module in plan.modules:
|
|
356
|
+
for dep in module.depends_on:
|
|
357
|
+
if dep == module.name:
|
|
358
|
+
add(
|
|
359
|
+
"plan.self_dependency",
|
|
360
|
+
f"module {module.name!r} depends on itself",
|
|
361
|
+
line=mline(module),
|
|
362
|
+
)
|
|
363
|
+
elif dep not in names:
|
|
364
|
+
add(
|
|
365
|
+
"plan.unknown_dependency",
|
|
366
|
+
f"module {module.name!r} depends on {dep!r}, which is not a module of "
|
|
367
|
+
f"this plan ({', '.join(names)})",
|
|
368
|
+
line=mline(module),
|
|
369
|
+
)
|
|
370
|
+
cycle = _find_cycle(plan)
|
|
371
|
+
if cycle is not None:
|
|
372
|
+
add(
|
|
373
|
+
"plan.dependency_cycle",
|
|
374
|
+
f"dependency cycle: {' -> '.join(cycle)}",
|
|
375
|
+
line=key_lines.get("modules"),
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
depth = dependency_depth(plan)
|
|
379
|
+
if depth > limits.max_depth:
|
|
380
|
+
add(
|
|
381
|
+
"plan.limit.depth",
|
|
382
|
+
f"the longest dependency chain is {depth} modules; the limit is "
|
|
383
|
+
f"{limits.max_depth} (profile plan.max_depth)",
|
|
384
|
+
line=key_lines.get("modules"),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# --- requirements
|
|
388
|
+
block_reqs = {ref: req for req in env.requirements for ref in req.refs}
|
|
389
|
+
other_reqs = {ref: req for req in env.other_requirements for ref in req.refs}
|
|
390
|
+
|
|
391
|
+
def known_req(ref: str, where: str, line: int | None) -> ReqInfo | None:
|
|
392
|
+
req = block_reqs.get(ref)
|
|
393
|
+
if req is not None:
|
|
394
|
+
return req
|
|
395
|
+
other = other_reqs.get(ref)
|
|
396
|
+
if other is not None:
|
|
397
|
+
add(
|
|
398
|
+
"plan.unknown_req",
|
|
399
|
+
f"{where}: {ref!r} is a requirement of block {other.block!r}, not of {env.block!r}",
|
|
400
|
+
line=line,
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
add(
|
|
404
|
+
"plan.unknown_req",
|
|
405
|
+
f"{where}: {ref!r} is not a requirement in the Design Model; do not invent "
|
|
406
|
+
"requirements",
|
|
407
|
+
line=line,
|
|
408
|
+
)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
covered: set[str] = set()
|
|
412
|
+
for module in plan.modules:
|
|
413
|
+
for ref in module.reqs:
|
|
414
|
+
req = known_req(ref, f"module {module.name!r}", mline(module))
|
|
415
|
+
if req is not None:
|
|
416
|
+
covered.add(req.key)
|
|
417
|
+
for item in plan.unassigned_reqs:
|
|
418
|
+
req = known_req(item.req, "unassigned_reqs", key_lines.get("unassigned_reqs"))
|
|
419
|
+
if req is not None:
|
|
420
|
+
covered.add(req.key)
|
|
421
|
+
for req in env.requirements:
|
|
422
|
+
if req.key not in covered:
|
|
423
|
+
add(
|
|
424
|
+
"plan.unassigned_req",
|
|
425
|
+
f"requirement {req.ref} is neither assigned to a module nor listed in "
|
|
426
|
+
"unassigned_reqs with a reason",
|
|
427
|
+
line=key_lines.get("unassigned_reqs") or key_lines.get("modules"),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# --- interfaces
|
|
431
|
+
interface = {p.name: p for p in env.interface}
|
|
432
|
+
for module in plan.modules:
|
|
433
|
+
seen_ports: set[str] = set()
|
|
434
|
+
for port in module.interface.ports:
|
|
435
|
+
where = f"module {module.name!r} port {port.name!r}"
|
|
436
|
+
if port.name in seen_ports:
|
|
437
|
+
add("plan.duplicate_port", f"{where} is listed twice", line=mline(module))
|
|
438
|
+
continue
|
|
439
|
+
seen_ports.add(port.name)
|
|
440
|
+
if port.internal:
|
|
441
|
+
if module.name == plan.top:
|
|
442
|
+
add(
|
|
443
|
+
"plan.internal_port_on_top",
|
|
444
|
+
f"{where} is marked internal, but {module.name!r} is the block's "
|
|
445
|
+
"top: its ports are the block's interface",
|
|
446
|
+
line=mline(module),
|
|
447
|
+
)
|
|
448
|
+
continue
|
|
449
|
+
known = interface.get(port.name)
|
|
450
|
+
if known is None:
|
|
451
|
+
listed = ", ".join(interface) or "none (run `chipgraph ingest`)"
|
|
452
|
+
add(
|
|
453
|
+
"plan.unknown_port",
|
|
454
|
+
f"{where} is not on block {env.block!r}'s interface in the Design Model "
|
|
455
|
+
f"({listed}); take ports from the model, or mark a helper port between "
|
|
456
|
+
"modules `internal: true`",
|
|
457
|
+
line=mline(module),
|
|
458
|
+
)
|
|
459
|
+
continue
|
|
460
|
+
if known.direction is not None and known.direction != port.direction:
|
|
461
|
+
add(
|
|
462
|
+
"plan.port_mismatch",
|
|
463
|
+
f"{where} is {port.direction}, but the Design Model has {known.direction}",
|
|
464
|
+
line=mline(module),
|
|
465
|
+
)
|
|
466
|
+
if known.width is not None and str(known.width) != str(port.width):
|
|
467
|
+
add(
|
|
468
|
+
"plan.port_mismatch",
|
|
469
|
+
f"{where} has width {port.width}, but the Design Model has {known.width}",
|
|
470
|
+
line=mline(module),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# --- write sets
|
|
474
|
+
layout = [compile_template(t) for t in env.layout]
|
|
475
|
+
writers: dict[str, str] = {}
|
|
476
|
+
for module in plan.modules:
|
|
477
|
+
for write in module.writes:
|
|
478
|
+
path = write.path
|
|
479
|
+
where = f"module {module.name!r} writes {path}"
|
|
480
|
+
other = writers.get(path)
|
|
481
|
+
if other is not None and other != module.name:
|
|
482
|
+
add(
|
|
483
|
+
"plan.write_overlap",
|
|
484
|
+
f"modules {other!r} and {module.name!r} both write {path}; the write "
|
|
485
|
+
"sets of modules must not overlap (F2)",
|
|
486
|
+
line=mline(module),
|
|
487
|
+
)
|
|
488
|
+
writers.setdefault(path, module.name)
|
|
489
|
+
if not any(t.match(path) is not None for t in layout):
|
|
490
|
+
listed = ", ".join(env.layout) or "none"
|
|
491
|
+
add(
|
|
492
|
+
"plan.write_layout",
|
|
493
|
+
f"{where}, outside block {env.block!r}'s layout paths ({listed})",
|
|
494
|
+
line=mline(module),
|
|
495
|
+
)
|
|
496
|
+
if path in env.engine_outputs:
|
|
497
|
+
add(
|
|
498
|
+
"plan.write_engine",
|
|
499
|
+
f"{where}, which rule instance {env.engine_outputs[path]!r} produces; "
|
|
500
|
+
"a file a gen, import or human rule writes is never a plan's (F2)",
|
|
501
|
+
line=mline(module),
|
|
502
|
+
)
|
|
503
|
+
elif path in env.agent_outputs:
|
|
504
|
+
add(
|
|
505
|
+
"plan.write_taken",
|
|
506
|
+
f"{where}, which rule instance {env.agent_outputs[path]!r} produces",
|
|
507
|
+
line=mline(module),
|
|
508
|
+
)
|
|
509
|
+
elif (
|
|
510
|
+
check_existing
|
|
511
|
+
and path in env.existing
|
|
512
|
+
and not write.replaces
|
|
513
|
+
and env.existing[path] not in env.built.get(path, frozenset())
|
|
514
|
+
):
|
|
515
|
+
add(
|
|
516
|
+
"plan.write_existing",
|
|
517
|
+
f"{where}, which already exists; set `replaces: true` on that write "
|
|
518
|
+
"to replace it on purpose",
|
|
519
|
+
line=mline(module),
|
|
520
|
+
)
|
|
521
|
+
for rule_id, template in env.module_outputs:
|
|
522
|
+
path_or_none = _fill(template, {"block": env.block, "module": module.name})
|
|
523
|
+
if path_or_none is not None and path_or_none not in module.write_paths:
|
|
524
|
+
add(
|
|
525
|
+
"plan.write_missing",
|
|
526
|
+
f"module {module.name!r}: rule {rule_id!r} writes {path_or_none} for it, "
|
|
527
|
+
"which is not in its writes",
|
|
528
|
+
line=mline(module),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# --- naming
|
|
532
|
+
if env.naming is not None:
|
|
533
|
+
for module in plan.modules:
|
|
534
|
+
line = mline(module) or 1
|
|
535
|
+
for issue in _check_decl(plan_path, Decl("module", module.name, line), env.naming):
|
|
536
|
+
add("plan.naming", f"module name {issue.msg} ({issue.rule})", line=line)
|
|
537
|
+
|
|
538
|
+
# --- what the approver must read
|
|
539
|
+
for question in plan.open_questions:
|
|
540
|
+
add(
|
|
541
|
+
"plan.open_questions",
|
|
542
|
+
f"open question for the approver: {question}",
|
|
543
|
+
line=key_lines.get("open_questions"),
|
|
544
|
+
severity="warning",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
issues.sort(key=lambda i: (i.line or 0, i.rule, i.msg))
|
|
548
|
+
return issues
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# --- reading a plan file --------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def parse_plan(text: str, *, plan_path: str) -> tuple[Plan | None, list[Issue]]:
|
|
555
|
+
"""Parse a plan file's text: the plan, or `None` and the schema issues."""
|
|
556
|
+
try:
|
|
557
|
+
data = yaml.safe_load(text)
|
|
558
|
+
except yaml.YAMLError as exc:
|
|
559
|
+
return None, [Issue(file=plan_path, rule="plan.schema", msg=f"invalid YAML: {exc}")]
|
|
560
|
+
try:
|
|
561
|
+
return Plan.model_validate(data), []
|
|
562
|
+
except ValidationError as exc:
|
|
563
|
+
issues = [
|
|
564
|
+
Issue(
|
|
565
|
+
file=plan_path,
|
|
566
|
+
rule="plan.schema",
|
|
567
|
+
msg=f"{'.'.join(str(p) for p in err['loc']) or '<file>'}: {err['msg']}",
|
|
568
|
+
)
|
|
569
|
+
for err in exc.errors()
|
|
570
|
+
]
|
|
571
|
+
return None, issues
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# --- the environment, from a project --------------------------------------------------
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
_PER_BLOCK = ("blocks", "plan.block", "plan.blocks")
|
|
578
|
+
"""Selectors with one entry per block (at most): every block, for the outputs they take."""
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class _BlocksOnly:
|
|
582
|
+
"""Expands the per-block selectors to every block (with its layout directories as
|
|
583
|
+
template values); any other selector (a plan's modules) to nothing."""
|
|
584
|
+
|
|
585
|
+
def __init__(self, blocks: Iterable[str], values: Mapping[str, Mapping[str, str]]) -> None:
|
|
586
|
+
self._blocks = sorted(blocks)
|
|
587
|
+
self._values = values
|
|
588
|
+
|
|
589
|
+
def expand(self, expr: str) -> list[dict[str, str]]:
|
|
590
|
+
return [dict(e.params) for e in self.expand_entries(expr)]
|
|
591
|
+
|
|
592
|
+
def expand_entries(self, expr: str) -> list[ForeachEntry]:
|
|
593
|
+
if expr not in _PER_BLOCK:
|
|
594
|
+
return []
|
|
595
|
+
return [
|
|
596
|
+
ForeachEntry(params={"block": b}, vars=dict(self._values.get(b, {})))
|
|
597
|
+
for b in self._blocks
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _block_vars(resolved: ResolvedProfile) -> dict[str, dict[str, str]]:
|
|
602
|
+
"""Each block's layout directories; a block whose layout is malformed gets none."""
|
|
603
|
+
out: dict[str, dict[str, str]] = {}
|
|
604
|
+
for block in resolved.profile.blocks:
|
|
605
|
+
try:
|
|
606
|
+
out[block] = layout_vars(resolved, block)
|
|
607
|
+
except LayoutError:
|
|
608
|
+
continue
|
|
609
|
+
return out
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def project_rules(root: Path, resolved: ResolvedProfile) -> list[RuleSpec]:
|
|
613
|
+
"""The rules of the profile's packs (the same as `chipgraph build` loads)."""
|
|
614
|
+
from chipgraph.app.build import rules_for_packs # lazy: app imports this module's users
|
|
615
|
+
|
|
616
|
+
return rules_for_packs(root, resolved.profile.packs)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _other_outputs(
|
|
620
|
+
rules: Sequence[RuleSpec], resolved: ResolvedProfile
|
|
621
|
+
) -> tuple[dict[str, str], dict[str, str]]:
|
|
622
|
+
"""Outputs of the graph's rules that are not expanded from a plan: non-agent, agent."""
|
|
623
|
+
fixed = [r for r in rules if r.foreach != PLAN_MODULES]
|
|
624
|
+
try:
|
|
625
|
+
graph = build_graph(fixed, _BlocksOnly(resolved.profile.blocks, _block_vars(resolved)))
|
|
626
|
+
except GraphError:
|
|
627
|
+
return {}, {}
|
|
628
|
+
engine: dict[str, str] = {}
|
|
629
|
+
agent: dict[str, str] = {}
|
|
630
|
+
for iid, instance in graph.instances.items():
|
|
631
|
+
target = agent if graph.rules[instance.rule_id].kind == "agent" else engine
|
|
632
|
+
for ref in instance.outputs:
|
|
633
|
+
if ref.path is not None:
|
|
634
|
+
target[ref.path] = iid
|
|
635
|
+
return engine, agent
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _instance_params(instance_id: str) -> tuple[str, dict[str, str]]:
|
|
639
|
+
rule_id, _, rest = instance_id.partition("[")
|
|
640
|
+
params: dict[str, str] = {}
|
|
641
|
+
for pair in rest.removesuffix("]").split(","):
|
|
642
|
+
key, sep, value = pair.partition("=")
|
|
643
|
+
if sep:
|
|
644
|
+
params[key] = value
|
|
645
|
+
return rule_id, params
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _built(root: Path, rules: Sequence[RuleSpec], block: str) -> dict[str, frozenset[str]]:
|
|
649
|
+
"""Path -> hashes recorded when this block's plan nodes last produced it."""
|
|
650
|
+
plan_rules = {r.id for r in rules if r.foreach == PLAN_MODULES}
|
|
651
|
+
found: dict[str, set[str]] = {}
|
|
652
|
+
for iid, record in RecordStore(StateLayout(root)).all().items():
|
|
653
|
+
rule_id, params = _instance_params(iid)
|
|
654
|
+
if rule_id not in plan_rules or params.get("block") != block:
|
|
655
|
+
continue
|
|
656
|
+
for key, digest in record.output_hashes.items():
|
|
657
|
+
found.setdefault(key.partition(":")[2], set()).add(digest)
|
|
658
|
+
return {path: frozenset(hashes) for path, hashes in found.items()}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _req(entity: Any) -> ReqInfo:
|
|
662
|
+
block = entity.attrs.get("block")
|
|
663
|
+
return ReqInfo(
|
|
664
|
+
key=entity.key,
|
|
665
|
+
name=str(entity.name),
|
|
666
|
+
text=str(getattr(entity, "text", None) or ""),
|
|
667
|
+
block=block.removeprefix("block:") if isinstance(block, str) else None,
|
|
668
|
+
declared=entity.attrs.get("id_source", "declared") == "declared",
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _port(entity: Any) -> PortInfo:
|
|
673
|
+
return PortInfo(name=str(entity.name), direction=entity.direction, width=entity.width)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _interface(
|
|
677
|
+
model: DesignModel, block: str, profile: Profile, modules: Sequence[ModuleInfo]
|
|
678
|
+
) -> tuple[tuple[PortInfo, ...], str]:
|
|
679
|
+
"""The block's interface: its spec ports, else its top RTL module's (ports_diff `top`)."""
|
|
680
|
+
key = f"block:{block}"
|
|
681
|
+
spec = [
|
|
682
|
+
_port(p)
|
|
683
|
+
for p in model.by_kind("port")
|
|
684
|
+
if p.attrs.get("block") == key and p.attrs.get("origin") == "spec"
|
|
685
|
+
]
|
|
686
|
+
if spec:
|
|
687
|
+
return tuple(spec), "spec"
|
|
688
|
+
top: str | None = None
|
|
689
|
+
cfg = profile.adapters.get("ports_diff")
|
|
690
|
+
raw_top = cfg.model_dump().get("top") if cfg is not None else None
|
|
691
|
+
if isinstance(raw_top, str):
|
|
692
|
+
top = _fill(raw_top, {"block": block})
|
|
693
|
+
if top is None and len(modules) == 1:
|
|
694
|
+
top = modules[0].name
|
|
695
|
+
for module in modules:
|
|
696
|
+
if module.name == top:
|
|
697
|
+
return module.ports, f"rtl:{module.name}"
|
|
698
|
+
return (), "none"
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _naming(profile: Profile, root: Path) -> tuple[_CompiledRules | None, str | None]:
|
|
702
|
+
ref = profile.naming.rules
|
|
703
|
+
if ref is None:
|
|
704
|
+
cfg = profile.adapters.get("naming")
|
|
705
|
+
dumped = cfg.model_dump() if cfg is not None else {}
|
|
706
|
+
if dumped.get("use") == "naming" and isinstance(dumped.get("rules"), str):
|
|
707
|
+
ref = dumped["rules"]
|
|
708
|
+
if ref is None:
|
|
709
|
+
return None, None
|
|
710
|
+
try:
|
|
711
|
+
rules, _ = _load_rules(_resolve_rules_path(ref, root))
|
|
712
|
+
except _RulesError as exc:
|
|
713
|
+
raise PlanEnvError(f"naming rules {ref!r}: {exc}") from exc
|
|
714
|
+
return _CompiledRules(rules), ref
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def load_env(
|
|
718
|
+
root: Path,
|
|
719
|
+
block: str,
|
|
720
|
+
*,
|
|
721
|
+
resolved: ResolvedProfile | None = None,
|
|
722
|
+
rules: Sequence[RuleSpec] | None = None,
|
|
723
|
+
write_paths: Iterable[str] = (),
|
|
724
|
+
) -> PlanEnv:
|
|
725
|
+
"""The `PlanEnv` of `block` in the project at `root`.
|
|
726
|
+
|
|
727
|
+
`resolved` and `rules` default to the project's profile and its packs' rules.
|
|
728
|
+
`write_paths` are the plan's writes, whose current hashes are looked up. Raises
|
|
729
|
+
`PlanEnvError` when there is no profile or no Design Model (run `chipgraph ingest`).
|
|
730
|
+
"""
|
|
731
|
+
if resolved is None:
|
|
732
|
+
resolved = load_profile(root)
|
|
733
|
+
if resolved is None:
|
|
734
|
+
raise PlanEnvError(f"no .chipgraph.yml found from {root}")
|
|
735
|
+
profile = resolved.for_block(block)
|
|
736
|
+
db = default_model_db_path(root)
|
|
737
|
+
if not db.is_file():
|
|
738
|
+
raise PlanEnvError(f"no Design Model at {db}; run `chipgraph ingest` first")
|
|
739
|
+
model = ModelStore(db).read()
|
|
740
|
+
if rules is None:
|
|
741
|
+
rules = project_rules(root, resolved)
|
|
742
|
+
|
|
743
|
+
key = f"block:{block}"
|
|
744
|
+
reqs = [_req(e) for e in model.by_kind("requirement")]
|
|
745
|
+
modules = tuple(
|
|
746
|
+
ModuleInfo(
|
|
747
|
+
name=str(m.name),
|
|
748
|
+
file=getattr(m, "file", None),
|
|
749
|
+
ports=tuple(_port(p) for p in model.by_kind("port") if p.module == m.key), # type: ignore[attr-defined]
|
|
750
|
+
)
|
|
751
|
+
for m in model.by_kind("module")
|
|
752
|
+
if getattr(m, "block", None) == key
|
|
753
|
+
)
|
|
754
|
+
interface, source = _interface(model, block, profile, modules)
|
|
755
|
+
try:
|
|
756
|
+
values = layout_vars(resolved, block)
|
|
757
|
+
except LayoutError as exc:
|
|
758
|
+
raise PlanEnvError(str(exc)) from exc
|
|
759
|
+
layout = write_globs(resolved, block)
|
|
760
|
+
engine, agent = _other_outputs(rules, resolved)
|
|
761
|
+
existing = {
|
|
762
|
+
path: sha256_file(root / path)
|
|
763
|
+
for path in dict.fromkeys(write_paths)
|
|
764
|
+
if (root / path).is_file()
|
|
765
|
+
}
|
|
766
|
+
naming, naming_ref = _naming(profile, root)
|
|
767
|
+
return PlanEnv(
|
|
768
|
+
block=block,
|
|
769
|
+
requirements=tuple(r for r in reqs if r.block == block),
|
|
770
|
+
other_requirements=tuple(r for r in reqs if r.block != block),
|
|
771
|
+
interface=interface,
|
|
772
|
+
interface_source=source,
|
|
773
|
+
existing_modules=modules,
|
|
774
|
+
layout=tuple(dict.fromkeys(layout)),
|
|
775
|
+
engine_outputs=engine,
|
|
776
|
+
agent_outputs=agent,
|
|
777
|
+
module_outputs=tuple(
|
|
778
|
+
(r.id, fill_vars(t, values))
|
|
779
|
+
for r in rules
|
|
780
|
+
if r.foreach == PLAN_MODULES
|
|
781
|
+
for t in r.outputs
|
|
782
|
+
),
|
|
783
|
+
existing=existing,
|
|
784
|
+
built=_built(root, rules, block),
|
|
785
|
+
limits=resolved.profile.plan,
|
|
786
|
+
naming=naming,
|
|
787
|
+
naming_ref=naming_ref,
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
@dataclass(frozen=True)
|
|
792
|
+
class PlanReport:
|
|
793
|
+
"""A block's plan file, checked: the plan (when it parses) and every issue."""
|
|
794
|
+
|
|
795
|
+
block: str
|
|
796
|
+
path: str
|
|
797
|
+
exists: bool
|
|
798
|
+
plan: Plan | None
|
|
799
|
+
issues: tuple[Issue, ...]
|
|
800
|
+
sha256: str | None
|
|
801
|
+
env: PlanEnv | None
|
|
802
|
+
|
|
803
|
+
@property
|
|
804
|
+
def ok(self) -> bool:
|
|
805
|
+
"""The plan exists, parses and has no error issue."""
|
|
806
|
+
return self.plan is not None and not any(i.severity == "error" for i in self.issues)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def check_plan_file(
|
|
810
|
+
root: Path,
|
|
811
|
+
block: str,
|
|
812
|
+
*,
|
|
813
|
+
plan_path: str | None = None,
|
|
814
|
+
resolved: ResolvedProfile | None = None,
|
|
815
|
+
rules: Sequence[RuleSpec] | None = None,
|
|
816
|
+
check_existing: bool = True,
|
|
817
|
+
) -> PlanReport:
|
|
818
|
+
"""Read, parse and validate `block`'s plan in the project at `root`.
|
|
819
|
+
|
|
820
|
+
Raises `PlanEnvError` when there is nothing to check it against (see `load_env`).
|
|
821
|
+
"""
|
|
822
|
+
rel = _fill(plan_path or DEFAULT_PLAN_PATH, {"block": block}) or DEFAULT_PLAN_PATH
|
|
823
|
+
path = root / rel
|
|
824
|
+
if not path.is_file():
|
|
825
|
+
return PlanReport(block, rel, False, None, (), None, None)
|
|
826
|
+
raw = path.read_bytes()
|
|
827
|
+
text = raw.decode("utf-8", errors="replace")
|
|
828
|
+
plan, issues = parse_plan(text, plan_path=rel)
|
|
829
|
+
if plan is None:
|
|
830
|
+
return PlanReport(block, rel, True, None, tuple(issues), sha256_bytes(raw), None)
|
|
831
|
+
writes = [w.path for m in plan.modules for w in m.writes]
|
|
832
|
+
env = load_env(root, block, resolved=resolved, rules=rules, write_paths=writes)
|
|
833
|
+
issues = validate_plan(plan, env, plan_path=rel, source=text, check_existing=check_existing)
|
|
834
|
+
return PlanReport(block, rel, True, plan, tuple(issues), sha256_bytes(raw), env)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
# --- the check plugin -----------------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
class PlanCheck:
|
|
841
|
+
"""Checks a block's plan file against the Design Model, the profile and the graph."""
|
|
842
|
+
|
|
843
|
+
id = "plan_check"
|
|
844
|
+
name = "Plan"
|
|
845
|
+
|
|
846
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
847
|
+
start = time.monotonic()
|
|
848
|
+
block = ctx.params.get("block")
|
|
849
|
+
if not block:
|
|
850
|
+
return error_result(spec, "plan_check runs per block: no block param", start)
|
|
851
|
+
template = spec.args.get("plan", DEFAULT_PLAN_PATH)
|
|
852
|
+
if not isinstance(template, str):
|
|
853
|
+
return error_result(spec, "args['plan'] must be a path template string", start)
|
|
854
|
+
try:
|
|
855
|
+
report = check_plan_file(ctx.repo_root, block, plan_path=template)
|
|
856
|
+
except PlanEnvError as exc:
|
|
857
|
+
return error_result(spec, str(exc), start, rule="plan.env")
|
|
858
|
+
|
|
859
|
+
if not report.exists:
|
|
860
|
+
issues: tuple[Issue, ...] = (
|
|
861
|
+
Issue(
|
|
862
|
+
file=report.path,
|
|
863
|
+
rule="plan.missing",
|
|
864
|
+
severity="info",
|
|
865
|
+
msg=f"block {block!r} has no plan yet",
|
|
866
|
+
),
|
|
867
|
+
)
|
|
868
|
+
else:
|
|
869
|
+
issues = report.issues
|
|
870
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
871
|
+
hashes = [(report.path, report.sha256 or "none")]
|
|
872
|
+
if report.env is not None:
|
|
873
|
+
hashes.extend(sorted(report.env.existing.items()))
|
|
874
|
+
key = compute_idempotency_key(spec.id, {**spec.args, "block": block}, hashes)
|
|
875
|
+
return CheckResult(
|
|
876
|
+
check_id=spec.id,
|
|
877
|
+
status=status,
|
|
878
|
+
issues=issues,
|
|
879
|
+
duration_s=time.monotonic() - start,
|
|
880
|
+
idempotency_key=key,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
__all__ = [
|
|
885
|
+
"DEFAULT_PLAN_PATH",
|
|
886
|
+
"PLAN_MODULES",
|
|
887
|
+
"ModuleInfo",
|
|
888
|
+
"PlanCheck",
|
|
889
|
+
"PlanEnv",
|
|
890
|
+
"PlanEnvError",
|
|
891
|
+
"PlanReport",
|
|
892
|
+
"PortInfo",
|
|
893
|
+
"ReqInfo",
|
|
894
|
+
"check_plan_file",
|
|
895
|
+
"dependency_depth",
|
|
896
|
+
"load_env",
|
|
897
|
+
"parse_plan",
|
|
898
|
+
"project_rules",
|
|
899
|
+
"topo_order",
|
|
900
|
+
"validate_plan",
|
|
901
|
+
]
|