Files
semantica/explorer/tests/graphStore.multi-edge.test.mjs
T
KaifAhmad1andClaude Sonnet 4.6 3ea1283626 feat(explorer): add Semantica Knowledge Explorer UI with full feature set
## Folder & Project
- Renamed `semantica-explorer/` → `explorer/` (cleaner path)
- Browser tab title: `Semantica Knowledge Explorer`
- Brand pill: `SEM` → `SKE` (tooltip: Semantica Knowledge Explorer)
- Nav rail label: `Explore` → `Knowledge Explorer`
- package.json name: `semantica-knowledge-explorer`
- Downgraded Vite 8 → Vite 5 for Node v20.17.0 compatibility

## App Shell
- Dynamic per-workspace kicker labels replacing static "Workspace" pill:
  Graph Studio · Vocabulary Browser · Reasoning Engine · SPARQL Query ·
  Decision Intelligence · Knowledge Audit · Graph Governance

## Enrich Workspace — 2 new tabs
### Entity Resolution tab
- Similarity threshold slider (0.50–0.99)
- Run Dedup Scan → POST /api/enrich/dedup
- Flagged pairs list with colour-coded score bars (red/amber/green)
- Expandable inline diff: primary vs duplicate side-by-side
- One-click Merge → POST /api/enrich/merge with logEvent dispatch
- Dismiss per pair; Clear all button
- Merge history sidebar pulled live from Registry store

### Registry tab (Document Registry)
- Live chronological audit log of all KG mutations in-session
- Colour-coded op-type badges: IMPORT · MERGE · ADD NODE · ADD EDGE ·
  INFER · DELETE · EXPORT · VOCAB
- Filter pills to narrow by operation type
- Expandable JSON detail rows per entry
- Clear log button
- Entirely client-side via registryStore (no backend needed)

## Manage Workspace — 2 new tabs
### KG Overview tab
- Stats chips: total nodes, edges, graph density
- Node type breakdown bar chart (up to 8 types, colour-coded)
- Edge type breakdown bar chart from /api/graph/stats
- Top-10 most connected nodes ranked by degree
- Skeleton loading states + Refresh button

### Ontology Summary tab
- Read-only SKOS scheme tree (scheme → top concepts → narrower)
- Concept detail panel: labels, notation, description, narrower nav
- "Open Full Browser" button deep-links to Vocabulary Browser tab

## Decision Workspace polish
- CausalFlowDiagram: vertical node cards connected by relationship pills
- Outcome badges: colour-coded (green=approved, red=rejected, amber=deferred)
- Live filter input across decision ID, category, and outcome
- Animated skeleton loading while list fetches

## Graph Inspector polish
- PathFlowViz: clickable node chips connected by edge-type labels;
  clicking a chip focuses that node in the canvas
- Link Prediction button shows spinner while computing
- Empty states for path trace and candidate links sections

## Registry dispatch — WebSocket
- ADD_NODE events → logEvent("add-node", …) in GraphWorkspace WS handler
- ADD_EDGE events → logEvent("add-edge", …) in GraphWorkspace WS handler
- Import, Export, Merge already dispatched logEvent on API response

## Graph visibility overhaul
### Edge colours (were nearly transparent, now clearly visible)
- edgeBackbone:    rgba(…, 0.04)  → rgba(…, 0.38)
- edgeStructure:   rgba(…, 0.009) → rgba(…, 0.28)
- edgeInspection:  rgba(…, 0.026) → rgba(…, 0.48)
- Muted edges:     0.009–0.02    → 0.12–0.26
- Focus edges:     0.16          → 0.42

### Edge sizes
- default minSize: 0.18 → 0.9 (always at least 1 pixel wide)
- path minSize:    1.8  → 2.4
- inactive/muted:  hide:true → hide:false (dimmed not hidden)

### Node sizes
- default sizeMultiplier: 0.72 → 0.92
- default minSize:        0.68 → 3.5 (visible at all zoom levels)
- overview nodeScale:     0.66 → 0.88
- nodeTintMix (colour):   0.03 → 0.14
- nodeCoreMix (brightness): 0.52 → 0.72

### Label budget
- overview:   10  → 28 labels
- structure:  36  → 60 labels
- inspection: 80  → 120 labels

