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.
Files changed (192) hide show
  1. modus/__init__.py +13 -0
  2. modus/__main__.py +6 -0
  3. modus/_bundled/schemas/module-meta.schema.yaml +86 -0
  4. modus/activation_guidance.py +18 -0
  5. modus/artifacts_io.py +1066 -0
  6. modus/catalog.py +361 -0
  7. modus/cli.py +1785 -0
  8. modus/commands/__init__.py +17 -0
  9. modus/commands/context_cmd.py +50 -0
  10. modus/commands/help.py +131 -0
  11. modus/commands/init_project.py +503 -0
  12. modus/commands/install_state.py +212 -0
  13. modus/commands/maintenance_scope.py +69 -0
  14. modus/commands/permission_errors.py +26 -0
  15. modus/commands/reporting.py +113 -0
  16. modus/commands/self_update.py +577 -0
  17. modus/commands/status.py +52 -0
  18. modus/commands/uninstall.py +359 -0
  19. modus/commands/uninstall_report.py +183 -0
  20. modus/commands/uninstall_self.py +373 -0
  21. modus/commands/update.py +526 -0
  22. modus/commands/update_report.py +162 -0
  23. modus/commands/verify_anchors.py +230 -0
  24. modus/config.py +492 -0
  25. modus/git_hygiene.py +466 -0
  26. modus/hooks/__init__.py +21 -0
  27. modus/hooks/bootstrap.py +232 -0
  28. modus/hooks/context.py +29 -0
  29. modus/hooks/contracts.py +111 -0
  30. modus/hooks/environment.py +86 -0
  31. modus/hooks/protocol.py +156 -0
  32. modus/hooks/runtime.py +136 -0
  33. modus/init_analysis/README.md +72 -0
  34. modus/init_analysis/__init__.py +35 -0
  35. modus/init_analysis/api.py +360 -0
  36. modus/init_analysis/authoring_projection.py +192 -0
  37. modus/init_analysis/build_context.py +1326 -0
  38. modus/init_analysis/bundle.py +1092 -0
  39. modus/init_analysis/capability_evidence.py +585 -0
  40. modus/init_analysis/catalog.py +448 -0
  41. modus/init_analysis/compiler/__init__.py +1 -0
  42. modus/init_analysis/compiler/cache.py +63 -0
  43. modus/init_analysis/compiler/cpp.py +536 -0
  44. modus/init_analysis/compiler/go.py +158 -0
  45. modus/init_analysis/compiler/java.py +112 -0
  46. modus/init_analysis/compiler/merge.py +211 -0
  47. modus/init_analysis/compiler/models.py +88 -0
  48. modus/init_analysis/compiler/process.py +259 -0
  49. modus/init_analysis/compiler/python.py +430 -0
  50. modus/init_analysis/compiler/typescript.py +73 -0
  51. modus/init_analysis/contracts.py +283 -0
  52. modus/init_analysis/engine.py +230 -0
  53. modus/init_analysis/entry_contracts.py +155 -0
  54. modus/init_analysis/evidence_graph.py +133 -0
  55. modus/init_analysis/evidence_records.py +211 -0
  56. modus/init_analysis/extraction_cache.py +309 -0
  57. modus/init_analysis/fingerprint.py +38 -0
  58. modus/init_analysis/framework_facts.py +472 -0
  59. modus/init_analysis/frontend.py +1523 -0
  60. modus/init_analysis/generic.py +77 -0
  61. modus/init_analysis/ids.py +35 -0
  62. modus/init_analysis/input_snapshot.py +675 -0
  63. modus/init_analysis/language-registry.json +550 -0
  64. modus/init_analysis/language_capabilities.py +78 -0
  65. modus/init_analysis/native_binding.py +734 -0
  66. modus/init_analysis/native_parser.py +905 -0
  67. modus/init_analysis/navigation.py +184 -0
  68. modus/init_analysis/pipeline.py +504 -0
  69. modus/init_analysis/projection.py +409 -0
  70. modus/init_analysis/provider_assets/ModusJavaAnalyzer.java +350 -0
  71. modus/init_analysis/provider_assets/modus_go_analyzer.go +367 -0
  72. modus/init_analysis/provider_assets/modus_typescript_analyzer.cjs +163 -0
  73. modus/init_analysis/reference_outcomes.py +318 -0
  74. modus/init_analysis/resources.py +78 -0
  75. modus/init_analysis/scan.py +95 -0
  76. modus/init_analysis/scope_candidates.py +372 -0
  77. modus/init_analysis/search_metadata.py +32 -0
  78. modus/init_analysis/semantic_contracts.py +119 -0
  79. modus/init_analysis/semantic_ir.py +582 -0
  80. modus/init_analysis/semantic_providers.py +188 -0
  81. modus/init_analysis/semantic_reconcile.py +491 -0
  82. modus/init_analysis/source_contracts.py +127 -0
  83. modus/init_analysis/source_coordinates.py +80 -0
  84. modus/init_analysis/source_filters.py +184 -0
  85. modus/init_analysis/source_inventory.py +251 -0
  86. modus/init_analysis/source_roles.py +99 -0
  87. modus/init_analysis/source_snapshot.py +65 -0
  88. modus/init_analysis/structural.py +119 -0
  89. modus/init_analysis/topology.py +2060 -0
  90. modus/init_analysis/value_summary.py +463 -0
  91. modus/init_analysis/view_quarantine.py +606 -0
  92. modus/knowledge_identity.py +200 -0
  93. modus/knowledge_reader.py +220 -0
  94. modus/knowledge_seeds.py +56 -0
  95. modus/knowledge_update.py +212 -0
  96. modus/knowledge_write_session.py +241 -0
  97. modus/manifest.py +210 -0
  98. modus/orchestrator/README.md +67 -0
  99. modus/orchestrator/__init__.py +14 -0
  100. modus/orchestrator/context_resolver.py +142 -0
  101. modus/orchestrator/evidence_search.py +473 -0
  102. modus/orchestrator/fallback.py +83 -0
  103. modus/orchestrator/knowledge_sync.py +349 -0
  104. modus/orchestrator/models.py +82 -0
  105. modus/orchestrator/module_meta.py +194 -0
  106. modus/orchestrator/path_scope.py +38 -0
  107. modus/orchestrator/preflight.py +125 -0
  108. modus/orchestrator/query_terms.py +124 -0
  109. modus/orchestrator/search_policy.py +161 -0
  110. modus/paths.py +279 -0
  111. modus/platforms/__init__.py +45 -0
  112. modus/platforms/base.py +299 -0
  113. modus/platforms/claude.py +98 -0
  114. modus/platforms/codebuddy.py +51 -0
  115. modus/platforms/codex.py +41 -0
  116. modus/platforms/cursor.py +45 -0
  117. modus/platforms/hooks.py +188 -0
  118. modus/platforms/models.py +166 -0
  119. modus/platforms/registry.py +88 -0
  120. modus/platforms/rendering.py +77 -0
  121. modus/platforms/sections.py +100 -0
  122. modus/precedence.py +47 -0
  123. modus/project_activation.py +99 -0
  124. modus/retrieval/README.md +64 -0
  125. modus/retrieval/__init__.py +9 -0
  126. modus/retrieval/budget.py +50 -0
  127. modus/retrieval/delivery.py +226 -0
  128. modus/retrieval/index.py +92 -0
  129. modus/retrieval/pipeline.py +718 -0
  130. modus/retrieval/query.py +189 -0
  131. modus/retrieval/routing.py +237 -0
  132. modus/retrieval/runtime.py +39 -0
  133. modus/retrieval/settings.py +75 -0
  134. modus/retrieval/stages/__init__.py +7 -0
  135. modus/retrieval/stages/discovery.py +91 -0
  136. modus/retrieval/stages/documents.py +694 -0
  137. modus/retrieval/stages/lexical.py +116 -0
  138. modus/retrieval/stages/ranking.py +179 -0
  139. modus/retrieval/stages/sections.py +64 -0
  140. modus/retrieval/stages/selection.py +221 -0
  141. modus/retrieval/stages/settings.py +7 -0
  142. modus/scaffold/README.md +68 -0
  143. modus/scaffold/__init__.py +36 -0
  144. modus/scaffold/catalog.py +170 -0
  145. modus/scaffold/project.py +361 -0
  146. modus/scaffold/templates/README.md +37 -0
  147. modus/scaffold/templates/commands/init.md +19 -0
  148. modus/scaffold/templates/commands/reinit.md +18 -0
  149. modus/scaffold/templates/commands/update-knowledge.md +19 -0
  150. modus/scaffold/templates/knowledge/constraint/global-standard.md +18 -0
  151. modus/scaffold/templates/knowledge/constraint/team-standard.md +28 -0
  152. modus/scaffold/templates/knowledge/handbook/SKILL.md +21 -0
  153. modus/scaffold/templates/skills/modus-init/SKILL.md +52 -0
  154. modus/scaffold/templates/skills/modus-init/references/architecture-overview.md +23 -0
  155. modus/scaffold/templates/skills/modus-init/references/business-skill.md +65 -0
  156. modus/scaffold/templates/skills/modus-init/references/code-skill.md +190 -0
  157. modus/scaffold/templates/skills/modus-init/references/constraint-extraction.md +86 -0
  158. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/cpp.md +39 -0
  159. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/go.md +39 -0
  160. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/java.md +39 -0
  161. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/php.md +39 -0
  162. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/web.md +50 -0
  163. modus/scaffold/templates/skills/modus-init/references/global-standard.md +9 -0
  164. modus/scaffold/templates/skills/modus-init/references/language-adaptation.md +19 -0
  165. modus/scaffold/templates/skills/modus-init/references/scope-discovery.md +86 -0
  166. modus/scaffold/templates/skills/modus-init/references/team-standard.md +52 -0
  167. modus/scaffold/templates/skills/modus-init/references/workspace-init.md +67 -0
  168. modus/scaffold/templates/skills/modus-init/references/zh-style.md +12 -0
  169. modus/scaffold/templates/skills/modus-reinit/SKILL.md +22 -0
  170. modus/scaffold/templates/skills/modus-sync-knowledge/SKILL.md +28 -0
  171. modus/scaffold/templates/skills/modus-update-knowledge/SKILL.md +22 -0
  172. modus/scaffold/templates/skills/using-modus/SKILL.md +31 -0
  173. modus/scaffold/templates/skills/using-modus/references/constraint-usage.md +28 -0
  174. modus/scaffold/templates/skills/using-modus/references/framework-update.md +15 -0
  175. modus/scaffold/templates/skills/using-modus/references/knowledge-update-lifecycle.md +68 -0
  176. modus/scaffold/templates/skills/using-modus/references/router-details.md +42 -0
  177. modus/scaffold/writer.py +101 -0
  178. modus/schema_resources.py +56 -0
  179. modus/scope_models.py +504 -0
  180. modus/scope_resolver.py +109 -0
  181. modus/sensitive_literals.py +64 -0
  182. modus/skillmd.py +144 -0
  183. modus/source_listing.py +82 -0
  184. modus/surface.py +65 -0
  185. modus/text_tree.py +64 -0
  186. modus/versioning.py +61 -0
  187. modus/workspace.py +881 -0
  188. openmodus-0.0.1.dist-info/METADATA +251 -0
  189. openmodus-0.0.1.dist-info/RECORD +192 -0
  190. openmodus-0.0.1.dist-info/WHEEL +4 -0
  191. openmodus-0.0.1.dist-info/entry_points.txt +2 -0
  192. 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
+ ]