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.
Files changed (252) hide show
  1. chipgraph/__init__.py +7 -0
  2. chipgraph/adapters/__init__.py +1 -0
  3. chipgraph/adapters/format/__init__.py +6 -0
  4. chipgraph/adapters/format/__main__.py +16 -0
  5. chipgraph/adapters/format/_yaml_lines.py +54 -0
  6. chipgraph/adapters/format/chip_yaml.py +323 -0
  7. chipgraph/adapters/format/qsoc_contract.py +246 -0
  8. chipgraph/adapters/llm/__init__.py +58 -0
  9. chipgraph/adapters/llm/_budget.py +173 -0
  10. chipgraph/adapters/llm/_errors.py +50 -0
  11. chipgraph/adapters/llm/_nda.py +30 -0
  12. chipgraph/adapters/llm/_retry.py +85 -0
  13. chipgraph/adapters/llm/anthropic.py +247 -0
  14. chipgraph/adapters/llm/decide_backend.py +179 -0
  15. chipgraph/adapters/llm/fake.py +87 -0
  16. chipgraph/adapters/llm/guard.py +68 -0
  17. chipgraph/adapters/parser/__init__.py +22 -0
  18. chipgraph/adapters/parser/cocotb.py +304 -0
  19. chipgraph/adapters/parser/generic_regex.py +75 -0
  20. chipgraph/adapters/parser/verible.py +55 -0
  21. chipgraph/adapters/parser/verilator.py +46 -0
  22. chipgraph/adapters/review/__init__.py +7 -0
  23. chipgraph/adapters/review/file.py +123 -0
  24. chipgraph/adapters/review/github.py +886 -0
  25. chipgraph/adapters/runner/__init__.py +7 -0
  26. chipgraph/adapters/runner/local.py +122 -0
  27. chipgraph/adapters/runtime/__init__.py +5 -0
  28. chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
  29. chipgraph/adapters/runtime/claude_code/agents.py +158 -0
  30. chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
  31. chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
  32. chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
  33. chipgraph/adapters/runtime/claude_code/loop.py +342 -0
  34. chipgraph/adapters/runtime/claude_code/previous.py +419 -0
  35. chipgraph/adapters/runtime/claude_code/review.py +492 -0
  36. chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
  37. chipgraph/adapters/runtime/claude_code/service.py +2285 -0
  38. chipgraph/adapters/tool/__init__.py +7 -0
  39. chipgraph/adapters/tool/cmd.py +229 -0
  40. chipgraph/adapters/tool/edalize.py +773 -0
  41. chipgraph/adapters/tool/filelist.py +259 -0
  42. chipgraph/adapters/tool/pyslang.py +485 -0
  43. chipgraph/adapters/vcs/__init__.py +1 -0
  44. chipgraph/adapters/vcs/git.py +521 -0
  45. chipgraph/app/__init__.py +10 -0
  46. chipgraph/app/baseline.py +339 -0
  47. chipgraph/app/build.py +332 -0
  48. chipgraph/app/checks.py +202 -0
  49. chipgraph/app/context.py +191 -0
  50. chipgraph/app/errors.py +11 -0
  51. chipgraph/app/executors.py +79 -0
  52. chipgraph/app/findings.py +203 -0
  53. chipgraph/app/ingest.py +523 -0
  54. chipgraph/checks/__init__.py +52 -0
  55. chipgraph/checks/_cdc_analyze.py +272 -0
  56. chipgraph/checks/_cdc_elaborate.py +455 -0
  57. chipgraph/checks/_common.py +268 -0
  58. chipgraph/checks/_cross.py +96 -0
  59. chipgraph/checks/_model.py +64 -0
  60. chipgraph/checks/_naming_pyslang.py +235 -0
  61. chipgraph/checks/_naming_rules.py +127 -0
  62. chipgraph/checks/_xref_syntax.py +260 -0
  63. chipgraph/checks/cdc_struct.py +235 -0
  64. chipgraph/checks/connect.py +353 -0
  65. chipgraph/checks/cross_chip.py +454 -0
  66. chipgraph/checks/duplicate.py +153 -0
  67. chipgraph/checks/filelist.py +323 -0
  68. chipgraph/checks/format.py +268 -0
  69. chipgraph/checks/generated.py +151 -0
  70. chipgraph/checks/hardcode.py +233 -0
  71. chipgraph/checks/layout.py +155 -0
  72. chipgraph/checks/naming.py +311 -0
  73. chipgraph/checks/plan_check.py +901 -0
  74. chipgraph/checks/ports_diff.py +203 -0
  75. chipgraph/checks/spec_schema.py +277 -0
  76. chipgraph/checks/style.py +910 -0
  77. chipgraph/checks/tb_static.py +599 -0
  78. chipgraph/checks/trace.py +224 -0
  79. chipgraph/checks/vendor_guard.py +369 -0
  80. chipgraph/checks/wrap_check.py +486 -0
  81. chipgraph/cli/__init__.py +1688 -0
  82. chipgraph/cli/next_steps.py +137 -0
  83. chipgraph/core/__init__.py +1 -0
  84. chipgraph/core/config/__init__.py +93 -0
  85. chipgraph/core/config/defaults.py +14 -0
  86. chipgraph/core/config/errors.py +32 -0
  87. chipgraph/core/config/loader.py +766 -0
  88. chipgraph/core/config/models.py +845 -0
  89. chipgraph/core/config/templates.py +340 -0
  90. chipgraph/core/contracts/__init__.py +75 -0
  91. chipgraph/core/contracts/_paths.py +26 -0
  92. chipgraph/core/contracts/agent.py +49 -0
  93. chipgraph/core/contracts/artifact.py +68 -0
  94. chipgraph/core/contracts/check.py +76 -0
  95. chipgraph/core/contracts/event.py +70 -0
  96. chipgraph/core/contracts/export.py +128 -0
  97. chipgraph/core/contracts/finding.py +171 -0
  98. chipgraph/core/contracts/gate.py +50 -0
  99. chipgraph/core/contracts/rule.py +177 -0
  100. chipgraph/core/contracts/types.py +45 -0
  101. chipgraph/core/engine/__init__.py +70 -0
  102. chipgraph/core/engine/agent_rule.py +1362 -0
  103. chipgraph/core/engine/baseline.py +283 -0
  104. chipgraph/core/engine/decide.py +462 -0
  105. chipgraph/core/engine/fanout.py +1358 -0
  106. chipgraph/core/engine/gate.py +170 -0
  107. chipgraph/core/engine/graph.py +647 -0
  108. chipgraph/core/engine/records.py +60 -0
  109. chipgraph/core/engine/rules.py +92 -0
  110. chipgraph/core/engine/scheduler.py +637 -0
  111. chipgraph/core/engine/select.py +1239 -0
  112. chipgraph/core/engine/targets.py +342 -0
  113. chipgraph/core/model/__init__.py +98 -0
  114. chipgraph/core/model/entities.py +244 -0
  115. chipgraph/core/model/export.py +16 -0
  116. chipgraph/core/model/ingest.py +693 -0
  117. chipgraph/core/model/json_value.py +4 -0
  118. chipgraph/core/model/keys.py +95 -0
  119. chipgraph/core/model/model.py +153 -0
  120. chipgraph/core/model/provenance.py +27 -0
  121. chipgraph/core/model/query.py +583 -0
  122. chipgraph/core/model/registry.py +80 -0
  123. chipgraph/core/model/relations.py +61 -0
  124. chipgraph/core/model/store.py +279 -0
  125. chipgraph/core/plugin_api/__init__.py +81 -0
  126. chipgraph/core/plugin_api/local.py +167 -0
  127. chipgraph/core/plugin_api/pack.py +159 -0
  128. chipgraph/core/plugin_api/protocols.py +177 -0
  129. chipgraph/core/plugin_api/registry.py +111 -0
  130. chipgraph/core/plugin_api/types.py +94 -0
  131. chipgraph/core/runtime/__init__.py +41 -0
  132. chipgraph/core/runtime/executor.py +79 -0
  133. chipgraph/core/runtime/queue.py +565 -0
  134. chipgraph/core/runtime/roles/__init__.py +91 -0
  135. chipgraph/core/runtime/roles/data/author.md +41 -0
  136. chipgraph/core/runtime/roles/data/critic.md +56 -0
  137. chipgraph/core/runtime/roles/data/planner.md +38 -0
  138. chipgraph/core/runtime/roles/data/researcher.md +37 -0
  139. chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
  140. chipgraph/core/runtime/roles/data/tb-author.md +48 -0
  141. chipgraph/core/runtime/roles/data/triage.md +34 -0
  142. chipgraph/core/runtime/roles/policy.py +299 -0
  143. chipgraph/core/runtime/roles/registry.py +52 -0
  144. chipgraph/core/runtime/roles/review.py +275 -0
  145. chipgraph/core/runtime/roles/schema.py +82 -0
  146. chipgraph/core/runtime/roles/skills.py +134 -0
  147. chipgraph/core/runtime/roles/spec.py +232 -0
  148. chipgraph/core/state/__init__.py +1 -0
  149. chipgraph/core/state/artifacts.py +156 -0
  150. chipgraph/core/state/backend.py +117 -0
  151. chipgraph/core/state/findings.py +151 -0
  152. chipgraph/core/state/handoff.py +411 -0
  153. chipgraph/core/state/idempotency.py +59 -0
  154. chipgraph/core/state/journal.py +238 -0
  155. chipgraph/core/state/layout.py +82 -0
  156. chipgraph/core/state/lock.py +195 -0
  157. chipgraph/core/state/trace.py +225 -0
  158. chipgraph/learn/__init__.py +96 -0
  159. chipgraph/learn/draft.py +187 -0
  160. chipgraph/learn/infer.py +580 -0
  161. chipgraph/learn/models.py +145 -0
  162. chipgraph/learn/try_run.py +228 -0
  163. chipgraph/mcp/__init__.py +12 -0
  164. chipgraph/mcp/model_tools.py +167 -0
  165. chipgraph/mcp/server.py +787 -0
  166. chipgraph/orgs/qnsc/naming-v1.yml +113 -0
  167. chipgraph/orgs/qnsc/profile.yml +5 -0
  168. chipgraph/packs/__init__.py +8 -0
  169. chipgraph/packs/assist/__init__.py +16 -0
  170. chipgraph/packs/assist/ask/__init__.py +58 -0
  171. chipgraph/packs/assist/ask/_db.py +57 -0
  172. chipgraph/packs/assist/ask/_project.py +230 -0
  173. chipgraph/packs/assist/ask/answer.py +266 -0
  174. chipgraph/packs/assist/ask/check.py +182 -0
  175. chipgraph/packs/assist/ask/contract.py +115 -0
  176. chipgraph/packs/assist/ask/documents.py +157 -0
  177. chipgraph/packs/assist/ask/fts.py +80 -0
  178. chipgraph/packs/assist/ask/retrieve.py +331 -0
  179. chipgraph/packs/assist/audit/__init__.py +383 -0
  180. chipgraph/packs/assist/pack.yml +13 -0
  181. chipgraph/packs/assist/triage/__init__.py +64 -0
  182. chipgraph/packs/assist/triage/contract.py +94 -0
  183. chipgraph/packs/assist/triage/parse.py +371 -0
  184. chipgraph/packs/assist/triage/question.py +384 -0
  185. chipgraph/packs/assist/triage/report.py +160 -0
  186. chipgraph/packs/assist/triage/rules.py +447 -0
  187. chipgraph/packs/assist/triage/run.py +224 -0
  188. chipgraph/packs/digital_rtl/__init__.py +19 -0
  189. chipgraph/packs/digital_rtl/layout.py +144 -0
  190. chipgraph/packs/digital_rtl/pack.yml +25 -0
  191. chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
  192. chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
  193. chipgraph/packs/digital_rtl/plan/context.py +82 -0
  194. chipgraph/packs/digital_rtl/plan/expand.py +94 -0
  195. chipgraph/packs/digital_rtl/plan/model.py +150 -0
  196. chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
  197. chipgraph/packs/digital_rtl/plan/show.py +176 -0
  198. chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
  199. chipgraph/packs/digital_rtl/pr/body.py +165 -0
  200. chipgraph/packs/digital_rtl/pr/gate.py +110 -0
  201. chipgraph/packs/digital_rtl/pr/open.py +397 -0
  202. chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
  203. chipgraph/packs/digital_rtl/rtl/context.py +272 -0
  204. chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
  205. chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
  206. chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
  207. chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
  208. chipgraph/packs/digital_rtl/rules/review.yml +31 -0
  209. chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
  210. chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
  211. chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
  212. chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
  213. chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
  214. chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
  215. chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
  216. chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
  217. chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
  218. chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
  219. chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
  220. chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
  221. chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
  222. chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
  223. chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
  224. chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
  225. chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
  226. chipgraph/packs/dv/__init__.py +15 -0
  227. chipgraph/packs/dv/declaration.py +392 -0
  228. chipgraph/packs/dv/interface.py +596 -0
  229. chipgraph/packs/dv/pack.yml +14 -0
  230. chipgraph/packs/dv/rules/tb_module.yml +42 -0
  231. chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
  232. chipgraph/packs/spec_core/__init__.py +9 -0
  233. chipgraph/packs/spec_core/extract/__init__.py +12 -0
  234. chipgraph/packs/spec_core/extract/_markdown.py +371 -0
  235. chipgraph/packs/spec_core/extract/_text.py +116 -0
  236. chipgraph/packs/spec_core/extract/mas.py +975 -0
  237. chipgraph/packs/spec_core/gen/__init__.py +11 -0
  238. chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
  239. chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
  240. chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
  241. chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
  242. chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
  243. chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
  244. chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
  245. chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
  246. chipgraph/packs/spec_core/pack.yml +6 -0
  247. chipgraph/py.typed +0 -0
  248. chipgraph-0.1.0.dist-info/METADATA +53 -0
  249. chipgraph-0.1.0.dist-info/RECORD +252 -0
  250. chipgraph-0.1.0.dist-info/WHEEL +4 -0
  251. chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
  252. chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,202 @@
