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,159 @@
1
+ """Packs: domain bundles of rules, roles, skills, checks, templates and commands.
2
+
3
+ A pack is a directory containing a `pack.yml` manifest. Core loads and validates the
4
+ manifest and checks that every path it advertises actually exists; it does not interpret
5
+ rules, skills or templates itself (that is packs' own concern, or later tasks').
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable, Mapping
11
+ from pathlib import Path
12
+ from typing import Annotated, Literal
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
16
+
17
+ from chipgraph.core.plugin_api.registry import PluginError
18
+
19
+ _NAME_PATTERN = r"^[a-z0-9][a-z0-9-]*$"
20
+ _VERSION_PATTERN = r"^\d+\.\d+\.\d+$"
21
+
22
+ PackName = Annotated[str, StringConstraints(pattern=_NAME_PATTERN)]
23
+ PackVersion = Annotated[str, StringConstraints(pattern=_VERSION_PATTERN)]
24
+
25
+
26
+ class PackRequires(BaseModel):
27
+ """What a pack needs: a core version range, other packs, and capabilities."""
28
+
29
+ model_config = ConfigDict(extra="forbid", frozen=True)
30
+
31
+ core: str = Field(default=">=0.0", description="Version range of chipgraph core required.")
32
+ packs: tuple[str, ...] = Field(default=(), description="Names of packs this pack requires.")
33
+ capabilities: tuple[str, ...] = Field(
34
+ default=(), description="Adapter capabilities this pack requires, e.g. 'lint'."
35
+ )
36
+
37
+
38
+ class PackProvides(BaseModel):
39
+ """What a pack provides: paths (relative to the pack directory) to its contents."""
40
+
41
+ # `schemas` is written `schema:` in pack.yml; the Python name avoids shadowing
42
+ # pydantic's deprecated BaseModel.schema().
43
+ model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
44
+
45
+ rules: tuple[str, ...] = Field(default=(), description="Paths to rule definitions.")
46
+ agents: tuple[str, ...] = Field(default=(), description="Paths to agent role definitions.")
47
+ skills: tuple[str, ...] = Field(default=(), description="Paths to skill definitions.")
48
+ checks: tuple[str, ...] = Field(default=(), description="Paths to check definitions.")
49
+ workflows: tuple[str, ...] = Field(default=(), description="Paths to workflow definitions.")
50
+ templates: tuple[str, ...] = Field(default=(), description="Paths to templates.")
51
+ commands: tuple[str, ...] = Field(
52
+ default=(), description="Command names this pack provides, e.g. '/rtl'."
53
+ )
54
+ schemas: tuple[str, ...] = Field(
55
+ default=(),
56
+ alias="schema",
57
+ description="Paths to schema extensions (`schema:` in pack.yml).",
58
+ )
59
+
60
+
61
+ class PackManifest(BaseModel):
62
+ """The validated contents of a pack's `pack.yml`."""
63
+
64
+ model_config = ConfigDict(extra="forbid", frozen=True)
65
+
66
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
67
+ name: PackName = Field(description="The pack's name, e.g. 'digital-rtl'.")
68
+ version: PackVersion = Field(description="The pack's version, MAJOR.MINOR.PATCH.")
69
+ description: str = Field(default="", description="A short, plain-English description.")
70
+ requires: PackRequires = Field(
71
+ default_factory=PackRequires, description="What this pack needs."
72
+ )
73
+ provides: PackProvides = Field(
74
+ default_factory=PackProvides, description="What this pack provides."
75
+ )
76
+
77
+
78
+ class Pack:
79
+ """A loaded pack: its manifest, plus the directory it lives in."""
80
+
81
+ def __init__(self, manifest: PackManifest, root: Path) -> None:
82
+ self.manifest = manifest
83
+ self.root = root
84
+
85
+ def path(self, rel: str) -> Path:
86
+ """Resolve a path relative to this pack's directory."""
87
+ return self.root / rel
88
+
89
+ def __repr__(self) -> str:
90
+ return (
91
+ f"Pack(name={self.manifest.name!r}, "
92
+ f"version={self.manifest.version!r}, root={self.root!r})"
93
+ )
94
+
95
+
96
+ _PROVIDES_PATH_FIELDS = ("rules", "agents", "skills", "checks", "workflows", "templates", "schemas")
97
+
98
+
99
+ def load_pack(directory: Path) -> Pack:
100
+ """Load and validate the pack at `directory`.
101
+
102
+ Raises `PluginError` if `pack.yml` is missing or invalid, or if any path listed in
103
+ `provides` (other than `commands`, which are names, not paths) does not exist under
104
+ `directory`.
105
+ """
106
+ manifest_path = directory / "pack.yml"
107
+ if not manifest_path.is_file():
108
+ raise PluginError(f"no pack.yml found in {directory}")
109
+ try:
110
+ raw = yaml.safe_load(manifest_path.read_text()) or {}
111
+ manifest = PackManifest.model_validate(raw)
112
+ except Exception as exc:
113
+ raise PluginError(f"invalid pack manifest at {manifest_path}: {exc}") from exc
114
+
115
+ for field in _PROVIDES_PATH_FIELDS:
116
+ for rel in getattr(manifest.provides, field):
117
+ if not (directory / rel).exists():
118
+ raise PluginError(
119
+ f"pack {manifest.name!r} provides missing path {rel!r} "
120
+ f"(expected at {directory / rel})"
121
+ )
122
+
123
+ return Pack(manifest, directory)
124
+
125
+
126
+ def discover_packs(search_paths: Iterable[Path]) -> dict[str, Pack]:
127
+ """Load every pack found as an immediate subdirectory (containing `pack.yml`) of any path.
128
+
129
+ Raises `PluginError` if two discovered packs share a name.
130
+ """
131
+ packs: dict[str, Pack] = {}
132
+ for search_path in search_paths:
133
+ if not search_path.is_dir():
134
+ continue
135
+ for entry in sorted(search_path.iterdir()):
136
+ if not entry.is_dir() or not (entry / "pack.yml").is_file():
137
+ continue
138
+ pack = load_pack(entry)
139
+ name = pack.manifest.name
140
+ if name in packs:
141
+ raise PluginError(
142
+ f"duplicate pack name {name!r}: found at {packs[name].root} and {entry}"
143
+ )
144
+ packs[name] = pack
145
+ return packs
146
+
147
+
148
+ def resolve_requires(packs: Mapping[str, Pack]) -> None:
149
+ """Check that every pack's `requires.packs` is present in `packs`.
150
+
151
+ Raises `PluginError` naming the first missing dependency found.
152
+ """
153
+ for pack in packs.values():
154
+ for required in pack.manifest.requires.packs:
155
+ if required not in packs:
156
+ raise PluginError(
157
+ f"pack {pack.manifest.name!r} requires pack {required!r}, "
158
+ "which is not available"
159
+ )
@@ -0,0 +1,177 @@
1
+ """Plugin protocols: the structural interfaces adapters, checks and packs implement.
2
+
3
+ Every protocol here is `@runtime_checkable`, so `isinstance(obj, Protocol)` works for
4
+ registry validation (see `registry.py`). Each protocol carries a `name: str` attribute
5
+ identifying the concrete implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable, Mapping, Sequence
11
+ from pathlib import Path
12
+ from typing import Protocol, runtime_checkable
13
+
14
+ from chipgraph.core.contracts import AgentResult, Approval, CheckResult, CheckSpec, Issue
15
+ from chipgraph.core.plugin_api.types import (
16
+ AgentTask,
17
+ LlmRequest,
18
+ LlmResponse,
19
+ RunResult,
20
+ ToolContext,
21
+ )
22
+
23
+
24
+ @runtime_checkable
25
+ class Runner(Protocol):
26
+ """Runs subprocess commands. Core never calls `subprocess` directly; it goes through this."""
27
+
28
+ name: str
29
+
30
+ async def run(
31
+ self,
32
+ cmd: Sequence[str],
33
+ *,
34
+ cwd: Path,
35
+ env: Mapping[str, str] | None = None,
36
+ timeout_s: float | None = None,
37
+ ) -> RunResult:
38
+ """Run `cmd` in `cwd` and return its result."""
39
+ ...
40
+
41
+
42
+ @runtime_checkable
43
+ class LogParser(Protocol):
44
+ """Parses a tool's raw log text into structured issues."""
45
+
46
+ name: str
47
+
48
+ def parse(self, log: str) -> tuple[Issue, ...]:
49
+ """Extract issues from `log`."""
50
+ ...
51
+
52
+
53
+ @runtime_checkable
54
+ class ToolAdapter(Protocol):
55
+ """Wraps an external EDA tool (or other command-line tool) behind a common interface."""
56
+
57
+ name: str
58
+ capability: str
59
+
60
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
61
+ """Run the check described by `spec` under `ctx` and return its result."""
62
+ ...
63
+
64
+
65
+ @runtime_checkable
66
+ class Check(Protocol):
67
+ """A built-in, deterministic check that needs no external tool."""
68
+
69
+ name: str
70
+ id: str
71
+
72
+ async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
73
+ """Run this check for `spec` under `ctx` and return its result."""
74
+ ...
75
+
76
+
77
+ @runtime_checkable
78
+ class VcsAdapter(Protocol):
79
+ """Version control operations the engine needs: head, diffs, and disposable workspaces."""
80
+
81
+ name: str
82
+
83
+ def head(self, root: Path) -> str:
84
+ """Return the current head revision of the repo at `root`."""
85
+ ...
86
+
87
+ def changed_files(self, root: Path, since: str | None = None) -> tuple[str, ...]:
88
+ """Return paths changed since `since` (or since the last commit, if None)."""
89
+ ...
90
+
91
+ def create_workspace(self, root: Path, name: str) -> Path:
92
+ """Create a disposable workspace (e.g. a worktree) named `name` and return its path."""
93
+ ...
94
+
95
+ def remove_workspace(self, path: Path) -> None:
96
+ """Remove a workspace previously created by `create_workspace`."""
97
+ ...
98
+
99
+
100
+ @runtime_checkable
101
+ class ReviewAdapter(Protocol):
102
+ """Records and retrieves human approvals against gates."""
103
+
104
+ name: str
105
+
106
+ def record(self, approval: Approval) -> None:
107
+ """Persist `approval`."""
108
+ ...
109
+
110
+ def approvals(self, gate_id: str) -> tuple[Approval, ...]:
111
+ """Return every approval recorded against `gate_id`."""
112
+ ...
113
+
114
+
115
+ @runtime_checkable
116
+ class LlmProvider(Protocol):
117
+ """Completes LLM requests. `local` gates whether it may see data labeled 'nda'."""
118
+
119
+ name: str
120
+ local: bool
121
+ """True when this provider is self-hosted; only then may it receive 'nda'-labeled data."""
122
+
123
+ async def complete(self, request: LlmRequest) -> LlmResponse:
124
+ """Complete `request` and return the response."""
125
+ ...
126
+
127
+
128
+ @runtime_checkable
129
+ class AgentRuntime(Protocol):
130
+ """Runs an `AgentTask` to completion (or to a stopping point) via some agent harness."""
131
+
132
+ name: str
133
+
134
+ async def run_task(self, task: AgentTask) -> AgentResult:
135
+ """Run `task` and return its result."""
136
+ ...
137
+
138
+
139
+ @runtime_checkable
140
+ class FormatAdapter(Protocol):
141
+ """Loads a domain file format into raw facts.
142
+
143
+ Returns raw facts (plain mappings), not the typed Design Model: the typed model
144
+ arrives in M1-01. Callers that need the Design Model translate these facts themselves.
145
+ """
146
+
147
+ name: str
148
+
149
+ def load(self, path: Path) -> Iterable[Mapping[str, object]]:
150
+ """Load `path` and yield its facts."""
151
+ ...
152
+
153
+
154
+ @runtime_checkable
155
+ class Extractor(Protocol):
156
+ """Extracts facts from a source file (e.g. RTL) by parsing it.
157
+
158
+ Returns raw facts (plain mappings), not the typed Design Model: the typed model
159
+ arrives in M1-01. Callers that need the Design Model translate these facts themselves.
160
+ """
161
+
162
+ name: str
163
+
164
+ def extract(self, path: Path) -> Iterable[Mapping[str, object]]:
165
+ """Extract facts from `path`."""
166
+ ...
167
+
168
+
169
+ @runtime_checkable
170
+ class Generator(Protocol):
171
+ """Generates output files from facts, e.g. rendering a template."""
172
+
173
+ name: str
174
+
175
+ def generate(self, out_dir: Path, facts: Iterable[Mapping[str, object]]) -> tuple[Path, ...]:
176
+ """Write generated files under `out_dir` from `facts` and return their paths."""
177
+ ...
@@ -0,0 +1,111 @@
1
+ """The plugin registry: where adapters, checks and other plugins register themselves.
2
+
3
+ Plugins register either directly (`Registry.register`) or by Python entry points
4
+ (`Registry.discover`), under the group `chipgraph.adapters.<kind>` for each kind in `KINDS`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ from importlib.metadata import entry_points
11
+ from typing import Any
12
+
13
+ from chipgraph.core.plugin_api.protocols import (
14
+ AgentRuntime,
15
+ Check,
16
+ Extractor,
17
+ FormatAdapter,
18
+ Generator,
19
+ LlmProvider,
20
+ LogParser,
21
+ ReviewAdapter,
22
+ Runner,
23
+ ToolAdapter,
24
+ VcsAdapter,
25
+ )
26
+
27
+ KINDS: dict[str, type] = {
28
+ "runner": Runner,
29
+ "parser": LogParser,
30
+ "tool": ToolAdapter,
31
+ "check": Check,
32
+ "vcs": VcsAdapter,
33
+ "review": ReviewAdapter,
34
+ "llm": LlmProvider,
35
+ "runtime": AgentRuntime,
36
+ "format": FormatAdapter,
37
+ "extractor": Extractor,
38
+ "generator": Generator,
39
+ }
40
+ """Maps a plugin kind name to the protocol it must satisfy."""
41
+
42
+
43
+ class PluginError(Exception):
44
+ """Raised for any registry or pack error: unknown kind, duplicate name, bad plugin, ..."""
45
+
46
+
47
+ class Registry:
48
+ """Holds registered plugin instances, keyed by kind and name."""
49
+
50
+ def __init__(self) -> None:
51
+ self._plugins: dict[str, dict[str, object]] = {kind: {} for kind in KINDS}
52
+
53
+ def register(self, kind: str, name: str, obj: object) -> None:
54
+ """Register `obj` under `kind`/`name`.
55
+
56
+ Raises `PluginError` if `kind` is unknown, `name` is already registered for that
57
+ kind, or `obj` does not satisfy the protocol for `kind`.
58
+ """
59
+ if kind not in KINDS:
60
+ raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
61
+ if name in self._plugins[kind]:
62
+ raise PluginError(f"{kind}/{name} is already registered")
63
+ protocol = KINDS[kind]
64
+ if not isinstance(obj, protocol):
65
+ raise PluginError(
66
+ f"{obj!r} does not satisfy the {protocol.__name__} protocol for kind {kind!r}"
67
+ )
68
+ self._plugins[kind][name] = obj
69
+
70
+ def get(self, kind: str, name: str) -> Any:
71
+ """Return the plugin registered under `kind`/`name`.
72
+
73
+ Raises `PluginError` if `kind` is unknown, listing the registered names otherwise.
74
+ """
75
+ if kind not in KINDS:
76
+ raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
77
+ try:
78
+ return self._plugins[kind][name]
79
+ except KeyError:
80
+ available = sorted(self._plugins[kind])
81
+ raise PluginError(f"no {kind} plugin named {name!r}; available: {available}") from None
82
+
83
+ def names(self, kind: str) -> tuple[str, ...]:
84
+ """Return the sorted names registered under `kind`."""
85
+ if kind not in KINDS:
86
+ raise PluginError(f"unknown plugin kind {kind!r}; known kinds: {sorted(KINDS)}")
87
+ return tuple(sorted(self._plugins[kind]))
88
+
89
+ def discover(self) -> int:
90
+ """Load plugins from Python entry points, one group per kind.
91
+
92
+ For each kind, entry points under group `chipgraph.adapters.<kind>` are loaded.
93
+ A loaded object that is a class or a no-argument factory is called to produce the
94
+ plugin instance; anything else is registered as-is. Returns the number registered.
95
+ Any error loading or registering one entry point becomes a `PluginError` naming it.
96
+ """
97
+ count = 0
98
+ for kind in KINDS:
99
+ group = f"chipgraph.adapters.{kind}"
100
+ for entry_point in entry_points(group=group):
101
+ try:
102
+ loaded = entry_point.load()
103
+ is_factory = inspect.isclass(loaded) or inspect.isfunction(loaded)
104
+ obj = loaded() if is_factory else loaded
105
+ self.register(kind, entry_point.name, obj)
106
+ except Exception as exc:
107
+ raise PluginError(
108
+ f"failed to load entry point {entry_point.name!r} in group {group!r}: {exc}"
109
+ ) from exc
110
+ count += 1
111
+ return count
@@ -0,0 +1,94 @@
1
+ """Shared plugin types: in-process data passed to and from plugin protocols.
2
+
3
+ These models are not persisted (no `schema_version`) and are not part of the artifact
4
+ store; they only carry data between core and adapters/packs during a single run.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Literal
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from chipgraph.core.contracts import Budget, DataLabel, RuleInstance
15
+
16
+ if TYPE_CHECKING:
17
+ from chipgraph.core.plugin_api.protocols import Runner
18
+
19
+
20
+ class RunResult(BaseModel):
21
+ """The outcome of running a subprocess command via a `Runner`."""
22
+
23
+ model_config = ConfigDict(extra="forbid", frozen=True)
24
+
25
+ returncode: int = Field(description="The process's exit code.")
26
+ stdout: str = Field(description="Captured standard output.")
27
+ stderr: str = Field(description="Captured standard error.")
28
+ duration_s: float = Field(ge=0, description="How long the command took, in seconds.")
29
+ timed_out: bool = Field(default=False, description="Whether the command was killed on timeout.")
30
+
31
+
32
+ class ToolContext(BaseModel):
33
+ """Context a `ToolAdapter` or `Check` runs with: repo, runner, env, and free params."""
34
+
35
+ model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
36
+
37
+ repo_root: Path = Field(description="Root of the repo (or workspace member) being worked on.")
38
+ runner: Runner = Field(description="The subprocess runner to use for any command.")
39
+ env: dict[str, str] = Field(default={}, description="Extra environment variables.")
40
+ params: dict[str, str] = Field(
41
+ default={}, description="Free-form parameters, e.g. block, module."
42
+ )
43
+
44
+
45
+ class LlmMessage(BaseModel):
46
+ """A single message in an LLM conversation."""
47
+
48
+ model_config = ConfigDict(extra="forbid", frozen=True)
49
+
50
+ role: Literal["system", "user", "assistant"] = Field(description="Who sent this message.")
51
+ content: str = Field(description="The message text.")
52
+
53
+
54
+ class LlmRequest(BaseModel):
55
+ """A request to complete a conversation with an LLM."""
56
+
57
+ model_config = ConfigDict(extra="forbid", frozen=True)
58
+
59
+ model: str = Field(description="The model id to use.")
60
+ messages: tuple[LlmMessage, ...] = Field(min_length=1, description="The conversation so far.")
61
+ max_tokens: int = Field(ge=1, description="Maximum number of tokens to generate.")
62
+ labels: tuple[DataLabel, ...] = Field(
63
+ default=(),
64
+ description=(
65
+ "Data labels of everything in the context. A provider must refuse a request "
66
+ "carrying 'nda' unless it is local (see LlmProvider.local)."
67
+ ),
68
+ )
69
+
70
+
71
+ class LlmResponse(BaseModel):
72
+ """The result of completing an LLM request."""
73
+
74
+ model_config = ConfigDict(extra="forbid", frozen=True)
75
+
76
+ text: str = Field(description="The generated text.")
77
+ input_tokens: int = Field(ge=0, description="Tokens consumed by the input.")
78
+ output_tokens: int = Field(ge=0, description="Tokens consumed by the output.")
79
+ model: str = Field(description="The model id that actually served the request.")
80
+
81
+
82
+ class AgentTask(BaseModel):
83
+ """A single task handed to an `AgentRuntime`: what to do, and under what constraints."""
84
+
85
+ model_config = ConfigDict(extra="forbid", frozen=True)
86
+
87
+ instance: RuleInstance = Field(description="The rule instance this task fulfils.")
88
+ role: str = Field(description="The agent role that runs this task.")
89
+ skills: tuple[str, ...] = Field(default=(), description="Skill ids available to the agent.")
90
+ allowed_writes: tuple[str, ...] = Field(
91
+ min_length=1, description="Repo-relative paths the agent is allowed to write."
92
+ )
93
+ context: dict[str, str] = Field(default={}, description="Free-form context for the agent.")
94
+ budget: Budget = Field(description="Resource budget for this task.")
@@ -0,0 +1,41 @@
1
+ """Agent runtime state: the task queue an out-of-process agent runtime works from, and
2
+ the executor that hands agent rules to an `AgentRuntime` (DESIGN.md 5.2, 5.5; D35). The
3
+ roles and skills, as data, are in `chipgraph.core.runtime.roles` (DESIGN.md 5.1; D8).
4
+
5
+ `chipgraph.core.runtime` is generic: it knows nothing about any agent harness, model or
6
+ tool. Runtimes live in `chipgraph.adapters.runtime`.
7
+ """
8
+
9
+ from chipgraph.core.runtime.executor import (
10
+ AgentRuntimeExecutor,
11
+ instance_inputs_hash,
12
+ outcome_for,
13
+ task_for,
14
+ )
15
+ from chipgraph.core.runtime.queue import (
16
+ ACTIVE_STATUSES,
17
+ DEFAULT_TOOL_CALL_CAP,
18
+ OPEN_STATUSES,
19
+ AgentBinding,
20
+ AgentTaskRecord,
21
+ QueueError,
22
+ TaskQueue,
23
+ TaskStatus,
24
+ state_key,
25
+ )
26
+
27
+ __all__ = [
28
+ "ACTIVE_STATUSES",
29
+ "DEFAULT_TOOL_CALL_CAP",
30
+ "OPEN_STATUSES",
31
+ "AgentBinding",
32
+ "AgentRuntimeExecutor",
33
+ "AgentTaskRecord",
34
+ "QueueError",
35
+ "TaskQueue",
36
+ "TaskStatus",
37
+ "instance_inputs_hash",
38
+ "outcome_for",
39
+ "state_key",
40
+ "task_for",
41
+ ]
@@ -0,0 +1,79 @@
1
+ """`AgentRuntimeExecutor`: runs `kind: agent` rules through an `AgentRuntime` (DESIGN.md 5.2).
2
+
3
+ The scheduler knows `Executor`s; agent runtimes implement `AgentRuntime.run_task`. This
4
+ executor turns a rule instance into an `AgentTask` (role, skills, budget, and the
5
+ instance's concrete outputs as the only files the agent may write) and maps the
6
+ runtime's `AgentResult` back to an `ExecOutcome`:
7
+
8
+ - `done` -> ok; the scheduler then runs the rule's checks and records the outputs.
9
+ - `needs_human` -> not ok (`planning`): the open questions are the message. A runtime
10
+ that runs outside the engine uses this for "the task is waiting for an agent" too.
11
+ - `budget_exhausted` / `failed` -> not ok (`verification`).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from chipgraph.core.contracts import AgentResult, RuleInstance, RuleSpec
17
+ from chipgraph.core.contracts.types import FailureLabel
18
+ from chipgraph.core.engine.scheduler import ExecOutcome
19
+ from chipgraph.core.plugin_api.protocols import AgentRuntime
20
+ from chipgraph.core.plugin_api.types import AgentTask
21
+ from chipgraph.core.state.artifacts import ArtifactStore, hash_inputs
22
+
23
+
24
+ def instance_inputs_hash(store: ArtifactStore, instance: RuleInstance) -> str:
25
+ """A hash over the instance's inputs, the same way the scheduler's records hash them.
26
+
27
+ File inputs hash their content (a missing file counts as empty); model inputs are
28
+ keyed by their model key only, since the Design Model hashes its own entities.
29
+ """
30
+ path_refs = [ref for ref in instance.inputs if ref.path is not None]
31
+ current = store.current_hashes(path_refs)
32
+ pairs = []
33
+ for ref in instance.inputs:
34
+ locator = ref.path if ref.path is not None else ref.model_key
35
+ key = f"{ref.repo}:{locator}"
36
+ pairs.append((key, current.get(key, "")))
37
+ return hash_inputs(pairs)
38
+
39
+
40
+ def task_for(rule: RuleSpec, instance: RuleInstance) -> AgentTask:
41
+ """The `AgentTask` for one instance of an agent rule."""
42
+ if rule.role is None:
43
+ raise ValueError(f"rule {rule.id!r} has no role")
44
+ writes = tuple(ref.path for ref in instance.outputs if ref.path is not None)
45
+ return AgentTask(
46
+ instance=instance,
47
+ role=rule.role,
48
+ skills=rule.skills,
49
+ allowed_writes=writes,
50
+ budget=rule.budget,
51
+ )
52
+
53
+
54
+ def outcome_for(result: AgentResult, *, task_id: str) -> ExecOutcome:
55
+ """Map an `AgentResult` to the scheduler's `ExecOutcome`."""
56
+ if result.status == "done":
57
+ return ExecOutcome(ok=True)
58
+ label: FailureLabel = "planning" if result.status == "needs_human" else "verification"
59
+ detail = "; ".join(result.open_questions)
60
+ default = f"agent task {task_id!r} ended with status {result.status!r}"
61
+ return ExecOutcome(ok=False, failure_label=label, message=detail or default)
62
+
63
+
64
+ class AgentRuntimeExecutor:
65
+ """An `Executor` for `kind: agent` rules, backed by an `AgentRuntime`."""
66
+
67
+ def __init__(self, runtime: AgentRuntime) -> None:
68
+ self.runtime = runtime
69
+
70
+ async def execute(self, rule: RuleSpec, instance: RuleInstance) -> ExecOutcome:
71
+ try:
72
+ task = task_for(rule, instance)
73
+ except ValueError as exc:
74
+ return ExecOutcome(ok=False, failure_label="planning", message=str(exc))
75
+ result = await self.runtime.run_task(task)
76
+ return outcome_for(result, task_id=instance.instance_id)
77
+
78
+
79
+ __all__ = ["AgentRuntimeExecutor", "instance_inputs_hash", "outcome_for", "task_for"]