graphlin 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +465 -0
  8. package/docs/visualizer-views.md +199 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +74 -9
  34. package/plugin.json +4 -2
  35. package/runtime/core/evidence.mjs +43 -9
  36. package/runtime/core/graph.mjs +11 -6
  37. package/runtime/core/privacy.mjs +1 -0
  38. package/runtime/daemon/auth.mjs +7 -3
  39. package/runtime/daemon/diagnostics.mjs +1 -1
  40. package/runtime/daemon/extension-api.mjs +203 -0
  41. package/runtime/daemon/lineage.mjs +70 -0
  42. package/runtime/daemon/manager.mjs +9 -6
  43. package/runtime/daemon/model-api.mjs +728 -0
  44. package/runtime/daemon/model-persistence.mjs +220 -0
  45. package/runtime/daemon/server.mjs +70 -12
  46. package/runtime/daemon/settings.mjs +11 -3
  47. package/runtime/decisions/broker.mjs +349 -0
  48. package/runtime/decisions/contracts.mjs +179 -0
  49. package/runtime/decisions/evaluation.mjs +305 -0
  50. package/runtime/decisions/faults.mjs +32 -0
  51. package/runtime/decisions/index.mjs +818 -0
  52. package/runtime/decisions/profiles.mjs +93 -0
  53. package/runtime/decisions/questions.mjs +268 -0
  54. package/runtime/discovery/index.mjs +2 -0
  55. package/runtime/discovery/inventory.mjs +160 -0
  56. package/runtime/discovery/parser.mjs +40 -0
  57. package/runtime/discovery/structure.mjs +232 -0
  58. package/runtime/extensions/contracts.mjs +59 -0
  59. package/runtime/extensions/frame.mjs +64 -0
  60. package/runtime/extensions/index.mjs +9 -0
  61. package/runtime/extensions/manifest.mjs +95 -0
  62. package/runtime/extensions/packages.mjs +222 -0
  63. package/runtime/extensions/profiles.mjs +36 -0
  64. package/runtime/extensions/projection.mjs +130 -0
  65. package/runtime/extensions/registry.mjs +285 -0
  66. package/runtime/extensions/scene.mjs +105 -0
  67. package/runtime/extensions/sdk.d.ts +205 -0
  68. package/runtime/extensions/sdk.mjs +88 -0
  69. package/runtime/jev/index.mjs +13 -777
  70. package/runtime/jev/provider.mjs +101 -0
  71. package/runtime/jev/questions.mjs +16 -258
  72. package/runtime/jev/wire.mjs +17 -25
  73. package/runtime/model/changes.mjs +42 -0
  74. package/runtime/model/history.mjs +124 -0
  75. package/runtime/model/index.mjs +2 -0
  76. package/runtime/model/project-model.mjs +889 -0
  77. package/runtime/model/records.mjs +239 -0
  78. package/runtime/pipeline.mjs +127 -48
  79. package/runtime/platform.mjs +254 -0
  80. package/runtime/visualizers/blocks.mjs +5 -0
  81. package/runtime/visualizers/c4.mjs +52 -0
  82. package/runtime/visualizers/changes.mjs +24 -0
  83. package/runtime/visualizers/code.mjs +5 -0
  84. package/runtime/visualizers/index.mjs +23 -0
  85. package/runtime/visualizers/structure.mjs +120 -0
  86. package/runtime/visualizers/timeline.mjs +66 -0
  87. package/runtime/web/app.js +369 -86
  88. package/runtime/web/extension-frame.js +128 -0
  89. package/runtime/web/index.html +123 -80
  90. package/runtime/web/model-client.js +162 -0
  91. package/runtime/web/platform.js +337 -0
  92. package/runtime/web/scene.js +111 -0
  93. package/runtime/web/style.css +152 -142
  94. package/schemas/graph.schema.json +4 -1
  95. package/scripts/arguments.mjs +5 -1
  96. package/scripts/build-packages.mjs +6 -2
  97. package/scripts/control.mjs +1 -1
  98. package/scripts/daemon.mjs +2 -1
  99. package/scripts/extensions.mjs +44 -0
  100. package/scripts/graphlin.mjs +23 -3
  101. package/scripts/onboarding.mjs +10 -3
  102. package/scripts/validate-packages.mjs +54 -8
