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,486 @@
1
+ """`WrapCheck` (`wrap_check`): a wrapper's generated file is what its source expands to.
2
+
3
+ The check of the `digital-rtl/wrapper` rules (task M2-12), and the `chipgraph check`
4
+ equivalent of QSoC's `make wrap-check`. For each wrapper (the task's, when the params
5
+ name one; else every wrapper of the params' block, or of the project) it
6
+
7
+ 1. refuses Lisp the source may not carry (`wrapper.lisp`), and reads the IP module's
8
+ port declaration from the wrapper's `vendor_files` (pyslang, header only);
9
+ 2. regenerates the expansion with emacs, in a scratch copy (`wrapper.emacs.expand`);
10
+ 3. checks the expanded instance against the declaration (`wrapper.expansion.validate`):
11
+ an `/*AUTOINST*/` that expanded to nothing, an IP port no template line maps (left
12
+ unconnected to any project signal), an open input, a port the IP does not declare,
13
+ an IP port not connected. These issues point at the source, and quote the expanded
14
+ connection: the author cannot run emacs, so this text is its redo;
15
+ 4. compares the regeneration with the committed generated file: each differing hunk is
16
+ an issue at the committed file's line, with both texts and "run the generator".
17
+ A difference in whitespace only is a warning (verilog-mode's column alignment can
18
+ differ between emacs versions; QSoC's check ignores whitespace too).
19
+
20
+ When it compares (arg `compare`): `always`; `never`; `auto` (default): over a block or
21
+ the project, always; for a task (a `wrapper` param), only when the generated file was
22
+ generated from the current source (its header's source sha256): the author's check runs
23
+ before the generator, so a missing or older file is not the author's fault.
24
+
25
+ Without emacs (or without verilog-mode) the check cannot regenerate. With arg
26
+ `require_emacs: true` that is an `error`; `require_emacs` defaults to true when the
27
+ environment variable `CI` is set (to anything but empty, `0`, `false` or `no`), so a
28
+ CI run never passes on the weaker fallback unless the profile says
29
+ `require_emacs: false`. Otherwise (outside CI, so a machine without emacs can still run
30
+ `chipgraph check`), when the committed file's `chipgraph:generated` marker matches its
31
+ content and its header names the current source's sha256, the committed expansion is
32
+ validated as in 3 and the check passes with a warning that the regeneration was not
33
+ verified; anything else is an `error` with the install hint.
34
+
35
+ The fallback is weaker than a regeneration, by design: the marker is a plain sha256 of
36
+ the file's content, not a MAC or a signature, so anyone who edits the generated body and
37
+ re-renders the marker and header passes it (with the `wrap.not_regenerated` warning).
38
+ Only a regeneration with emacs catches that edit (`wrap.differs`).
39
+
40
+ Args (all optional): `src`, `out` (path templates with `{block}`, `{wrapper}`; default
41
+ `rtl/emacs/{wrapper}.src.sv`, `rtl/{wrapper}.sv`, as the rules; refused when they
42
+ resolve outside the repository, or under `.git/` or `.chipgraph/`), `compare`,
43
+ `require_emacs`, `emacs` (the executable: a trusted profile value, since the check runs
44
+ it; a bare name is looked up on `PATH`, a value with a path separator must be an
45
+ absolute path to an executable file), `timeout_s` (a number of seconds,
46
+ `0 < timeout_s <= 600`; default 30). A source or generated file that cannot be read,
47
+ or is not UTF-8, is a `wrap.read` issue.
48
+
49
+ Profile example (`.chipgraph.yml`)::
50
+
51
+ adapters:
52
+ wrap_check: { use: wrap_check }
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import difflib
58
+ import math
59
+ import os
60
+ import re
61
+ import time
62
+ from collections.abc import Mapping, Sequence
63
+ from dataclasses import dataclass, field
64
+ from pathlib import Path
65
+ from typing import Any
66
+
67
+ from chipgraph.checks._common import compute_idempotency_key, sha256_file
68
+ from chipgraph.checks.generated import verify
69
+ from chipgraph.core.config.errors import ConfigError
70
+ from chipgraph.core.config.loader import load as load_profile
71
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
72
+ from chipgraph.core.contracts.types import CheckStatus
73
+ from chipgraph.core.plugin_api.types import ToolContext
74
+ from chipgraph.packs.digital_rtl.wrapper import emacs as gen
75
+ from chipgraph.packs.digital_rtl.wrapper import lisp
76
+ from chipgraph.packs.digital_rtl.wrapper.common import (
77
+ GEN_RULE,
78
+ Wrapper,
79
+ WrapperError,
80
+ all_wrappers,
81
+ find_wrapper,
82
+ inside,
83
+ instance_id,
84
+ paths,
85
+ )
86
+ from chipgraph.packs.digital_rtl.wrapper.expansion import instances, validate
87
+ from chipgraph.packs.dv.declaration import extract_declaration
88
+
89
+ COMPARE_MODES = ("auto", "always", "never")
90
+ MAX_HUNKS = 20
91
+ """Most differing hunks reported per file."""
92
+ MAX_HUNK_LINES = 12
93
+ """Most lines of each side shown per hunk."""
94
+ _MARKER_RE = re.compile(r"^\s*//\s*chipgraph:generated sha256=[0-9a-f]{64}\s*$")
95
+ _NO_EMACS = ("wrap.emacs_missing", "wrap.verilog_mode_missing")
96
+
97
+
98
+ @dataclass
99
+ class _Outcome:
100
+ issues: list[Issue] = field(default_factory=list)
101
+ files: list[tuple[str, str]] = field(default_factory=list)
102
+ infra: bool = False
103
+ log: str = ""
104
+
105
+
106
+ def _without_marker(text: str) -> list[str]:
107
+ lines = text.splitlines()
108
+ return lines[1:] if lines and _MARKER_RE.match(lines[0]) else lines
109
+
110
+
111
+ def _squash(lines: Sequence[str]) -> str:
112
+ return re.sub(r"\s+", "", "\n".join(lines))
113
+
114
+
115
+ def _shown(lines: Sequence[str], sign: str) -> str:
116
+ head = [f"{sign} {line}" for line in lines[:MAX_HUNK_LINES]]
117
+ if len(lines) > MAX_HUNK_LINES:
118
+ head.append(f"{sign} ... ({len(lines) - MAX_HUNK_LINES} more lines)")
119
+ return "\n".join(head)
120
+
121
+
122
+ def compare(committed: str, generated: str, out_rel: str, rerun: str) -> list[Issue]:
123
+ """The hunks where `committed` differs from `generated` (see the module docstring)."""
124
+ old, new = _without_marker(committed), _without_marker(generated)
125
+ if old == new:
126
+ return []
127
+ if _squash(old) == _squash(new):
128
+ return [
129
+ Issue(
130
+ file=out_rel,
131
+ severity="warning",
132
+ rule="wrap.whitespace",
133
+ msg=(
134
+ f"{out_rel} differs from the generator's output in whitespace only "
135
+ f"(another emacs or verilog-mode version?); {rerun} to refresh it"
136
+ ),
137
+ )
138
+ ]
139
+ edited = verify(committed) is False
140
+ note = (
141
+ " The file was edited after it was generated (its chipgraph:generated marker no "
142
+ "longer matches): never edit a generated wrapper."
143
+ if edited
144
+ else ""
145
+ )
146
+ key_old = [" ".join(line.split()) for line in old]
147
+ key_new = [" ".join(line.split()) for line in new]
148
+ offset = len(committed.splitlines()) - len(old) # the marker line, when there is one
149
+ issues: list[Issue] = []
150
+ matcher = difflib.SequenceMatcher(a=key_old, b=key_new, autojunk=False)
151
+ hunks = [op for op in matcher.get_opcodes() if op[0] != "equal"]
152
+ for tag, i1, i2, j1, j2 in hunks[:MAX_HUNKS]:
153
+ parts = []
154
+ if i2 > i1:
155
+ parts.append(_shown(old[i1:i2], "-"))
156
+ if j2 > j1:
157
+ parts.append(_shown(new[j1:j2], "+"))
158
+ what = {
159
+ "replace": "differs from",
160
+ "delete": "has lines that are not in",
161
+ "insert": "is missing lines of",
162
+ }[tag]
163
+ issues.append(
164
+ Issue(
165
+ file=out_rel,
166
+ line=min(i1, max(len(old) - 1, 0)) + 1 + offset,
167
+ rule="wrap.differs",
168
+ msg=(
169
+ f"{out_rel} {what} the generator's output here ('-' committed, '+' "
170
+ f"generated); {rerun}.{note}\n" + "\n".join(parts)
171
+ ),
172
+ )
173
+ )
174
+ note = ""
175
+ if len(hunks) > MAX_HUNKS:
176
+ issues.append(
177
+ Issue(
178
+ file=out_rel,
179
+ rule="wrap.differs",
180
+ msg=f"{len(hunks) - MAX_HUNKS} more differing hunks in {out_rel}; {rerun}",
181
+ )
182
+ )
183
+ return issues
184
+
185
+
186
+ def _mode(args: Mapping[str, Any]) -> str:
187
+ mode = args.get("compare", "auto")
188
+ if mode not in COMPARE_MODES:
189
+ raise WrapperError(f"args['compare'] must be one of {', '.join(COMPARE_MODES)}")
190
+ return str(mode)
191
+
192
+
193
+ def _opt_str(args: Mapping[str, Any], key: str) -> str | None:
194
+ value = args.get(key)
195
+ if value is not None and not isinstance(value, str):
196
+ raise WrapperError(f"args[{key!r}] must be a string")
197
+ return value
198
+
199
+
200
+ MAX_TIMEOUT_S = 600.0
201
+ """The longest `timeout_s` the check accepts."""
202
+ _PROTECTED_DIRS = (".git", ".chipgraph")
203
+ _CI_OFF = ("", "0", "false", "no")
204
+
205
+
206
+ def _timeout(args: Mapping[str, Any]) -> float:
207
+ """`timeout_s`: a finite number, `0 < t <= MAX_TIMEOUT_S` (never a bool or a string)."""
208
+ value = args.get("timeout_s", gen.TIMEOUT_S)
209
+ if (
210
+ isinstance(value, bool)
211
+ or not isinstance(value, int | float)
212
+ or not math.isfinite(value)
213
+ or not 0 < value <= MAX_TIMEOUT_S
214
+ ):
215
+ raise WrapperError(
216
+ f"args['timeout_s'] must be a number of seconds, 0 < timeout_s <= {MAX_TIMEOUT_S:g}"
217
+ )
218
+ return float(value)
219
+
220
+
221
+ def _in_ci() -> bool:
222
+ return os.environ.get("CI", "").strip().lower() not in _CI_OFF
223
+
224
+
225
+ def _require(args: Mapping[str, Any]) -> bool:
226
+ """`require_emacs`: as given, else whether this runs in CI (see the module)."""
227
+ value = args.get("require_emacs")
228
+ if value is None:
229
+ return _in_ci()
230
+ if not isinstance(value, bool):
231
+ raise WrapperError("args['require_emacs'] must be true or false")
232
+ return value
233
+
234
+
235
+ def _emacs(args: Mapping[str, Any]) -> str:
236
+ """`emacs`: a bare name (looked up on PATH) or an absolute path to an executable."""
237
+ value = _opt_str(args, "emacs") or gen.EMACS
238
+ if "/" in value or "\\" in value or os.sep in value:
239
+ path = Path(value)
240
+ if not path.is_absolute() or not path.is_file() or not os.access(path, os.X_OK):
241
+ raise WrapperError(
242
+ "args['emacs'] must be a command name or an absolute path to an executable"
243
+ )
244
+ return value
245
+
246
+
247
+ def _guarded(root: Path, rel: str, key: str) -> Path:
248
+ """The repo file `rel` (`inside`), refused under `.git/` or `.chipgraph/`, as written
249
+ or as resolved."""
250
+ path = inside(root, rel)
251
+ real = Path(os.path.realpath(path)).relative_to(Path(os.path.realpath(root)))
252
+ for parts in (Path(rel).parts, real.parts):
253
+ folded = [p.casefold() for p in parts]
254
+ if ".git" in folded or (folded and folded[0] in _PROTECTED_DIRS):
255
+ raise WrapperError(f"args[{key!r}] may not name a file under .git/ or .chipgraph/")
256
+ return path
257
+
258
+
259
+ def _read_issue(rel: str, exc: OSError | UnicodeDecodeError) -> Issue:
260
+ why = "not UTF-8" if isinstance(exc, UnicodeDecodeError) else exc.strerror or str(exc)
261
+ return Issue(file=rel, rule="wrap.read", msg=f"cannot read {rel}: {why}")
262
+
263
+
264
+ class WrapCheck:
265
+ """Regenerates each wrapper and compares it with the committed file (see the module)."""
266
+
267
+ id = "wrap_check"
268
+ name = "Wrap check"
269
+
270
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
271
+ start = time.monotonic()
272
+ args = spec.args
273
+ try:
274
+ mode = _mode(args)
275
+ require = _require(args)
276
+ emacs = _emacs(args)
277
+ timeout = _timeout(args)
278
+ resolved = load_profile(ctx.repo_root)
279
+ if resolved is None:
280
+ raise WrapperError("no .chipgraph.yml: the wrappers are listed in the profile")
281
+ profile = resolved.profile
282
+ if ctx.params.get("wrapper"):
283
+ targets = [find_wrapper(profile, ctx.params)]
284
+ else:
285
+ targets = all_wrappers(profile, ctx.params.get("block"))
286
+ except (WrapperError, ConfigError, TypeError, ValueError) as exc:
287
+ return self._result(spec, "error", [Issue(rule="wrap.args", msg=str(exc))], start)
288
+ if not targets:
289
+ note = Issue(severity="info", rule="wrap.none", msg="no wrapper to check here")
290
+ return self._result(spec, "skipped", [note], start)
291
+
292
+ task = bool(ctx.params.get("wrapper"))
293
+ outcomes = [
294
+ await self._one(
295
+ ctx, w, args, mode=mode, task=task, require=require, emacs=emacs, timeout=timeout
296
+ )
297
+ for w in targets
298
+ ]
299
+ issues = [i for o in outcomes for i in o.issues]
300
+ files = [f for o in outcomes for f in o.files]
301
+ log = "\n".join(o.log for o in outcomes if o.log)
302
+ if any(o.infra for o in outcomes):
303
+ status: CheckStatus = "error"
304
+ elif any(i.severity == "error" for i in issues):
305
+ status = "fail"
306
+ else:
307
+ status = "pass"
308
+ return self._result(spec, status, issues, start, files, log)
309
+
310
+ @staticmethod
311
+ def _result(
312
+ spec: CheckSpec,
313
+ status: CheckStatus,
314
+ issues: Sequence[Issue],
315
+ start: float,
316
+ files: Sequence[tuple[str, str]] = (),
317
+ log: str = "",
318
+ ) -> CheckResult:
319
+ return CheckResult(
320
+ check_id=spec.id,
321
+ status=status,
322
+ issues=tuple(issues),
323
+ log_tail=log[-4000:],
324
+ duration_s=time.monotonic() - start,
325
+ idempotency_key=compute_idempotency_key(spec.id, spec.args, files),
326
+ )
327
+
328
+ async def _one(
329
+ self,
330
+ ctx: ToolContext,
331
+ wrapper: Wrapper,
332
+ args: Mapping[str, Any],
333
+ *,
334
+ mode: str,
335
+ task: bool,
336
+ require: bool,
337
+ emacs: str,
338
+ timeout: float,
339
+ ) -> _Outcome:
340
+ root = ctx.repo_root
341
+ out = _Outcome()
342
+ try:
343
+ src_rel, out_rel = paths(wrapper, _opt_str(args, "src"), _opt_str(args, "out"))
344
+ src_path, out_path = _guarded(root, src_rel, "src"), _guarded(root, out_rel, "out")
345
+ vendor = [inside(root, rel) for rel in wrapper.cfg.vendor_files]
346
+ except WrapperError as exc:
347
+ out.issues.append(Issue(rule="wrap.args", msg=str(exc)))
348
+ out.infra = True
349
+ return out
350
+ rerun = f"run the generator (`chipgraph build '{instance_id(GEN_RULE, wrapper)}'`)"
351
+ if not src_path.is_file():
352
+ out.issues.append(
353
+ Issue(file=src_rel, rule="wrap.src_missing", msg=f"no wrapper source at {src_rel}")
354
+ )
355
+ return out
356
+ reading = src_rel
357
+ try:
358
+ source = src_path.read_text(encoding="utf-8")
359
+ out.files.append((src_rel, sha256_file(src_path)))
360
+ for path, rel in zip(vendor, wrapper.cfg.vendor_files, strict=True):
361
+ reading = rel
362
+ if path.is_file():
363
+ out.files.append((rel, sha256_file(path)))
364
+ reading = out_rel
365
+ committed = out_path.read_text(encoding="utf-8") if out_path.is_file() else None
366
+ if committed is not None:
367
+ out.files.append((out_rel, sha256_file(out_path)))
368
+ except (OSError, UnicodeDecodeError) as exc:
369
+ out.issues.append(_read_issue(reading, exc))
370
+ out.infra = isinstance(exc, OSError)
371
+ return out
372
+ for note in lisp.scan(source):
373
+ if note.note:
374
+ out.issues.append(
375
+ Issue(
376
+ file=src_rel,
377
+ line=note.line,
378
+ severity="info",
379
+ rule="wrap.lisp",
380
+ msg=note.message,
381
+ )
382
+ )
383
+
384
+ declaration = extract_declaration(vendor, wrapper.cfg.vendor_module)
385
+ if declaration is None:
386
+ out.infra = True
387
+ out.issues.append(
388
+ Issue(
389
+ rule="wrap.vendor_declaration",
390
+ msg=(
391
+ f"no declaration of module `{wrapper.cfg.vendor_module}` in "
392
+ f"{', '.join(wrapper.cfg.vendor_files)} (the wrapper's vendor_files)"
393
+ ),
394
+ )
395
+ )
396
+ return out
397
+
398
+ current = gen.source_sha(source)
399
+ origin = gen.generated_from(committed) if committed is not None else None
400
+ fresh = committed is not None and origin is not None and origin[1] == current
401
+ expansion = await gen.expand(
402
+ ctx.runner,
403
+ root,
404
+ src_rel,
405
+ wrapper.cfg.vendor_files,
406
+ name=wrapper.name,
407
+ emacs=emacs,
408
+ timeout_s=timeout,
409
+ text=source,
410
+ )
411
+ out.log = expansion.log
412
+ regenerated: str | None = None
413
+ if expansion.ok:
414
+ expanded = expansion.text
415
+ regenerated = gen.render(expanded, src_rel, source)
416
+ elif expansion.infra and {i.rule for i in expansion.issues} <= set(_NO_EMACS):
417
+ marked = committed is not None and verify(committed) is True
418
+ if require or not marked or not fresh:
419
+ if require and "require_emacs" not in args:
420
+ why = (
421
+ "the CI environment variable is set, so emacs is required (the "
422
+ "marker fallback is not a proof; `require_emacs: false` accepts it)"
423
+ )
424
+ elif require:
425
+ why = "the profile requires emacs (require_emacs)"
426
+ elif committed is None:
427
+ why = f"{out_rel} does not exist"
428
+ elif not marked:
429
+ why = f"{out_rel} has no valid generated marker (edited by hand?)"
430
+ else:
431
+ why = f"{out_rel} was not generated from the current source"
432
+ out.infra = True
433
+ out.issues.extend(
434
+ i.model_copy(update={"msg": f"{i.msg}; cannot check {src_rel}: {why}"})
435
+ for i in expansion.issues
436
+ )
437
+ return out
438
+ assert committed is not None
439
+ expanded = gen.body_of(committed)
440
+ out.issues.append(
441
+ Issue(
442
+ file=out_rel,
443
+ severity="warning",
444
+ rule="wrap.not_regenerated",
445
+ msg=(
446
+ f"emacs is not available, so {src_rel} was not regenerated: {out_rel} "
447
+ "is checked by its generated marker and its source's sha256 instead. "
448
+ "The marker is a plain sha256, not a signature: an edit of the body "
449
+ "with a re-rendered header passes here, and only a regeneration "
450
+ f"catches it ({gen.INSTALL_HINT})"
451
+ ),
452
+ )
453
+ )
454
+ else:
455
+ out.infra = expansion.infra
456
+ out.issues.extend(expansion.issues)
457
+ return out
458
+
459
+ src_lines = {i.name: i.line for i in instances(source, declaration.module)}
460
+ out.issues.extend(
461
+ validate(
462
+ expanded,
463
+ declaration,
464
+ file=src_rel,
465
+ line_for=lambda inst: src_lines.get(inst.name),
466
+ wrapper=wrapper.name,
467
+ )
468
+ )
469
+
470
+ wanted = mode == "always" or (mode == "auto" and (not task or fresh))
471
+ if not wanted or regenerated is None:
472
+ return out
473
+ if committed is None:
474
+ out.issues.append(
475
+ Issue(
476
+ file=out_rel,
477
+ rule="wrap.not_generated",
478
+ msg=f"{out_rel} does not exist; {rerun} and commit the result",
479
+ )
480
+ )
481
+ return out
482
+ out.issues.extend(compare(committed, regenerated, out_rel, rerun))
483
+ return out
484
+
485
+
486
+ __all__ = ["COMPARE_MODES", "MAX_TIMEOUT_S", "WrapCheck", "compare"]