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.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""Query API on the Design Model.
|
|
2
|
+
|
|
3
|
+
All query results are frozen pydantic models suitable for JSON serialization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import difflib
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from fnmatch import fnmatch
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from chipgraph.core.model.entities import EntityBase
|
|
17
|
+
from chipgraph.core.model.model import DesignModel
|
|
18
|
+
from chipgraph.core.model.store import ModelStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueryError(ValueError):
|
|
22
|
+
"""Raised when a query cannot be satisfied (unknown key, name, etc.).
|
|
23
|
+
|
|
24
|
+
Message includes close matches (via difflib) to help the user find what they meant.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _close_matches(name: str, choices: Iterable[str]) -> list[str]:
|
|
29
|
+
"""Return the N closest matches from `choices` to `name`, in score descending order."""
|
|
30
|
+
return difflib.get_close_matches(name, list(choices), n=5, cutoff=0.6)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BlockInfo(BaseModel):
|
|
34
|
+
"""Result of `model.block(name)`: the block and what it contains."""
|
|
35
|
+
|
|
36
|
+
model_config = {"frozen": True}
|
|
37
|
+
|
|
38
|
+
key: str = Field(description="Model key of the block")
|
|
39
|
+
name: str = Field(description="Block name")
|
|
40
|
+
owner: str | None = Field(default=None, description="Block owner")
|
|
41
|
+
path: str | None = Field(default=None, description="Repo-relative path")
|
|
42
|
+
modules: list[str] = Field(default_factory=list, description="Model keys of contained modules")
|
|
43
|
+
ports: list[str] = Field(
|
|
44
|
+
default_factory=list, description="Model keys of ports from contained modules"
|
|
45
|
+
)
|
|
46
|
+
registers: list[str] = Field(
|
|
47
|
+
default_factory=list, description="Model keys of contained registers"
|
|
48
|
+
)
|
|
49
|
+
interrupts: list[str] = Field(
|
|
50
|
+
default_factory=list, description="Model keys of contained interrupts"
|
|
51
|
+
)
|
|
52
|
+
memory_regions: list[str] = Field(
|
|
53
|
+
default_factory=list, description="Model keys of memory regions"
|
|
54
|
+
)
|
|
55
|
+
clocks: list[str] = Field(
|
|
56
|
+
default_factory=list, description="Model keys of clocks (from contained modules)"
|
|
57
|
+
)
|
|
58
|
+
resets: list[str] = Field(
|
|
59
|
+
default_factory=list, description="Model keys of resets (from contained modules)"
|
|
60
|
+
)
|
|
61
|
+
requirements: list[str] = Field(
|
|
62
|
+
default_factory=list, description="Model keys of requirements for this block"
|
|
63
|
+
)
|
|
64
|
+
open_items: list[str] = Field(
|
|
65
|
+
default_factory=list, description="Model keys of open items for this block"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ModuleInfo(BaseModel):
|
|
70
|
+
"""Result of `model.module(name)`: a single RTL module and its contents."""
|
|
71
|
+
|
|
72
|
+
model_config = {"frozen": True}
|
|
73
|
+
|
|
74
|
+
key: str = Field(description="Model key of the module")
|
|
75
|
+
name: str = Field(description="Module name")
|
|
76
|
+
file: str | None = Field(default=None, description="Repo-relative file this module is in")
|
|
77
|
+
block: str | None = Field(default=None, description="Model key of the owning block")
|
|
78
|
+
ports: list[str] = Field(default_factory=list, description="Model keys of this module's ports")
|
|
79
|
+
parameters: list[str] = Field(
|
|
80
|
+
default_factory=list, description="Model keys of this module's parameters"
|
|
81
|
+
)
|
|
82
|
+
instances: list[dict[str, str]] = Field(
|
|
83
|
+
default_factory=list,
|
|
84
|
+
description="Child modules: list of {instance_name, module_key} dicts",
|
|
85
|
+
)
|
|
86
|
+
instantiated_by: list[dict[str, str]] = Field(
|
|
87
|
+
default_factory=list,
|
|
88
|
+
description="Parent modules: list of {instance_name, module_key} dicts",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EntityMatch(BaseModel):
|
|
93
|
+
"""A single entity matching a `find` query."""
|
|
94
|
+
|
|
95
|
+
model_config = {"frozen": True}
|
|
96
|
+
|
|
97
|
+
key: str = Field(description="Model key")
|
|
98
|
+
kind: str = Field(description="Entity kind")
|
|
99
|
+
name: str = Field(description="Entity name")
|
|
100
|
+
source: str = Field(description="Source file or description")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TraceResult(BaseModel):
|
|
104
|
+
"""Result of `model.trace(key)`: requirements and links."""
|
|
105
|
+
|
|
106
|
+
model_config = {"frozen": True}
|
|
107
|
+
|
|
108
|
+
key: str = Field(description="The entity key being traced")
|
|
109
|
+
kind: str = Field(description="The entity kind")
|
|
110
|
+
name: str = Field(description="The entity name")
|
|
111
|
+
# For requirement entities:
|
|
112
|
+
implements: list[str] = Field(
|
|
113
|
+
default_factory=list, description="Entities implementing this requirement"
|
|
114
|
+
)
|
|
115
|
+
verifies: list[str] = Field(
|
|
116
|
+
default_factory=list, description="Tests verifying this requirement"
|
|
117
|
+
)
|
|
118
|
+
derives_from: list[str] = Field(
|
|
119
|
+
default_factory=list, description="Requirements this requirement derives from"
|
|
120
|
+
)
|
|
121
|
+
no_test: bool = Field(
|
|
122
|
+
default=False, description="True if this requirement has no verifying test"
|
|
123
|
+
)
|
|
124
|
+
no_implementation: bool = Field(
|
|
125
|
+
default=False, description="True if this requirement has no implementation"
|
|
126
|
+
)
|
|
127
|
+
# For any entity:
|
|
128
|
+
traced_by: list[str] = Field(
|
|
129
|
+
default_factory=list,
|
|
130
|
+
description="Model keys of requirements that trace to this entity",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ImpactNode(BaseModel):
|
|
135
|
+
"""A single entity in an impact graph result."""
|
|
136
|
+
|
|
137
|
+
model_config = {"frozen": True}
|
|
138
|
+
|
|
139
|
+
key: str = Field(description="Model key")
|
|
140
|
+
kind: str = Field(description="Entity kind")
|
|
141
|
+
name: str = Field(description="Entity name")
|
|
142
|
+
path: list[str] = Field(
|
|
143
|
+
description="Path from the root to this node (as relation kinds and entity keys)"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ImpactResult(BaseModel):
|
|
148
|
+
"""Result of `model.impact(key)`: downstream entities grouped by kind."""
|
|
149
|
+
|
|
150
|
+
model_config = {"frozen": True}
|
|
151
|
+
|
|
152
|
+
key: str = Field(description="The change root key")
|
|
153
|
+
max_depth: int = Field(description="Depth limit for the traversal")
|
|
154
|
+
by_kind: dict[str, list[ImpactNode]] = Field(
|
|
155
|
+
default_factory=dict, description="Entities grouped by their kind"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class NeighborResult(BaseModel):
|
|
160
|
+
"""Result of `model.neighbors(key)`: adjacent entities."""
|
|
161
|
+
|
|
162
|
+
model_config = {"frozen": True}
|
|
163
|
+
|
|
164
|
+
key: str = Field(description="The entity key")
|
|
165
|
+
incoming: list[dict[str, Any]] = Field(
|
|
166
|
+
default_factory=list,
|
|
167
|
+
description="Relations where this entity is the destination: {src_key, relation_kind}",
|
|
168
|
+
)
|
|
169
|
+
outgoing: list[dict[str, Any]] = Field(
|
|
170
|
+
default_factory=list,
|
|
171
|
+
description="Relations where this entity is the source: {dst_key, relation_kind}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SearchResult(BaseModel):
|
|
176
|
+
"""A single result from full-text search."""
|
|
177
|
+
|
|
178
|
+
model_config = {"frozen": True}
|
|
179
|
+
|
|
180
|
+
text: str = Field(description="Matched text snippet")
|
|
181
|
+
score: float = Field(description="Relevance score (higher is better)")
|
|
182
|
+
citation: str = Field(description="Model key or file:line")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ModelQuery:
|
|
186
|
+
"""The query API on a `DesignModel`, with optional backing `ModelStore` for search."""
|
|
187
|
+
|
|
188
|
+
def __init__(self, model: DesignModel, store: ModelStore | None = None) -> None:
|
|
189
|
+
"""Initialize with a model (required) and optional store (for full-text search)."""
|
|
190
|
+
self.model = model
|
|
191
|
+
self.store = store
|
|
192
|
+
|
|
193
|
+
def block(self, name: str) -> BlockInfo:
|
|
194
|
+
"""Return the block and all contained entities."""
|
|
195
|
+
# Find the block by name (exact match via find)
|
|
196
|
+
all_blocks = self.model.by_kind("block")
|
|
197
|
+
block = None
|
|
198
|
+
for b in all_blocks:
|
|
199
|
+
if b.name == name:
|
|
200
|
+
block = b
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if block is None:
|
|
204
|
+
matches = _close_matches(name, [e.name for e in all_blocks])
|
|
205
|
+
msg = f"block {name!r} not found"
|
|
206
|
+
if matches:
|
|
207
|
+
msg += f"; did you mean: {', '.join(matches)}"
|
|
208
|
+
raise QueryError(msg)
|
|
209
|
+
|
|
210
|
+
# Collect contained entities via relations
|
|
211
|
+
modules: list[EntityBase] = []
|
|
212
|
+
for rel in self.model.relations:
|
|
213
|
+
if rel.src == block.key and rel.kind == "contains":
|
|
214
|
+
m = self.model.get(rel.dst)
|
|
215
|
+
if m and m.kind == "module":
|
|
216
|
+
modules.append(m)
|
|
217
|
+
|
|
218
|
+
module_keys = [m.key for m in modules]
|
|
219
|
+
|
|
220
|
+
# Collect ports and clocks/resets from modules in this block
|
|
221
|
+
ports: list[EntityBase] = []
|
|
222
|
+
clocks: set[str] = set()
|
|
223
|
+
resets: set[str] = set()
|
|
224
|
+
for mod in modules:
|
|
225
|
+
for entity in self.model.entities.values():
|
|
226
|
+
if entity.kind == "port" and getattr(entity, "module", None) == mod.key:
|
|
227
|
+
ports.append(entity)
|
|
228
|
+
# Collect port-level clock and reset
|
|
229
|
+
clock = getattr(entity, "clock", None)
|
|
230
|
+
if isinstance(clock, str):
|
|
231
|
+
clocks.add(clock)
|
|
232
|
+
reset = getattr(entity, "reset", None)
|
|
233
|
+
if isinstance(reset, str):
|
|
234
|
+
resets.add(reset)
|
|
235
|
+
|
|
236
|
+
# Registers, interrupts, memory regions
|
|
237
|
+
registers: list[EntityBase] = []
|
|
238
|
+
interrupts: list[EntityBase] = []
|
|
239
|
+
memory_regions: list[EntityBase] = []
|
|
240
|
+
for entity in self.model.entities.values():
|
|
241
|
+
if entity.kind == "register" and getattr(entity, "block", None) == block.key:
|
|
242
|
+
registers.append(entity)
|
|
243
|
+
elif entity.kind == "interrupt" and getattr(entity, "block", None) == block.key:
|
|
244
|
+
interrupts.append(entity)
|
|
245
|
+
elif entity.kind == "memory_region" and getattr(entity, "block", None) == block.key:
|
|
246
|
+
memory_regions.append(entity)
|
|
247
|
+
|
|
248
|
+
# Find clocks and resets by key
|
|
249
|
+
block_clocks = sorted(list(clocks))
|
|
250
|
+
block_resets = sorted(list(resets))
|
|
251
|
+
|
|
252
|
+
# Requirements: find all requirements that implement/verify entities within this block
|
|
253
|
+
block_entity_keys = {block.key}
|
|
254
|
+
for mod in modules:
|
|
255
|
+
block_entity_keys.add(mod.key)
|
|
256
|
+
for port in ports:
|
|
257
|
+
block_entity_keys.add(port.key)
|
|
258
|
+
for reg in registers:
|
|
259
|
+
block_entity_keys.add(reg.key)
|
|
260
|
+
for inter in interrupts:
|
|
261
|
+
block_entity_keys.add(inter.key)
|
|
262
|
+
|
|
263
|
+
requirements_set: set[str] = set()
|
|
264
|
+
for rel in self.model.relations:
|
|
265
|
+
if rel.dst in block_entity_keys and rel.kind in (
|
|
266
|
+
"implements",
|
|
267
|
+
"verifies",
|
|
268
|
+
"derives_from",
|
|
269
|
+
):
|
|
270
|
+
# Get the source of the implements relation
|
|
271
|
+
src_entity = self.model.get(rel.src)
|
|
272
|
+
if src_entity and src_entity.kind == "requirement":
|
|
273
|
+
requirements_set.add(rel.src)
|
|
274
|
+
requirements = sorted(list(requirements_set))
|
|
275
|
+
|
|
276
|
+
# Open items: entities with kind "open_item" that relate to this block
|
|
277
|
+
open_items_set: set[str] = set()
|
|
278
|
+
for rel in self.model.relations:
|
|
279
|
+
src_entity = self.model.get(rel.src)
|
|
280
|
+
if src_entity and src_entity.kind == "open_item" and rel.dst in block_entity_keys:
|
|
281
|
+
open_items_set.add(rel.src)
|
|
282
|
+
open_items = sorted(list(open_items_set))
|
|
283
|
+
|
|
284
|
+
return BlockInfo(
|
|
285
|
+
key=block.key,
|
|
286
|
+
name=block.name,
|
|
287
|
+
owner=getattr(block, "owner", None),
|
|
288
|
+
path=getattr(block, "path", None),
|
|
289
|
+
modules=module_keys,
|
|
290
|
+
ports=[p.key for p in ports],
|
|
291
|
+
registers=[r.key for r in registers],
|
|
292
|
+
interrupts=[i.key for i in interrupts],
|
|
293
|
+
memory_regions=[m.key for m in memory_regions],
|
|
294
|
+
clocks=block_clocks,
|
|
295
|
+
resets=block_resets,
|
|
296
|
+
requirements=requirements,
|
|
297
|
+
open_items=open_items,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def module(self, name: str) -> ModuleInfo:
|
|
301
|
+
"""Return the module, its ports, parameters, instances, and who instantiates it."""
|
|
302
|
+
# Find module by name
|
|
303
|
+
modules = self.model.find(kind="module", name=name)
|
|
304
|
+
if not modules:
|
|
305
|
+
matches = _close_matches(
|
|
306
|
+
name, [e.name for e in self.model.by_kind("module") if isinstance(e.name, str)]
|
|
307
|
+
)
|
|
308
|
+
msg = f"module {name!r} not found"
|
|
309
|
+
if matches:
|
|
310
|
+
msg += f"; did you mean: {', '.join(matches)}"
|
|
311
|
+
raise QueryError(msg)
|
|
312
|
+
|
|
313
|
+
mod = modules[0]
|
|
314
|
+
|
|
315
|
+
# Ports and parameters
|
|
316
|
+
ports = self.model.find(kind="port", module=mod.key)
|
|
317
|
+
parameters = self.model.find(kind="parameter", module=mod.key)
|
|
318
|
+
|
|
319
|
+
# Instances: children (this module instantiates)
|
|
320
|
+
instances: list[dict[str, str]] = []
|
|
321
|
+
for rel in self.model.relations:
|
|
322
|
+
if rel.src == mod.key and rel.kind == "instantiates":
|
|
323
|
+
child = self.model.get(rel.dst)
|
|
324
|
+
if child:
|
|
325
|
+
instance_name = rel.attrs.get("instance_name", "")
|
|
326
|
+
instances.append({"instance_name": str(instance_name), "module_key": rel.dst})
|
|
327
|
+
|
|
328
|
+
# Instantiated by: parents (which modules instantiate this)
|
|
329
|
+
instantiated_by: list[dict[str, str]] = []
|
|
330
|
+
for rel in self.model.relations:
|
|
331
|
+
if rel.dst == mod.key and rel.kind == "instantiates":
|
|
332
|
+
parent = self.model.get(rel.src)
|
|
333
|
+
if parent:
|
|
334
|
+
instance_name = rel.attrs.get("instance_name", "")
|
|
335
|
+
instantiated_by.append(
|
|
336
|
+
{"instance_name": str(instance_name), "module_key": rel.src}
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return ModuleInfo(
|
|
340
|
+
key=mod.key,
|
|
341
|
+
name=mod.name,
|
|
342
|
+
file=getattr(mod, "file", None),
|
|
343
|
+
block=getattr(mod, "block", None),
|
|
344
|
+
ports=[p.key for p in ports],
|
|
345
|
+
parameters=[p.key for p in parameters],
|
|
346
|
+
instances=instances,
|
|
347
|
+
instantiated_by=instantiated_by,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def find(
|
|
351
|
+
self,
|
|
352
|
+
kind: str | None = None,
|
|
353
|
+
name: str | None = None,
|
|
354
|
+
limit: int = 100,
|
|
355
|
+
**attrs: object,
|
|
356
|
+
) -> list[EntityMatch]:
|
|
357
|
+
"""Find entities by kind, name (glob via fnmatch), and/or attributes.
|
|
358
|
+
|
|
359
|
+
Returns up to `limit` results. Raises QueryError if the query is invalid.
|
|
360
|
+
"""
|
|
361
|
+
results: list[EntityBase] = []
|
|
362
|
+
|
|
363
|
+
for entity in self.model.entities.values():
|
|
364
|
+
if kind is not None and entity.kind != kind:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
if name is not None and not fnmatch(entity.name, name):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Check typed attributes
|
|
371
|
+
if attrs and not _matches(entity, attrs):
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
results.append(entity)
|
|
375
|
+
|
|
376
|
+
results = results[:limit]
|
|
377
|
+
return [
|
|
378
|
+
EntityMatch(
|
|
379
|
+
key=e.key,
|
|
380
|
+
kind=e.kind,
|
|
381
|
+
name=e.name,
|
|
382
|
+
source=e.source.file or e.source.extractor or "unknown",
|
|
383
|
+
)
|
|
384
|
+
for e in results
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
def trace(self, key: str) -> TraceResult:
|
|
388
|
+
"""Trace a requirement or any entity: show requirements and links.
|
|
389
|
+
|
|
390
|
+
For requirement entities: shows implements, verifies, derives_from, and flags.
|
|
391
|
+
For other entities: shows requirements that point to them.
|
|
392
|
+
"""
|
|
393
|
+
entity = self.model.get(key)
|
|
394
|
+
if entity is None:
|
|
395
|
+
matches = _close_matches(key, list(self.model.entities.keys()))
|
|
396
|
+
msg = f"entity {key!r} not found"
|
|
397
|
+
if matches:
|
|
398
|
+
msg += f"; did you mean: {', '.join(matches)}"
|
|
399
|
+
raise QueryError(msg)
|
|
400
|
+
|
|
401
|
+
implements: list[str] = []
|
|
402
|
+
verifies: list[str] = []
|
|
403
|
+
derives_from: list[str] = []
|
|
404
|
+
no_test = False
|
|
405
|
+
no_implementation = False
|
|
406
|
+
traced_by: list[str] = []
|
|
407
|
+
|
|
408
|
+
# If this is a requirement, trace outward
|
|
409
|
+
if entity.kind == "requirement":
|
|
410
|
+
# implements: relations where this is src and kind is "implements"
|
|
411
|
+
for rel in self.model.relations:
|
|
412
|
+
if rel.src == key and rel.kind == "implements":
|
|
413
|
+
implements.append(rel.dst)
|
|
414
|
+
# verifies: test verifies requirement (relation dst is requirement)
|
|
415
|
+
elif rel.dst == key and rel.kind == "verifies":
|
|
416
|
+
verifies.append(rel.src)
|
|
417
|
+
# derives_from: relations where this is src and kind is "derives_from"
|
|
418
|
+
elif rel.src == key and rel.kind == "derives_from":
|
|
419
|
+
derives_from.append(rel.dst)
|
|
420
|
+
|
|
421
|
+
# Check for no_test and no_implementation
|
|
422
|
+
no_test = len(verifies) == 0
|
|
423
|
+
no_implementation = len(implements) == 0
|
|
424
|
+
|
|
425
|
+
# Any entity: find requirements that trace to it (verifies / implements reverse)
|
|
426
|
+
for rel in self.model.relations:
|
|
427
|
+
if rel.dst == key and rel.kind in ("verifies", "implements"):
|
|
428
|
+
traced_by.append(rel.src)
|
|
429
|
+
|
|
430
|
+
return TraceResult(
|
|
431
|
+
key=key,
|
|
432
|
+
kind=entity.kind,
|
|
433
|
+
name=entity.name,
|
|
434
|
+
implements=implements,
|
|
435
|
+
verifies=verifies,
|
|
436
|
+
derives_from=derives_from,
|
|
437
|
+
no_test=no_test,
|
|
438
|
+
no_implementation=no_implementation,
|
|
439
|
+
traced_by=traced_by,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def impact(self, key: str, max_depth: int = 5) -> ImpactResult:
|
|
443
|
+
"""Compute downstream impact from a change at `key`.
|
|
444
|
+
|
|
445
|
+
Traverses `contains`, `instantiates`, `connects`, `derives_from`, `implements`,
|
|
446
|
+
`verifies` relations downstream from the root, up to `max_depth` levels.
|
|
447
|
+
Returns entities grouped by kind, with the path to each.
|
|
448
|
+
"""
|
|
449
|
+
entity = self.model.get(key)
|
|
450
|
+
if entity is None:
|
|
451
|
+
matches = _close_matches(key, list(self.model.entities.keys()))
|
|
452
|
+
msg = f"entity {key!r} not found"
|
|
453
|
+
if matches:
|
|
454
|
+
msg += f"; did you mean: {', '.join(matches)}"
|
|
455
|
+
raise QueryError(msg)
|
|
456
|
+
|
|
457
|
+
# Downstream relations: those that go "forward" in the design
|
|
458
|
+
downstream_kinds = {
|
|
459
|
+
"contains",
|
|
460
|
+
"instantiates",
|
|
461
|
+
"connects",
|
|
462
|
+
"derives_from",
|
|
463
|
+
"implements",
|
|
464
|
+
"verifies",
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# BFS with depth tracking
|
|
468
|
+
visited: set[str] = {key}
|
|
469
|
+
by_kind: dict[str, list[ImpactNode]] = defaultdict(list)
|
|
470
|
+
|
|
471
|
+
queue: list[tuple[str, list[str], int]] = [
|
|
472
|
+
(key, [], 0)
|
|
473
|
+
] # (entity_key, path_to_here, depth)
|
|
474
|
+
|
|
475
|
+
while queue:
|
|
476
|
+
current, path, depth = queue.pop(0)
|
|
477
|
+
|
|
478
|
+
# Find outgoing relations
|
|
479
|
+
for rel in self.model.relations:
|
|
480
|
+
if rel.src == current and rel.kind in downstream_kinds:
|
|
481
|
+
dst = rel.dst
|
|
482
|
+
if dst not in visited and depth < max_depth:
|
|
483
|
+
visited.add(dst)
|
|
484
|
+
# Path includes the relation kind and destination key
|
|
485
|
+
new_path = [*path, rel.kind, dst]
|
|
486
|
+
dst_entity = self.model.get(dst)
|
|
487
|
+
if dst_entity:
|
|
488
|
+
by_kind[dst_entity.kind].append(
|
|
489
|
+
ImpactNode(
|
|
490
|
+
key=dst,
|
|
491
|
+
kind=dst_entity.kind,
|
|
492
|
+
name=dst_entity.name,
|
|
493
|
+
path=new_path,
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
# Continue BFS for next level
|
|
497
|
+
queue.append((dst, new_path, depth + 1))
|
|
498
|
+
|
|
499
|
+
# Sort each kind's list by key for determinism
|
|
500
|
+
for kind in by_kind:
|
|
501
|
+
by_kind[kind].sort(key=lambda n: n.key)
|
|
502
|
+
|
|
503
|
+
return ImpactResult(key=key, max_depth=max_depth, by_kind=dict(by_kind))
|
|
504
|
+
|
|
505
|
+
def neighbors(
|
|
506
|
+
self,
|
|
507
|
+
key: str,
|
|
508
|
+
kinds: Iterable[str] | None = None,
|
|
509
|
+
relation: str | None = None,
|
|
510
|
+
direction: str = "out",
|
|
511
|
+
) -> NeighborResult:
|
|
512
|
+
"""Find adjacent entities (one hop) in the specified direction.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
key: The entity key.
|
|
516
|
+
kinds: If given, filter destinations by these entity kinds.
|
|
517
|
+
relation: If given, filter relations by this kind.
|
|
518
|
+
direction: "out" (forward), "in" (backward), or "both" (both).
|
|
519
|
+
|
|
520
|
+
Returns: Lists of incoming and outgoing edges from this entity.
|
|
521
|
+
"""
|
|
522
|
+
entity = self.model.get(key)
|
|
523
|
+
if entity is None:
|
|
524
|
+
matches = _close_matches(key, list(self.model.entities.keys()))
|
|
525
|
+
msg = f"entity {key!r} not found"
|
|
526
|
+
if matches:
|
|
527
|
+
msg += f"; did you mean: {', '.join(matches)}"
|
|
528
|
+
raise QueryError(msg)
|
|
529
|
+
|
|
530
|
+
kinds_set = set(kinds) if kinds is not None else None
|
|
531
|
+
incoming: list[dict[str, Any]] = []
|
|
532
|
+
outgoing: list[dict[str, Any]] = []
|
|
533
|
+
|
|
534
|
+
for rel in self.model.relations:
|
|
535
|
+
if relation is not None and rel.kind != relation:
|
|
536
|
+
continue
|
|
537
|
+
|
|
538
|
+
if direction in ("in", "both") and rel.dst == key:
|
|
539
|
+
src_entity = self.model.get(rel.src)
|
|
540
|
+
if src_entity is not None and (kinds_set is None or src_entity.kind in kinds_set):
|
|
541
|
+
incoming.append({"src_key": rel.src, "relation_kind": rel.kind})
|
|
542
|
+
|
|
543
|
+
if direction in ("out", "both") and rel.src == key:
|
|
544
|
+
dst_entity = self.model.get(rel.dst)
|
|
545
|
+
if dst_entity is not None and (kinds_set is None or dst_entity.kind in kinds_set):
|
|
546
|
+
outgoing.append({"dst_key": rel.dst, "relation_kind": rel.kind})
|
|
547
|
+
|
|
548
|
+
return NeighborResult(key=key, incoming=incoming, outgoing=outgoing)
|
|
549
|
+
|
|
550
|
+
def search(
|
|
551
|
+
self,
|
|
552
|
+
text: str,
|
|
553
|
+
kinds: Iterable[str] | None = None,
|
|
554
|
+
limit: int = 20,
|
|
555
|
+
) -> list[SearchResult]:
|
|
556
|
+
"""Full-text search on the backing store.
|
|
557
|
+
|
|
558
|
+
Raises QueryError if no store is available.
|
|
559
|
+
"""
|
|
560
|
+
if self.store is None:
|
|
561
|
+
raise QueryError("full-text search requires a ModelStore")
|
|
562
|
+
|
|
563
|
+
kinds_list = list(kinds) if kinds is not None else None
|
|
564
|
+
hits = self.store.search(text, kinds=kinds_list, limit=limit)
|
|
565
|
+
|
|
566
|
+
return [SearchResult(text=hit.text, score=hit.score, citation=hit.citation) for hit in hits]
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _matches(entity: EntityBase, attrs: dict[str, object]) -> bool:
|
|
570
|
+
"""Check if entity's typed fields and attrs match the given dict."""
|
|
571
|
+
for key, value in attrs.items():
|
|
572
|
+
# Try typed field first
|
|
573
|
+
if hasattr(entity, key):
|
|
574
|
+
if getattr(entity, key) != value:
|
|
575
|
+
return False
|
|
576
|
+
# Try attrs dict
|
|
577
|
+
elif key in entity.attrs:
|
|
578
|
+
if entity.attrs[key] != value:
|
|
579
|
+
return False
|
|
580
|
+
else:
|
|
581
|
+
# Neither typed field nor attrs: no match
|
|
582
|
+
return False
|
|
583
|
+
return True
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""`EntityKinds`: the registry mapping an entity `kind` string to its pydantic class.
|
|
2
|
+
|
|
3
|
+
Core kinds (DESIGN.md 4.2) are registered by default. A pack that needs a new kind
|
|
4
|
+
(e.g. an analog pack adding `pin_spec`) registers its own frozen pydantic class that
|
|
5
|
+
subclasses `EntityBase`, without chipgraph.core knowing anything about it. A kind with
|
|
6
|
+
no registered class (because the reader does not have that pack installed, or has not
|
|
7
|
+
called `register` for it) still round-trips losslessly as `ExtEntity`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from chipgraph.core.model.entities import CORE_ENTITY_CLASSES, EntityBase, ExtEntity
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DuplicateKindError(ValueError):
|
|
21
|
+
"""Raised when registering a kind that is already registered to a different class."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EntityKinds:
|
|
25
|
+
"""A registry of entity `kind` -> pydantic class, seeded with the core kinds."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._classes: dict[str, type[EntityBase]] = {}
|
|
29
|
+
for cls in CORE_ENTITY_CLASSES:
|
|
30
|
+
self._classes[_kind_of(cls)] = cls
|
|
31
|
+
|
|
32
|
+
def register(self, cls: type[EntityBase]) -> None:
|
|
33
|
+
"""Register a pack-defined entity class, keyed by its `kind` literal default."""
|
|
34
|
+
kind = _kind_of(cls)
|
|
35
|
+
existing = self._classes.get(kind)
|
|
36
|
+
if existing is not None and existing is not cls:
|
|
37
|
+
raise DuplicateKindError(
|
|
38
|
+
f"kind {kind!r} is already registered to {existing.__qualname__}"
|
|
39
|
+
)
|
|
40
|
+
self._classes[kind] = cls
|
|
41
|
+
|
|
42
|
+
def get(self, kind: str) -> type[EntityBase] | None:
|
|
43
|
+
"""Return the registered class for `kind`, or None if unregistered."""
|
|
44
|
+
return self._classes.get(kind)
|
|
45
|
+
|
|
46
|
+
def kinds(self) -> tuple[str, ...]:
|
|
47
|
+
"""Return every registered kind, sorted."""
|
|
48
|
+
return tuple(sorted(self._classes))
|
|
49
|
+
|
|
50
|
+
def parse(self, data: Mapping[str, Any]) -> EntityBase:
|
|
51
|
+
"""Build an entity from a plain dict, using the registered class for its `kind`.
|
|
52
|
+
|
|
53
|
+
Falls back to `ExtEntity` when `kind` has no registered class, so entities from
|
|
54
|
+
packs the reader does not know about still round-trip.
|
|
55
|
+
"""
|
|
56
|
+
kind = data.get("kind")
|
|
57
|
+
if not isinstance(kind, str):
|
|
58
|
+
raise ValueError(f"entity data is missing a string 'kind': {data!r}")
|
|
59
|
+
cls = self.get(kind)
|
|
60
|
+
if cls is not None:
|
|
61
|
+
return cls.model_validate(data)
|
|
62
|
+
return ExtEntity.model_validate(data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _kind_of(cls: type[EntityBase]) -> str:
|
|
66
|
+
field = cls.model_fields.get("kind")
|
|
67
|
+
if field is None or not isinstance(field.default, str):
|
|
68
|
+
raise TypeError(
|
|
69
|
+
f"{cls.__qualname__} must define a 'kind: Literal[...]' field with a default"
|
|
70
|
+
)
|
|
71
|
+
return field.default
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
DEFAULT_ENTITY_KINDS = EntityKinds()
|
|
75
|
+
"""The process-wide default registry: core kinds only, until packs register more."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_pydantic_entity_class(obj: Any) -> bool:
|
|
79
|
+
"""Return True if `obj` looks like a valid entity class to register."""
|
|
80
|
+
return isinstance(obj, type) and issubclass(obj, BaseModel) and issubclass(obj, EntityBase)
|