graphlin 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +465 -0
- package/docs/visualizer-views.md +199 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +74 -9
- package/plugin.json +4 -2
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +70 -12
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +889 -0
- package/runtime/model/records.mjs +239 -0
- package/runtime/pipeline.mjs +127 -48
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +52 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +225 -63
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +36 -1
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +337 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +49 -0
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { opaque, plain, integer } from '../core/common.mjs';
|
|
2
|
+
import { excluded, safeText } from '../core/privacy.mjs';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
5
|
+
entities: 20_000, relations: 40_000, artifacts: 10_000, interpretations: 512,
|
|
6
|
+
imports: 20_000, scopes: 1_024, activity: 2_048, sessions: 64, checkpoints: 8,
|
|
7
|
+
bytes: 48 * 1024 * 1024, softBytes: 32 * 1024 * 1024,
|
|
8
|
+
checkpointBytes: 32 * 1024 * 1024, refs: 16,
|
|
9
|
+
});
|
|
10
|
+
export const id = value => typeof value === 'string' &&
|
|
11
|
+
/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(value) && safeText(value, 160) ? value : null;
|
|
12
|
+
export const token = (value, fallback = 'unknown') => typeof value === 'string' &&
|
|
13
|
+
/^[a-z][a-z0-9_.-]{0,63}$/.test(value) && safeText(value, 64) ? value : fallback;
|
|
14
|
+
export const label = (value, fallback = 'Unknown') => typeof value === 'string' &&
|
|
15
|
+
safeText(value, 240) && !/[\r\n<>]/.test(value) && !/^(?:\/|[a-z]+:)/i.test(value) ? value : fallback;
|
|
16
|
+
export const validity = value => ['current', 'stale', 'retracted'].includes(value) ? value : 'stale';
|
|
17
|
+
export const classification = value => ['accepted', 'tentative', 'unknown', 'stale', 'pending', 'abstained'].includes(value)
|
|
18
|
+
? value : 'unknown';
|
|
19
|
+
export const basis = value => ['metadata', 'parsed', 'lexical', 'decision', 'legacy'].includes(value) ? value : 'legacy';
|
|
20
|
+
export const byteSize = value => Buffer.byteLength(JSON.stringify(value));
|
|
21
|
+
export const key = (prefix, ...parts) => opaque(prefix, ...parts);
|
|
22
|
+
const version = value => typeof value === 'string' && value.length <= 256 &&
|
|
23
|
+
/^[@A-Za-z0-9][A-Za-z0-9_.@+/-]*$/.test(value) && safeText(value, 256) ? value : null;
|
|
24
|
+
|
|
25
|
+
export function relativePath(value, policy = {}) {
|
|
26
|
+
if (typeof value !== 'string' || value.length > 512 || !value ||
|
|
27
|
+
/[\\:\0\r\n<>%]/.test(value) || value.split('/').some(p => !p || p === '.' || p === '..') ||
|
|
28
|
+
!safeText(value, 512) || excluded(value, policy)) return null;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function currentPolicy(provider) {
|
|
33
|
+
const value = (typeof provider === 'function' ? provider() : provider) ?? {};
|
|
34
|
+
return {
|
|
35
|
+
readSource: value.readSource === true || value.transmitSource === true,
|
|
36
|
+
displayEvidence: value.displayEvidence !== false,
|
|
37
|
+
persistEvidence: value.persistEvidence === true,
|
|
38
|
+
excludePaths: Array.isArray(value.excludePaths) ? value.excludePaths : [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function lineageRecord(value) {
|
|
43
|
+
if (!plain(value) || !id(value.id)) return null;
|
|
44
|
+
const branch = label(value.branch, '');
|
|
45
|
+
return {
|
|
46
|
+
id: value.id, status: token(value.status),
|
|
47
|
+
...(branch ? { branch } : {}),
|
|
48
|
+
...(typeof value.head === 'string' && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.head) ? { head: value.head } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function references(values, max = DEFAULT_LIMITS.refs) {
|
|
53
|
+
if (!Array.isArray(values) || values.length > max) return null;
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
if (!plain(value) || !id(value.artifactId) || !integer(value.generation, 1) ||
|
|
57
|
+
typeof value.hash !== 'string' || !/^[a-f0-9]{64}$/.test(value.hash)) return null;
|
|
58
|
+
const ref = { artifactId: value.artifactId, hash: value.hash, generation: value.generation };
|
|
59
|
+
if (integer(value.startLine, 1, 10_000_000) && integer(value.endLine, value.startLine, 10_000_000)) {
|
|
60
|
+
ref.startLine = value.startLine;
|
|
61
|
+
ref.endLine = value.endLine;
|
|
62
|
+
}
|
|
63
|
+
if (id(value.eventId)) ref.eventId = value.eventId;
|
|
64
|
+
if (id(value.extractor)) ref.extractor = value.extractor;
|
|
65
|
+
if (version(value.extractorVersion)) ref.extractorVersion = value.extractorVersion;
|
|
66
|
+
if (version(value.identityVersion)) ref.identityVersion = value.identityVersion;
|
|
67
|
+
// Intent references cannot be promoted to deterministic source support.
|
|
68
|
+
if (value.sourceClass === 'public_intent') ref.sourceClass = 'public_intent';
|
|
69
|
+
result.push(ref);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function entityRecord(value, maxRefs) {
|
|
75
|
+
if (!plain(value) || !id(value.id)) return null;
|
|
76
|
+
const sourceRefs = references(value.sourceRefs ?? [], maxRefs);
|
|
77
|
+
if (!sourceRefs) return null;
|
|
78
|
+
const entity = {
|
|
79
|
+
id: value.id, label: label(value.label), kind: token(value.kind),
|
|
80
|
+
parentId: id(value.parentId), sourceRefs, basis: basis(value.basis),
|
|
81
|
+
validity: validity(value.validity), classification: classification(value.classification),
|
|
82
|
+
};
|
|
83
|
+
if (id(value.artifactId)) entity.artifactId = value.artifactId;
|
|
84
|
+
if (value.qualifiedName) entity.qualifiedName = label(value.qualifiedName);
|
|
85
|
+
if (id(value.sessionId)) entity.sessionId = value.sessionId;
|
|
86
|
+
if (id(value.legacyId)) entity.legacyId = value.legacyId;
|
|
87
|
+
if (relativePath(value.relativePath)) entity.relativePath = value.relativePath;
|
|
88
|
+
if (integer(value.knownAtSequence)) entity.knownAtSequence = value.knownAtSequence;
|
|
89
|
+
if (integer(value.createdAtSequence, 1)) entity.createdAtSequence = value.createdAtSequence;
|
|
90
|
+
if (value.ownership === 'unresolved') entity.ownership = 'unresolved';
|
|
91
|
+
return entity;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function relationRecord(value, maxRefs) {
|
|
95
|
+
if (!plain(value) || !id(value.id) || !id(value.source) || !id(value.target)) return null;
|
|
96
|
+
const sourceRefs = references(value.sourceRefs ?? [], maxRefs);
|
|
97
|
+
if (!sourceRefs) return null;
|
|
98
|
+
return {
|
|
99
|
+
id: value.id, source: value.source, target: value.target, kind: token(value.kind),
|
|
100
|
+
basis: basis(value.basis), validity: validity(value.validity), sourceRefs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function interpretationRecord(value, maxRefs) {
|
|
105
|
+
if (!plain(value) || !id(value.id) || typeof value.namespace !== 'string' ||
|
|
106
|
+
!/^[a-z][a-z0-9_-]*(?:[.:][a-z0-9_-]+)+$/.test(value.namespace) || !safeText(value.namespace, 80)) return null;
|
|
107
|
+
const sourceRefs = references(value.sourceRefs ?? [], maxRefs);
|
|
108
|
+
if (!sourceRefs || !Array.isArray(value.entityIds) || value.entityIds.length > 256 ||
|
|
109
|
+
value.entityIds.some(value => !id(value))) return null;
|
|
110
|
+
return {
|
|
111
|
+
id: value.id, namespace: value.namespace, kind: token(value.kind),
|
|
112
|
+
label: label(value.label), entityIds: [...new Set(value.entityIds)],
|
|
113
|
+
basis: 'decision', validity: validity(value.validity),
|
|
114
|
+
classification: classification(value.classification),
|
|
115
|
+
support: ['supported', 'tentative', 'unknown', 'contradicted'].includes(value.support) ? value.support : 'unknown',
|
|
116
|
+
sourceRefs, version: id(String(value.version ?? '1')) ?? '1',
|
|
117
|
+
...(id(value.sessionId) ? { sessionId: value.sessionId } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function certificate(value) {
|
|
122
|
+
if (!plain(value) || !id(value.artifactId) || !id(value.scopeId) ||
|
|
123
|
+
!integer(value.generation, 1) || !/^[a-f0-9]{64}$/.test(value.hash)) return null;
|
|
124
|
+
const ranges = Array.isArray(value.coveredRanges) ? value.coveredRanges.slice(0, 128).flatMap(range => {
|
|
125
|
+
const startLine = Array.isArray(range) ? range[0] : range?.startLine;
|
|
126
|
+
const endLine = Array.isArray(range) ? range[1] : range?.endLine;
|
|
127
|
+
return integer(startLine, 1, 10_000_000) && integer(endLine, startLine, 10_000_000) ? [{ startLine, endLine }] : [];
|
|
128
|
+
}) : [];
|
|
129
|
+
const omissions = Array.isArray(value.omissions) ? value.omissions.slice(0, 32).map(v => token(v, 'unspecified')) : ['unspecified'];
|
|
130
|
+
const validVersions = version(String(value.version ?? '')) && version(String(value.identityVersion ?? '')) && id(value.extractor);
|
|
131
|
+
return {
|
|
132
|
+
artifactId: value.artifactId, scopeId: value.scopeId, hash: value.hash, generation: value.generation,
|
|
133
|
+
complete: value.complete === true && omissions.length === 0 && !!validVersions && ranges.length > 0 &&
|
|
134
|
+
value.coveredRanges.length === ranges.length && value.capability === 'parsed',
|
|
135
|
+
extractor: id(value.extractor) ?? 'unknown', version: version(String(value.version ?? '')) ?? 'unknown',
|
|
136
|
+
identityVersion: version(String(value.identityVersion ?? '')) ?? 'unknown',
|
|
137
|
+
coveredRanges: ranges, omissions, capability: token(value.capability),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function activityRecord(value, sequence, now) {
|
|
142
|
+
if (!plain(value)) return null;
|
|
143
|
+
const at = time(value.at ?? value.timestamp, now);
|
|
144
|
+
const record = {
|
|
145
|
+
id: id(value.id) ?? key('activity', sequence, at),
|
|
146
|
+
kind: token(value.kind, 'capture.gap'), sequence, knownAtSequence: sequence,
|
|
147
|
+
at, recordedAt: time(now, now),
|
|
148
|
+
outcome: ['pending', 'succeeded', 'failed', 'interrupted', 'denied', 'unresolved', 'observed'].includes(value.outcome)
|
|
149
|
+
? value.outcome : 'unresolved',
|
|
150
|
+
toolCategory: token(value.toolCategory, 'other'),
|
|
151
|
+
attribution: ['observed', 'correlated', 'unknown'].includes(value.attribution) ? value.attribution : 'unknown',
|
|
152
|
+
};
|
|
153
|
+
for (const field of ['sessionId', 'agentId', 'toolCallId']) if (id(value[field])) record[field] = value[field];
|
|
154
|
+
for (const field of ['entityIds', 'artifactIds']) {
|
|
155
|
+
record[field] = Array.isArray(value[field]) ? [...new Set(value[field].slice(0, 256).filter(id))] : [];
|
|
156
|
+
}
|
|
157
|
+
const sourceRefs = references(value.sourceRefs ?? []);
|
|
158
|
+
record.sourceRefs = sourceRefs ?? [];
|
|
159
|
+
// Creation is accepted only as an explicit successful observation with source
|
|
160
|
+
// version correlation. A write attempt or a new file in inventory is not one.
|
|
161
|
+
record.creation = value.creation === true && record.outcome === 'succeeded' &&
|
|
162
|
+
record.kind !== 'tool.requested' && record.sourceRefs.length > 0 &&
|
|
163
|
+
record.sourceRefs.every(ref => ref.sourceClass !== 'public_intent');
|
|
164
|
+
return record;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function time(value, fallback = 0) {
|
|
168
|
+
const number = typeof value === 'string' ? Date.parse(value) : value;
|
|
169
|
+
const safe = Number.isFinite(number) && number >= 0 && number <= 8_640_000_000_000_000 ? number : fallback;
|
|
170
|
+
return new Date(Number.isFinite(safe) ? safe : 0).toISOString();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Projection is an allowlist, including for restored state and old checkpoints. */
|
|
174
|
+
export function projectSnapshot(state, policy, { persistent = false, sessionId, scopeId } = {}) {
|
|
175
|
+
const sourceLabels = policy.readSource && policy.displayEvidence && (!persistent || policy.persistEvidence);
|
|
176
|
+
const blockedArtifacts = new Set((state.coverage.artifacts ?? []).filter(value =>
|
|
177
|
+
value.relativePath && !relativePath(value.relativePath, policy)).map(value => value.id));
|
|
178
|
+
let entities = state.entities.map(value => entityRecord(value)).filter(Boolean).map(entity => {
|
|
179
|
+
const path = relativePath(entity.relativePath, policy);
|
|
180
|
+
const metadata = path && (entity.basis === 'metadata' || ['file', 'module'].includes(entity.kind));
|
|
181
|
+
if (metadata) entity.label = path.split('/').at(-1);
|
|
182
|
+
if (!sourceLabels || blockedArtifacts.has(entity.artifactId) || entity.relativePath && !path) {
|
|
183
|
+
if (!metadata) {
|
|
184
|
+
entity.label = entity.kind === 'project' ? 'Project' : token(entity.kind, 'entity');
|
|
185
|
+
}
|
|
186
|
+
delete entity.qualifiedName;
|
|
187
|
+
}
|
|
188
|
+
if (entity.basis === 'metadata' && !path && entity.kind !== 'project') entity.label = entity.kind;
|
|
189
|
+
if (!path) delete entity.relativePath;
|
|
190
|
+
return entity;
|
|
191
|
+
});
|
|
192
|
+
if (scopeId) {
|
|
193
|
+
const children = new Map();
|
|
194
|
+
for (const entity of entities) {
|
|
195
|
+
if (!children.has(entity.parentId)) children.set(entity.parentId, []);
|
|
196
|
+
children.get(entity.parentId).push(entity.id);
|
|
197
|
+
}
|
|
198
|
+
const selected = new Set(), pending = [scopeId];
|
|
199
|
+
while (pending.length) {
|
|
200
|
+
const next = pending.pop();
|
|
201
|
+
if (selected.has(next)) continue;
|
|
202
|
+
selected.add(next);
|
|
203
|
+
pending.push(...(children.get(next) ?? []));
|
|
204
|
+
}
|
|
205
|
+
const byId = new Map(entities.map(entity => [entity.id, entity]));
|
|
206
|
+
let parent = byId.get(scopeId)?.parentId;
|
|
207
|
+
while (parent && !selected.has(parent)) { selected.add(parent); parent = byId.get(parent)?.parentId; }
|
|
208
|
+
entities = entities.filter(entity => selected.has(entity.id));
|
|
209
|
+
}
|
|
210
|
+
const entityIds = new Set(entities.map(entity => entity.id));
|
|
211
|
+
const relations = state.relations.map(value => relationRecord(value)).filter(value =>
|
|
212
|
+
value && entityIds.has(value.source) && entityIds.has(value.target));
|
|
213
|
+
const interpretations = state.interpretations.map(value => interpretationRecord(value)).filter(value =>
|
|
214
|
+
value && value.entityIds.every(entityId => entityIds.has(entityId))).map(value =>
|
|
215
|
+
sourceLabels && !value.sourceRefs.some(ref => blockedArtifacts.has(ref.artifactId)) ? value : { ...value, label: 'Interpretation' });
|
|
216
|
+
const activity = state.activity.filter(value => !sessionId || value.sessionId === sessionId).map(value => {
|
|
217
|
+
const record = activityRecord(value, value.sequence, Date.parse(value.recordedAt));
|
|
218
|
+
record.entityIds = record.entityIds.filter(value => entityIds.has(value));
|
|
219
|
+
return record;
|
|
220
|
+
});
|
|
221
|
+
const coverage = structuredClone(state.coverage);
|
|
222
|
+
coverage.scopes = (coverage.scopes ?? []).map(scope => {
|
|
223
|
+
const path = relativePath(scope.relativePath, policy);
|
|
224
|
+
return { ...scope, label: path ?? 'Scope', ...(path ? { relativePath: path } : { relativePath: undefined }) };
|
|
225
|
+
});
|
|
226
|
+
coverage.files = (coverage.files ?? []).map(file => {
|
|
227
|
+
const path = relativePath(file.relativePath, policy);
|
|
228
|
+
return { ...file, relativePath: path ?? undefined, root: relativePath(file.root, policy) ?? undefined };
|
|
229
|
+
});
|
|
230
|
+
coverage.artifacts = (coverage.artifacts ?? []).map(artifact => ({
|
|
231
|
+
...artifact, relativePath: relativePath(artifact.relativePath, policy) ?? undefined,
|
|
232
|
+
}));
|
|
233
|
+
return {
|
|
234
|
+
schemaVersion: 2, projectId: state.projectId, revision: state.revision, sequence: state.sequence,
|
|
235
|
+
entities, relations, interpretations, activity, coverage,
|
|
236
|
+
sessions: state.sessions.filter(value => !sessionId || value.id === sessionId).map(value => ({ ...value })),
|
|
237
|
+
checkpoints: state.checkpoints.map(value => ({ ...value, label: sourceLabels ? label(value.label, 'Checkpoint') : 'Checkpoint' })),
|
|
238
|
+
};
|
|
239
|
+
}
|
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,7 @@ 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
11
|
|
|
12
12
|
const MAX_ACTIVITY = 200;
|
|
13
13
|
const MAX_HOOK_EVENTS = 200;
|
|
@@ -23,11 +23,6 @@ const CLASSIFICATION_QUEUE_TTL_MS = 120_000;
|
|
|
23
23
|
const DEADLINE_MS = 2000;
|
|
24
24
|
const PENDING_LEASE_MS = 60_000;
|
|
25
25
|
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
26
|
const FIXED_CLASSIFIER = new Set([
|
|
32
27
|
'ready', 'metadata_only', 'missing_key', 'paused', 'unavailable', 'timeout', 'demo',
|
|
33
28
|
]);
|
|
@@ -97,7 +92,7 @@ function restoreGraph(input, { stale = true } = {}) {
|
|
|
97
92
|
*/
|
|
98
93
|
export function createPipeline({
|
|
99
94
|
projectRoot, policy: policyOptions, decisionService, onChange = () => {},
|
|
100
|
-
onDiagnostic = () => {}, restoredState, mode = 'live', clock = Date.now,
|
|
95
|
+
onDiagnostic = () => {}, restoredState, restoredModel, mode = 'live', clock = Date.now,
|
|
101
96
|
classificationDeadlineMs = DEADLINE_MS,
|
|
102
97
|
} = {}) {
|
|
103
98
|
if (!Number.isSafeInteger(classificationDeadlineMs) ||
|
|
@@ -107,13 +102,37 @@ export function createPipeline({
|
|
|
107
102
|
const root = realpathSync(projectRoot);
|
|
108
103
|
const inputRoot = path.resolve(projectRoot);
|
|
109
104
|
const projectId = opaque(root);
|
|
105
|
+
const modelProjectId = createHash('sha256').update(root).digest('hex');
|
|
110
106
|
const policy = createPolicy(policyOptions ?? {});
|
|
111
|
-
|
|
107
|
+
// Restored claims stay stale until recaptured, and their versions must never
|
|
108
|
+
// collide with a new daemon's counters. No saved content is trusted here.
|
|
109
|
+
let generationFloor = 0;
|
|
110
|
+
const retainGeneration = value => {
|
|
111
|
+
if (Number.isSafeInteger(value) && value > generationFloor && value < Number.MAX_SAFE_INTEGER) generationFloor = value;
|
|
112
|
+
};
|
|
113
|
+
const retainRefs = rows => {
|
|
114
|
+
for (const row of Array.isArray(rows) ? rows.slice(0, 40000) : []) {
|
|
115
|
+
for (const ref of Array.isArray(row?.sourceRefs) ? row.sourceRefs.slice(0, 16) : []) retainGeneration(ref.generation);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
if (restoredModel?.projectId === modelProjectId) {
|
|
119
|
+
const artifacts = restoredModel.coverage?.artifacts;
|
|
120
|
+
for (const artifact of Array.isArray(artifacts) ? artifacts.slice(0, 10000) : []) retainGeneration(artifact?.generation);
|
|
121
|
+
for (const rows of [restoredModel.entities, restoredModel.relations, restoredModel.interpretations]) retainRefs(rows);
|
|
122
|
+
}
|
|
123
|
+
if (restoredState?.projectId === projectId) {
|
|
124
|
+
const states = Array.isArray(restoredState.sessionStates) ? restoredState.sessionStates.slice(0, MAX_SESSIONS) : [];
|
|
125
|
+
for (const session of [restoredState, ...states]) {
|
|
126
|
+
retainRefs(session?.graph?.nodes); retainRefs(session?.graph?.edges);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const evidence = new EvidenceStore({ projectRoot, policy, maxTrackedPaths: 10000, generationFloor });
|
|
112
130
|
const sessions = new Map();
|
|
113
131
|
const dedup = new Map();
|
|
114
132
|
const sessionStarts = new Map();
|
|
115
133
|
const hookEvents = [];
|
|
116
134
|
const knownArtifacts = new Map();
|
|
135
|
+
const lineageWork = new Set();
|
|
117
136
|
const messageVersions = new Map();
|
|
118
137
|
const deferredWork = new Map();
|
|
119
138
|
const classificationQueue = [];
|
|
@@ -130,16 +149,26 @@ export function createPipeline({
|
|
|
130
149
|
let pending = 0;
|
|
131
150
|
let classifier = mode === 'demo' ? 'demo' : policy.transmitSource ? 'ready' : 'metadata_only';
|
|
132
151
|
let serial = Promise.resolve();
|
|
133
|
-
let lastDiscoveryAt = -Infinity;
|
|
134
|
-
let discoveredPaths = [];
|
|
135
152
|
let reconciliationTask = null;
|
|
136
153
|
let resumeScheduled = false;
|
|
154
|
+
let lineageId = null;
|
|
137
155
|
|
|
138
156
|
const serialized = (fn) => {
|
|
139
157
|
const operation = serial.then(fn);
|
|
140
158
|
serial = operation.catch(() => {});
|
|
141
159
|
return operation;
|
|
142
160
|
};
|
|
161
|
+
const platform = createPlatform({
|
|
162
|
+
projectRoot: root, projectId: modelProjectId,
|
|
163
|
+
policy, restoredState: restoredModel, now: clock,
|
|
164
|
+
accept: operation => serialized(operation),
|
|
165
|
+
revalidate: refs => serialized(async () => {
|
|
166
|
+
if (closed) return false;
|
|
167
|
+
registerArtifacts(await evidence.reconcile({ refs }));
|
|
168
|
+
return evidence.isCurrent(refs);
|
|
169
|
+
}),
|
|
170
|
+
onChange: notify,
|
|
171
|
+
});
|
|
143
172
|
|
|
144
173
|
function artifactMetadata(artifact) {
|
|
145
174
|
const relative = artifact.relativePath;
|
|
@@ -217,17 +246,24 @@ export function createPipeline({
|
|
|
217
246
|
const session = freshSession(id, `Session ${sessions.size + 1}`);
|
|
218
247
|
sessions.set(id, session);
|
|
219
248
|
selectedSession ??= id;
|
|
249
|
+
platform.setSessions([...sessions.values()].map(item => ({ id: item.id, label: item.label })));
|
|
220
250
|
return session;
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
function evictSession(id) {
|
|
224
254
|
sessions.delete(id);
|
|
255
|
+
syncSessions();
|
|
225
256
|
deferredWork.delete(id);
|
|
226
257
|
for (const key of completedClassifications.keys()) {
|
|
227
258
|
if (key.startsWith(`${id}:`)) completedClassifications.delete(key);
|
|
228
259
|
}
|
|
229
260
|
}
|
|
230
261
|
|
|
262
|
+
function syncSessions() {
|
|
263
|
+
platform.setSessions([...sessions.values()].map(({ id, label, host, status, startedAt, endedAt }) =>
|
|
264
|
+
({ id, label, host, status, startedAt, endedAt })));
|
|
265
|
+
}
|
|
266
|
+
|
|
231
267
|
if (restoredState?.projectId === projectId) {
|
|
232
268
|
const saved = Array.isArray(restoredState.sessionStates)
|
|
233
269
|
? [restoredState, ...restoredState.sessionStates.filter(s => s.id !== restoredState.sessionId)].slice(-MAX_SESSIONS)
|
|
@@ -237,6 +273,7 @@ export function createPipeline({
|
|
|
237
273
|
if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(id)) continue;
|
|
238
274
|
const session = ensureSession(id);
|
|
239
275
|
session.graph = restoreGraph(entry.graph);
|
|
276
|
+
platform.observeLegacy(session.graph, { sessionId: session.id });
|
|
240
277
|
// Activity is reconstructed through the metadata allowlist, never trusted verbatim.
|
|
241
278
|
session.activity = (Array.isArray(entry.activity) ? entry.activity : [])
|
|
242
279
|
.slice(-MAX_ACTIVITY).map(item => {
|
|
@@ -329,6 +366,7 @@ export function createPipeline({
|
|
|
329
366
|
function recordPatch(session, patch) {
|
|
330
367
|
if (!patch || !patch.operations?.length) return;
|
|
331
368
|
session.graph = applyPatch(session.graph, patch);
|
|
369
|
+
platform.observeLegacy(session.graph, { sessionId: session.id, fresh: true });
|
|
332
370
|
session.history.push({
|
|
333
371
|
revision: session.graph.revision, at: new Date(clock()).toISOString(),
|
|
334
372
|
graph: structuredClone(session.graph),
|
|
@@ -356,7 +394,7 @@ export function createPipeline({
|
|
|
356
394
|
}
|
|
357
395
|
}
|
|
358
396
|
|
|
359
|
-
function registerArtifacts(artifacts) {
|
|
397
|
+
function registerArtifacts(artifacts, event) {
|
|
360
398
|
const changed = [];
|
|
361
399
|
for (const artifact of artifacts) {
|
|
362
400
|
const previous = knownArtifacts.get(artifact.id);
|
|
@@ -390,12 +428,14 @@ export function createPipeline({
|
|
|
390
428
|
recordPatch(session, invalidateArtifacts(session.graph, changed));
|
|
391
429
|
}
|
|
392
430
|
}
|
|
431
|
+
platform.observeArtifacts(artifacts, event);
|
|
393
432
|
return changed;
|
|
394
433
|
}
|
|
395
434
|
|
|
396
435
|
function addActivity(session, event) {
|
|
397
436
|
const state = activityState(event);
|
|
398
437
|
const row = { ...metadataEvent(event), label: activityLabel(event), state };
|
|
438
|
+
platform.recordActivity(row);
|
|
399
439
|
const existing = event.toolCallId
|
|
400
440
|
? session.activity.findIndex(item => item.toolCallId === event.toolCallId &&
|
|
401
441
|
item.agentId === event.agentId && item.kind?.startsWith('tool.'))
|
|
@@ -422,6 +462,7 @@ export function createPipeline({
|
|
|
422
462
|
sequence: ++sequence, incomplete: true,
|
|
423
463
|
});
|
|
424
464
|
session.activity[index] = { ...event, label: activityLabel(event), state: 'unresolved' };
|
|
465
|
+
platform.recordActivity(event);
|
|
425
466
|
changed = true;
|
|
426
467
|
}
|
|
427
468
|
}
|
|
@@ -429,31 +470,7 @@ export function createPipeline({
|
|
|
429
470
|
}
|
|
430
471
|
|
|
431
472
|
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;
|
|
473
|
+
return platform.discover({ limit: 64 });
|
|
457
474
|
}
|
|
458
475
|
|
|
459
476
|
function canonicalNamedPaths(paths, raw) {
|
|
@@ -555,7 +572,15 @@ export function createPipeline({
|
|
|
555
572
|
if (closed || paused || !deferredWork.size) return;
|
|
556
573
|
const work = [...deferredWork];
|
|
557
574
|
deferredWork.clear();
|
|
558
|
-
const
|
|
575
|
+
const needed = new Set(work.flatMap(([, entry]) => [...entry.artifacts]));
|
|
576
|
+
for (const [sessionId] of work) {
|
|
577
|
+
for (const edge of sessions.get(sessionId)?.graph.edges ?? []) {
|
|
578
|
+
if (edge.sourceRefs.some(ref => needed.has(ref.artifactId))) {
|
|
579
|
+
edge.sourceRefs.forEach(ref => needed.add(ref.artifactId));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const artifacts = await evidence.reconcile({ refs: [...needed].map(artifactId => ({ artifactId })) });
|
|
559
584
|
registerArtifacts(artifacts);
|
|
560
585
|
for (const [sessionId, entry] of work) {
|
|
561
586
|
const session = sessions.get(sessionId);
|
|
@@ -602,7 +627,7 @@ export function createPipeline({
|
|
|
602
627
|
const sourceIdentity = job => job.candidates.some(candidate => candidate.sourceRef?.type === 'message')
|
|
603
628
|
? null : sourceVersions(job.candidates).map(ref => ref.artifactId).sort().join(',');
|
|
604
629
|
const classificationKey = job => `${job.event.sessionId}:${opaque(JSON.stringify([
|
|
605
|
-
policy.version,
|
|
630
|
+
policy.version, job.lineageId,
|
|
606
631
|
{ kind: job.event.kind, toolCategory: job.event.toolCategory,
|
|
607
632
|
outcome: job.event.outcome, incomplete: job.event.incomplete !== false },
|
|
608
633
|
job.candidates.map(candidate => candidate.digest),
|
|
@@ -650,7 +675,7 @@ export function createPipeline({
|
|
|
650
675
|
const session = sessions.get(event.sessionId);
|
|
651
676
|
if (!session) { skip('session_evicted'); return; }
|
|
652
677
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); return; }
|
|
653
|
-
const job = { event, candidates, sourceEventId, session, enqueuedAt: clock(),
|
|
678
|
+
const job = { event, candidates, sourceEventId, session, lineageId, enqueuedAt: clock(),
|
|
654
679
|
needsRefresh: activeClassifications.size >= MAX_CLASSIFICATIONS };
|
|
655
680
|
const covered = coverageReason(job);
|
|
656
681
|
if (covered) { skipJob(job, covered); return; }
|
|
@@ -783,6 +808,7 @@ export function createPipeline({
|
|
|
783
808
|
diagnostics: result?.diagnostics ?? {} });
|
|
784
809
|
await serialized(async () => {
|
|
785
810
|
if (closed || !sessions.has(event.sessionId)) { skip(closed ? 'pipeline_closed' : 'session_evicted'); return; }
|
|
811
|
+
if (job.lineageId !== lineageId) { skip('source_changed_during_classification'); return; }
|
|
786
812
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); notify(); return; }
|
|
787
813
|
if (clock() >= deadlineAt) { classifier = 'timeout'; dropped++; skip('deadline_before_apply'); notify(); return; }
|
|
788
814
|
if (result.status === 'timeout') classifier = 'timeout';
|
|
@@ -796,7 +822,7 @@ export function createPipeline({
|
|
|
796
822
|
bundle?.policyVersion === policy.version && Array.isArray(bundle.candidates)) {
|
|
797
823
|
// Reobserve the worktree before accepting remote answers; a tool could
|
|
798
824
|
// have edited these files while either Jev request was in flight.
|
|
799
|
-
registerArtifacts(await evidence.reconcile());
|
|
825
|
+
registerArtifacts(await evidence.reconcile({ refs: sourceVersions(candidates) }));
|
|
800
826
|
if (closed || sessions.get(event.sessionId) !== session) { skip(closed ? 'pipeline_closed' : 'session_evicted'); return; }
|
|
801
827
|
if (paused) { deferClassification(event, candidates); skip('paused_deferred'); notify(); return; }
|
|
802
828
|
const artifactRefs = sourceVersions(candidates);
|
|
@@ -825,6 +851,10 @@ export function createPipeline({
|
|
|
825
851
|
const patch = compileDecision(before, { event, decision: result, policy,
|
|
826
852
|
onDiagnostic: entry => admission.push(entry) });
|
|
827
853
|
recordPatch(session, patch);
|
|
854
|
+
// The legacy canvas cap is not a semantic admission cap. Compile
|
|
855
|
+
// this bounded decision independently so the model can retain it.
|
|
856
|
+
const independent = compileDecision(emptyGraph(), { event, decision: result, policy });
|
|
857
|
+
if (independent) platform.observeLegacy(applyPatch(emptyGraph(), independent), { sessionId: session.id, fresh: true });
|
|
828
858
|
remember();
|
|
829
859
|
const after = session.graph;
|
|
830
860
|
const counts = { revisionBefore: before.revision, revisionAfter: after.revision };
|
|
@@ -923,6 +953,11 @@ export function createPipeline({
|
|
|
923
953
|
if (dedup.size > MAX_DEDUP) dedup.delete(dedup.keys().next().value);
|
|
924
954
|
}
|
|
925
955
|
const session = ensureSession(event.sessionId);
|
|
956
|
+
session.host = host;
|
|
957
|
+
session.startedAt ??= event.at;
|
|
958
|
+
session.status = event.kind === 'session.ended' ? 'ended' : 'active';
|
|
959
|
+
if (event.kind === 'session.ended') session.endedAt = event.at;
|
|
960
|
+
syncSessions();
|
|
926
961
|
if (followSession) selectedSession = session.id;
|
|
927
962
|
observeMessage(event, prepared.publicText);
|
|
928
963
|
addActivity(session, event);
|
|
@@ -935,20 +970,22 @@ export function createPipeline({
|
|
|
935
970
|
const named = Array.isArray(prepared.paths) ? canonicalNamedPaths(prepared.paths, raw) : [];
|
|
936
971
|
const discover = event.kind === 'session.started' ||
|
|
937
972
|
['tool.succeeded', 'tool.failed'].includes(event.kind);
|
|
938
|
-
const
|
|
973
|
+
const sessionPaths = event.kind === 'session.started'
|
|
974
|
+
? [...knownArtifacts.values()].filter(item => item.status === 'present').slice(0, 64).map(item => item.path) : [];
|
|
975
|
+
const paths = [...new Set([...named, ...sessionPaths, ...(discover ? await discoverPaths() : [])])];
|
|
939
976
|
// EvidenceStore bounds each capture to 32 paths. Process the bounded
|
|
940
977
|
// discovery list in chunks rather than silently losing its tail.
|
|
941
978
|
for (let index = 0; index < paths.length; index += 32) {
|
|
942
979
|
artifacts.push(...await evidence.capture(paths.slice(index, index + 32)));
|
|
943
980
|
}
|
|
944
|
-
const changed = registerArtifacts(artifacts);
|
|
981
|
+
const changed = registerArtifacts(artifacts, metadataEvent(event));
|
|
945
982
|
trace(event, 'capture', { status: 'observed', reason: 'artifacts_observed',
|
|
946
983
|
artifacts: artifacts.map(artifact => ({ ...artifactMetadata(artifact),
|
|
947
984
|
reason: changed.some(item => item.id === artifact.id) ? 'artifact_changed' : 'artifact_unchanged' })) });
|
|
948
985
|
// A tool completion also reconciles prior support, including deletions
|
|
949
986
|
// omitted from the tool's returned file list.
|
|
950
987
|
if (event.kind.startsWith('tool.') && event.kind !== 'tool.requested') {
|
|
951
|
-
registerArtifacts(await evidence.reconcile());
|
|
988
|
+
registerArtifacts(await evidence.reconcile({ limit: 32 }));
|
|
952
989
|
}
|
|
953
990
|
namedSet = new Set(named.map(file => path.resolve(root, file)));
|
|
954
991
|
// Global observation history is not a session's classification
|
|
@@ -998,10 +1035,26 @@ export function createPipeline({
|
|
|
998
1035
|
reconciliationTask = serialized(async () => {
|
|
999
1036
|
if (closed) return;
|
|
1000
1037
|
const expired = expirePending();
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1038
|
+
const beforeModel = platform.model.stats();
|
|
1039
|
+
const paths = await discoverPaths();
|
|
1040
|
+
const artifacts = [];
|
|
1041
|
+
for (let index = 0; index < paths.length; index += 32) {
|
|
1042
|
+
artifacts.push(...await evidence.capture(paths.slice(index, index + 32)));
|
|
1043
|
+
}
|
|
1044
|
+
const known = await evidence.reconcile({ limit: 32 });
|
|
1045
|
+
const observed = [...artifacts, ...known];
|
|
1046
|
+
const changed = registerArtifacts(observed);
|
|
1047
|
+
// Parser revalidation may have already refreshed a file's generation.
|
|
1048
|
+
// Retain canceled classifier work until this dispatch path sees it.
|
|
1049
|
+
for (const artifact of observed) {
|
|
1050
|
+
if (!lineageWork.delete(artifact.id)) continue;
|
|
1051
|
+
if (!changed.some(item => item.id === artifact.id)) changed.push(artifact);
|
|
1052
|
+
}
|
|
1053
|
+
if (!changed.length) {
|
|
1054
|
+
const afterModel = platform.model.stats();
|
|
1055
|
+
if (expired || beforeModel.revision !== afterModel.revision || beforeModel.sequence !== afterModel.sequence) notify();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1005
1058
|
const session = selectedSession ? sessions.get(selectedSession) : null;
|
|
1006
1059
|
if (session) {
|
|
1007
1060
|
const event = observationEvent(session.id);
|
|
@@ -1037,6 +1090,23 @@ export function createPipeline({
|
|
|
1037
1090
|
return getState();
|
|
1038
1091
|
}
|
|
1039
1092
|
|
|
1093
|
+
function observeLineage(lineage) {
|
|
1094
|
+
return serialized(() => {
|
|
1095
|
+
if (closed || lineage.id === lineageId) return;
|
|
1096
|
+
platform.observeLineage(lineage);
|
|
1097
|
+
lineageId = lineage.id;
|
|
1098
|
+
completedClassifications.clear();
|
|
1099
|
+
for (const job of activeClassifications) job.controller.abort();
|
|
1100
|
+
for (const job of classificationQueue.splice(0)) {
|
|
1101
|
+
pending--;
|
|
1102
|
+
skipJob(job, 'source_changed_during_classification');
|
|
1103
|
+
}
|
|
1104
|
+
for (const artifactId of knownArtifacts.keys()) lineageWork.add(artifactId);
|
|
1105
|
+
registerArtifacts(evidence.setLineage(lineage.id));
|
|
1106
|
+
notify();
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1040
1110
|
function selectSession(id) {
|
|
1041
1111
|
if (!sessions.has(id)) return false;
|
|
1042
1112
|
selectedSession = id;
|
|
@@ -1045,12 +1115,15 @@ export function createPipeline({
|
|
|
1045
1115
|
}
|
|
1046
1116
|
|
|
1047
1117
|
async function whenIdle() {
|
|
1118
|
+
await serial;
|
|
1119
|
+
await platform.whenIdle();
|
|
1048
1120
|
await serial;
|
|
1049
1121
|
while (tasks.size || classificationQueue.length) {
|
|
1050
1122
|
pumpClassifications();
|
|
1051
1123
|
await Promise.allSettled([...tasks]);
|
|
1052
1124
|
await serial;
|
|
1053
1125
|
}
|
|
1126
|
+
await platform.whenIdle();
|
|
1054
1127
|
await serial;
|
|
1055
1128
|
}
|
|
1056
1129
|
|
|
@@ -1064,8 +1137,14 @@ export function createPipeline({
|
|
|
1064
1137
|
}
|
|
1065
1138
|
for (const job of activeClassifications) job.controller.abort();
|
|
1066
1139
|
decisionService?.close?.();
|
|
1140
|
+
await platform.close();
|
|
1067
1141
|
await whenIdle();
|
|
1068
1142
|
}
|
|
1069
1143
|
|
|
1070
|
-
return {
|
|
1144
|
+
return {
|
|
1145
|
+
ingest, getState, reconcile, observeLineage, setPaused, selectSession, whenIdle, close,
|
|
1146
|
+
getModelState: options => platform.snapshot(options),
|
|
1147
|
+
createCheckpoint: options => platform.checkpoint(options),
|
|
1148
|
+
model: platform.model,
|
|
1149
|
+
};
|
|
1071
1150
|
}
|