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,95 @@
1
+ """The Design Model key grammar: how every entity gets a stable, parseable ``key``.
2
+
3
+ A model key has the form::
4
+
5
+ <kind>:<part>[.<part>...]
6
+
7
+ ``kind`` is the entity kind (``block``, ``module``, ``port``, ``register``, ``field``,
8
+ ``requirement``, ...). The parts after ``:`` are joined with ``.`` and describe the
9
+ entity's position, most-general first. There is no single fixed number of parts: each
10
+ kind picks a grammar that is convenient and stable for that kind, for example:
11
+
12
+ ====================== ===============================
13
+ Kind Example key
14
+ ====================== ===============================
15
+ ``project`` ``project:qsoc``
16
+ ``block`` ``block:timer``
17
+ ``module`` ``module:tiny_timer``
18
+ ``port`` ``port:tiny_timer.i_clk``
19
+ ``clock`` ``clock:peri``
20
+ ``reset`` ``reset:rst_n``
21
+ ``parameter`` ``parameter:tiny_timer.WIDTH``
22
+ ``register`` ``register:timer.CTRL``
23
+ ``field`` ``field:timer.CTRL.EN``
24
+ ``interrupt`` ``interrupt:timer.overflow``
25
+ ``memory_region`` ``memory_region:timer``
26
+ ``interface`` ``interface:timer.apb``
27
+ ``requirement`` ``requirement:REQ-TIM-004``
28
+ ``decision`` ``decision:D-timer-0001``
29
+ ``open_item`` ``open_item:OI-timer-0001``
30
+ ``test`` ``test:tb_timer_cnt.test_overflow``
31
+ ====================== ===============================
32
+
33
+ A part must not contain ``:`` (reserved for the kind separator) or be empty. Parts may
34
+ contain ``.`` internally only if that ``.`` is not meant as a separator; callers that
35
+ need a literal ``.`` inside a part should avoid this grammar or escape at a higher
36
+ layer, since ``parse_key`` always splits on every ``.``.
37
+
38
+ Extension kinds registered by packs (:mod:`chipgraph.core.model.registry`) follow the
39
+ same grammar: ``<pack_kind>:<part>[.<part>...]``.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ _KIND_SEPARATOR = ":"
45
+ _PART_SEPARATOR = "."
46
+
47
+
48
+ class InvalidKeyError(ValueError):
49
+ """Raised when a model key does not follow the key grammar."""
50
+
51
+
52
+ def make_key(kind: str, *parts: str) -> str:
53
+ """Build a model key for ``kind`` from one or more ``parts``.
54
+
55
+ Example: ``make_key("register", "timer", "CTRL") == "register:timer.CTRL"``.
56
+ """
57
+ if not kind:
58
+ raise InvalidKeyError("kind must not be empty")
59
+ if _KIND_SEPARATOR in kind:
60
+ raise InvalidKeyError(f"kind must not contain {_KIND_SEPARATOR!r}: {kind!r}")
61
+ if not parts:
62
+ raise InvalidKeyError(f"make_key({kind!r}) needs at least one part")
63
+ for part in parts:
64
+ _check_part(part)
65
+ return f"{kind}{_KIND_SEPARATOR}{_PART_SEPARATOR.join(parts)}"
66
+
67
+
68
+ def parse_key(key: str) -> tuple[str, tuple[str, ...]]:
69
+ """Split a model key into ``(kind, parts)``. Raises `InvalidKeyError` if malformed."""
70
+ if _KIND_SEPARATOR not in key:
71
+ raise InvalidKeyError(f"not a valid model key (missing {_KIND_SEPARATOR!r}): {key!r}")
72
+ kind, _, rest = key.partition(_KIND_SEPARATOR)
73
+ if not kind:
74
+ raise InvalidKeyError(f"not a valid model key (empty kind): {key!r}")
75
+ if not rest:
76
+ raise InvalidKeyError(
77
+ f"not a valid model key (no parts after {_KIND_SEPARATOR!r}): {key!r}"
78
+ )
79
+ parts = tuple(rest.split(_PART_SEPARATOR))
80
+ for part in parts:
81
+ _check_part(part)
82
+ return kind, parts
83
+
84
+
85
+ def key_kind(key: str) -> str:
86
+ """Return just the kind of a model key, without fully parsing its parts."""
87
+ kind, _ = parse_key(key)
88
+ return kind
89
+
90
+
91
+ def _check_part(part: str) -> None:
92
+ if not part:
93
+ raise InvalidKeyError("key parts must not be empty")
94
+ if _KIND_SEPARATOR in part:
95
+ raise InvalidKeyError(f"key parts must not contain {_KIND_SEPARATOR!r}: {part!r}")
@@ -0,0 +1,153 @@
1
+ """`DesignModel`: an immutable in-memory set of entities and relations.
2
+
3
+ This is the object every query in M1-02 (`model.block(...)`, `model.find(...)`, ...) is
4
+ built on top of. It never talks to disk: that is `ModelStore`'s job.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterable, Mapping
10
+ from dataclasses import dataclass, field
11
+
12
+ from chipgraph.core.model.entities import EntityBase
13
+ from chipgraph.core.model.provenance import Provenance
14
+ from chipgraph.core.model.relations import Relation
15
+
16
+
17
+ class ModelConflict(Exception):
18
+ """Two entities share a `key` but disagree on content.
19
+
20
+ Raised by `DesignModel.merge()` (and the `build`/`from_entities` constructors) when
21
+ the same key appears with two different entities. Both provenances are attached so
22
+ the caller can point a human at both sources.
23
+ """
24
+
25
+ def __init__(self, key: str, first: EntityBase, second: EntityBase) -> None:
26
+ self.key = key
27
+ self.first = first
28
+ self.second = second
29
+ super().__init__(
30
+ f"conflicting entities for key {key!r}: {_describe(first)} vs {_describe(second)}"
31
+ )
32
+
33
+
34
+ def _describe(entity: EntityBase) -> str:
35
+ return f"kind={entity.kind!r} source={entity.source!r}"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class DesignModel:
40
+ """An immutable snapshot of entities and relations.
41
+
42
+ Construct with `DesignModel.build(...)`, not the dataclass constructor directly,
43
+ so duplicate keys are always checked.
44
+ """
45
+
46
+ entities: Mapping[str, EntityBase] = field(default_factory=dict)
47
+ relations: tuple[Relation, ...] = field(default_factory=tuple)
48
+
49
+ @classmethod
50
+ def build(
51
+ cls, entities: Iterable[EntityBase], relations: Iterable[Relation] = ()
52
+ ) -> DesignModel:
53
+ """Build a `DesignModel`, raising `ModelConflict` on duplicate, differing keys."""
54
+ by_key: dict[str, EntityBase] = {}
55
+ for entity in entities:
56
+ existing = by_key.get(entity.key)
57
+ if existing is not None and existing != entity:
58
+ raise ModelConflict(entity.key, existing, entity)
59
+ by_key[entity.key] = entity
60
+ return cls(entities=by_key, relations=tuple(relations))
61
+
62
+ def get(self, key: str) -> EntityBase | None:
63
+ """Return the entity for `key`, or None if it is not in the model."""
64
+ return self.entities.get(key)
65
+
66
+ def by_kind(self, kind: str) -> tuple[EntityBase, ...]:
67
+ """Return every entity of a given `kind`, in insertion order."""
68
+ return tuple(e for e in self.entities.values() if e.kind == kind)
69
+
70
+ def children(self, key: str) -> tuple[EntityBase, ...]:
71
+ """Return entities this entity `contains` or `instantiates`, in relation order."""
72
+ hierarchy_kinds = ("contains", "instantiates")
73
+ dsts = [r.dst for r in self.relations if r.src == key and r.kind in hierarchy_kinds]
74
+ return tuple(self.entities[dst] for dst in dsts if dst in self.entities)
75
+
76
+ def find(self, kind: str | None = None, **attrs: object) -> tuple[EntityBase, ...]:
77
+ """Return entities matching `kind` (if given) and every `attrs` filter.
78
+
79
+ Each keyword is matched first against a typed field on the entity, then against
80
+ `entity.attrs`. An entity that lacks the field/attr entirely does not match.
81
+ """
82
+ result: list[EntityBase] = []
83
+ for entity in self.entities.values():
84
+ if kind is not None and entity.kind != kind:
85
+ continue
86
+ if _matches(entity, attrs):
87
+ result.append(entity)
88
+ return tuple(result)
89
+
90
+ def get_relations(
91
+ self,
92
+ src: str | None = None,
93
+ dst: str | None = None,
94
+ kind: str | None = None,
95
+ ) -> tuple[Relation, ...]:
96
+ """Return relations filtered by any combination of `src`, `dst`, `kind`."""
97
+ return tuple(
98
+ r
99
+ for r in self.relations
100
+ if (src is None or r.src == src)
101
+ and (dst is None or r.dst == dst)
102
+ and (kind is None or r.kind == kind)
103
+ )
104
+
105
+ def merge(self, other: DesignModel) -> DesignModel:
106
+ """Return a new `DesignModel` combining `self` and `other`.
107
+
108
+ Raises `ModelConflict` if the same key maps to different entities in the two
109
+ models. Relations are concatenated (duplicates are harmless and kept as-is).
110
+ """
111
+ by_key: dict[str, EntityBase] = dict(self.entities)
112
+ for key, entity in other.entities.items():
113
+ existing = by_key.get(key)
114
+ if existing is not None and existing != entity:
115
+ raise ModelConflict(key, existing, entity)
116
+ by_key[key] = entity
117
+ return DesignModel(entities=by_key, relations=self.relations + other.relations)
118
+
119
+ def validate_relations(self) -> list[str]:
120
+ """Return human-readable errors for every relation with a dangling endpoint.
121
+
122
+ Does not raise: callers decide whether dangling relations should block anything.
123
+ An empty list means every relation's `src` and `dst` resolve to a known entity.
124
+ """
125
+ errors: list[str] = []
126
+ for i, relation in enumerate(self.relations):
127
+ if relation.src not in self.entities:
128
+ errors.append(
129
+ f"relation[{i}] kind={relation.kind!r}: "
130
+ f"src {relation.src!r} is not a known entity"
131
+ )
132
+ if relation.dst not in self.entities:
133
+ errors.append(
134
+ f"relation[{i}] kind={relation.kind!r}: "
135
+ f"dst {relation.dst!r} is not a known entity"
136
+ )
137
+ return errors
138
+
139
+
140
+ def _matches(entity: EntityBase, attrs: Mapping[str, object]) -> bool:
141
+ for name, expected in attrs.items():
142
+ if hasattr(entity, name):
143
+ if getattr(entity, name) != expected:
144
+ return False
145
+ elif name in entity.attrs:
146
+ if entity.attrs[name] != expected:
147
+ return False
148
+ else:
149
+ return False
150
+ return True
151
+
152
+
153
+ __all__ = ["DesignModel", "ModelConflict", "Provenance"]
@@ -0,0 +1,27 @@
1
+ """Provenance: where a Design Model fact came from."""
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.contracts.types import Sha256
10
+
11
+
12
+ class Provenance(BaseModel):
13
+ """Records where a single entity or relation fact was learned from."""
14
+
15
+ model_config = ConfigDict(extra="forbid", frozen=True)
16
+
17
+ schema_version: Literal[1] = Field(default=1, description="Schema version of this model.")
18
+ file: str | None = Field(
19
+ default=None, description="POSIX path the fact was read from, relative to the repo root."
20
+ )
21
+ line: int | None = Field(default=None, ge=1, description="The 1-based line number, if any.")
22
+ extractor: str | None = Field(
23
+ default=None, description="The extractor or format adapter id that produced this fact."
24
+ )
25
+ artifact: Sha256 | None = Field(
26
+ default=None, description="Content hash of the source artifact, if known."
27
+ )