1
+ """`ProfileCheckRunner`: maps a check id to a project's `.chipgraph.yml` adapter config
2
+ and runs it, satisfying the scheduler's `CheckRunner` protocol.
3
+
4
+ Backs both `chipgraph check` (run directly, no run id, no caching) and the `checks=`
5
+ argument to `Scheduler` (see `app/build.py`). A run id may be set after construction
6
+ (`chipgraph resume RUN_ID` knows its run id ahead of time); when it is set, results are
7
+ cached in that run's `IdempotencyStore`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from chipgraph.app.context import AppContext
15
+ from chipgraph.app.findings import findings_from_check, layer_for_check
16
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue, RuleInstance
17
+ from chipgraph.core.plugin_api.protocols import Check, ToolAdapter
18
+ from chipgraph.core.plugin_api.registry import PluginError
19
+ from chipgraph.core.plugin_api.types import ToolContext
20
+ from chipgraph.core.state.artifacts import hash_inputs
21
+ from chipgraph.core.state.findings import FindingStore
22
+ from chipgraph.core.state.idempotency import IdempotencyStore, make_key
23
+
24
+ _RUNNER_KEYS = frozenset({"use", "per_block", "severity"})
25
+ """Adapter-config keys the runner itself reads; they are not passed to the plugin."""
26
+
27
+
28
+ _SEVERITY_RANK = {"info": 0, "warning": 1, "error": 2}
29
+
30
+
31
+ def _cap_severity(result: CheckResult, cap: object) -> CheckResult:
32
+ """Apply a profile's `adapters.<id>.severity` cap to `result` (DESIGN.md 4.8).
33
+
34
+ A project may run a check at a lower severity while it is not yet expected to pass
35
+ (e.g. `trace: { severity: warning }` before DV starts). Every issue that points at a
36
+ file and is above the cap is lowered to it, and a `fail` with no `error` issue left
37
+ becomes `pass`, so the check reports but does not block. An issue with no file says
38
+ the check itself could not run (a missing `make` target, an unreadable input); it is
39
+ not lowered, and neither is a check that returned `error`. A cap that is not one of
40
+ `error`/`warning`/`info` is ignored.
41
+ """
42
+ if not isinstance(cap, str) or cap not in _SEVERITY_RANK or cap == "error":
43
+ return result
44
+ if result.status == "error":
45
+ return result
46
+ limit = _SEVERITY_RANK[cap]
47
+ issues = tuple(
48
+ issue.model_copy(update={"severity": cap})
49
+ if issue.file is not None and _SEVERITY_RANK[issue.severity] > limit
50
+ else issue
51
+ for issue in result.issues
52
+ )
53
+ status = result.status
54
+ if status == "fail" and not any(i.severity == "error" for i in issues):
55
+ status = "pass"
56
+ return result.model_copy(update={"issues": issues, "status": status})
57
+
58
+
59
+ class ProfileCheckRunner:
60
+ """Runs a check id through the adapter `.chipgraph.yml` configures for it."""
61
+
62
+ def __init__(
63
+ self,
64
+ ctx: AppContext,
65
+ *,
66
+ run_id: str | None = None,
67
+ root_override: Path | None = None,
68
+ ) -> None:
69
+ self.ctx = ctx
70
+ self.run_id = run_id
71
+ self.finding_store = FindingStore(ctx.layout)
72
+ self._root = root_override if root_override is not None else ctx.root
73
+
74
+ async def run(self, check_id: str, instance: RuleInstance) -> CheckResult:
75
+ resolved = self.ctx.require_profile()
76
+ block = instance.params.get("block")
77
+ profile = resolved.for_block(block) if block is not None else resolved.profile
78
+
79
+ cfg = profile.adapters.get(check_id)
80
+ if cfg is None:
81
+ return self._record(
82
+ check_id,
83
+ CheckResult(
84
+ check_id=check_id,
85
+ status="error",
86
+ issues=(
87
+ Issue(
88
+ severity="error",
89
+ msg=(
90
+ f"no adapter configured for check {check_id!r}; "
91
+ f"configured adapters: {sorted(profile.adapters)}"
92
+ ),
93
+ ),
94
+ ),
95
+ duration_s=0.0,
96
+ idempotency_key=make_key({"check_id": check_id, "error": "unknown-check"}),
97
+ ),
98
+ )
99
+
100
+ plugin = self._resolve_plugin(cfg.use)
101
+ if plugin is None:
102
+ return self._record(
103
+ check_id,
104
+ CheckResult(
105
+ check_id=check_id,
106
+ status="error",
107
+ issues=(
108
+ Issue(
109
+ severity="error",
110
+ msg=(
111
+ f"no check or tool adapter named {cfg.use!r} "
112
+ f"(configured for check {check_id!r})"
113
+ ),
114
+ ),
115
+ ),
116
+ duration_s=0.0,
117
+ idempotency_key=make_key(
118
+ {"check_id": check_id, "error": "unknown-adapter", "use": cfg.use}
119
+ ),
120
+ ),
121
+ )
122
+
123
+ args = {k: v for k, v in cfg.model_dump().items() if k not in _RUNNER_KEYS}
124
+ spec = CheckSpec(id=check_id, capability=check_id, adapter=cfg.use, args=args)
125
+ tool_ctx = ToolContext(
126
+ repo_root=self._root,
127
+ runner=self.ctx.registry.get("runner", "local"),
128
+ env=dict(profile.env.vars),
129
+ params=dict(instance.params),
130
+ )
131
+
132
+ # Idempotent resume (DESIGN.md 6.2): a `pass` result is reused when the same
133
+ # check runs again in the same run with the same files. Only plugins with a
134
+ # `key_for(spec, ctx)` (today `CmdTool`) can be keyed before running; the
135
+ # built-in checks fold the files they read into their key inside `run()`, so
136
+ # they always run. The cache key adds the hashes of the instance's inputs and
137
+ # outputs, so editing a file between a kill and `resume` runs the check again.
138
+ # Failures are never reused: a fixed file must be checked again.
139
+ store = IdempotencyStore(self.ctx.layout, self.run_id) if self.run_id is not None else None
140
+ key_for = getattr(plugin, "key_for", None)
141
+ cache_key: str | None = None
142
+ if store is not None and callable(key_for):
143
+ refs = [r for r in (*instance.inputs, *instance.outputs) if r.path is not None]
144
+ files = self.ctx.store.current_hashes(refs)
145
+ cache_key = make_key(
146
+ {"check": str(key_for(spec, tool_ctx)), "files": hash_inputs(files.items())}
147
+ )
148
+ cached = store.get(cache_key)
149
+ if cached is not None and cached.ok:
150
+ return self._record(
151
+ check_id, _cap_severity(cached, cfg.model_dump().get("severity"))
152
+ )
153
+
154
+ result = await plugin.run(spec, tool_ctx)
155
+
156
+ if store is not None and cache_key is not None and result.ok:
157
+ store.put(result.model_copy(update={"idempotency_key": cache_key}))
158
+
159
+ return self._record(check_id, _cap_severity(result, cfg.model_dump().get("severity")))
160
+
161
+ def _record(self, check_id: str, result: CheckResult) -> CheckResult:
162
+ """Record `result`'s issues as findings, never failing the check for it.
163
+
164
+ Only an `OSError` writing to the finding store is swallowed; anything else
165
+ (e.g. a bug building a `Finding`) is a real error and propagates.
166
+ """
167
+ try:
168
+ layer = layer_for_check(check_id, result.check_id)
169
+ findings = findings_from_check(
170
+ result, layer=layer, store=self.ctx.store, run_id=self.run_id
171
+ )
172
+ if findings:
173
+ self.finding_store.upsert(findings)
174
+ except OSError:
175
+ pass
176
+ return result
177
+
178
+ def runs_per_block(self, check_id: str) -> bool:
179
+ """Whether `chipgraph check` runs `check_id` once per block (the default).
180
+
181
+ A profile's `adapters.<id>.per_block` wins; otherwise the plugin's own `per_block`
182
+ attribute (chip-wide checks set it to False); otherwise True.
183
+ """
184
+ cfg = self.ctx.require_profile().profile.adapters.get(check_id)
185
+ if cfg is None:
186
+ return True
187
+ override = cfg.model_dump().get("per_block")
188
+ if isinstance(override, bool):
189
+ return override
190
+ plugin = self._resolve_plugin(cfg.use)
191
+ return bool(getattr(plugin, "per_block", True))
192
+
193
+ def _resolve_plugin(self, use: str) -> Check | ToolAdapter | None:
194
+ for kind in ("check", "tool"):
195
+ try:
196
+ return self.ctx.registry.get(kind, use) # type: ignore[no-any-return]
197
+ except PluginError:
198
+ continue
199
+ return None
200
+
201
+
202
+ __all__ = ["ProfileCheckRunner"]
@@ -0,0 +1,191 @@
1
+ """`AppContext`: everything a CLI command needs, wired up from a project's `.chipgraph.yml`.
2
+
3
+ Read-only commands (`status`, `doctor`, `config show`, ...) must work even when no profile
4
+ exists yet (DESIGN.md 8.4/6.1): `AppContext.load` always succeeds, and only writes to disk
5
+ (state directory, git exclude) when a profile was actually found. Commands that need a
6
+ profile call `require_profile()`, which raises a clear `AppError`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import subprocess
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from chipgraph.adapters.review.file import FileReview, ReviewError
17
+ from chipgraph.adapters.review.github import GitHubReview
18
+ from chipgraph.app.errors import AppError
19
+ from chipgraph.core.config.errors import ConfigError
20
+ from chipgraph.core.config.loader import ResolvedProfile
21
+ from chipgraph.core.config.loader import load as load_profile
22
+ from chipgraph.core.config.models import DataCfg, Profile
23
+ from chipgraph.core.engine.gate import GateEvaluator
24
+ from chipgraph.core.plugin_api.local import LocalPluginRecord, load_local_plugins
25
+ from chipgraph.core.plugin_api.registry import PluginError, Registry
26
+ from chipgraph.core.state.artifacts import ArtifactStore, LabelRules
27
+ from chipgraph.core.state.backend import LocalBackend
28
+ from chipgraph.core.state.layout import StateLayout
29
+
30
+
31
+ def find_repo_root(start: Path) -> Path:
32
+ """The nearest ancestor of `start` containing `.git`, or `start` itself if none.
33
+
34
+ Mirrors `chipgraph.core.config.loader.find_profile`'s own boundary, but always
35
+ returns a directory (never `None`): a project root is needed even when no profile
36
+ exists yet, so read-only commands (and `init`) have somewhere to work from.
37
+ """
38
+ current = start.resolve()
39
+ if current.is_file():
40
+ current = current.parent
41
+ walker = current
42
+ while True:
43
+ if (walker / ".git").exists():
44
+ return walker
45
+ parent = walker.parent
46
+ if parent == walker:
47
+ return current
48
+ walker = parent
49
+
50
+
51
+ @dataclass
52
+ class AppContext:
53
+ """Everything wired up for one CLI invocation: profile, state, store, registry, ..."""
54
+
55
+ root: Path
56
+ resolved: ResolvedProfile | None
57
+ layout: StateLayout
58
+ store: ArtifactStore
59
+ registry: Registry
60
+ review: FileReview
61
+ gates: GateEvaluator
62
+ backend: LocalBackend | None = None
63
+ local_plugins: tuple[LocalPluginRecord, ...] = ()
64
+ pr_review: GitHubReview | None = None
65
+ """The PR review adapter when the profile selects `review: {use: github}`; `review`
66
+ stays the `file` adapter, which keeps waivers, baselines and light gates."""
67
+
68
+ @property
69
+ def profile(self) -> Profile | None:
70
+ """The resolved project profile, or `None` if no `.chipgraph.yml` was found."""
71
+ return self.resolved.profile if self.resolved is not None else None
72
+
73
+ def require_profile(self) -> ResolvedProfile:
74
+ """Return the resolved profile, or raise `AppError` if there is none."""
75
+ if self.resolved is None:
76
+ raise AppError("no .chipgraph.yml: only read-only commands work; run `chipgraph init`")
77
+ return self.resolved
78
+
79
+ @classmethod
80
+ def load(cls, start: Path, *, profile_path: Path | None = None) -> AppContext:
81
+ """Build an `AppContext` for a command starting at `start`.
82
+
83
+ `start` is searched upward for `.chipgraph.yml` (or `profile_path` is used
84
+ directly); the project root is the nearest git root above `start` (or `start`
85
+ itself, if it is not inside a git repo). State is only written to disk (state
86
+ directory created, `.git/info/exclude` updated) when a profile was found.
87
+ """
88
+ root = find_repo_root(start)
89
+ try:
90
+ resolved = load_profile(start, profile_path=profile_path)
91
+ except ConfigError as exc:
92
+ raise AppError(str(exc)) from exc
93
+
94
+ layout = StateLayout(root)
95
+ backend: LocalBackend | None = None
96
+ if resolved is not None:
97
+ backend = LocalBackend(root)
98
+ backend.prepare()
99
+
100
+ data_cfg = resolved.profile.data if resolved is not None else DataCfg()
101
+ labels = LabelRules(dict.fromkeys(data_cfg.nda_paths, "nda"), default=data_cfg.default)
102
+ store = ArtifactStore(root, labels)
103
+
104
+ registry = Registry()
105
+ try:
106
+ registry.discover()
107
+ except PluginError as exc:
108
+ raise AppError(str(exc)) from exc
109
+
110
+ local_plugins = cls._load_local_plugins(root, resolved, registry)
111
+
112
+ review = FileReview(layout.decisions_dir)
113
+ gates = GateEvaluator(review, store)
114
+
115
+ pr_review: GitHubReview | None = None
116
+ review_cfg = resolved.profile.review if resolved is not None else None
117
+ if review_cfg is not None and review_cfg.use == "github":
118
+ try:
119
+ pr_review = GitHubReview.from_config(review_cfg, state_dir=layout.state_dir)
120
+ except ReviewError as exc:
121
+ raise AppError(f"review: {exc}") from exc
122
+
123
+ return cls(
124
+ root=root,
125
+ resolved=resolved,
126
+ layout=layout,
127
+ store=store,
128
+ registry=registry,
129
+ review=review,
130
+ gates=gates,
131
+ backend=backend,
132
+ local_plugins=local_plugins,
133
+ pr_review=pr_review,
134
+ )
135
+
136
+ @staticmethod
137
+ def _load_local_plugins(
138
+ root: Path, resolved: ResolvedProfile | None, registry: Registry
139
+ ) -> tuple[LocalPluginRecord, ...]:
140
+ """Load the profile's local plugins into `registry`, honouring policy and env.
141
+
142
+ Disabled either by `policy.local_plugins: deny` (an org may turn it off) or by the
143
+ `CHIPGRAPH_LOCAL_PLUGINS=0` personal safety switch; the env switch can only disable,
144
+ never enable. A `PluginError` while loading becomes an `AppError`.
145
+ """
146
+ if resolved is None or not resolved.profile.plugins:
147
+ return ()
148
+
149
+ entries = resolved.profile.plugins
150
+ disabled_reason: str | None = None
151
+ if resolved.profile.policy.local_plugins == "deny":
152
+ source = resolved.provenance.get("policy.local_plugins")
153
+ where = source.location if source is not None else "profile"
154
+ disabled_reason = f"disabled by {where}"
155
+ elif os.environ.get("CHIPGRAPH_LOCAL_PLUGINS") == "0":
156
+ disabled_reason = "disabled by CHIPGRAPH_LOCAL_PLUGINS=0"
157
+
158
+ try:
159
+ return load_local_plugins(
160
+ root,
161
+ entries,
162
+ registry,
163
+ enabled=disabled_reason is None,
164
+ disabled_reason=disabled_reason,
165
+ )
166
+ except PluginError as exc:
167
+ raise AppError(str(exc)) from exc
168
+
169
+
170
+ def default_identity() -> str:
171
+ """The identity to attribute a decision to when `--by` is not given.
172
+
173
+ `git config user.name`, falling back to the `$USER` environment variable, then
174
+ `"unknown"`. Never raises: a missing/broken `git` just falls through.
175
+ """
176
+ try:
177
+ result = subprocess.run(
178
+ ["git", "config", "user.name"],
179
+ capture_output=True,
180
+ text=True,
181
+ timeout=5,
182
+ check=False,
183
+ )
184
+ except (OSError, subprocess.SubprocessError):
185
+ result = None
186
+ if result is not None and result.returncode == 0 and result.stdout.strip():
187
+ return result.stdout.strip()
188
+ return os.environ.get("USER", "unknown")
189
+
190
+
191
+ __all__ = ["AppContext", "default_identity", "find_repo_root"]
@@ -0,0 +1,11 @@
1
+ """`AppError`: raised for application-layer problems the CLI reports as a usage error."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AppError(Exception):
7
+ """A problem wiring the engine to a concrete project: bad profile, missing pack,
8
+ missing adapter, no profile for a command that needs one, and so on.
9
+
10
+ The CLI catches this (and `chipgraph.core.config.errors.ConfigError`) and exits 2.
11
+ """
@@ -0,0 +1,79 @@
1
+ """`Executor`s for the scheduler: one per `RuleKind` (DESIGN.md 3.3, 3.4).
2
+
3
+ `gen` and `import` rules run through a tool adapter (`rule.run`); `human` rules just
4
+ check whether their outputs exist yet; `agent` rules use the scheduler's own
5
+ `AgentStub` (agent rules arrive in M2).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from chipgraph.app.context import AppContext
11
+ from chipgraph.core.contracts import CheckSpec, RuleInstance, RuleSpec
12
+ from chipgraph.core.contracts.types import FailureLabel
13
+ from chipgraph.core.engine.scheduler import ExecOutcome
14
+ from chipgraph.core.plugin_api.registry import PluginError
15
+ from chipgraph.core.plugin_api.types import ToolContext
16
+
17
+
18
+ class RunExecutor:
19
+ """Runs a `gen`/`import` rule's `run` spec through a tool adapter."""
20
+
21
+ def __init__(self, ctx: AppContext) -> None:
22
+ self.ctx = ctx
23
+
24
+ async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
25
+ if rule.run is None:
26
+ return ExecOutcome(
27
+ ok=False, failure_label="infra", message=f"rule {rule.id!r} has no 'run'"
28
+ )
29
+
30
+ try:
31
+ tool = self.ctx.registry.get("tool", rule.run.use)
32
+ except PluginError as exc:
33
+ return ExecOutcome(ok=False, failure_label="infra", message=str(exc))
34
+
35
+ spec = CheckSpec(
36
+ id=rule.id, capability=rule.kind, adapter=rule.run.use, args=dict(rule.run.args)
37
+ )
38
+ env = dict(self.ctx.profile.env.vars) if self.ctx.profile is not None else {}
39
+ tool_ctx = ToolContext(
40
+ repo_root=self.ctx.root,
41
+ runner=self.ctx.registry.get("runner", "local"),
42
+ env=env,
43
+ params=dict(instance.params),
44
+ )
45
+
46
+ result = await tool.run(spec, tool_ctx)
47
+ if result.status != "pass":
48
+ label: FailureLabel = "verification" if result.status == "fail" else "infra"
49
+ message = (
50
+ "; ".join(issue.msg for issue in result.issues)
51
+ or result.log_tail
52
+ or f"tool {rule.run.use!r} did not pass for rule {rule.id!r}"
53
+ )
54
+ return ExecOutcome(ok=False, failure_label=label, message=message)
55
+ return ExecOutcome(ok=True)
56
+
57
+
58
+ class HumanExecutor:
59
+ """A `human` rule 'runs' by having a person write its outputs by hand."""
60
+
61
+ def __init__(self, ctx: AppContext) -> None:
62
+ self.ctx = ctx
63
+
64
+ async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
65
+ missing = [
66
+ ref.path
67
+ for ref in instance.outputs
68
+ if ref.path is not None and not self.ctx.store.exists(ref)
69
+ ]
70
+ if missing:
71
+ return ExecOutcome(
72
+ ok=False,
73
+ failure_label="planning",
74
+ message=f"waiting for a person to write {', '.join(missing)}",
75
+ )
76
+ return ExecOutcome(ok=True)
77
+
78
+
79
+ __all__ = ["HumanExecutor", "RunExecutor"]
@@ -0,0 +1,203 @@
1
+ """App-layer glue for findings: `CheckResult` -> `Finding`, and waiving a finding.
2
+
3
+ Core (`chipgraph.core.contracts.finding`, `chipgraph.core.state.findings`) defines what a
4
+ finding and a waiver are; this module wires them to the rest of the app: mapping a check
5
+ id to a DESIGN.md 4.8 layer, turning a check's `Issue`s into `Finding`s, and recording a
6
+ waiver decision through the `ReviewAdapter` (`chipgraph.adapters.review.file.FileReview`).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from collections.abc import Iterable, Mapping
13
+ from datetime import UTC, datetime
14
+
15
+ from chipgraph.app.errors import AppError
16
+ from chipgraph.core.contracts import Approval, ArtifactRef, CheckResult
17
+ from chipgraph.core.contracts.finding import Evidence, Finding, FindingSeverity
18
+ from chipgraph.core.plugin_api.protocols import ReviewAdapter
19
+ from chipgraph.core.state.artifacts import ArtifactStore
20
+ from chipgraph.core.state.findings import FindingStore, waiver_gate_id
21
+
22
+ # Layer per DESIGN.md 4.8's table, keyed by check id (falling back to capability, then to
23
+ # layer 1, "cấu trúc và số liệu", which is where most cross-artifact checks land). Checks
24
+ # not listed here (including ones not yet built, e.g. M1-07/M1-19) default to layer 1;
25
+ # add them here as they are built if they belong to a different layer.
26
+ LAYER_BY_CHECK: Mapping[str, int] = {
27
+ # layer 1: structural/deterministic cross-checks
28
+ "spec_schema": 1,
29
+ "cross_chip": 1,
30
+ "ports_diff": 1,
31
+ "hardcode": 1,
32
+ "duplicate": 1,
33
+ "connect": 1,
34
+ "cdc_struct": 1,
35
+ # layer 4: lint / synth "smells"
36
+ "lint": 4,
37
+ "layout": 4,
38
+ "synth": 4,
39
+ "naming": 4,
40
+ # layer 5: process (traceability, staleness, gate/approval hygiene)
41
+ "trace": 5,
42
+ }
43
+ _DEFAULT_LAYER = 1
44
+
45
+
46
+ def layer_for_check(check_id: str, capability: str = "") -> int:
47
+ """The DESIGN.md 4.8 layer a check's findings belong to, defaulting to layer 1."""
48
+ if check_id in LAYER_BY_CHECK:
49
+ return LAYER_BY_CHECK[check_id]
50
+ if capability in LAYER_BY_CHECK:
51
+ return LAYER_BY_CHECK[capability]
52
+ return _DEFAULT_LAYER
53
+
54
+
55
+ def _finding_severity(issue_severity: str) -> FindingSeverity:
56
+ # `Issue.severity` is "error" | "warning" | "info"; `Finding.severity` has no "info".
57
+ if issue_severity == "info":
58
+ return "warning"
59
+ if issue_severity == "error":
60
+ return "error"
61
+ return "warning"
62
+
63
+
64
+ def findings_from_check(
65
+ result: CheckResult,
66
+ *,
67
+ layer: int = 1,
68
+ store: ArtifactStore | None = None,
69
+ run_id: str | None = None,
70
+ ) -> list[Finding]:
71
+ """Build one `Finding` per `Issue` in `result`.
72
+
73
+ An issue with a `file` gets that as its evidence's `file`/`line`; if `store` is
74
+ given, the file's current content hash is recorded in `artifacts`/`artifact_hashes`
75
+ (what a waiver against this finding would bind to). An issue with no `file` (e.g. a
76
+ whole-check configuration error) has nowhere else to point at, so its evidence uses
77
+ the check id itself as a Design-Model-style key: `model_key="check:<check_id>"`.
78
+
79
+ All findings from a check are deterministic (`source="check:<check_id>"`), so they
80
+ may carry `severity="error"`.
81
+ """
82
+ now = datetime.now(UTC)
83
+ findings: list[Finding] = []
84
+ for issue in result.issues:
85
+ if issue.file is not None:
86
+ evidence = Evidence(file=issue.file, line=issue.line, note=issue.rule)
87
+ artifacts: tuple[ArtifactRef, ...] = (ArtifactRef(kind="other", path=issue.file),)
88
+ artifact_hashes = store.current_hashes(artifacts) if store is not None else {}
89
+ else:
90
+ evidence = Evidence(model_key=f"check:{result.check_id}", note=issue.rule)
91
+ artifacts = ()
92
+ artifact_hashes = {}
93
+
94
+ findings.append(
95
+ Finding(
96
+ layer=layer,
97
+ severity=_finding_severity(issue.severity),
98
+ source=f"check:{result.check_id}",
99
+ evidence=(evidence,),
100
+ claim=issue.msg,
101
+ artifacts=artifacts,
102
+ artifact_hashes=artifact_hashes,
103
+ first_seen=now,
104
+ last_seen=now,
105
+ run_id=run_id,
106
+ )
107
+ )
108
+ return findings
109
+
110
+
111
+ ABSENT_HASH = hashlib.sha256(b"chipgraph:absent-file").hexdigest()
112
+ """The hash a waiver records for a bound file that does not exist yet.
113
+
114
+ When the file appears, its real hash differs, so the waiver stops being current."""
115
+
116
+
117
+ def bound_hashes(store: ArtifactStore, paths: Iterable[str]) -> dict[str, str]:
118
+ """`{"<repo>:<path>": hash}` for `paths`, with `ABSENT_HASH` for a missing file."""
119
+ refs = [ArtifactRef(kind="other", path=path) for path in paths]
120
+ present = store.current_hashes(refs)
121
+ return {
122
+ f"{ref.repo}:{ref.path}": present.get(f"{ref.repo}:{ref.path}", ABSENT_HASH) for ref in refs
123
+ }
124
+
125
+
126
+ def finding_current_hashes(
127
+ store: ArtifactStore, finding: Finding, waivers: Iterable[Approval]
128
+ ) -> dict[str, str]:
129
+ """The current hashes to judge `finding`'s waivers against.
130
+
131
+ The finding's own artifacts, plus every file a waiver was bound to with `--bind`
132
+ (a missing one hashes to `ABSENT_HASH`), so a waiver on a whole-check finding
133
+ expires when a bound file appears or changes.
134
+ """
135
+ current = store.current_hashes(finding.artifacts)
136
+ extra = sorted(
137
+ key.partition(":")[2]
138
+ for waiver in waivers
139
+ for key in waiver.artifact_hashes
140
+ if key not in current and key.partition(":")[2]
141
+ )
142
+ current.update(bound_hashes(store, extra))
143
+ return current
144
+
145
+
146
+ def waive(
147
+ finding: Finding,
148
+ *,
149
+ by: str,
150
+ reason: str,
151
+ store: FindingStore,
152
+ review: ReviewAdapter,
153
+ current_hashes: Mapping[str, str] | None = None,
154
+ ) -> Finding:
155
+ """Waive `finding`: record a `waive` decision through `review`, bound to its artifacts.
156
+
157
+ `reason` is required (an empty reason is refused) and is stored as the decision's
158
+ `note`. The waiver's `artifact_hashes` come from `finding.artifact_hashes` (its
159
+ hashes at detection time), unless `current_hashes` is given, in which case the
160
+ waiver binds to `finding.artifacts`' *current* hashes instead (waiving as of now,
161
+ not as of whenever the finding was first recorded). A finding with no file (a
162
+ whole-check error) is bound with `current_hashes` from `bound_hashes` instead (the
163
+ CLI's `--bind`). Raises `ValueError` if `reason` is blank, or `AppError` if there is
164
+ no artifact hash to bind a waiver to.
165
+
166
+ Also updates the finding's stored `status` to `"waived"` in `store`, for display
167
+ convenience; `chipgraph.core.state.findings.effective_status` remains the source of
168
+ truth for whether the waiver is still current.
169
+ """
170
+ if not reason.strip():
171
+ raise ValueError("a waiver requires a non-empty reason")
172
+
173
+ hashes = dict(current_hashes) if current_hashes else dict(finding.artifact_hashes)
174
+ if not hashes:
175
+ raise AppError(
176
+ f"cannot waive finding {finding.id!r}: it has no file to bind the waiver to; "
177
+ "name the files whose change should end the waiver with --bind PATH"
178
+ )
179
+
180
+ approval = Approval(
181
+ gate_id=waiver_gate_id(finding.id),
182
+ by=by,
183
+ at=datetime.now(UTC),
184
+ artifact_hashes=hashes,
185
+ decision="waive",
186
+ note=reason,
187
+ )
188
+ review.record(approval)
189
+
190
+ updated = finding.model_copy(update={"status": "waived"})
191
+ (stored,) = store.upsert((updated,))
192
+ return stored
193
+
194
+
195
+ __all__ = [
196
+ "ABSENT_HASH",
197
+ "LAYER_BY_CHECK",
198
+ "bound_hashes",
199
+ "finding_current_hashes",
200
+ "findings_from_check",
201
+ "layer_for_check",
202
+ "waive",
203
+ ]