graphlin 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +369 -86
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +123 -80
- 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 +152 -142
- 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,889 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual as equal } from 'node:util';
|
|
3
|
+
import { integer, plain } from '../core/common.mjs';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_LIMITS, id, token, label, key, byteSize, relativePath, currentPolicy,
|
|
6
|
+
references, classification, entityRecord, relationRecord, interpretationRecord, certificate,
|
|
7
|
+
activityRecord, lineageRecord, projectSnapshot, time,
|
|
8
|
+
} from './records.mjs';
|
|
9
|
+
import { compareCheckpoint } from './changes.mjs';
|
|
10
|
+
import { restoreFrozenSnapshot } from './history.mjs';
|
|
11
|
+
|
|
12
|
+
const sameVersion = (a, b) => a?.hash === b?.hash && a?.generation === b?.generation;
|
|
13
|
+
const copy = value => structuredClone(value);
|
|
14
|
+
const fail = code => { throw Object.assign(new TypeError(code), { code }); };
|
|
15
|
+
const sourceRef = (cert, item, event) => ({
|
|
16
|
+
artifactId: cert.artifactId, hash: cert.hash, generation: cert.generation,
|
|
17
|
+
extractor: cert.extractor, extractorVersion: cert.version, identityVersion: cert.identityVersion,
|
|
18
|
+
...(integer(item?.startLine, 1) && integer(item?.endLine, item.startLine)
|
|
19
|
+
? { startLine: item.startLine, endLine: item.endLine } : {}),
|
|
20
|
+
...(id(event?.id) ? { eventId: event.id } : {}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Project facts belong to the worktree. Sessions only filter activity.
|
|
25
|
+
* No renderer state, source text, filesystem access, or asynchronous work lives here.
|
|
26
|
+
* @param {{projectId:string, policy?:object|Function, restoredState?:object,
|
|
27
|
+
* limits?:object, now?:Function|number}} options
|
|
28
|
+
*/
|
|
29
|
+
export function createProjectModel({ projectId, policy = {}, restoredState, limits = {}, now = Date.now } = {}) {
|
|
30
|
+
if (!id(projectId)) fail('INVALID_MODEL_PROJECT');
|
|
31
|
+
limits = Object.fromEntries(Object.entries(DEFAULT_LIMITS).map(([name, maximum]) =>
|
|
32
|
+
[name, integer(limits?.[name], 1) ? Math.min(limits[name], maximum) : maximum]));
|
|
33
|
+
const clock = () => typeof now === 'function' ? now() : now;
|
|
34
|
+
const entities = new Map(), relations = new Map(), interpretations = new Map();
|
|
35
|
+
const artifacts = new Map(), files = new Map(), scopes = new Map(), imports = new Map();
|
|
36
|
+
const activities = new Map(), sessions = new Map(), checkpoints = new Map();
|
|
37
|
+
const dependents = new Map(), children = new Map(), incident = new Map(), memberships = new Map(), symbols = new Set();
|
|
38
|
+
const importTargets = new Map(), pendingImports = new Set();
|
|
39
|
+
const weights = new WeakMap();
|
|
40
|
+
let bytes = 0, historyBytes = 0, revision = 0, sequence = 0, mutations = 0;
|
|
41
|
+
let lineage = null;
|
|
42
|
+
const deferred = { entities: 0, relations: 0, artifacts: 0, imports: 0, interpretations: 0, scopes: 0 };
|
|
43
|
+
let inventoryCoverage = { complete: false, inventoried: 0, deferred: 0, excluded: 0, unsupported: 0, unavailable: 0 };
|
|
44
|
+
const rootId = key('project', projectId);
|
|
45
|
+
|
|
46
|
+
function unlink(index, indexKey, value) {
|
|
47
|
+
const set = index.get(indexKey);
|
|
48
|
+
set?.delete(value);
|
|
49
|
+
if (set?.size === 0) index.delete(indexKey);
|
|
50
|
+
}
|
|
51
|
+
function link(index, indexKey, value) {
|
|
52
|
+
if (!indexKey) return;
|
|
53
|
+
if (!index.has(indexKey)) index.set(indexKey, new Set());
|
|
54
|
+
index.get(indexKey).add(value);
|
|
55
|
+
}
|
|
56
|
+
function unindex(record, type) {
|
|
57
|
+
for (const ref of record.sourceRefs ?? []) unlink(dependents, ref.artifactId, record);
|
|
58
|
+
if (type === 'entities') {
|
|
59
|
+
unlink(children, record.parentId, record.id);
|
|
60
|
+
symbols.delete(record.id);
|
|
61
|
+
} else if (type === 'relations') {
|
|
62
|
+
unlink(incident, record.source, record.id);
|
|
63
|
+
unlink(incident, record.target, record.id);
|
|
64
|
+
} else if (type === 'interpretations') {
|
|
65
|
+
for (const entityId of record.entityIds) unlink(memberships, entityId, record.id);
|
|
66
|
+
} else if (type === 'imports') {
|
|
67
|
+
for (const target of record.candidates) unlink(importTargets, target, record.id);
|
|
68
|
+
pendingImports.delete(record.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function index(record, type) {
|
|
72
|
+
for (const ref of record.sourceRefs ?? []) link(dependents, ref.artifactId, record);
|
|
73
|
+
if (type === 'entities') {
|
|
74
|
+
link(children, record.parentId, record.id);
|
|
75
|
+
if (!['project', 'directory', 'file', 'module'].includes(record.kind)) symbols.add(record.id);
|
|
76
|
+
} else if (type === 'relations') {
|
|
77
|
+
link(incident, record.source, record.id);
|
|
78
|
+
link(incident, record.target, record.id);
|
|
79
|
+
} else if (type === 'interpretations') {
|
|
80
|
+
for (const entityId of record.entityIds) link(memberships, entityId, record.id);
|
|
81
|
+
} else if (type === 'imports') {
|
|
82
|
+
for (const target of record.candidates) link(importTargets, target, record.id);
|
|
83
|
+
pendingImports.add(record.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function remove(map, recordId, type) {
|
|
87
|
+
const old = map.get(recordId);
|
|
88
|
+
if (!old) return;
|
|
89
|
+
bytes -= weights.get(old) ?? 0;
|
|
90
|
+
map.delete(recordId);
|
|
91
|
+
mutations++;
|
|
92
|
+
if (type) unindex(old, type);
|
|
93
|
+
}
|
|
94
|
+
function put(map, recordId, record, cap, type) {
|
|
95
|
+
const old = map.get(recordId), weight = byteSize(record);
|
|
96
|
+
if (old && equal(old, record)) return true;
|
|
97
|
+
const delta = weight - (old ? weights.get(old) ?? 0 : 0);
|
|
98
|
+
if ((!old && map.size >= cap) || bytes + historyBytes + delta > limits.bytes) {
|
|
99
|
+
// A byte limit must never preserve a now-invalid current relationship.
|
|
100
|
+
// Eviction is reported as coverage loss, never as source deletion.
|
|
101
|
+
if (old && ['relations', 'imports'].includes(type) && record.validity !== 'current') {
|
|
102
|
+
remove(map, recordId, type);
|
|
103
|
+
deferred[type]++;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (old && type) unindex(old, type);
|
|
108
|
+
bytes += delta;
|
|
109
|
+
map.set(recordId, record);
|
|
110
|
+
mutations++;
|
|
111
|
+
weights.set(record, weight);
|
|
112
|
+
if (type) index(record, type);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function detachChildren(parentId) {
|
|
116
|
+
for (const childId of [...(children.get(parentId) ?? [])]) {
|
|
117
|
+
const child = entities.get(childId);
|
|
118
|
+
if (!child) continue;
|
|
119
|
+
const detached = { ...child, parentId: null, ownership: 'unresolved' };
|
|
120
|
+
if (!put(entities, childId, detached, limits.entities, 'entities')) {
|
|
121
|
+
// Drop optional display metadata before allowing a dangling parent.
|
|
122
|
+
delete detached.qualifiedName;
|
|
123
|
+
delete detached.knownAtSequence;
|
|
124
|
+
put(entities, childId, detached, limits.entities, 'entities');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function evictEntity(entityId, defer = true) {
|
|
129
|
+
const entity = entities.get(entityId), artifact = artifacts.get(entity?.artifactId);
|
|
130
|
+
detachChildren(entityId);
|
|
131
|
+
for (const relationId of [...(incident.get(entityId) ?? [])]) remove(relations, relationId, 'relations');
|
|
132
|
+
for (const interpretationId of [...(memberships.get(entityId) ?? [])]) remove(interpretations, interpretationId, 'interpretations');
|
|
133
|
+
remove(entities, entityId, 'entities');
|
|
134
|
+
if (['parsed', 'lexical'].includes(entity?.basis) && artifact?.enumeration?.complete) {
|
|
135
|
+
// Release the entity's bytes first, so this correctness update also fits
|
|
136
|
+
// when eviction was triggered by the serialized-byte limit.
|
|
137
|
+
put(artifacts, artifact.id, { ...artifact, enumeration: {
|
|
138
|
+
...artifact.enumeration, complete: false, omissions: ['model_capacity'],
|
|
139
|
+
} }, limits.artifacts);
|
|
140
|
+
}
|
|
141
|
+
if (defer) deferred.entities++;
|
|
142
|
+
}
|
|
143
|
+
function admitEntity(entity, summary = false) {
|
|
144
|
+
// File and root summaries can replace expanded symbols, including when
|
|
145
|
+
// discovered after a tooling-heavy batch has filled the model.
|
|
146
|
+
const old = entities.get(entity.id);
|
|
147
|
+
const delta = byteSize(entity) - (weights.get(old) ?? 0);
|
|
148
|
+
while (summary && (!old && entities.size >= limits.entities ||
|
|
149
|
+
bytes + historyBytes + delta > limits.bytes) && symbols.size) {
|
|
150
|
+
evictEntity(symbols.values().next().value);
|
|
151
|
+
}
|
|
152
|
+
const accepted = put(entities, entity.id, entity, limits.entities, 'entities');
|
|
153
|
+
if (!accepted) deferred.entities++;
|
|
154
|
+
return accepted;
|
|
155
|
+
}
|
|
156
|
+
function admitRelation(relation) {
|
|
157
|
+
if (!entities.has(relation.source) || !entities.has(relation.target)) return false;
|
|
158
|
+
const accepted = put(relations, relation.id, relation, limits.relations, 'relations');
|
|
159
|
+
if (!accepted) deferred.relations++;
|
|
160
|
+
return accepted;
|
|
161
|
+
}
|
|
162
|
+
function appendActivity(event, nextSequence = sequence) {
|
|
163
|
+
const record = activityRecord(event, nextSequence, clock());
|
|
164
|
+
if (!record) return;
|
|
165
|
+
const storageId = `${nextSequence}`;
|
|
166
|
+
while (activities.size >= limits.activity) remove(activities, activities.keys().next().value);
|
|
167
|
+
put(activities, storageId, record, limits.activity);
|
|
168
|
+
}
|
|
169
|
+
function commit(kind, event = {}) {
|
|
170
|
+
revision++;
|
|
171
|
+
sequence++;
|
|
172
|
+
appendActivity({ ...event, kind, outcome: 'observed' });
|
|
173
|
+
}
|
|
174
|
+
function metadataEntity(recordId, displayPath, kind, parentId) {
|
|
175
|
+
return {
|
|
176
|
+
id: recordId, label: displayPath ? displayPath.split('/').at(-1) : 'Project',
|
|
177
|
+
kind, parentId, sourceRefs: [], basis: 'metadata', validity: 'current', classification: 'unknown',
|
|
178
|
+
knownAtSequence: sequence + 1, ...(displayPath ? { relativePath: displayPath } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
admitEntity(metadataEntity(rootId, null, 'project', null), true);
|
|
182
|
+
|
|
183
|
+
function ensureDirectory(directory) {
|
|
184
|
+
if (!directory || directory === '.') return rootId;
|
|
185
|
+
let parentId = rootId, accumulated = '';
|
|
186
|
+
for (const part of directory.split('/').slice(0, 32)) {
|
|
187
|
+
accumulated = accumulated ? `${accumulated}/${part}` : part;
|
|
188
|
+
const scopeId = key('scope', projectId, accumulated);
|
|
189
|
+
if (!scopes.has(scopeId)) {
|
|
190
|
+
if (scopes.size >= limits.scopes && parentId === rootId) {
|
|
191
|
+
const nested = [...scopes.values()].find(scope => scope.relativePath.includes('/'));
|
|
192
|
+
if (nested) {
|
|
193
|
+
evictEntity(nested.id, false);
|
|
194
|
+
remove(scopes, nested.id);
|
|
195
|
+
deferred.scopes++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (scopes.size >= limits.scopes) { deferred.scopes++; return parentId; }
|
|
199
|
+
const record = { id: scopeId, parentId, relativePath: accumulated, label: accumulated, inventoried: 0, deferred: 0 };
|
|
200
|
+
if (!admitEntity(metadataEntity(scopeId, accumulated, 'directory', parentId), true) ||
|
|
201
|
+
!put(scopes, scopeId, record, limits.scopes)) return parentId;
|
|
202
|
+
}
|
|
203
|
+
parentId = scopeId;
|
|
204
|
+
}
|
|
205
|
+
return parentId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function observeInventory({ entries = [], coverage = {} } = {}) {
|
|
209
|
+
if (!Array.isArray(entries)) return stats();
|
|
210
|
+
const before = mutations, oldCoverage = inventoryCoverage, oldDeferred = copy(deferred);
|
|
211
|
+
const effectivePolicy = currentPolicy(policy);
|
|
212
|
+
const accepted = [];
|
|
213
|
+
for (const entry of entries.slice(0, limits.artifacts * 2)) {
|
|
214
|
+
if (!plain(entry)) continue;
|
|
215
|
+
const name = relativePath(entry.relativePath, effectivePolicy);
|
|
216
|
+
if (!name) continue;
|
|
217
|
+
const suppliedRoot = relativePath(entry.root, effectivePolicy);
|
|
218
|
+
const root = suppliedRoot && (entry.kind === 'directory' && name === suppliedRoot || name.startsWith(`${suppliedRoot}/`))
|
|
219
|
+
? suppliedRoot : name.includes('/') ? name.split('/')[0] : '';
|
|
220
|
+
// Reserve known roots before spending the remaining budget on files.
|
|
221
|
+
if (root) ensureDirectory(root);
|
|
222
|
+
accepted.push({ entry, name, root });
|
|
223
|
+
}
|
|
224
|
+
for (const { entry, name, root } of accepted) {
|
|
225
|
+
const directory = entry.kind === 'directory';
|
|
226
|
+
const parentId = ensureDirectory(directory ? name : path.posix.dirname(name));
|
|
227
|
+
if (directory) continue;
|
|
228
|
+
const old = files.get(name);
|
|
229
|
+
if (!old && files.size >= limits.artifacts) {
|
|
230
|
+
deferred.artifacts++;
|
|
231
|
+
const scope = scopes.get(parentId);
|
|
232
|
+
if (scope) put(scopes, parentId, { ...scope, deferred: scope.deferred + 1 }, limits.scopes);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const scopeId = id(entry.scopeId) ?? old?.scopeId ?? key('file', projectId, name);
|
|
236
|
+
const record = {
|
|
237
|
+
id: key('inventory', projectId, name), relativePath: name, root, kind: token(entry.kind, 'file'),
|
|
238
|
+
size: integer(entry.size) ? entry.size : 0,
|
|
239
|
+
mtimeMs: Number.isFinite(entry.mtimeMs) && entry.mtimeMs >= 0 ? entry.mtimeMs : 0,
|
|
240
|
+
scopeId, parentId,
|
|
241
|
+
...(id(entry.artifactId) || old?.artifactId ? { artifactId: id(entry.artifactId) ?? old.artifactId } : {}),
|
|
242
|
+
};
|
|
243
|
+
if (!put(files, name, record, limits.artifacts)) { deferred.artifacts++; continue; }
|
|
244
|
+
if (!equal(old, record)) scheduleImports(name);
|
|
245
|
+
if (!entities.has(scopeId)) admitEntity(metadataEntity(scopeId, name, 'file', parentId), true);
|
|
246
|
+
if (!old) {
|
|
247
|
+
const scope = scopes.get(parentId);
|
|
248
|
+
if (scope) put(scopes, parentId, { ...scope, inventoried: scope.inventoried + 1 }, limits.scopes);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
inventoryCoverage = {
|
|
252
|
+
complete: coverage.complete === true && deferred.artifacts === 0 && deferred.scopes === 0,
|
|
253
|
+
...Object.fromEntries(['inventoried', 'deferred', 'excluded', 'unsupported', 'unavailable'].map(field =>
|
|
254
|
+
[field, integer(coverage[field]) ? coverage[field] : field === 'inventoried' ? files.size : 0])),
|
|
255
|
+
};
|
|
256
|
+
resolveImports();
|
|
257
|
+
if (mutations !== before || !equal(oldCoverage, inventoryCoverage) || !equal(oldDeferred, deferred)) commit('inventory.observed');
|
|
258
|
+
return stats();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function artifactDependents(artifactId, freshness) {
|
|
262
|
+
for (const record of [...(dependents.get(artifactId) ?? [])]) {
|
|
263
|
+
const type = Object.hasOwn(record, 'parentId') ? 'entities'
|
|
264
|
+
: Object.hasOwn(record, 'source') ? 'relations' : Object.hasOwn(record, 'specifier') ? 'imports' : 'interpretations';
|
|
265
|
+
const map = { entities, relations, interpretations, imports }[type];
|
|
266
|
+
const current = map.get(record.id);
|
|
267
|
+
if (!current) continue;
|
|
268
|
+
const updated = { ...current, validity: freshness };
|
|
269
|
+
if ('classification' in record) updated.classification = freshness === 'current' ? record.classification : 'stale';
|
|
270
|
+
put(map, record.id, updated, limits[type], type);
|
|
271
|
+
if (type === 'entities') invalidateEntitySupport(record.id, freshness);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function invalidateEntitySupport(entityId, freshness) {
|
|
275
|
+
for (const interpretationId of [...(memberships.get(entityId) ?? [])]) {
|
|
276
|
+
const record = interpretations.get(interpretationId);
|
|
277
|
+
if (record) put(interpretations, record.id, { ...record, validity: 'stale', classification: 'stale' }, limits.interpretations, 'interpretations');
|
|
278
|
+
}
|
|
279
|
+
for (const relationId of [...(incident.get(entityId) ?? [])]) {
|
|
280
|
+
const record = relations.get(relationId);
|
|
281
|
+
if (record) put(relations, record.id, { ...record, validity: freshness }, limits.relations, 'relations');
|
|
282
|
+
}
|
|
283
|
+
if (freshness === 'retracted') detachChildren(entityId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function invalidateArtifacts(values = []) {
|
|
287
|
+
if (!Array.isArray(values)) return stats();
|
|
288
|
+
const before = mutations, oldDeferred = copy(deferred);
|
|
289
|
+
for (const input of values.slice(0, limits.artifacts)) {
|
|
290
|
+
const value = typeof input === 'string' ? { id: input } : input;
|
|
291
|
+
const artifactId = id(value?.id ?? value?.artifactId);
|
|
292
|
+
if (!artifactId) continue;
|
|
293
|
+
const old = artifacts.get(artifactId);
|
|
294
|
+
const generation = integer(value.generation, 1) ? value.generation : old?.generation ?? 1;
|
|
295
|
+
if (old && generation < old.generation) continue;
|
|
296
|
+
const hash = typeof value.hash === 'string' && /^[a-f0-9]{64}$/.test(value.hash) ? value.hash : null;
|
|
297
|
+
if (old && generation === old.generation && old.hash && hash && old.hash !== hash) continue;
|
|
298
|
+
const status = ['present', 'partial', 'missing', 'unavailable'].includes(value.status) ? value.status : 'unavailable';
|
|
299
|
+
const name = relativePath(value.relativePath, currentPolicy(policy)) ?? old?.relativePath;
|
|
300
|
+
const record = {
|
|
301
|
+
id: artifactId, hash, generation, status,
|
|
302
|
+
fresh: status === 'present' && sameVersion(old, { hash, generation }) && old?.fresh === true,
|
|
303
|
+
observed: status === 'present' && hash !== null,
|
|
304
|
+
complete: value.complete === true,
|
|
305
|
+
...(name ? { relativePath: name } : {}),
|
|
306
|
+
...(old?.enumeration ? { enumeration: old.enumeration } : {}),
|
|
307
|
+
};
|
|
308
|
+
// Fresh parsing already establishes this exact capture. Do not add a
|
|
309
|
+
// redundant authority flag on every periodic source revalidation.
|
|
310
|
+
if (record.fresh && !old.observed) delete record.observed;
|
|
311
|
+
if (!put(artifacts, artifactId, record, limits.artifacts)) { deferred.artifacts++; continue; }
|
|
312
|
+
if (name && files.has(name)) put(files, name, { ...files.get(name), artifactId }, limits.artifacts);
|
|
313
|
+
if (status !== 'present' || !sameVersion(old, record)) {
|
|
314
|
+
artifactDependents(artifactId, status === 'missing' && value.complete === true ? 'retracted' : 'stale');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (mutations !== before || !equal(oldDeferred, deferred)) commit('artifacts.observed');
|
|
318
|
+
return stats();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function currentRefs(refs) {
|
|
322
|
+
return refs.length > 0 && refs.every(ref => {
|
|
323
|
+
const artifact = artifacts.get(ref.artifactId);
|
|
324
|
+
return ref.sourceClass !== 'public_intent' && artifact?.status === 'present' && sameVersion(artifact, ref) &&
|
|
325
|
+
artifact.fresh === true;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function validParent(entity, parentId) {
|
|
329
|
+
const parent = entities.get(parentId);
|
|
330
|
+
if (!parent || parent.validity === 'retracted' || parent.id === entity.id ||
|
|
331
|
+
parent.artifactId !== entity.artifactId) return false;
|
|
332
|
+
const childSpan = entity.sourceRefs[0], parentSpan = parent.sourceRefs[0];
|
|
333
|
+
if (!childSpan?.startLine || !parentSpan?.startLine ||
|
|
334
|
+
parentSpan.startLine > childSpan.startLine || parentSpan.endLine < childSpan.endLine) return false;
|
|
335
|
+
const seen = new Set([entity.id]);
|
|
336
|
+
let next = parent;
|
|
337
|
+
while (next) {
|
|
338
|
+
if (seen.has(next.id) || seen.size > 256) return false;
|
|
339
|
+
seen.add(next.id);
|
|
340
|
+
next = entities.get(next.parentId);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function observeStructure(structure, { event } = {}) {
|
|
346
|
+
const cert = certificate(structure?.enumeration);
|
|
347
|
+
if (!currentPolicy(policy).readSource || !cert || !Array.isArray(structure.entities) ||
|
|
348
|
+
event?.kind === 'tool.requested' || event?.sourceClass === 'public_intent') return stats();
|
|
349
|
+
const oldArtifact = artifacts.get(cert.artifactId);
|
|
350
|
+
if (oldArtifact && (cert.generation < oldArtifact.generation ||
|
|
351
|
+
cert.generation === oldArtifact.generation && (oldArtifact.hash && oldArtifact.hash !== cert.hash ||
|
|
352
|
+
['missing', 'unavailable'].includes(oldArtifact.status)))) return stats();
|
|
353
|
+
const root = structure.entities.find(entity => entity?.id === cert.scopeId);
|
|
354
|
+
if (!root || root.parentId != null || root.artifactId !== cert.artifactId) return stats();
|
|
355
|
+
let name = relativePath(structure.relativePath, currentPolicy(policy)) ?? oldArtifact?.relativePath;
|
|
356
|
+
if (!name && files.has(root.qualifiedName)) name = root.qualifiedName;
|
|
357
|
+
if (!name) {
|
|
358
|
+
for (const file of files.values()) {
|
|
359
|
+
if (file.artifactId === cert.artifactId || file.scopeId === cert.scopeId || file.relativePath === root.label) {
|
|
360
|
+
name = file.relativePath; break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const file = name ? files.get(name) : null;
|
|
365
|
+
if (name && /\.(?:md|markdown|mdx|rst|adoc|txt)$/i.test(name)) return stats();
|
|
366
|
+
const before = mutations, oldDeferred = copy(deferred);
|
|
367
|
+
const previous = new Set([...dependents.get(cert.artifactId) ?? []].filter(record =>
|
|
368
|
+
entities.get(record.id) === record && record.artifactId === cert.artifactId).map(record => record.id));
|
|
369
|
+
const artifact = {
|
|
370
|
+
id: cert.artifactId, hash: cert.hash, generation: cert.generation, status: 'present',
|
|
371
|
+
complete: sameVersion(oldArtifact, cert) ? oldArtifact.complete : cert.complete,
|
|
372
|
+
fresh: true, enumeration: cert, ...(name ? { relativePath: name } : {}),
|
|
373
|
+
};
|
|
374
|
+
if (!put(artifacts, artifact.id, artifact, limits.artifacts)) { deferred.artifacts++; return stats(); }
|
|
375
|
+
if (oldArtifact && !sameVersion(oldArtifact, cert)) artifactDependents(cert.artifactId, 'stale');
|
|
376
|
+
if (file) {
|
|
377
|
+
if (file.scopeId !== cert.scopeId && entities.get(file.scopeId)?.basis === 'metadata') evictEntity(file.scopeId, false);
|
|
378
|
+
put(files, name, { ...file, artifactId: cert.artifactId, scopeId: cert.scopeId }, limits.artifacts);
|
|
379
|
+
}
|
|
380
|
+
const incoming = new Map(), desiredParents = new Map();
|
|
381
|
+
let complete = cert.complete && structure.entities.length <= limits.entities * 2;
|
|
382
|
+
for (const value of structure.entities.slice(0, limits.entities * 2)) {
|
|
383
|
+
if (!plain(value) || value.artifactId !== cert.artifactId || !id(value.id) || incoming.has(value.id)) {
|
|
384
|
+
complete = false; continue;
|
|
385
|
+
}
|
|
386
|
+
const existing = entities.get(value.id);
|
|
387
|
+
if (existing && existing.artifactId && existing.artifactId !== cert.artifactId) { complete = false; continue; }
|
|
388
|
+
const entity = entityRecord({
|
|
389
|
+
...value, parentId: existing?.parentId === id(value.parentId) ? existing.parentId : null,
|
|
390
|
+
basis: cert.capability === 'parsed' ? 'parsed' : 'lexical',
|
|
391
|
+
validity: 'current', classification: cert.capability === 'parsed' ? 'accepted' : 'tentative',
|
|
392
|
+
sourceRefs: [sourceRef(cert, value, event)], knownAtSequence: existing?.knownAtSequence ?? sequence + 1,
|
|
393
|
+
createdAtSequence: existing?.createdAtSequence,
|
|
394
|
+
ownership: existing?.ownership,
|
|
395
|
+
...(value.id === cert.scopeId && name ? { relativePath: name } : {}),
|
|
396
|
+
}, limits.refs);
|
|
397
|
+
if (!entity) { complete = false; continue; }
|
|
398
|
+
incoming.set(entity.id, entity);
|
|
399
|
+
desiredParents.set(entity.id, id(value.parentId));
|
|
400
|
+
}
|
|
401
|
+
// Root admission precedes symbol admission even if a parser returns it last.
|
|
402
|
+
const rootEntity = incoming.get(cert.scopeId);
|
|
403
|
+
if (!rootEntity || !admitEntity({
|
|
404
|
+
...rootEntity, parentId: entities.has(file?.parentId) ? file.parentId : null,
|
|
405
|
+
}, true)) return stats();
|
|
406
|
+
for (const entity of incoming.values()) {
|
|
407
|
+
if (entity.id !== cert.scopeId && !admitEntity(entity)) complete = false;
|
|
408
|
+
}
|
|
409
|
+
for (const [entityId, parentId] of desiredParents) {
|
|
410
|
+
if (entityId === cert.scopeId) continue;
|
|
411
|
+
const entity = entities.get(entityId);
|
|
412
|
+
if (!entity || !incoming.has(entityId)) continue;
|
|
413
|
+
const supported = parentId && cert.capability === 'parsed' && validParent(entity, parentId);
|
|
414
|
+
if (!supported && parentId) complete = false;
|
|
415
|
+
const owned = { ...entity, parentId: supported ? parentId : null };
|
|
416
|
+
if (supported) delete owned.ownership;
|
|
417
|
+
else owned.ownership = 'unresolved';
|
|
418
|
+
put(entities, entity.id, owned, limits.entities, 'entities');
|
|
419
|
+
}
|
|
420
|
+
// Disappearance needs matching enumeration semantics. Grammar/identity
|
|
421
|
+
// upgrades make old identities stale, even when the source hash is unchanged.
|
|
422
|
+
for (const entityId of previous) {
|
|
423
|
+
if (incoming.has(entityId)) continue;
|
|
424
|
+
const old = entities.get(entityId);
|
|
425
|
+
if (!old) continue;
|
|
426
|
+
const ref = old.sourceRefs.find(ref => ref.artifactId === cert.artifactId);
|
|
427
|
+
const canRetract = complete && ref?.extractor === cert.extractor &&
|
|
428
|
+
ref.extractorVersion === cert.version && ref.identityVersion === cert.identityVersion;
|
|
429
|
+
put(entities, entityId, { ...old, validity: canRetract ? 'retracted' : 'stale', classification: 'stale' }, limits.entities, 'entities');
|
|
430
|
+
invalidateEntitySupport(entityId, canRetract ? 'retracted' : 'stale');
|
|
431
|
+
}
|
|
432
|
+
const enumeration = { ...cert, complete };
|
|
433
|
+
put(artifacts, artifact.id, { ...artifact, enumeration }, limits.artifacts);
|
|
434
|
+
// Containment is represented by parentId; other parser relations remain
|
|
435
|
+
// typed, with support for both endpoints rather than renderer-derived roles.
|
|
436
|
+
const freshRelations = new Set();
|
|
437
|
+
for (const value of (Array.isArray(structure.relations) ? structure.relations : []).slice(0, limits.relations)) {
|
|
438
|
+
if (['contains', 'containment'].includes(value?.kind)) continue;
|
|
439
|
+
const source = entities.get(value?.source), target = entities.get(value?.target);
|
|
440
|
+
if (!source || !target || source.validity !== 'current' || target.validity !== 'current' ||
|
|
441
|
+
source.artifactId !== cert.artifactId || target.artifactId !== cert.artifactId ||
|
|
442
|
+
!currentRefs(source.sourceRefs) || !currentRefs(target.sourceRefs)) continue;
|
|
443
|
+
const relation = relationRecord({
|
|
444
|
+
...value, basis: cert.capability === 'parsed' ? 'parsed' : 'lexical',
|
|
445
|
+
validity: 'current', sourceRefs: [sourceRef(cert, root, event)],
|
|
446
|
+
}, limits.refs);
|
|
447
|
+
if (relation && admitRelation(relation)) freshRelations.add(relation.id);
|
|
448
|
+
}
|
|
449
|
+
for (const relation of [...relations.values()]) {
|
|
450
|
+
if (relation.kind !== 'imports' && ['parsed', 'lexical'].includes(relation.basis) &&
|
|
451
|
+
relation.sourceRefs.some(ref => ref.artifactId === cert.artifactId) && !freshRelations.has(relation.id)) {
|
|
452
|
+
const sameExtractor = relation.sourceRefs.every(ref => ref.extractor === cert.extractor &&
|
|
453
|
+
ref.extractorVersion === cert.version && ref.identityVersion === cert.identityVersion);
|
|
454
|
+
put(relations, relation.id, { ...relation, validity: complete && sameExtractor ? 'retracted' : 'stale' }, limits.relations, 'relations');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
observeImports(structure.imports, cert, event, complete);
|
|
458
|
+
if (name) scheduleImports(name);
|
|
459
|
+
for (const activity of activities.values()) applyCreation(activity);
|
|
460
|
+
resolveImports();
|
|
461
|
+
if (mutations !== before || !equal(oldDeferred, deferred)) {
|
|
462
|
+
commit('structure.observed', { artifactIds: [cert.artifactId], ...(id(event?.sessionId) ? { sessionId: event.sessionId } : {}) });
|
|
463
|
+
}
|
|
464
|
+
return { ...stats(), accepted: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function observeImports(values, cert, event, complete) {
|
|
468
|
+
const seen = new Set();
|
|
469
|
+
for (const value of (Array.isArray(values) ? values : []).slice(0, limits.imports)) {
|
|
470
|
+
if (!plain(value) || !id(value.id) || entities.get(value.ownerId)?.artifactId !== cert.artifactId ||
|
|
471
|
+
value.artifactId !== cert.artifactId) continue;
|
|
472
|
+
const specifier = typeof value.specifier === 'string' && value.specifier.length <= 256 &&
|
|
473
|
+
/^\.{1,2}\/[A-Za-z0-9_./-]+$/.test(value.specifier) ? value.specifier : null;
|
|
474
|
+
const record = {
|
|
475
|
+
id: value.id, artifactId: cert.artifactId, ownerId: value.ownerId, specifier,
|
|
476
|
+
kind: token(value.kind), sourceRefs: [sourceRef(cert, value, event)], validity: 'current',
|
|
477
|
+
candidates: importCandidates(artifacts.get(cert.artifactId)?.relativePath, specifier),
|
|
478
|
+
};
|
|
479
|
+
if (!put(imports, value.id, record, limits.imports, 'imports')) deferred.imports++;
|
|
480
|
+
else seen.add(value.id);
|
|
481
|
+
}
|
|
482
|
+
for (const value of [...imports.values()]) {
|
|
483
|
+
if (value.artifactId === cert.artifactId && !seen.has(value.id)) {
|
|
484
|
+
if (complete) remove(imports, value.id, 'imports');
|
|
485
|
+
else put(imports, value.id, { ...value, validity: 'stale' }, limits.imports, 'imports');
|
|
486
|
+
const relation = relations.get(key('import', projectId, value.id));
|
|
487
|
+
if (relation) put(relations, relation.id, { ...relation, validity: complete ? 'retracted' : 'stale' }, limits.relations, 'relations');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function importCandidates(ownerPath, specifier) {
|
|
493
|
+
if (!ownerPath || !specifier) return [];
|
|
494
|
+
const targetPath = path.posix.normalize(path.posix.join(path.posix.dirname(ownerPath), specifier));
|
|
495
|
+
if (!relativePath(targetPath, currentPolicy(policy))) return [];
|
|
496
|
+
const candidates = [targetPath];
|
|
497
|
+
if (!path.posix.extname(targetPath)) {
|
|
498
|
+
candidates.push(...['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx'].map(ext => targetPath + ext));
|
|
499
|
+
candidates.push(...['.js', '.mjs', '.ts', '.tsx'].map(ext => `${targetPath}/index${ext}`));
|
|
500
|
+
}
|
|
501
|
+
return candidates;
|
|
502
|
+
}
|
|
503
|
+
function scheduleImports(name) {
|
|
504
|
+
for (const importId of importTargets.get(name) ?? []) pendingImports.add(importId);
|
|
505
|
+
}
|
|
506
|
+
function resolveImports() {
|
|
507
|
+
for (let count = 0; count < 256 && pendingImports.size; count++) {
|
|
508
|
+
const importId = pendingImports.values().next().value;
|
|
509
|
+
pendingImports.delete(importId);
|
|
510
|
+
const entry = imports.get(importId);
|
|
511
|
+
if (!entry) continue;
|
|
512
|
+
if (!entry.specifier || entry.validity !== 'current' || !currentRefs(entry.sourceRefs)) continue;
|
|
513
|
+
if (!['import', 'reexport', 'dynamic_import'].includes(entry.kind)) {
|
|
514
|
+
const previous = relations.get(key('import', projectId, entry.id));
|
|
515
|
+
if (previous) put(relations, previous.id, { ...previous, validity: 'stale' }, limits.relations, 'relations');
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const targets = entry.candidates.map(candidate => files.get(candidate)).filter(Boolean);
|
|
519
|
+
// Ambiguous extension/package resolution stays unknown. No binding-level
|
|
520
|
+
// target, external service, or deployment role is inferred from a name.
|
|
521
|
+
if (targets.length !== 1) {
|
|
522
|
+
const relation = relations.get(key('import', projectId, entry.id));
|
|
523
|
+
if (relation) put(relations, relation.id, { ...relation, validity: 'stale' }, limits.relations, 'relations');
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const target = entities.get(targets[0].scopeId);
|
|
527
|
+
if (!target || target.validity !== 'current' || !currentRefs(target.sourceRefs)) continue;
|
|
528
|
+
const refs = [...entry.sourceRefs, ...target.sourceRefs];
|
|
529
|
+
if (refs.length > limits.refs) { deferred.relations++; continue; }
|
|
530
|
+
admitRelation({
|
|
531
|
+
id: key('import', projectId, entry.id), source: entry.ownerId, target: target.id,
|
|
532
|
+
kind: 'imports', basis: 'parsed', validity: 'current', sourceRefs: copy(refs),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function observeInterpretations(values, { event } = {}) {
|
|
538
|
+
if (!Array.isArray(values) || !currentPolicy(policy).readSource) return stats();
|
|
539
|
+
for (const value of values.slice(0, limits.interpretations)) {
|
|
540
|
+
const record = interpretationRecord(value, limits.refs);
|
|
541
|
+
if (!record || record.entityIds.some(entityId => !entities.has(entityId))) continue;
|
|
542
|
+
if (record.support === 'supported' && (!currentRefs(record.sourceRefs) ||
|
|
543
|
+
record.entityIds.some(entityId => entities.get(entityId).validity !== 'current'))) continue;
|
|
544
|
+
const storageId = key('interpretation', projectId, record.namespace, record.id);
|
|
545
|
+
const normalized = { ...record, id: storageId, validity: record.sourceRefs.length && !currentRefs(record.sourceRefs) ? 'stale' : record.validity };
|
|
546
|
+
if (!put(interpretations, storageId, normalized, limits.interpretations, 'interpretations')) deferred.interpretations++;
|
|
547
|
+
}
|
|
548
|
+
commit('interpretations.observed', { sessionId: event?.sessionId });
|
|
549
|
+
return stats();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function decisionFreshness(value, sourceRefs, fresh) {
|
|
553
|
+
if (!fresh) return 'stale';
|
|
554
|
+
if (value.validity === 'retracted') return 'retracted';
|
|
555
|
+
if (value.validity !== 'current' || !currentPolicy(policy).readSource || !sourceRefs.length) return 'stale';
|
|
556
|
+
return sourceRefs.every(ref => {
|
|
557
|
+
const artifact = artifacts.get(ref.artifactId);
|
|
558
|
+
return ref.sourceClass !== 'public_intent' && artifact?.status === 'present' &&
|
|
559
|
+
(artifact.fresh || artifact.observed) && sameVersion(artifact, ref);
|
|
560
|
+
}) ? 'current' : 'stale';
|
|
561
|
+
}
|
|
562
|
+
function canonicalDecisionEntity(node, sourceRefs) {
|
|
563
|
+
const matches = new Map();
|
|
564
|
+
for (const ref of sourceRefs) {
|
|
565
|
+
const enumeration = artifacts.get(ref.artifactId)?.enumeration;
|
|
566
|
+
for (const record of dependents.get(ref.artifactId) ?? []) {
|
|
567
|
+
if (entities.get(record.id) !== record || record.basis !== 'parsed' || record.validity !== 'current') continue;
|
|
568
|
+
const qualified = node.qualifiedName ?? node.label;
|
|
569
|
+
const exact = record.qualifiedName === qualified;
|
|
570
|
+
// A bare name is only unique when complete enumeration established all
|
|
571
|
+
// declarations in the artifact. Nested same-name methods remain separate.
|
|
572
|
+
if (exact || enumeration?.complete && record.label === node.label) matches.set(record.id, record);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return matches.size === 1 ? matches.values().next().value : null;
|
|
576
|
+
}
|
|
577
|
+
function replaceDecisionAlias(oldId, canonicalId) {
|
|
578
|
+
if (oldId === canonicalId || !entities.has(oldId)) return;
|
|
579
|
+
for (const relationId of [...(incident.get(oldId) ?? [])]) {
|
|
580
|
+
const relation = relations.get(relationId);
|
|
581
|
+
put(relations, relationId, {
|
|
582
|
+
...relation, source: relation.source === oldId ? canonicalId : relation.source,
|
|
583
|
+
target: relation.target === oldId ? canonicalId : relation.target,
|
|
584
|
+
}, limits.relations, 'relations');
|
|
585
|
+
}
|
|
586
|
+
for (const interpretationId of [...(memberships.get(oldId) ?? [])]) {
|
|
587
|
+
const record = interpretations.get(interpretationId);
|
|
588
|
+
put(interpretations, interpretationId, {
|
|
589
|
+
...record, entityIds: [...new Set(record.entityIds.map(entityId => entityId === oldId ? canonicalId : entityId))],
|
|
590
|
+
}, limits.interpretations, 'interpretations');
|
|
591
|
+
}
|
|
592
|
+
evictEntity(oldId, false);
|
|
593
|
+
}
|
|
594
|
+
function observeLegacy(graph, { sessionId = 'unknown', fresh = false } = {}) {
|
|
595
|
+
if (!plain(graph) || !Array.isArray(graph.nodes) || !id(sessionId)) return stats();
|
|
596
|
+
const mapping = new Map();
|
|
597
|
+
for (const node of graph.nodes.slice(0, limits.entities)) {
|
|
598
|
+
if (!id(node?.id)) continue;
|
|
599
|
+
const legacyId = key('legacy', projectId, sessionId, node.id);
|
|
600
|
+
const sourceRefs = references(node.sourceRefs ?? [], limits.refs) ?? [];
|
|
601
|
+
const validity = decisionFreshness(node, sourceRefs, fresh);
|
|
602
|
+
const canonical = fresh && validity === 'current' ? canonicalDecisionEntity(node, sourceRefs) : null;
|
|
603
|
+
const entityId = canonical?.id ?? legacyId;
|
|
604
|
+
const entity = entityRecord({
|
|
605
|
+
...node, id: entityId, legacyId: node.id, sessionId, parentId: null,
|
|
606
|
+
kind: token(node.kind), basis: fresh ? 'decision' : 'legacy', validity,
|
|
607
|
+
classification: fresh ? validity === 'current' ? classification(node.classification) : 'stale' : 'unknown',
|
|
608
|
+
sourceRefs, knownAtSequence: entities.get(entityId)?.knownAtSequence ?? sequence + 1,
|
|
609
|
+
}, limits.refs);
|
|
610
|
+
if (canonical) {
|
|
611
|
+
replaceDecisionAlias(legacyId, canonical.id);
|
|
612
|
+
mapping.set(node.id, canonical.id);
|
|
613
|
+
} else if (entity && admitEntity(entity)) mapping.set(node.id, entityId);
|
|
614
|
+
if (fresh && mapping.has(node.id)) {
|
|
615
|
+
const interpretation = interpretationRecord({
|
|
616
|
+
id: key('role', projectId, sessionId, node.id), namespace: 'graphlin.legacy-role',
|
|
617
|
+
kind: node.kind, label: node.label, entityIds: [entityId], sourceRefs, validity,
|
|
618
|
+
classification: entity.classification, version: '1', sessionId,
|
|
619
|
+
support: validity !== 'current' ? 'unknown' : entity.classification === 'accepted' ? 'supported' : 'tentative',
|
|
620
|
+
}, limits.refs);
|
|
621
|
+
if (interpretation && !put(interpretations, interpretation.id, interpretation, limits.interpretations, 'interpretations')) {
|
|
622
|
+
deferred.interpretations++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const edge of (Array.isArray(graph.edges) ? graph.edges : []).slice(0, limits.relations)) {
|
|
627
|
+
if (!id(edge?.id) || !mapping.has(edge.source) || !mapping.has(edge.target)) continue;
|
|
628
|
+
const sourceRefs = references(edge.sourceRefs ?? [], limits.refs) ?? [];
|
|
629
|
+
const observedValidity = decisionFreshness(edge, sourceRefs, fresh);
|
|
630
|
+
const endpointsCurrent = [mapping.get(edge.source), mapping.get(edge.target)]
|
|
631
|
+
.every(entityId => entities.get(entityId)?.validity === 'current');
|
|
632
|
+
const relation = relationRecord({
|
|
633
|
+
id: key('legacy-relation', projectId, sessionId, edge.id), source: mapping.get(edge.source), target: mapping.get(edge.target),
|
|
634
|
+
kind: edge.kind ?? edge.relation, basis: fresh ? 'decision' : 'legacy',
|
|
635
|
+
validity: observedValidity === 'current' && !endpointsCurrent ? 'stale' : observedValidity, sourceRefs,
|
|
636
|
+
}, limits.refs);
|
|
637
|
+
if (relation) admitRelation(relation);
|
|
638
|
+
}
|
|
639
|
+
commit(fresh ? 'decisions.observed' : 'legacy.observed', { sessionId });
|
|
640
|
+
return stats();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function recordActivity(event) {
|
|
644
|
+
if (!plain(event)) return stats();
|
|
645
|
+
sequence++;
|
|
646
|
+
appendActivity(event);
|
|
647
|
+
if (applyCreation(activities.get(`${sequence}`))) revision++;
|
|
648
|
+
return stats();
|
|
649
|
+
}
|
|
650
|
+
function applyCreation(event) {
|
|
651
|
+
if (!event?.creation) return false;
|
|
652
|
+
let changed = false;
|
|
653
|
+
for (const entityId of event.entityIds) {
|
|
654
|
+
const entity = entities.get(entityId);
|
|
655
|
+
if (!entity || entity.createdAtSequence || entity.validity !== 'current' ||
|
|
656
|
+
!event.sourceRefs.some(ref => entity.sourceRefs.some(support =>
|
|
657
|
+
support.artifactId === ref.artifactId && sameVersion(support, ref)))) continue;
|
|
658
|
+
changed = put(entities, entityId, { ...entity, createdAtSequence: event.sequence }, limits.entities, 'entities') || changed;
|
|
659
|
+
}
|
|
660
|
+
return changed;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function setSessions(values = []) {
|
|
664
|
+
if (!Array.isArray(values)) return stats();
|
|
665
|
+
for (const sessionId of [...sessions.keys()]) remove(sessions, sessionId);
|
|
666
|
+
for (const value of values.slice(-limits.sessions)) {
|
|
667
|
+
const sessionId = id(value?.id ?? value?.sessionId);
|
|
668
|
+
if (!sessionId) continue;
|
|
669
|
+
const record = { id: sessionId, host: token(value.host), status: token(value.status), startedAt: time(value.startedAt, 0) };
|
|
670
|
+
if (value.endedAt != null) record.endedAt = time(value.endedAt, 0);
|
|
671
|
+
put(sessions, sessionId, record, limits.sessions);
|
|
672
|
+
}
|
|
673
|
+
commit('sessions.observed');
|
|
674
|
+
return stats();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function observeLineage(value) {
|
|
678
|
+
const next = lineageRecord(value);
|
|
679
|
+
const changed = !!next && !!lineage && next.id !== lineage.id;
|
|
680
|
+
if (!next || JSON.stringify(next) === JSON.stringify(lineage)) return { changed: false, lineage: copy(lineage) };
|
|
681
|
+
if (changed) {
|
|
682
|
+
// Branch/worktree observations change the authority of current evidence,
|
|
683
|
+
// not the recorded history or the identities we have already discovered.
|
|
684
|
+
for (const [map, type] of [[entities, 'entities'], [relations, 'relations'], [interpretations, 'interpretations'], [imports, 'imports']]) {
|
|
685
|
+
for (const record of map.values()) {
|
|
686
|
+
if (record.validity !== 'current' || record.basis === 'metadata') continue;
|
|
687
|
+
put(map, record.id, {
|
|
688
|
+
...record, validity: 'stale', ...('classification' in record ? { classification: 'stale' } : {}),
|
|
689
|
+
}, limits[type], type);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const artifact of artifacts.values()) {
|
|
693
|
+
const invalidated = { ...artifact, fresh: false, complete: false };
|
|
694
|
+
delete invalidated.observed;
|
|
695
|
+
if (artifact.enumeration) invalidated.enumeration = {
|
|
696
|
+
...artifact.enumeration, complete: false, coveredRanges: [], omissions: ['lineage_changed'],
|
|
697
|
+
};
|
|
698
|
+
if (!put(artifacts, artifact.id, invalidated, limits.artifacts)) {
|
|
699
|
+
// At the byte cap, discard an optional certificate rather than leave
|
|
700
|
+
// a complete absence proof attached to a different lineage.
|
|
701
|
+
delete invalidated.enumeration;
|
|
702
|
+
put(artifacts, artifact.id, invalidated, limits.artifacts);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
pendingImports.clear();
|
|
706
|
+
}
|
|
707
|
+
lineage = next;
|
|
708
|
+
commit('lineage.observed');
|
|
709
|
+
return { changed, lineage: copy(lineage) };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function coverage() {
|
|
713
|
+
let resolved = 0;
|
|
714
|
+
for (const value of imports.values()) if (relations.get(key('import', projectId, value.id))?.validity === 'current') resolved++;
|
|
715
|
+
return {
|
|
716
|
+
...inventoryCoverage, inspected: artifacts.size, retained: entities.size, deferred: copy(deferred),
|
|
717
|
+
inventoryDeferred: inventoryCoverage.deferred, scopes: [...scopes.values()],
|
|
718
|
+
files: [...files.values()], enumerations: [...artifacts.values()].flatMap(value => value.enumeration ? [value.enumeration] : []),
|
|
719
|
+
artifacts: [...artifacts.values()].map(({ enumeration, ...value }) => value),
|
|
720
|
+
relationships: { observed: imports.size, resolved, unresolved: imports.size - resolved, deferred: pendingImports.size },
|
|
721
|
+
...(lineage ? { lineage } : {}),
|
|
722
|
+
oldestSequence: activities.values().next().value?.sequence ?? sequence,
|
|
723
|
+
truncated: Object.values(deferred).some(Boolean) || !inventoryCoverage.complete,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function rawSnapshot() {
|
|
727
|
+
return {
|
|
728
|
+
schemaVersion: 2, projectId, revision, sequence, entities: [...entities.values()], relations: [...relations.values()],
|
|
729
|
+
interpretations: [...interpretations.values()], activity: [...activities.values()], coverage: coverage(),
|
|
730
|
+
sessions: [...sessions.values()], checkpoints: [...checkpoints.values()].map(value => value.marker),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function snapshot({ persistent = false, sessionId, checkpointId, scopeId } = {}) {
|
|
734
|
+
const checkpoint = checkpointId ? checkpoints.get(checkpointId) : null;
|
|
735
|
+
if (checkpointId && !checkpoint) fail('MODEL_CHECKPOINT_UNAVAILABLE');
|
|
736
|
+
const options = { persistent, sessionId, scopeId };
|
|
737
|
+
const effectivePolicy = currentPolicy(policy);
|
|
738
|
+
const result = projectSnapshot(checkpoint?.state ?? rawSnapshot(), effectivePolicy, options);
|
|
739
|
+
if (persistent && !checkpointId) {
|
|
740
|
+
result.checkpoints = [...checkpoints.values()].map(value => ({
|
|
741
|
+
...projectSnapshot({ ...value.state, checkpoints: [value.marker] }, effectivePolicy, options).checkpoints[0],
|
|
742
|
+
state: projectSnapshot(value.state, effectivePolicy, options),
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
return copy(result);
|
|
746
|
+
}
|
|
747
|
+
function checkpoint({ label: checkpointLabel, sessionId } = {}) {
|
|
748
|
+
const marker = {
|
|
749
|
+
id: key('checkpoint', projectId, revision, sequence + 1),
|
|
750
|
+
label: label(checkpointLabel, 'Checkpoint'), projectId, revision, sequence: sequence + 1, at: time(clock()),
|
|
751
|
+
...(id(sessionId) ? { sessionId } : {}),
|
|
752
|
+
};
|
|
753
|
+
const state = copy({ ...rawSnapshot(), sequence: sequence + 1 });
|
|
754
|
+
state.checkpoints.push(marker);
|
|
755
|
+
const size = byteSize(state);
|
|
756
|
+
if (size > limits.checkpointBytes || size + bytes > limits.bytes) fail('MODEL_CHECKPOINT_CAPACITY');
|
|
757
|
+
while (checkpoints.size >= limits.checkpoints || historyBytes + size > limits.checkpointBytes ||
|
|
758
|
+
bytes + historyBytes + size > limits.bytes) {
|
|
759
|
+
const first = checkpoints.keys().next().value;
|
|
760
|
+
historyBytes -= checkpoints.get(first).bytes;
|
|
761
|
+
checkpoints.delete(first);
|
|
762
|
+
}
|
|
763
|
+
sequence++;
|
|
764
|
+
checkpoints.set(marker.id, { marker, state, bytes: size });
|
|
765
|
+
historyBytes += size;
|
|
766
|
+
return copy(marker);
|
|
767
|
+
}
|
|
768
|
+
function changes(checkpointId) {
|
|
769
|
+
const checkpoint = checkpoints.get(checkpointId);
|
|
770
|
+
if (!checkpoint) fail('MODEL_CHECKPOINT_UNAVAILABLE');
|
|
771
|
+
// Compare protected records before redaction: tightening labels must not
|
|
772
|
+
// manufacture edits. Project only the returned records with current policy.
|
|
773
|
+
const current = rawSnapshot(), effectivePolicy = currentPolicy(policy);
|
|
774
|
+
const raw = compareCheckpoint(checkpoint.state, current, checkpointId);
|
|
775
|
+
const oldEntities = new Map(projectSnapshot(checkpoint.state, effectivePolicy).entities.map(value => [value.id, value]));
|
|
776
|
+
const newEntities = new Map(projectSnapshot(current, effectivePolicy).entities.map(value => [value.id, value]));
|
|
777
|
+
for (const name of ['discoveries', 'creations', 'removals', 'invalidations']) {
|
|
778
|
+
raw[name] = raw[name].map(value => newEntities.get(value.id));
|
|
779
|
+
}
|
|
780
|
+
raw.modifications = raw.modifications.map(value => ({
|
|
781
|
+
before: oldEntities.get(value.before.id), after: newEntities.get(value.after.id),
|
|
782
|
+
}));
|
|
783
|
+
return copy(raw);
|
|
784
|
+
}
|
|
785
|
+
function stats() {
|
|
786
|
+
return {
|
|
787
|
+
projectId, revision, sequence, entities: entities.size, relations: relations.size,
|
|
788
|
+
interpretations: interpretations.size, artifacts: artifacts.size, inventory: files.size,
|
|
789
|
+
activity: activities.size, sessions: sessions.size, checkpoints: checkpoints.size,
|
|
790
|
+
bytes: bytes + historyBytes, historyBytes, softLimitReached: bytes + historyBytes > limits.softBytes,
|
|
791
|
+
deferred: copy(deferred), limits: copy(limits),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function restore(state) {
|
|
796
|
+
if (!plain(state)) return;
|
|
797
|
+
if (state.schemaVersion !== 2) {
|
|
798
|
+
if (Array.isArray(state.nodes)) observeLegacy(state, { sessionId: 'restored' });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (state.projectId !== projectId) fail('MODEL_PROJECT_MISMATCH');
|
|
802
|
+
revision = integer(state.revision) ? state.revision : 0;
|
|
803
|
+
sequence = integer(state.sequence) ? state.sequence : 0;
|
|
804
|
+
lineage = lineageRecord(state.coverage?.lineage);
|
|
805
|
+
for (const value of (Array.isArray(state.entities) ? state.entities : []).slice(0, limits.entities)) {
|
|
806
|
+
const entity = entityRecord(value, limits.refs);
|
|
807
|
+
if (!entity) continue;
|
|
808
|
+
if (entity.basis !== 'metadata') { entity.validity = 'stale'; entity.classification = 'stale'; }
|
|
809
|
+
admitEntity(entity);
|
|
810
|
+
}
|
|
811
|
+
for (const entity of [...entities.values()]) {
|
|
812
|
+
const seen = new Set([entity.id]);
|
|
813
|
+
let parent = entities.get(entity.parentId), cycle = false;
|
|
814
|
+
while (parent) {
|
|
815
|
+
if (seen.has(parent.id) || seen.size > 256) { cycle = true; break; }
|
|
816
|
+
seen.add(parent.id);
|
|
817
|
+
parent = entities.get(parent.parentId);
|
|
818
|
+
}
|
|
819
|
+
if (entity.parentId && (cycle || !entities.has(entity.parentId) || entity.artifactId &&
|
|
820
|
+
entities.get(entity.parentId).basis !== 'metadata' && !validParent(entity, entity.parentId))) {
|
|
821
|
+
put(entities, entity.id, { ...entity, parentId: null, ownership: 'unresolved' }, limits.entities, 'entities');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
for (const value of (Array.isArray(state.relations) ? state.relations : []).slice(0, limits.relations)) {
|
|
825
|
+
const record = relationRecord(value, limits.refs);
|
|
826
|
+
if (record) admitRelation({ ...record, validity: 'stale' });
|
|
827
|
+
}
|
|
828
|
+
for (const value of (Array.isArray(state.interpretations) ? state.interpretations : []).slice(0, limits.interpretations)) {
|
|
829
|
+
const record = interpretationRecord(value, limits.refs);
|
|
830
|
+
if (record && record.entityIds.every(entityId => entities.has(entityId))) {
|
|
831
|
+
put(interpretations, record.id, { ...record, validity: 'stale', classification: 'stale' }, limits.interpretations, 'interpretations');
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const certificates = new Map((Array.isArray(state.coverage?.enumerations) ? state.coverage.enumerations : [])
|
|
835
|
+
.slice(0, limits.artifacts).map(value => certificate(value)).filter(Boolean).map(value => [value.artifactId, value]));
|
|
836
|
+
for (const value of (Array.isArray(state.coverage?.artifacts) ? state.coverage.artifacts : []).slice(0, limits.artifacts)) {
|
|
837
|
+
if (!id(value?.id) || !integer(value.generation, 1)) continue;
|
|
838
|
+
const cert = certificates.get(value.id);
|
|
839
|
+
const name = relativePath(value.relativePath, currentPolicy(policy));
|
|
840
|
+
put(artifacts, value.id, {
|
|
841
|
+
id: value.id, hash: /^[a-f0-9]{64}$/.test(value.hash) ? value.hash : null, generation: value.generation,
|
|
842
|
+
status: 'present', fresh: false, complete: false, ...(name ? { relativePath: name } : {}),
|
|
843
|
+
...(cert ? { enumeration: { ...cert, complete: false } } : {}),
|
|
844
|
+
}, limits.artifacts);
|
|
845
|
+
}
|
|
846
|
+
for (const value of (Array.isArray(state.coverage?.files) ? state.coverage.files : []).slice(0, limits.artifacts)) {
|
|
847
|
+
const name = relativePath(value?.relativePath, currentPolicy(policy));
|
|
848
|
+
if (!name || !id(value.scopeId)) continue;
|
|
849
|
+
const parentId = ensureDirectory(path.posix.dirname(name));
|
|
850
|
+
put(files, name, {
|
|
851
|
+
id: key('inventory', projectId, name), relativePath: name, root: relativePath(value.root) ?? '',
|
|
852
|
+
kind: token(value.kind, 'file'), size: integer(value.size) ? value.size : 0, mtimeMs: 0,
|
|
853
|
+
scopeId: value.scopeId, parentId, ...(id(value.artifactId) ? { artifactId: value.artifactId } : {}),
|
|
854
|
+
}, limits.artifacts);
|
|
855
|
+
}
|
|
856
|
+
for (const value of (Array.isArray(state.activity) ? state.activity : []).slice(-limits.activity)) {
|
|
857
|
+
if (!integer(value.sequence) || value.sequence > sequence) continue;
|
|
858
|
+
const record = activityRecord(value, value.sequence, Date.parse(value.recordedAt));
|
|
859
|
+
if (record) put(activities, `${value.sequence}`, record, limits.activity);
|
|
860
|
+
}
|
|
861
|
+
for (const value of (Array.isArray(state.sessions) ? state.sessions : []).slice(-limits.sessions)) {
|
|
862
|
+
if (id(value?.id)) put(sessions, value.id, {
|
|
863
|
+
id: value.id, host: token(value.host), status: token(value.status), startedAt: time(value.startedAt, 0),
|
|
864
|
+
}, limits.sessions);
|
|
865
|
+
}
|
|
866
|
+
// History retains recorded validity. It never enters the live evidence
|
|
867
|
+
// registry or gains authority to approve a new decision after restart.
|
|
868
|
+
for (const value of (Array.isArray(state.checkpoints) ? state.checkpoints : []).slice(-limits.checkpoints)) {
|
|
869
|
+
if (!id(value?.id) || value.projectId !== projectId || !plain(value.state) ||
|
|
870
|
+
!integer(value.sequence) || value.sequence > sequence || value.state.sequence !== value.sequence ||
|
|
871
|
+
value.state.revision !== value.revision) continue;
|
|
872
|
+
const restored = restoreFrozenSnapshot(value.state, { projectId, limits });
|
|
873
|
+
if (!restored) continue;
|
|
874
|
+
const marker = { id: value.id, label: label(value.label, 'Checkpoint'), projectId,
|
|
875
|
+
revision: restored.revision, sequence: restored.sequence, at: time(value.at, 0),
|
|
876
|
+
...(id(value.sessionId) ? { sessionId: value.sessionId } : {}) };
|
|
877
|
+
const size = byteSize(restored);
|
|
878
|
+
if (historyBytes + size <= limits.checkpointBytes && bytes + historyBytes + size <= limits.bytes) {
|
|
879
|
+
checkpoints.set(marker.id, { marker, state: restored, bytes: size });
|
|
880
|
+
historyBytes += size;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
restore(restoredState);
|
|
885
|
+
return Object.freeze({
|
|
886
|
+
observeInventory, observeStructure, invalidateArtifacts, observeLegacy, observeInterpretations,
|
|
887
|
+
recordActivity, setSessions, observeLineage, snapshot, checkpoint, changes, stats,
|
|
888
|
+
});
|
|
889
|
+
}
|