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

View File

@@ -0,0 +1,140 @@
# Canon Alignment Review For RAIL-FAB-WP-0016
Date: 2026-05-23
## Review Packet
This review follows the InfoTechCanon consumer alignment workflow for
`railiance-fabric`.
Reviewed Fabric sources:
- `INTENT.md`, `SCOPE.md`, and `README.md`
- `railiance_fabric/graph.py`, `scanner.py`, `registry.py`, and `graph_explorer.py`
- `schemas/discovery-snapshot.schema.yaml` and `schemas/state-hub-export.schema.yaml`
- workplans `RAIL-FAB-WP-0010` through `RAIL-FAB-WP-0016`
Reviewed canon sources:
- `infospace/agent/review-kit/review-kit.yaml`
- `infospace/agent/review-kit/review-workflow.yaml`
- `infospace/evaluations/railiance-fabric/conformance-pack.yaml`
- `infospace/evaluations/railiance-fabric/entity-edge-capture-criteria.yaml`
- `infospace/evaluations/railiance-fabric/mapping-expectations.yaml`
- `infospace/evaluations/railiance-fabric/visualization-examples.yaml`
The workplan's `review-kit/alignment` reference is the canon artifact id; the
physical files are under `infospace/agent/review-kit/`.
## Repository Context
Producer intent: Railiance Fabric owns repo-authored graph declarations,
scanner output, registry projection, validation, query tooling, and State Hub
export contracts for the Railiance ecosystem graph.
Current scope: source-controlled declarations model services, capabilities,
interfaces, dependencies, and binding assertions. Deterministic scanning adds
candidate repositories, libraries, manifests, runtime endpoints, domains,
ports, and source evidence. The graph explorer adds visual projection edges and
inferred runtime views for usability.
Consumer purposes:
- make cross-repo dependencies reviewable,
- make provider/consumer and blast-radius queries reliable,
- provide State Hub with a read model rather than an authoring surface,
- support reingest after a canon-aligned model reset,
- prevent visualization metadata from becoming graph truth.
## Selected Canon Surfaces
| Surface | Why selected |
| --- | --- |
| `model/landscape` | Services, software systems, runtime resources, environments, ownership, and dependency claims. |
| `model/devsecops` | Source repositories, packages, lockfiles, pipelines, artifacts, deployments, and attestations. |
| `model/network` | Endpoints, ports, DNS, routes, reachability, and future flow nodes. |
| `model/data` | Datastore and reads/writes relationships are expected gaps in current Fabric capture. |
| `model/observability` | Evidence, telemetry signals, scanner provenance, and validation support. |
| `model/governance` | Policies, reviews, decisions, exceptions, and reset guardrails. |
| `model/security` | Controls and findings that should not be flattened into generic dependency edges. |
| `model/task` | Review and remediation work created from graph gaps. |
| `model/purpose-demand-extension` | Consumer purpose, scope pressure, and canon evolution feedback. |
| `standard/tagging` | Non-relationship classification without using display edges as semantics. |
## Target Node Taxonomy
| Canon category | Current Fabric source | Fit | Target action |
| --- | --- | --- | --- |
| source-repository | `Repository` candidates and registered repos | direct | Keep as source of truth for repo-owned declarations and scans. |
| service | `ServiceDeclaration` | direct | Keep as logical/deployable service boundary. |
| software-system | `CapabilityDeclaration`, `Library`, `ExternalLibrary` | partial | Keep as transitional mapping; later split capability semantics from system/component boundaries. |
| endpoint | `InterfaceDeclaration`, `ApplicationEndpoint`, `NetworkPort`, `DomainName` | partial/direct | Prefer explicit endpoint nodes for network reachability; preserve interface contract attributes. |
| deployment | `DeploymentService`, `ScoreWorkload`, `ContainerBuild` | partial/direct | Add deployment nodes for actual release/deploy evidence before destructive reset. |
| runtime-resource | `RuntimeService`, `Server`, `Kubernetes*` candidates | partial/direct | Capture workload, service DNS, namespace, VM/server, and cluster objects as runtime reality. |
| datastore | none first-class | gap | Add explicit datastore extraction from declarations, manifests, and config before reingest acceptance. |
| flow | route/resolve/port edges only | gap | Add first-class Flow nodes when observed traffic or declared communication needs protocol/evidence context. |
| policy | none first-class | gap | Add policy nodes for governing declarations, reset gates, and validation controls. |
| control | none first-class | gap | Add control nodes only when preventive/detective/corrective controls have evidence. |
| evidence | `BindingAssertion`, `Lockfile`, `ServiceConfig`, contracts, source anchors | partial | Make evidence explicit instead of hiding it only in attributes. |
| task | workplans and review gaps | gap | Capture review/remediation tasks after State Hub readiness work is agreed. |
| consumer-purpose | interface card and purpose-fit review | gap | Keep in docs now; model as graph nodes only if State Hub needs purpose-driven filtering. |
| telemetry-signal | none first-class | gap | Add metrics/logs/alerts/dashboards as observed signals when connectors exist. |
## Target Edge Taxonomy
| Current Fabric edge | Canon relationship | Fit | Notes |
| --- | --- | --- | --- |
| `exposes`, `exposes_port`, `listens_on` | `exposes` | direct/partial | Good first canonical family for service/endpoint reachability. |
| `consumes`, `depends_on_library`, `binds:*`, `uses_interface` | `depends_on` | partial | Preserve helper nodes until a reingest can project direct service relationships safely. |
| `provides` | `implements` | partial | Service-to-capability is a Fabric helper relation; needs stronger canon treatment or collapse. |
| `available_via`, `names_endpoint` | `exposes` | partial | Useful transitional mappings from capability/interface/domain to endpoint. |
| `defines_deployment`, `builds_container`, `declares_package` | `built_from` | partial | Direction and artifact semantics need cleanup before being canonical claims. |
| `defines_runtime_object`, `defines_workload`, `runs_on`, `deployed_as` | `deploys` | partial | Current scanner/UI edges mix source definitions and deployment reality. |
| `routes_to_port`, `routes_to_service`, `resolves_to` | `flows_to` | partial | These are reachability hints, not logical dependencies. |
| `uses_lockfile`, `uses_config`, `documents_interface`, `cataloged_as` | `evidenced_by` | partial | Evidence should become first-class where it supports a specific claim. |
| `declares`, `owns_deployment`, canon display examples | display-only | direct | These are view/cluster edges and must not be conformance claims. |
## Mapping Findings
Direct mappings are strong for repositories, services, runtime resources,
application endpoints, network ports, and explicit `exposes` relationships.
Partial mappings are expected for capability, interface, dependency, binding,
package, manifest, and route concepts. They should keep legacy names during the
transition but carry `canon_category`, `canonical_type`, `mapping_fit`, and
`evidence_state` metadata.
Conflicts: network flow must not be treated as logical dependency, and
graph-explorer layout edges must not be treated as canonical ownership,
dependency, reachability, policy, or evidence.
Gaps: datastores, flows, policy, control, telemetry signals, tasks, and
consumer-purpose nodes are not first-class scanner outputs yet. These should be
implemented as explicit gaps or candidate mappings, not forced into nearby
legacy Fabric semantics.
## Execution Direction
The first implementation slice adds canon metadata beside existing node and
edge names. That keeps registry and graph explorer behavior stable while making
the renewed model inspectable and testable.
The destructive reset phase remains blocked until the following exist:
- export and archive command for current registry graph data,
- reset command that requires an explicit operator flag,
- rollback note that explains restore limits,
- validation that the new scanner/projection can reingest registered repos,
- before/after counts and sample graph review.
No destructive reset was executed during this T01/T02 start.
## Canon Feedback
- Capability and dependency helper nodes are useful Fabric authoring concepts
but do not map cleanly to canonical graph entity categories.
- Edge direction needs explicit treatment for source repo to package/deployment
evidence, because canon `built_from` points from deployment/artifact context
back to source.
- Evidence state should remain independent from review state: accepted inferred
claims and candidate declared claims are different situations.

