generated from coulomb/repo-seed
refactoring for canon conformity
This commit is contained in:
140
docs/canon-alignment-review.md
Normal file
140
docs/canon-alignment-review.md
Normal 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.
|
||||
100
docs/canon-interface-card.yaml
Normal file
100
docs/canon-interface-card.yaml
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
198
railiance_fabric/canon.py
Normal 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, "")
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "",
|
||||
)
|
||||
|
||||
@@ -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 || ""])
|
||||
|
||||
@@ -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=(",", ":"))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
68
tests/test_canon.py
Normal 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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user