@shapeshift-labs/frontier-lang-compiler 0.2.98 → 0.2.100
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/dist/declarations/semantic-edit-bundle.d.ts +90 -0
- package/dist/declarations/semantic-edit-script.d.ts +34 -37
- package/dist/declarations/semantic-lineage.d.ts +63 -34
- package/dist/declarations/semantic-patch-bundle-index.d.ts +3 -0
- package/dist/declarations/semantic-patch-bundle.d.ts +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/index-impl/declarationRecord.js +2 -2
- package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +56 -64
- package/dist/internal/index-impl/replaySemanticEditProjection.js +54 -22
- package/dist/internal/index-impl/semanticEditBundleAdmission.js +220 -0
- package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
- package/dist/internal/index-impl/semanticEditSourceRanges.js +204 -0
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +35 -1
- package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +150 -13
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +28 -1
- package/dist/internal/index-impl/semanticPatchBundleAdmission.js +130 -11
- package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +6 -2
- package/dist/internal/index-impl/semanticPatchBundleRecords.js +65 -126
- package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
- package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
- package/dist/lightweight-dependency-relations.js +113 -7
- package/dist/native-import-utils.js +15 -1
- package/dist/native-region-scanner-js-helpers.js +61 -17
- package/dist/native-region-scanner-js.js +12 -4
- package/dist/semantic-import-regions.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
|
|
3
|
+
export function projectionCoveredContainerOperationIds(operations, workerSourceText) {
|
|
4
|
+
if (typeof workerSourceText !== 'string') return new Set();
|
|
5
|
+
const result = new Set();
|
|
6
|
+
for (const operation of operations ?? []) {
|
|
7
|
+
if (!isProjectionCoverableContainer(operation)) continue;
|
|
8
|
+
if (workerContainerCoveredByInsertedChildren(operation, operations, workerSourceText)) result.add(operation.id);
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function spanOffsets(sourceText, span) {
|
|
14
|
+
if (typeof sourceText !== 'string' || !span) return undefined;
|
|
15
|
+
if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) {
|
|
16
|
+
return { start: span.start, end: span.end };
|
|
17
|
+
}
|
|
18
|
+
if (typeof span.startLine !== 'number') return undefined;
|
|
19
|
+
const lineStarts = [0];
|
|
20
|
+
for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
|
|
21
|
+
const startLine = Math.max(1, span.startLine);
|
|
22
|
+
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
23
|
+
const start = lineStarts[startLine - 1];
|
|
24
|
+
const endLineStart = lineStarts[endLine - 1];
|
|
25
|
+
if (start === undefined || endLineStart === undefined) return undefined;
|
|
26
|
+
const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
|
|
27
|
+
const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
|
|
28
|
+
const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
|
|
29
|
+
return { start: start + startColumn, end: endLineStart + endColumn };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function scopedBodyReplacement(operation, headSourceText, workerSourceText, headOffsets, workerOffsets) {
|
|
33
|
+
if (!isBodyReplacement(operation)) return undefined;
|
|
34
|
+
const head = bodyContentRange(headSourceText, headOffsets);
|
|
35
|
+
const worker = bodyContentRange(workerSourceText, workerOffsets);
|
|
36
|
+
if (!head || !worker) return undefined;
|
|
37
|
+
const headPrefix = headSourceText.slice(headOffsets.start, head.start);
|
|
38
|
+
const workerPrefix = workerSourceText.slice(workerOffsets.start, worker.start);
|
|
39
|
+
const headSuffix = headSourceText.slice(head.end, headOffsets.end);
|
|
40
|
+
const workerSuffix = workerSourceText.slice(worker.end, workerOffsets.end);
|
|
41
|
+
if (headPrefix !== workerPrefix || headSuffix !== workerSuffix) return undefined;
|
|
42
|
+
return { sourceRangeKind: 'body-content', head, worker };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function bodyContentRange(sourceText, range) {
|
|
46
|
+
const pairs = bracePairs(sourceText, range);
|
|
47
|
+
const close = trailingBodyCloseOffset(sourceText, range);
|
|
48
|
+
const pair = close === undefined ? undefined : pairs.find((candidate) => candidate.close === close);
|
|
49
|
+
return pair ? { start: pair.open + 1, end: pair.close } : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function insertionOffset(sourceText, insertion) {
|
|
53
|
+
if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
|
|
54
|
+
const mode = insertion?.mode;
|
|
55
|
+
if (mode === 'file-start') return { ok: true, offset: 0 };
|
|
56
|
+
if (mode === 'file-end') return { ok: true, offset: sourceText.length };
|
|
57
|
+
const range = spanOffsets(sourceText, insertion?.headSpan);
|
|
58
|
+
if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
|
|
59
|
+
if (mode === 'before') return { ok: true, offset: range.start };
|
|
60
|
+
if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
|
|
61
|
+
return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removalRange(sourceText, span) {
|
|
65
|
+
const range = { ...span };
|
|
66
|
+
if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
|
|
67
|
+
else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
68
|
+
return range;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function insertionReplacement(text, sourceText, offset) {
|
|
72
|
+
let replacement = String(text ?? '');
|
|
73
|
+
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
74
|
+
if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
75
|
+
if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
|
|
76
|
+
if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
77
|
+
return replacement;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function afterLineOffset(sourceText, offset) {
|
|
81
|
+
return sourceText[offset] === '\n' ? offset + 1 : offset;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isProjectionCoverableContainer(operation) {
|
|
85
|
+
if (['portable', 'already-applied', 'covered'].includes(operation.status)) return false;
|
|
86
|
+
if (operation.changeKind !== 'modified') return false;
|
|
87
|
+
if (!operation.spans?.worker || !operation.hashes?.baseTextHash) return false;
|
|
88
|
+
const kind = String(operation.anchor?.regionKind ?? operation.regionKind ?? '');
|
|
89
|
+
return kind === 'type' || kind === 'config' || kind === 'content' || kind === 'route' || kind === 'property';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function workerContainerCoveredByInsertedChildren(container, operations, workerSourceText) {
|
|
93
|
+
const containerWorker = spanOffsets(workerSourceText, container.spans?.worker);
|
|
94
|
+
if (!containerWorker) return false;
|
|
95
|
+
const childRanges = (operations ?? [])
|
|
96
|
+
.filter((operation) => operation.id !== container.id)
|
|
97
|
+
.filter((operation) => operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add'))
|
|
98
|
+
.filter((operation) => ['portable', 'already-applied'].includes(operation.status))
|
|
99
|
+
.map((operation) => spanOffsets(workerSourceText, operation.spans?.worker))
|
|
100
|
+
.filter((range) => containedRange(range, containerWorker))
|
|
101
|
+
.map((range) => insertionRemovalRange(workerSourceText, range, containerWorker));
|
|
102
|
+
if (!childRanges.length) return false;
|
|
103
|
+
const stripped = childRanges
|
|
104
|
+
.sort((left, right) => right.start - left.start || right.end - left.end)
|
|
105
|
+
.reduce((text, range) => text.slice(0, range.start - containerWorker.start) + text.slice(range.end - containerWorker.start), workerSourceText.slice(containerWorker.start, containerWorker.end));
|
|
106
|
+
return hashSemanticValue(stripped) === container.hashes.baseTextHash;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function containedRange(inner, outer) {
|
|
110
|
+
return Boolean(inner && outer && outer.start <= inner.start && inner.end <= outer.end);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function insertionRemovalRange(sourceText, span, container) {
|
|
114
|
+
const range = { ...span };
|
|
115
|
+
if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
|
|
116
|
+
else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
117
|
+
return range;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isBodyReplacement(operation) {
|
|
121
|
+
return operation.changeKind === 'modified' && (operation.kind === 'replaceBody' || operation.anchor?.regionKind === 'body');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function trailingBodyCloseOffset(sourceText, range) {
|
|
125
|
+
if (typeof sourceText !== 'string' || !range) return undefined;
|
|
126
|
+
let index = range.end - 1;
|
|
127
|
+
index = previousCodeOffset(sourceText, index, range.start);
|
|
128
|
+
if (sourceText[index] === ';' || sourceText[index] === ',') {
|
|
129
|
+
index -= 1;
|
|
130
|
+
index = previousCodeOffset(sourceText, index, range.start);
|
|
131
|
+
}
|
|
132
|
+
return sourceText[index] === '}' ? index : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function previousCodeOffset(sourceText, index, minIndex) {
|
|
136
|
+
let cursor = index;
|
|
137
|
+
while (cursor >= minIndex) {
|
|
138
|
+
while (cursor >= minIndex && /\s/.test(sourceText[cursor])) cursor -= 1;
|
|
139
|
+
const blockStart = sourceText.lastIndexOf('/*', cursor);
|
|
140
|
+
if (sourceText[cursor] === '/' && sourceText[cursor - 1] === '*' && blockStart >= minIndex) {
|
|
141
|
+
cursor = blockStart - 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const lineStart = Math.max(minIndex, sourceText.lastIndexOf('\n', cursor) + 1);
|
|
145
|
+
const lineComment = sourceText.lastIndexOf('//', cursor);
|
|
146
|
+
if (lineComment >= lineStart) {
|
|
147
|
+
cursor = lineComment - 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
return cursor;
|
|
151
|
+
}
|
|
152
|
+
return cursor;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bracePairs(sourceText, range) {
|
|
156
|
+
if (typeof sourceText !== 'string' || !range || range.end <= range.start) return [];
|
|
157
|
+
const stack = [];
|
|
158
|
+
const pairs = [];
|
|
159
|
+
let quote;
|
|
160
|
+
let escaped = false;
|
|
161
|
+
let lineComment = false;
|
|
162
|
+
let blockComment = false;
|
|
163
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
164
|
+
const char = sourceText[index];
|
|
165
|
+
const next = sourceText[index + 1];
|
|
166
|
+
if (lineComment) {
|
|
167
|
+
if (char === '\n' || char === '\r') lineComment = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (blockComment) {
|
|
171
|
+
if (char === '*' && next === '/') {
|
|
172
|
+
blockComment = false;
|
|
173
|
+
index += 1;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (quote) {
|
|
178
|
+
if (escaped) escaped = false;
|
|
179
|
+
else if (char === '\\') escaped = true;
|
|
180
|
+
else if (char === quote) quote = undefined;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === '/' && next === '/') {
|
|
184
|
+
lineComment = true;
|
|
185
|
+
index += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (char === '/' && next === '*') {
|
|
189
|
+
blockComment = true;
|
|
190
|
+
index += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (char === '\'' || char === '"' || char === '`') {
|
|
194
|
+
quote = char;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (char === '{') stack.push(index);
|
|
198
|
+
else if (char === '}') {
|
|
199
|
+
const open = stack.pop();
|
|
200
|
+
if (open !== undefined) pairs.push({ open, close: index });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return pairs;
|
|
204
|
+
}
|
|
@@ -22,7 +22,11 @@ export function resolveSemanticHistoryRecordLineage(record = {}, eventsOrMap = [
|
|
|
22
22
|
semanticAnchorKeys: resolveIndexKeys(sourceIndex.semanticAnchorKeys, byAnchorKey, options),
|
|
23
23
|
sourcePaths: uniqueStrings([
|
|
24
24
|
...array(sourceIndex.sourcePaths),
|
|
25
|
-
...resolutions.flatMap((resolution) => resolution
|
|
25
|
+
...resolutions.flatMap((resolution) => resolutionSourcePaths(resolution))
|
|
26
|
+
]),
|
|
27
|
+
lineageResolutionIds: uniqueStrings([
|
|
28
|
+
...array(sourceIndex.lineageResolutionIds),
|
|
29
|
+
...resolutions.map((resolution) => resolution.id)
|
|
26
30
|
]),
|
|
27
31
|
lineageEventIds: uniqueStrings([
|
|
28
32
|
...array(sourceIndex.lineageEventIds),
|
|
@@ -105,6 +109,13 @@ function createResolvedHistoryRecord(record, index, resolution) {
|
|
|
105
109
|
inactiveAnchorKeys: resolution.summary.inactiveAnchorKeys,
|
|
106
110
|
blockedAnchorKeys: resolution.summary.blockedAnchorKeys
|
|
107
111
|
},
|
|
112
|
+
lineageResolutionIds: resolution.summary.lineageResolutionIds,
|
|
113
|
+
lineageEventIds: resolution.summary.traversedEventIds,
|
|
114
|
+
terminalEventIds: resolution.summary.terminalEventIds,
|
|
115
|
+
sourcePaths: resolution.summary.sourcePaths,
|
|
116
|
+
evidenceIds: resolution.summary.evidenceIds,
|
|
117
|
+
proofIds: resolution.summary.proofIds,
|
|
118
|
+
reasonCodes: resolution.summary.reasonCodes,
|
|
108
119
|
autoMergeClaim: false,
|
|
109
120
|
semanticEquivalenceClaim: false
|
|
110
121
|
}
|
|
@@ -159,8 +170,11 @@ function summarizeSemanticHistoryLineageResolutions(resolutions, index, anchorIn
|
|
|
159
170
|
deletedAnchorKeys: uniqueStrings(anchorInventory.deleted.map((anchor) => anchor.key)),
|
|
160
171
|
unresolvedAnchorKeys: uniqueStrings(anchorInventory.unresolved.map((anchor) => anchor.key)),
|
|
161
172
|
blockedAnchorKeys: uniqueStrings(anchorInventory.blocked.map((anchor) => anchor.key)),
|
|
173
|
+
lineageResolutionIds: uniqueStrings(resolutions.map((resolution) => resolution.id)),
|
|
162
174
|
traversedEventIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.traversedEventIds)),
|
|
163
175
|
terminalEventIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.terminalEventIds)),
|
|
176
|
+
evidenceIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.evidenceIds)),
|
|
177
|
+
proofIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.proofIds)),
|
|
164
178
|
reasonCodes: uniqueStrings(resolutions.flatMap((resolution) => resolution.reasonCodes))
|
|
165
179
|
};
|
|
166
180
|
}
|
|
@@ -225,8 +239,17 @@ function anchorEntry(anchor, resolution) {
|
|
|
225
239
|
sourcePath: anchor.sourcePath,
|
|
226
240
|
symbolId: anchor.symbolId,
|
|
227
241
|
symbolName: anchor.symbolName,
|
|
242
|
+
sourcePaths: resolutionSourcePaths(resolution, anchor),
|
|
243
|
+
lineageEventIds: uniqueStrings(resolution.traversedEventIds),
|
|
244
|
+
terminalEventIds: uniqueStrings(resolution.terminalEventIds),
|
|
245
|
+
evidenceIds: uniqueStrings(resolution.evidenceIds),
|
|
246
|
+
proofIds: uniqueStrings(resolution.proofIds),
|
|
247
|
+
crdtOperationIds: uniqueStrings(resolution.crdtOperationIds),
|
|
248
|
+
crdtHeads: uniqueStrings(resolution.crdtHeads),
|
|
249
|
+
lineageEventKinds: uniqueStrings(resolution.lineageEventKinds),
|
|
228
250
|
status: resolution.status,
|
|
229
251
|
resolutionId: resolution.id,
|
|
252
|
+
confidence: resolution.confidence,
|
|
230
253
|
reasonCodes: uniqueStrings(resolution.reasonCodes)
|
|
231
254
|
});
|
|
232
255
|
}
|
|
@@ -250,6 +273,17 @@ function uniqueAnchorEntries(entries) {
|
|
|
250
273
|
});
|
|
251
274
|
}
|
|
252
275
|
|
|
276
|
+
function resolutionSourcePaths(resolution, anchor) {
|
|
277
|
+
return uniqueStrings([
|
|
278
|
+
anchor?.sourcePath,
|
|
279
|
+
...array(anchor?.lineageSourcePaths),
|
|
280
|
+
resolution.query?.sourcePath,
|
|
281
|
+
resolution.startAnchor?.sourcePath,
|
|
282
|
+
...array(resolution.sourcePaths),
|
|
283
|
+
...resolution.currentAnchors.flatMap((entry) => [entry.sourcePath, ...array(entry.lineageSourcePaths)])
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
|
|
253
287
|
function array(value) { return value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]; }
|
|
254
288
|
function uniqueStrings(values) { return uniqueRawStrings(array(values).map((value) => String(value ?? '')).filter(Boolean)); }
|
|
255
289
|
function firstString(...values) { return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean); }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{idFragment}from'../../native-import-utils.js';import{semanticOwnershipRegionForDeclaration}from'../../semantic-import-regions.js';import{createSemanticIndexRecord,hashSemanticValue}from'@shapeshift-labs/frontier-lang-kernel';
|
|
1
|
+
import{idFragment,caseSensitiveIdFragment}from'../../native-import-utils.js';import{semanticOwnershipRegionForDeclaration}from'../../semantic-import-regions.js';import{createSemanticIndexRecord,hashSemanticValue}from'@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import{relationPredicateForDeclaration}from'./relationPredicateForDeclaration.js';
|
|
3
3
|
export function semanticIndexFromNativeDeclarations(declarations, input, options) {
|
|
4
4
|
const documentId = `doc_${idFragment(input.sourcePath ?? input.language)}_${idFragment(input.sourceHash)}`;
|
|
@@ -9,7 +9,7 @@ export function semanticIndexFromNativeDeclarations(declarations, input, options
|
|
|
9
9
|
const facts = [];
|
|
10
10
|
const mappings = [];
|
|
11
11
|
for (const declaration of declarations) {
|
|
12
|
-
const symbolId = declaration.symbolId ?? `symbol:${input.language}:${declaration.role === 'import' ? 'import:' : ''}${
|
|
12
|
+
const symbolId = declaration.symbolId ?? `symbol:${input.language}:${declaration.role === 'import' ? 'import:' : ''}${caseSensitiveIdFragment(declaration.name)}`;
|
|
13
13
|
const occurrenceId = `occ_${idFragment(declaration.nativeNode.id)}_${declaration.role ?? 'definition'}`;
|
|
14
14
|
const ownershipRegion = semanticOwnershipRegionForDeclaration(input, {
|
|
15
15
|
...declaration,
|
|
@@ -8,8 +8,12 @@ export function matchExactAnchors(beforeSymbols, afterSymbols) {
|
|
|
8
8
|
const matchedAfterKeys = new Set();
|
|
9
9
|
for (const before of beforeSymbols) {
|
|
10
10
|
const after = afterByKey.get(before.anchor.key);
|
|
11
|
-
if (after
|
|
12
|
-
matched.push({
|
|
11
|
+
if (after) {
|
|
12
|
+
matched.push({
|
|
13
|
+
before: symbolSummary(before),
|
|
14
|
+
after: symbolSummary(after),
|
|
15
|
+
sourceSpanMoved: !anchorsSameLocation(before.anchor, after.anchor)
|
|
16
|
+
});
|
|
13
17
|
matchedAfterKeys.add(after.anchor.key);
|
|
14
18
|
} else {
|
|
15
19
|
unmatchedBefore.push(before);
|
|
@@ -27,21 +31,33 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
|
|
|
27
31
|
const events = [];
|
|
28
32
|
const ambiguous = [];
|
|
29
33
|
const unmatchedBefore = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const rankedByBefore = beforeSymbols.map((before) => ({
|
|
35
|
+
before,
|
|
36
|
+
ranked: rankLineageCandidates(before, afterSymbols, options)
|
|
37
|
+
}));
|
|
38
|
+
const contendersByAfter = afterContenderIndex(rankedByBefore);
|
|
39
|
+
for (const entry of rankedByBefore) {
|
|
40
|
+
const before = entry.before;
|
|
41
|
+
const ranked = entry.ranked.filter((candidate) => !claimedAfter.has(candidate.after.anchor.key));
|
|
36
42
|
const best = ranked[0];
|
|
37
43
|
const runnerUp = ranked[1];
|
|
38
44
|
if (!best) {
|
|
39
45
|
unmatchedBefore.push(before);
|
|
40
46
|
continue;
|
|
41
47
|
}
|
|
48
|
+
const splitTargets = splitLineageCandidates(before, ranked, contendersByAfter, options);
|
|
49
|
+
if (splitTargets.length > 1) {
|
|
50
|
+
for (const candidate of splitTargets) claimedAfter.add(candidate.after.anchor.key);
|
|
51
|
+
events.push(inferredSplitEvent(before, splitTargets, input));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const targetContention = ambiguousTargetContention(before, best, contendersByAfter, options);
|
|
55
|
+
if (targetContention.length > 0) {
|
|
56
|
+
ambiguous.push(ambiguousMatch(before, ranked, ['ambiguous-lineage-candidates', 'ambiguous-target-lineage-candidates']));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
42
59
|
if (runnerUp && best.score.confidence - runnerUp.score.confidence < options.ambiguityMargin) {
|
|
43
60
|
ambiguous.push(ambiguousMatch(before, ranked));
|
|
44
|
-
unmatchedBefore.push(before);
|
|
45
61
|
continue;
|
|
46
62
|
}
|
|
47
63
|
claimedAfter.add(best.after.anchor.key);
|
|
@@ -55,6 +71,14 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
|
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
function rankLineageCandidates(before, afterSymbols, options) {
|
|
75
|
+
return afterSymbols
|
|
76
|
+
.filter((after) => before.anchor.key !== after.anchor.key)
|
|
77
|
+
.map((after) => ({ before, after, score: scoreLineagePair(before, after) }))
|
|
78
|
+
.filter((candidate) => candidate.score.confidence >= options.minConfidence)
|
|
79
|
+
.sort(compareCandidateScores);
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export function symbolSummary(symbol) {
|
|
59
83
|
return {
|
|
60
84
|
key: symbol.anchor.key,
|
|
@@ -71,7 +95,7 @@ export function symbolSummary(symbol) {
|
|
|
71
95
|
};
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
function ambiguousMatch(before, ranked) {
|
|
98
|
+
function ambiguousMatch(before, ranked, reasonCodes = ['ambiguous-lineage-candidates']) {
|
|
75
99
|
return {
|
|
76
100
|
before: symbolSummary(before),
|
|
77
101
|
candidates: ranked.slice(0, 4).map((candidate) => ({
|
|
@@ -79,7 +103,7 @@ function ambiguousMatch(before, ranked) {
|
|
|
79
103
|
confidence: candidate.score.confidence,
|
|
80
104
|
reasons: candidate.score.reasons
|
|
81
105
|
})),
|
|
82
|
-
reasonCodes:
|
|
106
|
+
reasonCodes: uniqueStrings(reasonCodes)
|
|
83
107
|
};
|
|
84
108
|
}
|
|
85
109
|
|
|
@@ -94,7 +118,8 @@ function inferredEvent(before, after, score, input) {
|
|
|
94
118
|
&& before.anchor.symbolName !== after.anchor.symbolName;
|
|
95
119
|
const pathChanged = before.anchor.sourcePath !== after.anchor.sourcePath;
|
|
96
120
|
const spanMoved = JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(after.anchor.sourceSpan ?? null);
|
|
97
|
-
const
|
|
121
|
+
const recreated = !nameChanged && score.reasons.includes('delete-recreate-candidate');
|
|
122
|
+
const eventKind = nameChanged ? 'renamed' : recreated ? 'recreated' : 'moved';
|
|
98
123
|
return createSemanticLineageEvent({
|
|
99
124
|
id: `lineage_inferred_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(after.anchor.key)}`,
|
|
100
125
|
createdAt: input.generatedAt,
|
|
@@ -121,6 +146,53 @@ function inferredEvent(before, after, score, input) {
|
|
|
121
146
|
reasonCodes: score.reasons,
|
|
122
147
|
moved: pathChanged || spanMoved,
|
|
123
148
|
renamed: nameChanged,
|
|
149
|
+
recreated,
|
|
150
|
+
anchorKeyChanged: before.anchor.key !== after.anchor.key,
|
|
151
|
+
autoMergeClaim: false,
|
|
152
|
+
semanticEquivalenceClaim: false
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inferredSplitEvent(before, candidates, input) {
|
|
158
|
+
const targets = candidates.map((candidate) => candidate.after);
|
|
159
|
+
const reasons = uniqueStrings([
|
|
160
|
+
...candidates.flatMap((candidate) => candidate.score.reasons),
|
|
161
|
+
'split-lineage-candidate'
|
|
162
|
+
]);
|
|
163
|
+
const confidence = Math.min(...candidates.map((candidate) => candidate.score.confidence));
|
|
164
|
+
const pathMatch = targets.every((target) => before.anchor.sourcePath === target.anchor.sourcePath);
|
|
165
|
+
const spanMoved = targets.some((target) => (
|
|
166
|
+
before.anchor.sourcePath !== target.anchor.sourcePath
|
|
167
|
+
|| JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(target.anchor.sourceSpan ?? null)
|
|
168
|
+
));
|
|
169
|
+
return createSemanticLineageEvent({
|
|
170
|
+
id: `lineage_split_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(targets.map((target) => target.anchor.key).join('_'))}`,
|
|
171
|
+
createdAt: input.generatedAt,
|
|
172
|
+
eventKind: 'split',
|
|
173
|
+
from: before.anchor,
|
|
174
|
+
to: targets.map((target) => target.anchor),
|
|
175
|
+
confidence,
|
|
176
|
+
actor: input.actor,
|
|
177
|
+
actorId: input.actorId,
|
|
178
|
+
actorRole: input.actorRole ?? 'semantic-lineage-inference',
|
|
179
|
+
operationId: input.operationId,
|
|
180
|
+
deps: input.deps,
|
|
181
|
+
heads: input.heads,
|
|
182
|
+
stateVector: input.stateVector,
|
|
183
|
+
evidenceIds: [input.evidenceId ?? `evidence_${idFragment(input.id ?? before.anchor.key)}_lineage_inference`],
|
|
184
|
+
signatureHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('signature-hash-match')),
|
|
185
|
+
bodyHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('body-hash-match')),
|
|
186
|
+
pathMatch,
|
|
187
|
+
sourceSpanMoved: spanMoved,
|
|
188
|
+
conflictKeys: uniqueStrings([before.anchor.key, ...targets.map((target) => target.anchor.key)]),
|
|
189
|
+
metadata: {
|
|
190
|
+
inferred: true,
|
|
191
|
+
algorithm: 'frontier.semantic-lineage-inference.v1',
|
|
192
|
+
reasonCodes: reasons,
|
|
193
|
+
split: true,
|
|
194
|
+
targetCount: targets.length,
|
|
195
|
+
candidateConfidences: candidates.map((candidate) => candidate.score.confidence),
|
|
124
196
|
autoMergeClaim: false,
|
|
125
197
|
semanticEquivalenceClaim: false
|
|
126
198
|
}
|
|
@@ -134,6 +206,7 @@ function scoreLineagePair(before, after) {
|
|
|
134
206
|
score += value;
|
|
135
207
|
reasons.push(reason);
|
|
136
208
|
};
|
|
209
|
+
const note = (reason) => reasons.push(reason);
|
|
137
210
|
if (before.anchor.key && before.anchor.key === after.anchor.key) add(0.4, 'anchor-key-match');
|
|
138
211
|
if (before.name && before.name === after.name) add(0.28, 'symbol-name-match');
|
|
139
212
|
if (before.kind && before.kind === after.kind) add(0.12, 'symbol-kind-match');
|
|
@@ -145,7 +218,67 @@ function scoreLineagePair(before, after) {
|
|
|
145
218
|
if (before.ownershipRegionKind && before.ownershipRegionKind === after.ownershipRegionKind) add(0.04, 'ownership-kind-match');
|
|
146
219
|
if (before.nativeAstNodeId && before.nativeAstNodeId === after.nativeAstNodeId) add(0.06, 'native-node-id-match');
|
|
147
220
|
if (before.anchor.sourcePath !== after.anchor.sourcePath && (before.name === after.name || before.signatureHash === after.signatureHash)) add(0.04, 'source-path-moved');
|
|
148
|
-
|
|
221
|
+
if (before.anchor.key && after.anchor.key && before.anchor.key !== after.anchor.key) note('anchor-key-changed');
|
|
222
|
+
if (sameSymbolSurface(before, after)) note('same-symbol-surface');
|
|
223
|
+
if (deleteRecreateCandidate(before, after, reasons)) note('delete-recreate-candidate');
|
|
224
|
+
return { confidence: Math.max(0, Math.min(1, Number(score.toFixed(3)))), reasons: uniqueStrings(reasons) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function afterContenderIndex(rankedByBefore) {
|
|
228
|
+
const contenders = new Map();
|
|
229
|
+
for (const entry of rankedByBefore) {
|
|
230
|
+
for (const candidate of entry.ranked) {
|
|
231
|
+
const key = candidate.after.anchor.key;
|
|
232
|
+
if (!key) continue;
|
|
233
|
+
contenders.set(key, [...(contenders.get(key) ?? []), candidate]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return contenders;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function ambiguousTargetContention(before, candidate, contendersByAfter, options) {
|
|
240
|
+
return (contendersByAfter.get(candidate.after.anchor.key) ?? []).filter((contender) => (
|
|
241
|
+
contender.before.anchor.key !== before.anchor.key
|
|
242
|
+
&& candidate.score.confidence - contender.score.confidence < options.ambiguityMargin
|
|
243
|
+
));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function splitLineageCandidates(before, ranked, contendersByAfter, options) {
|
|
247
|
+
const best = ranked[0];
|
|
248
|
+
if (!best) return [];
|
|
249
|
+
const close = ranked.filter((candidate) => best.score.confidence - candidate.score.confidence < options.ambiguityMargin);
|
|
250
|
+
if (close.length < 2 || close.length > 4) return [];
|
|
251
|
+
if (!close.every((candidate) => hasStrongLineageEvidence(candidate.score))) return [];
|
|
252
|
+
if (!close.every((candidate) => splitNameEvidence(before, candidate.after))) return [];
|
|
253
|
+
if (close.some((candidate) => ambiguousTargetContention(before, candidate, contendersByAfter, options).length > 0)) return [];
|
|
254
|
+
return close;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hasStrongLineageEvidence(score) {
|
|
258
|
+
return score.reasons.includes('signature-hash-match') || score.reasons.includes('body-hash-match');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function splitNameEvidence(before, after) {
|
|
262
|
+
const beforeName = normalizedName(before.name);
|
|
263
|
+
const afterName = normalizedName(after.name);
|
|
264
|
+
return Boolean(beforeName && afterName && beforeName !== afterName && afterName.includes(beforeName));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function deleteRecreateCandidate(before, after, reasons) {
|
|
268
|
+
return before.anchor.key !== after.anchor.key
|
|
269
|
+
&& sameSymbolSurface(before, after)
|
|
270
|
+
&& (reasons.includes('signature-hash-match') || reasons.includes('body-hash-match'));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function sameSymbolSurface(before, after) {
|
|
274
|
+
return Boolean(
|
|
275
|
+
before.name
|
|
276
|
+
&& before.name === after.name
|
|
277
|
+
&& before.kind
|
|
278
|
+
&& before.kind === after.kind
|
|
279
|
+
&& before.anchor.sourcePath
|
|
280
|
+
&& before.anchor.sourcePath === after.anchor.sourcePath
|
|
281
|
+
);
|
|
149
282
|
}
|
|
150
283
|
|
|
151
284
|
function anchorsSameLocation(before, after) {
|
|
@@ -162,6 +295,10 @@ function sourceSpanRangeSame(before, after) {
|
|
|
162
295
|
&& before.endColumn === after.endColumn;
|
|
163
296
|
}
|
|
164
297
|
|
|
298
|
+
function normalizedName(value) {
|
|
299
|
+
return String(value ?? '').replace(/[^A-Za-z0-9_$]+/g, '').toLowerCase();
|
|
300
|
+
}
|
|
301
|
+
|
|
165
302
|
function firstString(...values) {
|
|
166
303
|
return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
|
|
167
304
|
}
|
|
@@ -49,6 +49,7 @@ function createResolutionState(start) {
|
|
|
49
49
|
current: start ? [start] : [],
|
|
50
50
|
traversed: [],
|
|
51
51
|
terminal: [],
|
|
52
|
+
sourcePaths: strings(start?.sourcePath),
|
|
52
53
|
conflictKeys: [],
|
|
53
54
|
evidenceIds: [],
|
|
54
55
|
proofIds: [],
|
|
@@ -64,14 +65,16 @@ function createResolutionState(start) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function buildResolutionRecord(state, start, maxDepth, query, options) {
|
|
68
|
+
const currentAnchors = uniqueAnchors(state.current).map((anchor) => anchorWithLineageLinks(anchor, state, start, query));
|
|
67
69
|
const core = {
|
|
68
70
|
kind: 'frontier.lang.semanticLineageResolution',
|
|
69
71
|
version: 1,
|
|
70
72
|
query: compactRecord({ anchorKey: start?.key, anchorId: start?.id, sourcePath: start?.sourcePath, symbolName: start?.symbolName, maxDepth }),
|
|
71
73
|
startAnchor: start,
|
|
72
|
-
currentAnchors
|
|
74
|
+
currentAnchors,
|
|
73
75
|
traversedEventIds: uniqueStrings(state.traversed),
|
|
74
76
|
terminalEventIds: uniqueStrings(state.terminal),
|
|
77
|
+
sourcePaths: lineageSourcePaths(state, start, query, currentAnchors),
|
|
75
78
|
status: state.status,
|
|
76
79
|
confidence: clampConfidence(state.confidence),
|
|
77
80
|
conflictKeys: uniqueStrings(state.conflictKeys),
|
|
@@ -102,6 +105,7 @@ function applyLineageEvent(state, event, visitedEvents) {
|
|
|
102
105
|
state.operationIds.push(event.crdt?.operationId);
|
|
103
106
|
state.heads.push(...(event.crdt?.heads ?? []));
|
|
104
107
|
state.eventKinds.push(event.eventKind);
|
|
108
|
+
state.sourcePaths.push(event.from?.sourcePath, ...event.to.map((anchor) => anchor.sourcePath));
|
|
105
109
|
if (event.confidence !== undefined) state.confidence = state.confidence === undefined ? event.confidence : Math.min(state.confidence, event.confidence);
|
|
106
110
|
const matched = state.current.filter((anchor) => anchorsMatch(anchor, event.from));
|
|
107
111
|
const unmatched = state.current.filter((anchor) => !anchorsMatch(anchor, event.from));
|
|
@@ -178,6 +182,29 @@ function uniqueEvents(events) {
|
|
|
178
182
|
return true;
|
|
179
183
|
});
|
|
180
184
|
}
|
|
185
|
+
function anchorWithLineageLinks(anchor, state, start, query) {
|
|
186
|
+
return compactRecord({
|
|
187
|
+
...anchor,
|
|
188
|
+
lineageEventIds: uniqueStrings(state.traversed),
|
|
189
|
+
terminalLineageEventIds: uniqueStrings(state.terminal),
|
|
190
|
+
lineageSourcePaths: lineageSourcePaths(state, start, query, [anchor]),
|
|
191
|
+
evidenceIds: uniqueStrings(state.evidenceIds),
|
|
192
|
+
proofIds: uniqueStrings(state.proofIds),
|
|
193
|
+
crdtOperationIds: uniqueStrings(state.operationIds),
|
|
194
|
+
crdtHeads: uniqueStrings(state.heads),
|
|
195
|
+
lineageEventKinds: uniqueStrings(state.eventKinds),
|
|
196
|
+
lineageReasonCodes: uniqueStrings(state.reasonCodes)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function lineageSourcePaths(state, start, query, anchors = []) {
|
|
200
|
+
return uniqueStrings([
|
|
201
|
+
query?.sourcePath,
|
|
202
|
+
start?.sourcePath,
|
|
203
|
+
...state.sourcePaths,
|
|
204
|
+
...array(anchors).map((anchor) => anchor?.sourcePath),
|
|
205
|
+
...array(anchors).flatMap((anchor) => anchor?.lineageSourcePaths ?? [])
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
181
208
|
function positiveInteger(value, fallback) {
|
|
182
209
|
const number = Number(value);
|
|
183
210
|
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|