graphlin 0.1.2 → 0.2.0
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 +465 -0
- package/docs/visualizer-views.md +199 -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 +74 -9
- package/plugin.json +4 -2
- 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 +70 -12
- 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 +889 -0
- package/runtime/model/records.mjs +239 -0
- package/runtime/pipeline.mjs +127 -48
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +52 -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 +369 -86
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +123 -80
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +337 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +152 -142
- 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',
|
|
@@ -157,10 +164,10 @@ export function liveNodeChanges(previous, next, eligible) {
|
|
|
157
164
|
};
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
export function filterDiagram(graph, query) {
|
|
161
|
-
if (!query) return graph;
|
|
167
|
+
export function filterDiagram(graph, query = '', kinds = null) {
|
|
168
|
+
if (!query && kinds === null) return graph;
|
|
162
169
|
const needle = query.toLowerCase();
|
|
163
|
-
const nodes = graph.nodes.filter(node => node.label.toLowerCase().includes(needle));
|
|
170
|
+
const nodes = graph.nodes.filter(node => (kinds === null || kinds.has(node.kind)) && node.label.toLowerCase().includes(needle));
|
|
164
171
|
const visible = new Set(nodes.map(node => node.id));
|
|
165
172
|
return { ...graph, nodes, edges: graph.edges.filter(edge => visible.has(edge.source) && visible.has(edge.target)) };
|
|
166
173
|
}
|
|
@@ -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,
|
|
@@ -842,6 +860,7 @@ export function startDashboardInfo({ load = signal => request('/api/about', { si
|
|
|
842
860
|
throw new Error('invalid_dashboard_info');
|
|
843
861
|
}
|
|
844
862
|
$('project-path').textContent = safeText(info.projectRoot, 4096) || 'Path unavailable';
|
|
863
|
+
$('project-path').title = $('project-path').textContent;
|
|
845
864
|
$('graphlin-version').textContent = info.version;
|
|
846
865
|
$('version-update-steps').textContent = info.mode === 'demo'
|
|
847
866
|
? 'Press Ctrl+C in the demo terminal, then run this command to restart the updated offline demo.'
|
|
@@ -851,9 +870,14 @@ export function startDashboardInfo({ load = signal => request('/api/about', { si
|
|
|
851
870
|
? safeText(branch.name, 1024) || 'Branch unavailable'
|
|
852
871
|
: branch?.status === 'detached' ? `Detached HEAD${branch.commit ? ` · ${safeText(branch.commit, 12)}` : ''}`
|
|
853
872
|
: branch?.status === 'not_git' ? 'Not a Git repository' : 'Branch unavailable';
|
|
873
|
+
$('project-branch').title = $('project-branch').textContent;
|
|
854
874
|
const latest = typeof info.update?.latest === 'string' &&
|
|
855
875
|
/^\d+\.\d+\.\d+$/.test(info.update.latest) ? info.update.latest : null;
|
|
856
876
|
const available = info.update?.status === 'available' && latest;
|
|
877
|
+
$('version-update-indicator').hidden = !available;
|
|
878
|
+
$('version-update-indicator').textContent = available ? `${latest} available` : 'Update available';
|
|
879
|
+
$('version-update-indicator').setAttribute('aria-label', available
|
|
880
|
+
? `Graphlin ${latest} is available. View update instructions.` : 'View update instructions');
|
|
857
881
|
$('version-update-status').textContent = available ? `Graphlin ${latest} is available`
|
|
858
882
|
: info.update?.status === 'current' ? 'No newer release found'
|
|
859
883
|
: 'Update check unavailable';
|
|
@@ -884,6 +908,7 @@ export function startDashboardInfo({ load = signal => request('/api/about', { si
|
|
|
884
908
|
if ($('project-path').textContent === 'Checking…') $('project-path').textContent = 'Path unavailable';
|
|
885
909
|
if ($('graphlin-version').textContent === 'Checking…') $('graphlin-version').textContent = 'Version unavailable';
|
|
886
910
|
$('version-update-status').textContent = 'Update check unavailable';
|
|
911
|
+
$('version-update-indicator').hidden = true;
|
|
887
912
|
$('version-update-guide').hidden = true;
|
|
888
913
|
$('version-update-command').textContent = '';
|
|
889
914
|
command = '';
|
|
@@ -1556,7 +1581,9 @@ export function startViewer() {
|
|
|
1556
1581
|
viewport: null, fitBounds: null, zoom: 1, followFit: true, lastGraphSignature: '', inspectorSignature: '',
|
|
1557
1582
|
nodeElements: new Map(), edgeElements: new Map(), activityElements: new Map(),
|
|
1558
1583
|
views: new Map(), viewKey: null, view: null, displayGraph: null, searchQuery: '',
|
|
1584
|
+
nodeTypes: null, nodeTypeButtons: new Map(),
|
|
1559
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',
|
|
1560
1587
|
};
|
|
1561
1588
|
const motionPreference = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
1562
1589
|
const sketches = createSketchCache();
|
|
@@ -1589,6 +1616,72 @@ export function startViewer() {
|
|
|
1589
1616
|
let pointer = null;
|
|
1590
1617
|
let canvasSize = '';
|
|
1591
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
|
+
});
|
|
1592
1685
|
|
|
1593
1686
|
function announce(message) {
|
|
1594
1687
|
clearTimeout(announcementTimer);
|
|
@@ -1630,16 +1723,17 @@ export function startViewer() {
|
|
|
1630
1723
|
$('onboarding-action').dataset.action = progress.next.action;
|
|
1631
1724
|
// Preserve a manual text selection while live snapshots arrive.
|
|
1632
1725
|
if ($('orientation-prompt').textContent !== ORIENTATION_PROMPT) $('orientation-prompt').textContent = ORIENTATION_PROMPT;
|
|
1633
|
-
$('orientation').hidden = Boolean(state.searchQuery || state.replayFrame || state.snapshot?.mode === 'demo' || state.snapshot?.mode === 'replay');
|
|
1726
|
+
$('orientation').hidden = Boolean(state.searchQuery || state.nodeTypes !== null || state.replayFrame || state.snapshot?.mode === 'demo' || state.snapshot?.mode === 'replay');
|
|
1634
1727
|
}
|
|
1635
|
-
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; }
|
|
1636
1730
|
function applyTheme() {
|
|
1637
1731
|
const theme = token(state.view?.theme, THEMES, 'sketchbook');
|
|
1638
1732
|
if ($('drawing').dataset.theme !== theme) $('drawing').dataset.theme = theme;
|
|
1639
1733
|
$('theme').value = theme;
|
|
1640
1734
|
}
|
|
1641
1735
|
function presentation() {
|
|
1642
|
-
const key = presentationKey(state.snapshot, state.replayFrame);
|
|
1736
|
+
const key = presentationKey(state.snapshot, state.replayFrame) + (state.platformActive ? `:${platform.active}:${JSON.stringify(platform.selection)}` : '');
|
|
1643
1737
|
if (state.viewKey !== key) {
|
|
1644
1738
|
finishPan();
|
|
1645
1739
|
clearMotion();
|
|
@@ -1766,6 +1860,8 @@ export function startViewer() {
|
|
|
1766
1860
|
$('theme').disabled = !state.snapshot;
|
|
1767
1861
|
$('arrange').disabled = !nodes;
|
|
1768
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; }
|
|
1769
1865
|
}
|
|
1770
1866
|
function resetView() {
|
|
1771
1867
|
resetMotionBaseline();
|
|
@@ -1782,17 +1878,21 @@ export function startViewer() {
|
|
|
1782
1878
|
const snapshot = normalizeSnapshot(raw);
|
|
1783
1879
|
if (!streamed) resetMotionBaseline();
|
|
1784
1880
|
const switched = state.snapshot && (snapshot.sessionId !== state.snapshot.sessionId || snapshot.projectId !== state.snapshot.projectId);
|
|
1785
|
-
const eligible = streamed && state.motionReady && !switched && !state.replayFrame &&
|
|
1881
|
+
const eligible = !state.platformActive && state.follow && streamed && state.motionReady && !switched && !state.replayFrame &&
|
|
1786
1882
|
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay' && motionAllowed();
|
|
1787
1883
|
const changes = liveNodeChanges(state.snapshot?.graph, snapshot.graph, eligible);
|
|
1788
1884
|
// A live baseline is independent of animation preferences. Reconnects and
|
|
1789
1885
|
// initial/session snapshots establish it without focusing an old arrival.
|
|
1790
|
-
const live = streamed && state.liveReady && !switched && !state.replayFrame &&
|
|
1886
|
+
const live = !state.platformActive && state.follow && streamed && state.liveReady && !switched && !state.replayFrame &&
|
|
1791
1887
|
snapshot.mode !== 'replay' && state.snapshot?.mode !== 'replay';
|
|
1792
|
-
const
|
|
1888
|
+
const visible = new Set(filterDiagram(snapshot.graph, state.searchQuery, state.nodeTypes).nodes.map(node => node.id));
|
|
1889
|
+
const focusNodeId = liveNodeChanges(state.snapshot?.graph, snapshot.graph, live).added.filter(id => visible.has(id)).at(-1);
|
|
1793
1890
|
const before = state.displayGraph;
|
|
1794
1891
|
cancelMovement({ fit: !focusNodeId });
|
|
1795
|
-
if (switched)
|
|
1892
|
+
if (switched) {
|
|
1893
|
+
resetView();
|
|
1894
|
+
state.nodeTypes = null;
|
|
1895
|
+
}
|
|
1796
1896
|
state.snapshot = snapshot;
|
|
1797
1897
|
state.epoch += 1;
|
|
1798
1898
|
state.frames = historyFrames(snapshot);
|
|
@@ -1836,7 +1936,7 @@ export function startViewer() {
|
|
|
1836
1936
|
$('session').replaceChildren(...options);
|
|
1837
1937
|
$('session').dataset.signature = signature;
|
|
1838
1938
|
}
|
|
1839
|
-
$('session').value = snapshot.sessionId || '';
|
|
1939
|
+
$('session').value = state.platformActive ? platform.selection.session || snapshot.sessionId || '' : snapshot.sessionId || '';
|
|
1840
1940
|
}
|
|
1841
1941
|
function shape(node) {
|
|
1842
1942
|
const details = detailSketches.paths(node.shape, node.id);
|
|
@@ -1916,6 +2016,10 @@ export function startViewer() {
|
|
|
1916
2016
|
}
|
|
1917
2017
|
function select(type, id) {
|
|
1918
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
|
+
}
|
|
1919
2023
|
updateSelection();
|
|
1920
2024
|
renderInspector();
|
|
1921
2025
|
renderActivity();
|
|
@@ -1926,14 +2030,18 @@ export function startViewer() {
|
|
|
1926
2030
|
}
|
|
1927
2031
|
}
|
|
1928
2032
|
function revealInspector() {
|
|
2033
|
+
revealDetailsSection($('inspector-body')?.parentElement);
|
|
2034
|
+
}
|
|
2035
|
+
function revealDetailsSection(panel) {
|
|
2036
|
+
setWorkspacePanel('details', true);
|
|
1929
2037
|
const container = $('live-sidebar');
|
|
1930
|
-
const panel = $('inspector-body')?.parentElement;
|
|
1931
2038
|
if (!container?.scrollTo || !container.getBoundingClientRect || !panel?.getBoundingClientRect ||
|
|
1932
2039
|
!container.contains(panel)) return;
|
|
1933
2040
|
const region = container.getBoundingClientRect();
|
|
1934
2041
|
const evidence = panel.getBoundingClientRect();
|
|
1935
2042
|
if (!Number.isFinite(region.top) || !Number.isFinite(evidence.top) || !(region.height > 0)) return;
|
|
1936
|
-
const
|
|
2043
|
+
const headingHeight = $('details-heading')?.getBoundingClientRect?.().height || 0;
|
|
2044
|
+
const offset = evidence.top - region.top - (container.clientTop || 0) - headingHeight;
|
|
1937
2045
|
if (Math.abs(offset) < 1) return;
|
|
1938
2046
|
container.scrollTo({
|
|
1939
2047
|
top: Math.max(0, (container.scrollTop || 0) + offset),
|
|
@@ -1990,8 +2098,8 @@ export function startViewer() {
|
|
|
1990
2098
|
const width = state.viewport.width * scale;
|
|
1991
2099
|
const height = state.viewport.height * scale;
|
|
1992
2100
|
state.viewport = {
|
|
1993
|
-
x: node.x + NODE_WIDTH / 2 - width / 2,
|
|
1994
|
-
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,
|
|
1995
2103
|
width, height,
|
|
1996
2104
|
};
|
|
1997
2105
|
state.zoom = zoom;
|
|
@@ -2016,14 +2124,48 @@ export function startViewer() {
|
|
|
2016
2124
|
state.followFit = false;
|
|
2017
2125
|
setViewBox();
|
|
2018
2126
|
}
|
|
2019
|
-
function
|
|
2127
|
+
function renderNodeTypeFilters(canonical) {
|
|
2128
|
+
const container = $('node-type-filters');
|
|
2129
|
+
if (!container) return;
|
|
2130
|
+
// Discover kinds from the current canonical canvas, never search results.
|
|
2131
|
+
const kinds = [...new Set(canonical.nodes.map(node => node.kind))].sort();
|
|
2132
|
+
for (const [kind, button] of state.nodeTypeButtons) {
|
|
2133
|
+
if (!kinds.includes(kind)) {
|
|
2134
|
+
button.remove();
|
|
2135
|
+
state.nodeTypeButtons.delete(kind);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
kinds.forEach((kind, index) => {
|
|
2139
|
+
let button = state.nodeTypeButtons.get(kind);
|
|
2140
|
+
if (!button) {
|
|
2141
|
+
button = html('button', upperFirst(kind), 'node-type-filter');
|
|
2142
|
+
button.setAttribute('type', 'button');
|
|
2143
|
+
button.dataset.kind = kind;
|
|
2144
|
+
state.nodeTypeButtons.set(kind, button);
|
|
2145
|
+
}
|
|
2146
|
+
button.setAttribute('aria-pressed', String(state.nodeTypes === null || state.nodeTypes.has(kind)));
|
|
2147
|
+
if (container.children[index] !== button) container.insertBefore(button, container.children[index] || null);
|
|
2148
|
+
});
|
|
2149
|
+
$('node-types-all')?.setAttribute('aria-pressed', String(state.nodeTypes === null));
|
|
2150
|
+
$('node-types-none')?.setAttribute('aria-pressed', String(state.nodeTypes?.size === 0));
|
|
2151
|
+
}
|
|
2152
|
+
function renderGraph({ forceFit = false, arrange = false, focusNodeId } = {}) {
|
|
2020
2153
|
const canonical = currentGraph();
|
|
2021
2154
|
if (!canonical) return;
|
|
2022
2155
|
const view = presentation();
|
|
2023
2156
|
applyTheme();
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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);
|
|
2165
|
+
// Lay out only visible nodes so hidden components leave no empty slots.
|
|
2166
|
+
// Evidence, exports and live arrival detection retain the canonical 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 });
|
|
2027
2169
|
state.displayGraph = graph;
|
|
2028
2170
|
const routes = graphEdgeRoutes(graph);
|
|
2029
2171
|
const bounds = graphBounds(graph, routes);
|
|
@@ -2032,7 +2174,7 @@ export function startViewer() {
|
|
|
2032
2174
|
// diagram changes retain fit-all; metadata-only updates keep the camera.
|
|
2033
2175
|
const newest = graph.nodes.find(node => node.id === focusNodeId);
|
|
2034
2176
|
if (newest && state.viewport && !forceFit) focusNode(newest, bounds);
|
|
2035
|
-
else if (forceFit || !state.viewport || signature !== state.lastGraphSignature) fitCamera(bounds);
|
|
2177
|
+
else if (forceFit || !state.viewport || (state.follow && signature !== state.lastGraphSignature)) fitCamera(bounds);
|
|
2036
2178
|
state.lastGraphSignature = signature;
|
|
2037
2179
|
$('layout').value = view.algorithm;
|
|
2038
2180
|
$('auto-arrange').checked = view.auto;
|
|
@@ -2087,6 +2229,35 @@ export function startViewer() {
|
|
|
2087
2229
|
}
|
|
2088
2230
|
for (const node of graph.nodes) {
|
|
2089
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.entityId, 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
|
+
}
|
|
2090
2261
|
if (!group) {
|
|
2091
2262
|
group = interactiveGroup('node', node.id);
|
|
2092
2263
|
const center = svgElement('g', { transform: `translate(${NODE_WIDTH / 2} ${NODE_HEIGHT / 2})` });
|
|
@@ -2109,6 +2280,7 @@ export function startViewer() {
|
|
|
2109
2280
|
group.setAttribute('transform', `translate(${node.x} ${node.y})`);
|
|
2110
2281
|
group.dataset.shape = node.shape;
|
|
2111
2282
|
group.dataset.kind = node.kind;
|
|
2283
|
+
group.dataset.change = node.style || 'default';
|
|
2112
2284
|
group.setAttribute('aria-label', `${node.label}. ${upperFirst(node.kind)}. ${summary.label}. Activity ${node.activityState}. Inspect evidence.`);
|
|
2113
2285
|
const nodeSignature = JSON.stringify([node.label, node.shape, node.kind, node.activityState, summary]);
|
|
2114
2286
|
if (group.renderSignature === nodeSignature) continue;
|
|
@@ -2140,11 +2312,11 @@ export function startViewer() {
|
|
|
2140
2312
|
}
|
|
2141
2313
|
updateSelection();
|
|
2142
2314
|
$('revision').textContent = `Revision ${graph.revision}`;
|
|
2143
|
-
$('canvas-title').textContent = state.replayFrame ? 'Architecture replay' : 'Live architecture';
|
|
2315
|
+
$('canvas-title').textContent = state.platformActive ? state.viewName : state.replayFrame ? 'Architecture replay' : 'Live architecture';
|
|
2144
2316
|
$('diagram-title').textContent = `${state.replayFrame ? 'Historical' : 'Live'} architecture, revision ${graph.revision}`;
|
|
2145
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.`;
|
|
2146
2318
|
$('graph-count').textContent = `${graph.nodes.length} components · ${graph.edges.length} relationships`;
|
|
2147
|
-
$('diagram-search-status').textContent = state.searchQuery
|
|
2319
|
+
$('diagram-search-status').textContent = state.searchQuery || state.nodeTypes !== null
|
|
2148
2320
|
? `${graph.nodes.length} of ${canonical.nodes.length} components shown` : '';
|
|
2149
2321
|
$('diagram-search-clear').hidden = !state.searchQuery;
|
|
2150
2322
|
$('empty-canvas').hidden = graph.nodes.length > 0 || removalBounds().length > 0;
|
|
@@ -2156,8 +2328,12 @@ export function startViewer() {
|
|
|
2156
2328
|
unavailable: ['Waiting for classification.', 'The classifier is unavailable. Safe activity continues below; supported architecture will appear when classification recovers.'],
|
|
2157
2329
|
timeout: ['Evidence needs another moment.', 'Classification exceeded its deadline. Activity still appears below, and no unsupported components are added.'],
|
|
2158
2330
|
};
|
|
2159
|
-
const message = state.
|
|
2160
|
-
? ['No
|
|
2331
|
+
const message = state.nodeTypes?.size === 0
|
|
2332
|
+
? ['No component types selected.', 'Choose a type or All types to show components.']
|
|
2333
|
+
: state.searchQuery
|
|
2334
|
+
? ['No matching components.', `No selected components match “${state.searchQuery}”. Try another search or press Esc to clear the search.`]
|
|
2335
|
+
: state.nodeTypes !== null && canonical.nodes.length
|
|
2336
|
+
? ['No matching components.', 'Choose another type or All types to show components.']
|
|
2161
2337
|
: state.replayFrame
|
|
2162
2338
|
? ['No components in this revision.', 'Move through the recent revisions or return to Live to follow the current map.']
|
|
2163
2339
|
: emptyMessages[classifier] || ['Your architecture starts here.', 'Work in a connected agent session. Components appear when approved evidence supports them; activity can arrive first.'];
|
|
@@ -2227,8 +2403,7 @@ export function startViewer() {
|
|
|
2227
2403
|
finishPan();
|
|
2228
2404
|
cancelMovement();
|
|
2229
2405
|
const before = state.displayGraph;
|
|
2230
|
-
|
|
2231
|
-
renderGraph({ forceFit: true });
|
|
2406
|
+
renderGraph({ forceFit: true, arrange: true });
|
|
2232
2407
|
moveLayout(before, state.displayGraph);
|
|
2233
2408
|
announce(`Arranged using ${LAYOUT_NAMES[state.view.algorithm]}. Evidence and selection are unchanged.`);
|
|
2234
2409
|
}
|
|
@@ -2241,7 +2416,11 @@ export function startViewer() {
|
|
|
2241
2416
|
const body = $('inspector-body');
|
|
2242
2417
|
const graph = currentGraph();
|
|
2243
2418
|
const selected = state.selection;
|
|
2244
|
-
|
|
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
|
+
}
|
|
2245
2424
|
const linkedIds = new Set(claim?.sourceRefs.map(ref => ref.eventId) || []);
|
|
2246
2425
|
const related = state.snapshot?.activity.filter(event => linkedIds.has(event.id)).slice(-5).reverse() || [];
|
|
2247
2426
|
const override = selected?.type === 'node' ? state.view?.shapes.get(selected.id) : undefined;
|
|
@@ -2270,7 +2449,9 @@ export function startViewer() {
|
|
|
2270
2449
|
fact(facts, 'Classification', upperFirst(claim.classification));
|
|
2271
2450
|
fact(facts, 'Validity', upperFirst(claim.validity));
|
|
2272
2451
|
fact(facts, 'Evidence state', claim.evidenceState === 'verified' ? 'Verification reported; scope unavailable' : upperFirst(claim.evidenceState));
|
|
2273
|
-
|
|
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');
|
|
2274
2455
|
fact(facts, 'Runtime', 'Not established by this snapshot');
|
|
2275
2456
|
if (selected.type === 'node') fact(facts, 'Activity', upperFirst(claim.activityState));
|
|
2276
2457
|
else {
|
|
@@ -2278,6 +2459,14 @@ export function startViewer() {
|
|
|
2278
2459
|
fact(facts, 'To', graph.nodes.find(node => node.id === claim.target)?.label || 'Unknown component');
|
|
2279
2460
|
}
|
|
2280
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(', '));
|
|
2281
2470
|
if (selected.type === 'node') {
|
|
2282
2471
|
const label = html('label', 'Shape · visual only', 'shape-picker');
|
|
2283
2472
|
label.setAttribute('for', 'display-shape');
|
|
@@ -2311,36 +2500,38 @@ export function startViewer() {
|
|
|
2311
2500
|
announce(`Display shape changed. ${claim.label} remains classified as ${claim.kind}.`);
|
|
2312
2501
|
});
|
|
2313
2502
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
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);
|
|
2341
2534
|
}
|
|
2342
|
-
details.append(list);
|
|
2343
|
-
body.append(details);
|
|
2344
2535
|
}
|
|
2345
2536
|
body.append(html('h4', `Source references (${claim.sourceRefs.length})`));
|
|
2346
2537
|
if (!claim.sourceRefs.length) body.append(html('p', 'No source references were supplied. This claim’s provenance cannot be inspected.', 'fine-print'));
|
|
@@ -2348,8 +2539,9 @@ export function startViewer() {
|
|
|
2348
2539
|
for (const ref of claim.sourceRefs) {
|
|
2349
2540
|
const item = html('li', undefined, 'source-reference');
|
|
2350
2541
|
item.append(
|
|
2351
|
-
html('strong', ref.sourceClass === 'public_intent' ? 'Public intent' : ref.sourceClass === 'source' ? 'Source artifact' : 'Unknown source class'),
|
|
2352
|
-
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'),
|
|
2353
2545
|
html('span', `${ref.sourceClass === 'public_intent' ? 'Message' : 'Artifact'}: ${ref.sourceRef?.messageId || ref.artifactId || 'not supplied'}`),
|
|
2354
2546
|
html('span', `Version: ${ref.hash || 'not supplied'} · ${ref.sourceClass === 'public_intent' ? 'content version' : 'generation'} ${ref.sourceRef?.contentVersion ?? ref.generation}`),
|
|
2355
2547
|
);
|
|
@@ -2371,6 +2563,7 @@ export function startViewer() {
|
|
|
2371
2563
|
button.addEventListener('click', () => {
|
|
2372
2564
|
const row = state.activityElements.get(event.id);
|
|
2373
2565
|
if (row) {
|
|
2566
|
+
setWorkspacePanel('activity', true);
|
|
2374
2567
|
row.querySelector('button').focus();
|
|
2375
2568
|
row.scrollIntoView({ block: 'nearest' });
|
|
2376
2569
|
}
|
|
@@ -2381,6 +2574,16 @@ export function startViewer() {
|
|
|
2381
2574
|
}
|
|
2382
2575
|
}
|
|
2383
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
|
+
}
|
|
2384
2587
|
const replay = Boolean(state.replayFrame);
|
|
2385
2588
|
const frameIndex = replay ? state.frames.findIndex(frame => frame.revision === state.replayFrame.revision) : state.frames.length - 1;
|
|
2386
2589
|
$('live').setAttribute('aria-pressed', String(!replay));
|
|
@@ -2433,6 +2636,7 @@ export function startViewer() {
|
|
|
2433
2636
|
updateSelection();
|
|
2434
2637
|
renderInspector();
|
|
2435
2638
|
renderActivity();
|
|
2639
|
+
revealInspector();
|
|
2436
2640
|
announce(`Related evidence for ${(node || edge).label} is shown in the inspector.`);
|
|
2437
2641
|
} else toast(state.replayFrame ? 'No evidence link for this event in the displayed revision. Return to Live to inspect current links.' : 'This captured event has no architecture evidence link. Tool activity alone does not establish an architectural claim.');
|
|
2438
2642
|
});
|
|
@@ -2463,6 +2667,7 @@ export function startViewer() {
|
|
|
2463
2667
|
function replayAt(index) {
|
|
2464
2668
|
const frame = state.frames[index];
|
|
2465
2669
|
if (!frame) return;
|
|
2670
|
+
setWorkspacePanel('history', true);
|
|
2466
2671
|
resetMotionBaseline();
|
|
2467
2672
|
state.replayFrame = frame;
|
|
2468
2673
|
render();
|
|
@@ -2524,6 +2729,7 @@ export function startViewer() {
|
|
|
2524
2729
|
connection('reconnecting');
|
|
2525
2730
|
error('The live connection was lost. Displaying the last received snapshot while the viewer reconnects.');
|
|
2526
2731
|
});
|
|
2732
|
+
void platform.start();
|
|
2527
2733
|
void dashboardInfo.refresh();
|
|
2528
2734
|
// Optional authenticated metadata must not hold up the event stream or
|
|
2529
2735
|
// turn an older server's missing endpoint into a connection failure.
|
|
@@ -2534,6 +2740,7 @@ export function startViewer() {
|
|
|
2534
2740
|
if (!state.closed && attempt === state.connectEpoch) {
|
|
2535
2741
|
state.projectName = friendlyProjectName(info.projectRoot);
|
|
2536
2742
|
$('project-path').textContent = info.projectRoot;
|
|
2743
|
+
$('project-path').title = info.projectRoot;
|
|
2537
2744
|
renderStatus();
|
|
2538
2745
|
}
|
|
2539
2746
|
} catch { /* The project ID remains a usable fallback. */ }
|
|
@@ -2588,24 +2795,87 @@ export function startViewer() {
|
|
|
2588
2795
|
$('orientation-prompt').focus();
|
|
2589
2796
|
}
|
|
2590
2797
|
};
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
$('
|
|
2594
|
-
$('activity-
|
|
2595
|
-
|
|
2798
|
+
const workspacePanels = {
|
|
2799
|
+
details: { panel: $('live-sidebar'), toggle: $('details-toggle') },
|
|
2800
|
+
history: { panel: $('history-panel'), toggle: $('history-toggle') },
|
|
2801
|
+
activity: { panel: $('activity-panel'), toggle: $('activity-toggle') },
|
|
2802
|
+
};
|
|
2803
|
+
function setWorkspacePanel(name, open) {
|
|
2804
|
+
const { panel, toggle } = workspacePanels[name];
|
|
2805
|
+
if (!open && panel.contains(document.activeElement)) toggle.focus();
|
|
2806
|
+
panel.hidden = !open;
|
|
2807
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
2808
|
+
if (name === 'details') $('workspace-body').dataset.detailsOpen = String(open);
|
|
2809
|
+
if (name === 'activity') $('activity-content').hidden = !open;
|
|
2810
|
+
}
|
|
2811
|
+
const onToggleDetails = () => setWorkspacePanel('details', $('live-sidebar').hidden);
|
|
2812
|
+
const onToggleHistory = () => setWorkspacePanel('history', $('history-panel').hidden);
|
|
2813
|
+
const onToggleActivity = () => setWorkspacePanel('activity', $('activity-panel').hidden);
|
|
2814
|
+
const onCloseDetails = () => {
|
|
2815
|
+
setWorkspacePanel('details', false);
|
|
2816
|
+
$('details-toggle').focus();
|
|
2596
2817
|
};
|
|
2818
|
+
const onViewUpdate = () => {
|
|
2819
|
+
$('version-update-guide').open = !$('version-update-guide').hidden;
|
|
2820
|
+
revealDetailsSection($('version-update-status'));
|
|
2821
|
+
};
|
|
2822
|
+
const panelKeyHandlers = new Map();
|
|
2823
|
+
for (const [name, { panel, toggle }] of Object.entries(workspacePanels)) {
|
|
2824
|
+
setWorkspacePanel(name, false);
|
|
2825
|
+
const onKeyDown = event => {
|
|
2826
|
+
if (event.key !== 'Escape' || event.defaultPrevented || event.target?.tagName?.toLowerCase() === 'select') return;
|
|
2827
|
+
event.preventDefault();
|
|
2828
|
+
setWorkspacePanel(name, false);
|
|
2829
|
+
toggle.focus();
|
|
2830
|
+
};
|
|
2831
|
+
panel.addEventListener('keydown', onKeyDown);
|
|
2832
|
+
panelKeyHandlers.set(panel, onKeyDown);
|
|
2833
|
+
}
|
|
2597
2834
|
$('onboarding-action').addEventListener('click', onOnboardingAction);
|
|
2598
2835
|
$('orientation-copy').addEventListener('click', onCopyOrientation);
|
|
2836
|
+
$('details-toggle').addEventListener('click', onToggleDetails);
|
|
2837
|
+
$('details-close').addEventListener('click', onCloseDetails);
|
|
2838
|
+
$('version-update-indicator').addEventListener('click', onViewUpdate);
|
|
2839
|
+
$('history-toggle').addEventListener('click', onToggleHistory);
|
|
2599
2840
|
$('activity-toggle').addEventListener('click', onToggleActivity);
|
|
2600
|
-
const
|
|
2601
|
-
if (state.
|
|
2602
|
-
state.searchQuery = $('diagram-search').value;
|
|
2841
|
+
const applyFilters = () => {
|
|
2842
|
+
if (state.platformActive) { platform.filter(state.searchQuery, state.nodeTypes); return; }
|
|
2603
2843
|
// Filtering is not a source change: cancel existing decoration/movement
|
|
2604
2844
|
// and render directly, without changing the snapshot arrival baseline.
|
|
2605
2845
|
finishPan();
|
|
2606
2846
|
clearMotion();
|
|
2607
2847
|
renderOnboarding();
|
|
2608
|
-
renderGraph({ forceFit: true });
|
|
2848
|
+
renderGraph({ forceFit: true, arrange: true });
|
|
2849
|
+
};
|
|
2850
|
+
const onSearchInput = () => {
|
|
2851
|
+
if (state.closed || state.searchQuery === $('diagram-search').value) return;
|
|
2852
|
+
state.searchQuery = $('diagram-search').value;
|
|
2853
|
+
if (state.platformActive) { platform.filter(state.searchQuery, state.nodeTypes); return; }
|
|
2854
|
+
applyFilters();
|
|
2855
|
+
};
|
|
2856
|
+
const onNodeTypeClick = event => {
|
|
2857
|
+
if (state.closed || !currentGraph()) return;
|
|
2858
|
+
const container = $('node-type-filters');
|
|
2859
|
+
let button = event.target;
|
|
2860
|
+
while (button && button.parentElement !== container) button = button.parentElement;
|
|
2861
|
+
const kind = button?.dataset.kind;
|
|
2862
|
+
if (!kind || !state.nodeTypeButtons.has(kind)) return;
|
|
2863
|
+
// null means all, including future kinds. Explicit selections retain their
|
|
2864
|
+
// choices when a kind disappears and returns; new kinds stay unselected.
|
|
2865
|
+
if (state.nodeTypes === null) state.nodeTypes = new Set((state.platformActive ? state.model.entities : currentGraph().nodes).map(node => node.kind));
|
|
2866
|
+
if (state.nodeTypes.has(kind)) state.nodeTypes.delete(kind);
|
|
2867
|
+
else state.nodeTypes.add(kind);
|
|
2868
|
+
applyFilters();
|
|
2869
|
+
};
|
|
2870
|
+
const onAllNodeTypes = () => {
|
|
2871
|
+
if (state.closed) return;
|
|
2872
|
+
state.nodeTypes = null;
|
|
2873
|
+
applyFilters();
|
|
2874
|
+
};
|
|
2875
|
+
const onNoNodeTypes = () => {
|
|
2876
|
+
if (state.closed) return;
|
|
2877
|
+
state.nodeTypes = new Set();
|
|
2878
|
+
applyFilters();
|
|
2609
2879
|
};
|
|
2610
2880
|
const clearSearch = () => {
|
|
2611
2881
|
$('diagram-search').value = '';
|
|
@@ -2627,11 +2897,15 @@ export function startViewer() {
|
|
|
2627
2897
|
};
|
|
2628
2898
|
$('diagram-search').addEventListener('input', onSearchInput);
|
|
2629
2899
|
$('diagram-search-clear').addEventListener('click', clearSearch);
|
|
2900
|
+
$('node-type-filters')?.addEventListener('click', onNodeTypeClick);
|
|
2901
|
+
$('node-types-all')?.addEventListener('click', onAllNodeTypes);
|
|
2902
|
+
$('node-types-none')?.addEventListener('click', onNoNodeTypes);
|
|
2630
2903
|
window.addEventListener('keydown', onSearchKeyDown);
|
|
2631
2904
|
$('pause').addEventListener('click', () => control(state.snapshot?.paused ? 'resume' : 'pause'));
|
|
2632
|
-
$('session').addEventListener('change', () => control('session', $('session').value));
|
|
2905
|
+
$('session').addEventListener('change', () => state.platformActive ? platform.session($('session').value) : control('session', $('session').value));
|
|
2633
2906
|
$('export').addEventListener('click', exportJSON);
|
|
2634
2907
|
$('live').addEventListener('click', () => {
|
|
2908
|
+
if (state.platformActive) { platform.live(); return; }
|
|
2635
2909
|
resetMotionBaseline();
|
|
2636
2910
|
state.replayFrame = null;
|
|
2637
2911
|
if (state.snapshot) render();
|
|
@@ -2737,7 +3011,7 @@ export function startViewer() {
|
|
|
2737
3011
|
$('architecture').addEventListener('pointerup', finishPan);
|
|
2738
3012
|
$('architecture').addEventListener('pointercancel', finishPan);
|
|
2739
3013
|
$('architecture').addEventListener('lostpointercapture', finishPan);
|
|
2740
|
-
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; };
|
|
2741
3015
|
const onPageShow = event => { if (!state.closed && event.persisted) connect(); };
|
|
2742
3016
|
const onOnline = () => { if (!state.closed && state.connection !== 'connected') connect(); };
|
|
2743
3017
|
const onVisibility = () => resetMotionBaseline();
|
|
@@ -2766,14 +3040,23 @@ export function startViewer() {
|
|
|
2766
3040
|
ready: connect(),
|
|
2767
3041
|
close() {
|
|
2768
3042
|
state.closed = true;
|
|
3043
|
+
platform.close();
|
|
2769
3044
|
projectController?.abort();
|
|
2770
3045
|
projectController = null;
|
|
2771
3046
|
dashboardInfo.close();
|
|
2772
3047
|
$('onboarding-action').removeEventListener('click', onOnboardingAction);
|
|
2773
3048
|
$('orientation-copy').removeEventListener('click', onCopyOrientation);
|
|
3049
|
+
$('details-toggle').removeEventListener('click', onToggleDetails);
|
|
3050
|
+
$('details-close').removeEventListener('click', onCloseDetails);
|
|
3051
|
+
$('version-update-indicator').removeEventListener('click', onViewUpdate);
|
|
3052
|
+
$('history-toggle').removeEventListener('click', onToggleHistory);
|
|
2774
3053
|
$('activity-toggle').removeEventListener('click', onToggleActivity);
|
|
3054
|
+
for (const [panel, handler] of panelKeyHandlers) panel.removeEventListener('keydown', handler);
|
|
2775
3055
|
$('diagram-search').removeEventListener('input', onSearchInput);
|
|
2776
3056
|
$('diagram-search-clear').removeEventListener('click', clearSearch);
|
|
3057
|
+
$('node-type-filters')?.removeEventListener('click', onNodeTypeClick);
|
|
3058
|
+
$('node-types-all')?.removeEventListener('click', onAllNodeTypes);
|
|
3059
|
+
$('node-types-none')?.removeEventListener('click', onNoNodeTypes);
|
|
2777
3060
|
window.removeEventListener?.('keydown', onSearchKeyDown);
|
|
2778
3061
|
$('architecture').removeEventListener('wheel', onDiagramWheel);
|
|
2779
3062
|
connectionDialog.dispose();
|