openmodus 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.
- modus/__init__.py +13 -0
- modus/__main__.py +6 -0
- modus/_bundled/schemas/module-meta.schema.yaml +86 -0
- modus/activation_guidance.py +18 -0
- modus/artifacts_io.py +1066 -0
- modus/catalog.py +361 -0
- modus/cli.py +1785 -0
- modus/commands/__init__.py +17 -0
- modus/commands/context_cmd.py +50 -0
- modus/commands/help.py +131 -0
- modus/commands/init_project.py +503 -0
- modus/commands/install_state.py +212 -0
- modus/commands/maintenance_scope.py +69 -0
- modus/commands/permission_errors.py +26 -0
- modus/commands/reporting.py +113 -0
- modus/commands/self_update.py +577 -0
- modus/commands/status.py +52 -0
- modus/commands/uninstall.py +359 -0
- modus/commands/uninstall_report.py +183 -0
- modus/commands/uninstall_self.py +373 -0
- modus/commands/update.py +526 -0
- modus/commands/update_report.py +162 -0
- modus/commands/verify_anchors.py +230 -0
- modus/config.py +492 -0
- modus/git_hygiene.py +466 -0
- modus/hooks/__init__.py +21 -0
- modus/hooks/bootstrap.py +232 -0
- modus/hooks/context.py +29 -0
- modus/hooks/contracts.py +111 -0
- modus/hooks/environment.py +86 -0
- modus/hooks/protocol.py +156 -0
- modus/hooks/runtime.py +136 -0
- modus/init_analysis/README.md +72 -0
- modus/init_analysis/__init__.py +35 -0
- modus/init_analysis/api.py +360 -0
- modus/init_analysis/authoring_projection.py +192 -0
- modus/init_analysis/build_context.py +1326 -0
- modus/init_analysis/bundle.py +1092 -0
- modus/init_analysis/capability_evidence.py +585 -0
- modus/init_analysis/catalog.py +448 -0
- modus/init_analysis/compiler/__init__.py +1 -0
- modus/init_analysis/compiler/cache.py +63 -0
- modus/init_analysis/compiler/cpp.py +536 -0
- modus/init_analysis/compiler/go.py +158 -0
- modus/init_analysis/compiler/java.py +112 -0
- modus/init_analysis/compiler/merge.py +211 -0
- modus/init_analysis/compiler/models.py +88 -0
- modus/init_analysis/compiler/process.py +259 -0
- modus/init_analysis/compiler/python.py +430 -0
- modus/init_analysis/compiler/typescript.py +73 -0
- modus/init_analysis/contracts.py +283 -0
- modus/init_analysis/engine.py +230 -0
- modus/init_analysis/entry_contracts.py +155 -0
- modus/init_analysis/evidence_graph.py +133 -0
- modus/init_analysis/evidence_records.py +211 -0
- modus/init_analysis/extraction_cache.py +309 -0
- modus/init_analysis/fingerprint.py +38 -0
- modus/init_analysis/framework_facts.py +472 -0
- modus/init_analysis/frontend.py +1523 -0
- modus/init_analysis/generic.py +77 -0
- modus/init_analysis/ids.py +35 -0
- modus/init_analysis/input_snapshot.py +675 -0
- modus/init_analysis/language-registry.json +550 -0
- modus/init_analysis/language_capabilities.py +78 -0
- modus/init_analysis/native_binding.py +734 -0
- modus/init_analysis/native_parser.py +905 -0
- modus/init_analysis/navigation.py +184 -0
- modus/init_analysis/pipeline.py +504 -0
- modus/init_analysis/projection.py +409 -0
- modus/init_analysis/provider_assets/ModusJavaAnalyzer.java +350 -0
- modus/init_analysis/provider_assets/modus_go_analyzer.go +367 -0
- modus/init_analysis/provider_assets/modus_typescript_analyzer.cjs +163 -0
- modus/init_analysis/reference_outcomes.py +318 -0
- modus/init_analysis/resources.py +78 -0
- modus/init_analysis/scan.py +95 -0
- modus/init_analysis/scope_candidates.py +372 -0
- modus/init_analysis/search_metadata.py +32 -0
- modus/init_analysis/semantic_contracts.py +119 -0
- modus/init_analysis/semantic_ir.py +582 -0
- modus/init_analysis/semantic_providers.py +188 -0
- modus/init_analysis/semantic_reconcile.py +491 -0
- modus/init_analysis/source_contracts.py +127 -0
- modus/init_analysis/source_coordinates.py +80 -0
- modus/init_analysis/source_filters.py +184 -0
- modus/init_analysis/source_inventory.py +251 -0
- modus/init_analysis/source_roles.py +99 -0
- modus/init_analysis/source_snapshot.py +65 -0
- modus/init_analysis/structural.py +119 -0
- modus/init_analysis/topology.py +2060 -0
- modus/init_analysis/value_summary.py +463 -0
- modus/init_analysis/view_quarantine.py +606 -0
- modus/knowledge_identity.py +200 -0
- modus/knowledge_reader.py +220 -0
- modus/knowledge_seeds.py +56 -0
- modus/knowledge_update.py +212 -0
- modus/knowledge_write_session.py +241 -0
- modus/manifest.py +210 -0
- modus/orchestrator/README.md +67 -0
- modus/orchestrator/__init__.py +14 -0
- modus/orchestrator/context_resolver.py +142 -0
- modus/orchestrator/evidence_search.py +473 -0
- modus/orchestrator/fallback.py +83 -0
- modus/orchestrator/knowledge_sync.py +349 -0
- modus/orchestrator/models.py +82 -0
- modus/orchestrator/module_meta.py +194 -0
- modus/orchestrator/path_scope.py +38 -0
- modus/orchestrator/preflight.py +125 -0
- modus/orchestrator/query_terms.py +124 -0
- modus/orchestrator/search_policy.py +161 -0
- modus/paths.py +279 -0
- modus/platforms/__init__.py +45 -0
- modus/platforms/base.py +299 -0
- modus/platforms/claude.py +98 -0
- modus/platforms/codebuddy.py +51 -0
- modus/platforms/codex.py +41 -0
- modus/platforms/cursor.py +45 -0
- modus/platforms/hooks.py +188 -0
- modus/platforms/models.py +166 -0
- modus/platforms/registry.py +88 -0
- modus/platforms/rendering.py +77 -0
- modus/platforms/sections.py +100 -0
- modus/precedence.py +47 -0
- modus/project_activation.py +99 -0
- modus/retrieval/README.md +64 -0
- modus/retrieval/__init__.py +9 -0
- modus/retrieval/budget.py +50 -0
- modus/retrieval/delivery.py +226 -0
- modus/retrieval/index.py +92 -0
- modus/retrieval/pipeline.py +718 -0
- modus/retrieval/query.py +189 -0
- modus/retrieval/routing.py +237 -0
- modus/retrieval/runtime.py +39 -0
- modus/retrieval/settings.py +75 -0
- modus/retrieval/stages/__init__.py +7 -0
- modus/retrieval/stages/discovery.py +91 -0
- modus/retrieval/stages/documents.py +694 -0
- modus/retrieval/stages/lexical.py +116 -0
- modus/retrieval/stages/ranking.py +179 -0
- modus/retrieval/stages/sections.py +64 -0
- modus/retrieval/stages/selection.py +221 -0
- modus/retrieval/stages/settings.py +7 -0
- modus/scaffold/README.md +68 -0
- modus/scaffold/__init__.py +36 -0
- modus/scaffold/catalog.py +170 -0
- modus/scaffold/project.py +361 -0
- modus/scaffold/templates/README.md +37 -0
- modus/scaffold/templates/commands/init.md +19 -0
- modus/scaffold/templates/commands/reinit.md +18 -0
- modus/scaffold/templates/commands/update-knowledge.md +19 -0
- modus/scaffold/templates/knowledge/constraint/global-standard.md +18 -0
- modus/scaffold/templates/knowledge/constraint/team-standard.md +28 -0
- modus/scaffold/templates/knowledge/handbook/SKILL.md +21 -0
- modus/scaffold/templates/skills/modus-init/SKILL.md +52 -0
- modus/scaffold/templates/skills/modus-init/references/architecture-overview.md +23 -0
- modus/scaffold/templates/skills/modus-init/references/business-skill.md +65 -0
- modus/scaffold/templates/skills/modus-init/references/code-skill.md +190 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-extraction.md +86 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-profiles/cpp.md +39 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-profiles/go.md +39 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-profiles/java.md +39 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-profiles/php.md +39 -0
- modus/scaffold/templates/skills/modus-init/references/constraint-profiles/web.md +50 -0
- modus/scaffold/templates/skills/modus-init/references/global-standard.md +9 -0
- modus/scaffold/templates/skills/modus-init/references/language-adaptation.md +19 -0
- modus/scaffold/templates/skills/modus-init/references/scope-discovery.md +86 -0
- modus/scaffold/templates/skills/modus-init/references/team-standard.md +52 -0
- modus/scaffold/templates/skills/modus-init/references/workspace-init.md +67 -0
- modus/scaffold/templates/skills/modus-init/references/zh-style.md +12 -0
- modus/scaffold/templates/skills/modus-reinit/SKILL.md +22 -0
- modus/scaffold/templates/skills/modus-sync-knowledge/SKILL.md +28 -0
- modus/scaffold/templates/skills/modus-update-knowledge/SKILL.md +22 -0
- modus/scaffold/templates/skills/using-modus/SKILL.md +31 -0
- modus/scaffold/templates/skills/using-modus/references/constraint-usage.md +28 -0
- modus/scaffold/templates/skills/using-modus/references/framework-update.md +15 -0
- modus/scaffold/templates/skills/using-modus/references/knowledge-update-lifecycle.md +68 -0
- modus/scaffold/templates/skills/using-modus/references/router-details.md +42 -0
- modus/scaffold/writer.py +101 -0
- modus/schema_resources.py +56 -0
- modus/scope_models.py +504 -0
- modus/scope_resolver.py +109 -0
- modus/sensitive_literals.py +64 -0
- modus/skillmd.py +144 -0
- modus/source_listing.py +82 -0
- modus/surface.py +65 -0
- modus/text_tree.py +64 -0
- modus/versioning.py +61 -0
- modus/workspace.py +881 -0
- openmodus-0.0.1.dist-info/METADATA +251 -0
- openmodus-0.0.1.dist-info/RECORD +192 -0
- openmodus-0.0.1.dist-info/WHEEL +4 -0
- openmodus-0.0.1.dist-info/entry_points.txt +2 -0
- openmodus-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""Language-neutral typed semantic facts emitted by compiler providers.
|
|
2
|
+
|
|
3
|
+
The wire contract is intentionally JSONL-friendly: every line is one ``node``,
|
|
4
|
+
``edge`` or ``diagnostic`` record. Keeping the provider boundary process based
|
|
5
|
+
isolates compiler crashes and version conflicts from the Modus process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Literal, cast
|
|
15
|
+
|
|
16
|
+
from .semantic_contracts import AnalysisContext, CoverageReceipt, ObservationDisposition
|
|
17
|
+
|
|
18
|
+
NODE_KINDS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"repository",
|
|
21
|
+
"build-unit",
|
|
22
|
+
"deployable-service",
|
|
23
|
+
"component",
|
|
24
|
+
"endpoint",
|
|
25
|
+
"function",
|
|
26
|
+
"method",
|
|
27
|
+
"type",
|
|
28
|
+
"rpc-service",
|
|
29
|
+
"topic",
|
|
30
|
+
"db",
|
|
31
|
+
"table",
|
|
32
|
+
"cache",
|
|
33
|
+
"keyspace",
|
|
34
|
+
"config-key",
|
|
35
|
+
"state-machine",
|
|
36
|
+
"state",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
RELATIONS = frozenset(
|
|
40
|
+
{
|
|
41
|
+
"contains",
|
|
42
|
+
"implements",
|
|
43
|
+
"injects",
|
|
44
|
+
"calls",
|
|
45
|
+
"exposes",
|
|
46
|
+
"invokes",
|
|
47
|
+
"publishes",
|
|
48
|
+
"consumes",
|
|
49
|
+
"reads",
|
|
50
|
+
"writes",
|
|
51
|
+
"invalidates",
|
|
52
|
+
"configured_by",
|
|
53
|
+
"transitions_to",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
RESOLUTIONS = frozenset({"exact", "candidate", "heuristic", "unresolved"})
|
|
57
|
+
BINDING_KINDS = frozenset(
|
|
58
|
+
{
|
|
59
|
+
"declared-exact",
|
|
60
|
+
"runtime-exact",
|
|
61
|
+
"runtime-candidate",
|
|
62
|
+
"heuristic",
|
|
63
|
+
"unresolved",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
ANALYSIS_COMPLETENESS = frozenset({"complete", "partial", "fallback"})
|
|
67
|
+
LANGUAGES = frozenset(
|
|
68
|
+
{"python", "go", "java", "cpp", "javascript", "typescript", "tsx", "generic", "external"}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def stable_id(prefix: str, *parts: object) -> str:
|
|
73
|
+
body = "\x1f".join(str(part) for part in parts)
|
|
74
|
+
return f"{prefix}:{hashlib.sha256(body.encode('utf-8')).hexdigest()[:24]}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def canonical_json(value: Any) -> str:
|
|
78
|
+
return json.dumps(
|
|
79
|
+
value,
|
|
80
|
+
ensure_ascii=False,
|
|
81
|
+
sort_keys=True,
|
|
82
|
+
separators=(",", ":"),
|
|
83
|
+
allow_nan=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class SourceAnchor:
|
|
89
|
+
file: str
|
|
90
|
+
line: int
|
|
91
|
+
column: int = 1
|
|
92
|
+
end_line: int | None = None
|
|
93
|
+
end_column: int | None = None
|
|
94
|
+
column_encoding: str = "unknown"
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
if not self.file or self.file.startswith("/") or ".." in self.file.split("/"):
|
|
98
|
+
raise ValueError("source anchor file must be a repository-relative path")
|
|
99
|
+
if self.line < 1 or self.column < 1:
|
|
100
|
+
raise ValueError("source anchor positions are one-based")
|
|
101
|
+
if self.end_line is not None and self.end_line < self.line:
|
|
102
|
+
raise ValueError("source anchor end line may not precede its start")
|
|
103
|
+
if self.end_column is not None and self.end_column < 1:
|
|
104
|
+
raise ValueError("source anchor end column is one-based")
|
|
105
|
+
if self.column_encoding not in {"unknown", "utf8-byte", "utf16", "unicode-codepoint"}:
|
|
106
|
+
raise ValueError("unsupported source column encoding")
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict[str, Any]:
|
|
109
|
+
result: dict[str, Any] = {
|
|
110
|
+
"file": self.file.replace("\\", "/"),
|
|
111
|
+
"line": self.line,
|
|
112
|
+
"column": self.column,
|
|
113
|
+
"columnEncoding": self.column_encoding,
|
|
114
|
+
}
|
|
115
|
+
result["endLine"] = self.end_line if self.end_line is not None else self.line
|
|
116
|
+
result["endColumn"] = self.end_column if self.end_column is not None else self.column
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, value: Mapping[str, Any]) -> SourceAnchor:
|
|
121
|
+
return cls(
|
|
122
|
+
file=str(value["file"]),
|
|
123
|
+
line=int(value["line"]),
|
|
124
|
+
column=int(value.get("column", 1)),
|
|
125
|
+
end_line=int(value["endLine"]) if value.get("endLine") is not None else None,
|
|
126
|
+
end_column=(int(value["endColumn"]) if value.get("endColumn") is not None else None),
|
|
127
|
+
column_encoding=str(value.get("columnEncoding") or "unknown"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class Provenance:
|
|
133
|
+
provider: str
|
|
134
|
+
provider_version: str
|
|
135
|
+
context_digest: str
|
|
136
|
+
rule_pack: str = ""
|
|
137
|
+
|
|
138
|
+
def __post_init__(self) -> None:
|
|
139
|
+
if not self.provider or not self.provider_version:
|
|
140
|
+
raise ValueError("provider and provider version are required")
|
|
141
|
+
if len(self.context_digest) != 64 or any(
|
|
142
|
+
item not in "0123456789abcdef" for item in self.context_digest
|
|
143
|
+
):
|
|
144
|
+
raise ValueError("context digest must be a lowercase SHA-256 digest")
|
|
145
|
+
|
|
146
|
+
def to_dict(self) -> dict[str, str]:
|
|
147
|
+
result = {
|
|
148
|
+
"provider": self.provider,
|
|
149
|
+
"providerVersion": self.provider_version,
|
|
150
|
+
"contextDigest": self.context_digest,
|
|
151
|
+
}
|
|
152
|
+
if self.rule_pack:
|
|
153
|
+
result["rulePack"] = self.rule_pack
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, value: Mapping[str, Any]) -> Provenance:
|
|
158
|
+
return cls(
|
|
159
|
+
provider=str(value["provider"]),
|
|
160
|
+
provider_version=str(value["providerVersion"]),
|
|
161
|
+
context_digest=str(value["contextDigest"]),
|
|
162
|
+
rule_pack=str(value.get("rulePack", "")),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class SemanticNode:
|
|
168
|
+
id: str
|
|
169
|
+
kind: str
|
|
170
|
+
language: str
|
|
171
|
+
qualified_name: str
|
|
172
|
+
build_unit_id: str
|
|
173
|
+
source_anchors: tuple[SourceAnchor, ...] = ()
|
|
174
|
+
deployable_id: str = ""
|
|
175
|
+
attributes: Mapping[str, Any] = field(default_factory=dict)
|
|
176
|
+
provenance: Provenance | None = None
|
|
177
|
+
symbol_id: str = ""
|
|
178
|
+
analysis_completeness: str = "complete"
|
|
179
|
+
analysis_context_id: str = ""
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
if self.kind not in NODE_KINDS:
|
|
183
|
+
raise ValueError(f"unsupported semantic node kind: {self.kind}")
|
|
184
|
+
if self.language not in LANGUAGES:
|
|
185
|
+
raise ValueError(f"unsupported semantic node language: {self.language}")
|
|
186
|
+
if not self.id or not self.qualified_name or not self.build_unit_id:
|
|
187
|
+
raise ValueError("semantic node id, qualifiedName and buildUnitId are required")
|
|
188
|
+
if self.analysis_completeness not in ANALYSIS_COMPLETENESS:
|
|
189
|
+
raise ValueError(f"unsupported analysis completeness: {self.analysis_completeness}")
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict[str, Any]:
|
|
192
|
+
result = {
|
|
193
|
+
"recordKind": "node",
|
|
194
|
+
"id": self.id,
|
|
195
|
+
"kind": self.kind,
|
|
196
|
+
"language": self.language,
|
|
197
|
+
"qualifiedName": self.qualified_name,
|
|
198
|
+
"buildUnitId": self.build_unit_id,
|
|
199
|
+
"symbolId": self.symbol_id or self.id,
|
|
200
|
+
"analysisContextId": self.analysis_context_id,
|
|
201
|
+
"analysisCompleteness": self.analysis_completeness,
|
|
202
|
+
"sourceAnchors": [item.to_dict() for item in self.source_anchors],
|
|
203
|
+
"attributes": dict(self.attributes),
|
|
204
|
+
}
|
|
205
|
+
if self.deployable_id:
|
|
206
|
+
result["deployableId"] = self.deployable_id
|
|
207
|
+
if self.provenance is not None:
|
|
208
|
+
result["provenance"] = self.provenance.to_dict()
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_dict(cls, value: Mapping[str, Any]) -> SemanticNode:
|
|
213
|
+
return cls(
|
|
214
|
+
id=str(value["id"]),
|
|
215
|
+
kind=str(value["kind"]),
|
|
216
|
+
language=str(value["language"]),
|
|
217
|
+
qualified_name=str(value["qualifiedName"]),
|
|
218
|
+
build_unit_id=str(value["buildUnitId"]),
|
|
219
|
+
source_anchors=tuple(
|
|
220
|
+
SourceAnchor.from_dict(item) for item in value.get("sourceAnchors", ())
|
|
221
|
+
),
|
|
222
|
+
deployable_id=str(value.get("deployableId", "")),
|
|
223
|
+
attributes=dict(value.get("attributes") or {}),
|
|
224
|
+
provenance=(
|
|
225
|
+
Provenance.from_dict(value["provenance"]) if value.get("provenance") else None
|
|
226
|
+
),
|
|
227
|
+
symbol_id=str(value.get("symbolId") or value["id"]),
|
|
228
|
+
analysis_context_id=str(value.get("analysisContextId", "")),
|
|
229
|
+
analysis_completeness=str(value.get("analysisCompleteness", "complete")),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class SemanticEdge:
|
|
235
|
+
id: str
|
|
236
|
+
source: str
|
|
237
|
+
target: str
|
|
238
|
+
relation: str
|
|
239
|
+
resolution: str
|
|
240
|
+
operation: str = ""
|
|
241
|
+
call_site: SourceAnchor | None = None
|
|
242
|
+
dispatch: str = ""
|
|
243
|
+
contract_key: str = ""
|
|
244
|
+
evidence: tuple[str, ...] = ()
|
|
245
|
+
attributes: Mapping[str, Any] = field(default_factory=dict)
|
|
246
|
+
provenance: Provenance | None = None
|
|
247
|
+
binding_kind: str = ""
|
|
248
|
+
analysis_completeness: str = "complete"
|
|
249
|
+
analysis_context_id: str = ""
|
|
250
|
+
declaration_resolution: str = ""
|
|
251
|
+
dispatch_resolution: str = ""
|
|
252
|
+
|
|
253
|
+
def __post_init__(self) -> None:
|
|
254
|
+
if self.relation not in RELATIONS:
|
|
255
|
+
raise ValueError(f"unsupported semantic edge relation: {self.relation}")
|
|
256
|
+
if self.resolution not in RESOLUTIONS:
|
|
257
|
+
raise ValueError(f"unsupported semantic edge resolution: {self.resolution}")
|
|
258
|
+
if not self.id or not self.source or not self.target:
|
|
259
|
+
raise ValueError("semantic edge id, from and to are required")
|
|
260
|
+
binding_kind = self.binding_kind or _default_binding_kind(self.resolution)
|
|
261
|
+
if binding_kind not in BINDING_KINDS:
|
|
262
|
+
raise ValueError(f"unsupported semantic binding kind: {binding_kind}")
|
|
263
|
+
if self.analysis_completeness not in ANALYSIS_COMPLETENESS:
|
|
264
|
+
raise ValueError(f"unsupported analysis completeness: {self.analysis_completeness}")
|
|
265
|
+
if self.declaration_resolution and self.declaration_resolution not in RESOLUTIONS:
|
|
266
|
+
raise ValueError("invalid declaration resolution")
|
|
267
|
+
if self.declaration_resolution and self.declaration_resolution != self.resolution:
|
|
268
|
+
raise ValueError("legacy resolution must mirror declaration resolution")
|
|
269
|
+
if self.dispatch_resolution not in {
|
|
270
|
+
"",
|
|
271
|
+
"not-analyzed",
|
|
272
|
+
"singleton",
|
|
273
|
+
"candidates",
|
|
274
|
+
"unresolved",
|
|
275
|
+
"non-applicable",
|
|
276
|
+
}:
|
|
277
|
+
raise ValueError("invalid dispatch resolution")
|
|
278
|
+
expected = {"runtime-exact": "singleton", "runtime-candidate": "candidates"}.get(
|
|
279
|
+
binding_kind
|
|
280
|
+
)
|
|
281
|
+
if expected and self.dispatch_resolution and self.dispatch_resolution != expected:
|
|
282
|
+
raise ValueError("binding kind conflicts with dispatch resolution")
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def declaration_precision(self) -> str:
|
|
286
|
+
"""Precision of the statically named declaration, independently of dispatch."""
|
|
287
|
+
return self.declaration_resolution or self.resolution
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def dispatch_precision(self) -> str:
|
|
291
|
+
"""A singleton is conditional on execution; it is never a must-execute claim."""
|
|
292
|
+
return self.dispatch_resolution or {
|
|
293
|
+
"runtime-exact": "singleton",
|
|
294
|
+
"runtime-candidate": "candidates",
|
|
295
|
+
"unresolved": "unresolved",
|
|
296
|
+
}.get(self.binding_kind, "not-analyzed")
|
|
297
|
+
|
|
298
|
+
def to_dict(self) -> dict[str, Any]:
|
|
299
|
+
result = {
|
|
300
|
+
"recordKind": "edge",
|
|
301
|
+
"id": self.id,
|
|
302
|
+
"from": self.source,
|
|
303
|
+
"to": self.target,
|
|
304
|
+
"relation": self.relation,
|
|
305
|
+
"resolution": self.resolution,
|
|
306
|
+
"bindingKind": self.binding_kind or _default_binding_kind(self.resolution),
|
|
307
|
+
"analysisContextId": self.analysis_context_id,
|
|
308
|
+
"declarationResolution": self.declaration_precision,
|
|
309
|
+
"dispatchResolution": self.dispatch_precision,
|
|
310
|
+
"analysisCompleteness": self.analysis_completeness,
|
|
311
|
+
"operation": self.operation,
|
|
312
|
+
"dispatch": self.dispatch,
|
|
313
|
+
"contractKey": self.contract_key,
|
|
314
|
+
"evidence": list(self.evidence),
|
|
315
|
+
"attributes": dict(self.attributes),
|
|
316
|
+
}
|
|
317
|
+
if self.call_site is not None:
|
|
318
|
+
result["callSite"] = self.call_site.to_dict()
|
|
319
|
+
if self.provenance is not None:
|
|
320
|
+
result["provenance"] = self.provenance.to_dict()
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
def from_dict(cls, value: Mapping[str, Any]) -> SemanticEdge:
|
|
325
|
+
return cls(
|
|
326
|
+
id=str(value["id"]),
|
|
327
|
+
source=str(value["from"]),
|
|
328
|
+
target=str(value["to"]),
|
|
329
|
+
relation=str(value["relation"]),
|
|
330
|
+
resolution=str(value["resolution"]),
|
|
331
|
+
operation=str(value.get("operation", "")),
|
|
332
|
+
call_site=(
|
|
333
|
+
SourceAnchor.from_dict(value["callSite"]) if value.get("callSite") else None
|
|
334
|
+
),
|
|
335
|
+
dispatch=str(value.get("dispatch", "")),
|
|
336
|
+
contract_key=str(value.get("contractKey", "")),
|
|
337
|
+
evidence=tuple(str(item) for item in value.get("evidence", ())),
|
|
338
|
+
attributes=dict(value.get("attributes") or {}),
|
|
339
|
+
provenance=(
|
|
340
|
+
Provenance.from_dict(value["provenance"]) if value.get("provenance") else None
|
|
341
|
+
),
|
|
342
|
+
analysis_context_id=str(value.get("analysisContextId", "")),
|
|
343
|
+
declaration_resolution=str(value.get("declarationResolution", "")),
|
|
344
|
+
dispatch_resolution=str(value.get("dispatchResolution", "")),
|
|
345
|
+
binding_kind=str(
|
|
346
|
+
value.get("bindingKind") or _default_binding_kind(str(value["resolution"]))
|
|
347
|
+
),
|
|
348
|
+
analysis_completeness=str(value.get("analysisCompleteness", "complete")),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _default_binding_kind(resolution: str) -> str:
|
|
353
|
+
return {
|
|
354
|
+
"exact": "declared-exact",
|
|
355
|
+
"candidate": "runtime-candidate",
|
|
356
|
+
"heuristic": "heuristic",
|
|
357
|
+
"unresolved": "unresolved",
|
|
358
|
+
}[resolution]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass(frozen=True)
|
|
362
|
+
class SemanticDiagnostic:
|
|
363
|
+
code: str
|
|
364
|
+
message: str
|
|
365
|
+
severity: Literal["info", "warning", "error"]
|
|
366
|
+
provider: str
|
|
367
|
+
build_unit_id: str = ""
|
|
368
|
+
subject: str = ""
|
|
369
|
+
|
|
370
|
+
def to_dict(self) -> dict[str, str]:
|
|
371
|
+
return {
|
|
372
|
+
"recordKind": "diagnostic",
|
|
373
|
+
"code": self.code,
|
|
374
|
+
"message": self.message,
|
|
375
|
+
"severity": self.severity,
|
|
376
|
+
"provider": self.provider,
|
|
377
|
+
"buildUnitId": self.build_unit_id,
|
|
378
|
+
"subject": self.subject,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass(frozen=True)
|
|
383
|
+
class SemanticFacts:
|
|
384
|
+
nodes: tuple[SemanticNode, ...] = ()
|
|
385
|
+
edges: tuple[SemanticEdge, ...] = ()
|
|
386
|
+
diagnostics: tuple[SemanticDiagnostic, ...] = ()
|
|
387
|
+
contexts: tuple[AnalysisContext, ...] = ()
|
|
388
|
+
dispositions: tuple[ObservationDisposition, ...] = ()
|
|
389
|
+
coverage_receipts: tuple[CoverageReceipt, ...] = ()
|
|
390
|
+
|
|
391
|
+
def to_dict(self) -> dict[str, Any]:
|
|
392
|
+
return {
|
|
393
|
+
"schemaVersion": 1,
|
|
394
|
+
"nodes": [item.to_dict() for item in self.nodes],
|
|
395
|
+
"edges": [item.to_dict() for item in self.edges],
|
|
396
|
+
"diagnostics": [item.to_dict() for item in self.diagnostics],
|
|
397
|
+
"contexts": [item.to_dict() for item in self.contexts],
|
|
398
|
+
"dispositions": [item.to_dict() for item in self.dispositions],
|
|
399
|
+
"coverageReceipts": [item.to_dict() for item in self.coverage_receipts],
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def parse_jsonl(lines: Iterable[str]) -> SemanticFacts:
|
|
404
|
+
nodes: list[SemanticNode] = []
|
|
405
|
+
edges: list[SemanticEdge] = []
|
|
406
|
+
diagnostics: list[SemanticDiagnostic] = []
|
|
407
|
+
contexts = []
|
|
408
|
+
dispositions = []
|
|
409
|
+
receipts = []
|
|
410
|
+
for position, line in enumerate(lines, start=1):
|
|
411
|
+
if not line.strip():
|
|
412
|
+
continue
|
|
413
|
+
try:
|
|
414
|
+
value = json.loads(line)
|
|
415
|
+
except json.JSONDecodeError as exc:
|
|
416
|
+
raise ValueError(f"provider JSONL line {position} is invalid: {exc}") from exc
|
|
417
|
+
if not isinstance(value, Mapping):
|
|
418
|
+
raise ValueError(f"provider JSONL line {position} must be an object")
|
|
419
|
+
kind = value.get("recordKind")
|
|
420
|
+
if kind == "node":
|
|
421
|
+
node = SemanticNode.from_dict(value)
|
|
422
|
+
nodes.append(node)
|
|
423
|
+
elif kind == "edge":
|
|
424
|
+
edge = SemanticEdge.from_dict(value)
|
|
425
|
+
edges.append(edge)
|
|
426
|
+
elif kind == "diagnostic":
|
|
427
|
+
severity = str(value.get("severity", "warning"))
|
|
428
|
+
if severity not in {"info", "warning", "error"}:
|
|
429
|
+
raise ValueError(f"provider diagnostic severity is invalid on line {position}")
|
|
430
|
+
diagnostics.append(
|
|
431
|
+
SemanticDiagnostic(
|
|
432
|
+
code=str(value.get("code", "provider.unknown")),
|
|
433
|
+
message=str(value.get("message", "")),
|
|
434
|
+
severity=cast(Literal["info", "warning", "error"], severity),
|
|
435
|
+
provider=str(value.get("provider", "unknown")),
|
|
436
|
+
build_unit_id=str(value.get("buildUnitId", "")),
|
|
437
|
+
subject=str(value.get("subject", "")),
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
elif kind == "analysis-context":
|
|
441
|
+
contexts.append(AnalysisContext.from_dict(value))
|
|
442
|
+
elif kind == "disposition":
|
|
443
|
+
dispositions.append(ObservationDisposition.from_dict(value))
|
|
444
|
+
elif kind == "coverage-receipt":
|
|
445
|
+
receipts.append(CoverageReceipt.from_dict(value))
|
|
446
|
+
else:
|
|
447
|
+
raise ValueError(f"provider JSONL line {position} has unknown recordKind {kind!r}")
|
|
448
|
+
return normalize_facts(
|
|
449
|
+
SemanticFacts(
|
|
450
|
+
tuple(nodes),
|
|
451
|
+
tuple(edges),
|
|
452
|
+
tuple(diagnostics),
|
|
453
|
+
tuple(contexts),
|
|
454
|
+
tuple(dispositions),
|
|
455
|
+
tuple(receipts),
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def observation_context(item: SemanticNode | SemanticEdge) -> str:
|
|
461
|
+
return item.analysis_context_id or (item.provenance.context_digest if item.provenance else "")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def normalize_facts(facts: SemanticFacts) -> SemanticFacts:
|
|
465
|
+
"""Quarantine contradictory identities; identical retransmissions are idempotent.
|
|
466
|
+
|
|
467
|
+
Processing receipts cover every emitted record. Conflicting payloads are
|
|
468
|
+
never resolved by arrival order or by selecting a cosmetically better node.
|
|
469
|
+
"""
|
|
470
|
+
dispositions = list(facts.dispositions)
|
|
471
|
+
diagnostics = list(facts.diagnostics)
|
|
472
|
+
accepted: dict[str, list[Any]] = {"node": [], "edge": []}
|
|
473
|
+
existing = {
|
|
474
|
+
(
|
|
475
|
+
row.record_kind,
|
|
476
|
+
row.record_id,
|
|
477
|
+
row.context_id,
|
|
478
|
+
row.status,
|
|
479
|
+
row.content_digest if row.status == "conflict" else "",
|
|
480
|
+
)
|
|
481
|
+
for row in dispositions
|
|
482
|
+
}
|
|
483
|
+
previous_duplicates: dict[tuple[str, str, str], int] = {}
|
|
484
|
+
for row in dispositions:
|
|
485
|
+
if row.status == "duplicate":
|
|
486
|
+
duplicate_key = (row.record_kind, row.record_id, row.context_id)
|
|
487
|
+
previous_duplicates[duplicate_key] = previous_duplicates.get(duplicate_key, 0) + 1
|
|
488
|
+
for kind, records in (("node", facts.nodes), ("edge", facts.edges)):
|
|
489
|
+
groups: dict[tuple[str, str], list[Any]] = {}
|
|
490
|
+
for item in records:
|
|
491
|
+
groups.setdefault((observation_context(item), item.id), []).append(item)
|
|
492
|
+
for (context, identity), items in sorted(groups.items()):
|
|
493
|
+
payloads = {canonical_json(item.to_dict()) for item in items}
|
|
494
|
+
conflict = len(payloads) > 1
|
|
495
|
+
if not conflict:
|
|
496
|
+
accepted[kind].append(items[0])
|
|
497
|
+
else:
|
|
498
|
+
diagnostics.append(
|
|
499
|
+
SemanticDiagnostic(
|
|
500
|
+
"semantic.identity-conflict",
|
|
501
|
+
f"Conflicting {kind} observations: {identity}",
|
|
502
|
+
"warning",
|
|
503
|
+
"semantic-contract",
|
|
504
|
+
subject=identity,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
# A normalization pass is idempotent for its already-receipted inputs.
|
|
508
|
+
for index, item in enumerate(
|
|
509
|
+
sorted(items, key=lambda row: canonical_json(row.to_dict()))
|
|
510
|
+
):
|
|
511
|
+
digest = hashlib.sha256(canonical_json(item.to_dict()).encode()).hexdigest()
|
|
512
|
+
status = "conflict" if conflict else "accepted" if index == 0 else "duplicate"
|
|
513
|
+
receipt = ObservationDisposition(
|
|
514
|
+
kind,
|
|
515
|
+
identity,
|
|
516
|
+
status,
|
|
517
|
+
"semantic.identity-conflict"
|
|
518
|
+
if conflict
|
|
519
|
+
else "semantic.identical-duplicate"
|
|
520
|
+
if index
|
|
521
|
+
else "",
|
|
522
|
+
context,
|
|
523
|
+
digest,
|
|
524
|
+
)
|
|
525
|
+
key = (kind, identity, context, status, digest if conflict else "")
|
|
526
|
+
if (
|
|
527
|
+
status == "duplicate"
|
|
528
|
+
and index > previous_duplicates.get((kind, identity, context), 0)
|
|
529
|
+
) or (status != "duplicate" and key not in existing):
|
|
530
|
+
dispositions.append(receipt)
|
|
531
|
+
existing.add(key)
|
|
532
|
+
nodes = tuple(accepted["node"])
|
|
533
|
+
node_keys = {(observation_context(item), item.id) for item in nodes}
|
|
534
|
+
edges = []
|
|
535
|
+
for edge in accepted["edge"]:
|
|
536
|
+
context = observation_context(edge)
|
|
537
|
+
if (context, edge.source) not in node_keys or (context, edge.target) not in node_keys:
|
|
538
|
+
dispositions.append(
|
|
539
|
+
ObservationDisposition(
|
|
540
|
+
"edge", edge.id, "rejected", "semantic.missing-endpoint", context
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
diagnostics.append(
|
|
544
|
+
SemanticDiagnostic(
|
|
545
|
+
"semantic.missing-endpoint",
|
|
546
|
+
f"Edge has no accepted endpoint: {edge.id}",
|
|
547
|
+
"warning",
|
|
548
|
+
"semantic-contract",
|
|
549
|
+
subject=edge.id,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
else:
|
|
553
|
+
edges.append(edge)
|
|
554
|
+
return SemanticFacts(
|
|
555
|
+
nodes,
|
|
556
|
+
tuple(edges),
|
|
557
|
+
tuple(diagnostics),
|
|
558
|
+
facts.contexts,
|
|
559
|
+
tuple(dispositions),
|
|
560
|
+
facts.coverage_receipts,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
__all__ = [
|
|
565
|
+
"ANALYSIS_COMPLETENESS",
|
|
566
|
+
"BINDING_KINDS",
|
|
567
|
+
"LANGUAGES",
|
|
568
|
+
"NODE_KINDS",
|
|
569
|
+
"RELATIONS",
|
|
570
|
+
"RESOLUTIONS",
|
|
571
|
+
"Provenance",
|
|
572
|
+
"SemanticDiagnostic",
|
|
573
|
+
"SemanticEdge",
|
|
574
|
+
"SemanticFacts",
|
|
575
|
+
"SemanticNode",
|
|
576
|
+
"SourceAnchor",
|
|
577
|
+
"canonical_json",
|
|
578
|
+
"parse_jsonl",
|
|
579
|
+
"normalize_facts",
|
|
580
|
+
"observation_context",
|
|
581
|
+
"stable_id",
|
|
582
|
+
]
|