View File

@@ -0,0 +1,100 @@
schema: info-tech-canon.interface-card.v1
id: railiance-fabric/interface-card
title: Railiance Fabric Canon Interface Card
consumer:
repo: railiance-fabric
domain: railiance
owner: codex
intent: Make the Railiance ecosystem understandable, discoverable, and evolvable through repo-owned graph declarations and discovery output.
scope: Shared schemas, validation, graph construction, registry projections, scanner output, and State Hub export contracts for repository, service, capability, interface, dependency, binding, and runtime discovery data.
purposes:
- id: purpose/graph-refactor
use_case: Canon-aligned graph model reset and reingest.
consumer_need: Separate canonical graph semantics from registry, scanner, and visualization convenience edges before broad adoption.
demand_signals:
- RAIL-FAB-WP-0016
- Registry graph explorer needs clearer canonical/display boundary.
- Scanner output needs evidence state and canon mapping metadata.
canon_surfaces:
implemented_profiles: []
consumed_artifacts:
- model/landscape
- model/network
- model/data
- model/devsecops
- model/observability
- model/governance
- model/security
- model/task
- model/purpose-demand-extension
- standard/tagging
- conformance/railiance-fabric
owned_concepts:
- FabricGraphExport
- FabricDiscoverySnapshot
- GraphExplorerPayload
- RegistryOnboardingManifest
produced_concepts:
- FabricEntity
- FabricEdge
- CaptureSource
- DisplayEdge
- CanonicalEdgeCandidate
- VisualizationView
consumed_concepts:
- Repository
- Service
- SoftwareSystem
- RuntimeResource
- Endpoint
- Deployment
- Flow
- Evidence
- Task
mappings:
- source: Repository
target: source-repository
fit: direct
- source: ServiceDeclaration
target: service
fit: direct
- source: InterfaceDeclaration
target: endpoint
fit: partial
- source: CapabilityDeclaration
target: software-system
fit: partial
- source: DependencyDeclaration
target: depends_on edge
fit: gap
validation_expectations:
commands:
- python3 -m pytest
evidence_required:
- Nodes carry canon_category, canon_anchor, mapping_fit, and evidence_state.
- Edges carry canonical_type, display_only, mapping_fit, and evidence_state.
- Display-only edges do not act as conformance claims.
- Reset commands are guarded and documented before graph data is dropped.
known_gaps:
- Datastore, flow, policy, control, telemetry-signal, task, and consumer-purpose are not first-class scanner outputs yet.
- Legacy declaration nodes still preserve capability/dependency/binding helper nodes until reingest rules collapse or replace them safely.
purpose_fit:
state: partial-fit
matched_capabilities:
- entity and edge capture criteria
- mapping expectations
- visualization boundary examples
scope_pressure: Fabric needs a stable edge vocabulary and evidence-state vocabulary for mixed declared, observed, inferred, and proposed graph claims.
recommended_disposition: Continue consumer-side refactor; record canon gaps as explicit feedback rather than silently extending Fabric semantics.
consumer_needs:
current:
- Canon-aligned graph metadata in scanner, registry, export, and graph explorer payloads.
- Safe destructive reset guardrails before dropping previous graph snapshots.
- Reingest validation across registered and local repositories.
requested_extensions:
- Stable relationship vocabulary for graph capture.
- Evidence-state vocabulary for captured edges.
- Visualization boundary guidance for display-only edges.
feedback:
- CapabilityDeclaration and DependencyDeclaration are useful Fabric authoring helpers but do not map cleanly to canon node categories.
- Network flow and logical dependency must remain separate even when both are inferred from the same source artifact.

