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.
Files changed (102) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +465 -0
  8. package/docs/visualizer-views.md +199 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +74 -9
  34. package/plugin.json +4 -2
  35. package/runtime/core/evidence.mjs +43 -9
  36. package/runtime/core/graph.mjs +11 -6
  37. package/runtime/core/privacy.mjs +1 -0
  38. package/runtime/daemon/auth.mjs +7 -3
  39. package/runtime/daemon/diagnostics.mjs +1 -1
  40. package/runtime/daemon/extension-api.mjs +203 -0
  41. package/runtime/daemon/lineage.mjs +70 -0
  42. package/runtime/daemon/manager.mjs +9 -6
  43. package/runtime/daemon/model-api.mjs +728 -0
  44. package/runtime/daemon/model-persistence.mjs +220 -0
  45. package/runtime/daemon/server.mjs +70 -12
  46. package/runtime/daemon/settings.mjs +11 -3
  47. package/runtime/decisions/broker.mjs +349 -0
  48. package/runtime/decisions/contracts.mjs +179 -0
  49. package/runtime/decisions/evaluation.mjs +305 -0
  50. package/runtime/decisions/faults.mjs +32 -0
  51. package/runtime/decisions/index.mjs +818 -0
  52. package/runtime/decisions/profiles.mjs +93 -0
  53. package/runtime/decisions/questions.mjs +268 -0
  54. package/runtime/discovery/index.mjs +2 -0
  55. package/runtime/discovery/inventory.mjs +160 -0
  56. package/runtime/discovery/parser.mjs +40 -0
  57. package/runtime/discovery/structure.mjs +232 -0
  58. package/runtime/extensions/contracts.mjs +59 -0
  59. package/runtime/extensions/frame.mjs +64 -0
  60. package/runtime/extensions/index.mjs +9 -0
  61. package/runtime/extensions/manifest.mjs +95 -0
  62. package/runtime/extensions/packages.mjs +222 -0
  63. package/runtime/extensions/profiles.mjs +36 -0
  64. package/runtime/extensions/projection.mjs +130 -0
  65. package/runtime/extensions/registry.mjs +285 -0
  66. package/runtime/extensions/scene.mjs +105 -0
  67. package/runtime/extensions/sdk.d.ts +205 -0
  68. package/runtime/extensions/sdk.mjs +88 -0
  69. package/runtime/jev/index.mjs +13 -777
  70. package/runtime/jev/provider.mjs +101 -0
  71. package/runtime/jev/questions.mjs +16 -258
  72. package/runtime/jev/wire.mjs +17 -25
  73. package/runtime/model/changes.mjs +42 -0
  74. package/runtime/model/history.mjs +124 -0
  75. package/runtime/model/index.mjs +2 -0
  76. package/runtime/model/project-model.mjs +889 -0
  77. package/runtime/model/records.mjs +239 -0
  78. package/runtime/pipeline.mjs +127 -48
  79. package/runtime/platform.mjs +254 -0
  80. package/runtime/visualizers/blocks.mjs +5 -0
  81. package/runtime/visualizers/c4.mjs +52 -0
  82. package/runtime/visualizers/changes.mjs +24 -0
  83. package/runtime/visualizers/code.mjs +5 -0
  84. package/runtime/visualizers/index.mjs +23 -0
  85. package/runtime/visualizers/structure.mjs +120 -0
  86. package/runtime/visualizers/timeline.mjs +66 -0
  87. package/runtime/web/app.js +369 -86
  88. package/runtime/web/extension-frame.js +128 -0
  89. package/runtime/web/index.html +123 -80
  90. package/runtime/web/model-client.js +162 -0
  91. package/runtime/web/platform.js +337 -0
  92. package/runtime/web/scene.js +111 -0
  93. package/runtime/web/style.css +152 -142
  94. package/schemas/graph.schema.json +4 -1
  95. package/scripts/arguments.mjs +5 -1
  96. package/scripts/build-packages.mjs +6 -2
  97. package/scripts/control.mjs +1 -1
  98. package/scripts/daemon.mjs +2 -1
  99. package/scripts/extensions.mjs +44 -0
  100. package/scripts/graphlin.mjs +23 -3
  101. package/scripts/onboarding.mjs +10 -3
  102. package/scripts/validate-packages.mjs +54 -8
