refactoring for canon conformity

This commit is contained in:
2026-05-23 14:00:59 +02:00
parent 0193c97094
commit 653411ffb8
16 changed files with 819 additions and 29 deletions

198
railiance_fabric/canon.py Normal file
View File

@@ -0,0 +1,198 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
CANONICAL_NODE_CATEGORIES = (
"source-repository",
"software-system",
"service",
"endpoint",
"deployment",
"runtime-resource",
"datastore",
"flow",
"policy",
"control",
"evidence",
"task",
"consumer-purpose",
"telemetry-signal",
)
CANONICAL_EDGE_TYPES = (
"built_from",
"implements",
"exposes",
"depends_on",
"deploys",
"flows_to",
"governed_by",
"evidenced_by",
"observed_by",
"part_of",
"reads_or_writes",
"creates_task",
)
DISPLAY_ONLY_EDGE_TYPES = (
"collapsed_into",
"declares",
"grouped_with",
"highlight_path",
"near",
"owns_deployment",
"same_color_group",
)
EVIDENCE_STATES = ("observed", "declared", "inferred", "proposed", "gap")
MAPPING_FITS = ("direct", "partial", "conflict", "gap", "unknown")
@dataclass(frozen=True)
class CanonNodeMapping:
category: str
canon_anchor: str
fit: str
notes: str = ""
@dataclass(frozen=True)
class CanonEdgeMapping:
canonical_type: str
canon_anchor: str
fit: str
display_only: bool = False
notes: str = ""
UNKNOWN_NODE_MAPPING = CanonNodeMapping(
category="unknown",
canon_anchor="",
fit="gap",
notes="No canon mapping has been selected for this Fabric node kind yet.",
)
UNKNOWN_EDGE_MAPPING = CanonEdgeMapping(
canonical_type="",
canon_anchor="",
fit="gap",
notes="No canon mapping has been selected for this Fabric edge type yet.",
)
NODE_KIND_CANON_MAP: dict[str, CanonNodeMapping] = {
"ApplicationEndpoint": CanonNodeMapping("endpoint", "model/network", "direct"),
"BindingAssertion": CanonNodeMapping("evidence", "model/observability", "partial"),
"CapabilityDeclaration": CanonNodeMapping("software-system", "model/landscape", "partial"),
"ContainerBuild": CanonNodeMapping("deployment", "model/devsecops", "partial"),
"DependencyDeclaration": CanonNodeMapping("service", "model/landscape", "gap"),
"DeploymentService": CanonNodeMapping("deployment", "model/devsecops", "direct"),
"DomainName": CanonNodeMapping("endpoint", "model/network", "partial"),
"ExternalLibrary": CanonNodeMapping("software-system", "model/landscape", "partial"),
"InterfaceDeclaration": CanonNodeMapping("endpoint", "model/network", "partial"),
"Library": CanonNodeMapping("software-system", "model/landscape", "partial"),
"Lockfile": CanonNodeMapping("evidence", "model/observability", "partial"),
"NetworkPort": CanonNodeMapping("endpoint", "model/network", "direct"),
"Repository": CanonNodeMapping("source-repository", "model/devsecops", "direct"),
"RuntimeService": CanonNodeMapping("runtime-resource", "model/landscape", "direct"),
"ScoreWorkload": CanonNodeMapping("deployment", "model/devsecops", "direct"),
"Server": CanonNodeMapping("runtime-resource", "model/landscape", "partial"),
"ServiceConfig": CanonNodeMapping("evidence", "model/observability", "partial"),
"ServiceDeclaration": CanonNodeMapping("service", "model/landscape", "direct"),
}
EDGE_TYPE_CANON_MAP: dict[str, CanonEdgeMapping] = {
"available_via": CanonEdgeMapping("exposes", "model/network", "partial"),
"binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"cataloged_as": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"consumes": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"declares": CanonEdgeMapping("part_of", "model/devsecops", "partial", display_only=True),
"declares_package": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"defines_deployment": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"defines_runtime_object": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"defines_workload": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"deployed_as": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"depends_on_library": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"documents_interface": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"exposes": CanonEdgeMapping("exposes", "model/network", "direct"),
"exposes_port": CanonEdgeMapping("exposes", "model/network", "direct"),
"listens_on": CanonEdgeMapping("exposes", "model/network", "direct"),
"names_endpoint": CanonEdgeMapping("exposes", "model/network", "partial"),
"opens_port": CanonEdgeMapping("exposes", "model/network", "partial"),
"owns_deployment": CanonEdgeMapping("part_of", "model/devsecops", "partial", display_only=True),
"provides": CanonEdgeMapping("implements", "model/landscape", "partial"),
"resolves_to": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_port": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_service": CanonEdgeMapping("flows_to", "model/network", "partial"),
"runs_on": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"suggests_capability": CanonEdgeMapping("creates_task", "model/task", "partial"),
"uses_config": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"uses_interface": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"uses_lockfile": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
}
def node_canon_mapping(kind: str) -> CanonNodeMapping:
if kind in NODE_KIND_CANON_MAP:
return NODE_KIND_CANON_MAP[kind]
if kind.startswith("Kubernetes"):
return CanonNodeMapping("runtime-resource", "model/landscape", "direct")
return UNKNOWN_NODE_MAPPING
def edge_canon_mapping(edge_type: str) -> CanonEdgeMapping:
normalized = str(edge_type or "").strip()
if normalized.startswith("binds:"):
return EDGE_TYPE_CANON_MAP["binds"]
if normalized in EDGE_TYPE_CANON_MAP:
return EDGE_TYPE_CANON_MAP[normalized]
if normalized in CANONICAL_EDGE_TYPES:
return CanonEdgeMapping(normalized, _anchor_for_canonical_edge(normalized), "direct")
if normalized in DISPLAY_ONLY_EDGE_TYPES:
return CanonEdgeMapping("", "", "gap", display_only=True)
return UNKNOWN_EDGE_MAPPING
def evidence_state_for(
*,
origin: str = "",
source_kind: str = "",
review_state: str = "",
confidence: float | None = None,
) -> str:
if review_state == "rejected":
return "gap"
if origin == "llm":
return "proposed"
if confidence is not None and confidence < 0.5:
return "inferred"
if source_kind in {"package_registry", "container_registry", "service_catalog", "fabric_registry"}:
return "observed"
if source_kind in {"llm"}:
return "proposed"
if not source_kind and origin == "deterministic":
return "inferred"
return "declared"
def source_kind_from_anchor(source_anchor: dict[str, Any]) -> str:
return str(source_anchor.get("source_kind") or "")
def _anchor_for_canonical_edge(edge_type: str) -> str:
return {
"built_from": "model/devsecops",
"implements": "model/security",
"exposes": "model/network",
"depends_on": "model/landscape",
"deploys": "model/devsecops",
"flows_to": "model/network",
"governed_by": "model/governance",
"evidenced_by": "model/observability",
"observed_by": "model/observability",
"part_of": "model/landscape",
"reads_or_writes": "model/data",
"creates_task": "model/task",
}.get(edge_type, "")

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from typing import Any
from .loader import load_declarations
from .canon import edge_canon_mapping, node_canon_mapping
from .model import Declaration
@@ -186,6 +187,7 @@ class FabricGraph:
edges: list[dict[str, str]] = []
for declaration in sorted(self.declarations, key=lambda item: (item.kind, item.id)):
canon_mapping = node_canon_mapping(declaration.kind)
nodes.append(
{
"id": declaration.id,
@@ -194,35 +196,39 @@ class FabricGraph:
"repo": declaration.metadata.get("repo", ""),
"domain": declaration.metadata.get("domain", ""),
"lifecycle": declaration.spec.get("lifecycle", ""),
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": "declared",
"attributes": _export_attributes(declaration),
}
)
for service in self.services.values():
for capability_id in service.spec.get("provides_capabilities", []):
edges.append({"from": service.id, "to": capability_id, "type": "provides"})
edges.append(_export_edge(service.id, capability_id, "provides"))
for interface_id in service.spec.get("exposes_interfaces", []):
edges.append({"from": service.id, "to": interface_id, "type": "exposes"})
edges.append(_export_edge(service.id, interface_id, "exposes"))
for capability in self.capabilities.values():
for interface_id in capability.spec.get("interface_ids", []):
edges.append({"from": capability.id, "to": interface_id, "type": "available_via"})
edges.append(_export_edge(capability.id, interface_id, "available_via"))
for dependency in self.dependencies.values():
consumer = str(dependency.spec.get("consumer_service_id", ""))
if consumer:
edges.append({"from": consumer, "to": dependency.id, "type": "consumes"})
edges.append(_export_edge(consumer, dependency.id, "consumes"))
for binding in self.bindings_by_dependency.get(dependency.id, []):
edges.append(
{
"from": dependency.id,
"to": str(binding.spec.get("provider_capability_id", "")),
"type": f"binds:{binding.spec.get('status', '')}",
}
_export_edge(
dependency.id,
str(binding.spec.get("provider_capability_id", "")),
f"binds:{binding.spec.get('status', '')}",
)
)
interface_id = str(binding.spec.get("provider_interface_id", ""))
if interface_id:
edges.append({"from": dependency.id, "to": interface_id, "type": "uses_interface"})
edges.append(_export_edge(dependency.id, interface_id, "uses_interface"))
return {
"apiVersion": "railiance.fabric/v1alpha1",
@@ -265,6 +271,20 @@ def _escape_mermaid(value: str) -> str:
return value.replace('"', '\\"')
def _export_edge(source: str, target: str, edge_type: str) -> dict[str, Any]:
canon_mapping = edge_canon_mapping(edge_type)
return {
"from": source,
"to": target,
"type": edge_type,
"canonical_type": canon_mapping.canonical_type,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"display_only": canon_mapping.display_only,
"evidence_state": "declared",
}
def _export_attributes(declaration: Declaration) -> dict[str, Any]:
spec = declaration.spec
base = _base_export_attributes(declaration)

View File

@@ -6,6 +6,8 @@ from re import sub
from typing import Any
from urllib.parse import urlparse
from .canon import edge_canon_mapping
DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
LAYER_ORDER = (
@@ -57,8 +59,18 @@ _LAYER_COLORS = {
}
_EDGE_STRENGTH = {
"provides": "strong",
"built_from": "medium",
"depends_on": "medium",
"deploys": "strong",
"evidenced_by": "medium",
"exposes": "strong",
"flows_to": "medium",
"governed_by": "medium",
"implements": "medium",
"observed_by": "medium",
"part_of": "weak",
"reads_or_writes": "medium",
"provides": "strong",
"available_via": "medium",
"consumes": "medium",
"uses_interface": "medium",
@@ -139,6 +151,9 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"kind",
"layer",
"edgeType",
"canonicalType",
"canonCategory",
"evidenceState",
],
"filter": {
"actions": list(DISPLAY_STATES),
@@ -153,6 +168,10 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
{"id": "reviewState", "label": "Review State", "type": "string"},
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
{"id": "edgeType", "label": "Edge Type", "type": "string"},
{"id": "canonicalType", "label": "Canonical Edge", "type": "string"},
{"id": "canonCategory", "label": "Canon Category", "type": "string"},
{"id": "evidenceState", "label": "Evidence State", "type": "string"},
{"id": "displayOnly", "label": "Display Only", "type": "boolean"},
{"id": "strength", "label": "Strength", "type": "string"},
{"id": "sameLayer", "label": "Same Layer", "type": "boolean"},
{"id": "text", "label": "Text", "type": "string"},
@@ -174,6 +193,8 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"repo",
"domain",
"lifecycle",
"canonCategory",
"evidenceState",
"unresolved",
"description",
"sourceReferences",
@@ -181,6 +202,9 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
],
"edge_fields": [
"edgeType",
"canonicalType",
"displayOnly",
"evidenceState",
"strength",
"source",
"target",
@@ -329,6 +353,14 @@ def fabric_graph_explorer_payload(
"repo": str(node.get("repo", "")),
"domain": str(node.get("domain", "")),
"lifecycle": str(node.get("lifecycle", "")),
"canonCategory": str(
node.get("canon_category") or attributes.get("canon_category") or ""
),
"canonAnchor": str(node.get("canon_anchor") or attributes.get("canon_anchor") or ""),
"mappingFit": str(node.get("mapping_fit") or attributes.get("mapping_fit") or ""),
"evidenceState": str(
node.get("evidence_state") or attributes.get("evidence_state") or ""
),
"reviewState": review_state,
"freshnessState": "current",
"unresolved": is_unresolved,
@@ -378,7 +410,17 @@ def fabric_graph_explorer_payload(
edge_type = _presentation_edge_type(str(edge.get("type", "")), source, target, node_kinds)
if not source or not target:
continue
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
elements.append(
_edge_element(
edge_index,
source,
target,
edge_type,
node_layers,
node_repos,
**_edge_metadata(edge, edge_type),
)
)
edge_index += 1
for slug in sorted(repo_slugs):
@@ -389,7 +431,17 @@ def fabric_graph_explorer_payload(
continue
if str(node.get("repo", "")) != slug or not node_id:
continue
elements.append(_edge_element(edge_index, repo_id, node_id, "declares", node_layers, node_repos))
elements.append(
_edge_element(
edge_index,
repo_id,
node_id,
"declares",
node_layers,
node_repos,
display_only=True,
)
)
edge_index += 1
visible_nodes = [element for element in elements if "source" not in element["data"]]
@@ -468,6 +520,17 @@ def _presentation_edge_type(edge_type: str, source: str, target: str, node_kinds
return edge_type
def _edge_metadata(edge: dict[str, Any], edge_type: str) -> dict[str, Any]:
canon_mapping = edge_canon_mapping(edge_type)
return {
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
"evidence_state": str(edge.get("evidence_state") or "declared"),
}
def _edge_strength(edge_type: str) -> str:
if edge_type.startswith("binds:"):
status = edge_type.split(":", 1)[1]
@@ -526,7 +589,7 @@ def _append_infrastructure_elements(
server_ids_by_host, port_ids_by_endpoint = _runtime_node_indexes(source_nodes)
generated_edge_keys: set[tuple[str, str, str]] = set()
def append_edge(source: str, target: str, edge_type: str) -> None:
def append_edge(source: str, target: str, edge_type: str, *, display_only: bool = True) -> None:
nonlocal edge_index
if not source or not target:
return
@@ -534,7 +597,17 @@ def _append_infrastructure_elements(
if key in generated_edge_keys:
return
generated_edge_keys.add(key)
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
elements.append(
_edge_element(
edge_index,
source,
target,
edge_type,
node_layers,
node_repos,
display_only=display_only,
)
)
edge_index += 1
service_nodes = sorted(
@@ -816,12 +889,24 @@ def _edge_element(
edge_type: str,
node_layers: dict[str, str],
node_repos: dict[str, str],
*,
canonical_type: str = "",
canon_anchor: str = "",
mapping_fit: str = "",
display_only: bool = False,
evidence_state: str = "",
) -> dict[str, Any]:
source_layer = node_layers.get(source, "unknown")
target_layer = node_layers.get(target, "unknown")
source_repo = node_repos.get(source, "")
target_repo = node_repos.get(target, "")
same_repo = bool(source_repo and source_repo == target_repo)
canon_mapping = edge_canon_mapping(edge_type)
canonical_type = canonical_type or canon_mapping.canonical_type
canon_anchor = canon_anchor or canon_mapping.canon_anchor
mapping_fit = mapping_fit or canon_mapping.fit
display_only = display_only or canon_mapping.display_only
evidence_state = evidence_state or "declared"
strength = _edge_strength(edge_type)
layout = _layout_hints(edge_type, source_layer, target_layer, same_repo)
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
@@ -840,6 +925,11 @@ def _edge_element(
"targetRepo": target_repo,
"edgeType": edge_type,
"dependencyType": edge_type,
"canonicalType": canonical_type,
"canonAnchor": canon_anchor,
"mappingFit": mapping_fit,
"displayOnly": display_only,
"evidenceState": evidence_state,
"strength": strength,
"edgeWidth": _edge_width(strength),
"sameLayer": source_layer == target_layer,
@@ -860,6 +950,7 @@ def _edge_element(
edge_type.replace(":", "-"),
strength,
str(layout["affinity"]),
"display-only" if display_only else "canonical",
"same-layer" if source_layer == target_layer else "",
"same-repo" if same_repo else "",
)

View File

@@ -1226,7 +1226,7 @@ def graph_explorer_page() -> str:
detailTitle.textContent = data.name || data.label || data.id;
detailSummary.textContent = data.description || data.id;
const nodeType = data.layer ? nodeTypeLabels[data.layer] || humanize(data.layer) : "";
detailPills.innerHTML = [data.kind, nodeType, data.repo, data.reviewState, data.displayState]
detailPills.innerHTML = [data.kind, nodeType, data.canonCategory || data.canonicalType, data.evidenceState, data.repo, data.reviewState, data.displayState]
.map((value) => value ? `<span class="pill">${escapeHtml(value)}</span>` : "")
.join("");
const links = data.deepLinks || {};
@@ -1236,6 +1236,10 @@ def graph_explorer_page() -> str:
["source", data.source],
["target", data.target],
["edge", data.edgeType],
["canonical", data.canonicalType || data.canonCategory],
["evidence", data.evidenceState],
["mapping", data.mappingFit],
["display only", data.displayOnly === true ? "yes" : ""],
["strength", data.strength],
...Object.entries(links),
...refs.map((ref) => [ref.label || ref.kind || "source", ref.path || ref.url || ref.ref || ""])

View File

@@ -9,6 +9,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .canon import edge_canon_mapping, node_canon_mapping
from .loader import repo_root
from .schema_validation import draft202012_validator
@@ -416,13 +417,7 @@ class RegistryStore:
nodes[str(node.get("id", ""))] = node
for edge in graph.get("edges", []):
if isinstance(edge, dict):
edges.append(
{
"from": str(edge.get("from", "")),
"to": str(edge.get("to", "")),
"type": str(edge.get("type", "")),
}
)
edges.append(_edge_with_canon_metadata(edge))
return {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricGraphExport",
@@ -1485,7 +1480,18 @@ def _project_discovery_snapshot(
target = key_to_graph_id.get(str(candidate.get("target_key") or ""))
if not source or not target:
continue
edge = {"from": source, "to": target, "type": str(candidate.get("edge_type") or "")}
edge = _edge_with_canon_metadata(
{
"from": source,
"to": target,
"type": str(candidate.get("edge_type") or ""),
"canonical_type": candidate.get("canonical_type", ""),
"canon_anchor": candidate.get("canon_anchor", ""),
"mapping_fit": candidate.get("mapping_fit", ""),
"display_only": candidate.get("display_only", False),
"evidence_state": candidate.get("evidence_state", ""),
}
)
edge_key = _edge_key(edge)
if edge["type"] and edge_key not in existing_edges:
existing_edges.add(edge_key)
@@ -1496,6 +1502,7 @@ def _project_discovery_snapshot(
def _project_candidate_node(candidate: dict[str, Any], graph_id: str) -> dict[str, Any]:
attributes = candidate.get("attributes") if isinstance(candidate.get("attributes"), dict) else {}
canon_mapping = node_canon_mapping(str(candidate.get("kind") or "DiscoveredEntity"))
return {
"id": graph_id,
"kind": str(candidate.get("kind") or "DiscoveredEntity"),
@@ -1503,6 +1510,10 @@ def _project_candidate_node(candidate: dict[str, Any], graph_id: str) -> dict[st
"repo": str(candidate.get("repo") or ""),
"domain": str(candidate.get("domain") or ""),
"lifecycle": str(candidate.get("lifecycle") or "active"),
"canon_category": str(candidate.get("canon_category") or canon_mapping.category),
"canon_anchor": str(candidate.get("canon_anchor") or canon_mapping.canon_anchor),
"mapping_fit": str(candidate.get("mapping_fit") or canon_mapping.fit),
"evidence_state": str(candidate.get("evidence_state") or "declared"),
"attributes": {
**attributes,
"discovery_stable_key": candidate.get("stable_key"),
@@ -1604,6 +1615,22 @@ def _edge_key(edge: dict[str, Any]) -> tuple[str, str, str]:
return (str(edge.get("from", "")), str(edge.get("to", "")), str(edge.get("type", "")))
def _edge_with_canon_metadata(edge: dict[str, Any]) -> dict[str, Any]:
edge_type = str(edge.get("type") or "")
canon_mapping = edge_canon_mapping(edge_type)
return {
"from": str(edge.get("from", "")),
"to": str(edge.get("to", "")),
"type": edge_type,
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
"evidence_state": str(edge.get("evidence_state") or "declared"),
"attributes": edge.get("attributes", {}) if isinstance(edge.get("attributes"), dict) else {},
}
def _stable_json(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"))

View File

@@ -13,6 +13,7 @@ from urllib.parse import urlparse
import yaml
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping, source_kind_from_anchor
from .connectors import ConnectorConfig, apply_connectors
from .discovery import (
attribute_stable_key,
@@ -158,12 +159,24 @@ class CandidateAccumulator:
attributes: dict[str, object] | None = None,
lifecycle: str | None = None,
domain: str | None = None,
evidence_state: str | None = None,
) -> dict[str, object]:
canon_mapping = node_canon_mapping(kind)
candidate: dict[str, object] = {
"stable_key": stable_key,
"kind": kind,
"label": label,
"repo": self.repo_slug,
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": evidence_state
or evidence_state_for(
origin=origin,
source_kind=source_kind_from_anchor(source_anchor),
review_state=review_state,
confidence=confidence,
),
"origin": origin,
"review_state": review_state,
"status": status,
@@ -203,6 +216,8 @@ class CandidateAccumulator:
confidence: float = 0.8,
aliases: Iterable[str] = (),
attributes: dict[str, object] | None = None,
display_only: bool | None = None,
evidence_state: str | None = None,
) -> dict[str, object]:
stable_key = relationship_stable_key(
source_key,
@@ -210,9 +225,21 @@ class CandidateAccumulator:
target_key,
evidence_scope=replacement_scope,
)
canon_mapping = edge_canon_mapping(edge_type)
candidate: dict[str, object] = {
"stable_key": stable_key,
"edge_type": edge_type,
"canonical_type": canon_mapping.canonical_type,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"display_only": canon_mapping.display_only if display_only is None else display_only,
"evidence_state": evidence_state
or evidence_state_for(
origin=origin,
source_kind=source_kind_from_anchor(source_anchor),
review_state=review_state,
confidence=confidence,
),
"source_key": source_key,
"target_key": target_key,
"origin": origin,