graphlin 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +472 -0
- package/docs/visualizer-views.md +240 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +78 -9
- package/plugin.json +4 -2
- package/runtime/architecture/analysis.mjs +344 -0
- package/runtime/architecture/controller.mjs +209 -0
- package/runtime/architecture/evidence.mjs +108 -0
- package/runtime/architecture/profile.mjs +56 -0
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +81 -14
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +1020 -0
- package/runtime/model/records.mjs +240 -0
- package/runtime/pipeline.mjs +267 -55
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +154 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +38 -1
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +445 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +51 -0
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
package/runtime/web/app.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { layoutGraph, LAYOUT_ALGORITHMS } from './layout.js';
|
|
2
2
|
import { sketchOutline, sketchDetails, sketchConnection } from './sketch.js';
|
|
3
3
|
import { createLiveSidebar } from './sidebar.js';
|
|
4
|
+
import { createViewPlatform } from './platform.js';
|
|
5
|
+
import { layoutScene, sceneGraph, representedSelection } from './scene.js';
|
|
4
6
|
|
|
5
7
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
6
8
|
const MAX_JSON_BYTES = 8 * 1024 * 1024;
|
|
7
9
|
const LIMITS = Object.freeze({ nodes: 500, edges: 1500, activity: 200, hookEvents: 200, history: 100, refs: 32, sessions: 100 });
|
|
8
|
-
const ROLES = ['client', 'service', 'datastore', 'queue', 'external', 'module', 'function', 'class', 'interface', 'event', 'configuration', 'package'
|
|
10
|
+
const ROLES = ['client', 'service', 'datastore', 'queue', 'external', 'module', 'function', 'class', 'interface', 'event', 'configuration', 'package',
|
|
11
|
+
'method', 'namespace', 'enum', 'type_alias', 'variable', 'file', 'directory', 'project', 'unknown', 'group'];
|
|
9
12
|
export const SHAPE_NAMES = Object.freeze({
|
|
10
13
|
rounded_rect: 'Rounded rectangle', rect: 'Rectangle', cylinder: 'Cylinder', cloud: 'Cloud',
|
|
11
14
|
diamond: 'Diamond', group: 'Group', browser: 'Browser', component: 'Component',
|
|
@@ -22,6 +25,7 @@ const RELATIONS = ['calls', 'reads', 'writes', 'publishes', 'consumes', 'depends
|
|
|
22
25
|
const CLASSIFICATIONS = ['pending', 'accepted', 'tentative', 'abstained', 'stale'];
|
|
23
26
|
const EVIDENCE = ['proposed', 'observed', 'verified', 'removed'];
|
|
24
27
|
const VALIDITY = ['current', 'stale', 'retracted'];
|
|
28
|
+
const INTERPRETATION_BASES = ['jev_interpretation', 'decision_interpretation'];
|
|
25
29
|
const ACTIVITY = ['idle', 'pending', 'running', 'failed', 'interrupted', 'unknown'];
|
|
26
30
|
const EVENT_STATES = ['pending', 'succeeded', 'failed', 'interrupted', 'unresolved', 'observed'];
|
|
27
31
|
const CLASSIFIERS = ['ready', 'metadata_only', 'missing_key', 'paused', 'unavailable', 'timeout', 'demo'];
|
|
@@ -29,12 +33,15 @@ const ROLE_SHAPES = {
|
|
|
29
33
|
client: 'browser', service: 'component', datastore: 'cylinder', queue: 'queue', external: 'cloud',
|
|
30
34
|
module: 'rect', function: 'hexagon', class: 'class_box', interface: 'interface_box',
|
|
31
35
|
event: 'document', configuration: 'parallelogram', package: 'folder',
|
|
36
|
+
method: 'hexagon', namespace: 'folder', enum: 'class_box', type_alias: 'interface_box',
|
|
37
|
+
variable: 'rect', file: 'document', directory: 'folder', project: 'folder', unknown: 'rect', group: 'group',
|
|
32
38
|
};
|
|
33
39
|
const PROBABILITIES = ['supportProbability', 'roleProbability', 'roleConfidence', 'missingContextProbability'];
|
|
34
40
|
const NODE_WIDTH = 190;
|
|
35
41
|
const NODE_HEIGHT = 104;
|
|
36
42
|
const EDGE_LANE_GAP = 36;
|
|
37
|
-
const EDGE_RELATION_ORDER = ['calls', 'writes', 'depends_on', 'reads', 'publishes', 'consumes'
|
|
43
|
+
const EDGE_RELATION_ORDER = ['calls', 'writes', 'depends_on', 'reads', 'publishes', 'consumes',
|
|
44
|
+
'imports', 'references', 'member_of', 'hosted_by', 'contains', 'unknown'];
|
|
38
45
|
const LAYOUT_NAMES = {
|
|
39
46
|
hierarchy: 'Hierarchy top-down', dependency: 'Dependency left-right', grouped: 'Group by type',
|
|
40
47
|
circular: 'Circular', grid: 'Grid', original: 'Original', force: 'Force-directed',
|
|
@@ -229,7 +236,7 @@ function normalizeRefs(value, includeExcerpts = true) {
|
|
|
229
236
|
startLine: count(ref.startLine),
|
|
230
237
|
endLine: count(ref.endLine),
|
|
231
238
|
sourceClass: token(ref.sourceClass, ['source', 'public_intent'], 'unknown'),
|
|
232
|
-
basis: ref.basis
|
|
239
|
+
basis: token(ref.basis, INTERPRETATION_BASES, 'unknown'),
|
|
233
240
|
};
|
|
234
241
|
if (record(ref.sourceRef) && ref.sourceRef.type === 'artifact') {
|
|
235
242
|
result.sourceRef = {
|
|
@@ -420,16 +427,27 @@ export function claimSummary(claim) {
|
|
|
420
427
|
if (claim.validity === 'stale' || claim.classification === 'stale') {
|
|
421
428
|
return { tone: 'stale', label: 'Evidence stale', explanation: 'The backing evidence is no longer current. This interpretation needs reconciliation with the current artifact version.' };
|
|
422
429
|
}
|
|
430
|
+
if (claim.basis === 'parsed' && refs.length && !refs.some(ref => ref.sourceClass === 'public_intent')) {
|
|
431
|
+
return { tone: 'observed', label: 'Parsed source', explanation: 'A local source parser identified this structure. Source structure does not establish execution or runtime connectivity.' };
|
|
432
|
+
}
|
|
433
|
+
if (claim.basis === 'metadata') {
|
|
434
|
+
return { tone: 'proposed', label: 'Filesystem scope', explanation: 'An observed filesystem scope. Its responsibility and runtime role are unknown.' };
|
|
435
|
+
}
|
|
436
|
+
if (claim.basis === 'decision' && claim.classification === 'accepted' && refs.length) {
|
|
437
|
+
return { tone: 'observed', label: 'Supported interpretation', explanation: 'The model records a supported interpretation of the referenced evidence. This does not establish runtime hosting or execution.' };
|
|
438
|
+
}
|
|
423
439
|
if (claim.evidenceState === 'proposed' || (refs.length && refs.every(ref => ref.sourceClass === 'public_intent'))) {
|
|
424
440
|
return { tone: 'proposed', label: 'Proposed', explanation: 'This is a proposal or stated intent. It does not establish that a component exists or that a change completed.' };
|
|
425
441
|
}
|
|
426
442
|
if (claim.classification !== 'accepted') {
|
|
427
443
|
return { tone: 'proposed', label: upperFirst(claim.classification), explanation: 'The code interpretation is uncertain or incomplete. Inspect the evidence before relying on this claim.' };
|
|
428
444
|
}
|
|
429
|
-
if (!refs.length || refs.some(ref => ref.basis
|
|
445
|
+
if (!refs.length || refs.some(ref => !INTERPRETATION_BASES.includes(ref.basis) || ref.sourceClass === 'unknown')) {
|
|
430
446
|
return { tone: 'proposed', label: 'Provenance incomplete', explanation: 'This snapshot does not provide enough provenance to establish the basis of this claim.' };
|
|
431
447
|
}
|
|
432
|
-
|
|
448
|
+
const generic = refs.some(ref => ref.basis === 'decision_interpretation');
|
|
449
|
+
return { tone: 'observed', label: generic ? 'Decision interpretation' : 'Code evidence',
|
|
450
|
+
explanation: `${generic ? 'A decision provider' : 'Jev'} interpreted approved source evidence as supporting this claim. Code or configuration can describe a dependency without proving that it runs or connects successfully.` };
|
|
433
451
|
}
|
|
434
452
|
|
|
435
453
|
export function edgeLanes(edges) {
|
|
@@ -519,8 +537,8 @@ export function graphBounds(graph, routes = graphEdgeRoutes(graph)) {
|
|
|
519
537
|
if (!graph.nodes.length) return { x: 0, y: 0, width: 920, height: 510 };
|
|
520
538
|
let minX = Math.min(...graph.nodes.map(node => node.x));
|
|
521
539
|
let minY = Math.min(...graph.nodes.map(node => node.y));
|
|
522
|
-
let maxX = Math.max(...graph.nodes.map(node => node.x + NODE_WIDTH));
|
|
523
|
-
let maxY = Math.max(...graph.nodes.map(node => node.y + NODE_HEIGHT));
|
|
540
|
+
let maxX = Math.max(...graph.nodes.map(node => node.x + (node.width || NODE_WIDTH)));
|
|
541
|
+
let maxY = Math.max(...graph.nodes.map(node => node.y + (node.height || NODE_HEIGHT)));
|
|
524
542
|
for (const edge of graph.edges) {
|
|
525
543
|
const route = routes.get(edge.id);
|
|
526
544
|
if (!route) continue;
|
|
@@ -604,16 +622,16 @@ function curveRoute(points, normal) {
|
|
|
604
622
|
};
|
|
605
623
|
}
|
|
606
624
|
|
|
607
|
-
function nodePort(center, direction, normal, offset, shapeName) {
|
|
625
|
+
function nodePort(center, direction, normal, offset, shapeName, width = NODE_WIDTH, height = NODE_HEIGHT) {
|
|
608
626
|
const reach = Math.min(
|
|
609
|
-
direction.x === 0 ? Infinity :
|
|
610
|
-
direction.y === 0 ? Infinity :
|
|
627
|
+
direction.x === 0 ? Infinity : width / 2 / Math.abs(direction.x),
|
|
628
|
+
direction.y === 0 ? Infinity : height / 2 / Math.abs(direction.y),
|
|
611
629
|
);
|
|
612
630
|
const x = direction.x * reach + normal.x * offset;
|
|
613
631
|
const y = direction.y * reach + normal.y * offset;
|
|
614
632
|
const scale = Math.min(
|
|
615
|
-
x === 0 ? Infinity :
|
|
616
|
-
y === 0 ? Infinity :
|
|
633
|
+
x === 0 ? Infinity : width / 2 / Math.abs(x),
|
|
634
|
+
y === 0 ? Infinity : height / 2 / Math.abs(y),
|
|
617
635
|
);
|
|
618
636
|
const polygons = {
|
|
619
637
|
diamond: [[95, -12], [204, 52], [95, 116], [-14, 52]],
|
|
@@ -663,15 +681,15 @@ export function routeEdge(source, target, lane = 0) {
|
|
|
663
681
|
boundary({ x: x - 80, y: source.y + (side > 0 ? 0 : NODE_HEIGHT) }),
|
|
664
682
|
], { x: 0, y: side });
|
|
665
683
|
}
|
|
666
|
-
const a = { x: source.x + NODE_WIDTH / 2, y: source.y + NODE_HEIGHT / 2 };
|
|
667
|
-
const b = { x: target.x + NODE_WIDTH / 2, y: target.y + NODE_HEIGHT / 2 };
|
|
684
|
+
const a = { x: source.x + (source.width || NODE_WIDTH) / 2, y: source.y + (source.height || NODE_HEIGHT) / 2 };
|
|
685
|
+
const b = { x: target.x + (target.width || NODE_WIDTH) / 2, y: target.y + (target.height || NODE_HEIGHT) / 2 };
|
|
668
686
|
const distance = Math.hypot(b.x - a.x, b.y - a.y);
|
|
669
687
|
const direction = distance ? { x: (b.x - a.x) / distance, y: (b.y - a.y) / distance } : { x: 1, y: 0 };
|
|
670
688
|
const canonicalDirection = source.id <= target.id ? 1 : -1;
|
|
671
689
|
const normal = { x: -direction.y * canonicalDirection, y: direction.x * canonicalDirection };
|
|
672
690
|
const portOffset = Math.max(-36, Math.min(36, slot * 6));
|
|
673
|
-
const start = nodePort(a, direction, normal, portOffset, source.shape);
|
|
674
|
-
const end = nodePort(b, { x: -direction.x, y: -direction.y }, normal, portOffset, target.shape);
|
|
691
|
+
const start = nodePort(a, direction, normal, portOffset, source.shape, source.width, source.height);
|
|
692
|
+
const end = nodePort(b, { x: -direction.x, y: -direction.y }, normal, portOffset, target.shape, target.width, target.height);
|
|
675
693
|
const arc = slot * EDGE_LANE_GAP * 4 / 3;
|
|
676
694
|
const control = fraction => ({
|
|
677
695
|
x: start.x + (end.x - start.x) * fraction + normal.x * arc,
|
|
@@ -1565,6 +1583,7 @@ export function startViewer() {
|
|
|
1565
1583
|
views: new Map(), viewKey: null, view: null, displayGraph: null, searchQuery: '',
|
|
1566
1584
|
nodeTypes: null, nodeTypeButtons: new Map(),
|
|
1567
1585
|
effects: new Map(), liveReady: false, motionReady: false, movement: null, closed: false, projectName: '',
|
|
1586
|
+
model: null, scene: null, projectedGraph: null, platformActive: false, custom: false, follow: true, viewName: 'Code',
|
|
1568
1587
|
};
|
|
1569
1588
|
const motionPreference = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
1570
1589
|
const sketches = createSketchCache();
|
|
@@ -1597,6 +1616,72 @@ export function startViewer() {
|
|
|
1597
1616
|
let pointer = null;
|
|
1598
1617
|
let canvasSize = '';
|
|
1599
1618
|
let projectController = null;
|
|
1619
|
+
const platform = createViewPlatform({
|
|
1620
|
+
document, request,
|
|
1621
|
+
onFollow(value) { state.follow = value; if (!value) { clearMotion(); state.followFit = false; } },
|
|
1622
|
+
onSelect({ entityId, relationId, activityId }) {
|
|
1623
|
+
if (entityId) {
|
|
1624
|
+
platform.selected(entityId);
|
|
1625
|
+
select('node', state.scene ? representedSelection(entityId, state.scene, state.model) || entityId : entityId);
|
|
1626
|
+
revealInspector();
|
|
1627
|
+
} else if (relationId) {
|
|
1628
|
+
const edge = state.scene?.edges.find(edge => edge.relationIds?.includes(relationId));
|
|
1629
|
+
if (edge) select('edge', edge.id);
|
|
1630
|
+
} else if (activityId) {
|
|
1631
|
+
const event = state.model?.activity.find(event => event.id === activityId);
|
|
1632
|
+
if (event) {
|
|
1633
|
+
$('inspector-body').replaceChildren(html('h3', readable(event.kind || 'Activity')),
|
|
1634
|
+
html('p', `Outcome: ${event.outcome || 'unresolved'}. Attribution: ${event.attribution || 'unknown'}.`),
|
|
1635
|
+
html('p', 'This observation has no linked source entity.'));
|
|
1636
|
+
revealInspector();
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
},
|
|
1640
|
+
onView(result) {
|
|
1641
|
+
if (state.closed) return;
|
|
1642
|
+
if (result.clear) {
|
|
1643
|
+
state.scene = null; state.projectedGraph = null; state.custom = false;
|
|
1644
|
+
state.displayGraph = null; state.selection = null; state.inspectorSignature = '';
|
|
1645
|
+
clearMotion();
|
|
1646
|
+
for (const layer of ['group-layer', 'node-layer', 'edge-layer', 'edge-label-layer']) $(layer)?.replaceChildren();
|
|
1647
|
+
state.nodeElements.clear(); state.edgeElements.clear();
|
|
1648
|
+
$('architecture').hidden = true; $('architecture').setAttribute('aria-hidden', 'true');
|
|
1649
|
+
$('custom-view').hidden = true; $('empty-canvas').hidden = true;
|
|
1650
|
+
$('inspector-body').replaceChildren(html('p', 'Select an item in the active view to inspect its evidence.'));
|
|
1651
|
+
updateControls();
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const previous = state.projectedGraph, previousModel = state.model;
|
|
1655
|
+
const switched = !state.platformActive || state.viewName !== result.name || previousModel?.projectId !== result.model.projectId;
|
|
1656
|
+
state.platformActive = true; state.model = result.model; state.scene = result.scene || null;
|
|
1657
|
+
state.custom = result.kind === 'custom'; state.customCount = result.itemCount; state.viewName = result.name;
|
|
1658
|
+
state.projectedGraph = result.scene ? sceneGraph(result.scene, result.model) : null;
|
|
1659
|
+
$('architecture').hidden = state.custom; $('custom-view').hidden = !state.custom;
|
|
1660
|
+
$('architecture').setAttribute('aria-hidden', String(state.custom));
|
|
1661
|
+
if (switched) resetMotionBaseline();
|
|
1662
|
+
const selectionId = state.scene && representedSelection(result.selection, state.scene, state.model);
|
|
1663
|
+
if (selectionId) state.selection = { type: 'node', id: selectionId };
|
|
1664
|
+
const live = result.streamed && !switched && state.follow;
|
|
1665
|
+
const arrivals = liveNodeChanges(previous, state.projectedGraph, live);
|
|
1666
|
+
const focusNodeId = live && state.scene && result.focusEntityId
|
|
1667
|
+
? representedSelection(result.focusEntityId, state.scene, state.model) : arrivals.added.at(-1);
|
|
1668
|
+
const before = state.displayGraph;
|
|
1669
|
+
render({ forceFit: result.force || switched, focusNodeId });
|
|
1670
|
+
if (!state.custom && state.scene && !state.scene.groups.length) animateChanges(arrivals, before, focusNodeId);
|
|
1671
|
+
const coverage = result.scene?.coverage;
|
|
1672
|
+
$('view-coverage').hidden = false;
|
|
1673
|
+
const counts = state.model.coverage?.counts || state.model.coverage || {};
|
|
1674
|
+
const progress = ['inventoried', 'inspected', 'deferred', 'unsupported', 'unavailable']
|
|
1675
|
+
.filter(key => Number.isSafeInteger(counts[key])).map(key => `${counts[key]} ${key}`).join(' · ');
|
|
1676
|
+
$('view-coverage').textContent = [coverage?.label || 'Observable activity; outcomes may be unresolved',
|
|
1677
|
+
coverage?.truncated ? `${coverage.shown} shown of ${coverage.total}; open a scope for more` : '',
|
|
1678
|
+
state.model.coverage?.client?.truncated
|
|
1679
|
+
? `Partial scope: ${Object.entries(state.model.coverage.client.totals)
|
|
1680
|
+
.filter(([kind, total]) => total > state.model.coverage.client.retained[kind])
|
|
1681
|
+
.map(([kind, total]) => `${state.model.coverage.client.retained[kind]} of ${total} ${kind}`).join(', ')}. Open a source scope for more.` : '',
|
|
1682
|
+
progress].filter(Boolean).join(' · ');
|
|
1683
|
+
},
|
|
1684
|
+
});
|
|
1600
1685
|
|
|
1601
1686
|
function announce(message) {
|
|
1602
1687
|
clearTimeout(announcementTimer);
|
|
@@ -1640,14 +1725,15 @@ export function startViewer() {
|
|
|
1640
1725
|
if ($('orientation-prompt').textContent !== ORIENTATION_PROMPT) $('orientation-prompt').textContent = ORIENTATION_PROMPT;
|
|
1641
1726
|
$('orientation').hidden = Boolean(state.searchQuery || state.nodeTypes !== null || state.replayFrame || state.snapshot?.mode === 'demo' || state.snapshot?.mode === 'replay');
|
|
1642
1727
|
}
|
|
1643
|
-
function currentGraph() { return state.
|
|
1728
|
+
function currentGraph() { return state.platformActive ? state.projectedGraph || { revision: state.model?.revision || 0, nodes: [], edges: [] }
|
|
1729
|
+
: state.replayFrame?.graph || state.snapshot?.graph; }
|
|
1644
1730
|
function applyTheme() {
|
|
1645
1731
|
const theme = token(state.view?.theme, THEMES, 'sketchbook');
|
|
1646
1732
|
if ($('drawing').dataset.theme !== theme) $('drawing').dataset.theme = theme;
|
|
1647
1733
|
$('theme').value = theme;
|
|
1648
1734
|
}
|
|
1649
1735
|
function presentation() {
|
|
1650
|
-
const key = presentationKey(state.snapshot, state.replayFrame);
|
|
1736
|
+
const key = presentationKey(state.snapshot, state.replayFrame) + (state.platformActive ? `:${platform.active}:${JSON.stringify(platform.selection)}` : '');
|
|
1651
1737
|
if (state.viewKey !== key) {
|
|
1652
1738
|
finishPan();
|
|
1653
1739
|
clearMotion();
|
|
@@ -1774,6 +1860,8 @@ export function startViewer() {
|
|
|
1774
1860
|
$('theme').disabled = !state.snapshot;
|
|
1775
1861
|
$('arrange').disabled = !nodes;
|
|
1776
1862
|
$('auto-arrange').disabled = !state.snapshot;
|
|
1863
|
+
if (state.custom) for (const id of ['fit', 'zoom-in', 'zoom-out', 'arrange', 'layout', 'auto-arrange']) $(id).disabled = true;
|
|
1864
|
+
if (state.platformActive) { $('replay').disabled = true; $('history').disabled = true; }
|
|
1777
1865
|
}
|
|
1778
1866
|
function resetView() {
|
|
1779
1867
|
resetMotionBaseline();
|
|
@@ -1790,12 +1878,12 @@ export function startViewer() {
|
|
|
1790
1878
|
const snapshot = normalizeSnapshot(raw);
|
|
1791
1879
|
if (!streamed) resetMotionBaseline();
|
|
1792
1880
|
const switched = state.snapshot && (snapshot.sessionId !== state.snapshot.sessionId || snapshot.projectId !== state.snapshot.projectId);
|
|
1793
|
-
const eligible = streamed && state.motionReady && !switched && !state.replayFrame &&
|
|
1881
|
+
const eligible = !state.platformActive && state.follow && streamed && state.motionReady && !switched && !state.replayFrame &&
|
|
1794
1882
|
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay' && motionAllowed();
|
|
1795
1883
|
const changes = liveNodeChanges(state.snapshot?.graph, snapshot.graph, eligible);
|
|
1796
1884
|
// A live baseline is independent of animation preferences. Reconnects and
|
|
1797
1885
|
// initial/session snapshots establish it without focusing an old arrival.
|
|
1798
|
-
const live = streamed && state.liveReady && !switched && !state.replayFrame &&
|
|
1886
|
+
const live = !state.platformActive && state.follow && streamed && state.liveReady && !switched && !state.replayFrame &&
|
|
1799
1887
|
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay';
|
|
1800
1888
|
const visible = new Set(filterDiagram(snapshot.graph, state.searchQuery, state.nodeTypes).nodes.map(node => node.id));
|
|
1801
1889
|
const focusNodeId = liveNodeChanges(state.snapshot?.graph, snapshot.graph, live).added.filter(id => visible.has(id)).at(-1);
|
|
@@ -1848,7 +1936,7 @@ export function startViewer() {
|
|
|
1848
1936
|
$('session').replaceChildren(...options);
|
|
1849
1937
|
$('session').dataset.signature = signature;
|
|
1850
1938
|
}
|
|
1851
|
-
$('session').value = snapshot.sessionId || '';
|
|
1939
|
+
$('session').value = state.platformActive ? platform.selection.session || snapshot.sessionId || '' : snapshot.sessionId || '';
|
|
1852
1940
|
}
|
|
1853
1941
|
function shape(node) {
|
|
1854
1942
|
const details = detailSketches.paths(node.shape, node.id);
|
|
@@ -1928,6 +2016,10 @@ export function startViewer() {
|
|
|
1928
2016
|
}
|
|
1929
2017
|
function select(type, id) {
|
|
1930
2018
|
state.selection = { type, id };
|
|
2019
|
+
if (type === 'node' && state.platformActive) {
|
|
2020
|
+
const node = currentGraph()?.nodes.find(node => node.id === id);
|
|
2021
|
+
if (node?.entityId) platform.selected(node.entityId);
|
|
2022
|
+
}
|
|
1931
2023
|
updateSelection();
|
|
1932
2024
|
renderInspector();
|
|
1933
2025
|
renderActivity();
|
|
@@ -2006,8 +2098,8 @@ export function startViewer() {
|
|
|
2006
2098
|
const width = state.viewport.width * scale;
|
|
2007
2099
|
const height = state.viewport.height * scale;
|
|
2008
2100
|
state.viewport = {
|
|
2009
|
-
x: node.x + NODE_WIDTH / 2 - width / 2,
|
|
2010
|
-
y: node.y + NODE_HEIGHT / 2 - height / 2,
|
|
2101
|
+
x: node.x + (node.width || NODE_WIDTH) / 2 - width / 2,
|
|
2102
|
+
y: node.y + (node.height || NODE_HEIGHT) / 2 - height / 2,
|
|
2011
2103
|
width, height,
|
|
2012
2104
|
};
|
|
2013
2105
|
state.zoom = zoom;
|
|
@@ -2062,10 +2154,18 @@ export function startViewer() {
|
|
|
2062
2154
|
if (!canonical) return;
|
|
2063
2155
|
const view = presentation();
|
|
2064
2156
|
applyTheme();
|
|
2065
|
-
|
|
2157
|
+
if (state.platformActive && !state.scene) {
|
|
2158
|
+
$('canvas-title').textContent = state.viewName;
|
|
2159
|
+
$('graph-count').textContent = state.custom && Number.isSafeInteger(state.customCount) ? `${state.customCount} items` : '';
|
|
2160
|
+
$('revision').textContent = `Revision ${state.model.revision}`;
|
|
2161
|
+
$('empty-canvas').hidden = true;
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
renderNodeTypeFilters(state.platformActive ? { nodes: state.model.entities } : canonical);
|
|
2066
2165
|
// Lay out only visible nodes so hidden components leave no empty slots.
|
|
2067
2166
|
// Evidence, exports and live arrival detection retain the canonical graph.
|
|
2068
|
-
const graph =
|
|
2167
|
+
const graph = state.scene?.groups.length ? sceneGraph(layoutScene(state.scene), state.model)
|
|
2168
|
+
: projectPresentation(state.scene ? canonical : filterDiagram(canonical, state.searchQuery, state.nodeTypes), view, { arrange });
|
|
2069
2169
|
state.displayGraph = graph;
|
|
2070
2170
|
const routes = graphEdgeRoutes(graph);
|
|
2071
2171
|
const bounds = graphBounds(graph, routes);
|
|
@@ -2074,7 +2174,7 @@ export function startViewer() {
|
|
|
2074
2174
|
// diagram changes retain fit-all; metadata-only updates keep the camera.
|
|
2075
2175
|
const newest = graph.nodes.find(node => node.id === focusNodeId);
|
|
2076
2176
|
if (newest && state.viewport && !forceFit) focusNode(newest, bounds);
|
|
2077
|
-
else if (forceFit || !state.viewport || signature !== state.lastGraphSignature) fitCamera(bounds);
|
|
2177
|
+
else if (forceFit || !state.viewport || (state.follow && signature !== state.lastGraphSignature)) fitCamera(bounds);
|
|
2078
2178
|
state.lastGraphSignature = signature;
|
|
2079
2179
|
$('layout').value = view.algorithm;
|
|
2080
2180
|
$('auto-arrange').checked = view.auto;
|
|
@@ -2129,6 +2229,35 @@ export function startViewer() {
|
|
|
2129
2229
|
}
|
|
2130
2230
|
for (const node of graph.nodes) {
|
|
2131
2231
|
let group = state.nodeElements.get(node.id);
|
|
2232
|
+
if (group && Boolean(group.isSceneGroup) !== Boolean(node.isGroup)) { group.remove(); state.nodeElements.delete(node.id); group = null; }
|
|
2233
|
+
if (node.isGroup) {
|
|
2234
|
+
if (!group) {
|
|
2235
|
+
group = interactiveGroup('node', node.id);
|
|
2236
|
+
group.isSceneGroup = true;
|
|
2237
|
+
group.setAttribute('class', 'diagram-group');
|
|
2238
|
+
state.nodeElements.set(node.id, group);
|
|
2239
|
+
$('group-layer').append(group);
|
|
2240
|
+
}
|
|
2241
|
+
group.setAttribute('transform', `translate(${node.x} ${node.y})`);
|
|
2242
|
+
group.setAttribute('aria-label', `${node.label}. ${node.memberCount} members, ${node.activityCount} with activity. ${node.collapsed ? 'Collapsed' : 'Expanded'}. Inspect evidence.`);
|
|
2243
|
+
group.dataset.change = node.style || 'default';
|
|
2244
|
+
const toggle = svgElement('g', { class: 'group-toggle', role: 'button', tabindex: 0,
|
|
2245
|
+
'aria-label': `${node.collapsed ? 'Expand' : 'Collapse'} ${node.label}`,
|
|
2246
|
+
'aria-expanded': String(!node.collapsed), transform: `translate(${node.width - 33} 10)` });
|
|
2247
|
+
toggle.append(svgElement('rect', { width: 24, height: 24, rx: 4 }),
|
|
2248
|
+
svgElement('text', { x: 12, y: 18, 'text-anchor': 'middle' }, node.collapsed ? '+' : '−'));
|
|
2249
|
+
const toggleGroup = event => { event.stopPropagation(); platform.toggle(node.id, node.collapsed); };
|
|
2250
|
+
toggle.addEventListener('click', toggleGroup);
|
|
2251
|
+
toggle.addEventListener('keydown', event => {
|
|
2252
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleGroup(event); }
|
|
2253
|
+
});
|
|
2254
|
+
const restoreToggleFocus = group.contains(document.activeElement) && document.activeElement !== group;
|
|
2255
|
+
group.replaceChildren(svgElement('rect', { class: 'group-frame', width: node.width, height: node.height, rx: 5 }),
|
|
2256
|
+
svgElement('text', { class: 'group-heading', x: 14, y: 27 }, clip(node.label, Math.max(15, Math.floor((node.width - 70) / 8)))),
|
|
2257
|
+
svgElement('text', { class: 'group-summary', x: 14, y: 47 }, `${node.memberCount} members · ${node.activityCount} with activity`), toggle);
|
|
2258
|
+
if (restoreToggleFocus) toggle.focus({ preventScroll: true });
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2132
2261
|
if (!group) {
|
|
2133
2262
|
group = interactiveGroup('node', node.id);
|
|
2134
2263
|
const center = svgElement('g', { transform: `translate(${NODE_WIDTH / 2} ${NODE_HEIGHT / 2})` });
|
|
@@ -2151,6 +2280,7 @@ export function startViewer() {
|
|
|
2151
2280
|
group.setAttribute('transform', `translate(${node.x} ${node.y})`);
|
|
2152
2281
|
group.dataset.shape = node.shape;
|
|
2153
2282
|
group.dataset.kind = node.kind;
|
|
2283
|
+
group.dataset.change = node.style || 'default';
|
|
2154
2284
|
group.setAttribute('aria-label', `${node.label}. ${upperFirst(node.kind)}. ${summary.label}. Activity ${node.activityState}. Inspect evidence.`);
|
|
2155
2285
|
const nodeSignature = JSON.stringify([node.label, node.shape, node.kind, node.activityState, summary]);
|
|
2156
2286
|
if (group.renderSignature === nodeSignature) continue;
|
|
@@ -2182,7 +2312,7 @@ export function startViewer() {
|
|
|
2182
2312
|
}
|
|
2183
2313
|
updateSelection();
|
|
2184
2314
|
$('revision').textContent = `Revision ${graph.revision}`;
|
|
2185
|
-
$('canvas-title').textContent = state.replayFrame ? 'Architecture replay' : 'Live architecture';
|
|
2315
|
+
$('canvas-title').textContent = state.platformActive ? state.viewName : state.replayFrame ? 'Architecture replay' : 'Live architecture';
|
|
2186
2316
|
$('diagram-title').textContent = `${state.replayFrame ? 'Historical' : 'Live'} architecture, revision ${graph.revision}`;
|
|
2187
2317
|
$('diagram-desc').textContent = `${graph.nodes.length} components and ${graph.edges.length} relationships. Code interpretation does not establish runtime connectivity. Use Tab and Enter to inspect a component or relationship. With the diagram focused, use plus and minus to zoom, arrow keys to pan, and 0 to fit.`;
|
|
2188
2318
|
$('graph-count').textContent = `${graph.nodes.length} components · ${graph.edges.length} relationships`;
|
|
@@ -2286,7 +2416,11 @@ export function startViewer() {
|
|
|
2286
2416
|
const body = $('inspector-body');
|
|
2287
2417
|
const graph = currentGraph();
|
|
2288
2418
|
const selected = state.selection;
|
|
2289
|
-
|
|
2419
|
+
let claim = selected && graph ? (selected.type === 'node' ? graph.nodes : graph.edges).find(item => item.id === selected.id) : null;
|
|
2420
|
+
if (!claim && state.platformActive && selected?.type === 'node') {
|
|
2421
|
+
const entity = state.model.entities.find(entity => entity.id === selected.id);
|
|
2422
|
+
if (entity) claim = sceneGraph({ groups: [], edges: [], nodes: [{ id: entity.id, entityId: entity.id, label: entity.label, kind: entity.kind }] }, state.model).nodes[0];
|
|
2423
|
+
}
|
|
2290
2424
|
const linkedIds = new Set(claim?.sourceRefs.map(ref => ref.eventId) || []);
|
|
2291
2425
|
const related = state.snapshot?.activity.filter(event => linkedIds.has(event.id)).slice(-5).reverse() || [];
|
|
2292
2426
|
const override = selected?.type === 'node' ? state.view?.shapes.get(selected.id) : undefined;
|
|
@@ -2315,7 +2449,9 @@ export function startViewer() {
|
|
|
2315
2449
|
fact(facts, 'Classification', upperFirst(claim.classification));
|
|
2316
2450
|
fact(facts, 'Validity', upperFirst(claim.validity));
|
|
2317
2451
|
fact(facts, 'Evidence state', claim.evidenceState === 'verified' ? 'Verification reported; scope unavailable' : upperFirst(claim.evidenceState));
|
|
2318
|
-
|
|
2452
|
+
const interpreted = claim.sourceRefs.length && claim.sourceRefs.every(ref => INTERPRETATION_BASES.includes(ref.basis));
|
|
2453
|
+
const interpretationLabel = claim.sourceRefs.some(ref => ref.basis === 'decision_interpretation') ? 'Decision interpretation' : 'Jev code interpretation';
|
|
2454
|
+
fact(facts, 'Basis', claim.basis ? upperFirst(claim.basis) : interpreted ? interpretationLabel : 'Provenance incomplete');
|
|
2319
2455
|
fact(facts, 'Runtime', 'Not established by this snapshot');
|
|
2320
2456
|
if (selected.type === 'node') fact(facts, 'Activity', upperFirst(claim.activityState));
|
|
2321
2457
|
else {
|
|
@@ -2323,6 +2459,14 @@ export function startViewer() {
|
|
|
2323
2459
|
fact(facts, 'To', graph.nodes.find(node => node.id === claim.target)?.label || 'Unknown component');
|
|
2324
2460
|
}
|
|
2325
2461
|
body.replaceChildren(type, html('h3', claim.label), badges, html('p', summary.explanation), facts);
|
|
2462
|
+
if (state.platformActive && selected.type === 'node') {
|
|
2463
|
+
fact(facts, 'Change', readable(claim.style || 'No comparison'));
|
|
2464
|
+
if (claim.memberCount) fact(facts, 'Members', String(claim.memberCount));
|
|
2465
|
+
const openScope = html('button', 'Open source scope');
|
|
2466
|
+
openScope.setAttribute('type', 'button');
|
|
2467
|
+
openScope.addEventListener('click', () => platform.scope(claim.entityId || claim.id));
|
|
2468
|
+
body.append(openScope);
|
|
2469
|
+
} else if (claim.relationIds?.length) fact(facts, 'Supporting relations', claim.relationIds.join(', '));
|
|
2326
2470
|
if (selected.type === 'node') {
|
|
2327
2471
|
const label = html('label', 'Shape · visual only', 'shape-picker');
|
|
2328
2472
|
label.setAttribute('for', 'display-shape');
|
|
@@ -2356,36 +2500,38 @@ export function startViewer() {
|
|
|
2356
2500
|
announce(`Display shape changed. ${claim.label} remains classified as ${claim.kind}.`);
|
|
2357
2501
|
});
|
|
2358
2502
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2503
|
+
if (!['parsed', 'metadata', 'lexical'].includes(claim.basis)) {
|
|
2504
|
+
const confidence = normalizeConfidence(claim.confidence);
|
|
2505
|
+
body.append(html('h4', 'Classifier confidence'));
|
|
2506
|
+
const confidenceList = html('ul', undefined, 'confidence-list');
|
|
2507
|
+
const confidenceLabels = {
|
|
2508
|
+
supportProbability: 'Evidence support probability',
|
|
2509
|
+
roleProbability: 'Selected role probability',
|
|
2510
|
+
roleConfidence: 'Role distribution confidence',
|
|
2511
|
+
missingContextProbability: 'Missing-context probability',
|
|
2512
|
+
reportedConfidence: 'Reported classifier confidence',
|
|
2513
|
+
};
|
|
2514
|
+
for (const key of Object.keys(confidenceLabels)) {
|
|
2515
|
+
if (!probability(confidence[key])) continue;
|
|
2516
|
+
const item = html('li');
|
|
2517
|
+
item.append(html('span', confidenceLabels[key]), html('strong', `${(confidence[key] * 100).toFixed(1)}%`));
|
|
2518
|
+
confidenceList.append(item);
|
|
2519
|
+
}
|
|
2520
|
+
if (confidenceList.childElementCount) body.append(confidenceList);
|
|
2521
|
+
else body.append(html('p', 'Confidence was not supplied for this claim.', 'fine-print'));
|
|
2522
|
+
body.append(html('p', 'These values describe the classifier’s interpretation. They are not measured accuracy or the probability that a runtime connection succeeds.', 'fine-print'));
|
|
2523
|
+
if (record(confidence.roleProbabilities)) {
|
|
2524
|
+
const details = html('details');
|
|
2525
|
+
details.append(html('summary', 'Role probabilities'));
|
|
2526
|
+
const list = html('ul', undefined, 'confidence-list');
|
|
2527
|
+
for (const [role, value] of Object.entries(confidence.roleProbabilities)) {
|
|
2528
|
+
const row = html('li');
|
|
2529
|
+
row.append(html('span', upperFirst(role)), html('strong', `${(value * 100).toFixed(1)}%`));
|
|
2530
|
+
list.append(row);
|
|
2531
|
+
}
|
|
2532
|
+
details.append(list);
|
|
2533
|
+
body.append(details);
|
|
2386
2534
|
}
|
|
2387
|
-
details.append(list);
|
|
2388
|
-
body.append(details);
|
|
2389
2535
|
}
|
|
2390
2536
|
body.append(html('h4', `Source references (${claim.sourceRefs.length})`));
|
|
2391
2537
|
if (!claim.sourceRefs.length) body.append(html('p', 'No source references were supplied. This claim’s provenance cannot be inspected.', 'fine-print'));
|
|
@@ -2393,8 +2539,9 @@ export function startViewer() {
|
|
|
2393
2539
|
for (const ref of claim.sourceRefs) {
|
|
2394
2540
|
const item = html('li', undefined, 'source-reference');
|
|
2395
2541
|
item.append(
|
|
2396
|
-
html('strong', ref.sourceClass === 'public_intent' ? 'Public intent' : ref.sourceClass === 'source' ? 'Source artifact' : 'Unknown source class'),
|
|
2397
|
-
html('span',
|
|
2542
|
+
html('strong', ref.sourceClass === 'public_intent' ? 'Public intent' : ref.sourceClass === 'source' || (claim.basis === 'parsed' && ref.artifactId) ? 'Source artifact' : 'Unknown source class'),
|
|
2543
|
+
html('span', claim.basis ? `Basis: ${upperFirst(claim.basis)}` : ref.basis === 'decision_interpretation'
|
|
2544
|
+
? 'Basis: Decision interpretation' : ref.basis === 'jev_interpretation' ? 'Basis: Jev interpretation' : 'Basis not supplied'),
|
|
2398
2545
|
html('span', `${ref.sourceClass === 'public_intent' ? 'Message' : 'Artifact'}: ${ref.sourceRef?.messageId || ref.artifactId || 'not supplied'}`),
|
|
2399
2546
|
html('span', `Version: ${ref.hash || 'not supplied'} · ${ref.sourceClass === 'public_intent' ? 'content version' : 'generation'} ${ref.sourceRef?.contentVersion ?? ref.generation}`),
|
|
2400
2547
|
);
|
|
@@ -2427,6 +2574,16 @@ export function startViewer() {
|
|
|
2427
2574
|
}
|
|
2428
2575
|
}
|
|
2429
2576
|
function renderHistory() {
|
|
2577
|
+
if (state.platformActive) {
|
|
2578
|
+
const replay = Boolean(platform.selection.checkpoint);
|
|
2579
|
+
$('live').setAttribute('aria-pressed', String(!replay));
|
|
2580
|
+
$('replay').setAttribute('aria-pressed', String(replay));
|
|
2581
|
+
$('history-position').textContent = `Rev. ${state.model.revision}`;
|
|
2582
|
+
$('replay-note').textContent = replay ? 'Recorded checkpoint. Choose Live to return to current observations.'
|
|
2583
|
+
: 'Use Position to inspect a retained model checkpoint.';
|
|
2584
|
+
$('activity-note').textContent = 'This panel shows live capture. The timeline follows the selected model position.';
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2430
2587
|
const replay = Boolean(state.replayFrame);
|
|
2431
2588
|
const frameIndex = replay ? state.frames.findIndex(frame => frame.revision === state.replayFrame.revision) : state.frames.length - 1;
|
|
2432
2589
|
$('live').setAttribute('aria-pressed', String(!replay));
|
|
@@ -2572,6 +2729,7 @@ export function startViewer() {
|
|
|
2572
2729
|
connection('reconnecting');
|
|
2573
2730
|
error('The live connection was lost. Displaying the last received snapshot while the viewer reconnects.');
|
|
2574
2731
|
});
|
|
2732
|
+
void platform.start();
|
|
2575
2733
|
void dashboardInfo.refresh();
|
|
2576
2734
|
// Optional authenticated metadata must not hold up the event stream or
|
|
2577
2735
|
// turn an older server's missing endpoint into a connection failure.
|
|
@@ -2681,6 +2839,7 @@ export function startViewer() {
|
|
|
2681
2839
|
$('history-toggle').addEventListener('click', onToggleHistory);
|
|
2682
2840
|
$('activity-toggle').addEventListener('click', onToggleActivity);
|
|
2683
2841
|
const applyFilters = () => {
|
|
2842
|
+
if (state.platformActive) { platform.filter(state.searchQuery, state.nodeTypes); return; }
|
|
2684
2843
|
// Filtering is not a source change: cancel existing decoration/movement
|
|
2685
2844
|
// and render directly, without changing the snapshot arrival baseline.
|
|
2686
2845
|
finishPan();
|
|
@@ -2691,6 +2850,7 @@ export function startViewer() {
|
|
|
2691
2850
|
const onSearchInput = () => {
|
|
2692
2851
|
if (state.closed || state.searchQuery === $('diagram-search').value) return;
|
|
2693
2852
|
state.searchQuery = $('diagram-search').value;
|
|
2853
|
+
if (state.platformActive) { platform.filter(state.searchQuery, state.nodeTypes); return; }
|
|
2694
2854
|
applyFilters();
|
|
2695
2855
|
};
|
|
2696
2856
|
const onNodeTypeClick = event => {
|
|
@@ -2702,7 +2862,7 @@ export function startViewer() {
|
|
|
2702
2862
|
if (!kind || !state.nodeTypeButtons.has(kind)) return;
|
|
2703
2863
|
// null means all, including future kinds. Explicit selections retain their
|
|
2704
2864
|
// choices when a kind disappears and returns; new kinds stay unselected.
|
|
2705
|
-
if (state.nodeTypes === null) state.nodeTypes = new Set(currentGraph().nodes.map(node => node.kind));
|
|
2865
|
+
if (state.nodeTypes === null) state.nodeTypes = new Set((state.platformActive ? state.model.entities : currentGraph().nodes).map(node => node.kind));
|
|
2706
2866
|
if (state.nodeTypes.has(kind)) state.nodeTypes.delete(kind);
|
|
2707
2867
|
else state.nodeTypes.add(kind);
|
|
2708
2868
|
applyFilters();
|
|
@@ -2742,9 +2902,10 @@ export function startViewer() {
|
|
|
2742
2902
|
$('node-types-none')?.addEventListener('click', onNoNodeTypes);
|
|
2743
2903
|
window.addEventListener('keydown', onSearchKeyDown);
|
|
2744
2904
|
$('pause').addEventListener('click', () => control(state.snapshot?.paused ? 'resume' : 'pause'));
|
|
2745
|
-
$('session').addEventListener('change', () => control('session', $('session').value));
|
|
2905
|
+
$('session').addEventListener('change', () => state.platformActive ? platform.session($('session').value) : control('session', $('session').value));
|
|
2746
2906
|
$('export').addEventListener('click', exportJSON);
|
|
2747
2907
|
$('live').addEventListener('click', () => {
|
|
2908
|
+
if (state.platformActive) { platform.live(); return; }
|
|
2748
2909
|
resetMotionBaseline();
|
|
2749
2910
|
state.replayFrame = null;
|
|
2750
2911
|
if (state.snapshot) render();
|
|
@@ -2850,7 +3011,7 @@ export function startViewer() {
|
|
|
2850
3011
|
$('architecture').addEventListener('pointerup', finishPan);
|
|
2851
3012
|
$('architecture').addEventListener('pointercancel', finishPan);
|
|
2852
3013
|
$('architecture').addEventListener('lostpointercapture', finishPan);
|
|
2853
|
-
const onPageHide = () => { connectionDialog.close(); diagnosticsDialog.close(); resetMotionBaseline(); state.stream?.close(); state.stream = null; };
|
|
3014
|
+
const onPageHide = () => { connectionDialog.close(); diagnosticsDialog.close(); platform.suspend(); resetMotionBaseline(); state.stream?.close(); state.stream = null; };
|
|
2854
3015
|
const onPageShow = event => { if (!state.closed && event.persisted) connect(); };
|
|
2855
3016
|
const onOnline = () => { if (!state.closed && state.connection !== 'connected') connect(); };
|
|
2856
3017
|
const onVisibility = () => resetMotionBaseline();
|
|
@@ -2879,6 +3040,7 @@ export function startViewer() {
|
|
|
2879
3040
|
ready: connect(),
|
|
2880
3041
|
close() {
|
|
2881
3042
|
state.closed = true;
|
|
3043
|
+
platform.close();
|
|
2882
3044
|
projectController?.abort();
|
|
2883
3045
|
projectController = null;
|
|
2884
3046
|
dashboardInfo.close();
|