graphlin 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +472 -0
- package/docs/visualizer-views.md +240 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +78 -9
- package/plugin.json +4 -2
- package/runtime/architecture/analysis.mjs +344 -0
- package/runtime/architecture/controller.mjs +209 -0
- package/runtime/architecture/evidence.mjs +108 -0
- package/runtime/architecture/profile.mjs +56 -0
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +81 -14
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +1020 -0
- package/runtime/model/records.mjs +240 -0
- package/runtime/pipeline.mjs +267 -55
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +154 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +38 -1
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +445 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +51 -0
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
|
@@ -0,0 +1,445 @@
|
|
|
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
|
+
const DISCOVERY_STATES = ['idle', 'waiting', 'queued', 'running', 'complete', 'partial', 'unavailable'];
|
|
9
|
+
const DISCOVERY_BLOCKED = ['source_consent_required', 'missing_key', 'no_source', 'paused', 'unsupported_service', 'endpoint_unavailable'];
|
|
10
|
+
const DISCOVERY_REASONS = {
|
|
11
|
+
source_consent_required: 'Architecture discovery needs source-transmission consent. Enable source mode for this project in Graphlin setup.',
|
|
12
|
+
missing_key: 'Configure a classification service key in Graphlin setup to discover architecture.',
|
|
13
|
+
no_source: 'No source evidence is available yet. Let Graphlin inspect project files, then try again.',
|
|
14
|
+
paused: 'Classification is paused. Resume classification to discover architecture.',
|
|
15
|
+
analysis_failed: 'Architecture discovery could not finish. Try again; supported boundaries remain available.',
|
|
16
|
+
unsupported_service: 'The configured classification service does not support architecture discovery.',
|
|
17
|
+
source_changed: 'Source changed during discovery. Boundaries will be checked against current evidence.',
|
|
18
|
+
partial_coverage: 'Some evidence is unavailable. The shown boundaries cover only inspected evidence.',
|
|
19
|
+
none_supported: 'No supported application or component boundaries were found. Source scopes remain available.',
|
|
20
|
+
endpoint_unavailable: 'Architecture discovery requires a newer local service.',
|
|
21
|
+
request_failed: 'Could not check architecture discovery. Reconnect to the local service or try again.',
|
|
22
|
+
};
|
|
23
|
+
function discoveryStatus(value) {
|
|
24
|
+
if (!value || !DISCOVERY_STATES.includes(value.status)) throw new Error('invalid_architecture_status');
|
|
25
|
+
const result = { status: value.status, reason: typeof value.reason === 'string' ? value.reason : '' };
|
|
26
|
+
for (const name of ['applications', 'components', 'pending', 'inspected', 'total']) {
|
|
27
|
+
if (Number.isSafeInteger(value[name]) && value[name] >= 0 && value[name] <= 1_000_000) result[name] = value[name];
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createViewPlatform({ document, request, onView, onSelect, onFollow = () => {},
|
|
33
|
+
createFrame = createExtensionFrame, grantPollMs = 2000, architecturePollMs = 2000 }) {
|
|
34
|
+
const $ = id => document.getElementById(id);
|
|
35
|
+
let model, active = 'graphlin.code', instance, installed = [], generation = 0, closed = false;
|
|
36
|
+
let projectionController;
|
|
37
|
+
let analysisBusy = false;
|
|
38
|
+
let grantWatch;
|
|
39
|
+
let architectureWatch, architectureState, architectureProject, architectureBusy = false, suspended = false;
|
|
40
|
+
let selection = {}, canonicalSelection = null, follow = true, query = '', kinds = null, focusEntityId = null;
|
|
41
|
+
const settings = new Map(), listeners = [];
|
|
42
|
+
const ownSettings = () => {
|
|
43
|
+
if (!settings.has(active)) settings.set(active, { expanded: [], collapsed: [], level: 'applications' });
|
|
44
|
+
return settings.get(active);
|
|
45
|
+
};
|
|
46
|
+
const element = (tag, text) => {
|
|
47
|
+
const value = document.createElement(tag);
|
|
48
|
+
if (text !== undefined) value.textContent = text;
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
const listen = (id, event, handler) => {
|
|
52
|
+
const node = $(id);
|
|
53
|
+
if (node) { node.addEventListener(event, handler); listeners.push(() => node.removeEventListener(event, handler)); }
|
|
54
|
+
};
|
|
55
|
+
function status(message = '') {
|
|
56
|
+
$('view-status').textContent = message;
|
|
57
|
+
$('view-status').hidden = !message;
|
|
58
|
+
$('view-retry').hidden = !message;
|
|
59
|
+
}
|
|
60
|
+
function dispose() {
|
|
61
|
+
stopArchitecture();
|
|
62
|
+
if (grantWatch) { clearTimeout(grantWatch.timer); grantWatch.controller?.abort(); grantWatch = null; }
|
|
63
|
+
instance?.dispose(); instance = null;
|
|
64
|
+
}
|
|
65
|
+
const architectureLive = () => !closed && !suspended && model && active === 'graphlin.c4' && !selection.checkpoint;
|
|
66
|
+
function stopArchitecture() {
|
|
67
|
+
if (architectureWatch) {
|
|
68
|
+
clearTimeout(architectureWatch.timer);
|
|
69
|
+
clearTimeout(architectureWatch.deadline);
|
|
70
|
+
architectureWatch.controller?.abort();
|
|
71
|
+
architectureWatch = null;
|
|
72
|
+
}
|
|
73
|
+
architectureBusy = false;
|
|
74
|
+
}
|
|
75
|
+
function renderArchitecture() {
|
|
76
|
+
const shown = active === 'graphlin.c4';
|
|
77
|
+
$('architecture-discover').hidden = !shown;
|
|
78
|
+
$('architecture-status').hidden = !shown;
|
|
79
|
+
const state = architectureState;
|
|
80
|
+
$('architecture-discover').disabled = !architectureLive() || architectureBusy ||
|
|
81
|
+
['queued', 'running'].includes(state?.status) || DISCOVERY_BLOCKED.includes(state?.reason);
|
|
82
|
+
$('architecture-discover').setAttribute('aria-busy', String(architectureBusy));
|
|
83
|
+
if (!shown) return;
|
|
84
|
+
if (selection.checkpoint) {
|
|
85
|
+
$('architecture-status').textContent = 'Recorded architecture. Return to Live to discover current boundaries.';
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!state) { $('architecture-status').textContent = 'Checking architecture discovery…'; return; }
|
|
89
|
+
const messages = {
|
|
90
|
+
idle: 'Ready to discover application and component boundaries for this project.',
|
|
91
|
+
waiting: 'Waiting for source evidence before architecture discovery can continue.',
|
|
92
|
+
queued: 'Architecture discovery queued.',
|
|
93
|
+
running: 'Discovering architecture…',
|
|
94
|
+
complete: 'Architecture discovery complete.',
|
|
95
|
+
partial: 'Architecture is partly discovered; some boundaries remain unknown.',
|
|
96
|
+
unavailable: 'Architecture discovery is unavailable. Check project source settings and the classification service.',
|
|
97
|
+
};
|
|
98
|
+
const counts = ['applications', 'components'].filter(name => state[name] !== undefined)
|
|
99
|
+
.map(name => `${state[name]} ${name}`);
|
|
100
|
+
if (state.inspected !== undefined && state.total !== undefined) counts.push(`${state.inspected} of ${state.total} inspected`);
|
|
101
|
+
if (state.pending > 0) counts.push(`${state.pending} pending`);
|
|
102
|
+
$('architecture-status').textContent = [
|
|
103
|
+
DISCOVERY_REASONS[state.reason] || messages[state.status], counts.join(' · '),
|
|
104
|
+
].filter(Boolean).join(' ');
|
|
105
|
+
}
|
|
106
|
+
async function refreshArchitecture(watch, manual = false) {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
watch.controller = controller;
|
|
109
|
+
const deadline = watch.deadline = setTimeout(() => controller.abort(), 8000);
|
|
110
|
+
try {
|
|
111
|
+
const result = await request(manual ? '/api/architecture/discover' : '/api/architecture',
|
|
112
|
+
{ signal: controller.signal, ...(manual ? { method: 'POST', body: '{}' } : {}) });
|
|
113
|
+
if (architectureWatch !== watch || !architectureLive()) return;
|
|
114
|
+
architectureState = discoveryStatus(manual && !DISCOVERY_STATES.includes(result?.status) ? { status: 'queued' } : result);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (architectureWatch !== watch || !architectureLive()) return;
|
|
117
|
+
architectureState = { status: 'unavailable', reason: error.status === 404 ? 'endpoint_unavailable' : 'request_failed' };
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(deadline);
|
|
120
|
+
if (architectureWatch === watch && architectureLive()) {
|
|
121
|
+
architectureBusy = false;
|
|
122
|
+
renderArchitecture();
|
|
123
|
+
if (architectureState?.reason !== 'endpoint_unavailable')
|
|
124
|
+
watch.timer = setTimeout(() => refreshArchitecture(watch), architecturePollMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function syncArchitecture() {
|
|
129
|
+
if (!architectureLive()) stopArchitecture();
|
|
130
|
+
else if (!architectureWatch) {
|
|
131
|
+
if (architectureProject !== model.projectId) { architectureState = null; architectureProject = model.projectId; }
|
|
132
|
+
architectureWatch = {};
|
|
133
|
+
void refreshArchitecture(architectureWatch);
|
|
134
|
+
}
|
|
135
|
+
renderArchitecture();
|
|
136
|
+
}
|
|
137
|
+
function watchGrant(row) {
|
|
138
|
+
if (grantWatch?.instance === instance) return;
|
|
139
|
+
const watch = { instance, id: row.id, digest: row.digest, grant: JSON.stringify(row.grant), timer: null };
|
|
140
|
+
grantWatch = watch;
|
|
141
|
+
const check = async () => {
|
|
142
|
+
if (closed || grantWatch !== watch || instance !== watch.instance) return;
|
|
143
|
+
watch.controller = new AbortController();
|
|
144
|
+
try {
|
|
145
|
+
const response = await request('/api/extensions', { signal: watch.controller.signal });
|
|
146
|
+
if (closed || grantWatch !== watch) return;
|
|
147
|
+
const rows = Array.isArray(response) ? response : response.extensions;
|
|
148
|
+
if (!Array.isArray(rows)) throw new Error('invalid_extension_catalogue');
|
|
149
|
+
const current = rows.find(value => value.id === watch.id);
|
|
150
|
+
if (!current || current.digest !== watch.digest || !current.grant?.approved ||
|
|
151
|
+
JSON.stringify(current.grant) !== watch.grant) {
|
|
152
|
+
generation++; dispose(); onView({ clear: true });
|
|
153
|
+
installed = rows; controls();
|
|
154
|
+
if (current) consent(current);
|
|
155
|
+
status('Visualizer access or its installed version changed. Review access before continuing.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
if (closed || grantWatch !== watch) return;
|
|
160
|
+
generation++; dispose(); onView({ clear: true });
|
|
161
|
+
status('Visualizer access could not be verified. Retry the view after reconnecting.');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (grantWatch === watch) watch.timer = setTimeout(check, grantPollMs);
|
|
165
|
+
};
|
|
166
|
+
watch.timer = setTimeout(check, grantPollMs);
|
|
167
|
+
}
|
|
168
|
+
function descriptor() { return BUILTIN_VIEWS.find(view => view.id === active) || installed.find(view => view.id === active); }
|
|
169
|
+
const profilesFor = row => Array.isArray(row?.profiles) ? row.profiles.filter(profile => validId(profile?.id)) : [];
|
|
170
|
+
function options(select, values, chosen) {
|
|
171
|
+
const signature = JSON.stringify(values);
|
|
172
|
+
if (select.dataset.options !== signature) {
|
|
173
|
+
select.replaceChildren(...values.map(([id, label]) => { const option = element('option', label); option.value = id; return option; }));
|
|
174
|
+
select.dataset.options = signature;
|
|
175
|
+
}
|
|
176
|
+
select.value = chosen || '';
|
|
177
|
+
}
|
|
178
|
+
function controls() {
|
|
179
|
+
options($('visualizer'), [...BUILTIN_VIEWS.map(view => [view.id, view.name]),
|
|
180
|
+
...installed.map(view => [view.id, view.manifest.name || view.id])], active);
|
|
181
|
+
for (const option of $('visualizer').children) option.disabled = !model && option.value !== 'graphlin.code';
|
|
182
|
+
$('view-context').hidden = !model;
|
|
183
|
+
$('c4-level-label').hidden = active !== 'graphlin.c4';
|
|
184
|
+
syncArchitecture();
|
|
185
|
+
$('baseline-label').hidden = active !== 'graphlin.changes';
|
|
186
|
+
$('baseline-create').hidden = active !== 'graphlin.changes';
|
|
187
|
+
$('baseline-create').disabled = Boolean(selection.checkpoint);
|
|
188
|
+
const row = descriptor(), profiles = profilesFor(row);
|
|
189
|
+
$('analysis-controls').hidden = !profiles.length;
|
|
190
|
+
options($('analysis-profile'), profiles.map(profile => [profile.id,
|
|
191
|
+
`${profile.id}${row.grant?.profiles?.includes(profile.id) ? '' : ' (approval needed)'}`]),
|
|
192
|
+
ownSettings().profile || profiles[0]?.id);
|
|
193
|
+
$('analysis-run').disabled = analysisBusy || Boolean(selection.checkpoint) ||
|
|
194
|
+
!row?.grant?.approved || !row.grant.profiles?.includes($('analysis-profile').value) || !model?.entities.length;
|
|
195
|
+
$('c4-level').value = ownSettings().level;
|
|
196
|
+
const checkpoints = (model?.checkpoints || []).map(checkpoint => [checkpoint.id, checkpoint.label || `Revision ${checkpoint.revision}`]);
|
|
197
|
+
options($('task-baseline'), [['', 'Choose checkpoint'], ...checkpoints], ownSettings().baseline);
|
|
198
|
+
options($('model-position'), [['', 'Live'], ...checkpoints], selection.checkpoint);
|
|
199
|
+
const byId = new Map((model?.entities || []).map(entity => [entity.id, entity]));
|
|
200
|
+
const path = selection.scope ? [...ancestors(selection.scope, byId).reverse(), selection.scope] : [];
|
|
201
|
+
const crumbs = [['', 'Project'], ...path.map(id => [id, byId.get(id)?.label || 'Scope'])];
|
|
202
|
+
const breadcrumbSignature = JSON.stringify(crumbs);
|
|
203
|
+
if ($('scope-breadcrumbs').dataset.signature === breadcrumbSignature) return;
|
|
204
|
+
const buttons = crumbs.map(([id, label]) => {
|
|
205
|
+
const button = element('button', label); button.setAttribute('type', 'button');
|
|
206
|
+
button.setAttribute('aria-current', id === (selection.scope || '') ? 'location' : 'false');
|
|
207
|
+
button.addEventListener('click', () => scope(id));
|
|
208
|
+
return button;
|
|
209
|
+
});
|
|
210
|
+
$('scope-breadcrumbs').replaceChildren(...buttons);
|
|
211
|
+
$('scope-breadcrumbs').dataset.signature = breadcrumbSignature;
|
|
212
|
+
}
|
|
213
|
+
function consent(row) {
|
|
214
|
+
$('extension-access').hidden = false;
|
|
215
|
+
const key = `${model.projectId}:${row.id}:${row.digest}:${JSON.stringify(row.grant)}`;
|
|
216
|
+
if ($('extension-access').dataset.key === key) return;
|
|
217
|
+
$('extension-access').dataset.key = key;
|
|
218
|
+
$('extension-access-description').textContent = `${row.manifest.name || row.id} can read the selected project data. ` +
|
|
219
|
+
'Approval applies to this installed version. Source text, excerpts, prompts, and transcripts are never provided.';
|
|
220
|
+
const capabilities = row.manifest.capabilities || [];
|
|
221
|
+
const fields = DATA_FIELDS.filter(field => field !== 'checkpoints' &&
|
|
222
|
+
capabilities.includes(field === 'activity' ? 'activity.read' : 'model.read'));
|
|
223
|
+
$('extension-fields').replaceChildren(...fields.map(field => {
|
|
224
|
+
const label = element('label'), checkbox = element('input');
|
|
225
|
+
checkbox.type = 'checkbox'; checkbox.value = field;
|
|
226
|
+
checkbox.checked = row.grant ? row.grant.fields?.includes(field) : ['entities', 'relations', 'coverage', 'activity'].includes(field);
|
|
227
|
+
label.append(checkbox, element('span', field)); return label;
|
|
228
|
+
}));
|
|
229
|
+
$('extension-history').checked = row.grant?.history === true;
|
|
230
|
+
$('extension-history').disabled = !capabilities.includes('history.read');
|
|
231
|
+
const profiles = profilesFor(row);
|
|
232
|
+
$('extension-profiles').replaceChildren(...profiles.map(profile => {
|
|
233
|
+
const label = element('label'), checkbox = element('input');
|
|
234
|
+
checkbox.type = 'checkbox'; checkbox.value = profile.id;
|
|
235
|
+
checkbox.checked = row.grant?.profiles?.includes(profile.id) || false;
|
|
236
|
+
label.append(checkbox, element('span', `Allow ${profile.id} analysis (${(profile.selectors?.fields || []).join(', ') || 'declared fields'})`));
|
|
237
|
+
return label;
|
|
238
|
+
}));
|
|
239
|
+
$('analysis-note').hidden = !profiles.length;
|
|
240
|
+
}
|
|
241
|
+
async function catalogue(signal) {
|
|
242
|
+
try {
|
|
243
|
+
const result = await request('/api/extensions', { signal });
|
|
244
|
+
if (closed) return;
|
|
245
|
+
installed = (Array.isArray(result) ? result : result.extensions || []).filter(row =>
|
|
246
|
+
extensionId(row.id) && digest(row.digest) && row.manifest?.graphlinApi === '1' &&
|
|
247
|
+
row.manifest?.modelSchema === '2' && ['custom', 'graphlin-scene'].includes(row.manifest?.renderer?.kind));
|
|
248
|
+
controls();
|
|
249
|
+
} catch { /* Older daemons still provide the legacy code map. */ }
|
|
250
|
+
}
|
|
251
|
+
async function project(streamed = false, force = false) {
|
|
252
|
+
if (!model || closed) return;
|
|
253
|
+
projectionController?.abort();
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
projectionController = controller;
|
|
256
|
+
const mine = ++generation, view = descriptor();
|
|
257
|
+
controls();
|
|
258
|
+
$('extension-access').hidden = true;
|
|
259
|
+
if (!view) { dispose(); onView({ clear: true }); status('This visualizer is no longer installed. Choose another view.'); return; }
|
|
260
|
+
try {
|
|
261
|
+
let inputModel = model;
|
|
262
|
+
const inputSettings = { ...ownSettings(), ...selection, query, kinds: kinds ? [...kinds] : null, follow };
|
|
263
|
+
if (BUILTIN_VIEWS.some(item => item.id === active)) {
|
|
264
|
+
if (!instance) instance = createBuiltin(active, { root: $('custom-view'), select: onSelect });
|
|
265
|
+
let baseline;
|
|
266
|
+
if (active === 'graphlin.changes' && inputSettings.baseline) {
|
|
267
|
+
const baselineSelection = { ...selection, checkpoint: inputSettings.baseline };
|
|
268
|
+
const raw = await request(`/api/model/v1/snapshot${modelQuery(baselineSelection)}`, { signal: controller.signal });
|
|
269
|
+
baseline = await hydrateModel(raw, { request, selection: baselineSelection, signal: controller.signal });
|
|
270
|
+
if (mine !== generation || closed) return;
|
|
271
|
+
}
|
|
272
|
+
const result = await instance.update({ model: inputModel, settings: inputSettings, baseline,
|
|
273
|
+
apiVersion: 1, projectId: model.projectId, revision: model.revision, viewEpoch: mine });
|
|
274
|
+
if (mine !== generation || closed) return;
|
|
275
|
+
if (result.scene) result.scene = filterScene(result.scene, { query, kinds }, inputModel);
|
|
276
|
+
status();
|
|
277
|
+
onView({ ...result, model: inputModel, id: active, name: view.name, streamed, force,
|
|
278
|
+
focusEntityId: streamed ? focusEntityId : null, selection: canonicalSelection });
|
|
279
|
+
} else {
|
|
280
|
+
// Re-read catalogue/grant for every delivery; an old row is never a
|
|
281
|
+
// continuing permission. Clear controlled content before policy refresh.
|
|
282
|
+
onView({ clear: true });
|
|
283
|
+
await catalogue(controller.signal);
|
|
284
|
+
if (mine !== generation || closed) return;
|
|
285
|
+
const current = installed.find(row => row.id === active);
|
|
286
|
+
if (!current || current.digest !== view.digest) { dispose(); status('The installed version changed. Select it again to review access.'); return; }
|
|
287
|
+
if (JSON.stringify(current.grant) !== JSON.stringify(view.grant)) dispose();
|
|
288
|
+
if (!current.grant?.approved || current.grant.projectId !== model.projectId ||
|
|
289
|
+
current.grant.digest !== current.digest || (selection.checkpoint && !current.grant.history)) {
|
|
290
|
+
dispose(); consent(current); status(); return;
|
|
291
|
+
}
|
|
292
|
+
inputModel = await request(`/api/extensions/data/${encodeURIComponent(active)}${modelQuery(selection)}`, { signal: controller.signal });
|
|
293
|
+
if (mine !== generation || closed) return;
|
|
294
|
+
if (!instance) instance = createFrame({
|
|
295
|
+
root: $('custom-view'), extension: current, onSelect,
|
|
296
|
+
onFailure: () => { dispose(); onView({ clear: true }); status('Visualizer stopped. Retry view to restart it.'); },
|
|
297
|
+
});
|
|
298
|
+
watchGrant(current);
|
|
299
|
+
const requestedSelection = inputModel.entities.some(entity => entity.id === canonicalSelection)
|
|
300
|
+
? { entityId: canonicalSelection } : null;
|
|
301
|
+
const result = await instance.update({ model: inputModel, settings: inputSettings, selection: requestedSelection, viewEpoch: mine });
|
|
302
|
+
if (mine !== generation || closed) return;
|
|
303
|
+
if (result.scene) result.scene = filterScene(result.scene, { query, kinds }, inputModel);
|
|
304
|
+
status();
|
|
305
|
+
onView({ ...result, model: inputModel, id: active, name: current.manifest.name || active, streamed, force,
|
|
306
|
+
focusEntityId: streamed ? focusEntityId : null, selection: canonicalSelection });
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (mine !== generation || closed) return;
|
|
310
|
+
if ([401, 403, 404].includes(error.status)) { dispose(); onView({ clear: true }); }
|
|
311
|
+
else if (view.manifest) {
|
|
312
|
+
await choose('graphlin.code');
|
|
313
|
+
status('The visualizer stopped. Code is shown; select the visualizer to retry.');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
status('Visualizer unavailable. Retry the view or choose Code.');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const client = createModelClient({ request, onError: status, onSnapshot(value, streamed) {
|
|
320
|
+
focusEntityId = null;
|
|
321
|
+
if (model && streamed && follow && !selection.checkpoint && value.projectId === model.projectId) {
|
|
322
|
+
const known = new Set(model.entities.map(entity => entity.id));
|
|
323
|
+
const arrival = value.entities.filter(entity => !known.has(entity.id)).at(-1)?.id;
|
|
324
|
+
const observed = value.activity.filter(event => event.sequence > model.sequence).at(-1)?.entityIds?.[0];
|
|
325
|
+
focusEntityId = observed || arrival || null;
|
|
326
|
+
if (focusEntityId) {
|
|
327
|
+
const byId = new Map(value.entities.map(entity => [entity.id, entity]));
|
|
328
|
+
ownSettings().expanded = [...new Set([...ownSettings().expanded, ...ancestors(focusEntityId, byId)])];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (model && model.projectId !== value.projectId) {
|
|
332
|
+
dispose(); settings.clear(); canonicalSelection = null; selection = {};
|
|
333
|
+
onView({ clear: true });
|
|
334
|
+
}
|
|
335
|
+
model = value;
|
|
336
|
+
void project(streamed);
|
|
337
|
+
} });
|
|
338
|
+
function choose(id) {
|
|
339
|
+
if (id === active) return;
|
|
340
|
+
if (!model) { $('visualizer').value = 'graphlin.code'; status('Model views require a newer local service. The Code map remains available.'); return; }
|
|
341
|
+
generation++; dispose(); active = id;
|
|
342
|
+
onView({ clear: true });
|
|
343
|
+
return project(false, true);
|
|
344
|
+
}
|
|
345
|
+
function scope(id) {
|
|
346
|
+
selection.scope = id || undefined;
|
|
347
|
+
generation++; dispose();
|
|
348
|
+
void client.open(selection);
|
|
349
|
+
}
|
|
350
|
+
async function grant(approved) {
|
|
351
|
+
const row = installed.find(value => value.id === active);
|
|
352
|
+
if (!row) return;
|
|
353
|
+
generation++; dispose(); onView({ clear: true });
|
|
354
|
+
const fields = approved ? [...$('extension-fields').children]
|
|
355
|
+
.map(label => label.children[0]).filter(input => input.checked).map(input => input.value) : [];
|
|
356
|
+
const history = approved && $('extension-history').checked;
|
|
357
|
+
const profiles = approved ? [...$('extension-profiles').children].map(label => label.children[0])
|
|
358
|
+
.filter(input => input.checked).map(input => input.value) : [];
|
|
359
|
+
if (history) fields.push('checkpoints');
|
|
360
|
+
try {
|
|
361
|
+
await request('/api/extensions/grant', { method: 'POST',
|
|
362
|
+
body: JSON.stringify({ id: row.id, digest: row.digest, fields, history, approved, profiles }) });
|
|
363
|
+
await catalogue();
|
|
364
|
+
if (approved) await project(false, true);
|
|
365
|
+
else { $('extension-access').hidden = true; status('Access denied. Choose another view.'); }
|
|
366
|
+
} catch { status('Access could not be updated. Retry after reconnecting.'); }
|
|
367
|
+
}
|
|
368
|
+
listen('visualizer', 'change', () => choose($('visualizer').value));
|
|
369
|
+
listen('view-retry', 'click', () => { dispose(); void client.open(selection); });
|
|
370
|
+
listen('c4-level', 'change', () => { ownSettings().level = $('c4-level').value; void project(false, true); });
|
|
371
|
+
listen('architecture-discover', 'click', () => {
|
|
372
|
+
if (!architectureLive() || $('architecture-discover').disabled) return;
|
|
373
|
+
stopArchitecture();
|
|
374
|
+
architectureBusy = true;
|
|
375
|
+
architectureState = { status: 'queued' };
|
|
376
|
+
architectureWatch = {};
|
|
377
|
+
renderArchitecture();
|
|
378
|
+
void refreshArchitecture(architectureWatch, true);
|
|
379
|
+
});
|
|
380
|
+
listen('task-baseline', 'change', () => { ownSettings().baseline = $('task-baseline').value; void project(false, true); });
|
|
381
|
+
listen('baseline-create', 'click', async () => {
|
|
382
|
+
if (selection.checkpoint) return;
|
|
383
|
+
const button = $('baseline-create');
|
|
384
|
+
button.disabled = true;
|
|
385
|
+
try {
|
|
386
|
+
const marker = await request('/api/model/v1/checkpoints', { method: 'POST',
|
|
387
|
+
body: JSON.stringify({ label: 'Task baseline', ...(selection.session ? { sessionId: selection.session } : {}) }) });
|
|
388
|
+
ownSettings().baseline = marker.id || marker.checkpoint?.id || marker.marker?.id;
|
|
389
|
+
await client.open(selection);
|
|
390
|
+
} catch { status('The checkpoint could not be saved. Retry after reconnecting.'); }
|
|
391
|
+
finally { button.disabled = Boolean(selection.checkpoint); }
|
|
392
|
+
});
|
|
393
|
+
listen('model-position', 'change', () => {
|
|
394
|
+
selection.checkpoint = $('model-position').value;
|
|
395
|
+
controls();
|
|
396
|
+
void client.open(selection);
|
|
397
|
+
});
|
|
398
|
+
listen('follow-agent', 'change', () => { follow = $('follow-agent').checked; onFollow(follow); void project(); });
|
|
399
|
+
listen('extension-approve', 'click', () => grant(true));
|
|
400
|
+
listen('extension-deny', 'click', () => grant(false));
|
|
401
|
+
listen('analysis-profile', 'change', () => { ownSettings().profile = $('analysis-profile').value; controls(); });
|
|
402
|
+
listen('analysis-access', 'click', () => { const row = descriptor(); if (row?.manifest) consent(row); });
|
|
403
|
+
listen('analysis-run', 'click', async () => {
|
|
404
|
+
const row = descriptor(), profile = profilesFor(row).find(profile => profile.id === $('analysis-profile').value);
|
|
405
|
+
if (analysisBusy || !profile || !row.grant?.approved || !row.grant.profiles?.includes(profile.id) || selection.checkpoint) return;
|
|
406
|
+
const byId = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
407
|
+
const candidates = byId.has(canonicalSelection) ? [canonicalSelection] : model.entities
|
|
408
|
+
.filter(entity => !selection.scope || entity.id === selection.scope || ancestors(entity.id, byId).includes(selection.scope))
|
|
409
|
+
.map(entity => entity.id);
|
|
410
|
+
const entityIds = candidates.filter(id => !profile.selectors?.candidateIds?.length || profile.selectors.candidateIds.includes(id)).slice(0, 256);
|
|
411
|
+
if (!entityIds.length) { status('Select a supported entity in the current scope before running this profile.'); return; }
|
|
412
|
+
analysisBusy = true; controls();
|
|
413
|
+
try {
|
|
414
|
+
const result = await request('/api/extensions/analysis', { method: 'POST',
|
|
415
|
+
body: JSON.stringify({ id: row.id, digest: row.digest, profileId: profile.id, entityIds, revision: model.revision }) });
|
|
416
|
+
if (active === row.id && !closed) status(result.status === 'complete' ? 'Analysis complete.'
|
|
417
|
+
: result.status === 'unavailable' ? 'Analysis is unavailable under the current source-transmission settings or service status.'
|
|
418
|
+
: 'Analysis requested. Accepted results will arrive with model updates.');
|
|
419
|
+
} catch {
|
|
420
|
+
if (active === row.id && !closed) status('Analysis was not accepted. Check profile/data approval and this project’s source-transmission setting.');
|
|
421
|
+
} finally { analysisBusy = false; if (!closed) controls(); }
|
|
422
|
+
});
|
|
423
|
+
controls();
|
|
424
|
+
return {
|
|
425
|
+
async start() { suspended = false; if (await client.open(selection)) await catalogue(); },
|
|
426
|
+
choose,
|
|
427
|
+
filter(nextQuery, nextKinds) { query = nextQuery; kinds = nextKinds; void project(false, true); },
|
|
428
|
+
selected(id) { canonicalSelection = id; },
|
|
429
|
+
scope,
|
|
430
|
+
session(id) { selection.session = id || undefined; generation++; dispose(); void client.open(selection); },
|
|
431
|
+
live() { selection.checkpoint = undefined; generation++; dispose(); void client.open(selection); },
|
|
432
|
+
toggle(id, isCollapsed = true) {
|
|
433
|
+
const own = ownSettings(), expanded = new Set(own.expanded), collapsed = new Set(own.collapsed);
|
|
434
|
+
if (!isCollapsed) { expanded.delete(id); collapsed.add(id); }
|
|
435
|
+
else { expanded.add(id); collapsed.delete(id); }
|
|
436
|
+
own.expanded = [...expanded]; own.collapsed = [...collapsed];
|
|
437
|
+
void project(false, true);
|
|
438
|
+
},
|
|
439
|
+
close() { closed = true; generation++; projectionController?.abort(); client.close(); dispose(); listeners.forEach(remove => remove()); },
|
|
440
|
+
suspend() { suspended = true; generation++; projectionController?.abort(); client.suspend(); dispose(); },
|
|
441
|
+
get active() { return active; },
|
|
442
|
+
get model() { return model; },
|
|
443
|
+
get selection() { return selection; },
|
|
444
|
+
};
|
|
445
|
+
}
|
|
@@ -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
|
+
}
|
package/runtime/web/style.css
CHANGED
|
@@ -15,6 +15,53 @@
|
|
|
15
15
|
|
|
16
16
|
* { box-sizing: border-box; }
|
|
17
17
|
[hidden] { display: none !important; }
|
|
18
|
+
.view-context { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; padding: 5px 12px; border-bottom: 1px solid var(--line); font-size: 11px; }
|
|
19
|
+
.view-context label { display: inline-flex; gap: 5px; align-items: center; }
|
|
20
|
+
.view-context select, .view-context button { max-width: 210px; font-size: 11px; min-height: 28px; padding: 3px 7px; color: var(--ink); background: var(--white); border: 1px solid var(--line); border-radius: 5px; }
|
|
21
|
+
#scope-breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; }
|
|
22
|
+
.view-status, .view-coverage { padding: 4px 12px; font-size: 11px; color: var(--muted); }
|
|
23
|
+
.architecture-status { padding: 5px 12px; font-size: 11px; color: var(--muted); border-bottom: 1px solid var(--line); }
|
|
24
|
+
.view-context #architecture-discover { color: var(--blue); border-color: var(--blue); }
|
|
25
|
+
.extension-access { padding: 12px; border-bottom: 1px solid var(--line); background: var(--paper); font-size: 12px; }
|
|
26
|
+
.extension-access p { max-width: 78ch; margin-bottom: 8px; }
|
|
27
|
+
#extension-fields { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; }
|
|
28
|
+
#extension-profiles { display: flex; flex-wrap: wrap; gap: 8px 14px; margin: 8px 0; }
|
|
29
|
+
#analysis-controls { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
30
|
+
.diagram-group .group-frame { fill: var(--paper); fill-opacity: .7; stroke: var(--node-ink); stroke-width: 1.5; }
|
|
31
|
+
.diagram-group[data-change="unknown"] .group-frame, .diagram-group[data-change="discovered"] .group-frame { stroke-dasharray: 5 4; }
|
|
32
|
+
.diagram-group[data-change="modified"] .group-frame { stroke: var(--amber); }
|
|
33
|
+
.diagram-group[data-change="added"] .group-frame { stroke: var(--teal); }
|
|
34
|
+
.diagram-group:focus-visible { outline: none; }
|
|
35
|
+
.diagram-group:focus-visible .group-frame, .diagram-group[aria-pressed="true"] .group-frame { stroke: var(--diagram-focus); stroke-width: 3; }
|
|
36
|
+
.diagram-group[data-change="stale"] .group-frame { stroke-dasharray: 7 4; opacity: .65; }
|
|
37
|
+
.group-heading { fill: var(--ink); font: 600 14px var(--body); }
|
|
38
|
+
.group-summary { fill: var(--muted); font: 11px var(--body); }
|
|
39
|
+
.group-toggle { cursor: pointer; }
|
|
40
|
+
.group-toggle rect { fill: var(--white); stroke: var(--line); }
|
|
41
|
+
.group-toggle text { fill: var(--ink); font: 16px var(--body); }
|
|
42
|
+
.group-toggle:focus-visible { outline: none; }
|
|
43
|
+
.group-toggle:focus-visible rect { stroke: var(--diagram-focus); stroke-width: 3; }
|
|
44
|
+
.diagram-node[data-change="added"] .node-shape { stroke: var(--teal); stroke-width: 3; }
|
|
45
|
+
.diagram-node[data-change="discovered"] .node-shape { stroke-dasharray: 3 3; }
|
|
46
|
+
.diagram-node[data-change="modified"] .node-shape { stroke: var(--amber); stroke-width: 3; }
|
|
47
|
+
.diagram-node[data-change="removed"] { opacity: .6; }
|
|
48
|
+
.custom-view { position: absolute; inset: 0; overflow: auto; background: var(--canvas); }
|
|
49
|
+
.custom-view iframe { width: 100%; height: 100%; border: 0; background: var(--canvas); }
|
|
50
|
+
.timeline-note { padding: 14px 18px; font-size: 12px; color: var(--muted); }
|
|
51
|
+
.timeline-events { padding: 0 18px 20px; margin: 0; list-style: none; }
|
|
52
|
+
.timeline-event { display: grid; grid-template-columns: 85px minmax(95px, .7fr) minmax(180px, 2fr) 90px; gap: 6px 12px; align-items: center; padding: 9px 0; border-top: 1px solid var(--line); font-size: 12px; }
|
|
53
|
+
.timeline-event button { text-align: left; padding: 4px 8px; min-height: 30px; }
|
|
54
|
+
.timeline-lane { overflow-wrap: anywhere; }
|
|
55
|
+
.timeline-attribution { grid-column: 3; color: var(--muted); font-size: 11px; }
|
|
56
|
+
.timeline-outcome { color: var(--muted); }
|
|
57
|
+
.timeline-event[data-outcome="failed"] .timeline-outcome { color: var(--red); }
|
|
58
|
+
.timeline-event[data-outcome="succeeded"] .timeline-outcome { color: var(--teal); }
|
|
59
|
+
@media (max-width: 650px) {
|
|
60
|
+
.timeline-event { grid-template-columns: 75px 1fr; }
|
|
61
|
+
.timeline-event button, .timeline-attribution { grid-column: 2; }
|
|
62
|
+
.view-context { gap: 6px; }
|
|
63
|
+
#scope-breadcrumbs { flex-basis: 100%; }
|
|
64
|
+
}
|
|
18
65
|
body { margin: 0; color: var(--ink); background: var(--paper); font: 14px/1.5 var(--body); }
|
|
19
66
|
button, input, select { font: inherit; }
|
|
20
67
|
button, a, input, select { -webkit-tap-highlight-color: transparent; }
|
|
@@ -164,6 +211,10 @@ h1 { font-size: clamp(27px, 2.3vw, 36px); letter-spacing: -1px; font-weight: 650
|
|
|
164
211
|
.drawing [data-kind="event"], .sidebar-theme [data-kind="event"] { --node-fill: var(--role-event); }
|
|
165
212
|
.drawing [data-kind="configuration"], .sidebar-theme [data-kind="configuration"] { --node-fill: var(--role-configuration); }
|
|
166
213
|
.drawing [data-kind="package"], .sidebar-theme [data-kind="package"] { --node-fill: var(--role-package); }
|
|
214
|
+
.drawing [data-kind="method"] { --node-fill: var(--role-function); }
|
|
215
|
+
.drawing [data-kind="enum"] { --node-fill: var(--role-class); }
|
|
216
|
+
.drawing [data-kind="type_alias"] { --node-fill: var(--role-interface); }
|
|
217
|
+
.drawing [data-kind="namespace"], .drawing [data-kind="directory"], .drawing [data-kind="project"] { --node-fill: var(--role-package); }
|
|
167
218
|
.drawing button:hover:not(:disabled) { background: var(--soft); border-color: var(--diagram-focus); }
|
|
168
219
|
.drawing :is(button, input, select, summary):focus-visible { outline-color: var(--diagram-focus); }
|
|
169
220
|
.drawing-toolbar { display: flex; flex-wrap: wrap; align-items: center; padding: 6px 12px; gap: 6px 18px; background: var(--canvas); border-bottom: 1px solid var(--line); flex-shrink: 0; }
|
package/scripts/arguments.mjs
CHANGED
|
@@ -21,8 +21,12 @@ export function parseArguments(args, { worker = false } = {}) {
|
|
|
21
21
|
const value = args.shift();
|
|
22
22
|
if (!/^\d{1,5}$/.test(value ?? '') || Number(value) > 65535) throw new Error('invalid_port');
|
|
23
23
|
values.port = Number(value);
|
|
24
|
+
} else if (argument === '--local-source') {
|
|
25
|
+
if (seen.has('--allow-source') || seen.has('--no-source')) throw new Error('conflicting_arguments');
|
|
26
|
+
values.allowSource = false;
|
|
27
|
+
values.localSource = true;
|
|
24
28
|
} else if (argument === '--allow-source' || argument === '--no-source' && !worker) {
|
|
25
|
-
if (seen.has(argument === '--allow-source' ? '--no-source' : '--allow-source')) throw new Error('conflicting_arguments');
|
|
29
|
+
if (seen.has('--local-source') || seen.has(argument === '--allow-source' ? '--no-source' : '--allow-source')) throw new Error('conflicting_arguments');
|
|
26
30
|
values.allowSource = argument === '--allow-source';
|
|
27
31
|
}
|
|
28
32
|
else if (argument === '--persist-evidence') values.persistEvidence = true;
|