@@ -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 === 'jev_interpretation' ? 'jev_interpretation' : 'unknown',
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 !== 'jev_interpretation' || ref.sourceClass === 'unknown')) {
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
- return { tone: 'observed', label: 'Code evidence', explanation: 'Jev interpreted approved source evidence as supporting this claim. Code or configuration can describe a dependency without proving that it runs or connects successfully.' };
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 : NODE_WIDTH / 2 / Math.abs(direction.x),
610
- direction.y === 0 ? Infinity : NODE_HEIGHT / 2 / Math.abs(direction.y),
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 : NODE_WIDTH / 2 / Math.abs(x),
616
- y === 0 ? Infinity : NODE_HEIGHT / 2 / Math.abs(y),
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.replayFrame?.graph || state.snapshot?.graph; }
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 focusNodeId = liveNodeChanges(state.snapshot?.graph, snapshot.graph, live).added.at(-1);
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) resetView();
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 offset = evidence.top - region.top - (container.clientTop || 0);
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 renderGraph({ forceFit = false, focusNodeId } = {}) {
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
- // Search only projects visibility; layout, evidence and live arrival
2025
- // detection retain the complete source graph.
2026
- const graph = filterDiagram(projectPresentation(canonical, view), state.searchQuery);
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.searchQuery
2160
- ? ['No matching components.', `No labels contain “${state.searchQuery}”. Try another search or press Esc to restore the diagram.`]
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
- projectPresentation(graph, presentation(), { arrange: true });
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
- const claim = selected && graph ? (selected.type === 'node' ? graph.nodes : graph.edges).find(item => item.id === selected.id) : null;
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
- fact(facts, 'Basis', claim.sourceRefs.length && claim.sourceRefs.every(ref => ref.basis === 'jev_interpretation') ? 'Jev code interpretation' : 'Provenance incomplete');
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
- const confidence = normalizeConfidence(claim.confidence);
2315
- body.append(html('h4', 'Classifier confidence'));
2316
- const confidenceList = html('ul', undefined, 'confidence-list');
2317
- const confidenceLabels = {
2318
- supportProbability: 'Evidence support probability',
2319
- roleProbability: 'Selected role probability',
2320
- roleConfidence: 'Role distribution confidence',
2321
- missingContextProbability: 'Missing-context probability',
2322
- reportedConfidence: 'Reported classifier confidence',
2323
- };
2324
- for (const key of Object.keys(confidenceLabels)) {
2325
- if (!probability(confidence[key])) continue;
2326
- const item = html('li');
2327
- item.append(html('span', confidenceLabels[key]), html('strong', `${(confidence[key] * 100).toFixed(1)}%`));
2328
- confidenceList.append(item);
2329
- }
2330
- if (confidenceList.childElementCount) body.append(confidenceList);
2331
- else body.append(html('p', 'Confidence was not supplied for this claim.', 'fine-print'));
2332
- 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'));
2333
- if (record(confidence.roleProbabilities)) {
2334
- const details = html('details');
2335
- details.append(html('summary', 'Role probabilities'));
2336
- const list = html('ul', undefined, 'confidence-list');
2337
- for (const [role, value] of Object.entries(confidence.roleProbabilities)) {
2338
- const row = html('li');
2339
- row.append(html('span', upperFirst(role)), html('strong', `${(value * 100).toFixed(1)}%`));
2340
- list.append(row);
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', ref.basis === 'jev_interpretation' ? 'Basis: Jev interpretation' : 'Basis not supplied'),
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 onToggleActivity = () => {
2592
- const hidden = !$('activity-content').hidden;
2593
- $('activity-content').hidden = hidden;
2594
- $('activity-toggle').textContent = hidden ? 'Show activity log' : 'Hide activity log';
2595
- $('activity-toggle').setAttribute('aria-expanded', String(!hidden));
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 onSearchInput = () => {
2601
- if (state.closed || state.searchQuery === $('diagram-search').value) return;
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();