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
package/runtime/pipeline.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
|
-
import { readdir } from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import {
|
|
6
5
|
createPolicy, normalizeHostEvent, metadataEvent, EvidenceStore,
|
|
@@ -8,6 +7,9 @@ import {
|
|
|
8
7
|
applyPatch, projectGraph,
|
|
9
8
|
} from './core/index.mjs';
|
|
10
9
|
import { safeLabel, safeText, excluded } from './core/privacy.mjs';
|
|
10
|
+
import { createPlatform } from './platform.mjs';
|
|
11
|
+
import { createArchitectureController } from './architecture/controller.mjs';
|
|
12
|
+
import { analyzeArchitecture, ARCHITECTURE_NAMESPACE } from './architecture/analysis.mjs';
|
|
11
13
|
|
|
12
14
|
const MAX_ACTIVITY = 200;
|
|
13
15
|
const MAX_HOOK_EVENTS = 200;
|
|
@@ -23,11 +25,6 @@ const CLASSIFICATION_QUEUE_TTL_MS = 120_000;
|
|
|
23
25
|
const DEADLINE_MS = 2000;
|
|
24
26
|
const PENDING_LEASE_MS = 60_000;
|
|
25
27
|
const TERMINAL = new Set(['succeeded', 'failed', 'interrupted', 'unresolved']);
|
|
26
|
-
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|cs|swift|sql|ya?ml|json|toml|tf)$/i;
|
|
27
|
-
const SKIP_DIR = new Set([
|
|
28
|
-
'.git', '.graphlin', '.graphlin-data', 'node_modules', 'dist', 'build',
|
|
29
|
-
'coverage', '.next', '.cache', '.venv', 'venv', 'vendor', 'research',
|
|
30
|
-
]);
|
|
31
28
|
const FIXED_CLASSIFIER = new Set([
|
|
32
29
|
'ready', 'metadata_only', 'missing_key', 'paused', 'unavailable', 'timeout', 'demo',
|
|
33
30
|
]);
|
|
@@ -97,8 +94,8 @@ function restoreGraph(input, { stale = true } = {}) {
|
|
|
97
94
|
*/
|
|
98
95
|
export function createPipeline({
|
|
99
96
|
projectRoot, policy: policyOptions, decisionService, onChange = () => {},
|
|
100
|
-
onDiagnostic = () => {}, restoredState, mode = 'live', clock = Date.now,
|
|
101
|
-
classificationDeadlineMs = DEADLINE_MS,
|
|
97
|
+
onDiagnostic = () => {}, restoredState, restoredModel, mode = 'live', clock = Date.now,
|
|
98
|
+
classificationDeadlineMs = DEADLINE_MS, missingKey = false,
|
|
102
99
|
} = {}) {
|
|
103
100
|
if (!Number.isSafeInteger(classificationDeadlineMs) ||
|
|
104
101
|
classificationDeadlineMs < DEADLINE_MS || classificationDeadlineMs > 10_000) {
|
|
@@ -107,13 +104,37 @@ export function createPipeline({
|
|
|
107
104
|
const root = realpathSync(projectRoot);
|
|
108
105
|
const inputRoot = path.resolve(projectRoot);
|
|
109
106
|
const projectId = opaque(root);
|
|
107
|
+
const modelProjectId = createHash('sha256').update(root).digest('hex');
|
|
110
108
|
const policy = createPolicy(policyOptions ?? {});
|
|
111
|
-
|
|
109
|
+
// Restored claims stay stale until recaptured, and their versions must never
|
|
110
|
+
// collide with a new daemon's counters. No saved content is trusted here.
|
|
111
|
+
let generationFloor = 0;
|
|
112
|
+
const retainGeneration = value => {
|
|
113
|
+
if (Number.isSafeInteger(value) && value > generationFloor && value < Number.MAX_SAFE_INTEGER) generationFloor = value;
|
|
114
|
+
};
|
|
115
|
+
const retainRefs = rows => {
|
|
116
|
+
for (const row of Array.isArray(rows) ? rows.slice(0, 40000) : []) {
|
|
117
|
+
for (const ref of Array.isArray(row?.sourceRefs) ? row.sourceRefs.slice(0, 16) : []) retainGeneration(ref.generation);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (restoredModel?.projectId === modelProjectId) {
|
|
121
|
+
const artifacts = restoredModel.coverage?.artifacts;
|
|
122
|
+
for (const artifact of Array.isArray(artifacts) ? artifacts.slice(0, 10000) : []) retainGeneration(artifact?.generation);
|
|
123
|
+
for (const rows of [restoredModel.entities, restoredModel.relations, restoredModel.interpretations]) retainRefs(rows);
|
|
124
|
+
}
|
|
125
|
+
if (restoredState?.projectId === projectId) {
|
|
126
|
+
const states = Array.isArray(restoredState.sessionStates) ? restoredState.sessionStates.slice(0, MAX_SESSIONS) : [];
|
|
127
|
+
for (const session of [restoredState, ...states]) {
|
|
128
|
+
retainRefs(session?.graph?.nodes); retainRefs(session?.graph?.edges);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const evidence = new EvidenceStore({ projectRoot, policy, maxTrackedPaths: 10000, generationFloor });
|
|
112
132
|
const sessions = new Map();
|
|
113
133
|
const dedup = new Map();
|
|
114
134
|
const sessionStarts = new Map();
|
|
115
135
|
const hookEvents = [];
|
|
116
136
|
const knownArtifacts = new Map();
|
|
137
|
+
const lineageWork = new Set();
|
|
117
138
|
const messageVersions = new Map();
|
|
118
139
|
const deferredWork = new Map();
|
|
119
140
|
const classificationQueue = [];
|
|
@@ -130,16 +151,144 @@ export function createPipeline({
|
|
|
130
151
|
let pending = 0;
|
|
131
152
|
let classifier = mode === 'demo' ? 'demo' : policy.transmitSource ? 'ready' : 'metadata_only';
|
|
132
153
|
let serial = Promise.resolve();
|
|
133
|
-
let lastDiscoveryAt = -Infinity;
|
|
134
|
-
let discoveredPaths = [];
|
|
135
154
|
let reconciliationTask = null;
|
|
136
155
|
let resumeScheduled = false;
|
|
156
|
+
let lineageId = null;
|
|
157
|
+
let architecture;
|
|
158
|
+
let architectureEpoch = 0;
|
|
159
|
+
const architectureGuards = new WeakMap();
|
|
137
160
|
|
|
138
161
|
const serialized = (fn) => {
|
|
139
162
|
const operation = serial.then(fn);
|
|
140
163
|
serial = operation.catch(() => {});
|
|
141
164
|
return operation;
|
|
142
165
|
};
|
|
166
|
+
const platform = createPlatform({
|
|
167
|
+
projectRoot: root, projectId: modelProjectId,
|
|
168
|
+
policy, restoredState: restoredModel, now: clock,
|
|
169
|
+
accept: operation => serialized(operation),
|
|
170
|
+
revalidate: refs => serialized(async () => {
|
|
171
|
+
if (closed) return false;
|
|
172
|
+
registerArtifacts(await evidence.reconcile({ refs }));
|
|
173
|
+
return evidence.isCurrent(refs);
|
|
174
|
+
}),
|
|
175
|
+
onChange: () => { architecture?.wake(); notify(); },
|
|
176
|
+
});
|
|
177
|
+
architecture = createArchitectureController({
|
|
178
|
+
snapshot: () => platform.snapshot(),
|
|
179
|
+
capture: captureArchitecture,
|
|
180
|
+
commit: commitArchitecture,
|
|
181
|
+
analyze: async input => {
|
|
182
|
+
const guard = {
|
|
183
|
+
epoch: architectureEpoch, signal: input.signal, policyVersion: policy.version,
|
|
184
|
+
lineageId: input.model.coverage.lineage?.id ?? modelProjectId,
|
|
185
|
+
};
|
|
186
|
+
const result = await analyzeArchitecture({ ...input, service: decisionService, policy });
|
|
187
|
+
architectureGuards.set(result, guard);
|
|
188
|
+
return result;
|
|
189
|
+
},
|
|
190
|
+
available: architectureUnavailable,
|
|
191
|
+
ready: () => {
|
|
192
|
+
const { queued, active } = platform.stats();
|
|
193
|
+
return queued === 0 && active === 0;
|
|
194
|
+
},
|
|
195
|
+
onChange: notify,
|
|
196
|
+
onDiagnostic: result => trace(null, 'classification', {
|
|
197
|
+
status: result.status === 'complete' ? 'accepted' : result.status === 'partial' ? 'partial' : 'unavailable',
|
|
198
|
+
reason: result.status === 'complete' ? 'ok' : result.status === 'partial' ? 'unknown' : 'decision_failure',
|
|
199
|
+
diagnostics: { calls: result.providerRequests },
|
|
200
|
+
}),
|
|
201
|
+
now: clock,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
function architectureUnavailable() {
|
|
205
|
+
if (closed) return 'closed';
|
|
206
|
+
if (!policy.transmitSource) return 'source_consent_required';
|
|
207
|
+
if (missingKey) return 'missing_key';
|
|
208
|
+
if (paused) return 'paused';
|
|
209
|
+
if (mode === 'demo') return 'demo';
|
|
210
|
+
if (typeof decisionService?.analyze !== 'function' || typeof decisionService?.evaluate !== 'function') {
|
|
211
|
+
return 'unsupported_service';
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function captureArchitecture(ids) {
|
|
217
|
+
const artifacts = await serialized(async () => {
|
|
218
|
+
if (architectureUnavailable()) return [];
|
|
219
|
+
const refs = ids.slice(0, 64).filter(id => knownArtifacts.has(id)).map(artifactId => ({ artifactId }));
|
|
220
|
+
const captures = await evidence.reconcile({ refs });
|
|
221
|
+
registerArtifacts(captures);
|
|
222
|
+
return captures;
|
|
223
|
+
});
|
|
224
|
+
// Parser acceptance uses the same serial queue; waiting inside it deadlocks.
|
|
225
|
+
await platform.whenIdle();
|
|
226
|
+
return artifacts;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function commitArchitecture(result, context) {
|
|
230
|
+
return serialized(async () => {
|
|
231
|
+
const guard = architectureGuards.get(result);
|
|
232
|
+
architectureGuards.delete(result);
|
|
233
|
+
const current = () => guard && guard.signal === context.signal && !context.signal.aborted &&
|
|
234
|
+
guard.epoch === architectureEpoch && guard.policyVersion === policy.version &&
|
|
235
|
+
guard.lineageId === (lineageId ?? platform.snapshot().coverage.lineage?.id ?? modelProjectId) &&
|
|
236
|
+
!architectureUnavailable();
|
|
237
|
+
if (!current() || result.sourceRefs.length > 256 || context.artifacts.length > 64) return false;
|
|
238
|
+
const expected = new Map();
|
|
239
|
+
const sameVersion = (a, b) => a && b && a.hash === b.hash &&
|
|
240
|
+
a.generation === b.generation && a.status === b.status;
|
|
241
|
+
for (const value of [...context.artifacts, ...result.sourceRefs.map(ref => ({ ...ref, status: 'present' }))]) {
|
|
242
|
+
if (!knownArtifacts.has(value.artifactId) ||
|
|
243
|
+
expected.has(value.artifactId) && !sameVersion(expected.get(value.artifactId), value)) return false;
|
|
244
|
+
expected.set(value.artifactId, value);
|
|
245
|
+
}
|
|
246
|
+
if (expected.size > 256) return false;
|
|
247
|
+
const observed = new Map();
|
|
248
|
+
const refs = [...expected.keys()].map(artifactId => ({ artifactId }));
|
|
249
|
+
// Re-read all selected versions, including absence, and any extra
|
|
250
|
+
// membership support. Retain metadata only across these bounded reads.
|
|
251
|
+
for (let offset = 0; offset < refs.length; offset += 32) {
|
|
252
|
+
const captures = await evidence.reconcile({ refs: refs.slice(offset, offset + 32) });
|
|
253
|
+
registerArtifacts(captures);
|
|
254
|
+
for (const artifact of captures) observed.set(artifact.id, {
|
|
255
|
+
hash: artifact.hash, generation: artifact.generation, status: artifact.status,
|
|
256
|
+
exists: artifact.exists, complete: artifact.complete,
|
|
257
|
+
});
|
|
258
|
+
if (!current()) return false;
|
|
259
|
+
}
|
|
260
|
+
if ([...expected].some(([id, value]) => !sameVersion(value, observed.get(id)))) return false;
|
|
261
|
+
const missing = new Set(result.coverage.missingArtifactIds ?? []);
|
|
262
|
+
const selected = new Set(context.artifacts.map(value => value.artifactId));
|
|
263
|
+
for (const id of missing) {
|
|
264
|
+
const value = observed.get(id);
|
|
265
|
+
if (!selected.has(id) || value?.status !== 'missing' || value.hash !== null ||
|
|
266
|
+
value.exists !== false || value.complete !== true) return false;
|
|
267
|
+
}
|
|
268
|
+
const withdrawn = new Set(result.coverage.withdrawnEntityIds ?? []);
|
|
269
|
+
const model = platform.snapshot();
|
|
270
|
+
const entities = new Map(model.entities.map(value => [value.id, value]));
|
|
271
|
+
if ([...withdrawn].some(id => !missing.has(entities.get(id)?.artifactId))) return false;
|
|
272
|
+
const affected = result.affectedEntityIds.filter(id => !withdrawn.has(id));
|
|
273
|
+
let omitted = 0;
|
|
274
|
+
if (affected.length || result.interpretations.length) {
|
|
275
|
+
const replacement = platform.model.replaceInterpretations(ARCHITECTURE_NAMESPACE, result.interpretations, {
|
|
276
|
+
affectedEntityIds: affected, sourceRefs: result.sourceRefs,
|
|
277
|
+
});
|
|
278
|
+
if (!replacement.accepted) return false;
|
|
279
|
+
omitted = Math.max(0, result.interpretations.length - replacement.retained);
|
|
280
|
+
}
|
|
281
|
+
if (missing.size) {
|
|
282
|
+
// Present-source guards cannot prove deletion. The serialized reread
|
|
283
|
+
// above authorizes this clear, even when deleted anchors were evicted.
|
|
284
|
+
const cleared = platform.model.replaceInterpretations(ARCHITECTURE_NAMESPACE, [], {
|
|
285
|
+
affectedEntityIds: [...withdrawn], artifactIds: [...missing],
|
|
286
|
+
});
|
|
287
|
+
if (!cleared.accepted) return false;
|
|
288
|
+
}
|
|
289
|
+
return omitted ? { accepted: true, omitted } : true;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
143
292
|
|
|
144
293
|
function artifactMetadata(artifact) {
|
|
145
294
|
const relative = artifact.relativePath;
|
|
@@ -217,17 +366,24 @@ export function createPipeline({
|
|
|
217
366
|
const session = freshSession(id, `Session ${sessions.size + 1}`);
|
|
218
367
|
sessions.set(id, session);
|
|
219
368
|
selectedSession ??= id;
|
|
369
|
+
platform.setSessions([...sessions.values()].map(item => ({ id: item.id, label: item.label })));
|
|
220
370
|
return session;
|
|
221
371
|
}
|
|
222
372
|
|
|
223
373
|
function evictSession(id) {
|
|
224
374
|
sessions.delete(id);
|
|
375
|
+
syncSessions();
|
|
225
376
|
deferredWork.delete(id);
|
|
226
377
|
for (const key of completedClassifications.keys()) {
|
|
227
378
|
if (key.startsWith(`${id}:`)) completedClassifications.delete(key);
|
|
228
379
|
}
|
|
229
380
|
}
|
|
230
381
|
|
|
382
|
+
function syncSessions() {
|
|
383
|
+
platform.setSessions([...sessions.values()].map(({ id, label, host, status, startedAt, endedAt }) =>
|
|
384
|
+
({ id, label, host, status, startedAt, endedAt })));
|
|
385
|
+
}
|
|
386
|
+
|
|
231
387
|
if (restoredState?.projectId === projectId) {
|
|
232
388
|
const saved = Array.isArray(restoredState.sessionStates)
|
|
233
389
|
? [restoredState, ...restoredState.sessionStates.filter(s => s.id !== restoredState.sessionId)].slice(-MAX_SESSIONS)
|
|
@@ -237,6 +393,7 @@ export function createPipeline({
|
|
|
237
393
|
if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(id)) continue;
|
|
238
394
|
const session = ensureSession(id);
|
|
239
395
|
session.graph = restoreGraph(entry.graph);
|
|
396
|
+
platform.observeLegacy(session.graph, { sessionId: session.id });
|
|
240
397
|
// Activity is reconstructed through the metadata allowlist, never trusted verbatim.
|
|
241
398
|
session.activity = (Array.isArray(entry.activity) ? entry.activity : [])
|
|
242
399
|
.slice(-MAX_ACTIVITY).map(item => {
|
|
@@ -329,6 +486,7 @@ export function createPipeline({
|
|
|
329
486
|
function recordPatch(session, patch) {
|
|
330
487
|
if (!patch || !patch.operations?.length) return;
|
|
331
488
|
session.graph = applyPatch(session.graph, patch);
|
|
489
|
+
platform.observeLegacy(session.graph, { sessionId: session.id, fresh: true });
|
|
332
490
|
session.history.push({
|
|
333
491
|
revision: session.graph.revision, at: new Date(clock()).toISOString(),
|
|
334
492
|
graph: structuredClone(session.graph),
|
|
@@ -356,7 +514,7 @@ export function createPipeline({
|
|
|
356
514
|
}
|
|
357
515
|
}
|
|
358
516
|
|
|
359
|
-
function registerArtifacts(artifacts) {
|
|
517
|
+
function registerArtifacts(artifacts, event) {
|
|
360
518
|
const changed = [];
|
|
361
519
|
for (const artifact of artifacts) {
|
|
362
520
|
const previous = knownArtifacts.get(artifact.id);
|
|
@@ -390,12 +548,15 @@ export function createPipeline({
|
|
|
390
548
|
recordPatch(session, invalidateArtifacts(session.graph, changed));
|
|
391
549
|
}
|
|
392
550
|
}
|
|
551
|
+
platform.observeArtifacts(artifacts, event);
|
|
552
|
+
if (changed.length) architecture.observe(changed);
|
|
393
553
|
return changed;
|
|
394
554
|
}
|
|
395
555
|
|
|
396
556
|
function addActivity(session, event) {
|
|
397
557
|
const state = activityState(event);
|
|
398
558
|
const row = { ...metadataEvent(event), label: activityLabel(event), state };
|
|
559
|
+
platform.recordActivity(row);
|
|
399
560
|
const existing = event.toolCallId
|
|
400
561
|
? session.activity.findIndex(item => item.toolCallId === event.toolCallId &&
|
|
401
562
|
item.agentId === event.agentId && item.kind?.startsWith('tool.'))
|
|
@@ -422,6 +583,7 @@ export function createPipeline({
|
|
|
422
583
|
sequence: ++sequence, incomplete: true,
|
|
423
584
|
});
|
|
424
585
|
session.activity[index] = { ...event, label: activityLabel(event), state: 'unresolved' };
|
|
586
|
+
platform.recordActivity(event);
|
|
425
587
|
changed = true;
|
|
426
588
|
}
|
|
427
589
|
}
|
|
@@ -429,31 +591,7 @@ export function createPipeline({
|
|
|
429
591
|
}
|
|
430
592
|
|
|
431
593
|
async function discoverPaths() {
|
|
432
|
-
|
|
433
|
-
// Core still authorizes each resolved file before reading any content.
|
|
434
|
-
if (clock() - lastDiscoveryAt < 1000) return discoveredPaths;
|
|
435
|
-
lastDiscoveryAt = clock();
|
|
436
|
-
const found = [];
|
|
437
|
-
const dirs = [{ dir: root, depth: 0 }];
|
|
438
|
-
let visited = 0;
|
|
439
|
-
while (dirs.length && found.length < 64 && visited++ < 100) {
|
|
440
|
-
const { dir, depth } = dirs.shift();
|
|
441
|
-
let entries;
|
|
442
|
-
try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
|
|
443
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
444
|
-
for (const entry of entries) {
|
|
445
|
-
if (entry.isSymbolicLink() || entry.name.startsWith('.')) continue;
|
|
446
|
-
if (entry.isDirectory() && depth < 5 && !SKIP_DIR.has(entry.name)) {
|
|
447
|
-
dirs.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
448
|
-
} else if (entry.isFile() && SOURCE_EXT.test(entry.name) &&
|
|
449
|
-
!/(?:package-lock|pnpm-lock|yarn\.lock)/.test(entry.name)) {
|
|
450
|
-
found.push(path.join(dir, entry.name));
|
|
451
|
-
if (found.length >= 64) break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
discoveredPaths = found;
|
|
456
|
-
return found;
|
|
594
|
+
return platform.discover({ limit: 64 });
|
|
457
595
|
}
|
|
458
596
|
|
|
459
597
|
function canonicalNamedPaths(paths, raw) {
|
|
@@ -555,7 +693,15 @@ export function createPipeline({
|
|
|
555
693
|
if (closed || paused || !deferredWork.size) return;
|
|
556
694
|
const work = [...deferredWork];
|
|
557
695
|
deferredWork.clear();
|
|
558
|
-
const
|
|
696
|
+
const needed = new Set(work.flatMap(([, entry]) => [...entry.artifacts]));
|
|
697
|
+
for (const [sessionId] of work) {
|
|
698
|
+
for (const edge of sessions.get(sessionId)?.graph.edges ?? []) {
|
|
699
|
+
if (edge.sourceRefs.some(ref => needed.has(ref.artifactId))) {
|
|
700
|
+
edge.sourceRefs.forEach(ref => needed.add(ref.artifactId));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const artifacts = await evidence.reconcile({ refs: [...needed].map(artifactId => ({ artifactId })) });
|
|
559
705
|
registerArtifacts(artifacts);
|
|
560
706
|
for (const [sessionId, entry] of work) {
|
|
561
707
|
const session = sessions.get(sessionId);
|
|
@@ -602,7 +748,7 @@ export function createPipeline({
|
|
|
602
748
|
const sourceIdentity = job => job.candidates.some(candidate => candidate.sourceRef?.type === 'message')
|
|
603
749
|
? null : sourceVersions(job.candidates).map(ref => ref.artifactId).sort().join(',');
|
|
604
750
|
const classificationKey = job => `${job.event.sessionId}:${opaque(JSON.stringify([
|
|
605
|
-
policy.version,
|
|
751
|
+
policy.version, job.lineageId,
|
|
606
752
|
{ kind: job.event.kind, toolCategory: job.event.toolCategory,
|
|
607
753
|
outcome: job.event.outcome, incomplete: job.event.incomplete !== false },
|
|
608
754
|
job.candidates.map(candidate => candidate.digest),
|
|
@@ -650,7 +796,7 @@ export function createPipeline({
|
|
|
650
796
|
const session = sessions.get(event.sessionId);
|
|
651
797
|
if (!session) { skip('session_evicted'); return; }
|
|
652
798
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); return; }
|
|
653
|
-
const job = { event, candidates, sourceEventId, session, enqueuedAt: clock(),
|
|
799
|
+
const job = { event, candidates, sourceEventId, session, lineageId, enqueuedAt: clock(),
|
|
654
800
|
needsRefresh: activeClassifications.size >= MAX_CLASSIFICATIONS };
|
|
655
801
|
const covered = coverageReason(job);
|
|
656
802
|
if (covered) { skipJob(job, covered); return; }
|
|
@@ -783,6 +929,7 @@ export function createPipeline({
|
|
|
783
929
|
diagnostics: result?.diagnostics ?? {} });
|
|
784
930
|
await serialized(async () => {
|
|
785
931
|
if (closed || !sessions.has(event.sessionId)) { skip(closed ? 'pipeline_closed' : 'session_evicted'); return; }
|
|
932
|
+
if (job.lineageId !== lineageId) { skip('source_changed_during_classification'); return; }
|
|
786
933
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); notify(); return; }
|
|
787
934
|
if (clock() >= deadlineAt) { classifier = 'timeout'; dropped++; skip('deadline_before_apply'); notify(); return; }
|
|
788
935
|
if (result.status === 'timeout') classifier = 'timeout';
|
|
@@ -796,7 +943,7 @@ export function createPipeline({
|
|
|
796
943
|
bundle?.policyVersion === policy.version && Array.isArray(bundle.candidates)) {
|
|
797
944
|
// Reobserve the worktree before accepting remote answers; a tool could
|
|
798
945
|
// have edited these files while either Jev request was in flight.
|
|
799
|
-
registerArtifacts(await evidence.reconcile());
|
|
946
|
+
registerArtifacts(await evidence.reconcile({ refs: sourceVersions(candidates) }));
|
|
800
947
|
if (closed || sessions.get(event.sessionId) !== session) { skip(closed ? 'pipeline_closed' : 'session_evicted'); return; }
|
|
801
948
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); notify(); return; }
|
|
802
949
|
const artifactRefs = sourceVersions(candidates);
|
|
@@ -825,6 +972,10 @@ export function createPipeline({
|
|
|
825
972
|
const patch = compileDecision(before, { event, decision: result, policy,
|
|
826
973
|
onDiagnostic: entry => admission.push(entry) });
|
|
827
974
|
recordPatch(session, patch);
|
|
975
|
+
// The legacy canvas cap is not a semantic admission cap. Compile
|
|
976
|
+
// this bounded decision independently so the model can retain it.
|
|
977
|
+
const independent = compileDecision(emptyGraph(), { event, decision: result, policy });
|
|
978
|
+
if (independent) platform.observeLegacy(applyPatch(emptyGraph(), independent), { sessionId: session.id, fresh: true });
|
|
828
979
|
remember();
|
|
829
980
|
const after = session.graph;
|
|
830
981
|
const counts = { revisionBefore: before.revision, revisionAfter: after.revision };
|
|
@@ -923,6 +1074,11 @@ export function createPipeline({
|
|
|
923
1074
|
if (dedup.size > MAX_DEDUP) dedup.delete(dedup.keys().next().value);
|
|
924
1075
|
}
|
|
925
1076
|
const session = ensureSession(event.sessionId);
|
|
1077
|
+
session.host = host;
|
|
1078
|
+
session.startedAt ??= event.at;
|
|
1079
|
+
session.status = event.kind === 'session.ended' ? 'ended' : 'active';
|
|
1080
|
+
if (event.kind === 'session.ended') session.endedAt = event.at;
|
|
1081
|
+
syncSessions();
|
|
926
1082
|
if (followSession) selectedSession = session.id;
|
|
927
1083
|
observeMessage(event, prepared.publicText);
|
|
928
1084
|
addActivity(session, event);
|
|
@@ -935,20 +1091,22 @@ export function createPipeline({
|
|
|
935
1091
|
const named = Array.isArray(prepared.paths) ? canonicalNamedPaths(prepared.paths, raw) : [];
|
|
936
1092
|
const discover = event.kind === 'session.started' ||
|
|
937
1093
|
['tool.succeeded', 'tool.failed'].includes(event.kind);
|
|
938
|
-
const
|
|
1094
|
+
const sessionPaths = event.kind === 'session.started'
|
|
1095
|
+
? [...knownArtifacts.values()].filter(item => item.status === 'present').slice(0, 64).map(item => item.path) : [];
|
|
1096
|
+
const paths = [...new Set([...named, ...sessionPaths, ...(discover ? await discoverPaths() : [])])];
|
|
939
1097
|
// EvidenceStore bounds each capture to 32 paths. Process the bounded
|
|
940
1098
|
// discovery list in chunks rather than silently losing its tail.
|
|
941
1099
|
for (let index = 0; index < paths.length; index += 32) {
|
|
942
1100
|
artifacts.push(...await evidence.capture(paths.slice(index, index + 32)));
|
|
943
1101
|
}
|
|
944
|
-
const changed = registerArtifacts(artifacts);
|
|
1102
|
+
const changed = registerArtifacts(artifacts, metadataEvent(event));
|
|
945
1103
|
trace(event, 'capture', { status: 'observed', reason: 'artifacts_observed',
|
|
946
1104
|
artifacts: artifacts.map(artifact => ({ ...artifactMetadata(artifact),
|
|
947
1105
|
reason: changed.some(item => item.id === artifact.id) ? 'artifact_changed' : 'artifact_unchanged' })) });
|
|
948
1106
|
// A tool completion also reconciles prior support, including deletions
|
|
949
1107
|
// omitted from the tool's returned file list.
|
|
950
1108
|
if (event.kind.startsWith('tool.') && event.kind !== 'tool.requested') {
|
|
951
|
-
registerArtifacts(await evidence.reconcile());
|
|
1109
|
+
registerArtifacts(await evidence.reconcile({ limit: 32 }));
|
|
952
1110
|
}
|
|
953
1111
|
namedSet = new Set(named.map(file => path.resolve(root, file)));
|
|
954
1112
|
// Global observation history is not a session's classification
|
|
@@ -998,10 +1156,26 @@ export function createPipeline({
|
|
|
998
1156
|
reconciliationTask = serialized(async () => {
|
|
999
1157
|
if (closed) return;
|
|
1000
1158
|
const expired = expirePending();
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1159
|
+
const beforeModel = platform.model.stats();
|
|
1160
|
+
const paths = await discoverPaths();
|
|
1161
|
+
const artifacts = [];
|
|
1162
|
+
for (let index = 0; index < paths.length; index += 32) {
|
|
1163
|
+
artifacts.push(...await evidence.capture(paths.slice(index, index + 32)));
|
|
1164
|
+
}
|
|
1165
|
+
const known = await evidence.reconcile({ limit: 32 });
|
|
1166
|
+
const observed = [...artifacts, ...known];
|
|
1167
|
+
const changed = registerArtifacts(observed);
|
|
1168
|
+
// Parser revalidation may have already refreshed a file's generation.
|
|
1169
|
+
// Retain canceled classifier work until this dispatch path sees it.
|
|
1170
|
+
for (const artifact of observed) {
|
|
1171
|
+
if (!lineageWork.delete(artifact.id)) continue;
|
|
1172
|
+
if (!changed.some(item => item.id === artifact.id)) changed.push(artifact);
|
|
1173
|
+
}
|
|
1174
|
+
if (!changed.length) {
|
|
1175
|
+
const afterModel = platform.model.stats();
|
|
1176
|
+
if (expired || beforeModel.revision !== afterModel.revision || beforeModel.sequence !== afterModel.sequence) notify();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1005
1179
|
const session = selectedSession ? sessions.get(selectedSession) : null;
|
|
1006
1180
|
if (session) {
|
|
1007
1181
|
const event = observationEvent(session.id);
|
|
@@ -1021,6 +1195,7 @@ export function createPipeline({
|
|
|
1021
1195
|
function setPaused(value) {
|
|
1022
1196
|
const wasPaused = paused;
|
|
1023
1197
|
paused = Boolean(value);
|
|
1198
|
+
if (wasPaused !== paused) architectureEpoch++;
|
|
1024
1199
|
if (!wasPaused && paused) {
|
|
1025
1200
|
for (const job of classificationQueue.splice(0)) {
|
|
1026
1201
|
deferClassification(job.event, job.candidates);
|
|
@@ -1033,10 +1208,30 @@ export function createPipeline({
|
|
|
1033
1208
|
serialized(flushDeferred).catch(() => { dropped++; })
|
|
1034
1209
|
.finally(() => { resumeScheduled = false; });
|
|
1035
1210
|
}
|
|
1211
|
+
architecture.wake();
|
|
1036
1212
|
notify();
|
|
1037
1213
|
return getState();
|
|
1038
1214
|
}
|
|
1039
1215
|
|
|
1216
|
+
function observeLineage(lineage) {
|
|
1217
|
+
return serialized(() => {
|
|
1218
|
+
if (closed || lineage.id === lineageId) return;
|
|
1219
|
+
platform.observeLineage(lineage);
|
|
1220
|
+
lineageId = lineage.id;
|
|
1221
|
+
architectureEpoch++;
|
|
1222
|
+
architecture.invalidate();
|
|
1223
|
+
completedClassifications.clear();
|
|
1224
|
+
for (const job of activeClassifications) job.controller.abort();
|
|
1225
|
+
for (const job of classificationQueue.splice(0)) {
|
|
1226
|
+
pending--;
|
|
1227
|
+
skipJob(job, 'source_changed_during_classification');
|
|
1228
|
+
}
|
|
1229
|
+
for (const artifactId of knownArtifacts.keys()) lineageWork.add(artifactId);
|
|
1230
|
+
registerArtifacts(evidence.setLineage(lineage.id));
|
|
1231
|
+
notify();
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1040
1235
|
function selectSession(id) {
|
|
1041
1236
|
if (!sessions.has(id)) return false;
|
|
1042
1237
|
selectedSession = id;
|
|
@@ -1045,18 +1240,26 @@ export function createPipeline({
|
|
|
1045
1240
|
}
|
|
1046
1241
|
|
|
1047
1242
|
async function whenIdle() {
|
|
1048
|
-
|
|
1049
|
-
while (tasks.size || classificationQueue.length) {
|
|
1050
|
-
pumpClassifications();
|
|
1051
|
-
await Promise.allSettled([...tasks]);
|
|
1243
|
+
do {
|
|
1052
1244
|
await serial;
|
|
1053
|
-
|
|
1054
|
-
|
|
1245
|
+
await platform.whenIdle();
|
|
1246
|
+
await serial;
|
|
1247
|
+
while (tasks.size || classificationQueue.length) {
|
|
1248
|
+
pumpClassifications();
|
|
1249
|
+
await Promise.allSettled([...tasks]);
|
|
1250
|
+
await serial;
|
|
1251
|
+
}
|
|
1252
|
+
await platform.whenIdle();
|
|
1253
|
+
await architecture.whenIdle();
|
|
1254
|
+
await serial;
|
|
1255
|
+
} while (tasks.size || classificationQueue.length || platform.stats().queued || platform.stats().active);
|
|
1055
1256
|
}
|
|
1056
1257
|
|
|
1057
1258
|
async function close() {
|
|
1058
1259
|
if (closed) return;
|
|
1059
1260
|
closed = true;
|
|
1261
|
+
architectureEpoch++;
|
|
1262
|
+
const architectureClosed = architecture.close();
|
|
1060
1263
|
deferredWork.clear();
|
|
1061
1264
|
for (const job of classificationQueue.splice(0)) {
|
|
1062
1265
|
skipJob(job, 'pipeline_closed');
|
|
@@ -1064,8 +1267,17 @@ export function createPipeline({
|
|
|
1064
1267
|
}
|
|
1065
1268
|
for (const job of activeClassifications) job.controller.abort();
|
|
1066
1269
|
decisionService?.close?.();
|
|
1270
|
+
await platform.close();
|
|
1271
|
+
await architectureClosed;
|
|
1067
1272
|
await whenIdle();
|
|
1068
1273
|
}
|
|
1069
1274
|
|
|
1070
|
-
return {
|
|
1275
|
+
return {
|
|
1276
|
+
ingest, getState, reconcile, observeLineage, setPaused, selectSession, whenIdle, close,
|
|
1277
|
+
getArchitectureStatus: () => architecture.status(),
|
|
1278
|
+
discoverArchitecture: () => architecture.request(),
|
|
1279
|
+
getModelState: options => platform.snapshot(options),
|
|
1280
|
+
createCheckpoint: options => platform.checkpoint(options),
|
|
1281
|
+
model: platform.model,
|
|
1282
|
+
};
|
|
1071
1283
|
}
|