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,311 @@
1
+ """`NamingCheck`: identifiers follow the project's naming rule, classified by pyslang.
2
+
3
+ The rule is data (`org:qnsc/naming-v1.yml`, a `chipgraph.checks._naming_rules.NamingRules`),
4
+ not code: per object kind an allowed pattern, plus lexical and vocabulary rules. The
5
+ check parses each in-scope SystemVerilog file into a pyslang **syntax tree** (no
6
+ elaboration, so a single file with missing packages still parses), classifies every
7
+ identifier the file *declares* by its syntax node kind (module, port, parameter vs
8
+ localparam, enum member, instance, signal vs memory, genvar), and checks each name
9
+ against the rule for its kind and the lexical/vocabulary rules. A regex over text cannot
10
+ tell a port from a signal; pyslang can, so this is more precise than the reference
11
+ text checker it is modelled on.
12
+
13
+ Example profile usage (`.chipgraph.yml`):
14
+
15
+ ```yaml
16
+ adapters:
17
+ naming: { use: naming, rules: "org:qnsc/naming-v1.yml", scope: ["design/{block}/rtl/**/*.sv"] }
18
+ ```
19
+
20
+ `{block}` in a scope glob is substituted from `ctx.params["block"]`. `ignore` globs
21
+ (default: any path with a `vendor` segment) drop files that keep their upstream's
22
+ conventions. A per-declaration exemption comment (`// naming-check: ignore`) on the
23
+ declaration line suppresses that line. A parse error becomes an `error` issue for the
24
+ file, never an exception, and the names pyslang recovered around it are still checked;
25
+ a missing rules file or bad rules ref is a whole-check `error`.
26
+
27
+ Code in an inactive `` `ifdef `` branch is not seen: the file is preprocessed without the
28
+ project's defines, so names there are checked only when that branch is active.
29
+
30
+ Regenerate `schemas/formats/naming-rules.schema.json` with `python -m chipgraph.checks.naming`.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ import time
37
+ from collections.abc import Iterable
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ import yaml
42
+ from pydantic import ValidationError
43
+
44
+ from chipgraph.checks._common import (
45
+ ArgError,
46
+ compute_idempotency_key,
47
+ error_result,
48
+ glob_to_regex,
49
+ iter_repo_files,
50
+ optional_list_of_str,
51
+ require_list_of_str,
52
+ require_str,
53
+ sha256_bytes,
54
+ sha256_file,
55
+ )
56
+ from chipgraph.checks._naming_pyslang import (
57
+ Decl,
58
+ ParseFailure,
59
+ declared_identifiers,
60
+ parse_errors,
61
+ parse_file,
62
+ )
63
+ from chipgraph.checks._naming_rules import LexicalRule, NamingRules, VocabularyRule
64
+ from chipgraph.core.config.loader import resolve_data_ref
65
+ from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
66
+ from chipgraph.core.contracts.types import CheckStatus
67
+ from chipgraph.core.plugin_api.types import ToolContext
68
+
69
+ _DEFAULT_IGNORE = ("**/vendor/**", "vendor/**")
70
+ """By default, never check anything under a `vendor` path segment (D: it keeps upstream
71
+ conventions and is not ours to rename)."""
72
+
73
+
74
+ class NamingCheck:
75
+ """Checks that declared identifiers follow the project's data-defined naming rule."""
76
+
77
+ id = "naming"
78
+ name = "Naming"
79
+
80
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
81
+ start = time.monotonic()
82
+ args = spec.args
83
+
84
+ try:
85
+ rules_ref = require_str(args, "rules")
86
+ scope = _require_scope(args)
87
+ ignore = optional_list_of_str(args, "ignore") or list(_DEFAULT_IGNORE)
88
+ block = ctx.params.get("block")
89
+ scope = [_substitute(glob, block) for glob in scope]
90
+ rules_path = _resolve_rules_path(rules_ref, ctx.repo_root)
91
+ rules, rules_bytes = _load_rules(rules_path)
92
+ except (ArgError, _RulesError) as exc:
93
+ return error_result(spec, str(exc), start)
94
+
95
+ scope_regexes = [glob_to_regex(p) for p in scope]
96
+ ignore_regexes = [glob_to_regex(p) for p in ignore]
97
+ in_scope = [
98
+ f
99
+ for f in iter_repo_files(ctx.repo_root)
100
+ if any(r.fullmatch(f) for r in scope_regexes)
101
+ and not any(r.fullmatch(f) for r in ignore_regexes)
102
+ ]
103
+
104
+ compiled = _CompiledRules(rules)
105
+ issues: list[Issue] = []
106
+ for rel in in_scope:
107
+ issues.extend(_check_file(ctx.repo_root / rel, rel, compiled))
108
+ issues.sort(key=lambda i: (i.file or "", i.line or 0, i.rule))
109
+
110
+ file_hashes = [(f, sha256_file(ctx.repo_root / f)) for f in in_scope]
111
+ file_hashes.append(("<rules>", sha256_bytes(rules_bytes)))
112
+ key = compute_idempotency_key(spec.id, args, file_hashes)
113
+ status: CheckStatus = "fail" if issues else "pass"
114
+ return CheckResult(
115
+ check_id=spec.id,
116
+ status=status,
117
+ issues=tuple(issues),
118
+ duration_s=time.monotonic() - start,
119
+ idempotency_key=key,
120
+ )
121
+
122
+
123
+ # --------------------------------------------------------------------------------------
124
+ # argument and rule loading
125
+ # --------------------------------------------------------------------------------------
126
+
127
+
128
+ class _RulesError(Exception):
129
+ """Raised when the rules ref cannot be resolved or the rules file is invalid."""
130
+
131
+
132
+ def _require_scope(args: Any) -> list[str]:
133
+ scope = require_list_of_str(args, "scope")
134
+ if not scope:
135
+ raise ArgError("args['scope'] must list at least one glob")
136
+ return scope
137
+
138
+
139
+ def _substitute(glob: str, block: str | None) -> str:
140
+ """Substitute `{block}` in a scope glob from `ctx.params`, or raise if it is unset."""
141
+ if "{block}" not in glob:
142
+ return glob
143
+ if block is None:
144
+ raise ArgError(f"scope glob {glob!r} uses {{block}} but no block is set in ctx.params")
145
+ return glob.replace("{block}", block)
146
+
147
+
148
+ def _resolve_rules_path(ref: str, repo_root: Path) -> Path:
149
+ try:
150
+ return resolve_data_ref(ref, repo_root)
151
+ except Exception as exc:
152
+ raise _RulesError(f"cannot resolve rules ref {ref!r}: {exc}") from exc
153
+
154
+
155
+ def _load_rules(path: Path) -> tuple[NamingRules, bytes]:
156
+ try:
157
+ raw_bytes = path.read_bytes()
158
+ except OSError as exc:
159
+ raise _RulesError(f"cannot read rules file {path}: {exc}") from exc
160
+ try:
161
+ data = yaml.safe_load(raw_bytes)
162
+ except yaml.YAMLError as exc:
163
+ raise _RulesError(f"invalid YAML in rules file {path}: {exc}") from exc
164
+ try:
165
+ return NamingRules.model_validate(data), raw_bytes
166
+ except ValidationError as exc:
167
+ first = exc.errors()[0]
168
+ loc = ".".join(str(p) for p in first["loc"])
169
+ raise _RulesError(f"invalid rules file {path}: {loc}: {first['msg']}") from exc
170
+
171
+
172
+ # --------------------------------------------------------------------------------------
173
+ # compiled rules and file checking
174
+ # --------------------------------------------------------------------------------------
175
+
176
+
177
+ class _CompiledRules:
178
+ """`NamingRules` with its regexes compiled once, ready to check names against."""
179
+
180
+ def __init__(self, rules: NamingRules) -> None:
181
+ self.rules = rules
182
+ self.kind_patterns: dict[str, tuple[str, re.Pattern[str], str]] = {
183
+ str(kind): (rule.rule, re.compile(rule.pattern), rule.message)
184
+ for kind, rule in rules.identifiers.items()
185
+ }
186
+ self.lexical = [_CompiledLexical(lx) for lx in rules.lexical]
187
+ self.vocabulary = _CompiledVocabulary(rules.vocabulary) if rules.vocabulary else None
188
+ self.ignore = re.compile(r"//\s*" + re.escape(rules.ignore_comment))
189
+
190
+
191
+ class _CompiledLexical:
192
+ def __init__(self, rule: LexicalRule) -> None:
193
+ self.rule = rule.rule
194
+ self.message = rule.message
195
+ self.forbid = re.compile(rule.forbid_pattern)
196
+ self.allow = re.compile(rule.allow_if_matches) if rule.allow_if_matches else None
197
+
198
+ def violated_by(self, name: str) -> bool:
199
+ if self.allow is not None and self.allow.fullmatch(name):
200
+ return False
201
+ return self.forbid.search(name) is not None
202
+
203
+
204
+ class _CompiledVocabulary:
205
+ def __init__(self, rule: VocabularyRule) -> None:
206
+ self.rule = rule.rule
207
+ self.message = rule.message
208
+ self.replace = dict(rule.replace)
209
+
210
+ def offending_word(self, name: str) -> tuple[str, str] | None:
211
+ low = name.lower()
212
+ for bad, good in self.replace.items():
213
+ if re.search(rf"(?:^|_){re.escape(bad)}(?:_|$)", low):
214
+ return bad, good
215
+ return None
216
+
217
+
218
+ def _check_file(path: Path, rel: str, rules: _CompiledRules) -> list[Issue]:
219
+ try:
220
+ text = path.read_text(errors="ignore")
221
+ tree = parse_file(path)
222
+ except OSError as exc:
223
+ return [Issue(file=rel, rule="parse", severity="error", msg=f"cannot read file: {exc}")]
224
+
225
+ # A parse error is reported, but the names pyslang still recovered are checked too:
226
+ # one unexpanded vendor macro must not hide every naming violation in the file.
227
+ issues: list[Issue] = [_parse_issue(rel, f) for f in parse_errors(tree)]
228
+ exempt_lines = _exempt_lines(text, rules.ignore)
229
+ for decl in declared_identifiers(tree):
230
+ if decl.line in exempt_lines:
231
+ continue
232
+ issues.extend(_check_decl(rel, decl, rules))
233
+ return issues
234
+
235
+
236
+ def _parse_issue(rel: str, failure: ParseFailure) -> Issue:
237
+ return Issue(
238
+ file=rel,
239
+ line=failure.line,
240
+ rule="parse",
241
+ severity="error",
242
+ msg=f"parse error: {failure.message}",
243
+ )
244
+
245
+
246
+ def _exempt_lines(text: str, ignore: re.Pattern[str]) -> frozenset[int]:
247
+ return frozenset(i for i, line in enumerate(text.splitlines(), start=1) if ignore.search(line))
248
+
249
+
250
+ def _check_decl(rel: str, decl: Decl, rules: _CompiledRules) -> Iterable[Issue]:
251
+ issues: list[Issue] = []
252
+ kind_rule = rules.kind_patterns.get(decl.kind)
253
+ if kind_rule is not None:
254
+ rule_id, pattern, message = kind_rule
255
+ if not pattern.fullmatch(decl.name):
256
+ issues.append(
257
+ Issue(
258
+ file=rel,
259
+ line=decl.line,
260
+ rule=rule_id,
261
+ severity="error",
262
+ msg=f"{decl.kind} '{decl.name}' {message}",
263
+ )
264
+ )
265
+
266
+ for lx in rules.lexical:
267
+ if lx.violated_by(decl.name):
268
+ issues.append(
269
+ Issue(
270
+ file=rel,
271
+ line=decl.line,
272
+ rule=lx.rule,
273
+ severity="error",
274
+ msg=f"'{decl.name}' {lx.message}",
275
+ )
276
+ )
277
+
278
+ if rules.vocabulary is not None:
279
+ hit = rules.vocabulary.offending_word(decl.name)
280
+ if hit is not None:
281
+ bad, good = hit
282
+ issues.append(
283
+ Issue(
284
+ file=rel,
285
+ line=decl.line,
286
+ rule=rules.vocabulary.rule,
287
+ severity="error",
288
+ msg=f"'{decl.name}' uses '{bad}'; {rules.vocabulary.message} '{good}'",
289
+ )
290
+ )
291
+ return issues
292
+
293
+
294
+ def main() -> None:
295
+ """Write the naming-rules JSON Schema (`python -m chipgraph.checks.naming [PATH]`)."""
296
+ import sys
297
+
298
+ from chipgraph.checks._naming_rules import schema_json
299
+
300
+ out = (
301
+ Path(sys.argv[1]) if len(sys.argv) > 1 else Path("schemas/formats/naming-rules.schema.json")
302
+ )
303
+ out.write_text(schema_json(), encoding="utf-8")
304
+ print(f"wrote {out}")
305
+
306
+
307
+ if __name__ == "__main__":
308
+ main()
309
+
310
+
311
+ __all__ = ["NamingCheck"]