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,220 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readPrivateJSON } from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
export const MAX_MODEL_STATE_BYTES = 48 * 1024 * 1024;
|
|
8
|
+
const MAX_NODES = 2_000_000, MAX_DEPTH = 64;
|
|
9
|
+
const COLLECTIONS = ['entities', 'relations', 'interpretations', 'activity', 'sessions', 'checkpoints'];
|
|
10
|
+
const ROOT_FIELDS = new Set(['schemaVersion', 'projectId', 'revision', 'sequence', 'coverage', 'storage', ...COLLECTIONS]);
|
|
11
|
+
const plain = value => value !== null && typeof value === 'object' &&
|
|
12
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
13
|
+
const natural = value => Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(value);
|
|
15
|
+
const invalid = () => { throw new Error('invalid_model_state'); };
|
|
16
|
+
|
|
17
|
+
function validateSnapshot(value, projectId, depth = 0) {
|
|
18
|
+
if (!plain(value) || depth > 8 || value.schemaVersion !== 2 || value.projectId !== projectId ||
|
|
19
|
+
!natural(value.revision) || !natural(value.sequence) || !plain(value.coverage) ||
|
|
20
|
+
Object.keys(value).some(key => !ROOT_FIELDS.has(key)) ||
|
|
21
|
+
COLLECTIONS.some(key => !Array.isArray(value[key])) ||
|
|
22
|
+
(value.storage !== undefined && !plain(value.storage))) invalid();
|
|
23
|
+
for (const checkpoint of value.checkpoints) {
|
|
24
|
+
if (!plain(checkpoint) || !id(checkpoint.id) ||
|
|
25
|
+
(checkpoint.projectId !== undefined && checkpoint.projectId !== projectId)) invalid();
|
|
26
|
+
if (checkpoint.state !== undefined) validateSnapshot(checkpoint.state, projectId, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Count exact UTF-8 JSON bytes before allocating the complete encoded snapshot.
|
|
31
|
+
// Reject cycles, getters and non-JSON values instead of executing toJSON hooks or
|
|
32
|
+
// silently losing Map/BigInt values. Undefined object fields are omitted, as in
|
|
33
|
+
// JSON.stringify: current policy uses them to withhold display/path metadata.
|
|
34
|
+
function boundedJSON(value, limit) {
|
|
35
|
+
let size = 0, nodes = 0;
|
|
36
|
+
const ancestors = new Set();
|
|
37
|
+
const add = count => { size += count; if (size > limit) invalid(); };
|
|
38
|
+
function string(value) {
|
|
39
|
+
add(2);
|
|
40
|
+
for (let i = 0; i < value.length; i++) {
|
|
41
|
+
const code = value.charCodeAt(i);
|
|
42
|
+
if (code === 34 || code === 92) add(2);
|
|
43
|
+
else if (code < 32) add([8, 9, 10, 12, 13].includes(code) ? 2 : 6);
|
|
44
|
+
else if (code < 128) add(1);
|
|
45
|
+
else if (code < 2048) add(2);
|
|
46
|
+
else if (code >= 0xd800 && code <= 0xdbff &&
|
|
47
|
+
value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) { add(4); i++; }
|
|
48
|
+
else if (code >= 0xd800 && code <= 0xdfff) add(6);
|
|
49
|
+
else add(3);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function visit(value, depth) {
|
|
53
|
+
if (++nodes > MAX_NODES || depth > MAX_DEPTH) invalid();
|
|
54
|
+
if (value === null) { add(4); return; }
|
|
55
|
+
if (typeof value === 'string') { string(value); return; }
|
|
56
|
+
if (typeof value === 'boolean') { add(value ? 4 : 5); return; }
|
|
57
|
+
if (typeof value === 'number' && Number.isFinite(value)) { add(JSON.stringify(value).length); return; }
|
|
58
|
+
if ((!plain(value) && !Array.isArray(value)) || ancestors.has(value)) invalid();
|
|
59
|
+
ancestors.add(value);
|
|
60
|
+
const keys = Reflect.ownKeys(value);
|
|
61
|
+
if (keys.some(key => typeof key !== 'string')) invalid();
|
|
62
|
+
add(2);
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (value.length > MAX_NODES || keys.length !== value.length + 1) invalid();
|
|
65
|
+
for (let i = 0; i < value.length; i++) {
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
67
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) invalid();
|
|
68
|
+
if (i) add(1);
|
|
69
|
+
visit(descriptor.value, depth + 1);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
let emitted = 0;
|
|
73
|
+
for (let i = 0; i < keys.length; i++) {
|
|
74
|
+
const key = keys[i], descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
75
|
+
if (!Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) invalid();
|
|
76
|
+
if (descriptor.value === undefined) continue;
|
|
77
|
+
if (emitted++) add(1);
|
|
78
|
+
string(key); add(1); visit(descriptor.value, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ancestors.delete(value);
|
|
82
|
+
}
|
|
83
|
+
visit(value, 0);
|
|
84
|
+
const result = JSON.stringify(value);
|
|
85
|
+
if (Buffer.byteLength(result) !== size || size > limit) invalid();
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function owned(info) {
|
|
90
|
+
return process.getuid === undefined || info.uid === process.getuid();
|
|
91
|
+
}
|
|
92
|
+
async function safeDirectory(directory) {
|
|
93
|
+
const info = await lstat(directory);
|
|
94
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !owned(info) ||
|
|
95
|
+
(info.mode & 0o077) !== 0 || await realpath(directory) !== directory) invalid();
|
|
96
|
+
return info;
|
|
97
|
+
}
|
|
98
|
+
async function safeDestination(filename) {
|
|
99
|
+
try {
|
|
100
|
+
const info = await lstat(filename);
|
|
101
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || !owned(info) ||
|
|
102
|
+
(info.mode & 0o077) !== 0) invalid();
|
|
103
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The parent MUST pass model.snapshot({ persistent: true }) under current policy.
|
|
108
|
+
* This storage layer preserves that JSON (including storage/checkpoint state);
|
|
109
|
+
* it is not an export projector, source reader, migration, or policy authority.
|
|
110
|
+
*
|
|
111
|
+
* Use a separate, canonical private path such as <project-data>/model-state.json.
|
|
112
|
+
* The parent retains its existing state.json writer and owns daemon locking.
|
|
113
|
+
* An omitted projectId binds to the first successfully loaded/scheduled model.
|
|
114
|
+
*/
|
|
115
|
+
export function createModelPersistence(filename, {
|
|
116
|
+
projectId, maxBytes = MAX_MODEL_STATE_BYTES, debounceMs = 100, now = Date.now,
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (typeof filename !== 'string' || !filename || filename.includes('\0') || !path.isAbsolute(filename) ||
|
|
119
|
+
path.resolve(filename) !== filename || path.basename(filename).toLowerCase() === 'state.json' ||
|
|
120
|
+
(projectId !== undefined && !id(projectId)) || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_MODEL_STATE_BYTES ||
|
|
121
|
+
!Number.isInteger(debounceMs) || debounceMs < 0 || debounceMs > 1000 || typeof now !== 'function') {
|
|
122
|
+
throw new TypeError('invalid_model_persistence_options');
|
|
123
|
+
}
|
|
124
|
+
const directory = path.dirname(filename);
|
|
125
|
+
let pending = null, timer = null, running = null, closing = null, closed = false;
|
|
126
|
+
let failures = 0, writes = 0;
|
|
127
|
+
|
|
128
|
+
async function write(body) {
|
|
129
|
+
const parent = await safeDirectory(directory);
|
|
130
|
+
await safeDestination(filename);
|
|
131
|
+
const temporary = path.join(directory, `.${path.basename(filename)}.${randomUUID()}.tmp`);
|
|
132
|
+
let file, directoryHandle;
|
|
133
|
+
try {
|
|
134
|
+
directoryHandle = await open(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
135
|
+
const opened = await directoryHandle.stat();
|
|
136
|
+
if (opened.dev !== parent.dev || opened.ino !== parent.ino) invalid();
|
|
137
|
+
file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL |
|
|
138
|
+
(constants.O_NOFOLLOW ?? 0), 0o600);
|
|
139
|
+
await file.writeFile(body, 'utf8');
|
|
140
|
+
await file.sync();
|
|
141
|
+
await file.close(); file = null;
|
|
142
|
+
const current = await safeDirectory(directory);
|
|
143
|
+
if (current.dev !== parent.dev || current.ino !== parent.ino) invalid();
|
|
144
|
+
await safeDestination(filename);
|
|
145
|
+
await rename(temporary, filename);
|
|
146
|
+
try { await directoryHandle.sync(); }
|
|
147
|
+
catch (error) { if (!['EINVAL', 'ENOTSUP'].includes(error.code)) throw error; }
|
|
148
|
+
writes++;
|
|
149
|
+
} finally {
|
|
150
|
+
await file?.close().catch(() => {});
|
|
151
|
+
await directoryHandle?.close().catch(() => {});
|
|
152
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function drain() {
|
|
156
|
+
if (running) return running;
|
|
157
|
+
running = (async () => {
|
|
158
|
+
while (pending !== null) {
|
|
159
|
+
const body = pending; pending = null;
|
|
160
|
+
try { await write(body); } catch { failures++; }
|
|
161
|
+
}
|
|
162
|
+
})().finally(() => { running = null; });
|
|
163
|
+
return running;
|
|
164
|
+
}
|
|
165
|
+
async function flush() {
|
|
166
|
+
do {
|
|
167
|
+
clearTimeout(timer); timer = null;
|
|
168
|
+
await drain();
|
|
169
|
+
} while (pending !== null || running !== null);
|
|
170
|
+
}
|
|
171
|
+
async function load() {
|
|
172
|
+
try {
|
|
173
|
+
await safeDirectory(directory);
|
|
174
|
+
const envelope = await readPrivateJSON(filename, maxBytes);
|
|
175
|
+
if (!plain(envelope) || envelope.schemaVersion !== 2 || !id(envelope.projectId) ||
|
|
176
|
+
(projectId !== undefined && envelope.projectId !== projectId) ||
|
|
177
|
+
!natural(envelope.savedAt) || envelope.savedAt > now() + 60_000 ||
|
|
178
|
+
Object.keys(envelope).some(key => !['schemaVersion', 'savedAt', 'projectId', 'snapshot'].includes(key))) invalid();
|
|
179
|
+
// Revalidate the same depth, JSON and schema limits when reading a file
|
|
180
|
+
// made by an older daemon. Never delete an incompatible or invalid file.
|
|
181
|
+
boundedJSON(envelope, maxBytes);
|
|
182
|
+
validateSnapshot(envelope.snapshot, envelope.projectId);
|
|
183
|
+
projectId ??= envelope.projectId;
|
|
184
|
+
return envelope.snapshot;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code !== 'ENOENT') failures++;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function schedule(snapshot) {
|
|
191
|
+
if (closed) return false;
|
|
192
|
+
try {
|
|
193
|
+
const savedAt = now();
|
|
194
|
+
if (!natural(savedAt)) invalid();
|
|
195
|
+
const candidateProjectId = projectId ?? (plain(snapshot)
|
|
196
|
+
? Object.getOwnPropertyDescriptor(snapshot, 'projectId')?.value : undefined);
|
|
197
|
+
if (!id(candidateProjectId)) invalid();
|
|
198
|
+
// Bounded serialization runs first, before reading any schema properties
|
|
199
|
+
// that could otherwise invoke a caller-provided getter.
|
|
200
|
+
const body = boundedJSON({ schemaVersion: 2, savedAt, projectId: candidateProjectId, snapshot }, maxBytes);
|
|
201
|
+
validateSnapshot(snapshot, candidateProjectId);
|
|
202
|
+
projectId ??= candidateProjectId;
|
|
203
|
+
pending = body; // An immutable serialization, never a caller-owned object.
|
|
204
|
+
if (!timer && !running) {
|
|
205
|
+
timer = setTimeout(() => { timer = null; void drain(); }, debounceMs);
|
|
206
|
+
timer.unref?.();
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
} catch {
|
|
210
|
+
failures++;
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function close() {
|
|
215
|
+
if (!closing) { closed = true; closing = flush(); }
|
|
216
|
+
return closing;
|
|
217
|
+
}
|
|
218
|
+
return { load, schedule, flush, close,
|
|
219
|
+
stats: () => ({ persistenceFailures: failures, writes, pending: pending !== null || running !== null, maxBytes }) };
|
|
220
|
+
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { readFile, chmod, rm } from 'node:fs/promises';
|
|
4
5
|
import { createPipeline } from '../pipeline.mjs';
|
|
5
6
|
import { createPolicy, materializeBundle, buildRelationProposals } from '../core/index.mjs';
|
|
6
|
-
import { createDecisionService } from '../
|
|
7
|
+
import { createDecisionService } from '../decisions/index.mjs';
|
|
8
|
+
import { createJevProvider } from '../jev/provider.mjs';
|
|
9
|
+
import { createAnalysisBroker } from '../decisions/broker.mjs';
|
|
7
10
|
import { projectPaths, canonicalProjectRoot, MAX_IPC_BYTES, MAX_STATE_BYTES, PROTOCOL, runtimeError } from './paths.mjs';
|
|
8
11
|
import { acquireLock } from './lock.mjs';
|
|
9
12
|
import { createAuth } from './auth.mjs';
|
|
10
13
|
import { createPersistence } from './persistence.mjs';
|
|
14
|
+
import { createModelPersistence } from './model-persistence.mjs';
|
|
15
|
+
import { createModelAPI } from './model-api.mjs';
|
|
16
|
+
import { createExtensionAPI } from './extension-api.mjs';
|
|
17
|
+
import { createExtensionRegistry } from '../extensions/index.mjs';
|
|
11
18
|
import { exportSnapshot } from './export.mjs';
|
|
12
19
|
import { createDiagnostics } from './diagnostics.mjs';
|
|
13
20
|
import { createDashboardInfoProvider } from './dashboard-info.mjs';
|
|
21
|
+
import { createLineageReader } from './lineage.mjs';
|
|
22
|
+
import { ARCHITECTURE_PROFILES } from '../architecture/profile.mjs';
|
|
14
23
|
|
|
15
24
|
const WEB = new URL('../web/', import.meta.url);
|
|
16
25
|
const assets = new Map([
|
|
@@ -21,8 +30,17 @@ const assets = new Map([
|
|
|
21
30
|
['/sketch.js', ['sketch.js', 'text/javascript; charset=utf-8']],
|
|
22
31
|
['/style.css', ['style.css', 'text/css; charset=utf-8']],
|
|
23
32
|
]);
|
|
33
|
+
for (const filename of ['platform.js', 'model-client.js', 'scene.js', 'extension-frame.js']) {
|
|
34
|
+
assets.set(`/${filename}`, [filename, 'text/javascript; charset=utf-8']);
|
|
35
|
+
}
|
|
36
|
+
for (const filename of [
|
|
37
|
+
'visualizers/index.mjs', 'visualizers/structure.mjs', 'visualizers/code.mjs',
|
|
38
|
+
'visualizers/blocks.mjs', 'visualizers/c4.mjs', 'visualizers/changes.mjs', 'visualizers/timeline.mjs',
|
|
39
|
+
'model/changes.mjs', 'extensions/scene.mjs', 'extensions/contracts.mjs',
|
|
40
|
+
'extensions/sdk.mjs', 'extensions/profiles.mjs',
|
|
41
|
+
]) assets.set(`/${filename}`, [`../${filename}`, 'text/javascript; charset=utf-8']);
|
|
24
42
|
const HOSTS = new Set(['claude', 'codex', 'kiro']);
|
|
25
|
-
const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'";
|
|
43
|
+
const CSP = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'";
|
|
26
44
|
|
|
27
45
|
function headers(res) {
|
|
28
46
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -53,25 +71,27 @@ async function bodyJSON(req) {
|
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
56
|
-
decisionService, apiKey: configuredKey, mode = 'live', port = 0, dashboardInfoDependencies } = {}) {
|
|
74
|
+
decisionService, decisionProvider, apiKey: configuredKey, mode = 'live', port = 0, dashboardInfoDependencies } = {}) {
|
|
57
75
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw runtimeError('invalid_port');
|
|
58
76
|
if (!['live', 'demo'].includes(mode)) throw runtimeError('invalid_mode');
|
|
59
77
|
const paths = await projectPaths(projectRoot, dataDir, { create: true });
|
|
78
|
+
const readLineage = createLineageReader({ projectRoot: paths.projectRoot, projectId: paths.projectId });
|
|
60
79
|
const dashboardInfo = createDashboardInfoProvider({
|
|
61
80
|
projectRoot: paths.projectRoot, dataDir: paths.dataDir, mode,
|
|
62
81
|
}, dashboardInfoDependencies);
|
|
63
82
|
const lock = await acquireLock(paths);
|
|
64
83
|
let finished;
|
|
65
84
|
const whenClosed = new Promise(resolve => { finished = resolve; });
|
|
66
|
-
let web, ipc, pipeline, auth, diagnostics, interval, ping, closing, reconciling = false;
|
|
85
|
+
let web, ipc, pipeline, auth, diagnostics, modelAPI, extensionAPI, modelFlush, interval, ping, closing, reconciling = false;
|
|
67
86
|
let drops = 0, intake = 0;
|
|
68
87
|
const receivedHooks = { claude: 0, codex: 0, kiro: 0 };
|
|
69
88
|
const clients = new Set(), connections = new Set(), ipcConnections = new Set();
|
|
70
89
|
const persistence = createPersistence(paths.state);
|
|
90
|
+
const modelPersistence = createModelPersistence(path.join(paths.directory, 'model-state.json'), { projectId: paths.projectId });
|
|
71
91
|
const policy = createPolicy(policyOptions ?? {});
|
|
72
|
-
const apiKey = !decisionService && policy.transmitSource && mode === 'live'
|
|
92
|
+
const apiKey = !decisionService && !decisionProvider && policy.transmitSource && mode === 'live'
|
|
73
93
|
? configuredKey ?? process.env.TYPESAFE_API_KEY : undefined;
|
|
74
|
-
const missingKey = !decisionService && policy.transmitSource && mode === 'live' && !apiKey;
|
|
94
|
+
const missingKey = !decisionService && !decisionProvider && policy.transmitSource && mode === 'live' && !apiKey;
|
|
75
95
|
function snapshot(persistent = false) {
|
|
76
96
|
const state = pipeline.getState({ persistent });
|
|
77
97
|
return { ...state, status: { ...state.status,
|
|
@@ -88,13 +108,25 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
88
108
|
function notify() {
|
|
89
109
|
if (!pipeline || closing) return;
|
|
90
110
|
persistence.schedule(snapshot(true));
|
|
111
|
+
if (!modelFlush) {
|
|
112
|
+
// Coalesce model serialization outside the hook intake path.
|
|
113
|
+
modelFlush = setTimeout(() => {
|
|
114
|
+
modelFlush = null;
|
|
115
|
+
if (!closing) modelPersistence.schedule(pipeline.getModelState({ persistent: true }));
|
|
116
|
+
}, 100);
|
|
117
|
+
modelFlush.unref?.();
|
|
118
|
+
}
|
|
119
|
+
modelAPI?.notify();
|
|
91
120
|
const state = snapshot();
|
|
92
121
|
for (const res of clients) stream(res, state);
|
|
93
122
|
}
|
|
94
123
|
async function reconcile() {
|
|
95
124
|
if (reconciling || closing || !pipeline) return;
|
|
96
125
|
reconciling = true;
|
|
97
|
-
try {
|
|
126
|
+
try {
|
|
127
|
+
await pipeline.observeLineage(await readLineage());
|
|
128
|
+
await pipeline.reconcile();
|
|
129
|
+
}
|
|
98
130
|
catch { drops++; }
|
|
99
131
|
finally { reconciling = false; }
|
|
100
132
|
}
|
|
@@ -102,6 +134,8 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
102
134
|
if (closing) return closing;
|
|
103
135
|
closing = (async () => {
|
|
104
136
|
clearInterval(interval); clearInterval(ping);
|
|
137
|
+
clearTimeout(modelFlush);
|
|
138
|
+
modelAPI?.close();
|
|
105
139
|
auth?.clear();
|
|
106
140
|
for (const res of clients) res.end();
|
|
107
141
|
for (const socket of [...connections, ...ipcConnections]) socket.destroy();
|
|
@@ -109,8 +143,11 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
109
143
|
new Promise(resolve => server.close(() => resolve()))));
|
|
110
144
|
try {
|
|
111
145
|
await pipeline?.close();
|
|
112
|
-
if (pipeline)
|
|
113
|
-
|
|
146
|
+
if (pipeline) {
|
|
147
|
+
persistence.schedule(snapshot(true));
|
|
148
|
+
modelPersistence.schedule(pipeline.getModelState({ persistent: true }));
|
|
149
|
+
}
|
|
150
|
+
await Promise.all([persistence.close(), modelPersistence.close()]);
|
|
114
151
|
} finally {
|
|
115
152
|
try { await diagnostics?.close(); }
|
|
116
153
|
finally {
|
|
@@ -127,19 +164,40 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
127
164
|
// Reading the key is conditional on explicit source-transmission permission.
|
|
128
165
|
// Test/demo services are injected; this module never logs request bodies.
|
|
129
166
|
const service = decisionService ?? createDecisionService({
|
|
130
|
-
apiKey,
|
|
131
|
-
materializeBundle, buildRelationProposals,
|
|
167
|
+
provider: decisionProvider ?? createJevProvider({ apiKey }),
|
|
168
|
+
materializeBundle, buildRelationProposals, profiles: ARCHITECTURE_PROFILES,
|
|
132
169
|
limits: { eventDeadlineMs: 5000 },
|
|
133
170
|
});
|
|
134
171
|
pipeline = createPipeline({ projectRoot: paths.projectRoot, policy, decisionService: service,
|
|
135
|
-
classificationDeadlineMs: 5000,
|
|
136
|
-
mode, restoredState: await persistence.load(),
|
|
172
|
+
classificationDeadlineMs: 5000, missingKey,
|
|
173
|
+
mode, restoredState: await persistence.load(), restoredModel: await modelPersistence.load(),
|
|
174
|
+
onChange: notify, onDiagnostic: diagnostics.record });
|
|
175
|
+
modelAPI = createModelAPI({ projectId: paths.projectId, getSnapshot: pipeline.getModelState,
|
|
176
|
+
createCheckpoint: options => {
|
|
177
|
+
const marker = pipeline.createCheckpoint(options);
|
|
178
|
+
notify();
|
|
179
|
+
return marker;
|
|
180
|
+
} });
|
|
181
|
+
const registry = await createExtensionRegistry({ dataDir: paths.dataDir, projectId: paths.projectId });
|
|
182
|
+
const analyze = typeof service.evaluate === 'function'
|
|
183
|
+
? createAnalysisBroker({ service, model: pipeline.model, policy, projectId: paths.projectId, registry })
|
|
184
|
+
: async () => ({ status: 'unavailable' });
|
|
185
|
+
extensionAPI = createExtensionAPI({ registry, projectId: paths.projectId, getSnapshot: pipeline.getModelState,
|
|
186
|
+
runAnalysis: async input => { const result = await analyze(input); notify(); return result; } });
|
|
137
187
|
web = http.createServer({ maxHeaderSize: 8192, requestTimeout: 2000, headersTimeout: 2000 }, (req, res) => {
|
|
138
188
|
headers(res);
|
|
139
189
|
void (async () => {
|
|
190
|
+
if (req.url?.startsWith('/api/model/v1/')) {
|
|
191
|
+
if (!auth?.validTransport(req)) return json(res, 403, { error: 'forbidden_origin' });
|
|
192
|
+
await modelAPI.handle(req, res, {
|
|
193
|
+
viewerAuthorized: auth.validRequest(req, { mutation: req.method !== 'GET' }) && auth.authorized(req),
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
140
197
|
if (!auth || !auth.validRequest(req, { mutation: req.method !== 'GET' })) {
|
|
141
198
|
return json(res, 403, { error: 'forbidden_origin' });
|
|
142
199
|
}
|
|
200
|
+
if (await extensionAPI.handle(req, res, { viewerAuthorized: auth.authorized(req) })) return;
|
|
143
201
|
if (typeof req.url !== 'string' || req.url.length > 1024 || req.url.includes('?')) {
|
|
144
202
|
return json(res, 400, { error: 'invalid_route' });
|
|
145
203
|
}
|
|
@@ -161,6 +219,14 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
161
219
|
return;
|
|
162
220
|
}
|
|
163
221
|
if (!auth.authorized(req)) return json(res, 401, { error: 'authentication_required' });
|
|
222
|
+
if (req.method === 'GET' && req.url === '/api/architecture') {
|
|
223
|
+
return json(res, 200, pipeline.getArchitectureStatus());
|
|
224
|
+
}
|
|
225
|
+
if (req.method === 'POST' && req.url === '/api/architecture/discover') {
|
|
226
|
+
const input = await bodyJSON(req);
|
|
227
|
+
if (Object.keys(input).length) return json(res, 400, { error: 'invalid_input' });
|
|
228
|
+
return json(res, 202, pipeline.discoverArchitecture());
|
|
229
|
+
}
|
|
164
230
|
if (req.method === 'GET' && req.url === '/api/about') {
|
|
165
231
|
try { return json(res, 200, await dashboardInfo()); }
|
|
166
232
|
catch { return json(res, 503, { error: 'dashboard_info_unavailable' }); }
|
|
@@ -218,10 +284,11 @@ export async function startServer({ projectRoot, dataDir, policy: policyOptions,
|
|
|
218
284
|
function describe() {
|
|
219
285
|
return { ok: true, protocol: PROTOCOL, instanceId: lock.owner.instanceId,
|
|
220
286
|
projectId: paths.projectId, pid: process.pid, port: actualPort, mode,
|
|
221
|
-
policy: { transmitSource: policy.transmitSource, displayEvidence: policy.displayEvidence,
|
|
287
|
+
policy: { readSource: policy.readSource, transmitSource: policy.transmitSource, displayEvidence: policy.displayEvidence,
|
|
222
288
|
persistEvidence: policy.persistEvidence, version: policy.version },
|
|
223
289
|
status: snapshot().status, ...persistence.stats(), captureDropped: drops,
|
|
224
290
|
observations: { hooks: { ...receivedHooks }, shapes: pipeline.getState().graph.nodes.length },
|
|
291
|
+
model: pipeline.model.stats(), modelPersistence: modelPersistence.stats(),
|
|
225
292
|
logPath: diagnostics.stats().logPath, diagnostics: diagnostics.stats() };
|
|
226
293
|
}
|
|
227
294
|
// Only the exclusive owner may remove a stale socket from a prior process.
|
|
@@ -7,8 +7,11 @@ const LIMIT = 16 * 1024;
|
|
|
7
7
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
8
|
const validKey = value => typeof value === 'string' && value.length > 0 &&
|
|
9
9
|
value.length <= 4096 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
10
|
-
const validPolicy = value => record(value) &&
|
|
11
|
-
['allowSource', 'persistEvidence', 'displayEvidence'].
|
|
10
|
+
const validPolicy = value => record(value) &&
|
|
11
|
+
Object.keys(value).every(key => ['allowSource', 'localSource', 'persistEvidence', 'displayEvidence'].includes(key)) &&
|
|
12
|
+
['allowSource', 'persistEvidence', 'displayEvidence'].every(key => typeof value[key] === 'boolean') &&
|
|
13
|
+
(value.localSource === undefined || typeof value.localSource === 'boolean') &&
|
|
14
|
+
!(value.localSource && value.allowSource);
|
|
12
15
|
const validHosts = value => Array.isArray(value) && value.length <= 2 &&
|
|
13
16
|
new Set(value).size === value.length && value.every(host => ['claude', 'codex'].includes(host));
|
|
14
17
|
const validInstallation = value => record(value) &&
|
|
@@ -89,14 +92,19 @@ export function savedPolicy(policy) {
|
|
|
89
92
|
allowSource: policy.transmitSource,
|
|
90
93
|
persistEvidence: policy.persistEvidence,
|
|
91
94
|
displayEvidence: policy.displayEvidence,
|
|
95
|
+
...(policy.readSource && !policy.transmitSource ? { localSource: true } : {}),
|
|
92
96
|
} : {};
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// An omitted field means reuse consent, while false is an explicit opt-out.
|
|
96
100
|
export function resolvePolicy(options, { current, saved } = {}) {
|
|
97
101
|
const fallback = current ? savedPolicy(current) : saved ?? {};
|
|
102
|
+
const allowSource = options.allowSource ?? fallback.allowSource ?? false;
|
|
103
|
+
const localSource = !allowSource && (options.localSource ??
|
|
104
|
+
(options.allowSource === false ? false : fallback.localSource) ?? false);
|
|
98
105
|
return {
|
|
99
|
-
allowSource
|
|
106
|
+
allowSource,
|
|
107
|
+
...(localSource ? { localSource: true } : {}),
|
|
100
108
|
persistEvidence: options.persistEvidence ?? fallback.persistEvidence ?? false,
|
|
101
109
|
displayEvidence: options.displayEvidence ?? fallback.displayEvidence ?? true,
|
|
102
110
|
};
|