graphlin 0.1.3 → 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 +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +36 -1
- 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 +49 -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,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
|
+
}
|
package/runtime/web/style.css
CHANGED
|
@@ -15,6 +15,51 @@
|
|
|
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
|
+
.extension-access { padding: 12px; border-bottom: 1px solid var(--line); background: var(--paper); font-size: 12px; }
|
|
24
|
+
.extension-access p { max-width: 78ch; margin-bottom: 8px; }
|
|
25
|
+
#extension-fields { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; }
|
|
26
|
+
#extension-profiles { display: flex; flex-wrap: wrap; gap: 8px 14px; margin: 8px 0; }
|
|
27
|
+
#analysis-controls { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
28
|
+
.diagram-group .group-frame { fill: var(--paper); fill-opacity: .7; stroke: var(--node-ink); stroke-width: 1.5; }
|
|
29
|
+
.diagram-group[data-change="unknown"] .group-frame, .diagram-group[data-change="discovered"] .group-frame { stroke-dasharray: 5 4; }
|
|
30
|
+
.diagram-group[data-change="modified"] .group-frame { stroke: var(--amber); }
|
|
31
|
+
.diagram-group[data-change="added"] .group-frame { stroke: var(--teal); }
|
|
32
|
+
.diagram-group:focus-visible { outline: none; }
|
|
33
|
+
.diagram-group:focus-visible .group-frame, .diagram-group[aria-pressed="true"] .group-frame { stroke: var(--diagram-focus); stroke-width: 3; }
|
|
34
|
+
.diagram-group[data-change="stale"] .group-frame { stroke-dasharray: 7 4; opacity: .65; }
|
|
35
|
+
.group-heading { fill: var(--ink); font: 600 14px var(--body); }
|
|
36
|
+
.group-summary { fill: var(--muted); font: 11px var(--body); }
|
|
37
|
+
.group-toggle { cursor: pointer; }
|
|
38
|
+
.group-toggle rect { fill: var(--white); stroke: var(--line); }
|
|
39
|
+
.group-toggle text { fill: var(--ink); font: 16px var(--body); }
|
|
40
|
+
.group-toggle:focus-visible { outline: none; }
|
|
41
|
+
.group-toggle:focus-visible rect { stroke: var(--diagram-focus); stroke-width: 3; }
|
|
42
|
+
.diagram-node[data-change="added"] .node-shape { stroke: var(--teal); stroke-width: 3; }
|
|
43
|
+
.diagram-node[data-change="discovered"] .node-shape { stroke-dasharray: 3 3; }
|
|
44
|
+
.diagram-node[data-change="modified"] .node-shape { stroke: var(--amber); stroke-width: 3; }
|
|
45
|
+
.diagram-node[data-change="removed"] { opacity: .6; }
|
|
46
|
+
.custom-view { position: absolute; inset: 0; overflow: auto; background: var(--canvas); }
|
|
47
|
+
.custom-view iframe { width: 100%; height: 100%; border: 0; background: var(--canvas); }
|
|
48
|
+
.timeline-note { padding: 14px 18px; font-size: 12px; color: var(--muted); }
|
|
49
|
+
.timeline-events { padding: 0 18px 20px; margin: 0; list-style: none; }
|
|
50
|
+
.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; }
|
|
51
|
+
.timeline-event button { text-align: left; padding: 4px 8px; min-height: 30px; }
|
|
52
|
+
.timeline-lane { overflow-wrap: anywhere; }
|
|
53
|
+
.timeline-attribution { grid-column: 3; color: var(--muted); font-size: 11px; }
|
|
54
|
+
.timeline-outcome { color: var(--muted); }
|
|
55
|
+
.timeline-event[data-outcome="failed"] .timeline-outcome { color: var(--red); }
|
|
56
|
+
.timeline-event[data-outcome="succeeded"] .timeline-outcome { color: var(--teal); }
|
|
57
|
+
@media (max-width: 650px) {
|
|
58
|
+
.timeline-event { grid-template-columns: 75px 1fr; }
|
|
59
|
+
.timeline-event button, .timeline-attribution { grid-column: 2; }
|
|
60
|
+
.view-context { gap: 6px; }
|
|
61
|
+
#scope-breadcrumbs { flex-basis: 100%; }
|
|
62
|
+
}
|
|
18
63
|
body { margin: 0; color: var(--ink); background: var(--paper); font: 14px/1.5 var(--body); }
|
|
19
64
|
button, input, select { font: inherit; }
|
|
20
65
|
button, a, input, select { -webkit-tap-highlight-color: transparent; }
|
|
@@ -164,6 +209,10 @@ h1 { font-size: clamp(27px, 2.3vw, 36px); letter-spacing: -1px; font-weight: 650
|
|
|
164
209
|
.drawing [data-kind="event"], .sidebar-theme [data-kind="event"] { --node-fill: var(--role-event); }
|
|
165
210
|
.drawing [data-kind="configuration"], .sidebar-theme [data-kind="configuration"] { --node-fill: var(--role-configuration); }
|
|
166
211
|
.drawing [data-kind="package"], .sidebar-theme [data-kind="package"] { --node-fill: var(--role-package); }
|
|
212
|
+
.drawing [data-kind="method"] { --node-fill: var(--role-function); }
|
|
213
|
+
.drawing [data-kind="enum"] { --node-fill: var(--role-class); }
|
|
214
|
+
.drawing [data-kind="type_alias"] { --node-fill: var(--role-interface); }
|
|
215
|
+
.drawing [data-kind="namespace"], .drawing [data-kind="directory"], .drawing [data-kind="project"] { --node-fill: var(--role-package); }
|
|
167
216
|
.drawing button:hover:not(:disabled) { background: var(--soft); border-color: var(--diagram-focus); }
|
|
168
217
|
.drawing :is(button, input, select, summary):focus-visible { outline-color: var(--diagram-focus); }
|
|
169
218
|
.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;
|
|
@@ -7,6 +7,7 @@ import { publicPackageFiles, validatePackage } from './validate-packages.mjs';
|
|
|
7
7
|
|
|
8
8
|
const SOURCE = fileURLToPath(new URL('../', import.meta.url));
|
|
9
9
|
const SOURCE_DIRECTORIES = ['.claude-plugin', '.codex-plugin', 'skills', 'scripts', 'runtime', 'schemas', 'adapters'];
|
|
10
|
+
const PROTECTED_DIRECTORIES = [...SOURCE_DIRECTORIES, 'docs', 'node_modules'];
|
|
10
11
|
|
|
11
12
|
async function assertDestination(destination, { directory = false } = {}) {
|
|
12
13
|
const absolute = path.resolve(destination), root = path.parse(absolute).root;
|
|
@@ -57,7 +58,7 @@ async function copy(source, destination) {
|
|
|
57
58
|
export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sourceDir = SOURCE } = {}) {
|
|
58
59
|
const source = path.resolve(sourceDir), output = path.resolve(outputDir);
|
|
59
60
|
if (output === source || source.startsWith(`${output}${path.sep}`)) throw new Error('unsafe_output_directory');
|
|
60
|
-
if (
|
|
61
|
+
if (PROTECTED_DIRECTORIES.some(file => output === path.join(source, file) || output.startsWith(`${path.join(source, file)}${path.sep}`))) {
|
|
61
62
|
throw new Error('unsafe_output_directory');
|
|
62
63
|
}
|
|
63
64
|
// Preflight every output before replacing even the first generated profile.
|
|
@@ -92,6 +93,9 @@ export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sou
|
|
|
92
93
|
await mkdir(temporary);
|
|
93
94
|
try {
|
|
94
95
|
const profileFiles = files.filter(file => profile === 'claude' || file !== '.claude-plugin/plugin.json');
|
|
96
|
+
// publicPackageFiles includes every reviewed file of the full pinned
|
|
97
|
+
// parser under Graphlin's own node_modules. Never resolve a hoisted or
|
|
98
|
+
// inspected-project dependency, and never copy an arbitrary dependency tree.
|
|
95
99
|
for (const file of profileFiles) await copy(path.join(source, file), path.join(temporary, file));
|
|
96
100
|
const metadata = JSON.parse(await readFile(path.join(temporary, 'package.json'), 'utf8'));
|
|
97
101
|
metadata.files = metadata.files.filter(file => profileFiles.includes(file.slice(2)));
|
|
@@ -114,7 +118,7 @@ export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sou
|
|
|
114
118
|
await chmod(path.join(temporary, 'scripts', file), 0o755);
|
|
115
119
|
}
|
|
116
120
|
await writeFile(path.join(temporary, 'PACKAGE-NOTES.md'),
|
|
117
|
-
`# Graphlin ${profile} package\n\nNode.js 22.14+; macOS/Linux. Self-contained
|
|
121
|
+
`# Graphlin ${profile} package\n\nNode.js 22.14+; macOS/Linux. Self-contained runtime and parser bundle; no dependency install.\n` +
|
|
118
122
|
'Run scripts/graphlin.mjs --help for local controls. This package does not install itself.\n' +
|
|
119
123
|
'Hook activation/trust has not been certified. Kiro is inactive and experimental.\n' +
|
|
120
124
|
'See adapters/README.md and skills/graphlin/SKILL.md for privacy and coverage.\n');
|
package/scripts/control.mjs
CHANGED
|
@@ -39,7 +39,7 @@ async function handle(line) {
|
|
|
39
39
|
const supported = ['2024-11-05', '2025-03-26', '2025-06-18'];
|
|
40
40
|
return result({ protocolVersion: supported.includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18',
|
|
41
41
|
capabilities: { tools: { listChanged: false } },
|
|
42
|
-
serverInfo: { name: 'graphlin', version: '0.
|
|
42
|
+
serverInfo: { name: 'graphlin', version: '0.2.0' },
|
|
43
43
|
instructions: 'Controls only. Passive host hooks provide observations when separately activated. No drawing calls after each action.' });
|
|
44
44
|
}
|
|
45
45
|
if (message.method === 'ping') return result({});
|
package/scripts/daemon.mjs
CHANGED
|
@@ -5,7 +5,8 @@ process.umask(0o077);
|
|
|
5
5
|
let server;
|
|
6
6
|
try {
|
|
7
7
|
const options = parseArguments(process.argv.slice(2), { worker: true });
|
|
8
|
-
const policy = {
|
|
8
|
+
const policy = { readSource: options.localSource || options.allowSource,
|
|
9
|
+
transmitSource: options.allowSource, persistEvidence: options.persistEvidence,
|
|
9
10
|
displayEvidence: options.displayEvidence };
|
|
10
11
|
let decisionService;
|
|
11
12
|
if (options.mode === 'demo') {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { projectPaths, defaultDataDir } from '../runtime/daemon/paths.mjs';
|
|
3
|
+
import { createExtensionRegistry, extensionError } from '../runtime/extensions/index.mjs';
|
|
4
|
+
|
|
5
|
+
/** Parent CLI dispatches its already-parsed extension arguments here. This
|
|
6
|
+
* module neither parses global CLI options nor starts/stops the daemon. */
|
|
7
|
+
export async function runExtensions(args, {
|
|
8
|
+
projectRoot, dataDir = defaultDataDir(), signal, transport, output = process.stdout,
|
|
9
|
+
registry: suppliedRegistry,
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (!Array.isArray(args) || args.some(value => typeof value !== 'string')) throw extensionError('invalid_extension_arguments');
|
|
12
|
+
const [command = 'list', source, ...rest] = args;
|
|
13
|
+
if (!['list', 'add', 'update', 'remove', 'doctor', 'dev'].includes(command) || rest.length ||
|
|
14
|
+
(['add', 'update', 'remove', 'dev'].includes(command) ? !source : source !== undefined)) {
|
|
15
|
+
throw extensionError('usage_extensions_list_add_update_remove_doctor_dev');
|
|
16
|
+
}
|
|
17
|
+
let registry = suppliedRegistry;
|
|
18
|
+
if (!registry) {
|
|
19
|
+
const paths = await projectPaths(projectRoot ?? process.cwd(), dataDir);
|
|
20
|
+
registry = await createExtensionRegistry({ dataDir: paths.dataDir, projectId: paths.projectId, transport });
|
|
21
|
+
}
|
|
22
|
+
const resolveSource = value => value.startsWith('.') || path.isAbsolute(value)
|
|
23
|
+
? path.resolve(projectRoot ?? process.cwd(), value) : value;
|
|
24
|
+
let result;
|
|
25
|
+
if (command === 'list') result = await registry.list();
|
|
26
|
+
if (command === 'doctor') result = await registry.doctor();
|
|
27
|
+
if (command === 'remove') result = await registry.remove(source);
|
|
28
|
+
if (command === 'add' || command === 'update') result = await registry.install(resolveSource(source), { signal });
|
|
29
|
+
if (command === 'dev') {
|
|
30
|
+
if (!(source.startsWith('.') || path.isAbsolute(source))) throw extensionError('development_directory_required');
|
|
31
|
+
result = await registry.dev(resolveSource(source), { signal });
|
|
32
|
+
}
|
|
33
|
+
if (output) {
|
|
34
|
+
if (Array.isArray(result)) output.write(result.length ? result.map(row =>
|
|
35
|
+
`${row.id} ${row.version} ${row.digest.slice(0, 12)}${row.development ? ' development' : ''} ${row.grant ? 'granted' : 'approval required'}`
|
|
36
|
+
).join('\n') + '\n' : 'No visualizer extensions installed.\n');
|
|
37
|
+
else if (command === 'doctor') output.write(`${result.ok ? 'Extension checks passed.' : 'Extension checks failed.'}\n${
|
|
38
|
+
result.results.map(row => `${row.id ?? 'catalogue'}: ${row.code}`).join('\n')}${result.results.length ? '\n' : ''}`);
|
|
39
|
+
else output.write(command === 'remove' ? `Removed ${result.id} and its grants.\n` :
|
|
40
|
+
`Installed ${result.id} ${result.version}${result.development ? ' (development snapshot)' : ''}. ${
|
|
41
|
+
result.grant ? 'Existing digest approval retained.' : 'Project approval is required before data delivery.'}\n`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|