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,70 @@
1
+ """Events: the append-only record of what happened during a run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Self
6
+
7
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from chipgraph.core.contracts.types import FailureLabel, Sha256
10
+
11
+ EventType = Literal[
12
+ "run_start",
13
+ "rule_start",
14
+ "tool_call",
15
+ "check_result",
16
+ "agent_turn",
17
+ "gate_wait",
18
+ "gate_decision",
19
+ "rule_done",
20
+ "rule_fail",
21
+ "run_stop",
22
+ ]
23
+ """The kind of thing that happened, recorded as one event in a run's journal."""
24
+
25
+
26
+ class Event(BaseModel):
27
+ """A single entry in a run's append-only journal."""
28
+
29
+ model_config = ConfigDict(extra="forbid", frozen=True)
30
+
31
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
32
+ run_id: str = Field(description="The id of the run this event belongs to.")
33
+ seq: int = Field(ge=0, description="The 0-based sequence number of this event within the run.")
34
+ ts: AwareDatetime = Field(description="When this event occurred.")
35
+ type: EventType = Field(description="The kind of event this is.")
36
+ rule_instance: str | None = Field(
37
+ default=None, description="The rule instance this event concerns, if any."
38
+ )
39
+ payload: dict[str, Any] = Field(default={}, description="Event-type-specific data.")
40
+ failure_label: FailureLabel | None = Field(
41
+ default=None, description="Why a rule failed; only set when type is 'rule_fail'."
42
+ )
43
+
44
+ @model_validator(mode="after")
45
+ def _check_failure_label_only_on_rule_fail(self) -> Self:
46
+ if self.failure_label is not None and self.type != "rule_fail":
47
+ raise ValueError("failure_label is only allowed when type='rule_fail'")
48
+ return self
49
+
50
+
51
+ class RunManifest(BaseModel):
52
+ """A snapshot of everything needed to reproduce or audit a run."""
53
+
54
+ model_config = ConfigDict(extra="forbid", frozen=True)
55
+
56
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
57
+ run_id: str = Field(description="The id of this run.")
58
+ started_at: AwareDatetime = Field(description="When this run started.")
59
+ target: str = Field(description="The build target this run was invoked with.")
60
+ chipgraph_version: str = Field(description="The chipgraph version that executed this run.")
61
+ profile_hash: Sha256 = Field(description="Hash of the resolved profile used for this run.")
62
+ profile_sources: tuple[str, ...] = Field(
63
+ default=(),
64
+ description="Sources the profile was assembled from, e.g. '.chipgraph.yml@<commit>'.",
65
+ )
66
+ packs: dict[str, str] = Field(default={}, description="Pack ids to their versions.")
67
+ adapters: dict[str, str] = Field(default={}, description="Adapter ids to their versions.")
68
+ models: dict[str, str] = Field(default={}, description="Model ids to their versions.")
69
+ tools: dict[str, str] = Field(default={}, description="EDA tool ids to their versions.")
70
+ runner: str = Field(default="local", description="The runner used to execute this run.")
@@ -0,0 +1,128 @@
1
+ """Export JSON Schema files for the core contracts.
2
+
3
+ Run as ``python -m chipgraph.core.contracts.export [--check] [--out DIR]``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from chipgraph.core.contracts import TOP_LEVEL_MODELS
14
+ from chipgraph.core.model.export import MODEL_SCHEMA_MODELS
15
+
16
+ _README_CONTENT = (
17
+ "# schemas\n"
18
+ "\n"
19
+ "These JSON Schema files are generated from the pydantic models in\n"
20
+ "`src/chipgraph/core/contracts/` by `make schemas`\n"
21
+ "(`python -m chipgraph.core.contracts.export`). Do not edit them by hand.\n"
22
+ "\n"
23
+ "`model/` holds the schemas for the Design Model's entities and relations\n"
24
+ "(`src/chipgraph/core/model/`).\n"
25
+ )
26
+
27
+
28
+ def find_repo_root(start: Path) -> Path:
29
+ """Walk up from `start` until a directory containing `pyproject.toml` is found."""
30
+ current = start.resolve()
31
+ for candidate in (current, *current.parents):
32
+ if (candidate / "pyproject.toml").is_file():
33
+ return candidate
34
+ raise FileNotFoundError(f"no pyproject.toml found above {start}")
35
+
36
+
37
+ def generate_schema_files() -> dict[str, str]:
38
+ """Build the mapping of {relative path: contents} for every schema file to write.
39
+
40
+ Core contract schemas are top-level (`ArtifactRef.schema.json`, ...); Design Model
41
+ schemas live under `model/` (`model/BlockEntity.schema.json`, ...).
42
+ """
43
+ files: dict[str, str] = {"README.md": _README_CONTENT}
44
+ for model in TOP_LEVEL_MODELS:
45
+ filename = f"{model.__name__}.schema.json"
46
+ schema = model.model_json_schema()
47
+ files[filename] = json.dumps(schema, indent=2, sort_keys=True) + "\n"
48
+ for model_cls in MODEL_SCHEMA_MODELS:
49
+ filename = f"model/{model_cls.__name__}.schema.json"
50
+ model_schema = model_cls.model_json_schema()
51
+ files[filename] = json.dumps(model_schema, indent=2, sort_keys=True) + "\n"
52
+ return files
53
+
54
+
55
+ def write_schema_files(out_dir: Path) -> None:
56
+ """Write the generated schema files to `out_dir`, creating it (and `model/`) if needed."""
57
+ out_dir.mkdir(parents=True, exist_ok=True)
58
+ for rel_path, content in generate_schema_files().items():
59
+ path = out_dir / rel_path
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ path.write_text(content, encoding="utf-8")
62
+
63
+
64
+ def check_schema_files(out_dir: Path) -> list[str]:
65
+ """Compare generated schema files against what's on disk in `out_dir`.
66
+
67
+ Returns a list of human-readable problem descriptions; empty means up to date.
68
+ """
69
+ expected = generate_schema_files()
70
+ problems: list[str] = []
71
+
72
+ existing_names: set[str] = set()
73
+ if out_dir.is_dir():
74
+ existing_names |= {p.name for p in out_dir.glob("*") if p.is_file()}
75
+ model_dir = out_dir / "model"
76
+ if model_dir.is_dir():
77
+ existing_names |= {f"model/{p.name}" for p in model_dir.glob("*") if p.is_file()}
78
+
79
+ for rel_path, content in expected.items():
80
+ path = out_dir / rel_path
81
+ if not path.is_file():
82
+ problems.append(f"missing: {rel_path}")
83
+ elif path.read_text(encoding="utf-8") != content:
84
+ problems.append(f"outdated: {rel_path}")
85
+
86
+ extra_names = existing_names - set(expected)
87
+ for rel_path in sorted(extra_names):
88
+ if rel_path.endswith(".schema.json") or rel_path == "README.md":
89
+ problems.append(f"extra: {rel_path}")
90
+
91
+ return problems
92
+
93
+
94
+ def main(argv: list[str] | None = None) -> int:
95
+ """CLI entry point: regenerate or check the `schemas/` directory."""
96
+ parser = argparse.ArgumentParser(description=__doc__)
97
+ parser.add_argument(
98
+ "--check",
99
+ action="store_true",
100
+ help="check that schemas/ matches the models, without writing; exit 1 if not",
101
+ )
102
+ parser.add_argument(
103
+ "--out",
104
+ type=Path,
105
+ default=None,
106
+ help="output directory (default: schemas/ at the repo root)",
107
+ )
108
+ args = parser.parse_args(argv)
109
+
110
+ out_dir = args.out if args.out is not None else find_repo_root(Path(__file__)) / "schemas"
111
+
112
+ if args.check:
113
+ problems = check_schema_files(out_dir)
114
+ if problems:
115
+ print("schemas out of date:", file=sys.stderr)
116
+ for problem in problems:
117
+ print(f" {problem}", file=sys.stderr)
118
+ return 1
119
+ print(f"schemas up to date in {out_dir}")
120
+ return 0
121
+
122
+ write_schema_files(out_dir)
123
+ print(f"wrote schemas to {out_dir}")
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())
@@ -0,0 +1,171 @@
1
+ """Findings: problems found in the design, at one of 5 layers of certainty (DESIGN.md 4.8).
2
+
3
+ A `Finding` is the common shape every check, Critic (AI) pass, z3, or formal run reports
4
+ issues in. Evidence is mandatory: a finding with nothing pointing at a file:line or a
5
+ Design Model key is not a finding. AI sources (anything other than the known
6
+ deterministic ones: `check:*`, `z3`, `formal`, `lint`) can never report `severity="error"`,
7
+ so an unreliable finding never blocks a build on its own (DESIGN.md 4.8).
8
+
9
+ A finding's identity across runs is its `fingerprint`: a stable hash of its source, claim,
10
+ and evidence locations, independent of `first_seen`/`last_seen`/`confidence`. The same
11
+ problem found again by the same source is the same finding, not a new one.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ from collections.abc import Iterable
18
+ from typing import Literal, Self
19
+
20
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
21
+
22
+ from chipgraph.core.contracts._paths import validate_relative_path
23
+ from chipgraph.core.contracts.artifact import ArtifactRef
24
+ from chipgraph.core.contracts.types import Sha256
25
+
26
+ FindingSeverity = Literal["error", "warning", "question"]
27
+ """How serious a finding is. Only deterministic layers may use `error` (DESIGN.md 4.8)."""
28
+
29
+ FindingStatus = Literal["open", "fixed", "waived"]
30
+ """A finding's lifecycle: newly (re)detected, no longer detected, or waived by a person."""
31
+
32
+ _DETERMINISTIC_SOURCES = frozenset({"z3", "formal", "lint"})
33
+ _FINGERPRINT_ID_CHARS = 8
34
+
35
+
36
+ def is_ai_source(source: str) -> bool:
37
+ """Whether `source` is an AI source (Critic or similar), never a deterministic one.
38
+
39
+ Deterministic sources are `check:<id>`, `z3`, `formal`, and `lint`. Anything else
40
+ -- `critic`, or any future AI source id -- is treated as AI, conservatively: an
41
+ unrecognized source is never assumed safe enough to block a build.
42
+ """
43
+ if source.startswith("check:"):
44
+ return False
45
+ return source not in _DETERMINISTIC_SOURCES
46
+
47
+
48
+ def _artifact_key(ref: ArtifactRef) -> str:
49
+ return f"{ref.repo}:{ref.path if ref.path is not None else ref.model_key}"
50
+
51
+
52
+ def _evidence_key(evidence: Evidence) -> str:
53
+ if evidence.file is not None:
54
+ return f"{evidence.file}:{evidence.line if evidence.line is not None else '-'}"
55
+ return f"model:{evidence.model_key}"
56
+
57
+
58
+ def compute_fingerprint(source: str, claim: str, evidence: Iterable[Evidence]) -> str:
59
+ """A stable sha256 hex digest identifying a finding across runs.
60
+
61
+ Built from `source`, `claim`, and the sorted set of evidence locations
62
+ (`file:line` or `model:<key>`). Deliberately excludes anything that can change
63
+ run to run without the underlying problem changing: `first_seen`, `last_seen`,
64
+ `confidence`, `suggestion`, `artifacts`/`artifact_hashes`, `run_id`.
65
+ """
66
+ digest = hashlib.sha256()
67
+ digest.update(source.encode("utf-8"))
68
+ digest.update(b"\0")
69
+ digest.update(claim.encode("utf-8"))
70
+ digest.update(b"\0")
71
+ for loc in sorted(_evidence_key(e) for e in evidence):
72
+ digest.update(loc.encode("utf-8"))
73
+ digest.update(b"\0")
74
+ return digest.hexdigest()
75
+
76
+
77
+ class Evidence(BaseModel):
78
+ """A pointer to where a finding was observed: a file:line, or a Design Model key."""
79
+
80
+ model_config = ConfigDict(extra="forbid", frozen=True)
81
+
82
+ file: str | None = Field(default=None, description="POSIX path, relative to the repo root.")
83
+ line: int | None = Field(default=None, ge=1, description="1-based line number, if any.")
84
+ model_key: str | None = Field(
85
+ default=None, description="Key into the Design Model, when this evidence is not a file."
86
+ )
87
+ note: str = Field(default="", description="An optional free-text note about this evidence.")
88
+
89
+ @model_validator(mode="after")
90
+ def _check_exactly_one_locator(self) -> Self:
91
+ if (self.file is None) == (self.model_key is None):
92
+ raise ValueError("Evidence needs exactly one of 'file' or 'model_key' to be set")
93
+ if self.file is not None:
94
+ validate_relative_path(self.file, field_name="file")
95
+ elif self.line is not None:
96
+ raise ValueError("Evidence 'line' requires 'file' to be set")
97
+ return self
98
+
99
+
100
+ class Finding(BaseModel):
101
+ """A single detected problem, at one of the 5 layers of DESIGN.md 4.8."""
102
+
103
+ model_config = ConfigDict(extra="forbid", frozen=True)
104
+
105
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
106
+ id: str = Field(default="", description="'F-<first 8 chars of fingerprint>'; computed.")
107
+ fingerprint: str = Field(
108
+ default="", description="Stable identity hash of source+claim+evidence; computed."
109
+ )
110
+ layer: int = Field(ge=1, le=5, description="Which of the 5 layers of DESIGN.md 4.8 found this.")
111
+ severity: FindingSeverity = Field(description="How serious this finding is.")
112
+ source: str = Field(
113
+ description="Where this came from: 'check:<id>', 'critic', 'z3', 'formal', or 'lint'."
114
+ )
115
+ evidence: tuple[Evidence, ...] = Field(
116
+ min_length=1, description="Where this was observed; at least one is required."
117
+ )
118
+ claim: str = Field(description="What is wrong, in plain language.")
119
+ suggestion: str = Field(default="", description="A suggested fix, if any.")
120
+ confidence: float = Field(default=1.0, ge=0, le=1, description="Confidence in this finding.")
121
+ status: FindingStatus = Field(default="open", description="This finding's lifecycle status.")
122
+ artifacts: tuple[ArtifactRef, ...] = Field(
123
+ default=(), description="Artifacts this finding concerns; what a waiver binds to."
124
+ )
125
+ artifact_hashes: dict[str, Sha256] = Field(
126
+ default={},
127
+ description="Content hashes of `artifacts` at detection time, keyed by "
128
+ "'<repo>:<path or model_key>'.",
129
+ )
130
+ first_seen: AwareDatetime = Field(description="When this finding (fingerprint) was first seen.")
131
+ last_seen: AwareDatetime = Field(description="When this finding was last (re)detected.")
132
+ run_id: str | None = Field(default=None, description="The run that (re)detected this finding.")
133
+
134
+ @model_validator(mode="after")
135
+ def _fill_fingerprint_and_id(self) -> Self:
136
+ if not self.fingerprint:
137
+ object.__setattr__(
138
+ self, "fingerprint", compute_fingerprint(self.source, self.claim, self.evidence)
139
+ )
140
+ if not self.id:
141
+ object.__setattr__(self, "id", f"F-{self.fingerprint[:_FINGERPRINT_ID_CHARS]}")
142
+ return self
143
+
144
+ @model_validator(mode="after")
145
+ def _check_ai_source_not_error(self) -> Self:
146
+ if self.severity == "error" and is_ai_source(self.source):
147
+ raise ValueError(
148
+ f"source {self.source!r} is an AI source; it cannot report severity='error' "
149
+ "(DESIGN.md 4.8: AI findings never block, only deterministic layers can)"
150
+ )
151
+ return self
152
+
153
+ @model_validator(mode="after")
154
+ def _check_artifact_hashes_match_artifacts(self) -> Self:
155
+ valid_keys = {_artifact_key(ref) for ref in self.artifacts}
156
+ for key in self.artifact_hashes:
157
+ if key not in valid_keys:
158
+ raise ValueError(
159
+ f"artifact_hashes key {key!r} does not match any entry in 'artifacts'"
160
+ )
161
+ return self
162
+
163
+
164
+ __all__ = [
165
+ "Evidence",
166
+ "Finding",
167
+ "FindingSeverity",
168
+ "FindingStatus",
169
+ "compute_fingerprint",
170
+ "is_ai_source",
171
+ ]
@@ -0,0 +1,50 @@
1
+ """Gates: human approval points, and the approvals recorded against them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Literal
7
+
8
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
9
+
10
+ from chipgraph.core.contracts.artifact import ArtifactRef
11
+ from chipgraph.core.contracts.types import Sha256
12
+
13
+
14
+ class GateSpec(BaseModel):
15
+ """The specification of a gate: which artifacts need human approval, and how."""
16
+
17
+ model_config = ConfigDict(extra="forbid", frozen=True)
18
+
19
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
20
+ id: str = Field(description="The id of this gate.")
21
+ artifacts: tuple[ArtifactRef, ...] = Field(
22
+ min_length=1, description="The artifacts this gate covers."
23
+ )
24
+ approvers: tuple[str, ...] = Field(
25
+ default=(), description="Identities allowed to approve this gate."
26
+ )
27
+ mode: Literal["pr", "file"] = Field(default="file", description="How approval is recorded.")
28
+
29
+
30
+ class Approval(BaseModel):
31
+ """A recorded decision against a gate, pinned to the artifact hashes it approved."""
32
+
33
+ model_config = ConfigDict(extra="forbid", frozen=True)
34
+
35
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
36
+ gate_id: str = Field(description="The id of the gate this approval is for.")
37
+ by: str = Field(description="The identity that made this decision.")
38
+ at: AwareDatetime = Field(description="When this decision was made.")
39
+ artifact_hashes: dict[str, Sha256] = Field(
40
+ min_length=1,
41
+ description="Hashes the decision was made against, keyed by '<repo>:<path or model_key>'.",
42
+ )
43
+ decision: Literal["approve", "reject", "baseline", "waive"] = Field(
44
+ description="The decision that was made."
45
+ )
46
+ note: str = Field(default="", description="An optional free-text note explaining the decision.")
47
+
48
+ def is_current(self, current: Mapping[str, str]) -> bool:
49
+ """Return True only if every hash this approval covers still matches `current`."""
50
+ return all(current.get(key) == expected for key, expected in self.artifact_hashes.items())
@@ -0,0 +1,177 @@
1
+ """Rules: how artifacts get produced, and instances of rules bound to concrete params."""
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._paths import validate_relative_path
10
+ from chipgraph.core.contracts.artifact import ArtifactRef
11
+ from chipgraph.core.contracts.types import ModelTier, RuleId, RuleKind
12
+
13
+ _TIER_ORDER: dict[ModelTier, int] = {"small": 0, "medium": 1, "large": 2}
14
+
15
+
16
+ class InputSpec(BaseModel):
17
+ """A declared input to a rule: where it comes from and how to select it."""
18
+
19
+ model_config = ConfigDict(extra="forbid", frozen=True)
20
+
21
+ source: Literal["model", "spec", "artifact", "path"] = Field(
22
+ description="Where this input comes from: the Design Model, a spec, an artifact, or a path."
23
+ )
24
+ selector: str = Field(description="A template selecting the input, e.g. 'block/{block}'.")
25
+
26
+ @model_validator(mode="before")
27
+ @classmethod
28
+ def _accept_short_form(cls, data: Any) -> Any:
29
+ if (
30
+ isinstance(data, dict)
31
+ and len(data) == 1
32
+ and not (set(data.keys()) & {"source", "selector"})
33
+ ):
34
+ ((key, value),) = data.items()
35
+ if key in ("model", "spec", "artifact", "path"):
36
+ return {"source": key, "selector": value}
37
+ return data
38
+
39
+
40
+ class Budget(BaseModel):
41
+ """Resource budget for an agent rule: retries, model tier, and token cap."""
42
+
43
+ model_config = ConfigDict(extra="forbid", frozen=True)
44
+
45
+ tries: int = Field(default=3, ge=1, description="Maximum number of attempts.")
46
+ tier: ModelTier = Field(default="medium", description="Model tier to use.")
47
+ escalate: ModelTier | None = Field(
48
+ default=None, description="Higher model tier to escalate to after repeated failures."
49
+ )
50
+ tokens: int | None = Field(default=None, ge=1, description="Maximum tokens allowed, if capped.")
51
+
52
+ @model_validator(mode="after")
53
+ def _check_escalate_order(self) -> Self:
54
+ if self.escalate is not None and _TIER_ORDER[self.escalate] <= _TIER_ORDER[self.tier]:
55
+ raise ValueError(
56
+ f"escalate tier {self.escalate!r} must be higher than tier {self.tier!r}"
57
+ )
58
+ return self
59
+
60
+
61
+ _DEFAULT_BUDGET = Budget()
62
+
63
+
64
+ class RunSpec(BaseModel):
65
+ """How a `gen`/`import` rule actually runs: a tool adapter and its arguments."""
66
+
67
+ model_config = ConfigDict(extra="forbid", frozen=True)
68
+
69
+ use: str = Field(description="The tool adapter to run, e.g. 'cmd'.")
70
+ args: dict[str, Any] = Field(default={}, description="Arguments passed to the tool adapter.")
71
+
72
+
73
+ class RuleSpec(BaseModel):
74
+ """The specification of a rule: how it produces outputs from inputs."""
75
+
76
+ model_config = ConfigDict(extra="forbid", frozen=True)
77
+
78
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
79
+ id: RuleId = Field(description="Namespaced rule id, e.g. 'digital-rtl/rtl_module'.")
80
+ kind: RuleKind = Field(description="How this rule produces its outputs.")
81
+ description: str = Field(
82
+ default="", description="A short, plain-English description of the rule."
83
+ )
84
+ foreach: str | None = Field(
85
+ default=None,
86
+ description="A selector this rule is expanded over, e.g. one instance per block.",
87
+ )
88
+ inputs: tuple[InputSpec, ...] = Field(default=(), description="The inputs this rule consumes.")
89
+ outputs: tuple[str, ...] = Field(
90
+ min_length=1, description="Output path templates this rule produces."
91
+ )
92
+ checks: tuple[str, ...] = Field(
93
+ default=(), description="Check ids run to verify this rule's outputs."
94
+ )
95
+ verified_by: RuleId | str | None = Field(
96
+ default=None, description="A rule or check id that verifies this rule's outputs."
97
+ )
98
+ budget: Budget = Field(default_factory=lambda: _DEFAULT_BUDGET, description="Resource budget.")
99
+ role: str | None = Field(
100
+ default=None, description="The agent role that runs this rule, if any."
101
+ )
102
+ skills: tuple[str, ...] = Field(
103
+ default=(), description="Skill ids available to the agent role."
104
+ )
105
+ gate: str | None = Field(
106
+ default=None, description="A gate id that must approve before this rule runs."
107
+ )
108
+ run: RunSpec | None = Field(
109
+ default=None,
110
+ description="How this rule actually runs (kind 'gen'/'import' only): tool adapter + args.",
111
+ )
112
+
113
+ @model_validator(mode="after")
114
+ def _check_outputs_present(self) -> Self:
115
+ if len(self.outputs) < 1:
116
+ raise ValueError("RuleSpec.outputs must have at least one output template")
117
+ for output in self.outputs:
118
+ validate_relative_path(output, field_name="outputs")
119
+ return self
120
+
121
+ @model_validator(mode="after")
122
+ def _check_agent_fields(self) -> Self:
123
+ if self.kind == "agent":
124
+ if self.role is None:
125
+ raise ValueError("RuleSpec with kind='agent' requires 'role'")
126
+ else:
127
+ if self.role is not None:
128
+ raise ValueError("'role' is only allowed when kind='agent'")
129
+ if self.skills:
130
+ raise ValueError("'skills' is only allowed when kind='agent'")
131
+ if self.budget != _DEFAULT_BUDGET:
132
+ raise ValueError("a non-default 'budget' is only allowed when kind='agent'")
133
+ return self
134
+
135
+ @model_validator(mode="after")
136
+ def _check_run_field(self) -> Self:
137
+ if self.run is not None and self.kind not in ("gen", "import"):
138
+ raise ValueError("'run' is only allowed when kind is 'gen' or 'import'")
139
+ if self.kind == "gen" and self.run is None:
140
+ raise ValueError("RuleSpec with kind='gen' requires 'run'")
141
+ return self
142
+
143
+
144
+ class RuleInstance(BaseModel):
145
+ """A rule bound to concrete parameters, with resolved inputs and outputs."""
146
+
147
+ model_config = ConfigDict(extra="forbid", frozen=True)
148
+
149
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
150
+ rule_id: RuleId = Field(description="The id of the rule this is an instance of.")
151
+ params: dict[str, str] = Field(
152
+ default={}, description="Concrete parameter values, e.g. block, module."
153
+ )
154
+ inputs: tuple[ArtifactRef, ...] = Field(
155
+ default=(), description="Resolved input artifact references."
156
+ )
157
+ outputs: tuple[ArtifactRef, ...] = Field(
158
+ min_length=1, description="Resolved output artifact references."
159
+ )
160
+ instance_id: str = Field(description="A stable id for this instance, built by make_id().")
161
+
162
+ @classmethod
163
+ def make_id(cls, rule_id: str, params: dict[str, str]) -> str:
164
+ """Build the stable instance id for a rule id and its parameters."""
165
+ sorted_params = ",".join(f"{k}={params[k]}" for k in sorted(params))
166
+ return f"{rule_id}[{sorted_params}]"
167
+
168
+ @model_validator(mode="after")
169
+ def _check_outputs_and_id(self) -> Self:
170
+ if len(self.outputs) < 1:
171
+ raise ValueError("RuleInstance.outputs must have at least one output")
172
+ expected = self.make_id(self.rule_id, self.params)
173
+ if self.instance_id != expected:
174
+ raise ValueError(
175
+ f"instance_id {self.instance_id!r} does not match make_id() result {expected!r}"
176
+ )
177
+ return self
@@ -0,0 +1,45 @@
1
+ """Shared type aliases and literals used across the core contracts."""
2
+
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import StringConstraints
6
+
7
+ DataLabel = Literal["public", "internal", "nda"]
8
+ """How sensitive an artifact's content is: public, internal-only, or NDA-restricted."""
9
+
10
+ ArtifactKind = Literal[
11
+ "spec",
12
+ "rtl",
13
+ "tb",
14
+ "sva",
15
+ "report",
16
+ "plan",
17
+ "model",
18
+ "doc",
19
+ "diagram",
20
+ "script",
21
+ "config",
22
+ "other",
23
+ ]
24
+ """The kind of thing an artifact is, independent of its file format."""
25
+
26
+ RuleKind = Literal["gen", "agent", "human", "import"]
27
+ """How a rule produces its outputs: a generator, an AI agent, a human, or an import."""
28
+
29
+ ModelTier = Literal["small", "medium", "large"]
30
+ """A relative capability/cost tier for an LLM used by an agent rule."""
31
+
32
+ CheckStatus = Literal["pass", "fail", "error", "skipped"]
33
+ """The outcome of running a check: it passed, it failed, it errored, or it was skipped."""
34
+
35
+ Severity = Literal["error", "warning", "info"]
36
+ """How serious a single check issue is."""
37
+
38
+ FailureLabel = Literal["context", "constraint", "verification", "planning", "infra"]
39
+ """A coarse category for why a rule failed, used for run analytics."""
40
+
41
+ RuleId = Annotated[str, StringConstraints(pattern=r"^[a-z0-9][a-z0-9_-]*/[a-z0-9][a-z0-9_]*$")]
42
+ """A namespaced rule identifier of the form ``<pack>/<name>``, e.g. ``digital-rtl/rtl_module``."""
43
+
44
+ Sha256 = Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{64}$")]
45
+ """A lowercase hex-encoded SHA-256 digest."""