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,61 @@
1
+ """Relations between Design Model entities (DESIGN.md 4.2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from chipgraph.core.model.json_value import JSONValue
10
+ from chipgraph.core.model.provenance import Provenance
11
+
12
+ CoreRelationKind = Literal[
13
+ "implements",
14
+ "verifies",
15
+ "connects",
16
+ "derives_from",
17
+ "contains",
18
+ "instantiates",
19
+ "instance_of",
20
+ ]
21
+ """The relation kinds DESIGN.md names, plus `contains`/`instantiates`/`instance_of`.
22
+
23
+ DESIGN.md 4.2 only lists `implements`, `verifies`, `connects`, `derives_from`. Building
24
+ `DesignModel.children()` needs a way to walk hierarchy (block -> module -> port,
25
+ module -> module instance), so this adds `contains` (a owns b, e.g. block contains
26
+ module) and `instantiates` (a module instantiates another module) as core kinds.
27
+ `instance_of` links a block that is an instance on the memory map to the IP block it is a
28
+ copy of (`block:<instance>` -> `block:<ip>`, D38). Packs may add further relation kinds;
29
+ `Relation.kind` is a plain `str` so those round-trip too.
30
+ """
31
+
32
+ CORE_RELATION_KINDS: tuple[str, ...] = (
33
+ "implements",
34
+ "verifies",
35
+ "connects",
36
+ "derives_from",
37
+ "contains",
38
+ "instantiates",
39
+ "instance_of",
40
+ )
41
+
42
+
43
+ class Relation(BaseModel):
44
+ """A directed edge between two entities, identified by their model keys."""
45
+
46
+ model_config = ConfigDict(extra="forbid", frozen=True)
47
+
48
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
49
+ kind: str = Field(
50
+ description=(
51
+ "The relation kind: one of CORE_RELATION_KINDS, or a pack-defined kind string."
52
+ )
53
+ )
54
+ src: str = Field(description="Model key of the source entity.")
55
+ dst: str = Field(description="Model key of the destination entity.")
56
+ source: Provenance = Field(
57
+ default_factory=Provenance, description="Where this fact was learned from."
58
+ )
59
+ attrs: dict[str, JSONValue] = Field(
60
+ default_factory=dict, description="Open-ended extra facts not covered by typed fields."
61
+ )
@@ -0,0 +1,279 @@
1
+ """`ModelStore`: the SQLite-backed, rebuildable index for a `DesignModel` (DESIGN.md 4.5).
2
+
3
+ The index lives at ``.chipgraph/state/cache/model.db`` by default (`default_model_db_path`)
4
+ and is never the source of truth: it is always rebuildable from the project's own spec
5
+ and RTL files (M1-06 `ingest`). `write()` replaces the whole file atomically so a reader
6
+ never observes a half-written database, and `read()` reconstructs an equivalent
7
+ `DesignModel` from it.
8
+
9
+ The store also holds an FTS5 index, both over entities (name plus their text-ish
10
+ fields/attrs) and over registered documents (`add_document`), for the `/ask` full-text
11
+ search in a later task (M1-13). If the running Python's sqlite3 lacks the FTS5
12
+ extension, `ModelStore()` raises `ModelStoreError` immediately, rather than failing
13
+ later on first use.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import sqlite3
21
+ import uuid
22
+ from collections.abc import Iterable, Sequence
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ from chipgraph.core.model.entities import EntityBase
27
+ from chipgraph.core.model.model import DesignModel
28
+ from chipgraph.core.model.registry import DEFAULT_ENTITY_KINDS, EntityKinds
29
+ from chipgraph.core.model.relations import Relation
30
+ from chipgraph.core.state.layout import StateLayout
31
+
32
+ _SCHEMA_VERSION = 1
33
+
34
+
35
+ class ModelStoreError(RuntimeError):
36
+ """Raised for store-level problems, e.g. a SQLite build without the FTS5 extension."""
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class SearchHit:
41
+ """A single full-text search result: either a model entity, or a document line."""
42
+
43
+ text: str
44
+ score: float
45
+ key: str | None = None
46
+ file: str | None = None
47
+ line: int | None = None
48
+
49
+ @property
50
+ def citation(self) -> str:
51
+ """A human-readable citation: the model key, or ``file:line``."""
52
+ if self.key is not None:
53
+ return self.key
54
+ if self.file is not None and self.line is not None:
55
+ return f"{self.file}:{self.line}"
56
+ if self.file is not None:
57
+ return self.file
58
+ raise ValueError("SearchHit has neither a key nor a file to cite")
59
+
60
+
61
+ def default_model_db_path(root: Path) -> Path:
62
+ """The default `ModelStore` path for a project rooted at `root`."""
63
+ return StateLayout(root).cache_dir / "model.db"
64
+
65
+
66
+ class ModelStore:
67
+ """Reads and writes a `DesignModel` to a single SQLite file at `path`."""
68
+
69
+ def __init__(self, path: Path, entity_kinds: EntityKinds | None = None) -> None:
70
+ _check_fts5_available()
71
+ self.path = path
72
+ self._entity_kinds = entity_kinds if entity_kinds is not None else DEFAULT_ENTITY_KINDS
73
+
74
+ def write(self, model: DesignModel, *, build_inputs_hash: str | None = None) -> None:
75
+ """Atomically replace the store's content with `model`.
76
+
77
+ Writes a fresh database to a temp file next to `path`, then `os.replace`s it
78
+ into place. If anything fails before the replace, `path` is left untouched.
79
+ """
80
+ self.path.parent.mkdir(parents=True, exist_ok=True)
81
+ tmp_path = self.path.with_name(f".{self.path.name}.tmp-{os.getpid()}-{uuid.uuid4().hex}")
82
+ try:
83
+ conn = sqlite3.connect(tmp_path)
84
+ try:
85
+ _init_schema(conn)
86
+ _write_entities(conn, model.entities.values())
87
+ _write_relations(conn, model.relations)
88
+ _write_meta(conn, build_inputs_hash=build_inputs_hash)
89
+ conn.commit()
90
+ finally:
91
+ conn.close()
92
+ os.replace(tmp_path, self.path)
93
+ finally:
94
+ tmp_path.unlink(missing_ok=True)
95
+
96
+ def read(self) -> DesignModel:
97
+ """Return the `DesignModel` currently stored, or an empty model if none exists."""
98
+ if not self.path.is_file():
99
+ return DesignModel.build([], [])
100
+ conn = sqlite3.connect(self.path)
101
+ try:
102
+ entities = [
103
+ self._entity_kinds.parse(json.loads(raw))
104
+ for (raw,) in conn.execute("SELECT json FROM entities")
105
+ ]
106
+ relations = [
107
+ Relation.model_validate(json.loads(raw))
108
+ for (raw,) in conn.execute("SELECT json FROM relations")
109
+ ]
110
+ finally:
111
+ conn.close()
112
+ return DesignModel.build(entities, relations)
113
+
114
+ def add_document(
115
+ self,
116
+ path: str,
117
+ text: str,
118
+ anchors: Sequence[tuple[int, str]] | None = None,
119
+ ) -> None:
120
+ """Register a document's text for full-text search, replacing any prior copy.
121
+
122
+ Stores one row per line so hits can cite `file:line`. By default, lines are
123
+ `text.splitlines()` numbered from 1; pass `anchors` (explicit `(line, text)`
124
+ pairs) when the caller already knows the true source line for each chunk (e.g.
125
+ after stripping something from the original file).
126
+ """
127
+ conn = self._connect_ensuring_schema()
128
+ try:
129
+ conn.execute("DELETE FROM documents WHERE path = ?", (path,))
130
+ conn.execute("DELETE FROM documents_fts WHERE path = ?", (path,))
131
+ rows = anchors if anchors is not None else list(enumerate(text.splitlines(), start=1))
132
+ conn.executemany(
133
+ "INSERT INTO documents (path, line, text) VALUES (?, ?, ?)",
134
+ [(path, line, line_text) for line, line_text in rows],
135
+ )
136
+ conn.executemany(
137
+ "INSERT INTO documents_fts (path, line, text) VALUES (?, ?, ?)",
138
+ [(path, line, line_text) for line, line_text in rows],
139
+ )
140
+ conn.commit()
141
+ finally:
142
+ conn.close()
143
+
144
+ def search(
145
+ self,
146
+ query: str,
147
+ kinds: Iterable[str] | None = None,
148
+ limit: int = 20,
149
+ ) -> tuple[SearchHit, ...]:
150
+ """Full-text search over entities and (unless `kinds` is given) documents.
151
+
152
+ Returns hits ordered by relevance (best first). Each hit cites either a model
153
+ `key` (entity) or `file`/`line` (document).
154
+ """
155
+ if not self.path.is_file():
156
+ return ()
157
+ conn = sqlite3.connect(self.path)
158
+ try:
159
+ hits: list[SearchHit] = []
160
+ kind_set = set(kinds) if kinds is not None else None
161
+ entity_rows = conn.execute(
162
+ "SELECT key, kind, text, bm25(entities_fts) FROM entities_fts "
163
+ "WHERE entities_fts MATCH ? ORDER BY bm25(entities_fts) LIMIT ?",
164
+ (query, limit),
165
+ )
166
+ for key, kind, text, score in entity_rows:
167
+ if kind_set is not None and kind not in kind_set:
168
+ continue
169
+ hits.append(SearchHit(key=key, text=text, score=score))
170
+ if kind_set is None:
171
+ doc_rows = conn.execute(
172
+ "SELECT path, line, text, bm25(documents_fts) FROM documents_fts "
173
+ "WHERE documents_fts MATCH ? ORDER BY bm25(documents_fts) LIMIT ?",
174
+ (query, limit),
175
+ )
176
+ for doc_path, line, text, score in doc_rows:
177
+ hits.append(SearchHit(file=doc_path, line=line, text=text, score=score))
178
+ finally:
179
+ conn.close()
180
+ hits.sort(key=lambda hit: hit.score)
181
+ return tuple(hits[:limit])
182
+
183
+ def _connect_ensuring_schema(self) -> sqlite3.Connection:
184
+ self.path.parent.mkdir(parents=True, exist_ok=True)
185
+ is_new = not self.path.is_file()
186
+ conn = sqlite3.connect(self.path)
187
+ if is_new:
188
+ _init_schema(conn)
189
+ conn.commit()
190
+ return conn
191
+
192
+
193
+ def _check_fts5_available() -> None:
194
+ conn = sqlite3.connect(":memory:")
195
+ try:
196
+ conn.execute("CREATE VIRTUAL TABLE _fts5_check USING fts5(x)")
197
+ except sqlite3.OperationalError as exc:
198
+ raise ModelStoreError(
199
+ "this Python's sqlite3 build lacks the FTS5 extension, which ModelStore "
200
+ "requires for full-text search over the Design Model; rebuild Python (or its "
201
+ "sqlite3 dependency) with FTS5 enabled"
202
+ ) from exc
203
+ finally:
204
+ conn.close()
205
+
206
+
207
+ def _init_schema(conn: sqlite3.Connection) -> None:
208
+ conn.executescript(
209
+ """
210
+ CREATE TABLE entities (
211
+ key TEXT PRIMARY KEY,
212
+ kind TEXT NOT NULL,
213
+ name TEXT NOT NULL,
214
+ json TEXT NOT NULL
215
+ );
216
+ CREATE TABLE relations (
217
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
218
+ kind TEXT NOT NULL,
219
+ src TEXT NOT NULL,
220
+ dst TEXT NOT NULL,
221
+ json TEXT NOT NULL
222
+ );
223
+ CREATE TABLE meta (
224
+ key TEXT PRIMARY KEY,
225
+ value TEXT NOT NULL
226
+ );
227
+ CREATE VIRTUAL TABLE entities_fts USING fts5(key UNINDEXED, kind UNINDEXED, text);
228
+ CREATE TABLE documents (
229
+ path TEXT NOT NULL,
230
+ line INTEGER NOT NULL,
231
+ text TEXT NOT NULL
232
+ );
233
+ CREATE VIRTUAL TABLE documents_fts USING fts5(path UNINDEXED, line UNINDEXED, text);
234
+ """
235
+ )
236
+
237
+
238
+ def _write_entities(conn: sqlite3.Connection, entities: Iterable[EntityBase]) -> None:
239
+ for entity in entities:
240
+ payload = json.dumps(entity.model_dump(mode="json"))
241
+ conn.execute(
242
+ "INSERT INTO entities (key, kind, name, json) VALUES (?, ?, ?, ?)",
243
+ (entity.key, entity.kind, entity.name, payload),
244
+ )
245
+ conn.execute(
246
+ "INSERT INTO entities_fts (key, kind, text) VALUES (?, ?, ?)",
247
+ (entity.key, entity.kind, _entity_search_text(entity)),
248
+ )
249
+
250
+
251
+ def _entity_search_text(entity: EntityBase) -> str:
252
+ parts = [entity.name]
253
+ for field_name in ("text", "rationale"):
254
+ value = getattr(entity, field_name, None)
255
+ if isinstance(value, str):
256
+ parts.append(value)
257
+ for value in entity.attrs.values():
258
+ if isinstance(value, str):
259
+ parts.append(value)
260
+ return " ".join(parts)
261
+
262
+
263
+ def _write_relations(conn: sqlite3.Connection, relations: Iterable[Relation]) -> None:
264
+ for relation in relations:
265
+ payload = json.dumps(relation.model_dump(mode="json"))
266
+ conn.execute(
267
+ "INSERT INTO relations (kind, src, dst, json) VALUES (?, ?, ?, ?)",
268
+ (relation.kind, relation.src, relation.dst, payload),
269
+ )
270
+
271
+
272
+ def _write_meta(conn: sqlite3.Connection, *, build_inputs_hash: str | None) -> None:
273
+ conn.execute(
274
+ "INSERT INTO meta (key, value) VALUES ('schema_version', ?)", (str(_SCHEMA_VERSION),)
275
+ )
276
+ if build_inputs_hash is not None:
277
+ conn.execute(
278
+ "INSERT INTO meta (key, value) VALUES ('build_inputs_hash', ?)", (build_inputs_hash,)
279
+ )
@@ -0,0 +1,81 @@
1
+ """The plugin API: protocols adapters implement, a registry, and pack loading.
2
+
3
+ `chipgraph.core.plugin_api` knows nothing about chips or tools: no project, bus, PDK or
4
+ tool names appear here. Concrete adapters live under `chipgraph.adapters`, which core
5
+ never imports.
6
+ """
7
+
8
+ from chipgraph.core.plugin_api.local import (
9
+ LocalPluginApi,
10
+ LocalPluginRecord,
11
+ load_local_plugins,
12
+ )
13
+ from chipgraph.core.plugin_api.pack import (
14
+ Pack,
15
+ PackManifest,
16
+ PackProvides,
17
+ PackRequires,
18
+ discover_packs,
19
+ load_pack,
20
+ resolve_requires,
21
+ )
22
+ from chipgraph.core.plugin_api.protocols import (
23
+ AgentRuntime,
24
+ Check,
25
+ Extractor,
26
+ FormatAdapter,
27
+ Generator,
28
+ LlmProvider,
29
+ LogParser,
30
+ ReviewAdapter,
31
+ Runner,
32
+ ToolAdapter,
33
+ VcsAdapter,
34
+ )
35
+ from chipgraph.core.plugin_api.registry import KINDS, PluginError, Registry
36
+ from chipgraph.core.plugin_api.types import (
37
+ AgentTask,
38
+ LlmMessage,
39
+ LlmRequest,
40
+ LlmResponse,
41
+ RunResult,
42
+ ToolContext,
43
+ )
44
+
45
+ __all__ = [
46
+ "KINDS",
47
+ "AgentRuntime",
48
+ "AgentTask",
49
+ "Check",
50
+ "Extractor",
51
+ "FormatAdapter",
52
+ "Generator",
53
+ "LlmMessage",
54
+ "LlmProvider",
55
+ "LlmRequest",
56
+ "LlmResponse",
57
+ "LocalPluginApi",
58
+ "LocalPluginRecord",
59
+ "LogParser",
60
+ "Pack",
61
+ "PackManifest",
62
+ "PackProvides",
63
+ "PackRequires",
64
+ "PluginError",
65
+ "Registry",
66
+ "ReviewAdapter",
67
+ "RunResult",
68
+ "Runner",
69
+ "ToolAdapter",
70
+ "ToolContext",
71
+ "VcsAdapter",
72
+ "discover_packs",
73
+ "load_local_plugins",
74
+ "load_pack",
75
+ "resolve_requires",
76
+ ]
77
+
78
+ # `ToolContext.runner` names the `Runner` protocol, which types.py can only import under
79
+ # TYPE_CHECKING (protocols.py imports types.py). Resolve the reference once, here, so any
80
+ # import of the plugin API gives a usable `ToolContext`.
81
+ ToolContext.model_rebuild(_types_namespace={"Runner": Runner})
@@ -0,0 +1,167 @@
1
+ """Load a project's own plugins from files it lists, through the plugin API.
2
+
3
+ A project may keep small Python plugins next to its code, under `<root>/.chipgraph/plugins/`,
4
+ and list them in the profile so the tool loads them into the registry. They run code: an
5
+ organization can forbid them with a `deny` policy, a user can switch them off with an
6
+ environment variable, and the caller decides whether loading is enabled.
7
+
8
+ This module stays generic: it knows nothing about chips, tools or any concrete plugin kind.
9
+ A listed file must define ``register(api)`` and call ``api.register(kind, name, obj)`` for
10
+ each thing it contributes; a class is instantiated the same way the entry-point `discover`
11
+ does. A plugin may not replace an existing registration.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import importlib.util
18
+ import inspect
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from pydantic import BaseModel, ConfigDict, Field
23
+
24
+ from chipgraph.core.plugin_api.registry import PluginError, Registry
25
+
26
+ _PLUGINS_SUBDIR = (".chipgraph", "plugins")
27
+
28
+
29
+ class LocalPluginRecord(BaseModel):
30
+ """What loading one listed plugin produced: its file, its hash, what it registered."""
31
+
32
+ model_config = ConfigDict(extra="forbid", frozen=True)
33
+
34
+ entry: str = Field(description="The plugin path exactly as the profile listed it.")
35
+ path: str | None = Field(
36
+ default=None,
37
+ description="Repo-relative path of the resolved file, or None when not loaded.",
38
+ )
39
+ sha256: str | None = Field(
40
+ default=None, description="SHA-256 of the file's bytes, or None when not loaded."
41
+ )
42
+ registered: tuple[tuple[str, str], ...] = Field(
43
+ default=(), description="The (kind, name) pairs this plugin registered, in order."
44
+ )
45
+ loaded: bool = Field(default=True, description="Whether the plugin was actually loaded.")
46
+ disabled_reason: str | None = Field(
47
+ default=None, description="Why loading was skipped, when it was."
48
+ )
49
+
50
+
51
+ class LocalPluginApi:
52
+ """The object handed to a plugin's ``register`` function.
53
+
54
+ It forwards to the real `Registry` and records every ``(kind, name)`` the plugin
55
+ registers, so callers can report what a plugin contributed. It mirrors the registry's
56
+ own rules: a class is instantiated, a duplicate name is refused.
57
+ """
58
+
59
+ def __init__(self, registry: Registry) -> None:
60
+ self._registry = registry
61
+ self._registered: list[tuple[str, str]] = []
62
+
63
+ def register(self, kind: str, name: str, obj: object) -> None:
64
+ """Register `obj` under `kind`/`name`, instantiating it if it is a class/factory."""
65
+ instance = obj() if inspect.isclass(obj) or inspect.isfunction(obj) else obj
66
+ self._registry.register(kind, name, instance)
67
+ self._registered.append((kind, name))
68
+
69
+ @property
70
+ def registered(self) -> tuple[tuple[str, str], ...]:
71
+ """The (kind, name) pairs registered so far, in registration order."""
72
+ return tuple(self._registered)
73
+
74
+
75
+ def _resolve_under_plugins_dir(root: Path, entry: str) -> Path:
76
+ """Resolve `entry` (repo-relative) to a real `.py` file under `<root>/.chipgraph/plugins/`.
77
+
78
+ Rejects absolute paths, `..` escapes, files outside the plugins directory (after
79
+ following symlinks), missing files, and non-`.py` files, each as a `PluginError`.
80
+ """
81
+ if Path(entry).is_absolute():
82
+ raise PluginError(f"local plugin {entry!r}: must be a repo-relative path, not absolute")
83
+
84
+ plugins_dir = root.joinpath(*_PLUGINS_SUBDIR).resolve()
85
+ target = (root / entry).resolve()
86
+
87
+ if not target.is_relative_to(plugins_dir):
88
+ raise PluginError(
89
+ f"local plugin {entry!r}: must resolve to a file under "
90
+ f"{Path(*_PLUGINS_SUBDIR)}/, got {target}"
91
+ )
92
+ if not target.is_file():
93
+ raise PluginError(f"local plugin {entry!r}: no file at {target}")
94
+ if target.suffix != ".py":
95
+ raise PluginError(f"local plugin {entry!r}: must be a .py file")
96
+ return target
97
+
98
+
99
+ def _module_name(path: Path, digest: str) -> str:
100
+ """A unique module name that cannot shadow a real package."""
101
+ return f"chipgraph_local_plugin_{path.stem}_{digest[:8]}"
102
+
103
+
104
+ def _load_one(root: Path, entry: str, registry: Registry) -> LocalPluginRecord:
105
+ root = root.resolve()
106
+ target = _resolve_under_plugins_dir(root, entry)
107
+ data = target.read_bytes()
108
+ digest = hashlib.sha256(data).hexdigest()
109
+ rel = target.relative_to(root).as_posix()
110
+
111
+ module_name = _module_name(target, digest)
112
+ spec = importlib.util.spec_from_file_location(module_name, target)
113
+ if spec is None or spec.loader is None: # pragma: no cover - defensive
114
+ raise PluginError(f"local plugin {entry!r}: cannot create an import spec for {target}")
115
+
116
+ module = importlib.util.module_from_spec(spec)
117
+ # The module must be in `sys.modules` while it runs (e.g. `dataclasses` looks its
118
+ # module up there). The name is unique to this file's content, so it cannot shadow a
119
+ # real package; it is removed again if the import fails.
120
+ sys.modules[module_name] = module
121
+ try:
122
+ spec.loader.exec_module(module)
123
+ except Exception as exc:
124
+ sys.modules.pop(module_name, None)
125
+ raise PluginError(f"local plugin {entry!r}: failed to import: {exc}") from exc
126
+
127
+ register = getattr(module, "register", None)
128
+ if not callable(register):
129
+ raise PluginError(f"local plugin {entry!r}: must define a callable register(api)")
130
+
131
+ api = LocalPluginApi(registry)
132
+ try:
133
+ register(api)
134
+ except PluginError as exc:
135
+ raise PluginError(f"local plugin {entry!r}: {exc}") from exc
136
+ except Exception as exc:
137
+ raise PluginError(f"local plugin {entry!r}: register() failed: {exc}") from exc
138
+
139
+ return LocalPluginRecord(
140
+ entry=entry, path=rel, sha256=digest, registered=api.registered, loaded=True
141
+ )
142
+
143
+
144
+ def load_local_plugins(
145
+ root: Path,
146
+ entries: tuple[str, ...],
147
+ registry: Registry,
148
+ *,
149
+ enabled: bool,
150
+ disabled_reason: str | None = None,
151
+ ) -> tuple[LocalPluginRecord, ...]:
152
+ """Load each listed plugin into `registry`, in profile order.
153
+
154
+ When `enabled` is False nothing is loaded: a record per entry is returned, marked not
155
+ loaded, carrying `disabled_reason`. When enabled, each entry is resolved under
156
+ `<root>/.chipgraph/plugins/`, imported and asked to `register`; any failure raises a
157
+ `PluginError` naming the entry (the cause stays chained).
158
+ """
159
+ if not enabled:
160
+ return tuple(
161
+ LocalPluginRecord(entry=entry, loaded=False, disabled_reason=disabled_reason)
162
+ for entry in entries
163
+ )
164
+ return tuple(_load_one(root, entry, registry) for entry in entries)
165
+
166
+
167
+ __all__ = ["LocalPluginApi", "LocalPluginRecord", "load_local_plugins"]