View File

@@ -90,6 +90,11 @@ The JSON export has two top-level arrays:
- `edges`: graph relationships such as `provides`, `exposes`,
`available_via`, `consumes`, `binds:<status>`, and `uses_interface`
Canon-aligned exports also carry mapping metadata beside the existing Fabric
terms: nodes include `canon_category`, `canon_anchor`, `mapping_fit`, and
`evidence_state`; edges include `canonical_type`, `display_only`,
`mapping_fit`, and `evidence_state`.
The graph explorer payload wraps those nodes and edges as Cytoscape-compatible
elements with stable keys, layers, display state, visual facets, source
references, and deep links. The registry service exposes the same projection at

View File

@@ -58,6 +58,10 @@ Node fields:
| `repo` | Owning repo slug. |
| `domain` | Owning domain slug. |
| `lifecycle` | Declaration lifecycle. |
| `canon_category` | Canon-aligned entity category when known. |
| `canon_anchor` | Canon surface that owns the selected category. |
| `mapping_fit` | Mapping confidence bucket: `direct`, `partial`, `conflict`, `gap`, or `unknown`. |
| `evidence_state` | Evidence state for the node claim: `observed`, `declared`, `inferred`, `proposed`, or `gap`. |
Edge fields:
@@ -65,7 +69,10 @@ Edge fields:
|-------|---------|
| `from` | Source node id. |
| `to` | Target node id. |
| `type` | Relationship type, such as `provides`, `exposes`, `available_via`, `consumes`, `binds:exact`, or `uses_interface`. |
| `type` | Fabric relationship type, such as `provides`, `exposes`, `available_via`, `consumes`, `binds:exact`, or `uses_interface`. |
| `canonical_type` | Canon-aligned relationship family when known, such as `exposes`, `depends_on`, `deploys`, or `flows_to`. |
| `display_only` | `true` when the edge is a visualization/layout relationship rather than a canonical graph claim. |
| `evidence_state` | Evidence state for the claim: `observed`, `declared`, `inferred`, `proposed`, or `gap`. |
## Proposed State Hub Read Model

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,

