parkshinyoung
release: publish article-local kg v2.1
23ade4e
Raw History Blame Contribute Delete
6.31 kB
"""Runtime/legacy graph input adapter and immutable resolved assembly carrier."""
from __future__ import annotations
from dataclasses import dataclass
import json
from types import MappingProxyType
from typing import Any, Mapping
from runtime.eventframe.contracts import ArticleLocalRuntimeResult
from .assembly import ArticleLocalKGAssembler
from .contracts import ArticleLocalKnowledgeGraphResult, GRAPH_SCHEMA_VERSION
def deep_freeze(value: Any) -> Any:
"""Recursively detach and freeze a JSON-compatible value."""
if isinstance(value, Mapping):
return MappingProxyType({str(key): deep_freeze(item) for key, item in value.items()})
if isinstance(value, (list, tuple)):
return tuple(deep_freeze(item) for item in value)
return value
def deep_thaw(value: Any) -> Any:
"""Return a detached JSON-compatible copy of a frozen carrier value."""
if isinstance(value, Mapping):
return {str(key): deep_thaw(item) for key, item in value.items()}
if isinstance(value, tuple):
return [deep_thaw(item) for item in value]
return value
def _json_copy(value: Mapping[str, Any]) -> dict[str, Any]:
return json.loads(json.dumps(value, ensure_ascii=False, allow_nan=False))
@dataclass(frozen=True, slots=True)
class ResolvedAssemblyState:
"""Immutable assembly-only view of already resolved runtime identities."""
source_kind: str
source_graph: Mapping[str, Any]
article: Mapping[str, Any]
resolved_events: tuple[Mapping[str, Any], ...]
resolved_entities: tuple[Mapping[str, Any], ...]
statements: tuple[Mapping[str, Any], ...]
time_expressions: tuple[Mapping[str, Any], ...]
semantic_edges: tuple[Mapping[str, Any], ...]
unmaterialized_evidence: Mapping[str, Any]
source_lanes: Mapping[str, Any]
coverage: Mapping[str, Any]
runtime_provenance: Mapping[str, Any]
@classmethod
def from_legacy_graph(
cls,
graph: Mapping[str, Any],
*,
source_kind: str,
) -> "ResolvedAssemblyState":
detached = _json_copy(graph)
if detached.get("schema_version") != GRAPH_SCHEMA_VERSION:
raise ValueError("resolved assembly state requires the canonical legacy graph")
nodes = detached.get("nodes", [])
edges = detached.get("edges", [])
member_event_ids = {
str(edge["source_id"])
for edge in edges
if edge.get("edge_type") == "MEMBER_OF_EVENT"
}
resolved_events = [node for node in nodes if node.get("kind") == "LOCAL_EVENT"]
resolved_events.extend(
node
for node in nodes
if node.get("kind") == "EVENT" and str(node["node_id"]) not in member_event_ids
)
frozen_graph = deep_freeze(detached)
return cls(
source_kind=source_kind,
source_graph=frozen_graph,
article=deep_freeze(detached["article"]),
resolved_events=tuple(deep_freeze(row) for row in resolved_events),
resolved_entities=tuple(
deep_freeze(node) for node in nodes if node.get("kind") == "LOCAL_ENTITY"
),
statements=tuple(
deep_freeze(node) for node in nodes if node.get("kind") == "STATEMENT"
),
time_expressions=tuple(
deep_freeze(node) for node in nodes if node.get("kind") == "TIME"
),
semantic_edges=tuple(
deep_freeze(edge)
for edge in edges
if edge.get("edge_type") != "MEMBER_OF_EVENT"
),
unmaterialized_evidence=deep_freeze(
detached.get("unmaterialized_evidence", {})
),
source_lanes=deep_freeze(detached.get("source_lanes", {})),
coverage=deep_freeze(detached.get("lane_statuses", {})),
runtime_provenance=deep_freeze(detached.get("provenance", {})),
)
def identity_ids_by_scope(self) -> Mapping[str, tuple[str, ...]]:
return MappingProxyType(
{
"EVENT": tuple(str(row["node_id"]) for row in self.resolved_events),
"ENTITY": tuple(str(row["node_id"]) for row in self.resolved_entities),
"STATEMENT": tuple(str(row["node_id"]) for row in self.statements),
"TIME": tuple(str(row["node_id"]) for row in self.time_expressions),
}
)
def identity_ids(self) -> tuple[str, ...]:
return tuple(
identity
for scope in ("EVENT", "ENTITY", "STATEMENT", "TIME")
for identity in self.identity_ids_by_scope()[scope]
)
def to_legacy_graph(self) -> dict[str, Any]:
return deep_thaw(self.source_graph)
class AssemblyInputAdapter:
"""Adapt only canonical runtime/legacy contracts without model or Gold imports."""
policy_id = "ASSEMBLY_INPUT_ADAPTER_V1"
def __init__(self, legacy_assembler: ArticleLocalKGAssembler) -> None:
self._legacy_assembler = legacy_assembler
def adapt(
self,
source: (
ArticleLocalRuntimeResult
| ArticleLocalKnowledgeGraphResult
| Mapping[str, Any]
),
) -> ResolvedAssemblyState:
if isinstance(source, ArticleLocalRuntimeResult):
graph = self._legacy_assembler.assemble(source).to_dict()
source_kind = "ARTICLE_LOCAL_RUNTIME_RESULT"
elif isinstance(source, ArticleLocalKnowledgeGraphResult):
graph = source.to_dict()
source_kind = "LEGACY_ASSEMBLY_RESULT"
elif isinstance(source, Mapping):
snapshot = _json_copy(source)
if snapshot.get("schema_version") == GRAPH_SCHEMA_VERSION:
graph = snapshot
source_kind = "LEGACY_ASSEMBLY_MAPPING"
else:
graph = self._legacy_assembler.assemble(snapshot).to_dict()
source_kind = "RUNTIME_RESULT_MAPPING"
if snapshot != _json_copy(source):
raise ValueError("assembly input adapter mutated its source mapping")
else:
raise TypeError("unsupported assembly input contract")
return ResolvedAssemblyState.from_legacy_graph(graph, source_kind=source_kind)