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.
Files changed (106) 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 +472 -0
  8. package/docs/visualizer-views.md +240 -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 +78 -9
  34. package/plugin.json +4 -2
  35. package/runtime/architecture/analysis.mjs +344 -0
  36. package/runtime/architecture/controller.mjs +209 -0
  37. package/runtime/architecture/evidence.mjs +108 -0
  38. package/runtime/architecture/profile.mjs +56 -0
  39. package/runtime/core/evidence.mjs +43 -9
  40. package/runtime/core/graph.mjs +11 -6
  41. package/runtime/core/privacy.mjs +1 -0
  42. package/runtime/daemon/auth.mjs +7 -3
  43. package/runtime/daemon/diagnostics.mjs +1 -1
  44. package/runtime/daemon/extension-api.mjs +203 -0
  45. package/runtime/daemon/lineage.mjs +70 -0
  46. package/runtime/daemon/manager.mjs +9 -6
  47. package/runtime/daemon/model-api.mjs +728 -0
  48. package/runtime/daemon/model-persistence.mjs +220 -0
  49. package/runtime/daemon/server.mjs +81 -14
  50. package/runtime/daemon/settings.mjs +11 -3
  51. package/runtime/decisions/broker.mjs +349 -0
  52. package/runtime/decisions/contracts.mjs +179 -0
  53. package/runtime/decisions/evaluation.mjs +305 -0
  54. package/runtime/decisions/faults.mjs +32 -0
  55. package/runtime/decisions/index.mjs +818 -0
  56. package/runtime/decisions/profiles.mjs +93 -0
  57. package/runtime/decisions/questions.mjs +268 -0
  58. package/runtime/discovery/index.mjs +2 -0
  59. package/runtime/discovery/inventory.mjs +160 -0
  60. package/runtime/discovery/parser.mjs +40 -0
  61. package/runtime/discovery/structure.mjs +232 -0
  62. package/runtime/extensions/contracts.mjs +59 -0
  63. package/runtime/extensions/frame.mjs +64 -0
  64. package/runtime/extensions/index.mjs +9 -0
  65. package/runtime/extensions/manifest.mjs +95 -0
  66. package/runtime/extensions/packages.mjs +222 -0
  67. package/runtime/extensions/profiles.mjs +36 -0
  68. package/runtime/extensions/projection.mjs +130 -0
  69. package/runtime/extensions/registry.mjs +285 -0
  70. package/runtime/extensions/scene.mjs +105 -0
  71. package/runtime/extensions/sdk.d.ts +205 -0
  72. package/runtime/extensions/sdk.mjs +88 -0
  73. package/runtime/jev/index.mjs +13 -777
  74. package/runtime/jev/provider.mjs +101 -0
  75. package/runtime/jev/questions.mjs +16 -258
  76. package/runtime/jev/wire.mjs +17 -25
  77. package/runtime/model/changes.mjs +42 -0
  78. package/runtime/model/history.mjs +124 -0
  79. package/runtime/model/index.mjs +2 -0
  80. package/runtime/model/project-model.mjs +1020 -0
  81. package/runtime/model/records.mjs +240 -0
  82. package/runtime/pipeline.mjs +267 -55
  83. package/runtime/platform.mjs +254 -0
  84. package/runtime/visualizers/blocks.mjs +5 -0
  85. package/runtime/visualizers/c4.mjs +154 -0
  86. package/runtime/visualizers/changes.mjs +24 -0
  87. package/runtime/visualizers/code.mjs +5 -0
  88. package/runtime/visualizers/index.mjs +23 -0
  89. package/runtime/visualizers/structure.mjs +120 -0
  90. package/runtime/visualizers/timeline.mjs +66 -0
  91. package/runtime/web/app.js +225 -63
  92. package/runtime/web/extension-frame.js +128 -0
  93. package/runtime/web/index.html +38 -1
  94. package/runtime/web/model-client.js +162 -0
  95. package/runtime/web/platform.js +445 -0
  96. package/runtime/web/scene.js +111 -0
  97. package/runtime/web/style.css +51 -0
  98. package/schemas/graph.schema.json +4 -1
  99. package/scripts/arguments.mjs +5 -1
  100. package/scripts/build-packages.mjs +6 -2
  101. package/scripts/control.mjs +1 -1
  102. package/scripts/daemon.mjs +2 -1
  103. package/scripts/extensions.mjs +44 -0
  104. package/scripts/graphlin.mjs +23 -3
  105. package/scripts/onboarding.mjs +10 -3
  106. 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 '../jev/index.mjs';
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 { await pipeline.reconcile(); }
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) persistence.schedule(snapshot(true));
113
- await persistence.close();
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(), onChange: notify, onDiagnostic: diagnostics.record });
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) && Object.keys(value).length === 3 &&
11
- ['allowSource', 'persistEvidence', 'displayEvidence'].every(key => typeof value[key] === 'boolean');
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: options.allowSource ?? fallback.allowSource ?? false,
106
+ allowSource,
107
+ ...(localSource ? { localSource: true } : {}),
100
108
  persistEvidence: options.persistEvidence ?? fallback.persistEvidence ?? false,
101
109
  displayEvidence: options.displayEvidence ?? fallback.displayEvidence ?? true,
102
110
  };