View File

@@ -185,6 +185,24 @@ $defs:
- needs_review
- rejected
mappingFit:
type: string
enum:
- direct
- partial
- conflict
- gap
- unknown
evidenceState:
type: string
enum:
- observed
- declared
- inferred
- proposed
- gap
entityStatus:
type: string
enum:
@@ -348,6 +366,15 @@ $defs:
kind:
type: string
minLength: 1
canon_category:
type: string
minLength: 1
canon_anchor:
type: string
mapping_fit:
$ref: "#/$defs/mappingFit"
evidence_state:
$ref: "#/$defs/evidenceState"
label:
type: string
minLength: 1
@@ -410,6 +437,16 @@ $defs:
edge_type:
type: string
minLength: 1
canonical_type:
type: string
canon_anchor:
type: string
mapping_fit:
$ref: "#/$defs/mappingFit"
display_only:
type: boolean
evidence_state:
$ref: "#/$defs/evidenceState"
source_key:
$ref: "#/$defs/stableKey"
target_key:

View File

@@ -53,6 +53,26 @@ properties:
type: string
lifecycle:
type: string
canon_category:
type: string
canon_anchor:
type: string
mapping_fit:
type: string
enum:
- direct
- partial
- conflict
- gap
- unknown
evidence_state:
type: string
enum:
- observed
- declared
- inferred
- proposed
- gap
attributes:
type: object
additionalProperties: true
@@ -72,3 +92,28 @@ properties:
$ref: "./common.schema.yaml#/$defs/graphId"
type:
type: string
canonical_type:
type: string
canon_anchor:
type: string
mapping_fit:
type: string
enum:
- direct
- partial
- conflict
- gap
- unknown
display_only:
type: boolean
evidence_state:
type: string
enum:
- observed
- declared
- inferred
- proposed
- gap
attributes:
type: object
additionalProperties: true

68
tests/test_canon.py Normal file
View File

@@ -0,0 +1,68 @@
from railiance_fabric.canon import (
CANONICAL_EDGE_TYPES,
CANONICAL_NODE_CATEGORIES,
DISPLAY_ONLY_EDGE_TYPES,
edge_canon_mapping,
evidence_state_for,
node_canon_mapping,
)
def test_wp0016_target_taxonomy_is_registered() -> None:
assert set(CANONICAL_NODE_CATEGORIES) >= {
"source-repository",
"software-system",
"service",
"endpoint",
"deployment",
"runtime-resource",
"datastore",
"flow",
"policy",
"control",
"evidence",
"task",
"consumer-purpose",
"telemetry-signal",
}
assert set(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",
}
assert set(DISPLAY_ONLY_EDGE_TYPES) >= {
"collapsed_into",
"grouped_with",
"highlight_path",
"near",
"same_color_group",
}
def test_legacy_fabric_terms_map_without_becoming_display_edges() -> None:
assert node_canon_mapping("Repository").category == "source-repository"
assert node_canon_mapping("KubernetesDeployment").category == "runtime-resource"
assert node_canon_mapping("DependencyDeclaration").fit == "gap"
assert edge_canon_mapping("exposes").canonical_type == "exposes"
assert edge_canon_mapping("provides").canonical_type == "implements"
assert edge_canon_mapping("binds:exact").canonical_type == "depends_on"
assert edge_canon_mapping("resolves_to").canonical_type == "flows_to"
assert edge_canon_mapping("declares").display_only is True
def test_evidence_state_separates_origin_from_review_state() -> None:
assert evidence_state_for(origin="repo_declaration", source_kind="declaration") == "declared"
assert evidence_state_for(origin="llm", source_kind="llm") == "proposed"
assert evidence_state_for(origin="deterministic", source_kind="", confidence=0.4) == "inferred"
assert evidence_state_for(origin="deterministic", source_kind="package_registry") == "observed"
assert evidence_state_for(review_state="rejected") == "gap"

View File