@@ -0,0 +1,285 @@
1
+ import { mkdir, mkdtemp, writeFile, rename, rm, open, lstat, watch } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import {
6
+ MANIFEST_FILE, EXTENSION_LIMITS as L, check, exact, id, extensionId, digest,
7
+ extensionError, assetPath, jsonBytes,
8
+ } from './contracts.mjs';
9
+ import { validateManifest, validateAssets, bundleDigest, decisionProfiles } from './manifest.mjs';
10
+ import { validateGrant } from './projection.mjs';
11
+ import {
12
+ safeDirectory, readRegular, readLocalPackage, readPackageArchive, npmPackTransport,
13
+ parsePackageSpec, checkCancelled,
14
+ } from './packages.mjs';
15
+
16
+ async function makePrivate(directory) {
17
+ try { await safeDirectory(directory, { owned: true }); }
18
+ catch (error) {
19
+ if (error.code !== 'ENOENT') throw error;
20
+ await makeParent(path.dirname(directory));
21
+ await mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
22
+ await safeDirectory(directory, { owned: true });
23
+ }
24
+ return directory;
25
+ }
26
+ async function makeParent(directory) {
27
+ try { await safeDirectory(directory); }
28
+ catch (error) {
29
+ if (error.code !== 'ENOENT') throw error;
30
+ await makeParent(path.dirname(directory));
31
+ await mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
32
+ await safeDirectory(directory);
33
+ }
34
+ }
35
+ async function atomicJSON(filename, value) {
36
+ const body = jsonBytes(value, 2 * 1024 * 1024, 'catalogue_limit');
37
+ const temporary = `${filename}.${randomUUID()}.tmp`;
38
+ let file;
39
+ try {
40
+ file = await open(temporary, 'wx', 0o600);
41
+ await file.writeFile(body); await file.sync(); await file.close(); file = null;
42
+ await rename(temporary, filename);
43
+ } finally {
44
+ await file?.close();
45
+ await rm(temporary, { force: true });
46
+ }
47
+ }
48
+
49
+ export async function createExtensionRegistry({ dataDir, projectId, transport = npmPackTransport } = {}) {
50
+ check(typeof dataDir === 'string' && path.isAbsolute(dataDir) && id(projectId), 'invalid_registry_options');
51
+ await makePrivate(dataDir);
52
+ const directory = await makePrivate(path.join(dataDir, 'extensions'));
53
+ const bundles = await makePrivate(path.join(directory, 'bundles'));
54
+ const staging = await makePrivate(path.join(directory, 'staging'));
55
+ const catalogue = path.join(directory, 'catalogue.json');
56
+ const lock = path.join(directory, 'write.lock');
57
+ const bundlePath = row => path.join(bundles, row.id, row.version, row.digest);
58
+
59
+ async function readState() {
60
+ let state;
61
+ try { state = JSON.parse((await readRegular(catalogue, 2 * 1024 * 1024, { privateFile: true })).toString('utf8')); }
62
+ catch (error) {
63
+ if (error.code === 'ENOENT') return { schemaVersion: 1, installed: [], grants: [] };
64
+ throw extensionError('invalid_extension_catalogue');
65
+ }
66
+ check(exact(state, ['schemaVersion', 'installed', 'grants']) && state.schemaVersion === 1 &&
67
+ Array.isArray(state.installed) && state.installed.length <= 128 &&
68
+ Array.isArray(state.grants) && state.grants.length <= 2048, 'invalid_extension_catalogue');
69
+ const seen = new Set();
70
+ for (const row of state.installed) {
71
+ check(exact(row, ['id', 'version', 'digest', 'manifest', 'development']) &&
72
+ typeof row.development === 'boolean', 'invalid_extension_catalogue');
73
+ const manifest = validateManifest(row.manifest);
74
+ check(row.id === manifest.id && row.version === manifest.version &&
75
+ row.digest === bundleDigest(manifest) && !seen.has(row.id), 'invalid_extension_catalogue');
76
+ seen.add(row.id);
77
+ }
78
+ state.grants.forEach(validateGrant);
79
+ return state;
80
+ }
81
+ async function locked(action, signal) {
82
+ checkCancelled(signal);
83
+ await safeDirectory(directory, { owned: true });
84
+ let acquired = false;
85
+ for (let attempt = 0; attempt < 100; attempt++) {
86
+ try { await mkdir(lock, { mode: 0o700 }); acquired = true; break; }
87
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
88
+ const stat = await lstat(lock);
89
+ check(stat.isDirectory() && !stat.isSymbolicLink(), 'unsafe_extension_lock');
90
+ await delay(20); checkCancelled(signal);
91
+ }
92
+ check(acquired, 'extension_registry_busy');
93
+ try {
94
+ const state = await readState();
95
+ const result = await action(state);
96
+ checkCancelled(signal);
97
+ await atomicJSON(catalogue, state);
98
+ return result;
99
+ } finally { await rm(lock, { recursive: true, force: true }); }
100
+ }
101
+ function rowFor(state, extension) {
102
+ check(extensionId(extension), 'invalid_extension_id');
103
+ const row = state.installed.find(value => value.id === extension);
104
+ check(row, 'extension_not_installed');
105
+ return row;
106
+ }
107
+ function currentGrant(state, row) {
108
+ return state.grants.find(value => value.projectId === projectId &&
109
+ value.extensionId === row.id && value.digest === row.digest && value.approved) ?? null;
110
+ }
111
+ function describe(state, row, profiles = []) {
112
+ return { ...row, profiles, grant: currentGrant(state, row) };
113
+ }
114
+ async function loadAssets(row) {
115
+ const root = await safeDirectory(bundlePath(row), { owned: true });
116
+ const manifest = validateManifest(JSON.parse((await readRegular(path.join(root, MANIFEST_FILE),
117
+ L.manifestBytes, { privateFile: true })).toString('utf8')));
118
+ check(bundleDigest(manifest) === row.digest, 'installed_manifest_changed');
119
+ const inputs = {};
120
+ for (const name of Object.keys(manifest.assets)) {
121
+ inputs[name] = await readRegular(path.join(root, name), L.assetBytes, { privateFile: true });
122
+ }
123
+ const assets = validateAssets(manifest, inputs);
124
+ return { manifest, assets, profiles: decisionProfiles(manifest, assets),
125
+ digest: row.digest, development: row.development };
126
+ }
127
+ async function saveBundle(bundle, temporary) {
128
+ const row = { id: bundle.manifest.id, version: bundle.manifest.version, digest: bundle.digest };
129
+ const destination = bundlePath(row);
130
+ try {
131
+ await safeDirectory(destination, { owned: true });
132
+ await loadAssets({ ...row, manifest: bundle.manifest });
133
+ return;
134
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
135
+ const root = await makePrivate(path.join(temporary, 'bundle'));
136
+ await writeFile(path.join(root, MANIFEST_FILE), JSON.stringify(bundle.manifest), { flag: 'wx', mode: 0o400 });
137
+ for (const [name, bytes] of Object.entries(bundle.assets)) {
138
+ await makeParent(path.dirname(path.join(root, name)));
139
+ await writeFile(path.join(root, name), bytes, { flag: 'wx', mode: 0o400 });
140
+ }
141
+ await makeParent(path.dirname(destination));
142
+ // No existing immutable bundle is overwritten; install is serialized.
143
+ await rename(root, destination);
144
+ }
145
+ async function install(source, { signal, dev = false } = {}) {
146
+ check(typeof source === 'string' && source.length > 0 && source.length <= 4096, 'invalid_extension_source');
147
+ checkCancelled(signal);
148
+ const local = path.isAbsolute(source) || source.startsWith('./') || source.startsWith('../');
149
+ check(!dev || local, 'development_directory_required');
150
+ const temporary = await mkdtemp(path.join(staging, 'install-'));
151
+ try {
152
+ let bundle;
153
+ if (local) bundle = await readLocalPackage(source, { signal });
154
+ else {
155
+ parsePackageSpec(source);
156
+ let packed = await transport(source, { directory: temporary, signal });
157
+ checkCancelled(signal);
158
+ if (typeof packed === 'string') {
159
+ const filename = path.resolve(packed);
160
+ check(filename.startsWith(`${temporary}${path.sep}`), 'unsafe_transport_path');
161
+ packed = await readRegular(filename, L.archiveBytes);
162
+ }
163
+ bundle = readPackageArchive(packed, source);
164
+ }
165
+ checkCancelled(signal);
166
+ return await locked(async state => {
167
+ await saveBundle(bundle, temporary);
168
+ checkCancelled(signal);
169
+ const row = { id: bundle.manifest.id, version: bundle.manifest.version, digest: bundle.digest,
170
+ manifest: bundle.manifest, development: dev };
171
+ // An update invalidates every project's old approval, including rollback
172
+ // to a previously approved digest. Installation never approves access.
173
+ const prior = state.installed.find(value => value.id === row.id);
174
+ if (!prior || prior.digest !== row.digest) {
175
+ state.grants = state.grants.filter(value => value.extensionId !== row.id);
176
+ }
177
+ state.installed = state.installed.filter(value => value.id !== row.id);
178
+ state.installed.push(row);
179
+ return describe(state, row, bundle.profiles);
180
+ }, signal);
181
+ } finally { await rm(temporary, { recursive: true, force: true }); }
182
+ }
183
+ async function getAssets(extension, { digest: requestedDigest, assetPath: name } = {}) {
184
+ const state = await readState(), row = rowFor(state, extension);
185
+ check(requestedDigest === undefined || (digest(requestedDigest) && requestedDigest === row.digest),
186
+ 'extension_digest_changed');
187
+ if (name !== undefined) check(assetPath(name) && Object.hasOwn(row.manifest.assets, name), 'unknown_extension_asset');
188
+ const loaded = await loadAssets(row);
189
+ return name === undefined ? loaded : {
190
+ bytes: loaded.assets[name], contentType: name.endsWith('.js') ? 'text/javascript; charset=utf-8' : 'application/json',
191
+ digest: row.digest, assetPath: name,
192
+ };
193
+ }
194
+ async function grant(extension, request) {
195
+ return locked(async state => {
196
+ const row = rowFor(state, extension);
197
+ check(exact(request, ['digest', 'fields', 'history', 'approved'], ['profiles']), 'invalid_extension_grant');
198
+ check(request.digest === row.digest, 'extension_digest_changed');
199
+ const value = validateGrant({ ...request, extensionId: extension, projectId, grantedAt: new Date().toISOString() });
200
+ const capabilities = row.manifest.capabilities;
201
+ for (const field of value.fields) {
202
+ check(capabilities.includes(field === 'activity' ? 'activity.read' : 'model.read'), 'unrequested_data_field');
203
+ }
204
+ check(!value.history || capabilities.includes('history.read'), 'unrequested_history');
205
+ if (value.profiles?.length) {
206
+ check(capabilities.includes('analysis.request'), 'unrequested_analysis');
207
+ const loaded = await loadAssets(row);
208
+ check(value.profiles.every(profile => loaded.profiles.some(item => item.id === profile)), 'unrequested_profile');
209
+ }
210
+ state.grants = state.grants.filter(item => !(item.projectId === projectId && item.extensionId === extension));
211
+ state.grants.push(value);
212
+ return value;
213
+ });
214
+ }
215
+ async function revoke(extension) {
216
+ return locked(state => {
217
+ rowFor(state, extension);
218
+ state.grants = state.grants.filter(value => !(value.projectId === projectId && value.extensionId === extension));
219
+ return { id: extension, revoked: true };
220
+ });
221
+ }
222
+ async function remove(extension) {
223
+ // Remove visibility and grants in one atomic write. Retained immutable
224
+ // versions are inaccessible through getAssets after removal; keeping them
225
+ // avoids deleting bytes held by a concurrent reader or a pinned checkpoint.
226
+ return locked(state => {
227
+ rowFor(state, extension);
228
+ state.installed = state.installed.filter(value => value.id !== extension);
229
+ state.grants = state.grants.filter(value => value.extensionId !== extension);
230
+ return { id: extension, removed: true };
231
+ });
232
+ }
233
+ async function doctor() {
234
+ const results = [];
235
+ let state;
236
+ try { state = await readState(); }
237
+ catch (error) { return { ok: false, results: [{ code: error.code ?? 'invalid_extension_catalogue' }] }; }
238
+ for (const row of state.installed) {
239
+ try { await loadAssets(row); results.push({ id: row.id, ok: true, code: 'verified', digest: row.digest }); }
240
+ catch (error) { results.push({ id: row.id, ok: false, code: error.code ?? 'invalid_installed_extension' }); }
241
+ }
242
+ try { await lstat(lock); results.push({ ok: false, code: 'extension_registry_busy' }); }
243
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
244
+ return { ok: results.every(value => value.ok), results };
245
+ }
246
+ async function dev(source, { signal, watch: watchChanges = false, onChange = () => {} } = {}) {
247
+ check(path.isAbsolute(source) || source.startsWith('./') || source.startsWith('../'), 'development_directory_required');
248
+ const installed = await install(source, { signal, dev: true });
249
+ if (!watchChanges) return installed;
250
+ const controller = new AbortController();
251
+ const abort = () => controller.abort();
252
+ signal?.addEventListener('abort', abort, { once: true });
253
+ if (signal?.aborted) abort();
254
+ const done = (async () => {
255
+ let last = installed.digest;
256
+ try {
257
+ for await (const event of watch(source, { recursive: true, signal: controller.signal })) {
258
+ if (!event.filename) continue;
259
+ try {
260
+ const row = await install(source, { signal: controller.signal, dev: true });
261
+ if (row.digest !== last) { last = row.digest; onChange({ ok: true, extension: row }); }
262
+ } catch (error) {
263
+ if (controller.signal.aborted) break;
264
+ onChange({ ok: false, code: error.code ?? 'extension_reload_failed' });
265
+ }
266
+ }
267
+ } catch (error) {
268
+ if (!controller.signal.aborted) onChange({ ok: false, code: 'extension_watch_failed' });
269
+ } finally { signal?.removeEventListener('abort', abort); }
270
+ })();
271
+ return { ...installed, close: abort, done };
272
+ }
273
+ return {
274
+ list: async () => {
275
+ const state = await readState(), rows = [];
276
+ for (const row of state.installed) {
277
+ const profiles = row.manifest.decisionProfiles?.length ? (await loadAssets(row)).profiles : [];
278
+ rows.push(describe(state, row, profiles));
279
+ }
280
+ return rows;
281
+ },
282
+ install, remove, grant, revoke, getAssets, doctor, dev,
283
+ getGrant: async extension => { const state = await readState(); return currentGrant(state, rowFor(state, extension)); },
284
+ };
285
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ SCENE_VERSION, EXTENSION_LIMITS as L, check, exact, id, integer,
3
+ text, uniqueStrings, jsonBytes,
4
+ } from './contracts.mjs';
5
+
6
+ export const SCENE_KINDS = Object.freeze([
7
+ 'client', 'service', 'datastore', 'queue', 'external', 'module', 'function',
8
+ 'class', 'interface', 'event', 'configuration', 'package', 'group', 'unknown',
9
+ ]);
10
+ export const SCENE_SHAPES = Object.freeze([
11
+ 'rounded_rect', 'rect', 'cylinder', 'cloud', 'diamond', 'group', 'hexagon',
12
+ 'class_box', 'interface_box', 'document', 'parallelogram', 'folder', 'browser', 'component', 'queue',
13
+ ]);
14
+ export const EDGE_KINDS = Object.freeze([
15
+ 'calls', 'reads', 'writes', 'publishes', 'consumes', 'depends_on', 'contains',
16
+ 'imports', 'references', 'member_of', 'hosted_by', 'unknown',
17
+ ]);
18
+ const STATES = ['default', 'muted', 'selected', 'active', 'pending', 'failed', 'stale',
19
+ 'tentative', 'added', 'modified', 'removed', 'discovered', 'unknown'];
20
+ const BOX_FIELDS = ['parentId', 'x', 'y', 'width', 'height', 'style', 'shape', 'collapsed'];
21
+
22
+ function box(value) {
23
+ check(value.parentId === undefined || value.parentId === null || id(value.parentId), 'invalid_scene_parent');
24
+ for (const field of ['x', 'y']) {
25
+ check(value[field] === undefined ||
26
+ (Number.isFinite(value[field]) && Math.abs(value[field]) <= L.coordinate), 'invalid_scene_coordinate');
27
+ }
28
+ for (const field of ['width', 'height']) {
29
+ check(value[field] === undefined ||
30
+ (Number.isFinite(value[field]) && value[field] > 0 && value[field] <= L.dimension), 'invalid_scene_size');
31
+ }
32
+ check(value.style === undefined || STATES.includes(value.style), 'invalid_scene_style');
33
+ check(value.shape === undefined || SCENE_SHAPES.includes(value.shape), 'invalid_scene_shape');
34
+ check(value.collapsed === undefined || typeof value.collapsed === 'boolean', 'invalid_scene_group');
35
+ }
36
+
37
+ /** Returns a detached scene, throwing a fixed code on invalid/unbounded output.
38
+ * Pass the delivered model to verify mappings, never the unprojected core state.
39
+ */
40
+ export function validateScene(scene, { model } = {}) {
41
+ check(exact(scene, ['sceneVersion', 'nodes', 'groups', 'edges'], ['coverage']), 'invalid_scene');
42
+ check(scene.sceneVersion === SCENE_VERSION, 'incompatible_scene');
43
+ check(Array.isArray(scene.nodes) && scene.nodes.length <= L.nodes &&
44
+ Array.isArray(scene.groups) && scene.groups.length <= L.groups &&
45
+ Array.isArray(scene.edges) && scene.edges.length <= L.edges, 'scene_item_limit');
46
+ jsonBytes(scene, L.sceneBytes, 'scene_byte_limit');
47
+ const entities = model ? new Set(model.entities.map(value => value.id)) : null;
48
+ const relations = model ? new Map(model.relations.map(value => [value.id, value])) : null;
49
+ const items = new Map();
50
+ const groups = new Set(scene.groups.map(value => value?.id));
51
+ for (const [values, isGroup] of [[scene.nodes, false], [scene.groups, true]]) {
52
+ for (const value of values) {
53
+ const required = isGroup ? ['id', 'entityIds', 'label'] : ['id', 'entityId', 'label', 'kind'];
54
+ check(exact(value, required, [...BOX_FIELDS, ...(isGroup ? ['kind', 'membershipId'] : [])]) &&
55
+ id(value.id) && text(value.label) && !items.has(value.id), 'invalid_scene_node');
56
+ if (isGroup) {
57
+ check(uniqueStrings(value.entityIds, id, L.members) && value.entityIds.length > 0 &&
58
+ (value.membershipId === undefined || id(value.membershipId)), 'invalid_scene_mapping');
59
+ } else check(id(value.entityId), 'invalid_scene_mapping');
60
+ check((isGroup && value.kind === undefined) || SCENE_KINDS.includes(value.kind), 'invalid_scene_kind');
61
+ const members = isGroup ? value.entityIds : [value.entityId];
62
+ check(!entities || members.every(member => entities.has(member)), 'unknown_scene_entity');
63
+ box(value);
64
+ items.set(value.id, { ...value, members });
65
+ }
66
+ }
67
+ for (const item of items.values()) {
68
+ const seen = new Set([item.id]);
69
+ let parent = item.parentId;
70
+ while (parent !== undefined && parent !== null) {
71
+ check(groups.has(parent) && !seen.has(parent), 'invalid_scene_parent');
72
+ seen.add(parent);
73
+ parent = items.get(parent).parentId;
74
+ }
75
+ }
76
+ const edgeIds = new Set();
77
+ for (const edge of scene.edges) {
78
+ check(exact(edge, ['id', 'source', 'target', 'kind'], ['relationIds', 'label', 'count', 'style']) &&
79
+ id(edge.id) && !edgeIds.has(edge.id) && !items.has(edge.id) &&
80
+ items.has(edge.source) && items.has(edge.target) && EDGE_KINDS.includes(edge.kind), 'invalid_scene_edge');
81
+ check(edge.label === undefined || text(edge.label), 'invalid_scene_text');
82
+ check(edge.style === undefined || STATES.includes(edge.style), 'invalid_scene_style');
83
+ check(edge.count === undefined || (integer(edge.count, 1_000_000) && edge.count > 0), 'invalid_scene_count');
84
+ check(edge.relationIds === undefined || uniqueStrings(edge.relationIds, id, L.members), 'invalid_scene_mapping');
85
+ if (relations) {
86
+ check(edge.relationIds?.length > 0, 'missing_scene_relation');
87
+ for (const relationId of edge.relationIds) {
88
+ const relation = relations.get(relationId);
89
+ check(relation && relation.kind === edge.kind &&
90
+ items.get(edge.source).members.includes(relation.source) &&
91
+ items.get(edge.target).members.includes(relation.target), 'invalid_scene_relation');
92
+ }
93
+ check(edge.count === undefined || edge.count === edge.relationIds.length, 'invalid_scene_count');
94
+ }
95
+ edgeIds.add(edge.id);
96
+ }
97
+ if (scene.coverage !== undefined) {
98
+ check(exact(scene.coverage, [], ['shown', 'total', 'truncated', 'label']) &&
99
+ (scene.coverage.shown === undefined || integer(scene.coverage.shown)) &&
100
+ (scene.coverage.total === undefined || integer(scene.coverage.total)) &&
101
+ (scene.coverage.truncated === undefined || typeof scene.coverage.truncated === 'boolean') &&
102
+ (scene.coverage.label === undefined || text(scene.coverage.label)), 'invalid_scene_coverage');
103
+ }
104
+ return JSON.parse(JSON.stringify(scene));
105
+ }
@@ -0,0 +1,205 @@
1
+ /** Graphlin extension API 1; model schema and scene schema are independent. */
2
+ export type Identifier = string;
3
+ export type Basis = 'metadata' | 'parsed' | 'lexical' | 'decision' | 'legacy'
4
+ | 'documentation' | 'annotation' | 'runtime';
5
+ export type Validity = 'current' | 'stale' | 'retracted';
6
+ export type Classification = 'accepted' | 'tentative' | 'unknown' | 'stale' | 'pending' | 'abstained';
7
+ export interface SourceReference {
8
+ artifactId?: Identifier;
9
+ eventId?: Identifier;
10
+ hash?: string;
11
+ generation?: number;
12
+ startLine?: number;
13
+ endLine?: number;
14
+ sourceClass?: 'source' | 'public_intent' | 'runtime';
15
+ }
16
+ export interface Entity {
17
+ id: Identifier;
18
+ label: string;
19
+ kind: string;
20
+ parentId: Identifier | null;
21
+ artifactId?: Identifier;
22
+ qualifiedName?: string;
23
+ sourceRefs: SourceReference[];
24
+ basis: Basis;
25
+ validity: Validity;
26
+ classification: Classification;
27
+ }
28
+ export interface Relation {
29
+ id: Identifier;
30
+ source: Identifier;
31
+ target: Identifier;
32
+ kind: string;
33
+ basis: Basis;
34
+ validity: Validity;
35
+ sourceRefs: SourceReference[];
36
+ }
37
+ export interface Interpretation {
38
+ id: Identifier;
39
+ namespace: string;
40
+ version: string;
41
+ kind: string;
42
+ label: string;
43
+ entityIds: Identifier[];
44
+ basis: Basis;
45
+ validity: Validity;
46
+ classification: Classification;
47
+ support: 'supported' | 'tentative' | 'unknown' | 'contradicted';
48
+ sourceRefs: SourceReference[];
49
+ }
50
+ export interface Activity {
51
+ id: Identifier;
52
+ kind: string;
53
+ sequence: number;
54
+ knownAtSequence: number;
55
+ at: string;
56
+ recordedAt: string;
57
+ sessionId?: Identifier;
58
+ agentId?: Identifier;
59
+ toolCallId?: Identifier;
60
+ toolCategory?: 'read' | 'write' | 'edit' | 'shell' | 'search' | 'test' | 'other';
61
+ outcome?: 'pending' | 'succeeded' | 'failed' | 'interrupted' | 'denied' | 'unresolved' | 'observed';
62
+ attribution?: 'observed' | 'correlated' | 'unknown';
63
+ creation?: boolean;
64
+ entityIds: Identifier[];
65
+ artifactIds: Identifier[];
66
+ sourceRefs: SourceReference[];
67
+ }
68
+ export interface CoverageCounts {
69
+ total?: number;
70
+ inventoried?: number;
71
+ inspected?: number;
72
+ parsed?: number;
73
+ deferred?: number;
74
+ excluded?: number;
75
+ unsupported?: number;
76
+ unavailable?: number;
77
+ truncated?: number;
78
+ retained?: number;
79
+ files?: number;
80
+ entities?: number;
81
+ relations?: number;
82
+ }
83
+ export interface Coverage extends CoverageCounts {
84
+ complete?: boolean;
85
+ status?: string;
86
+ counts?: CoverageCounts;
87
+ scopes?: (CoverageCounts & { id: Identifier; label: string; entityId?: Identifier; parentId?: Identifier; status?: string })[];
88
+ }
89
+ export interface Session {
90
+ id: Identifier;
91
+ host?: 'claude' | 'codex' | 'kiro' | 'demo' | 'unknown';
92
+ startedAt?: string;
93
+ endedAt?: string;
94
+ status?: string;
95
+ }
96
+ export interface Checkpoint {
97
+ id: Identifier;
98
+ revision?: number;
99
+ sequence?: number;
100
+ at?: string;
101
+ createdAt?: string;
102
+ label: string;
103
+ }
104
+ export interface ModelSnapshot {
105
+ schemaVersion: 2;
106
+ projectId: Identifier;
107
+ revision: number;
108
+ sequence: number;
109
+ entities: Entity[];
110
+ relations: Relation[];
111
+ interpretations: Interpretation[];
112
+ activity: Activity[];
113
+ coverage: Coverage;
114
+ sessions: Session[];
115
+ checkpoints: Checkpoint[];
116
+ }
117
+ export type SceneKind = 'client' | 'service' | 'datastore' | 'queue' | 'external' | 'module'
118
+ | 'function' | 'class' | 'interface' | 'event' | 'configuration' | 'package' | 'group' | 'unknown';
119
+ export type SceneStyle = 'default' | 'muted' | 'selected' | 'active' | 'pending' | 'failed'
120
+ | 'stale' | 'tentative' | 'added' | 'modified' | 'removed' | 'discovered' | 'unknown';
121
+ export type SceneShape = 'rounded_rect' | 'rect' | 'cylinder' | 'cloud' | 'diamond' | 'group'
122
+ | 'hexagon' | 'class_box' | 'interface_box' | 'document' | 'parallelogram' | 'folder' | 'browser' | 'component' | 'queue';
123
+ export type EdgeKind = 'calls' | 'reads' | 'writes' | 'publishes' | 'consumes' | 'depends_on'
124
+ | 'contains' | 'imports' | 'references' | 'member_of' | 'hosted_by' | 'unknown';
125
+ export interface SceneBox {
126
+ id: Identifier;
127
+ label: string;
128
+ parentId?: Identifier | null;
129
+ x?: number;
130
+ y?: number;
131
+ width?: number;
132
+ height?: number;
133
+ style?: SceneStyle;
134
+ shape?: SceneShape;
135
+ collapsed?: boolean;
136
+ }
137
+ export interface SceneNode extends SceneBox { entityId: Identifier; kind: SceneKind }
138
+ export interface SceneGroup extends SceneBox { entityIds: Identifier[]; kind?: SceneKind; membershipId?: Identifier }
139
+ export interface SceneEdge {
140
+ id: Identifier;
141
+ source: Identifier;
142
+ target: Identifier;
143
+ kind: EdgeKind;
144
+ relationIds?: Identifier[];
145
+ label?: string;
146
+ count?: number;
147
+ style?: SceneStyle;
148
+ }
149
+ export interface Scene {
150
+ sceneVersion: 1;
151
+ nodes: SceneNode[];
152
+ groups: SceneGroup[];
153
+ edges: SceneEdge[];
154
+ coverage?: { shown?: number; total?: number; truncated?: boolean; label?: string };
155
+ }
156
+ export type C4InterpretationKind = 'application' | 'container' | 'component' | 'system'
157
+ | 'external_system' | 'actor' | 'person' | 'context' | 'datastore';
158
+ type FixedInterpretation =
159
+ | { interpretationKind?: undefined; interpretationLabel?: undefined }
160
+ | { interpretationKind: C4InterpretationKind; interpretationLabel?: string };
161
+ /** Optional mappings apply only to supported, accepted answers with current
162
+ * source references. Labels are at most 80 characters; the core filters them.
163
+ * Without a label, the broker uses the first selected entity's core label. */
164
+ export type DecisionQuestion = { id: string; question: string } & (
165
+ | (({ kind: 'boolean' | 'score'; options?: never }
166
+ | { kind: 'choice'; options: string[] }) & FixedInterpretation)
167
+ | { kind: 'choice'; options: (C4InterpretationKind | 'unknown')[];
168
+ interpretationKind: 'selected-choice'; interpretationLabel?: string }
169
+ );
170
+ export interface DecisionProfile {
171
+ id: string;
172
+ questions: DecisionQuestion[];
173
+ selectors: { fields: ('entities' | 'relations' | 'interpretations')[]; candidateIds: Identifier[] };
174
+ }
175
+ export interface MessageContext {
176
+ apiVersion: 1;
177
+ instanceId: Identifier;
178
+ projectId: Identifier;
179
+ revision: number;
180
+ viewEpoch: number;
181
+ requestId: Identifier;
182
+ }
183
+ export type ExtensionMessage =
184
+ | (MessageContext & { type: 'graphlin:project'; model: ModelSnapshot; settings?: Record<string, unknown>; selection?: Identifier | null })
185
+ | (MessageContext & { type: 'graphlin:scene'; scene: Scene })
186
+ | (MessageContext & { type: 'graphlin:dispose' })
187
+ | (MessageContext & { type: 'graphlin:error'; code: string })
188
+ | (MessageContext & { type: 'graphlin:status'; status: 'ready' | 'busy' | 'empty' | 'error'; itemCount: number })
189
+ | (MessageContext & { type: 'graphlin:select'; selection:
190
+ { entityId: Identifier } | { relationId: Identifier } | { activityId: Identifier } });
191
+ export function validateScene(scene: unknown, options?: { model?: ModelSnapshot }): Scene;
192
+ export function validateDecisionProfile(profile: unknown): DecisionProfile;
193
+ export function validateMessage(message: unknown, options?: { model?: ModelSnapshot; context?: MessageContext }): ExtensionMessage;
194
+ export function connectExtension(options: {
195
+ project?: (model: ModelSnapshot, settings: Record<string, unknown>, selection: Identifier | null) => Scene | void | Promise<Scene | void>;
196
+ mount?: (context: { root: HTMLElement | null; assets: Record<string, unknown> }) => void;
197
+ dispose?: () => void;
198
+ }): () => void;
199
+ export const API_VERSION: 1;
200
+ export const MODEL_SCHEMA: 2;
201
+ export const SCENE_VERSION: 1;
202
+ export const SCENE_KINDS: readonly SceneKind[];
203
+ export const SCENE_SHAPES: readonly SceneShape[];
204
+ export const EDGE_KINDS: readonly EdgeKind[];
205
+ export const EXTENSION_LIMITS: Readonly<Record<string, number>>;