From 653411ffb84f5f8eac885cc983faa24e56172c32 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 23 May 2026 14:00:59 +0200 Subject: [PATCH] refactoring for canon conformity --- docs/canon-alignment-review.md | 140 +++++++++++++ docs/canon-interface-card.yaml | 100 +++++++++ docs/discovery-queries.md | 5 + docs/state-hub-integration.md | 9 +- railiance_fabric/canon.py | 198 ++++++++++++++++++ railiance_fabric/graph.py | 40 +++- railiance_fabric/graph_explorer.py | 101 ++++++++- railiance_fabric/graph_explorer_ui.py | 6 +- railiance_fabric/registry.py | 43 +++- railiance_fabric/scanner.py | 27 +++ schemas/discovery-snapshot.schema.yaml | 37 ++++ schemas/state-hub-export.schema.yaml | 45 ++++ tests/test_canon.py | 68 ++++++ tests/test_graph_explorer.py | 7 +- tests/test_scanner.py | 16 ++ ...anon-aligned-graph-model-reset-reingest.md | 6 +- 16 files changed, 819 insertions(+), 29 deletions(-) create mode 100644 docs/canon-alignment-review.md create mode 100644 docs/canon-interface-card.yaml create mode 100644 railiance_fabric/canon.py create mode 100644 tests/test_canon.py diff --git a/docs/canon-alignment-review.md b/docs/canon-alignment-review.md new file mode 100644 index 0000000..88665bb --- /dev/null +++ b/docs/canon-alignment-review.md @@ -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. diff --git a/docs/canon-interface-card.yaml b/docs/canon-interface-card.yaml new file mode 100644 index 0000000..fd24de2 --- /dev/null +++ b/docs/canon-interface-card.yaml @@ -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. diff --git a/docs/discovery-queries.md b/docs/discovery-queries.md index 016e347..3560e01 100644 --- a/docs/discovery-queries.md +++ b/docs/discovery-queries.md @@ -90,6 +90,11 @@ The JSON export has two top-level arrays: - `edges`: graph relationships such as `provides`, `exposes`, `available_via`, `consumes`, `binds:`, 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 diff --git a/docs/state-hub-integration.md b/docs/state-hub-integration.md index 5f8e8a7..7daa47b 100644 --- a/docs/state-hub-integration.md +++ b/docs/state-hub-integration.md @@ -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 diff --git a/railiance_fabric/canon.py b/railiance_fabric/canon.py new file mode 100644 index 0000000..e72b0d1 --- /dev/null +++ b/railiance_fabric/canon.py @@ -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, "") diff --git a/railiance_fabric/graph.py b/railiance_fabric/graph.py index f541e4c..f1a6009 100644 --- a/railiance_fabric/graph.py +++ b/railiance_fabric/graph.py @@ -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) diff --git a/railiance_fabric/graph_explorer.py b/railiance_fabric/graph_explorer.py index 2fc04d9..f7b71f5 100644 --- a/railiance_fabric/graph_explorer.py +++ b/railiance_fabric/graph_explorer.py @@ -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 "", ) diff --git a/railiance_fabric/graph_explorer_ui.py b/railiance_fabric/graph_explorer_ui.py index 38d42f1..c937c4b 100644 --- a/railiance_fabric/graph_explorer_ui.py +++ b/railiance_fabric/graph_explorer_ui.py @@ -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 ? `${escapeHtml(value)}` : "") .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 || ""]) diff --git a/railiance_fabric/registry.py b/railiance_fabric/registry.py index e6dca32..55295d5 100644 --- a/railiance_fabric/registry.py +++ b/railiance_fabric/registry.py @@ -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=(",", ":")) diff --git a/railiance_fabric/scanner.py b/railiance_fabric/scanner.py index 10d8c7c..02ac64e 100644 --- a/railiance_fabric/scanner.py +++ b/railiance_fabric/scanner.py @@ -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, diff --git a/schemas/discovery-snapshot.schema.yaml b/schemas/discovery-snapshot.schema.yaml index 05e7e16..9232cbc 100644 --- a/schemas/discovery-snapshot.schema.yaml +++ b/schemas/discovery-snapshot.schema.yaml @@ -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: diff --git a/schemas/state-hub-export.schema.yaml b/schemas/state-hub-export.schema.yaml index df6d440..a349196 100644 --- a/schemas/state-hub-export.schema.yaml +++ b/schemas/state-hub-export.schema.yaml @@ -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 diff --git a/tests/test_canon.py b/tests/test_canon.py new file mode 100644 index 0000000..25e46dc --- /dev/null +++ b/tests/test_canon.py @@ -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" diff --git a/tests/test_graph_explorer.py b/tests/test_graph_explorer.py index 129a609..53c8887 100644 --- a/tests/test_graph_explorer.py +++ b/tests/test_graph_explorer.py @@ -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: diff --git a/tests/test_scanner.py b/tests/test_scanner.py index 3b63eb3..e7259b9 100644 --- a/tests/test_scanner.py +++ b/tests/test_scanner.py @@ -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", diff --git a/workplans/RAIL-FAB-WP-0016-canon-aligned-graph-model-reset-reingest.md b/workplans/RAIL-FAB-WP-0016-canon-aligned-graph-model-reset-reingest.md index 618f0ed..d9ea195 100644 --- a/workplans/RAIL-FAB-WP-0016-canon-aligned-graph-model-reset-reingest.md +++ b/workplans/RAIL-FAB-WP-0016-canon-aligned-graph-model-reset-reingest.md @@ -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" ```