@@ -64,6 +64,7 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
network_port = next(element for element in nodes if element["data"]["kind"] == "NetworkPort")
same_repo_edge = next(edge for edge in edges if edge["data"].get("sameRepo") is True)
cross_repo_edge = next(edge for edge in edges if edge["data"].get("layoutAffinity") == "cross-repo")
declares_edge = next(edge for edge in edges if edge["data"]["edgeType"] == "declares")
assert registered_only["data"]["reviewState"] == "candidate"
assert registered_only["data"]["unresolved"] is True
@@ -86,8 +87,10 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
)
assert runs_on["data"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
assert runs_on["data"]["layoutElasticity"] > cross_repo_edge["data"]["layoutElasticity"]
assert runs_on["data"]["displayOnly"] is True
assert runs_on["data"]["canonicalType"] == "deploys"
assert same_repo_edge["data"]["layoutIdealLength"] < cross_repo_edge["data"]["layoutIdealLength"]
assert any(edge["data"]["edgeType"] == "declares" for edge in edges)
assert declares_edge["data"]["displayOnly"] is True
assert any(node["data"]["sourceReferences"] for node in nodes if node["data"]["kind"] != "Repository")
assert payload["metrics"]["deployment_node_count"] >= 1
assert payload["metrics"]["server_node_count"] >= 1
@@ -145,6 +148,8 @@ def test_graph_explorer_collapses_discovered_repository_nodes() -> None:
assert [node["data"]["id"] for node in repository_nodes] == ["repo:fixture-repo"]
assert declares_package["data"]["source"] == "repo:fixture-repo"
assert declares_package["data"]["target"] == "discovery:fixture-repo:library:fixture-service"
assert declares_package["data"]["canonicalType"] == "built_from"
assert declares_package["data"]["displayOnly"] is False
def test_graph_explorer_presents_legacy_server_nodes_as_runtime_entities() -> None:

View File

@@ -30,7 +30,10 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
candidates = snapshot["candidates"]
nodes_by_label = {(node["kind"], node["label"]): node for node in candidates["nodes"]}
assert nodes_by_label[("Repository", "Fixture Repo")]["review_state"] == "candidate"
assert nodes_by_label[("Repository", "Fixture Repo")]["canon_category"] == "source-repository"
assert nodes_by_label[("Repository", "Fixture Repo")]["evidence_state"] == "declared"
assert nodes_by_label[("ServiceDeclaration", "Fixture API")]["review_state"] == "accepted"
assert nodes_by_label[("ServiceDeclaration", "Fixture API")]["canon_category"] == "service"
assert nodes_by_label[("Library", "fixture-service")]["attributes"]["language"] == "python"
assert nodes_by_label[("ExternalLibrary", "PyYAML")]["attributes"]["ecosystem"] == "python"
assert nodes_by_label[("DeploymentService", "api")]["attributes"]["orchestrator"] == "docker-compose"
@@ -45,10 +48,15 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"]
== "kubernetes-service-dns"
)
assert (
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["canon_category"]
== "runtime-resource"
)
assert (
nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["attributes"]["runtime_target_type"]
== "declared-endpoint"
)
assert nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["canon_category"] == "endpoint"
assert nodes_by_label[("NetworkPort", "127.0.0.1:8080/tcp")]["attributes"]["target_port"] == 8080
assert nodes_by_label[("NetworkPort", "fixture-api.testing.svc.cluster.local:8080/tcp")]["attributes"]["service_port"] == 8080
assert nodes_by_label[("NetworkPort", "declared.fixture.test:9443/tcp")]["attributes"]["scheme"] == "https"
@@ -76,6 +84,14 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
"routes_to_service",
"resolves_to",
}
edges_by_type = {edge["edge_type"]: edge for edge in candidates["edges"]}
assert edges_by_type["exposes"]["canonical_type"] == "exposes"
assert edges_by_type["provides"]["canonical_type"] == "implements"
assert edges_by_type["provides"]["mapping_fit"] == "partial"
assert edges_by_type["opens_port"]["canonical_type"] == "exposes"
assert edges_by_type["resolves_to"]["canonical_type"] == "flows_to"
assert all(edge["display_only"] is False for edge in candidates["edges"])
assert all(edge["evidence_state"] in {"declared", "observed", "inferred", "proposed", "gap"} for edge in candidates["edges"])
assert {attribute["name"] for attribute in candidates["attributes"]} >= {
"readme_title",
"intent_present",

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Canon-Aligned Graph Model Reset And Reingest"
domain: railiance
repo: railiance-fabric
status: proposed
status: active
owner: codex
topic_slug: railiance
planning_priority: high
@@ -69,7 +69,7 @@ path are ready.
```task
id: RAIL-FAB-WP-0016-T01
status: todo
status: done
priority: high
state_hub_task_id: "865c048b-fddc-43ee-a379-b61ca31df85b"
```
@@ -84,7 +84,7 @@ state_hub_task_id: "865c048b-fddc-43ee-a379-b61ca31df85b"
```task
id: RAIL-FAB-WP-0016-T02
status: todo
status: in_progress
priority: high
state_hub_task_id: "26fbc0d5-3b82-45d2-8307-97dffb9de500"
```