generated from coulomb/repo-seed
Start graph explorer contract
This commit is contained in:
@@ -94,7 +94,14 @@ GET /repositories/{repo_slug}/inventory
|
||||
GET /repositories/{repo_slug}/snapshots
|
||||
GET /repositories/{repo_slug}/snapshots/diff
|
||||
GET /search?q=jsonschema
|
||||
GET /exports/graph-explorer/manifest
|
||||
GET /exports/graph-explorer
|
||||
```
|
||||
|
||||
See `docs/registry-onboarding.md` for the multi-repo manifest and operating
|
||||
loop.
|
||||
|
||||
The graph explorer export is the first executable slice of the interactive
|
||||
Fabric map. See `docs/graph-explorer-transfer-review.md` for the repo-scoping
|
||||
transfer review and `docs/graph-explorer-contract.md` for the shared manifest
|
||||
and payload contract.
|
||||
|
||||
145
docs/graph-explorer-contract.md
Normal file
145
docs/graph-explorer-contract.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Graph Explorer Contract
|
||||
|
||||
This note defines the first manifest and payload contract for the interactive
|
||||
Fabric map and the possible reusable graph explorer engine.
|
||||
|
||||
The contract is intentionally host-neutral. Fabric and repo-scoping should be
|
||||
able to use the same interaction shell by exposing a manifest and a graph
|
||||
payload with stable fields.
|
||||
|
||||
## Files
|
||||
|
||||
- `schemas/graph-explorer-manifest.schema.yaml` validates a host manifest.
|
||||
- `schemas/graph-explorer-payload.schema.yaml` validates graph payloads.
|
||||
- `railiance_fabric.graph_explorer` provides the first Fabric registry
|
||||
manifest and payload projection.
|
||||
|
||||
## Registry Endpoints
|
||||
|
||||
The registry service exposes the first Fabric projection:
|
||||
|
||||
```text
|
||||
GET /exports/graph-explorer/manifest
|
||||
GET /exports/graph-explorer
|
||||
```
|
||||
|
||||
The manifest tells a graph shell where to load data, which fields are stable,
|
||||
which layers exist, which filter fields are available, and which modes the host
|
||||
supports.
|
||||
|
||||
The payload is compatible with Cytoscape-style element arrays:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiVersion": "railiance.fabric/v1alpha1",
|
||||
"kind": "GraphExplorerPayload",
|
||||
"manifest_id": "railiance-fabric.registry-map",
|
||||
"mode": "full",
|
||||
"elements": [
|
||||
{
|
||||
"data": {
|
||||
"id": "repo:railiance-fabric",
|
||||
"stableKey": "repo:railiance-fabric",
|
||||
"kind": "Repository",
|
||||
"layer": "repository",
|
||||
"label": "Railiance Fabric",
|
||||
"displayState": "show"
|
||||
}
|
||||
}
|
||||
],
|
||||
"hidden_elements": []
|
||||
}
|
||||
```
|
||||
|
||||
## Required Payload Semantics
|
||||
|
||||
Every element must include:
|
||||
|
||||
- `id`: the current element id used by the graph library.
|
||||
- `stableKey`: the durable id used by profile rules, manual overrides, layout
|
||||
state, and deep links.
|
||||
- `kind`: host-specific entity kind.
|
||||
- `layer`: host-declared layer used for layout, grouping, and color.
|
||||
- `displayState`: one of `show`, `blur`, or `hide`.
|
||||
|
||||
Edges are ordinary elements whose data also includes:
|
||||
|
||||
- `source`
|
||||
- `target`
|
||||
- `edgeType`
|
||||
- `strength`
|
||||
- `sameLayer`
|
||||
|
||||
Hosts should also include useful optional fields when available: `label`,
|
||||
`name`, `description`, `repo`, `domain`, `lifecycle`, `reviewState`,
|
||||
`freshnessState`, `confidence`, `visualSize`, `ownership`, `unresolved`,
|
||||
`sourceReferences`, and `deepLinks`.
|
||||
|
||||
## Display State Ownership
|
||||
|
||||
The contract allows either the host service or the engine to evaluate display
|
||||
state.
|
||||
|
||||
The precedence rule is fixed:
|
||||
|
||||
1. Default element state is `show`.
|
||||
2. Rules are applied in list order; later matching rules override earlier
|
||||
matching rules.
|
||||
3. Manual overrides win last.
|
||||
4. Edges connected to hidden nodes are hidden.
|
||||
5. Edges connected to blurred nodes may carry a contextual muted class or data
|
||||
hint.
|
||||
|
||||
Repo-scoping currently evaluates this host-side. A future extracted engine can
|
||||
evaluate it client-side for static exports, but host-provided `displayState`
|
||||
must remain valid input.
|
||||
|
||||
## Fabric Layers
|
||||
|
||||
The first Fabric manifest declares:
|
||||
|
||||
| Layer | Purpose |
|
||||
|-------|---------|
|
||||
| `repository` | Registered source repositories, including registered-only repos. |
|
||||
| `service` | Service declarations. |
|
||||
| `capability` | Capability declarations. |
|
||||
| `interface` | Interface declarations. |
|
||||
| `dependency` | Dependency declarations, including unresolved dependency nodes. |
|
||||
| `binding` | Binding assertions between consumer dependencies and providers. |
|
||||
| `library` | Future library/SBOM inventory nodes. |
|
||||
|
||||
## Repo-Scoping Compatibility
|
||||
|
||||
Repo-scoping can adapt without a rewrite because its current graph payload
|
||||
already exposes most required fields:
|
||||
|
||||
- `id`, `stableKey`, `kind`, `layer`, labels, and metadata-rich data objects.
|
||||
- `displayState`, `visibilitySource`, and `visibilityReason`.
|
||||
- edge `source`, `target`, `dependencyType`, `strength`, `sameLayer`, and
|
||||
visual width.
|
||||
- profile data, filter rules, manual overrides, hidden elements, and orphaned
|
||||
overrides.
|
||||
|
||||
The main adapter work is manifest generation and small field aliases:
|
||||
`dependencyType` can map to `edgeType`, repo-scoping layers become manifest
|
||||
layers, and existing profile endpoints can be listed under manifest
|
||||
`endpoints.profiles`.
|
||||
|
||||
## Extraction Boundary
|
||||
|
||||
The extracted `graph-explorer-engine` should own:
|
||||
|
||||
- graph rendering and layout controls
|
||||
- filter and manual override UI
|
||||
- hover popups and selection detail panels
|
||||
- profile UI when the host declares profile endpoints
|
||||
- URL state and copied state blobs
|
||||
- schema definitions and compatibility tests
|
||||
|
||||
Host repos should own:
|
||||
|
||||
- graph projection and metadata enrichment
|
||||
- profile persistence
|
||||
- authentication and authorization
|
||||
- domain-specific graph modes
|
||||
- deep links back to source systems
|
||||
84
docs/graph-explorer-transfer-review.md
Normal file
84
docs/graph-explorer-transfer-review.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Graph Explorer Transfer Review
|
||||
|
||||
This note closes the first implementation step for `RAIL-FAB-WP-0008-T01`.
|
||||
It reviews the repo-scoping dependency graph work from `RREG-WP-0010` and
|
||||
`RREG-WP-0011` and turns the transferable parts into Fabric requirements.
|
||||
|
||||
## Source Implementation
|
||||
|
||||
Repo-scoping already has a useful graph explorer shape:
|
||||
|
||||
- `docs/adr-dependency-graph-visualization-framework.md` chooses Cytoscape.js
|
||||
because the required interaction model is graph-native: pan, zoom, selection,
|
||||
layouts, styling, filtering, and path exploration.
|
||||
- `docs/dependency-visualization-exploration.md` defines layered graph data,
|
||||
`show` / `blur` / `hide` display states, deterministic filters, manual
|
||||
overrides, and saved view profiles.
|
||||
- `src/repo_scoping/core/service.py` emits Cytoscape-compatible payloads with
|
||||
stable keys, metadata-rich node and edge data, visibility state, hidden
|
||||
elements, profile application, confidence sizing, and edge strength sizing.
|
||||
- `src/repo_scoping/web_ui/views.py` provides the first UI shell: profile
|
||||
controls, structured filters, manual overrides, focus depth, hover popups,
|
||||
selection details, and a layered layout.
|
||||
- `tests/test_web_api.py` verifies graph endpoints, ad hoc filters, profile
|
||||
creation, duplicate profiles, latest-profile defaulting, and UI wiring.
|
||||
|
||||
## Reusable Behaviors
|
||||
|
||||
Fabric should carry these behaviors into the shared graph explorer contract:
|
||||
|
||||
- Cytoscape-compatible element arrays with `data` and optional `classes`.
|
||||
- Stable element keys that survive refreshes, so layout state, profile rules,
|
||||
and deep links do not churn.
|
||||
- Metadata-rich nodes and edges rather than UI-only labels.
|
||||
- Explicit `layer`, `kind`, `reviewState`, `freshnessState`, and
|
||||
`displayState` fields.
|
||||
- Visibility actions of `show`, `blur`, and `hide`, with later rules overriding
|
||||
earlier rules and manual overrides winning last.
|
||||
- Hidden element reporting, so over-filtered views are recoverable.
|
||||
- Edge treatment for context-muted nodes.
|
||||
- View profiles that can persist filter rules and manual overrides when a host
|
||||
provides profile endpoints.
|
||||
- Hover popups for compact inspection and selection panels for full detail.
|
||||
- Layout modes for full graph, impact or filtered graph, selected path, and
|
||||
neighborhood focus.
|
||||
- Visual hints for confidence, strength, same-layer edges, stale or unresolved
|
||||
state, and review state.
|
||||
|
||||
## Adapter-Owned Semantics
|
||||
|
||||
These repo-scoping concepts should remain in the repo-scoping adapter and not
|
||||
be hard-coded into the engine:
|
||||
|
||||
- Layer names of `fact`, `evidence`, `feature`, `capability`, `ability`, and
|
||||
`scope`.
|
||||
- Candidate graph approval semantics.
|
||||
- Dependency impact analysis over base and target analysis runs.
|
||||
- Document fact normalization and README/SCOPE de-duplication.
|
||||
- Scope curation recommendations.
|
||||
|
||||
Fabric has its own adapter semantics:
|
||||
|
||||
- Layer names of `repository`, `service`, `capability`, `interface`,
|
||||
`dependency`, `binding`, and `library`.
|
||||
- Registered-only repositories without accepted graph snapshots.
|
||||
- State Hub repo ids, workplan links, and registry endpoints as deep links.
|
||||
- Unresolved dependencies where provider bindings are missing or disputed.
|
||||
- Blast-radius and provider-chain exploration across accepted Fabric
|
||||
declarations.
|
||||
|
||||
## Extraction Recommendation
|
||||
|
||||
Start in `railiance-fabric` with a host-neutral manifest and payload contract,
|
||||
plus a Fabric registry projection. The first UI shell can live locally while
|
||||
the contract is still moving.
|
||||
|
||||
Extract into a separate repository only after two host adapters are proven:
|
||||
|
||||
1. Fabric registry map adapter.
|
||||
2. Repo-scoping dependency graph adapter.
|
||||
|
||||
The likely extracted repository is `graph-explorer-engine`. It should own the
|
||||
static interaction shell, schema definitions, layout/filter/profile client
|
||||
logic, and adapter manifest contract. Host repos should keep graph projection,
|
||||
profile persistence, authentication, and domain-specific deep links.
|
||||
@@ -64,4 +64,6 @@ GET /exports/state-hub
|
||||
GET /exports/backstage
|
||||
GET /exports/xregistry
|
||||
GET /exports/libraries/xregistry
|
||||
GET /exports/graph-explorer/manifest
|
||||
GET /exports/graph-explorer
|
||||
```
|
||||
|
||||
511
railiance_fabric/graph_explorer.py
Normal file
511
railiance_fabric/graph_explorer.py
Normal file
@@ -0,0 +1,511 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
DISPLAY_STATES = ("show", "blur", "hide")
|
||||
LAYER_ORDER = (
|
||||
"repository",
|
||||
"service",
|
||||
"capability",
|
||||
"interface",
|
||||
"dependency",
|
||||
"binding",
|
||||
"library",
|
||||
)
|
||||
|
||||
_KIND_LAYER = {
|
||||
"Repository": "repository",
|
||||
"ServiceDeclaration": "service",
|
||||
"CapabilityDeclaration": "capability",
|
||||
"InterfaceDeclaration": "interface",
|
||||
"DependencyDeclaration": "dependency",
|
||||
"BindingAssertion": "binding",
|
||||
"Library": "library",
|
||||
}
|
||||
|
||||
_LAYER_COLORS = {
|
||||
"repository": "#475569",
|
||||
"service": "#0f766e",
|
||||
"capability": "#2563eb",
|
||||
"interface": "#7c3aed",
|
||||
"dependency": "#b45309",
|
||||
"binding": "#be123c",
|
||||
"library": "#0891b2",
|
||||
}
|
||||
|
||||
_EDGE_STRENGTH = {
|
||||
"provides": "strong",
|
||||
"exposes": "strong",
|
||||
"available_via": "medium",
|
||||
"consumes": "medium",
|
||||
"uses_interface": "medium",
|
||||
"declares": "weak",
|
||||
}
|
||||
|
||||
|
||||
def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
|
||||
"""Return the host manifest for the reusable graph explorer shell."""
|
||||
|
||||
return {
|
||||
"apiVersion": "railiance.fabric/v1alpha1",
|
||||
"kind": "GraphExplorerManifest",
|
||||
"id": "railiance-fabric.registry-map",
|
||||
"title": "Railiance Fabric Registry Map",
|
||||
"description": "Manifest for exploring registered Fabric ecosystem entities.",
|
||||
"engine": {
|
||||
"suggested_package": "graph-explorer-engine",
|
||||
"preferred_library": "cytoscape",
|
||||
"display_state_owner": "host-or-engine",
|
||||
},
|
||||
"endpoints": {
|
||||
"graph": {
|
||||
"method": "GET",
|
||||
"url": f"{base_url}/exports/graph-explorer",
|
||||
},
|
||||
"manifest": {
|
||||
"method": "GET",
|
||||
"url": f"{base_url}/exports/graph-explorer/manifest",
|
||||
},
|
||||
},
|
||||
"identity": {
|
||||
"node_id_field": "id",
|
||||
"stable_key_field": "stableKey",
|
||||
"edge_id_field": "id",
|
||||
"source_field": "source",
|
||||
"target_field": "target",
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": layer,
|
||||
"label": layer.replace("_", " ").title(),
|
||||
"order": index,
|
||||
"color": _LAYER_COLORS[layer],
|
||||
}
|
||||
for index, layer in enumerate(LAYER_ORDER)
|
||||
],
|
||||
"grouping_fields": ["domain", "repo", "layer", "kind", "lifecycle", "unresolved"],
|
||||
"search_fields": [
|
||||
"id",
|
||||
"stableKey",
|
||||
"label",
|
||||
"name",
|
||||
"description",
|
||||
"repo",
|
||||
"domain",
|
||||
"kind",
|
||||
"layer",
|
||||
"edgeType",
|
||||
],
|
||||
"filter": {
|
||||
"actions": list(DISPLAY_STATES),
|
||||
"fields": [
|
||||
{"id": "kind", "label": "Kind", "type": "string"},
|
||||
{"id": "layer", "label": "Layer", "type": "string"},
|
||||
{"id": "repo", "label": "Repo", "type": "string"},
|
||||
{"id": "domain", "label": "Domain", "type": "string"},
|
||||
{"id": "lifecycle", "label": "Lifecycle", "type": "string"},
|
||||
{"id": "reviewState", "label": "Review State", "type": "string"},
|
||||
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
|
||||
{"id": "edgeType", "label": "Edge Type", "type": "string"},
|
||||
{"id": "strength", "label": "Strength", "type": "string"},
|
||||
{"id": "sameLayer", "label": "Same Layer", "type": "boolean"},
|
||||
{"id": "text", "label": "Text", "type": "string"},
|
||||
],
|
||||
},
|
||||
"visual_encodings": {
|
||||
"node_color": "layer",
|
||||
"node_shape": "kind",
|
||||
"node_size": "confidence",
|
||||
"edge_width": "strength",
|
||||
"edge_style": "sameLayer",
|
||||
"edge_opacity": "displayState",
|
||||
},
|
||||
"detail": {
|
||||
"node_fields": [
|
||||
"name",
|
||||
"kind",
|
||||
"layer",
|
||||
"repo",
|
||||
"domain",
|
||||
"lifecycle",
|
||||
"unresolved",
|
||||
"description",
|
||||
"sourceReferences",
|
||||
"deepLinks",
|
||||
],
|
||||
"edge_fields": [
|
||||
"edgeType",
|
||||
"strength",
|
||||
"source",
|
||||
"target",
|
||||
"sourceLayer",
|
||||
"targetLayer",
|
||||
"sameLayer",
|
||||
"deepLinks",
|
||||
],
|
||||
},
|
||||
"modes": [
|
||||
{
|
||||
"id": "full",
|
||||
"label": "Full",
|
||||
"description": "Show the complete registered ecosystem graph.",
|
||||
},
|
||||
{
|
||||
"id": "selected-path",
|
||||
"label": "Selected Path",
|
||||
"description": "Show selected nodes with predecessor and successor context.",
|
||||
"requires_selection": True,
|
||||
},
|
||||
{
|
||||
"id": "neighborhood",
|
||||
"label": "Neighborhood",
|
||||
"description": "Show the selected node neighborhood by configurable depth.",
|
||||
"requires_selection": True,
|
||||
},
|
||||
{
|
||||
"id": "onboarding-gaps",
|
||||
"label": "Onboarding Gaps",
|
||||
"description": "Highlight registered repos without accepted Fabric graph snapshots.",
|
||||
},
|
||||
{
|
||||
"id": "unresolved",
|
||||
"label": "Unresolved",
|
||||
"description": "Highlight dependencies that have no accepted provider binding.",
|
||||
},
|
||||
],
|
||||
"profile_persistence": "none",
|
||||
"shareable_state": {
|
||||
"url_parameters": True,
|
||||
"profile_id": False,
|
||||
"state_blob": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def fabric_graph_explorer_payload(
|
||||
graph: dict[str, Any],
|
||||
repositories: list[dict[str, Any]] | None = None,
|
||||
snapshot_repo_slugs: set[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Project a Fabric graph export into the shared graph explorer payload."""
|
||||
|
||||
source_nodes = [node for node in graph.get("nodes", []) if isinstance(node, dict)]
|
||||
source_edges = [edge for edge in graph.get("edges", []) if isinstance(edge, dict)]
|
||||
repositories = repositories or []
|
||||
snapshot_repo_slugs = snapshot_repo_slugs or set()
|
||||
|
||||
layers_by_id = {
|
||||
str(node.get("id", "")): _layer_for_kind(str(node.get("kind", "")))
|
||||
for node in source_nodes
|
||||
}
|
||||
source_repo_slugs = {
|
||||
str(node.get("repo", "")).strip()
|
||||
for node in source_nodes
|
||||
if str(node.get("repo", "")).strip()
|
||||
}
|
||||
registered_repo_slugs = {
|
||||
str(repo.get("slug", "")).strip()
|
||||
for repo in repositories
|
||||
if str(repo.get("slug", "")).strip()
|
||||
}
|
||||
repo_slugs = set(source_repo_slugs)
|
||||
repo_slugs.update(registered_repo_slugs)
|
||||
repo_slugs.update(snapshot_repo_slugs)
|
||||
repo_slugs.discard("")
|
||||
unresolved = _unresolved_dependency_ids(source_nodes, source_edges)
|
||||
|
||||
elements: list[dict[str, Any]] = []
|
||||
repository_index = {str(repo.get("slug", "")): repo for repo in repositories}
|
||||
for slug in sorted(repo_slugs):
|
||||
repo = repository_index.get(slug, {})
|
||||
has_snapshot = slug in snapshot_repo_slugs or slug in source_repo_slugs
|
||||
is_registered = slug in registered_repo_slugs
|
||||
repo_id = f"repo:{slug}"
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": repo_id,
|
||||
"stableKey": repo_id,
|
||||
"kind": "Repository",
|
||||
"layer": "repository",
|
||||
"label": str(repo.get("name") or slug),
|
||||
"name": str(repo.get("name") or slug),
|
||||
"description": (
|
||||
"Registered repository with accepted Fabric graph snapshot."
|
||||
if has_snapshot
|
||||
else "Registered repository without an accepted Fabric graph snapshot."
|
||||
),
|
||||
"repo": slug,
|
||||
"domain": "railiance",
|
||||
"lifecycle": "active" if has_snapshot else "registered-only",
|
||||
"reviewState": "accepted" if has_snapshot else "candidate",
|
||||
"freshnessState": "current" if has_snapshot else "missing",
|
||||
"unresolved": is_registered and not has_snapshot,
|
||||
"confidence": 1.0 if has_snapshot else 0.3,
|
||||
"visualSize": 56 if has_snapshot else 42,
|
||||
"ownership": "registry",
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"sourceReferences": [],
|
||||
"deepLinks": _repository_links(repo),
|
||||
},
|
||||
"classes": "repository accepted" if has_snapshot else "repository candidate unresolved",
|
||||
}
|
||||
)
|
||||
|
||||
for node in source_nodes:
|
||||
node_id = str(node.get("id", ""))
|
||||
if not node_id:
|
||||
continue
|
||||
kind = str(node.get("kind", ""))
|
||||
layer = _layer_for_kind(kind)
|
||||
is_unresolved = node_id in unresolved
|
||||
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": node_id,
|
||||
"stableKey": node_id,
|
||||
"kind": kind,
|
||||
"layer": layer,
|
||||
"label": str(node.get("name") or node_id),
|
||||
"name": str(node.get("name") or node_id),
|
||||
"description": _node_description(kind, attributes),
|
||||
"repo": str(node.get("repo", "")),
|
||||
"domain": str(node.get("domain", "")),
|
||||
"lifecycle": str(node.get("lifecycle", "")),
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"unresolved": is_unresolved,
|
||||
"confidence": 0.45 if is_unresolved else 1.0,
|
||||
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
|
||||
"ownership": "repo",
|
||||
"attributes": attributes,
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"sourceReferences": _source_references(node),
|
||||
"deepLinks": _node_links(node_id),
|
||||
},
|
||||
"classes": " ".join(
|
||||
part
|
||||
for part in (layer, kind, "unresolved" if is_unresolved else "accepted")
|
||||
if part
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
edge_index = 0
|
||||
for edge in source_edges:
|
||||
source = str(edge.get("from", ""))
|
||||
target = str(edge.get("to", ""))
|
||||
edge_type = str(edge.get("type", ""))
|
||||
if not source or not target:
|
||||
continue
|
||||
source_layer = layers_by_id.get(source, "unknown")
|
||||
target_layer = layers_by_id.get(target, "unknown")
|
||||
strength = _edge_strength(edge_type)
|
||||
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
|
||||
edge_index += 1
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": edge_id,
|
||||
"stableKey": edge_id,
|
||||
"kind": "edge",
|
||||
"layer": "relationship",
|
||||
"label": edge_type,
|
||||
"source": source,
|
||||
"target": target,
|
||||
"sourceLayer": source_layer,
|
||||
"targetLayer": target_layer,
|
||||
"edgeType": edge_type,
|
||||
"dependencyType": edge_type,
|
||||
"strength": strength,
|
||||
"edgeWidth": _edge_width(strength),
|
||||
"sameLayer": source_layer == target_layer,
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"deepLinks": {},
|
||||
},
|
||||
"classes": " ".join(
|
||||
part
|
||||
for part in (
|
||||
edge_type.replace(":", "-"),
|
||||
strength,
|
||||
"same-layer" if source_layer == target_layer else "",
|
||||
)
|
||||
if part
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
for slug in sorted(repo_slugs):
|
||||
repo_id = f"repo:{slug}"
|
||||
for node in source_nodes:
|
||||
node_id = str(node.get("id", ""))
|
||||
if str(node.get("repo", "")) != slug or not node_id:
|
||||
continue
|
||||
edge_id = f"edge:{edge_index}:{repo_id}:declares:{node_id}"
|
||||
edge_index += 1
|
||||
target_layer = layers_by_id.get(node_id, "unknown")
|
||||
elements.append(
|
||||
{
|
||||
"data": {
|
||||
"id": edge_id,
|
||||
"stableKey": edge_id,
|
||||
"kind": "edge",
|
||||
"layer": "relationship",
|
||||
"label": "declares",
|
||||
"source": repo_id,
|
||||
"target": node_id,
|
||||
"sourceLayer": "repository",
|
||||
"targetLayer": target_layer,
|
||||
"edgeType": "declares",
|
||||
"dependencyType": "declares",
|
||||
"strength": "weak",
|
||||
"edgeWidth": 1,
|
||||
"sameLayer": False,
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"deepLinks": {},
|
||||
},
|
||||
"classes": "declares weak",
|
||||
}
|
||||
)
|
||||
|
||||
visible_nodes = [element for element in elements if "source" not in element["data"]]
|
||||
visible_edges = [element for element in elements if "source" in element["data"]]
|
||||
return {
|
||||
"apiVersion": "railiance.fabric/v1alpha1",
|
||||
"kind": "GraphExplorerPayload",
|
||||
"manifest_id": "railiance-fabric.registry-map",
|
||||
"generated_at": _utc_now(),
|
||||
"repository": {"slug": "registry", "name": "Railiance Fabric Registry"},
|
||||
"mode": "full",
|
||||
"profile": None,
|
||||
"metrics": {
|
||||
"node_count": len(visible_nodes),
|
||||
"edge_count": len(visible_edges),
|
||||
"hidden_count": 0,
|
||||
"blurred_count": 0,
|
||||
"registered_repo_count": len(registered_repo_slugs),
|
||||
"repo_node_count": len(repo_slugs),
|
||||
"registered_only_repo_count": sum(
|
||||
1
|
||||
for element in visible_nodes
|
||||
if element["data"].get("repo") in registered_repo_slugs
|
||||
and element["data"].get("lifecycle") == "registered-only"
|
||||
),
|
||||
"unresolved_count": len(unresolved),
|
||||
},
|
||||
"filter": {
|
||||
"rules": [],
|
||||
"manual_overrides": {},
|
||||
"orphaned_overrides": [],
|
||||
"precedence": "later rules override earlier rules; manual overrides win last",
|
||||
"connected_edge_behavior": "edges connected to hidden nodes are hidden",
|
||||
},
|
||||
"elements": elements,
|
||||
"hidden_elements": [],
|
||||
}
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _layer_for_kind(kind: str) -> str:
|
||||
return _KIND_LAYER.get(kind, kind.lower() or "unknown")
|
||||
|
||||
|
||||
def _edge_strength(edge_type: str) -> str:
|
||||
if edge_type.startswith("binds:"):
|
||||
status = edge_type.split(":", 1)[1]
|
||||
if status in {"missing", "disputed"}:
|
||||
return "weak"
|
||||
if status == "accepted":
|
||||
return "strong"
|
||||
return "medium"
|
||||
return _EDGE_STRENGTH.get(edge_type, "medium")
|
||||
|
||||
|
||||
def _edge_width(strength: str) -> int:
|
||||
return {"weak": 1, "medium": 3, "strong": 5}.get(strength, 2)
|
||||
|
||||
|
||||
def _unresolved_dependency_ids(
|
||||
nodes: list[dict[str, Any]],
|
||||
edges: list[dict[str, Any]],
|
||||
) -> set[str]:
|
||||
dependency_ids = {
|
||||
str(node.get("id", ""))
|
||||
for node in nodes
|
||||
if node.get("kind") == "DependencyDeclaration" and str(node.get("id", ""))
|
||||
}
|
||||
resolved: set[str] = set()
|
||||
unresolved: set[str] = set()
|
||||
for edge in edges:
|
||||
edge_type = str(edge.get("type", ""))
|
||||
source = str(edge.get("from", ""))
|
||||
if source not in dependency_ids or not edge_type.startswith("binds:"):
|
||||
continue
|
||||
status = edge_type.split(":", 1)[1]
|
||||
if status in {"accepted", "candidate"}:
|
||||
resolved.add(source)
|
||||
if status in {"missing", "disputed"}:
|
||||
unresolved.add(source)
|
||||
return (dependency_ids - resolved) | unresolved
|
||||
|
||||
|
||||
def _node_description(kind: str, attributes: object) -> str:
|
||||
if not isinstance(attributes, dict):
|
||||
return ""
|
||||
if kind == "CapabilityDeclaration":
|
||||
return str(attributes.get("capability_type", ""))
|
||||
if kind == "InterfaceDeclaration":
|
||||
version = str(attributes.get("version", ""))
|
||||
interface_type = str(attributes.get("interface_type", ""))
|
||||
return " ".join(part for part in (interface_type, version) if part)
|
||||
if kind == "DependencyDeclaration":
|
||||
return str(
|
||||
attributes.get("requires_capability_type")
|
||||
or attributes.get("requires_capability_id")
|
||||
or attributes.get("interface_type")
|
||||
or ""
|
||||
)
|
||||
if kind == "BindingAssertion":
|
||||
return str(attributes.get("status", ""))
|
||||
return ""
|
||||
|
||||
|
||||
def _source_references(node: dict[str, Any]) -> list[dict[str, str]]:
|
||||
attributes = node.get("attributes")
|
||||
references: list[dict[str, str]] = []
|
||||
if isinstance(attributes, dict):
|
||||
for source in attributes.get("source_links", []):
|
||||
if isinstance(source, dict):
|
||||
references.append({key: str(value) for key, value in source.items()})
|
||||
return references
|
||||
|
||||
|
||||
def _node_links(node_id: str) -> dict[str, str]:
|
||||
return {"registry": f"/graph/nodes/{node_id}"}
|
||||
|
||||
|
||||
def _repository_links(repository: dict[str, Any]) -> dict[str, str]:
|
||||
slug = str(repository.get("slug", ""))
|
||||
links = {"registry": f"/repositories/{slug}"} if slug else {}
|
||||
state_hub_repo_id = str(repository.get("state_hub_repo_id") or "")
|
||||
if state_hub_repo_id:
|
||||
links["stateHubRepo"] = f"/repos/by-id/{state_hub_repo_id}"
|
||||
return links
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from .graph_explorer import fabric_graph_explorer_manifest, fabric_graph_explorer_payload
|
||||
from .registry import (
|
||||
RegistryError,
|
||||
RegistryStore,
|
||||
@@ -79,6 +80,14 @@ class RegistryHandler(BaseHTTPRequestHandler):
|
||||
return HTTPStatus.OK, backstage_projection(self.store.combined_graph())
|
||||
if parts == ["exports", "xregistry"]:
|
||||
return HTTPStatus.OK, xregistry_projection(self.store.combined_graph())
|
||||
if parts == ["exports", "graph-explorer"]:
|
||||
return HTTPStatus.OK, fabric_graph_explorer_payload(
|
||||
self.store.combined_graph(),
|
||||
self.store.list_repositories(),
|
||||
{str(snapshot["repo_slug"]) for snapshot in self.store.latest_snapshots()},
|
||||
)
|
||||
if parts == ["exports", "graph-explorer", "manifest"]:
|
||||
return HTTPStatus.OK, fabric_graph_explorer_manifest()
|
||||
if parts == ["artifacts"]:
|
||||
return HTTPStatus.OK, self.store.list_artifacts(
|
||||
repo_slug=_query_optional(query, "repo_slug"),
|
||||
|
||||
232
schemas/graph-explorer-manifest.schema.yaml
Normal file
232
schemas/graph-explorer-manifest.schema.yaml
Normal file
@@ -0,0 +1,232 @@
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
$id: "https://railiance.local/fabric/schemas/graph-explorer-manifest.schema.yaml"
|
||||
title: "GraphExplorerManifest"
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- apiVersion
|
||||
- kind
|
||||
- id
|
||||
- title
|
||||
- endpoints
|
||||
- identity
|
||||
- layers
|
||||
- filter
|
||||
- modes
|
||||
- profile_persistence
|
||||
properties:
|
||||
apiVersion:
|
||||
$ref: "./common.schema.yaml#/$defs/apiVersion"
|
||||
kind:
|
||||
type: string
|
||||
const: GraphExplorerManifest
|
||||
id:
|
||||
type: string
|
||||
minLength: 3
|
||||
title:
|
||||
type: string
|
||||
minLength: 1
|
||||
description:
|
||||
type: string
|
||||
engine:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
suggested_package:
|
||||
type: string
|
||||
preferred_library:
|
||||
type: string
|
||||
display_state_owner:
|
||||
type: string
|
||||
enum:
|
||||
- host
|
||||
- engine
|
||||
- host-or-engine
|
||||
endpoints:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- graph
|
||||
properties:
|
||||
graph:
|
||||
$ref: "#/$defs/endpoint"
|
||||
manifest:
|
||||
$ref: "#/$defs/endpoint"
|
||||
refilter:
|
||||
$ref: "#/$defs/endpoint"
|
||||
profiles:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
list:
|
||||
$ref: "#/$defs/endpoint"
|
||||
create:
|
||||
$ref: "#/$defs/endpoint"
|
||||
get:
|
||||
$ref: "#/$defs/endpoint"
|
||||
update:
|
||||
$ref: "#/$defs/endpoint"
|
||||
delete:
|
||||
$ref: "#/$defs/endpoint"
|
||||
duplicate:
|
||||
$ref: "#/$defs/endpoint"
|
||||
latest_default:
|
||||
type: boolean
|
||||
identity:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- node_id_field
|
||||
- stable_key_field
|
||||
- edge_id_field
|
||||
- source_field
|
||||
- target_field
|
||||
properties:
|
||||
node_id_field:
|
||||
type: string
|
||||
stable_key_field:
|
||||
type: string
|
||||
edge_id_field:
|
||||
type: string
|
||||
source_field:
|
||||
type: string
|
||||
target_field:
|
||||
type: string
|
||||
layers:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- label
|
||||
- order
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
order:
|
||||
type: integer
|
||||
minimum: 0
|
||||
color:
|
||||
type: string
|
||||
pattern: "^#[0-9a-fA-F]{6}$"
|
||||
grouping_fields:
|
||||
$ref: "#/$defs/stringList"
|
||||
search_fields:
|
||||
$ref: "#/$defs/stringList"
|
||||
filter:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- actions
|
||||
- fields
|
||||
properties:
|
||||
actions:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- show
|
||||
- blur
|
||||
- hide
|
||||
fields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- label
|
||||
- type
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- string
|
||||
- number
|
||||
- boolean
|
||||
- array
|
||||
visual_encodings:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
detail:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
node_fields:
|
||||
$ref: "#/$defs/stringList"
|
||||
edge_fields:
|
||||
$ref: "#/$defs/stringList"
|
||||
modes:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- label
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
minLength: 1
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
description:
|
||||
type: string
|
||||
requires_selection:
|
||||
type: boolean
|
||||
profile_persistence:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- host
|
||||
shareable_state:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
url_parameters:
|
||||
type: boolean
|
||||
profile_id:
|
||||
type: boolean
|
||||
state_blob:
|
||||
type: boolean
|
||||
|
||||
$defs:
|
||||
endpoint:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- method
|
||||
- url
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
- PATCH
|
||||
- DELETE
|
||||
url:
|
||||
type: string
|
||||
minLength: 1
|
||||
stringList:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
minLength: 1
|
||||
258
schemas/graph-explorer-payload.schema.yaml
Normal file
258
schemas/graph-explorer-payload.schema.yaml
Normal file
@@ -0,0 +1,258 @@
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
$id: "https://railiance.local/fabric/schemas/graph-explorer-payload.schema.yaml"
|
||||
title: "GraphExplorerPayload"
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- apiVersion
|
||||
- kind
|
||||
- elements
|
||||
- hidden_elements
|
||||
properties:
|
||||
apiVersion:
|
||||
$ref: "./common.schema.yaml#/$defs/apiVersion"
|
||||
kind:
|
||||
type: string
|
||||
const: GraphExplorerPayload
|
||||
manifest_id:
|
||||
type: string
|
||||
generated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
repository:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
scope:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
mode:
|
||||
type: string
|
||||
profile:
|
||||
anyOf:
|
||||
- type: "null"
|
||||
- $ref: "#/$defs/profile"
|
||||
metrics:
|
||||
type: object
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: string
|
||||
- type: "null"
|
||||
filter:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
rules:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/rule"
|
||||
manual_overrides:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/$defs/displayState"
|
||||
orphaned_overrides:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
precedence:
|
||||
type: string
|
||||
connected_edge_behavior:
|
||||
type: string
|
||||
elements:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/element"
|
||||
hidden_elements:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/element"
|
||||
impacts:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
changed_fact_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
$defs:
|
||||
element:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- id
|
||||
- stableKey
|
||||
- kind
|
||||
- layer
|
||||
- displayState
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/$defs/stableKey"
|
||||
key:
|
||||
$ref: "#/$defs/stableKey"
|
||||
stableKey:
|
||||
$ref: "#/$defs/stableKey"
|
||||
kind:
|
||||
type: string
|
||||
minLength: 1
|
||||
layer:
|
||||
type: string
|
||||
minLength: 1
|
||||
label:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
repo:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
lifecycle:
|
||||
type: string
|
||||
reviewState:
|
||||
type: string
|
||||
freshnessState:
|
||||
type: string
|
||||
displayState:
|
||||
$ref: "#/$defs/displayState"
|
||||
visibilitySource:
|
||||
type: string
|
||||
visibilityReason:
|
||||
type: string
|
||||
confidence:
|
||||
anyOf:
|
||||
- type: number
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
- type: "null"
|
||||
visualSize:
|
||||
type: number
|
||||
minimum: 1
|
||||
ownership:
|
||||
type: string
|
||||
unresolved:
|
||||
type: boolean
|
||||
source:
|
||||
$ref: "#/$defs/stableKey"
|
||||
target:
|
||||
$ref: "#/$defs/stableKey"
|
||||
sourceLayer:
|
||||
type: string
|
||||
targetLayer:
|
||||
type: string
|
||||
edgeType:
|
||||
type: string
|
||||
dependencyType:
|
||||
type: string
|
||||
strength:
|
||||
type: string
|
||||
enum:
|
||||
- weak
|
||||
- medium
|
||||
- strong
|
||||
edgeWidth:
|
||||
type: number
|
||||
minimum: 0
|
||||
sameLayer:
|
||||
type: boolean
|
||||
connectedToBlurred:
|
||||
type: boolean
|
||||
path:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
attributes:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
sourceReferences:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
- type: number
|
||||
- type: "null"
|
||||
deepLinks:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
classes:
|
||||
type: string
|
||||
group:
|
||||
type: string
|
||||
enum:
|
||||
- nodes
|
||||
- edges
|
||||
displayState:
|
||||
type: string
|
||||
enum:
|
||||
- show
|
||||
- blur
|
||||
- hide
|
||||
profile:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
repository_id:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
default_mode:
|
||||
type: string
|
||||
filter_rules:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/rule"
|
||||
manual_overrides:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/$defs/displayState"
|
||||
rule:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- action
|
||||
properties:
|
||||
action:
|
||||
$ref: "#/$defs/displayState"
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
match:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
stableKey:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 240
|
||||
pattern: "^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$"
|
||||
177
tests/test_graph_explorer.py
Normal file
177
tests/test_graph_explorer.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from railiance_fabric.graph import build_graph
|
||||
from railiance_fabric.graph_explorer import (
|
||||
fabric_graph_explorer_manifest,
|
||||
fabric_graph_explorer_payload,
|
||||
)
|
||||
from railiance_fabric.registry import RegistryStore
|
||||
from railiance_fabric.schema_validation import draft202012_validator
|
||||
from railiance_fabric.server import RegistryHandler
|
||||
|
||||
|
||||
def test_graph_explorer_manifest_and_payload_validate() -> None:
|
||||
graph = build_graph([Path(".")]).to_export()
|
||||
manifest = fabric_graph_explorer_manifest()
|
||||
payload = fabric_graph_explorer_payload(
|
||||
graph,
|
||||
[
|
||||
{
|
||||
"slug": "railiance-fabric",
|
||||
"name": "Railiance Fabric",
|
||||
"state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05",
|
||||
},
|
||||
{
|
||||
"slug": "registered-only",
|
||||
"name": "Registered Only",
|
||||
},
|
||||
],
|
||||
{"railiance-fabric"},
|
||||
)
|
||||
|
||||
_validate_schema("graph-explorer-manifest.schema.yaml", manifest)
|
||||
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
||||
|
||||
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
|
||||
edges = [element for element in payload["elements"] if "source" in element["data"]]
|
||||
registered_only = next(
|
||||
element for element in nodes if element["data"]["id"] == "repo:registered-only"
|
||||
)
|
||||
|
||||
assert registered_only["data"]["reviewState"] == "candidate"
|
||||
assert registered_only["data"]["unresolved"] is True
|
||||
assert any(edge["data"]["edgeType"] == "declares" for edge in edges)
|
||||
assert payload["metrics"]["registered_repo_count"] == 2
|
||||
|
||||
|
||||
def test_graph_explorer_payload_accepts_repo_scoping_shape() -> None:
|
||||
payload = {
|
||||
"apiVersion": "railiance.fabric/v1alpha1",
|
||||
"kind": "GraphExplorerPayload",
|
||||
"manifest_id": "repo-scoping.dependency-graph",
|
||||
"mode": "full",
|
||||
"profile": {
|
||||
"id": 1,
|
||||
"repository_id": 1,
|
||||
"name": "Evidence Audit",
|
||||
"description": "Show supporting evidence.",
|
||||
"default_mode": "full",
|
||||
"filter_rules": [
|
||||
{"name": "Blur facts", "action": "blur", "match": {"layer": "fact"}}
|
||||
],
|
||||
"manual_overrides": {"feature:1": "show"},
|
||||
},
|
||||
"filter": {
|
||||
"rules": [],
|
||||
"manual_overrides": {},
|
||||
"orphaned_overrides": [],
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"data": {
|
||||
"id": "fact:document:README.md",
|
||||
"stableKey": "fact:document:README.md",
|
||||
"kind": "fact",
|
||||
"layer": "fact",
|
||||
"label": "README.md",
|
||||
"displayState": "blur",
|
||||
"reviewState": "accepted",
|
||||
"freshnessState": "current",
|
||||
"confidence": 0.8,
|
||||
"visualSize": 50,
|
||||
"sourceReferences": [{"path": "README.md", "kind": "document"}],
|
||||
},
|
||||
"classes": "fact display-blur",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": "capability:1",
|
||||
"stableKey": "capability:1",
|
||||
"kind": "capability",
|
||||
"layer": "capability",
|
||||
"label": "Registry Capabilities",
|
||||
"displayState": "show",
|
||||
"reviewState": "accepted",
|
||||
},
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"id": "edge:fact:capability",
|
||||
"stableKey": "edge:fact:capability",
|
||||
"kind": "edge",
|
||||
"layer": "dependency",
|
||||
"label": "supports",
|
||||
"source": "fact:document:README.md",
|
||||
"target": "capability:1",
|
||||
"edgeType": "supports",
|
||||
"dependencyType": "supports",
|
||||
"strength": "strong",
|
||||
"edgeWidth": 5,
|
||||
"sameLayer": False,
|
||||
"displayState": "show",
|
||||
},
|
||||
"classes": "supports strong",
|
||||
},
|
||||
],
|
||||
"hidden_elements": [],
|
||||
}
|
||||
|
||||
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
||||
|
||||
|
||||
def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
store = RegistryStore(tmp_path / "registry.sqlite3")
|
||||
store.init_schema()
|
||||
store.upsert_repository(
|
||||
{
|
||||
"slug": "railiance-fabric",
|
||||
"name": "Railiance Fabric",
|
||||
"state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05",
|
||||
}
|
||||
)
|
||||
store.upsert_repository({"slug": "registered-only", "name": "Registered Only"})
|
||||
store.add_snapshot(
|
||||
"railiance-fabric",
|
||||
{
|
||||
"commit": "test-commit",
|
||||
"generated_at": "2026-05-18T00:00:00Z",
|
||||
"graph": build_graph([Path(".")]).to_export(),
|
||||
},
|
||||
)
|
||||
|
||||
class Handler(RegistryHandler):
|
||||
pass
|
||||
|
||||
Handler.store = store
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
base_url = f"http://127.0.0.1:{server.server_port}"
|
||||
with urllib.request.urlopen(
|
||||
f"{base_url}/exports/graph-explorer/manifest",
|
||||
timeout=5,
|
||||
) as response:
|
||||
manifest = json.loads(response.read())
|
||||
with urllib.request.urlopen(f"{base_url}/exports/graph-explorer", timeout=5) as response:
|
||||
payload = json.loads(response.read())
|
||||
|
||||
_validate_schema("graph-explorer-manifest.schema.yaml", manifest)
|
||||
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
||||
assert manifest["id"] == "railiance-fabric.registry-map"
|
||||
assert payload["metrics"]["registered_only_repo_count"] == 1
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def _validate_schema(schema_name: str, payload: dict[str, object]) -> None:
|
||||
validator = draft202012_validator(Path("schemas") / schema_name)
|
||||
validator.validate(payload)
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "Interactive Fabric Map"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: proposed
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -132,7 +132,7 @@ Candidate extraction shape:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0008-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9844a9a7-f285-4523-a8d6-4ca62008ce08"
|
||||
```
|
||||
@@ -158,7 +158,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0008-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "cb0cc9f1-5225-47e5-8b47-a945c92e7168"
|
||||
```
|
||||
@@ -184,7 +184,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0008-T03
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: high
|
||||
state_hub_task_id: "ecd967fc-05ed-4cda-bca2-cf74e26e60b3"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user