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,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"]
|