cseq 0.0.1__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.
@@ -0,0 +1,69 @@
1
+ {
2
+ "schema_version": 1,
3
+ "purpose": "G2B Tree-sitter Fast Syntax Front-end acceptance corpus",
4
+ "cases": [
5
+ {
6
+ "id": "TS-01-basic-direct-call",
7
+ "source": "static int add1(int x){return x+1;} int main(void){return add1(1);}",
8
+ "required_node_types": [
9
+ "function_definition",
10
+ "call_expression",
11
+ "return_statement"
12
+ ],
13
+ "must_not_require_semantic_refinement": true
14
+ },
15
+ {
16
+ "id": "TS-02-control-structure",
17
+ "source": "int f(int x){if(x){while(x--){;} } else {for(;;){break;}} return x;}",
18
+ "required_node_types": [
19
+ "function_definition",
20
+ "if_statement",
21
+ "while_statement",
22
+ "for_statement",
23
+ "break_statement"
24
+ ],
25
+ "must_not_require_semantic_refinement": true
26
+ },
27
+ {
28
+ "id": "TS-03-typedef-struct",
29
+ "source": "typedef struct Ctx { int value; } Ctx; int f(Ctx *c){return c->value;}",
30
+ "required_node_types": [
31
+ "type_definition",
32
+ "struct_specifier",
33
+ "field_expression"
34
+ ],
35
+ "must_not_require_semantic_refinement": false,
36
+ "refinement_reason": "TYPE_AMBIGUOUS"
37
+ },
38
+ {
39
+ "id": "TS-04-preprocessor",
40
+ "source": "#ifdef FEATURE_X\nint f(void){return 1;}\n#else\nint f(void){return 0;}\n#endif\n",
41
+ "required_node_types": [
42
+ "preproc_ifdef",
43
+ "function_definition"
44
+ ],
45
+ "must_preserve_all_branches": true
46
+ },
47
+ {
48
+ "id": "TS-05-error-recovery",
49
+ "source": "int good(void){return 1;} int broken( { int after(void){return 2;}",
50
+ "required_node_types": [],
51
+ "must_report_error_or_missing_node": true,
52
+ "must_preserve_recoverable_regions": true,
53
+ "expected_parse_status_any_of": [
54
+ "ERROR_RECOVERED",
55
+ "PARTIAL"
56
+ ],
57
+ "minimum_recovered_functions": 1
58
+ },
59
+ {
60
+ "id": "TS-06-vendor-extension",
61
+ "source": "__vendor_magic int helper(void){return 1;} int main(void){return helper();}",
62
+ "required_node_types": [
63
+ "translation_unit"
64
+ ],
65
+ "must_require_semantic_refinement": true,
66
+ "refinement_reason": "COMPILER_EXTENSION"
67
+ }
68
+ ]
69
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "schema_version": 1,
3
+ "decision_id": "ADR-001",
4
+ "canonical_frontend": "tree-sitter-c",
5
+ "packages": {
6
+ "tree-sitter": {
7
+ "version": "0.26.0",
8
+ "python_requires": ">=3.10",
9
+ "artifacts": {
10
+ "cp313-linux-x86_64-glibc": {
11
+ "filename": "tree_sitter-0.26.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
12
+ "sha256": "7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7"
13
+ },
14
+ "cp313-windows-x86_64": {
15
+ "filename": "tree_sitter-0.26.0-cp313-cp313-win_amd64.whl",
16
+ "sha256": "93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f"
17
+ },
18
+ "cp311-linux-x86_64-glibc": {
19
+ "filename": "tree_sitter-0.26.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
20
+ "sha256": "335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8"
21
+ },
22
+ "cp311-windows-x86_64": {
23
+ "filename": "tree_sitter-0.26.0-cp311-cp311-win_amd64.whl",
24
+ "sha256": "0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754"
25
+ },
26
+ "cp312-linux-x86_64-glibc": {
27
+ "filename": "tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
28
+ "sha256": "5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4"
29
+ },
30
+ "cp312-windows-x86_64": {
31
+ "filename": "tree_sitter-0.26.0-cp312-cp312-win_amd64.whl",
32
+ "sha256": "ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37"
33
+ }
34
+ }
35
+ },
36
+ "tree-sitter-c": {
37
+ "version": "0.24.2",
38
+ "python_requires": ">=3.10",
39
+ "grammar_abi": 15,
40
+ "artifacts": {
41
+ "abi3-linux-x86_64-glibc": {
42
+ "filename": "tree_sitter_c-0.24.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
43
+ "sha256": "5041ef67eb68ce6bc8bb0b1f8ef3a5585ce523dae0c7eec109ab0627dd75aede"
44
+ },
45
+ "abi3-windows-x86_64": {
46
+ "filename": "tree_sitter_c-0.24.2-cp310-abi3-win_amd64.whl",
47
+ "sha256": "abb549225091f7b25df2dd3a0143ece6e208f7055d8bcb4700b41ee79b9ef1e1"
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "expected_api": {
53
+ "language_factory": "tree_sitter.Language(tree_sitter_c.language())",
54
+ "parser_factory": "tree_sitter.Parser(language)",
55
+ "parse_call": "parser.parse(source_bytes)",
56
+ "smoke_source": "int main(void) { return 0; }",
57
+ "smoke_root_type": "translation_unit"
58
+ }
59
+ }
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from importlib import metadata, util
5
+ import json
6
+ from pathlib import Path
7
+ import platform
8
+ import sys
9
+ from typing import Any
10
+
11
+
12
+ _LOCK_PATH = Path(__file__).with_name("parser_dependency_lock.json")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ParserEnvironment:
17
+ tree_sitter_available: bool
18
+ tree_sitter_c_available: bool
19
+ tree_sitter_version: str | None
20
+ tree_sitter_c_version: str | None
21
+ expected_tree_sitter_version: str
22
+ expected_tree_sitter_c_version: str
23
+ versions_match_lock: bool
24
+ api_smoke_tested: bool
25
+ api_smoke_ok: bool | None
26
+ grammar_abi: int | None
27
+ expected_grammar_abi: int
28
+ grammar_abi_match: bool | None
29
+ platform_lock_key: str | None
30
+ runtime_wheel_filename: str | None
31
+ runtime_wheel_sha256: str | None
32
+ grammar_wheel_filename: str | None
33
+ grammar_wheel_sha256: str | None
34
+ canonical_fast_frontend_ready: bool
35
+ fallback_required: bool
36
+ incompatibility_reasons: tuple[str, ...]
37
+
38
+ def to_dict(self) -> dict[str, object]:
39
+ return asdict(self)
40
+
41
+
42
+ def load_parser_dependency_lock() -> dict[str, Any]:
43
+ return json.loads(_LOCK_PATH.read_text(encoding="utf-8"))
44
+
45
+
46
+ def _distribution_version(name: str) -> str | None:
47
+ try:
48
+ return metadata.version(name)
49
+ except metadata.PackageNotFoundError:
50
+ return None
51
+
52
+
53
+ def _platform_keys() -> tuple[str | None, str | None]:
54
+ machine = platform.machine().lower()
55
+ if machine in {"amd64", "x86_64"}:
56
+ arch = "x86_64"
57
+ elif machine in {"arm64", "aarch64"}:
58
+ arch = "aarch64"
59
+ else:
60
+ arch = machine
61
+ py = f"cp{sys.version_info.major}{sys.version_info.minor}"
62
+ system = platform.system().lower()
63
+ if system == "linux":
64
+ return f"{py}-linux-{arch}-glibc", f"abi3-linux-{arch}-glibc"
65
+ if system == "windows":
66
+ return f"{py}-windows-{arch}", f"abi3-windows-{arch}"
67
+ return None, None
68
+
69
+
70
+ def _artifact(lock: dict[str, Any], package: str, key: str | None) -> tuple[str | None, str | None]:
71
+ if key is None:
72
+ return None, None
73
+ entry = lock["packages"][package].get("artifacts", {}).get(key)
74
+ if not entry:
75
+ return None, None
76
+ return entry.get("filename"), entry.get("sha256")
77
+
78
+
79
+ def _smoke_tree_sitter(expected_abi: int, smoke_source: str, smoke_root: str) -> tuple[bool, int | None, str | None]:
80
+ """Import the locked API and execute a minimal C parse.
81
+
82
+ The function is called only when both distributions are discoverable. Any
83
+ native/API incompatibility becomes a diagnostic reason rather than leaking
84
+ an import exception out of `cseq doctor`.
85
+ """
86
+ try:
87
+ import tree_sitter # type: ignore
88
+ import tree_sitter_c # type: ignore
89
+
90
+ language = tree_sitter.Language(tree_sitter_c.language())
91
+ abi = int(getattr(language, "abi_version"))
92
+ parser = tree_sitter.Parser(language)
93
+ tree = parser.parse(smoke_source.encode("utf-8"))
94
+ root_type = getattr(tree.root_node, "type", None)
95
+ ok = abi == expected_abi and root_type == smoke_root
96
+ reason = None if ok else f"smoke-mismatch: abi={abi}, root={root_type!r}"
97
+ return ok, abi, reason
98
+ except Exception as exc: # pragma: no cover - depends on native runtime
99
+ return False, None, f"api-smoke-failed: {type(exc).__name__}: {exc}"
100
+
101
+
102
+ def probe_parser_environment() -> ParserEnvironment:
103
+ """Inspect the parser runtime against the G2 dependency lock.
104
+
105
+ Availability alone is insufficient. Canonical readiness requires the exact
106
+ locked distribution versions plus a real API/grammar smoke parse. Missing
107
+ dependencies and incompatible installed dependencies are therefore reported
108
+ as distinct states by the returned diagnostics.
109
+ """
110
+ lock = load_parser_dependency_lock()
111
+ ts_lock = lock["packages"]["tree-sitter"]
112
+ c_lock = lock["packages"]["tree-sitter-c"]
113
+ expected_ts = str(ts_lock["version"])
114
+ expected_c = str(c_lock["version"])
115
+ expected_abi = int(c_lock["grammar_abi"])
116
+
117
+ ts = util.find_spec("tree_sitter") is not None
118
+ tsc = util.find_spec("tree_sitter_c") is not None
119
+ ts_version = _distribution_version("tree-sitter") if ts else None
120
+ c_version = _distribution_version("tree-sitter-c") if tsc else None
121
+ versions_match = ts_version == expected_ts and c_version == expected_c
122
+
123
+ runtime_key, grammar_key = _platform_keys()
124
+ runtime_filename, runtime_hash = _artifact(lock, "tree-sitter", runtime_key)
125
+ grammar_filename, grammar_hash = _artifact(lock, "tree-sitter-c", grammar_key)
126
+
127
+ reasons: list[str] = []
128
+ if not ts:
129
+ reasons.append("tree-sitter-not-installed")
130
+ if not tsc:
131
+ reasons.append("tree-sitter-c-not-installed")
132
+ if ts and ts_version != expected_ts:
133
+ reasons.append(f"tree-sitter-version-mismatch:{ts_version}!={expected_ts}")
134
+ if tsc and c_version != expected_c:
135
+ reasons.append(f"tree-sitter-c-version-mismatch:{c_version}!={expected_c}")
136
+ if runtime_key is not None and runtime_filename is None:
137
+ reasons.append(f"runtime-wheel-not-locked-for:{runtime_key}")
138
+ if grammar_key is not None and grammar_filename is None:
139
+ reasons.append(f"grammar-wheel-not-locked-for:{grammar_key}")
140
+
141
+ smoke_tested = bool(ts and tsc and versions_match)
142
+ smoke_ok: bool | None = None
143
+ grammar_abi: int | None = None
144
+ grammar_abi_match: bool | None = None
145
+ if smoke_tested:
146
+ smoke_ok, grammar_abi, smoke_reason = _smoke_tree_sitter(
147
+ expected_abi,
148
+ str(lock["expected_api"]["smoke_source"]),
149
+ str(lock["expected_api"]["smoke_root_type"]),
150
+ )
151
+ grammar_abi_match = grammar_abi == expected_abi if grammar_abi is not None else False
152
+ if smoke_reason:
153
+ reasons.append(smoke_reason)
154
+
155
+ ready = bool(
156
+ ts
157
+ and tsc
158
+ and versions_match
159
+ and smoke_tested
160
+ and smoke_ok
161
+ and grammar_abi_match
162
+ and runtime_filename
163
+ and grammar_filename
164
+ )
165
+ return ParserEnvironment(
166
+ tree_sitter_available=ts,
167
+ tree_sitter_c_available=tsc,
168
+ tree_sitter_version=ts_version,
169
+ tree_sitter_c_version=c_version,
170
+ expected_tree_sitter_version=expected_ts,
171
+ expected_tree_sitter_c_version=expected_c,
172
+ versions_match_lock=versions_match,
173
+ api_smoke_tested=smoke_tested,
174
+ api_smoke_ok=smoke_ok,
175
+ grammar_abi=grammar_abi,
176
+ expected_grammar_abi=expected_abi,
177
+ grammar_abi_match=grammar_abi_match,
178
+ platform_lock_key=runtime_key,
179
+ runtime_wheel_filename=runtime_filename,
180
+ runtime_wheel_sha256=runtime_hash,
181
+ grammar_wheel_filename=grammar_filename,
182
+ grammar_wheel_sha256=grammar_hash,
183
+ canonical_fast_frontend_ready=ready,
184
+ fallback_required=not ready,
185
+ incompatibility_reasons=tuple(reasons),
186
+ )
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .parser_environment import load_parser_dependency_lock, probe_parser_environment
10
+
11
+
12
+ _CONTRACT_PATH = Path(__file__).with_name("parser_contract_cases.json")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class WheelArtifactCheck:
17
+ package: str
18
+ filename: str | None
19
+ expected_sha256: str | None
20
+ path: str | None
21
+ exists: bool
22
+ actual_sha256: str | None
23
+ hash_match: bool
24
+
25
+ def to_dict(self) -> dict[str, object]:
26
+ return asdict(self)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class G2BResumeReport:
31
+ status: str
32
+ environment_ready: bool
33
+ wheelhouse_checked: bool
34
+ wheelhouse_ready: bool | None
35
+ wheel_checks: tuple[WheelArtifactCheck, ...]
36
+ contract_case_count: int
37
+ next_action: str
38
+
39
+ def to_dict(self) -> dict[str, object]:
40
+ payload = asdict(self)
41
+ payload["wheel_checks"] = [item.to_dict() for item in self.wheel_checks]
42
+ return payload
43
+
44
+
45
+ def _sha256(path: Path) -> str:
46
+ h = hashlib.sha256()
47
+ with path.open("rb") as f:
48
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
49
+ h.update(chunk)
50
+ return h.hexdigest()
51
+
52
+
53
+ def load_parser_contract_cases() -> tuple[dict[str, Any], ...]:
54
+ raw = json.loads(_CONTRACT_PATH.read_text(encoding="utf-8"))
55
+ cases = raw.get("cases", [])
56
+ if not isinstance(cases, list):
57
+ raise ValueError("parser contract cases must be a list")
58
+ return tuple(cases)
59
+
60
+
61
+ def verify_parser_wheelhouse(directory: str | Path) -> tuple[WheelArtifactCheck, ...]:
62
+ """Verify the exact locked wheels required by the current platform.
63
+
64
+ This deliberately does not install anything. It makes the G2B restart
65
+ condition deterministic and safe for offline/corporate environments.
66
+ """
67
+ root = Path(directory)
68
+ env = probe_parser_environment()
69
+ required = (
70
+ ("tree-sitter", env.runtime_wheel_filename, env.runtime_wheel_sha256),
71
+ ("tree-sitter-c", env.grammar_wheel_filename, env.grammar_wheel_sha256),
72
+ )
73
+ checks: list[WheelArtifactCheck] = []
74
+ for package, filename, expected in required:
75
+ if filename is None or expected is None:
76
+ checks.append(WheelArtifactCheck(package, filename, expected, None, False, None, False))
77
+ continue
78
+ path = root / filename
79
+ exists = path.is_file()
80
+ actual = _sha256(path) if exists else None
81
+ checks.append(WheelArtifactCheck(
82
+ package=package,
83
+ filename=filename,
84
+ expected_sha256=expected,
85
+ path=str(path),
86
+ exists=exists,
87
+ actual_sha256=actual,
88
+ hash_match=bool(exists and actual == expected),
89
+ ))
90
+ return tuple(checks)
91
+
92
+
93
+ def build_g2b_resume_report(wheel_dir: str | Path | None = None) -> G2BResumeReport:
94
+ env = probe_parser_environment()
95
+ contract_count = len(load_parser_contract_cases())
96
+ if env.canonical_fast_frontend_ready:
97
+ return G2BResumeReport(
98
+ status="READY_FOR_TREE_SITTER_EXECUTION",
99
+ environment_ready=True,
100
+ wheelhouse_checked=wheel_dir is not None,
101
+ wheelhouse_ready=None if wheel_dir is None else all(x.hash_match for x in verify_parser_wheelhouse(wheel_dir)),
102
+ wheel_checks=() if wheel_dir is None else verify_parser_wheelhouse(wheel_dir),
103
+ contract_case_count=contract_count,
104
+ next_action="run-cseq-analysis",
105
+ )
106
+
107
+ checks = () if wheel_dir is None else verify_parser_wheelhouse(wheel_dir)
108
+ wheel_ready = None if wheel_dir is None else bool(checks and all(x.hash_match for x in checks))
109
+ if wheel_ready:
110
+ next_action = "offline-install-locked-wheels-then-rerun-doctor"
111
+ status = "WHEELS_VERIFIED_INSTALL_REQUIRED"
112
+ elif wheel_dir is not None:
113
+ next_action = "supply-exact-locked-wheels"
114
+ status = "WHEELHOUSE_INCOMPLETE_OR_MISMATCHED"
115
+ else:
116
+ next_action = "supply-locked-wheelhouse-or-install-locked-runtime"
117
+ status = "DEPENDENCY_BLOCKED"
118
+ return G2BResumeReport(
119
+ status=status,
120
+ environment_ready=False,
121
+ wheelhouse_checked=wheel_dir is not None,
122
+ wheelhouse_ready=wheel_ready,
123
+ wheel_checks=checks,
124
+ contract_case_count=contract_count,
125
+ next_action=next_action,
126
+ )
cseq/plugin.py ADDED
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from importlib import metadata
6
+ from typing import Any, Iterable, Protocol, runtime_checkable
7
+
8
+ from .model import CallSiteRecord, FunctionRecord
9
+ from .runtime import RuntimeEvent
10
+
11
+ PLUGIN_API_VERSION = 1
12
+ ENTRY_POINT_GROUP = "cseq.plugins"
13
+
14
+
15
+ class PluginKind(str, Enum):
16
+ COMPILER_EXTENSION = "CompilerExtensionPlugin"
17
+ CPU_RULE_PROVIDER = "CpuRuleProvider"
18
+ SEMANTIC_HINT_PROVIDER = "SemanticHintProvider"
19
+ RUNTIME_TIMESTAMP_PARSER = "RuntimeTimestampParser"
20
+ RUNTIME_EVENT_PROVIDER = "RuntimeEventProvider"
21
+ LINKER_MAP_ADAPTER = "LinkerMapAdapter"
22
+ RENDERER_EXTENSION = "RendererExtension"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class FunctionView:
27
+ name: str
28
+ qualified_id: str
29
+ source_path: str
30
+ storage_class: str | None
31
+ parameters: tuple[str, ...]
32
+
33
+ @classmethod
34
+ def from_record(cls, record: FunctionRecord) -> "FunctionView":
35
+ return cls(record.name, record.qualified_id, record.source_path, record.storage_class, record.parameters)
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class CallSiteView:
40
+ caller_id: str
41
+ caller_name: str
42
+ callee_name: str | None
43
+ raw_text: str
44
+ source_path: str
45
+ target_kind: str
46
+ target_function_ids: tuple[str, ...]
47
+ unknown_possible: bool
48
+
49
+ @classmethod
50
+ def from_record(cls, record: CallSiteRecord) -> "CallSiteView":
51
+ return cls(
52
+ record.caller_id,
53
+ record.caller_name,
54
+ record.callee_name,
55
+ record.raw_text,
56
+ record.source_path,
57
+ record.target_kind.value,
58
+ record.target_function_ids,
59
+ record.unknown_possible,
60
+ )
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class RuntimeEventView:
65
+ event_index: int
66
+ marker_id: str
67
+ timestamp_raw: str | None
68
+ cpu_hint: str | None
69
+ task_hint: str | None
70
+ message: str | None
71
+ payload: tuple[tuple[str, str], ...]
72
+
73
+ @classmethod
74
+ def from_record(cls, record: RuntimeEvent) -> "RuntimeEventView":
75
+ return cls(
76
+ record.event_index,
77
+ record.marker_id,
78
+ record.timestamp_raw,
79
+ record.cpu_hint,
80
+ record.task_hint,
81
+ record.message,
82
+ record.payload,
83
+ )
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class PluginEvidence:
88
+ plugin_name: str
89
+ kind: PluginKind
90
+ target: str
91
+ evidence_type: str
92
+ value: str
93
+ confidence: str = "MEDIUM"
94
+ details: tuple[tuple[str, str], ...] = ()
95
+
96
+
97
+ @dataclass(frozen=True, slots=True)
98
+ class PluginDiagnostic:
99
+ plugin_name: str
100
+ severity: str
101
+ code: str
102
+ message: str
103
+
104
+
105
+ @dataclass(frozen=True, slots=True)
106
+ class PluginDescriptor:
107
+ name: str
108
+ kinds: tuple[PluginKind, ...]
109
+ api_version: int = PLUGIN_API_VERSION
110
+ version: str | None = None
111
+
112
+
113
+ @runtime_checkable
114
+ class CseqPlugin(Protocol):
115
+ descriptor: PluginDescriptor
116
+
117
+
118
+ @dataclass(slots=True)
119
+ class PluginRunResult:
120
+ evidence: list[PluginEvidence] = field(default_factory=list)
121
+ diagnostics: list[PluginDiagnostic] = field(default_factory=list)
122
+
123
+ @property
124
+ def has_errors(self) -> bool:
125
+ return any(d.severity in {"error", "fatal"} for d in self.diagnostics)
126
+
127
+
128
+ class PluginManager:
129
+ """Fail-safe plugin executor.
130
+
131
+ Plugins receive immutable façade views rather than internal IR objects. Any
132
+ plugin exception becomes a diagnostic; the core caller can continue with
133
+ ordinary analysis and simply lose the extra evidence.
134
+ """
135
+
136
+ def __init__(self, plugins: Iterable[CseqPlugin] = ()):
137
+ self._plugins: dict[str, CseqPlugin] = {}
138
+ self.diagnostics: list[PluginDiagnostic] = []
139
+ for plugin in plugins:
140
+ self.register(plugin)
141
+
142
+ @property
143
+ def plugins(self) -> tuple[CseqPlugin, ...]:
144
+ return tuple(self._plugins[name] for name in sorted(self._plugins))
145
+
146
+ def register(self, plugin: CseqPlugin) -> None:
147
+ descriptor = getattr(plugin, "descriptor", None)
148
+ if not isinstance(descriptor, PluginDescriptor):
149
+ raise TypeError("plugin must expose PluginDescriptor as descriptor")
150
+ if descriptor.api_version != PLUGIN_API_VERSION:
151
+ raise ValueError(
152
+ f"plugin {descriptor.name} API {descriptor.api_version} is incompatible with cseq API {PLUGIN_API_VERSION}"
153
+ )
154
+ self._plugins[descriptor.name] = plugin
155
+
156
+ @classmethod
157
+ def discover(cls, enabled: Iterable[str] | None = None) -> "PluginManager":
158
+ enabled_set = set(enabled or ())
159
+ manager = cls()
160
+ try:
161
+ eps = metadata.entry_points()
162
+ group = eps.select(group=ENTRY_POINT_GROUP) if hasattr(eps, "select") else eps.get(ENTRY_POINT_GROUP, [])
163
+ except Exception as exc: # discovery itself must not kill core analysis
164
+ manager.diagnostics.append(PluginDiagnostic("<discovery>", "warning", "PLUGIN_DISCOVERY_FAILED", str(exc)))
165
+ return manager
166
+ discovered_names: set[str] = set()
167
+ for ep in group:
168
+ discovered_names.add(ep.name)
169
+ if enabled_set and ep.name not in enabled_set:
170
+ continue
171
+ try:
172
+ obj = ep.load()
173
+ plugin = obj() if isinstance(obj, type) else obj
174
+ manager.register(plugin)
175
+ except Exception as exc:
176
+ manager.diagnostics.append(PluginDiagnostic(ep.name, "warning", "PLUGIN_LOAD_FAILED", str(exc)))
177
+ for missing in sorted(enabled_set - discovered_names):
178
+ manager.diagnostics.append(PluginDiagnostic(missing, "warning", "PLUGIN_NOT_FOUND", "enabled plugin was not discovered"))
179
+ return manager
180
+
181
+ def run_semantic_hints(
182
+ self,
183
+ *,
184
+ functions: Iterable[FunctionRecord] = (),
185
+ callsites: Iterable[CallSiteRecord] = (),
186
+ ) -> PluginRunResult:
187
+ fviews = tuple(FunctionView.from_record(x) for x in functions)
188
+ cviews = tuple(CallSiteView.from_record(x) for x in callsites)
189
+ return self._run_hook("semantic_hints", functions=fviews, callsites=cviews)
190
+
191
+ def run_runtime_event_hints(self, events: Iterable[RuntimeEvent]) -> PluginRunResult:
192
+ views = tuple(RuntimeEventView.from_record(x) for x in events)
193
+ return self._run_hook("runtime_event_hints", events=views)
194
+
195
+ def _run_hook(self, hook: str, **kwargs: Any) -> PluginRunResult:
196
+ result = PluginRunResult(diagnostics=list(self.diagnostics))
197
+ for plugin in self.plugins:
198
+ fn = getattr(plugin, hook, None)
199
+ if fn is None:
200
+ continue
201
+ name = plugin.descriptor.name
202
+ try:
203
+ returned = fn(**kwargs)
204
+ if returned is None:
205
+ continue
206
+ for item in returned:
207
+ if not isinstance(item, PluginEvidence):
208
+ raise TypeError(f"{hook} must yield PluginEvidence, got {type(item).__name__}")
209
+ if item.plugin_name != name:
210
+ raise ValueError("PluginEvidence.plugin_name must match plugin descriptor name")
211
+ if item.kind not in plugin.descriptor.kinds:
212
+ raise ValueError(f"evidence kind {item.kind.value} was not declared by plugin")
213
+ result.evidence.append(item)
214
+ except Exception as exc:
215
+ result.diagnostics.append(PluginDiagnostic(name, "warning", "PLUGIN_HOOK_FAILED", f"{hook}: {exc}"))
216
+ return result