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
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { BUILTIN_VIEWS, createBuiltin } from '../visualizers/index.mjs';
|
|
2
|
+
import { createModelClient, modelQuery, hydrateModel } from './model-client.js';
|
|
3
|
+
import { ancestors } from '../visualizers/structure.mjs';
|
|
4
|
+
import { createExtensionFrame } from './extension-frame.js';
|
|
5
|
+
import { DATA_FIELDS, extensionId, digest, id as validId } from '../extensions/contracts.mjs';
|
|
6
|
+
import { filterScene } from './scene.js';
|
|
7
|
+
|
|
8
|
+
export function createViewPlatform({ document, request, onView, onSelect, onFollow = () => {},
|
|
9
|
+
createFrame = createExtensionFrame, grantPollMs = 2000 }) {
|
|
10
|
+
const $ = id => document.getElementById(id);
|
|
11
|
+
let model, active = 'graphlin.code', instance, installed = [], generation = 0, closed = false;
|
|
12
|
+
let projectionController;
|
|
13
|
+
let analysisBusy = false;
|
|
14
|
+
let grantWatch;
|
|
15
|
+
let selection = {}, canonicalSelection = null, follow = true, query = '', kinds = null, focusEntityId = null;
|
|
16
|
+
const settings = new Map(), listeners = [];
|
|
17
|
+
const ownSettings = () => {
|
|
18
|
+
if (!settings.has(active)) settings.set(active, { expanded: [], collapsed: [], level: 'applications' });
|
|
19
|
+
return settings.get(active);
|
|
20
|
+
};
|
|
21
|
+
const element = (tag, text) => {
|
|
22
|
+
const value = document.createElement(tag);
|
|
23
|
+
if (text !== undefined) value.textContent = text;
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const listen = (id, event, handler) => {
|
|
27
|
+
const node = $(id);
|
|
28
|
+
if (node) { node.addEventListener(event, handler); listeners.push(() => node.removeEventListener(event, handler)); }
|
|
29
|
+
};
|
|
30
|
+
function status(message = '') {
|
|
31
|
+
$('view-status').textContent = message;
|
|
32
|
+
$('view-status').hidden = !message;
|
|
33
|
+
$('view-retry').hidden = !message;
|
|
34
|
+
}
|
|
35
|
+
function dispose() {
|
|
36
|
+
if (grantWatch) { clearTimeout(grantWatch.timer); grantWatch.controller?.abort(); grantWatch = null; }
|
|
37
|
+
instance?.dispose(); instance = null;
|
|
38
|
+
}
|
|
39
|
+
function watchGrant(row) {
|
|
40
|
+
if (grantWatch?.instance === instance) return;
|
|
41
|
+
const watch = { instance, id: row.id, digest: row.digest, grant: JSON.stringify(row.grant), timer: null };
|
|
42
|
+
grantWatch = watch;
|
|
43
|
+
const check = async () => {
|
|
44
|
+
if (closed || grantWatch !== watch || instance !== watch.instance) return;
|
|
45
|
+
watch.controller = new AbortController();
|
|
46
|
+
try {
|
|
47
|
+
const response = await request('/api/extensions', { signal: watch.controller.signal });
|
|
48
|
+
if (closed || grantWatch !== watch) return;
|
|
49
|
+
const rows = Array.isArray(response) ? response : response.extensions;
|
|
50
|
+
if (!Array.isArray(rows)) throw new Error('invalid_extension_catalogue');
|
|
51
|
+
const current = rows.find(value => value.id === watch.id);
|
|
52
|
+
if (!current || current.digest !== watch.digest || !current.grant?.approved ||
|
|
53
|
+
JSON.stringify(current.grant) !== watch.grant) {
|
|
54
|
+
generation++; dispose(); onView({ clear: true });
|
|
55
|
+
installed = rows; controls();
|
|
56
|
+
if (current) consent(current);
|
|
57
|
+
status('Visualizer access or its installed version changed. Review access before continuing.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
if (closed || grantWatch !== watch) return;
|
|
62
|
+
generation++; dispose(); onView({ clear: true });
|
|
63
|
+
status('Visualizer access could not be verified. Retry the view after reconnecting.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (grantWatch === watch) watch.timer = setTimeout(check, grantPollMs);
|
|
67
|
+
};
|
|
68
|
+
watch.timer = setTimeout(check, grantPollMs);
|
|
69
|
+
}
|
|
70
|
+
function descriptor() { return BUILTIN_VIEWS.find(view => view.id === active) || installed.find(view => view.id === active); }
|
|
71
|
+
const profilesFor = row => Array.isArray(row?.profiles) ? row.profiles.filter(profile => validId(profile?.id)) : [];
|
|
72
|
+
function options(select, values, chosen) {
|
|
73
|
+
const signature = JSON.stringify(values);
|
|
74
|
+
if (select.dataset.options !== signature) {
|
|
75
|
+
select.replaceChildren(...values.map(([id, label]) => { const option = element('option', label); option.value = id; return option; }));
|
|
76
|
+
select.dataset.options = signature;
|
|
77
|
+
}
|
|
78
|
+
select.value = chosen || '';
|
|
79
|
+
}
|
|
80
|
+
function controls() {
|
|
81
|
+
options($('visualizer'), [...BUILTIN_VIEWS.map(view => [view.id, view.name]),
|
|
82
|
+
...installed.map(view => [view.id, view.manifest.name || view.id])], active);
|
|
83
|
+
for (const option of $('visualizer').children) option.disabled = !model && option.value !== 'graphlin.code';
|
|
84
|
+
$('view-context').hidden = !model;
|
|
85
|
+
$('c4-level-label').hidden = active !== 'graphlin.c4';
|
|
86
|
+
$('baseline-label').hidden = active !== 'graphlin.changes';
|
|
87
|
+
$('baseline-create').hidden = active !== 'graphlin.changes';
|
|
88
|
+
$('baseline-create').disabled = Boolean(selection.checkpoint);
|
|
89
|
+
const row = descriptor(), profiles = profilesFor(row);
|
|
90
|
+
$('analysis-controls').hidden = !profiles.length;
|
|
91
|
+
options($('analysis-profile'), profiles.map(profile => [profile.id,
|
|
92
|
+
`${profile.id}${row.grant?.profiles?.includes(profile.id) ? '' : ' (approval needed)'}`]),
|
|
93
|
+
ownSettings().profile || profiles[0]?.id);
|
|
94
|
+
$('analysis-run').disabled = analysisBusy || Boolean(selection.checkpoint) ||
|
|
95
|
+
!row?.grant?.approved || !row.grant.profiles?.includes($('analysis-profile').value) || !model?.entities.length;
|
|
96
|
+
$('c4-level').value = ownSettings().level;
|
|
97
|
+
const checkpoints = (model?.checkpoints || []).map(checkpoint => [checkpoint.id, checkpoint.label || `Revision ${checkpoint.revision}`]);
|
|
98
|
+
options($('task-baseline'), [['', 'Choose checkpoint'], ...checkpoints], ownSettings().baseline);
|
|
99
|
+
options($('model-position'), [['', 'Live'], ...checkpoints], selection.checkpoint);
|
|
100
|
+
const byId = new Map((model?.entities || []).map(entity => [entity.id, entity]));
|
|
101
|
+
const path = selection.scope ? [...ancestors(selection.scope, byId).reverse(), selection.scope] : [];
|
|
102
|
+
const crumbs = [['', 'Project'], ...path.map(id => [id, byId.get(id)?.label || 'Scope'])];
|
|
103
|
+
const breadcrumbSignature = JSON.stringify(crumbs);
|
|
104
|
+
if ($('scope-breadcrumbs').dataset.signature === breadcrumbSignature) return;
|
|
105
|
+
const buttons = crumbs.map(([id, label]) => {
|
|
106
|
+
const button = element('button', label); button.setAttribute('type', 'button');
|
|
107
|
+
button.setAttribute('aria-current', id === (selection.scope || '') ? 'location' : 'false');
|
|
108
|
+
button.addEventListener('click', () => scope(id));
|
|
109
|
+
return button;
|
|
110
|
+
});
|
|
111
|
+
$('scope-breadcrumbs').replaceChildren(...buttons);
|
|
112
|
+
$('scope-breadcrumbs').dataset.signature = breadcrumbSignature;
|
|
113
|
+
}
|
|
114
|
+
function consent(row) {
|
|
115
|
+
$('extension-access').hidden = false;
|
|
116
|
+
const key = `${model.projectId}:${row.id}:${row.digest}:${JSON.stringify(row.grant)}`;
|
|
117
|
+
if ($('extension-access').dataset.key === key) return;
|
|
118
|
+
$('extension-access').dataset.key = key;
|
|
119
|
+
$('extension-access-description').textContent = `${row.manifest.name || row.id} can read the selected project data. ` +
|
|
120
|
+
'Approval applies to this installed version. Source text, excerpts, prompts, and transcripts are never provided.';
|
|
121
|
+
const capabilities = row.manifest.capabilities || [];
|
|
122
|
+
const fields = DATA_FIELDS.filter(field => field !== 'checkpoints' &&
|
|
123
|
+
capabilities.includes(field === 'activity' ? 'activity.read' : 'model.read'));
|
|
124
|
+
$('extension-fields').replaceChildren(...fields.map(field => {
|
|
125
|
+
const label = element('label'), checkbox = element('input');
|
|
126
|
+
checkbox.type = 'checkbox'; checkbox.value = field;
|
|
127
|
+
checkbox.checked = row.grant ? row.grant.fields?.includes(field) : ['entities', 'relations', 'coverage', 'activity'].includes(field);
|
|
128
|
+
label.append(checkbox, element('span', field)); return label;
|
|
129
|
+
}));
|
|
130
|
+
$('extension-history').checked = row.grant?.history === true;
|
|
131
|
+
$('extension-history').disabled = !capabilities.includes('history.read');
|
|
132
|
+
const profiles = profilesFor(row);
|
|
133
|
+
$('extension-profiles').replaceChildren(...profiles.map(profile => {
|
|
134
|
+
const label = element('label'), checkbox = element('input');
|
|
135
|
+
checkbox.type = 'checkbox'; checkbox.value = profile.id;
|
|
136
|
+
checkbox.checked = row.grant?.profiles?.includes(profile.id) || false;
|
|
137
|
+
label.append(checkbox, element('span', `Allow ${profile.id} analysis (${(profile.selectors?.fields || []).join(', ') || 'declared fields'})`));
|
|
138
|
+
return label;
|
|
139
|
+
}));
|
|
140
|
+
$('analysis-note').hidden = !profiles.length;
|
|
141
|
+
}
|
|
142
|
+
async function catalogue(signal) {
|
|
143
|
+
try {
|
|
144
|
+
const result = await request('/api/extensions', { signal });
|
|
145
|
+
if (closed) return;
|
|
146
|
+
installed = (Array.isArray(result) ? result : result.extensions || []).filter(row =>
|
|
147
|
+
extensionId(row.id) && digest(row.digest) && row.manifest?.graphlinApi === '1' &&
|
|
148
|
+
row.manifest?.modelSchema === '2' && ['custom', 'graphlin-scene'].includes(row.manifest?.renderer?.kind));
|
|
149
|
+
controls();
|
|
150
|
+
} catch { /* Older daemons still provide the legacy code map. */ }
|
|
151
|
+
}
|
|
152
|
+
async function project(streamed = false, force = false) {
|
|
153
|
+
if (!model || closed) return;
|
|
154
|
+
projectionController?.abort();
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
projectionController = controller;
|
|
157
|
+
const mine = ++generation, view = descriptor();
|
|
158
|
+
controls();
|
|
159
|
+
$('extension-access').hidden = true;
|
|
160
|
+
if (!view) { dispose(); onView({ clear: true }); status('This visualizer is no longer installed. Choose another view.'); return; }
|
|
161
|
+
try {
|
|
162
|
+
let inputModel = model;
|
|
163
|
+
const inputSettings = { ...ownSettings(), ...selection, query, kinds: kinds ? [...kinds] : null, follow };
|
|
164
|
+
if (BUILTIN_VIEWS.some(item => item.id === active)) {
|
|
165
|
+
if (!instance) instance = createBuiltin(active, { root: $('custom-view'), select: onSelect });
|
|
166
|
+
let baseline;
|
|
167
|
+
if (active === 'graphlin.changes' && inputSettings.baseline) {
|
|
168
|
+
const baselineSelection = { ...selection, checkpoint: inputSettings.baseline };
|
|
169
|
+
const raw = await request(`/api/model/v1/snapshot${modelQuery(baselineSelection)}`, { signal: controller.signal });
|
|
170
|
+
baseline = await hydrateModel(raw, { request, selection: baselineSelection, signal: controller.signal });
|
|
171
|
+
if (mine !== generation || closed) return;
|
|
172
|
+
}
|
|
173
|
+
const result = await instance.update({ model: inputModel, settings: inputSettings, baseline,
|
|
174
|
+
apiVersion: 1, projectId: model.projectId, revision: model.revision, viewEpoch: mine });
|
|
175
|
+
if (mine !== generation || closed) return;
|
|
176
|
+
if (result.scene) result.scene = filterScene(result.scene, { query, kinds }, inputModel);
|
|
177
|
+
status();
|
|
178
|
+
onView({ ...result, model: inputModel, id: active, name: view.name, streamed, force,
|
|
179
|
+
focusEntityId: streamed ? focusEntityId : null, selection: canonicalSelection });
|
|
180
|
+
} else {
|
|
181
|
+
// Re-read catalogue/grant for every delivery; an old row is never a
|
|
182
|
+
// continuing permission. Clear controlled content before policy refresh.
|
|
183
|
+
onView({ clear: true });
|
|
184
|
+
await catalogue(controller.signal);
|
|
185
|
+
if (mine !== generation || closed) return;
|
|
186
|
+
const current = installed.find(row => row.id === active);
|
|
187
|
+
if (!current || current.digest !== view.digest) { dispose(); status('The installed version changed. Select it again to review access.'); return; }
|
|
188
|
+
if (JSON.stringify(current.grant) !== JSON.stringify(view.grant)) dispose();
|
|
189
|
+
if (!current.grant?.approved || current.grant.projectId !== model.projectId ||
|
|
190
|
+
current.grant.digest !== current.digest || (selection.checkpoint && !current.grant.history)) {
|
|
191
|
+
dispose(); consent(current); status(); return;
|
|
192
|
+
}
|
|
193
|
+
inputModel = await request(`/api/extensions/data/${encodeURIComponent(active)}${modelQuery(selection)}`, { signal: controller.signal });
|
|
194
|
+
if (mine !== generation || closed) return;
|
|
195
|
+
if (!instance) instance = createFrame({
|
|
196
|
+
root: $('custom-view'), extension: current, onSelect,
|
|
197
|
+
onFailure: () => { dispose(); onView({ clear: true }); status('Visualizer stopped. Retry view to restart it.'); },
|
|
198
|
+
});
|
|
199
|
+
watchGrant(current);
|
|
200
|
+
const requestedSelection = inputModel.entities.some(entity => entity.id === canonicalSelection)
|
|
201
|
+
? { entityId: canonicalSelection } : null;
|
|
202
|
+
const result = await instance.update({ model: inputModel, settings: inputSettings, selection: requestedSelection, viewEpoch: mine });
|
|
203
|
+
if (mine !== generation || closed) return;
|
|
204
|
+
if (result.scene) result.scene = filterScene(result.scene, { query, kinds }, inputModel);
|
|
205
|
+
status();
|
|
206
|
+
onView({ ...result, model: inputModel, id: active, name: current.manifest.name || active, streamed, force,
|
|
207
|
+
focusEntityId: streamed ? focusEntityId : null, selection: canonicalSelection });
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (mine !== generation || closed) return;
|
|
211
|
+
if ([401, 403, 404].includes(error.status)) { dispose(); onView({ clear: true }); }
|
|
212
|
+
else if (view.manifest) {
|
|
213
|
+
await choose('graphlin.code');
|
|
214
|
+
status('The visualizer stopped. Code is shown; select the visualizer to retry.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
status('Visualizer unavailable. Retry the view or choose Code.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const client = createModelClient({ request, onError: status, onSnapshot(value, streamed) {
|
|
221
|
+
focusEntityId = null;
|
|
222
|
+
if (model && streamed && follow && !selection.checkpoint && value.projectId === model.projectId) {
|
|
223
|
+
const known = new Set(model.entities.map(entity => entity.id));
|
|
224
|
+
const arrival = value.entities.filter(entity => !known.has(entity.id)).at(-1)?.id;
|
|
225
|
+
const observed = value.activity.filter(event => event.sequence > model.sequence).at(-1)?.entityIds?.[0];
|
|
226
|
+
focusEntityId = observed || arrival || null;
|
|
227
|
+
if (focusEntityId) {
|
|
228
|
+
const byId = new Map(value.entities.map(entity => [entity.id, entity]));
|
|
229
|
+
ownSettings().expanded = [...new Set([...ownSettings().expanded, ...ancestors(focusEntityId, byId)])];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (model && model.projectId !== value.projectId) {
|
|
233
|
+
dispose(); settings.clear(); canonicalSelection = null; selection = {};
|
|
234
|
+
onView({ clear: true });
|
|
235
|
+
}
|
|
236
|
+
model = value;
|
|
237
|
+
void project(streamed);
|
|
238
|
+
} });
|
|
239
|
+
function choose(id) {
|
|
240
|
+
if (id === active) return;
|
|
241
|
+
if (!model) { $('visualizer').value = 'graphlin.code'; status('Model views require a newer local service. The Code map remains available.'); return; }
|
|
242
|
+
generation++; dispose(); active = id;
|
|
243
|
+
onView({ clear: true });
|
|
244
|
+
return project(false, true);
|
|
245
|
+
}
|
|
246
|
+
function scope(id) {
|
|
247
|
+
selection.scope = id || undefined;
|
|
248
|
+
generation++; dispose();
|
|
249
|
+
void client.open(selection);
|
|
250
|
+
}
|
|
251
|
+
async function grant(approved) {
|
|
252
|
+
const row = installed.find(value => value.id === active);
|
|
253
|
+
if (!row) return;
|
|
254
|
+
generation++; dispose(); onView({ clear: true });
|
|
255
|
+
const fields = approved ? [...$('extension-fields').children]
|
|
256
|
+
.map(label => label.children[0]).filter(input => input.checked).map(input => input.value) : [];
|
|
257
|
+
const history = approved && $('extension-history').checked;
|
|
258
|
+
const profiles = approved ? [...$('extension-profiles').children].map(label => label.children[0])
|
|
259
|
+
.filter(input => input.checked).map(input => input.value) : [];
|
|
260
|
+
if (history) fields.push('checkpoints');
|
|
261
|
+
try {
|
|
262
|
+
await request('/api/extensions/grant', { method: 'POST',
|
|
263
|
+
body: JSON.stringify({ id: row.id, digest: row.digest, fields, history, approved, profiles }) });
|
|
264
|
+
await catalogue();
|
|
265
|
+
if (approved) await project(false, true);
|
|
266
|
+
else { $('extension-access').hidden = true; status('Access denied. Choose another view.'); }
|
|
267
|
+
} catch { status('Access could not be updated. Retry after reconnecting.'); }
|
|
268
|
+
}
|
|
269
|
+
listen('visualizer', 'change', () => choose($('visualizer').value));
|
|
270
|
+
listen('view-retry', 'click', () => { dispose(); void client.open(selection); });
|
|
271
|
+
listen('c4-level', 'change', () => { ownSettings().level = $('c4-level').value; void project(false, true); });
|
|
272
|
+
listen('task-baseline', 'change', () => { ownSettings().baseline = $('task-baseline').value; void project(false, true); });
|
|
273
|
+
listen('baseline-create', 'click', async () => {
|
|
274
|
+
if (selection.checkpoint) return;
|
|
275
|
+
const button = $('baseline-create');
|
|
276
|
+
button.disabled = true;
|
|
277
|
+
try {
|
|
278
|
+
const marker = await request('/api/model/v1/checkpoints', { method: 'POST',
|
|
279
|
+
body: JSON.stringify({ label: 'Task baseline', ...(selection.session ? { sessionId: selection.session } : {}) }) });
|
|
280
|
+
ownSettings().baseline = marker.id || marker.checkpoint?.id || marker.marker?.id;
|
|
281
|
+
await client.open(selection);
|
|
282
|
+
} catch { status('The checkpoint could not be saved. Retry after reconnecting.'); }
|
|
283
|
+
finally { button.disabled = Boolean(selection.checkpoint); }
|
|
284
|
+
});
|
|
285
|
+
listen('model-position', 'change', () => {
|
|
286
|
+
selection.checkpoint = $('model-position').value;
|
|
287
|
+
controls();
|
|
288
|
+
void client.open(selection);
|
|
289
|
+
});
|
|
290
|
+
listen('follow-agent', 'change', () => { follow = $('follow-agent').checked; onFollow(follow); void project(); });
|
|
291
|
+
listen('extension-approve', 'click', () => grant(true));
|
|
292
|
+
listen('extension-deny', 'click', () => grant(false));
|
|
293
|
+
listen('analysis-profile', 'change', () => { ownSettings().profile = $('analysis-profile').value; controls(); });
|
|
294
|
+
listen('analysis-access', 'click', () => { const row = descriptor(); if (row?.manifest) consent(row); });
|
|
295
|
+
listen('analysis-run', 'click', async () => {
|
|
296
|
+
const row = descriptor(), profile = profilesFor(row).find(profile => profile.id === $('analysis-profile').value);
|
|
297
|
+
if (analysisBusy || !profile || !row.grant?.approved || !row.grant.profiles?.includes(profile.id) || selection.checkpoint) return;
|
|
298
|
+
const byId = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
299
|
+
const candidates = byId.has(canonicalSelection) ? [canonicalSelection] : model.entities
|
|
300
|
+
.filter(entity => !selection.scope || entity.id === selection.scope || ancestors(entity.id, byId).includes(selection.scope))
|
|
301
|
+
.map(entity => entity.id);
|
|
302
|
+
const entityIds = candidates.filter(id => !profile.selectors?.candidateIds?.length || profile.selectors.candidateIds.includes(id)).slice(0, 256);
|
|
303
|
+
if (!entityIds.length) { status('Select a supported entity in the current scope before running this profile.'); return; }
|
|
304
|
+
analysisBusy = true; controls();
|
|
305
|
+
try {
|
|
306
|
+
const result = await request('/api/extensions/analysis', { method: 'POST',
|
|
307
|
+
body: JSON.stringify({ id: row.id, digest: row.digest, profileId: profile.id, entityIds, revision: model.revision }) });
|
|
308
|
+
if (active === row.id && !closed) status(result.status === 'complete' ? 'Analysis complete.'
|
|
309
|
+
: result.status === 'unavailable' ? 'Analysis is unavailable under the current source-transmission settings or service status.'
|
|
310
|
+
: 'Analysis requested. Accepted results will arrive with model updates.');
|
|
311
|
+
} catch {
|
|
312
|
+
if (active === row.id && !closed) status('Analysis was not accepted. Check profile/data approval and this project’s source-transmission setting.');
|
|
313
|
+
} finally { analysisBusy = false; if (!closed) controls(); }
|
|
314
|
+
});
|
|
315
|
+
controls();
|
|
316
|
+
return {
|
|
317
|
+
async start() { if (await client.open(selection)) await catalogue(); },
|
|
318
|
+
choose,
|
|
319
|
+
filter(nextQuery, nextKinds) { query = nextQuery; kinds = nextKinds; void project(false, true); },
|
|
320
|
+
selected(id) { canonicalSelection = id; },
|
|
321
|
+
scope,
|
|
322
|
+
session(id) { selection.session = id || undefined; generation++; dispose(); void client.open(selection); },
|
|
323
|
+
live() { selection.checkpoint = undefined; generation++; dispose(); void client.open(selection); },
|
|
324
|
+
toggle(id, isCollapsed = true) {
|
|
325
|
+
const own = ownSettings(), expanded = new Set(own.expanded), collapsed = new Set(own.collapsed);
|
|
326
|
+
if (!isCollapsed) { expanded.delete(id); collapsed.add(id); }
|
|
327
|
+
else { expanded.add(id); collapsed.delete(id); }
|
|
328
|
+
own.expanded = [...expanded]; own.collapsed = [...collapsed];
|
|
329
|
+
void project(false, true);
|
|
330
|
+
},
|
|
331
|
+
close() { closed = true; generation++; projectionController?.abort(); client.close(); dispose(); listeners.forEach(remove => remove()); },
|
|
332
|
+
suspend() { generation++; projectionController?.abort(); client.suspend(); dispose(); },
|
|
333
|
+
get active() { return active; },
|
|
334
|
+
get model() { return model; },
|
|
335
|
+
get selection() { return selection; },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ancestors, sceneKind } from '../visualizers/structure.mjs';
|
|
2
|
+
|
|
3
|
+
const SHAPES = {
|
|
4
|
+
client: 'browser', service: 'component', datastore: 'cylinder', queue: 'queue', external: 'cloud',
|
|
5
|
+
module: 'rect', function: 'hexagon', class: 'class_box', interface: 'interface_box',
|
|
6
|
+
event: 'document', configuration: 'parallelogram', package: 'folder', unknown: 'rect', group: 'group',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function filterScene(scene, { query = '', kinds = null } = {}, model) {
|
|
10
|
+
if (!query && kinds === null) return scene;
|
|
11
|
+
const entities = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
12
|
+
const matches = entityId => {
|
|
13
|
+
const entity = entities.get(entityId);
|
|
14
|
+
return entity && (!kinds || kinds.has(entity.kind)) &&
|
|
15
|
+
`${entity.label} ${entity.qualifiedName || ''}`.toLowerCase().includes(query.toLowerCase());
|
|
16
|
+
};
|
|
17
|
+
const retained = new Set(), groups = new Map(scene.groups.map(group => [group.id, group]));
|
|
18
|
+
const keep = item => {
|
|
19
|
+
retained.add(item.id);
|
|
20
|
+
let parent = item.parentId;
|
|
21
|
+
while (parent && !retained.has(parent)) { retained.add(parent); parent = groups.get(parent)?.parentId; }
|
|
22
|
+
};
|
|
23
|
+
scene.nodes.filter(node => matches(node.entityId)).forEach(keep);
|
|
24
|
+
scene.groups.filter(group => group.entityIds.some(matches) ||
|
|
25
|
+
(query && group.label.toLowerCase().includes(query.toLowerCase()) &&
|
|
26
|
+
(!kinds || group.entityIds.some(id => kinds.has(entities.get(id)?.kind))))).forEach(keep);
|
|
27
|
+
return { ...scene, nodes: scene.nodes.filter(node => retained.has(node.id)),
|
|
28
|
+
groups: scene.groups.filter(group => retained.has(group.id)),
|
|
29
|
+
edges: scene.edges.filter(edge => retained.has(edge.source) && retained.has(edge.target)) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Containment uses padding and a title band; sibling boxes never overlap.
|
|
33
|
+
* Layout algorithms still apply to ordinary flat scenes through the old viewer.
|
|
34
|
+
*/
|
|
35
|
+
export function layoutScene(scene) {
|
|
36
|
+
const items = new Map([...scene.groups, ...scene.nodes].map(item => [item.id, { ...item }]));
|
|
37
|
+
const groups = new Set(scene.groups.map(group => group.id)), children = new Map();
|
|
38
|
+
for (const item of items.values()) {
|
|
39
|
+
const parent = item.parentId || null;
|
|
40
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
41
|
+
children.get(parent).push(item);
|
|
42
|
+
}
|
|
43
|
+
function pack(parent, x, y) {
|
|
44
|
+
const siblings = children.get(parent) || [];
|
|
45
|
+
let cursorX = x, cursorY = y, rowHeight = 0, maxX = x;
|
|
46
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(siblings.length)));
|
|
47
|
+
siblings.forEach((item, index) => {
|
|
48
|
+
if (index && index % columns === 0) { cursorX = x; cursorY += rowHeight + 64; rowHeight = 0; }
|
|
49
|
+
item.x = cursorX; item.y = cursorY;
|
|
50
|
+
if (groups.has(item.id) && !item.collapsed && children.has(item.id)) {
|
|
51
|
+
const content = pack(item.id, cursorX + 28, cursorY + 64);
|
|
52
|
+
item.width = Math.max(246, content.width + 56);
|
|
53
|
+
item.height = Math.max(144, content.height + 92);
|
|
54
|
+
} else {
|
|
55
|
+
item.width = groups.has(item.id) ? Math.max(190, Math.min(300, item.label.length * 8 + 70)) : 190;
|
|
56
|
+
item.height = 104;
|
|
57
|
+
}
|
|
58
|
+
cursorX += item.width + 80; maxX = Math.max(maxX, cursorX - 80);
|
|
59
|
+
rowHeight = Math.max(rowHeight, item.height);
|
|
60
|
+
});
|
|
61
|
+
return { width: maxX - x, height: cursorY - y + rowHeight };
|
|
62
|
+
}
|
|
63
|
+
pack(null, 0, 0);
|
|
64
|
+
return { ...scene, groups: scene.groups.map(group => items.get(group.id)), nodes: scene.nodes.map(node => items.get(node.id)) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sceneGraph(scene, model) {
|
|
68
|
+
const entities = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
69
|
+
const relations = new Map(model.relations.map(relation => [relation.id, relation]));
|
|
70
|
+
const claim = entity => ({
|
|
71
|
+
sourceRefs: entity?.sourceRefs || [], validity: entity?.validity || 'current',
|
|
72
|
+
classification: entity?.classification || 'unknown', basis: entity?.basis || 'metadata',
|
|
73
|
+
evidenceState: entity?.basis === 'parsed' ? 'observed' : 'proposed', activityState: 'idle',
|
|
74
|
+
});
|
|
75
|
+
const active = new Map();
|
|
76
|
+
for (const event of model.activity) for (const id of event.entityIds || []) active.set(id, event.outcome);
|
|
77
|
+
const nodes = [
|
|
78
|
+
...scene.groups.map(group => {
|
|
79
|
+
const entity = entities.get(group.entityIds[0]);
|
|
80
|
+
const interpretation = group.membershipId && model.interpretations.find(value => value.id === group.membershipId);
|
|
81
|
+
return { ...claim(interpretation || entity), ...group, kind: group.kind || entity?.kind || 'unknown',
|
|
82
|
+
entityId: entity?.id, shape: 'group', isGroup: true, label: group.label,
|
|
83
|
+
memberCount: group.entityIds.length, activityCount: group.entityIds.filter(id => active.has(id)).length,
|
|
84
|
+
activityState: 'idle' };
|
|
85
|
+
}),
|
|
86
|
+
...scene.nodes.map(node => {
|
|
87
|
+
const entity = entities.get(node.entityId);
|
|
88
|
+
return { ...claim(entity), ...node, kind: entity?.kind || node.kind,
|
|
89
|
+
shape: node.shape || SHAPES[sceneKind(node.kind)] || 'rect',
|
|
90
|
+
activityState: active.get(node.entityId) === 'pending' ? 'pending' : active.get(node.entityId) === 'failed' ? 'failed' : 'idle' };
|
|
91
|
+
}),
|
|
92
|
+
].map(node => ({ x: 0, y: 0, ...node }));
|
|
93
|
+
return { revision: model.revision, nodes, edges: scene.edges.map(edge => ({
|
|
94
|
+
...claim(relations.get(edge.relationIds?.[0])), ...edge, relation: edge.kind,
|
|
95
|
+
label: edge.label || edge.kind.replaceAll('_', ' '),
|
|
96
|
+
sourceRefs: (edge.relationIds || []).flatMap(id => relations.get(id)?.sourceRefs || []).slice(0, 32),
|
|
97
|
+
})) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function representedSelection(entityId, scene, model) {
|
|
101
|
+
if (!entityId) return null;
|
|
102
|
+
const byId = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
103
|
+
for (const id of [entityId, ...ancestors(entityId, byId)]) {
|
|
104
|
+
const node = scene.nodes.find(node => node.entityId === id);
|
|
105
|
+
if (node) return node.id;
|
|
106
|
+
// Reverse order prefers a deeper group over an outer frame.
|
|
107
|
+
const group = [...scene.groups].reverse().find(group => group.entityIds.includes(id));
|
|
108
|
+
if (group) return group.id;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|