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,60 @@
|
|
|
1
|
+
"""Persisted `ProductionRecord`s: what the scheduler recorded the last time each rule
|
|
2
|
+
instance finished, kept across runs so staleness (`graph.compute_staleness`) can be
|
|
3
|
+
computed the next time the engine looks at this project.
|
|
4
|
+
|
|
5
|
+
Each record lives at ``layout.state_dir / "records" / <sha256(instance_id)>.json``,
|
|
6
|
+
written atomically (tmp file + `os.replace`), one file per instance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from chipgraph.core.engine.graph import ProductionRecord
|
|
16
|
+
from chipgraph.core.state.layout import StateLayout
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RecordStore:
|
|
20
|
+
"""Reads and writes `ProductionRecord`s, one JSON file per instance id."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, layout: StateLayout) -> None:
|
|
23
|
+
self.layout = layout
|
|
24
|
+
self._dir = layout.state_dir / "records"
|
|
25
|
+
|
|
26
|
+
def _path(self, instance_id: str) -> Path:
|
|
27
|
+
digest = hashlib.sha256(instance_id.encode("utf-8")).hexdigest()
|
|
28
|
+
return self._dir / f"{digest}.json"
|
|
29
|
+
|
|
30
|
+
def get(self, instance_id: str) -> ProductionRecord | None:
|
|
31
|
+
"""Return the record for `instance_id`, or None if it was never recorded."""
|
|
32
|
+
path = self._path(instance_id)
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return None
|
|
35
|
+
return ProductionRecord.model_validate_json(path.read_text(encoding="utf-8"))
|
|
36
|
+
|
|
37
|
+
def put(self, record: ProductionRecord) -> None:
|
|
38
|
+
"""Atomically write `record`, replacing any previous record for the same instance."""
|
|
39
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
path = self._path(record.instance_id)
|
|
41
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
42
|
+
tmp_path.write_text(record.model_dump_json(indent=2), encoding="utf-8")
|
|
43
|
+
os.replace(tmp_path, path)
|
|
44
|
+
|
|
45
|
+
def delete(self, instance_id: str) -> None:
|
|
46
|
+
"""Remove the record for `instance_id`, if any. A no-op if it has none."""
|
|
47
|
+
self._path(instance_id).unlink(missing_ok=True)
|
|
48
|
+
|
|
49
|
+
def all(self) -> dict[str, ProductionRecord]:
|
|
50
|
+
"""Return every stored record, keyed by instance id."""
|
|
51
|
+
if not self._dir.exists():
|
|
52
|
+
return {}
|
|
53
|
+
result: dict[str, ProductionRecord] = {}
|
|
54
|
+
for path in self._dir.glob("*.json"):
|
|
55
|
+
record = ProductionRecord.model_validate_json(path.read_text(encoding="utf-8"))
|
|
56
|
+
result[record.instance_id] = record
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["RecordStore"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Loading `RuleSpec` from rule files and packs.
|
|
2
|
+
|
|
3
|
+
Pure data loading: this module only turns YAML into validated `RuleSpec` objects. It
|
|
4
|
+
does not expand `foreach`, resolve inputs/outputs, or build a graph (see `graph.py`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from chipgraph.core.contracts import RuleSpec
|
|
15
|
+
from chipgraph.core.plugin_api.pack import Pack
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RuleLoadError(Exception):
|
|
19
|
+
"""Raised when a rule file, or a pack's set of rules, fails to load."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_rule_file(path: Path, *, namespace: str | None = None) -> RuleSpec:
|
|
23
|
+
"""Load a single `RuleSpec` from a YAML file at `path`.
|
|
24
|
+
|
|
25
|
+
The id key may be written `rule:` (as in DESIGN.md 3.4) or `id:`; having both is
|
|
26
|
+
an error. With `namespace` (the pack name), a short id such as `rtl_module` becomes
|
|
27
|
+
`<namespace>/rtl_module`. Any other validation failure (bad YAML, schema mismatch) raises
|
|
28
|
+
`RuleLoadError` naming `path`.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
text = path.read_text()
|
|
32
|
+
except OSError as exc:
|
|
33
|
+
raise RuleLoadError(f"cannot read rule file {path}: {exc}") from exc
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
raw = yaml.safe_load(text)
|
|
37
|
+
except yaml.YAMLError as exc:
|
|
38
|
+
raise RuleLoadError(f"invalid YAML in {path}: {exc}") from exc
|
|
39
|
+
|
|
40
|
+
if not isinstance(raw, dict):
|
|
41
|
+
raise RuleLoadError(f"rule file {path} must contain a YAML mapping")
|
|
42
|
+
|
|
43
|
+
data = dict(raw)
|
|
44
|
+
if "rule" in data and "id" in data:
|
|
45
|
+
raise RuleLoadError(f"rule file {path} has both 'rule' and 'id' keys; use only one")
|
|
46
|
+
if "rule" in data:
|
|
47
|
+
data["id"] = data.pop("rule")
|
|
48
|
+
if namespace is not None and isinstance(data.get("id"), str) and "/" not in data["id"]:
|
|
49
|
+
data["id"] = f"{namespace}/{data['id']}"
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
return RuleSpec.model_validate(data)
|
|
53
|
+
except ValidationError as exc:
|
|
54
|
+
raise RuleLoadError(f"invalid rule spec in {path}: {exc}") from exc
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _rule_files_under(path: Path) -> list[Path]:
|
|
58
|
+
"""Return the rule files a `provides.rules` entry resolves to.
|
|
59
|
+
|
|
60
|
+
A file resolves to itself; a directory resolves to every `*.yml`/`*.yaml` file
|
|
61
|
+
directly inside it, sorted for determinism.
|
|
62
|
+
"""
|
|
63
|
+
if path.is_dir():
|
|
64
|
+
return sorted({*path.glob("*.yml"), *path.glob("*.yaml")})
|
|
65
|
+
return [path]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_pack_rules(pack: Pack) -> list[RuleSpec]:
|
|
69
|
+
"""Load every rule a pack provides.
|
|
70
|
+
|
|
71
|
+
Every path in `pack.manifest.provides.rules` is loaded (a file directly, or every
|
|
72
|
+
`*.yml`/`*.yaml` file inside a directory). Each rule's id namespace (the part
|
|
73
|
+
before `/`) must equal the pack's name, and rule ids must be unique across all of
|
|
74
|
+
the pack's rule files; either violation raises `RuleLoadError`.
|
|
75
|
+
"""
|
|
76
|
+
specs: dict[str, RuleSpec] = {}
|
|
77
|
+
for rel in pack.manifest.provides.rules:
|
|
78
|
+
for file_path in _rule_files_under(pack.path(rel)):
|
|
79
|
+
spec = load_rule_file(file_path, namespace=pack.manifest.name)
|
|
80
|
+
namespace = spec.id.split("/", 1)[0]
|
|
81
|
+
if namespace != pack.manifest.name:
|
|
82
|
+
raise RuleLoadError(
|
|
83
|
+
f"rule {spec.id!r} in {file_path} does not belong to namespace "
|
|
84
|
+
f"{pack.manifest.name!r}"
|
|
85
|
+
)
|
|
86
|
+
if spec.id in specs:
|
|
87
|
+
raise RuleLoadError(
|
|
88
|
+
f"duplicate rule id {spec.id!r} in pack {pack.manifest.name!r} "
|
|
89
|
+
f"(also defined in {file_path})"
|
|
90
|
+
)
|
|
91
|
+
specs[spec.id] = spec
|
|
92
|
+
return list(specs.values())
|