### Sigma settings
- renderEdgeLabels:        false → true  (relationship type on every edge)
- edgeLabelSize:           —    → 10
- labelRenderedSizeThreshold: 4 → 2
- labelDensity:            0.86 → 1.1
- hideLabelsOnMove:        true → false (labels stay visible while panning)
- hideEdgesOnMove:         true → false (edges stay visible while panning)
- minCameraRatio:          —    → 0.04 (prevents zooming inside a node)
- maxCameraRatio:          —    → 8    (graph stays visible when zoomed out)

### Zoom controls
- Added Zoom In (+) and Zoom Out (−) buttons to graph toolbar
- Smooth animated zoom via camera.animatedZoom / animatedUnzoom (200ms)
- Mouse scroll wheel clamped between minCameraRatio and maxCameraRatio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:40:40 +05:30

112 lines
3.6 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import Graph from "graphology";
import { curveGroupForPair, pairRegistryKey } from "../src/store/edgePairKeys.js";
function normalizeParallelMetadataForPair(graph, source, target) {
const edgeIds = [];
graph.forEachDirectedEdge(source, target, (edgeId) => {
edgeIds.push(String(edgeId));
});
const pairCount = edgeIds.length;
const familyCounts = new Map();
edgeIds.forEach((edgeId) => {
const attrs = graph.getEdgeAttributes(edgeId);
const familyId = String(attrs.familyId || edgeId);
familyCounts.set(familyId, (familyCounts.get(familyId) ?? 0) + 1);
});
edgeIds
.sort((left, right) => left.localeCompare(right))
.forEach((edgeId, index) => {
const attrs = graph.getEdgeAttributes(edgeId);
const familyId = String(attrs.familyId || edgeId);
graph.mergeEdgeAttributes(edgeId, {
edgeId,
familyId,
sourceId: source,
targetId: target,
isParallelPair: pairCount > 1,
parallelIndex: index,
parallelCount: pairCount,
familySize: familyCounts.get(familyId) ?? 1,
curveGroup: curveGroupForPair(source, target),
});
});
}
function batchMergeEdges(graph, edges) {
const touchedPairs = new Map();
for (const { id, familyId, source, target, attributes } of edges) {
const edgeId = String(attributes.edgeId || id);
const resolvedFamilyId = String(attributes.familyId || familyId || edgeId);
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.mergeDirectedEdgeWithKey(edgeId, source, target, {
...attributes,
edgeId,
familyId: resolvedFamilyId,
sourceId: source,
targetId: target,
});
touchedPairs.set(pairRegistryKey(source, target), { source, target });
}
}
touchedPairs.forEach(({ source, target }) => {
normalizeParallelMetadataForPair(graph, source, target);
});
}
test("structured pair keys support node ids containing double colons", () => {
const graph = new Graph({ type: "directed", multi: true, allowSelfLoops: false });
graph.addNode("gene/protein::10");
graph.addNode("gene/protein::472");
graph.addNode("gene/protein::500");
assert.doesNotThrow(() => {
batchMergeEdges(graph, [
{
id: "edge-a",
familyId: "family-1",
source: "gene/protein::10",
target: "gene/protein::472",
attributes: { edgeType: "protein_protein", weight: 1, properties: {} },
},
{
id: "edge-b",
familyId: "family-2",
source: "gene/protein::10",
target: "gene/protein::472",
attributes: { edgeType: "protein_protein", weight: 1, properties: {} },
},
{
id: "edge-c",
familyId: "family-3",
source: "gene/protein::10",
target: "gene/protein::500",
attributes: { edgeType: "protein_protein", weight: 1, properties: {} },
},
]);
});
const edgeA = graph.getEdgeAttributes("edge-a");
const edgeB = graph.getEdgeAttributes("edge-b");
const edgeC = graph.getEdgeAttributes("edge-c");
assert.equal(edgeA.sourceId, "gene/protein::10");
assert.equal(edgeA.targetId, "gene/protein::472");
assert.equal(edgeA.parallelCount, 2);
assert.equal(edgeB.parallelCount, 2);
assert.equal(edgeA.isParallelPair, true);
assert.equal(edgeB.isParallelPair, true);
assert.equal(edgeC.parallelCount, 1);
assert.equal(edgeC.isParallelPair, false);
assert.equal(edgeA.curveGroup, JSON.stringify(["gene/protein::10", "gene/protein::472"]));
assert.equal(edgeC.curveGroup, JSON.stringify(["gene/protein::10", "gene/protein::500"]));
});