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,340 @@
1
+ """Block-by-block Jinja2 template overrides (DESIGN 8.6 V2, docs/IMPLEMENTATION_PLAN.md M1-22).
2
+
3
+ A `TemplateSet` renders named templates (e.g. `rtl_module.sv.j2`) built from an ordered
4
+ stack of layers, most specific first. DESIGN.md's lookup order for a template is::
5
+
6
+ block -> project -> organisation -> pack
7
+
8
+ ("block" here is the project's own per-IP-block layer, DESIGN 8.6's `blocks:` section;
9
+ "pack" is the lowest, always-present fallback.) Each layer may contribute, for a given
10
+ template name:
11
+
12
+ * a **whole-file** template: a file named exactly like the template, in that layer's
13
+ ``directory``, whose top level has real content (not just `{% block %}` tags). The most
14
+ specific layer that has a whole file for a name is the base of the render.
15
+ * a **block-only fragment** that overrides individual `{% block %}` regions without
16
+ touching the rest of the file. Two conventions produce a fragment, and both may be used
17
+ together:
18
+
19
+ 1. **Same file name, in a more specific layer's directory**, containing *only*
20
+ `{% block ... %}...{% endblock %}` definitions (no other top-level output). Detected
21
+ automatically by parsing the file: if every top-level node is a block definition or
22
+ whitespace, it is a fragment, not a whole-file replacement.
23
+ 2. **`templates.override` mapping** (`ResolvedProfile.for_block(...).profile.templates
24
+ .override`, or any other `Mapping[str, str]` a caller assembles the same way): a
25
+ block name (not necessarily the template's file name) to a file path containing that
26
+ one block's `{% block <name> %}...{% endblock %}` definition. This is what DESIGN.md's
27
+ example ``templates: override: { rtl_header: templates/header.j2 }`` means: the key is
28
+ the *block name* `rtl_header`, not a template file name, so it applies to any template
29
+ in the chain that defines a block called `rtl_header`.
30
+
31
+ Both conventions are implemented with real Jinja2 inheritance: each more specific layer's
32
+ contribution is compiled as a virtual template `layer:<n>/<name>` that `{% extends %}` the
33
+ next less specific layer's virtual template. A block a more specific layer does not touch
34
+ is therefore untouched in the rendered output -- it simply falls through Jinja's own block
35
+ resolution. This is also why overriding one block never changes any other block's source
36
+ or rendering: each block's *content* lives in exactly one layer's fragment/whole file.
37
+
38
+ Gap vs. `core/config/models.py`: `BlockOverride` (the project's own `blocks:` per-IP-block
39
+ section) has no `templates` field, so a project cannot currently express a block-scoped
40
+ template override through the resolved `Profile` alone -- only a project-wide
41
+ `templates.override`. `TemplateSet` accepts a "block" layer as plain data (an optional
42
+ directory and/or override mapping) so a caller can still supply one (e.g. read from
43
+ `.chipgraph/blocks/<block>/templates/`, by convention, until `models.py` grows a field);
44
+ this module does not itself decide where a per-block layer's directory or overrides come
45
+ from.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from collections.abc import Callable, Mapping, Sequence
51
+ from dataclasses import dataclass, field
52
+ from pathlib import Path
53
+
54
+ from jinja2 import BaseLoader, Environment, StrictUndefined, nodes
55
+ from jinja2.exceptions import TemplateNotFound as Jinja2TemplateNotFound
56
+ from jinja2.exceptions import UndefinedError
57
+ from jinja2.sandbox import SandboxedEnvironment
58
+
59
+ __all__ = [
60
+ "BlockSource",
61
+ "TemplateLayer",
62
+ "TemplateNotFoundError",
63
+ "TemplateRenderError",
64
+ "TemplateSet",
65
+ "TemplateSetError",
66
+ ]
67
+
68
+ _VIRTUAL_PREFIX = "layer:"
69
+
70
+
71
+ class TemplateSetError(Exception):
72
+ """Base error for `TemplateSet` problems."""
73
+
74
+
75
+ class TemplateNotFoundError(TemplateSetError):
76
+ """No layer contributes anything (whole file or fragment) for a template name."""
77
+
78
+ def __init__(self, name: str) -> None:
79
+ super().__init__(f"template {name!r} not found in any layer")
80
+ self.name = name
81
+
82
+
83
+ class TemplateRenderError(TemplateSetError):
84
+ """Rendering failed; names the template and the layer chain that produced it."""
85
+
86
+ def __init__(self, name: str, layers: Sequence[str], cause: Exception) -> None:
87
+ chain = " -> ".join(layers) if layers else "(no layer)"
88
+ super().__init__(
89
+ f"error rendering template {name!r} (layer chain, least to most specific: "
90
+ f"{chain}): {cause}"
91
+ )
92
+ self.name = name
93
+ self.layers = tuple(layers)
94
+ self.__cause__ = cause
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class TemplateLayer:
99
+ """One layer in the lookup stack: a directory of whole/fragment files, and/or an
100
+ explicit block-name-to-path override mapping (DESIGN 8.6 V2's `templates.override`).
101
+
102
+ `name` is a short label used in `explain()` output and error messages, e.g. "block",
103
+ "project", "org", "pack".
104
+ """
105
+
106
+ name: str
107
+ directory: Path | None = None
108
+ override: Mapping[str, str] = field(default_factory=dict)
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class BlockSource:
113
+ """Which layer (and file, if any) a rendered block's content came from."""
114
+
115
+ block: str
116
+ layer: str
117
+ path: Path | None
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class _Contribution:
122
+ """One layer's compiled contribution to a template's inheritance chain."""
123
+
124
+ layer_name: str
125
+ source: str
126
+ is_root: bool # True: a whole-file base/replacement (does not extend the chain so far)
127
+ block_paths: dict[str, Path | None]
128
+
129
+
130
+ def _read(path: str | Path) -> str:
131
+ p = Path(path)
132
+ try:
133
+ return p.read_text(encoding="utf-8")
134
+ except OSError as exc:
135
+ raise TemplateSetError(f"cannot read template file {p}: {exc}") from exc
136
+
137
+
138
+ def _is_pure_fragment(source: str, parser_env: Environment, origin: str) -> bool:
139
+ """True if `source`'s top level has only `{% block %}` tags and/or whitespace text."""
140
+ try:
141
+ parsed = parser_env.parse(source)
142
+ except Exception as exc:
143
+ raise TemplateSetError(f"cannot parse template fragment {origin}: {exc}") from exc
144
+ for top in parsed.body:
145
+ if isinstance(top, nodes.Block):
146
+ continue
147
+ if isinstance(top, nodes.Output) and all(
148
+ isinstance(child, nodes.TemplateData) and child.data.strip() == ""
149
+ for child in top.nodes
150
+ ):
151
+ continue
152
+ return False
153
+ return True
154
+
155
+
156
+ def _block_names(source: str, parser_env: Environment, origin: str) -> list[str]:
157
+ try:
158
+ parsed = parser_env.parse(source)
159
+ except Exception as exc:
160
+ raise TemplateSetError(f"cannot parse template fragment {origin}: {exc}") from exc
161
+ return [b.name for b in parsed.find_all(nodes.Block)]
162
+
163
+
164
+ class _LayeredLoader(BaseLoader):
165
+ """Serves the virtual `layer:<n>/<name>` sources a `TemplateSet` compiles."""
166
+
167
+ def __init__(self) -> None:
168
+ self._sources: dict[str, str] = {}
169
+
170
+ def add(self, virtual_name: str, source: str) -> None:
171
+ self._sources[virtual_name] = source
172
+
173
+ def get_source(
174
+ self, environment: Environment, template: str
175
+ ) -> tuple[str, str | None, Callable[[], bool] | None]:
176
+ try:
177
+ source = self._sources[template]
178
+ except KeyError as exc:
179
+ raise Jinja2TemplateNotFound(template) from exc
180
+ return source, None, lambda: True
181
+
182
+
183
+ class TemplateSet:
184
+ """Renders templates built from ordered layers (most specific first)."""
185
+
186
+ def __init__(self, layers: Sequence[TemplateLayer]) -> None:
187
+ self._layers: tuple[TemplateLayer, ...] = tuple(layers)
188
+ self._loader = _LayeredLoader()
189
+ self._env = SandboxedEnvironment(
190
+ loader=self._loader,
191
+ undefined=StrictUndefined,
192
+ autoescape=False,
193
+ keep_trailing_newline=True,
194
+ )
195
+ # A plain (non-sandboxed) environment used only to parse candidate sources when
196
+ # deciding whether they are whole files or block-only fragments, and to list the
197
+ # block names they define. No template of ours is *rendered* through it.
198
+ self._parser_env = Environment()
199
+ self._chains: dict[str, tuple[list[str], dict[str, BlockSource]]] = {}
200
+
201
+ # -- chain construction --------------------------------------------------------
202
+
203
+ def _gather_layer_candidates(
204
+ self, layer: TemplateLayer, name: str
205
+ ) -> tuple[str | None, Path | None, dict[str, str]]:
206
+ """Return (whole_or_fragment_source_for_name, its_path, {block_name: path}) for
207
+ one layer's directory file plus any `override` entries keyed by `name` itself."""
208
+ whole_source: str | None = None
209
+ whole_path: Path | None = None
210
+
211
+ if name in layer.override:
212
+ whole_path = Path(layer.override[name])
213
+ whole_source = _read(whole_path)
214
+ elif layer.directory is not None:
215
+ candidate = layer.directory / name
216
+ if candidate.is_file():
217
+ whole_path = candidate
218
+ whole_source = _read(candidate)
219
+
220
+ block_overrides = {
221
+ block_name: path for block_name, path in layer.override.items() if block_name != name
222
+ }
223
+ return whole_source, whole_path, block_overrides
224
+
225
+ def _build_contribution(self, layer: TemplateLayer, name: str) -> _Contribution | None:
226
+ whole_source, whole_path, block_override_paths = self._gather_layer_candidates(layer, name)
227
+
228
+ pieces: list[str] = []
229
+ block_paths: dict[str, Path | None] = {}
230
+ is_root = False
231
+
232
+ if whole_source is not None:
233
+ origin = str(whole_path)
234
+ if _is_pure_fragment(whole_source, self._parser_env, origin):
235
+ pieces.append(whole_source)
236
+ for block_name in _block_names(whole_source, self._parser_env, origin):
237
+ block_paths[block_name] = whole_path
238
+ else:
239
+ # A whole (non-fragment) file: it is the base, replacing anything less
240
+ # specific accumulated so far.
241
+ pieces = [whole_source]
242
+ for block_name in _block_names(whole_source, self._parser_env, origin):
243
+ block_paths[block_name] = whole_path
244
+ is_root = True
245
+
246
+ for block_name, path_str in block_override_paths.items():
247
+ path = Path(path_str)
248
+ source = _read(path)
249
+ origin = str(path)
250
+ if not _is_pure_fragment(source, self._parser_env, origin):
251
+ raise TemplateSetError(
252
+ f"templates.override[{block_name!r}] = {origin!r} must contain only "
253
+ "{% block %} definitions, not whole-template content"
254
+ )
255
+ pieces.append(source)
256
+ for found in _block_names(source, self._parser_env, origin):
257
+ block_paths[found] = path
258
+
259
+ if not pieces:
260
+ return None
261
+
262
+ return _Contribution(
263
+ layer_name=layer.name,
264
+ source="\n".join(pieces),
265
+ is_root=is_root,
266
+ block_paths=block_paths,
267
+ )
268
+
269
+ def _build_chain(self, name: str) -> tuple[list[str], dict[str, BlockSource]]:
270
+ cached = self._chains.get(name)
271
+ if cached is not None:
272
+ return cached
273
+
274
+ # Layers are stored most-specific-first; process least-specific-first so each
275
+ # more specific contribution can `{% extends %}` the previous virtual template.
276
+ layers_asc = list(reversed(self._layers))
277
+
278
+ parent_virtual: str | None = None
279
+ chain_layer_names: list[str] = []
280
+ block_sources: dict[str, BlockSource] = {}
281
+ index = 0
282
+
283
+ for layer in layers_asc:
284
+ contribution = self._build_contribution(layer, name)
285
+ if contribution is None:
286
+ continue
287
+
288
+ if contribution.is_root:
289
+ # Resets the chain: this whole file replaces anything accumulated so far.
290
+ source = contribution.source
291
+ parent_virtual = None
292
+ chain_layer_names = []
293
+ else:
294
+ if parent_virtual is None:
295
+ raise TemplateSetError(
296
+ f"layer {layer.name!r} overrides block(s) "
297
+ f"{sorted(contribution.block_paths)} of template {name!r}, but no "
298
+ "less specific layer provides a whole (base) file for it"
299
+ )
300
+ source = f'{{% extends "{parent_virtual}" %}}\n{contribution.source}'
301
+
302
+ virtual_name = f"{_VIRTUAL_PREFIX}{index}/{name}"
303
+ self._loader.add(virtual_name, source)
304
+ index += 1
305
+ parent_virtual = virtual_name
306
+ chain_layer_names.append(layer.name)
307
+
308
+ for block_name, path in contribution.block_paths.items():
309
+ block_sources[block_name] = BlockSource(
310
+ block=block_name, layer=layer.name, path=path
311
+ )
312
+
313
+ if parent_virtual is None:
314
+ raise TemplateNotFoundError(name)
315
+
316
+ # Publish the final, most specific virtual template under the public name too.
317
+ self._loader.add(name, f'{{% extends "{parent_virtual}" %}}\n')
318
+
319
+ result = (chain_layer_names, block_sources)
320
+ self._chains[name] = result
321
+ return result
322
+
323
+ # -- public API ------------------------------------------------------------------
324
+
325
+ def render(self, name: str, context: Mapping[str, object] | None = None) -> str:
326
+ """Render template `name` against `context`. Deterministic, no implicit state."""
327
+ chain_layers, _ = self._build_chain(name)
328
+ template = self._env.get_template(name)
329
+ try:
330
+ return template.render(dict(context) if context is not None else {})
331
+ except UndefinedError as exc:
332
+ raise TemplateRenderError(name, chain_layers, exc) from exc
333
+
334
+ def explain(self, name: str) -> list[tuple[str, str, Path | None]]:
335
+ """Which layer (and file, if any) each block of `name` was resolved from.
336
+
337
+ Returns a list of `(block_name, layer_name, path)`, sorted by block name.
338
+ """
339
+ _, block_sources = self._build_chain(name)
340
+ return sorted((b.block, b.layer, b.path) for b in block_sources.values())
@@ -0,0 +1,75 @@
1
+ """Core contracts: the pydantic models every other part of chipgraph builds against.
2
+
3
+ `chipgraph.core.contracts` knows nothing about chips or tools: no project, bus, PDK or
4
+ tool names appear here.
5
+ """
6
+
7
+ from chipgraph.core.contracts.agent import AgentResult, Decision
8
+ from chipgraph.core.contracts.artifact import Artifact, ArtifactRef, ProducedBy
9
+ from chipgraph.core.contracts.check import CheckResult, CheckSpec, Issue
10
+ from chipgraph.core.contracts.event import Event, EventType, RunManifest
11
+ from chipgraph.core.contracts.finding import Evidence, Finding, FindingSeverity, FindingStatus
12
+ from chipgraph.core.contracts.gate import Approval, GateSpec
13
+ from chipgraph.core.contracts.rule import Budget, InputSpec, RuleInstance, RuleSpec, RunSpec
14
+ from chipgraph.core.contracts.types import (
15
+ ArtifactKind,
16
+ CheckStatus,
17
+ DataLabel,
18
+ FailureLabel,
19
+ ModelTier,
20
+ RuleId,
21
+ RuleKind,
22
+ Severity,
23
+ Sha256,
24
+ )
25
+
26
+ __all__ = [
27
+ "AgentResult",
28
+ "Approval",
29
+ "Artifact",
30
+ "ArtifactKind",
31
+ "ArtifactRef",
32
+ "Budget",
33
+ "CheckResult",
34
+ "CheckSpec",
35
+ "CheckStatus",
36
+ "DataLabel",
37
+ "Decision",
38
+ "Event",
39
+ "EventType",
40
+ "Evidence",
41
+ "FailureLabel",
42
+ "Finding",
43
+ "FindingSeverity",
44
+ "FindingStatus",
45
+ "GateSpec",
46
+ "InputSpec",
47
+ "Issue",
48
+ "ModelTier",
49
+ "ProducedBy",
50
+ "RuleId",
51
+ "RuleInstance",
52
+ "RuleKind",
53
+ "RuleSpec",
54
+ "RunManifest",
55
+ "RunSpec",
56
+ "Severity",
57
+ "Sha256",
58
+ ]
59
+
60
+ TOP_LEVEL_MODELS = (
61
+ ArtifactRef,
62
+ Artifact,
63
+ RuleSpec,
64
+ RuleInstance,
65
+ CheckSpec,
66
+ CheckResult,
67
+ GateSpec,
68
+ Approval,
69
+ Event,
70
+ RunManifest,
71
+ AgentResult,
72
+ Decision,
73
+ Finding,
74
+ )
75
+ """Top-level models: each gets its own file in `schemas/` (see export.py)."""
@@ -0,0 +1,26 @@
1
+ """Shared path validation for artifact and rule-output paths.
2
+
3
+ Not part of the public API of this package (no leading re-export in ``__init__``).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ def validate_relative_path(path: str, *, field_name: str = "path") -> str:
10
+ """Validate a POSIX path relative to a repo root; return it unchanged if valid.
11
+
12
+ The path must not be empty, must not start with ``/`` (absolute), must not
13
+ contain backslashes, and no segment may be ``.`` or ``..``. Placeholders such
14
+ as ``{block}`` are allowed inside segments (used by rule output templates).
15
+ """
16
+ if path == "":
17
+ raise ValueError(f"{field_name} must not be empty")
18
+ if path.startswith("/"):
19
+ raise ValueError(f"{field_name} must be relative, got absolute path {path!r}")
20
+ if "\\" in path:
21
+ raise ValueError(f"{field_name} must be a POSIX path, got {path!r}")
22
+ segments = path.split("/")
23
+ for segment in segments:
24
+ if segment in ("", ".", ".."):
25
+ raise ValueError(f"{field_name} must not contain '.', '..' or empty segments: {path!r}")
26
+ return path
@@ -0,0 +1,49 @@
1
+ """Agent results and decisions: the output of AI agent rules and their sub-decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+
10
+ class AgentResult(BaseModel):
11
+ """The outcome of running an agent rule."""
12
+
13
+ model_config = ConfigDict(extra="forbid", frozen=True)
14
+
15
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
16
+ status: Literal["done", "failed", "needs_human", "budget_exhausted"] = Field(
17
+ description="The final status of the agent run."
18
+ )
19
+ files_written: tuple[str, ...] = Field(
20
+ default=(), description="Paths of files the agent wrote."
21
+ )
22
+ assumptions: tuple[str, ...] = Field(
23
+ default=(), description="Assumptions the agent made while working."
24
+ )
25
+ open_questions: tuple[str, ...] = Field(
26
+ default=(), description="Questions the agent could not resolve on its own."
27
+ )
28
+ tokens: int | None = Field(default=None, ge=0, description="Tokens consumed by the agent run.")
29
+ cost: float | None = Field(default=None, ge=0, description="Cost of the agent run, in USD.")
30
+
31
+ @model_validator(mode="after")
32
+ def _check_needs_human_has_question(self) -> Self:
33
+ if self.status == "needs_human" and len(self.open_questions) < 1:
34
+ raise ValueError("status='needs_human' requires at least one open question")
35
+ return self
36
+
37
+
38
+ class Decision(BaseModel):
39
+ """A single resolved decision, made by a rule, a small model, or a large model."""
40
+
41
+ model_config = ConfigDict(extra="forbid", frozen=True)
42
+
43
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
44
+ question_id: str = Field(description="The id of the question this decision answers.")
45
+ value: str | int | float | bool = Field(description="The decided value.")
46
+ confidence: float = Field(ge=0, le=1, description="Confidence in this decision, from 0 to 1.")
47
+ backend: Literal["rule", "small", "large"] = Field(
48
+ description="What made this decision: a rule, a small model, or a large model."
49
+ )
@@ -0,0 +1,68 @@
1
+ """Artifacts: the nodes of the build graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from chipgraph.core.contracts._paths import validate_relative_path
10
+ from chipgraph.core.contracts.types import ArtifactKind, DataLabel, RuleId, Sha256
11
+
12
+
13
+ class ArtifactRef(BaseModel):
14
+ """A reference to an artifact, identifying it without describing its content."""
15
+
16
+ model_config = ConfigDict(extra="forbid", frozen=True)
17
+
18
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
19
+ repo: str = Field(
20
+ default=".", description="The workspace member (repo) id this artifact belongs to."
21
+ )
22
+ kind: ArtifactKind = Field(description="The kind of artifact this is, e.g. rtl, spec, report.")
23
+ path: str | None = Field(
24
+ default=None,
25
+ description="POSIX path to the artifact, relative to the repo root, when it is file-based.",
26
+ )
27
+ model_key: str | None = Field(
28
+ default=None,
29
+ description="Key into the Design Model when this artifact is model-based, not a file.",
30
+ )
31
+ label: DataLabel = Field(
32
+ default="internal", description="Data sensitivity label: public, internal, or nda."
33
+ )
34
+
35
+ @model_validator(mode="after")
36
+ def _check_exactly_one_locator(self) -> Self:
37
+ if (self.path is None) == (self.model_key is None):
38
+ raise ValueError("ArtifactRef needs exactly one of 'path' or 'model_key' to be set")
39
+ if self.path is not None:
40
+ validate_relative_path(self.path, field_name="path")
41
+ return self
42
+
43
+
44
+ class ProducedBy(BaseModel):
45
+ """Records which rule instance and run produced an artifact."""
46
+
47
+ model_config = ConfigDict(extra="forbid", frozen=True)
48
+
49
+ rule_id: RuleId = Field(description="The id of the rule that produced this artifact.")
50
+ run_id: str = Field(description="The id of the run in which this artifact was produced.")
51
+
52
+
53
+ class Artifact(BaseModel):
54
+ """A concrete artifact: a reference plus its content hash and provenance."""
55
+
56
+ model_config = ConfigDict(extra="forbid", frozen=True)
57
+
58
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
59
+ ref: ArtifactRef = Field(description="The reference identifying this artifact.")
60
+ content_hash: Sha256 = Field(description="SHA-256 hash of the artifact's content.")
61
+ produced_by: ProducedBy | None = Field(
62
+ default=None,
63
+ description="Which rule and run produced this; None if human-written or pre-existing.",
64
+ )
65
+ inputs_hash: Sha256 | None = Field(
66
+ default=None,
67
+ description="Hash of the inputs this was produced from; None when not produced by a rule.",
68
+ )
@@ -0,0 +1,76 @@
1
+ """Checks: deterministic verification of artifacts, and their specs and results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from chipgraph.core.contracts.types import CheckStatus, Severity
10
+
11
+ _LOG_TAIL_MAX_CHARS = 4000
12
+ _TRUNCATION_MARKER = "…"
13
+
14
+
15
+ class CheckSpec(BaseModel):
16
+ """The specification of a check: which adapter runs it and with what arguments."""
17
+
18
+ model_config = ConfigDict(extra="forbid", frozen=True)
19
+
20
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
21
+ id: str = Field(description="The id of this check.")
22
+ capability: str = Field(description="The capability this check exercises, e.g. lint, sim.")
23
+ adapter: str = Field(description="The adapter id that implements this check.")
24
+ args: dict[str, Any] = Field(default={}, description="Arguments passed to the adapter.")
25
+
26
+
27
+ class Issue(BaseModel):
28
+ """A single issue found by a check, such as a lint violation."""
29
+
30
+ model_config = ConfigDict(extra="forbid", frozen=True)
31
+
32
+ file: str | None = Field(default=None, description="The file this issue was found in, if any.")
33
+ line: int | None = Field(default=None, ge=1, description="The 1-based line number, if any.")
34
+ rule: str = Field(default="", description="The specific rule within the check that fired.")
35
+ severity: Severity = Field(default="error", description="How serious this issue is.")
36
+ msg: str = Field(description="A human-readable message describing the issue.")
37
+
38
+
39
+ class CheckResult(BaseModel):
40
+ """The outcome of running a check."""
41
+
42
+ model_config = ConfigDict(extra="forbid", frozen=True)
43
+
44
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
45
+ check_id: str = Field(description="The id of the check that was run.")
46
+ status: CheckStatus = Field(description="The overall outcome of the check.")
47
+ issues: tuple[Issue, ...] = Field(
48
+ default=(), description="Issues found while running the check."
49
+ )
50
+ log_tail: str = Field(
51
+ default="", description="The tail of the check's log output, truncated to 4000 characters."
52
+ )
53
+ duration_s: float = Field(ge=0, description="How long the check took to run, in seconds.")
54
+ idempotency_key: str = Field(
55
+ description="A key identifying repeat runs of the same check invocation."
56
+ )
57
+
58
+ @property
59
+ def ok(self) -> bool:
60
+ """Whether this result counts as acceptable: pass or skipped."""
61
+ return self.status in ("pass", "skipped")
62
+
63
+ @model_validator(mode="after")
64
+ def _truncate_log_tail(self) -> Self:
65
+ if len(self.log_tail) > _LOG_TAIL_MAX_CHARS:
66
+ keep = _LOG_TAIL_MAX_CHARS - len(_TRUNCATION_MARKER)
67
+ object.__setattr__(self, "log_tail", _TRUNCATION_MARKER + self.log_tail[-keep:])
68
+ return self
69
+
70
+ @model_validator(mode="after")
71
+ def _check_pass_has_no_errors(self) -> Self:
72
+ if self.status == "pass" and any(issue.severity == "error" for issue in self.issues):
73
+ raise ValueError(
74
+ "CheckResult with status='pass' must not have any issue of severity 'error'"
75
+ )
76
+ return self