@shapeshift-labs/frontier-lang-compiler 0.2.100 → 0.2.102
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/bidirectional-target-change-evidence.d.ts +299 -0
- package/dist/declarations/bidirectional-target-change.d.ts +19 -120
- package/dist/declarations/import-adapter-core.d.ts +6 -0
- package/dist/declarations/native-project-admission.d.ts +43 -22
- package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
- package/dist/declarations/semantic-edit-script.d.ts +20 -15
- package/dist/declarations/semantic-lineage.d.ts +3 -21
- package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
- package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
- package/dist/declarations/semantic-sidecar.d.ts +12 -14
- package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
- package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
- package/dist/internal/index-impl/createLightweightNativeImport.js +9 -1
- package/dist/internal/index-impl/createNativeSourcePreservation.js +16 -1
- package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +151 -1
- package/dist/internal/index-impl/createSemanticImportSidecar.js +5 -0
- package/dist/internal/index-impl/createSemanticImportSidecarAdmission.js +29 -11
- package/dist/internal/index-impl/importNativeSource.js +14 -14
- package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
- package/dist/internal/index-impl/nativeImportSemanticIndex.js +33 -0
- package/dist/internal/index-impl/projectImportAdmissionMergeScore.js +26 -74
- package/dist/internal/index-impl/projectImportAdmissionProjectionCoverage.js +74 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +39 -13
- package/dist/internal/index-impl/replaySemanticEditProjection.js +65 -23
- package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
- package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
- package/dist/internal/index-impl/semanticEditSourceRanges.js +94 -15
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +21 -2
- package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +8 -0
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +18 -1
- package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
- package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +23 -1
- package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
- package/dist/native-import-language-profiles.js +10 -2
- package/dist/native-region-scanner-js-helpers.js +8 -2
- package/dist/native-region-scanner-js-imports.js +7 -0
- package/dist/native-region-scanner-js.js +4 -4
- package/dist/native-region-scanner.js +2 -1
- package/dist/semantic-import-regions.js +18 -5
- package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
- package/dist/semantic-import-sidecar-entry.js +151 -7
- package/dist/semantic-import-sidecar-types.d.ts +18 -13
- package/dist/semantic-import-source-preservation-utils.js +55 -0
- package/dist/semantic-import-source-preservation.js +98 -3
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const readinessScore = Object.freeze({ ready: 100, 'ready-with-losses': 75, 'needs-review': 45, blocked: 0 });
|
|
2
|
+
|
|
3
|
+
export function targetProjectionCoverageSignals(input) {
|
|
4
|
+
const entries = targetProjectionEntries(input.projectResult, input.imports);
|
|
5
|
+
const matrices = projectionMatrices(input.projectResult, input.imports);
|
|
6
|
+
for (const matrix of matrices) {
|
|
7
|
+
for (const language of matrix?.languages ?? []) entries.push(...(language?.targets ?? []));
|
|
8
|
+
}
|
|
9
|
+
const sourceMapSummary = input.contract?.sourceMaps ?? {};
|
|
10
|
+
const summary = matrices.reduce((current, matrix) => {
|
|
11
|
+
current.exactSourceProjection += matrix?.summary?.exactSourceProjection ?? 0;
|
|
12
|
+
current.targetAdapterProjection += matrix?.summary?.targetAdapterProjection ?? 0;
|
|
13
|
+
current.missingAdapters += matrix?.summary?.missingAdapters ?? 0;
|
|
14
|
+
current.unsupportedTargetFeatures += matrix?.summary?.unsupportedTargetFeatures ?? 0;
|
|
15
|
+
return current;
|
|
16
|
+
}, { exactSourceProjection: 0, targetAdapterProjection: 0, missingAdapters: 0, unsupportedTargetFeatures: 0 });
|
|
17
|
+
const targetEntries = entries.length;
|
|
18
|
+
const supportedTargets = entries.filter((entry) => entry?.supported === true).length;
|
|
19
|
+
const adapterProjectionTargets = entries.filter((entry) =>
|
|
20
|
+
entry?.lossClass === 'targetAdapterProjection' || entry?.lossClass === 'exactSourceProjection' || entry?.adapter || entry?.adapterKind === 'targetProjection'
|
|
21
|
+
).length + summary.targetAdapterProjection + summary.exactSourceProjection;
|
|
22
|
+
const readinessValues = entries.map((entry) => readinessScore[entry?.readiness] ?? 45);
|
|
23
|
+
const readinessAverage = readinessValues.length ? readinessValues.reduce((sum, value) => sum + value, 0) / readinessValues.length : 0;
|
|
24
|
+
return {
|
|
25
|
+
targetEntries,
|
|
26
|
+
supportedTargets,
|
|
27
|
+
adapterProjectionTargets,
|
|
28
|
+
exactSourceProjection: Math.max(summary.exactSourceProjection, input.sourcePreservation.exactSourceAvailable ?? 0),
|
|
29
|
+
targetAdapterProjection: summary.targetAdapterProjection,
|
|
30
|
+
missingAdapters: summary.missingAdapters + entries.filter((entry) => entry?.lossClass === 'missingAdapter' || entry?.supported === false).length,
|
|
31
|
+
unsupportedTargetFeatures: summary.unsupportedTargetFeatures + entries.filter((entry) => entry?.lossClass === 'unsupportedTargetFeatures').length,
|
|
32
|
+
readinessScore: roundScore(readinessAverage),
|
|
33
|
+
sourceMapMappings: sourceMapSummary.mappingCount ?? 0,
|
|
34
|
+
generatedRangeMappings: sourceMapSummary.generatedRangeMappings ?? 0,
|
|
35
|
+
targetPaths: sourceMapSummary.targetPaths?.length ?? 0,
|
|
36
|
+
adapterGeneratedRanges: input.contract?.adapterCoverage?.generatedRanges ?? 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function targetProjectionEntries(projectResult, imports) {
|
|
41
|
+
return [
|
|
42
|
+
projectResult?.targetCoverage,
|
|
43
|
+
projectResult?.metadata?.targetCoverage,
|
|
44
|
+
projectResult?.metadata?.targetProjectionCoverage,
|
|
45
|
+
...(projectResult?.targetCoverages ?? []),
|
|
46
|
+
...(projectResult?.metadata?.targetCoverages ?? []),
|
|
47
|
+
...(imports ?? []).flatMap((imported) => [
|
|
48
|
+
imported?.targetCoverage,
|
|
49
|
+
imported?.metadata?.targetCoverage,
|
|
50
|
+
imported?.metadata?.targetProjectionCoverage,
|
|
51
|
+
...(imported?.targetCoverages ?? []),
|
|
52
|
+
...(imported?.metadata?.targetCoverages ?? [])
|
|
53
|
+
])
|
|
54
|
+
].flatMap((entry) => Array.isArray(entry) ? entry : [entry]).filter((entry) => entry && typeof entry === 'object' && (entry.target || entry.lossClass || entry.supported !== undefined));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectionMatrices(projectResult, imports) {
|
|
58
|
+
return [
|
|
59
|
+
projectResult?.projectionMatrix,
|
|
60
|
+
projectResult?.metadata?.projectionMatrix,
|
|
61
|
+
...(projectResult?.projectionMatrices ?? []),
|
|
62
|
+
...(projectResult?.metadata?.projectionMatrices ?? []),
|
|
63
|
+
...(imports ?? []).flatMap((imported) => [
|
|
64
|
+
imported?.projectionMatrix,
|
|
65
|
+
imported?.metadata?.projectionMatrix,
|
|
66
|
+
...(imported?.projectionMatrices ?? []),
|
|
67
|
+
...(imported?.metadata?.projectionMatrices ?? [])
|
|
68
|
+
])
|
|
69
|
+
].filter((matrix) => matrix?.kind === 'frontier.lang.projectionTargetLossMatrix' || Array.isArray(matrix?.languages));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function roundScore(value) {
|
|
73
|
+
return Math.round((Number.isFinite(value) ? value : 0) * 100) / 100;
|
|
74
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
-
import { idFragment, uniqueStrings } from '../../native-import-utils.js';
|
|
2
|
+
import { idFragment, normalizeNativeLanguageId, uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
|
|
4
|
+
import { mapDiffSymbols } from './mapDiffSymbols.js';
|
|
5
|
+
import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
|
|
3
6
|
import { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
|
|
4
7
|
import {
|
|
5
8
|
insertionOffset,
|
|
@@ -10,7 +13,6 @@ import {
|
|
|
10
13
|
spanOffsets
|
|
11
14
|
} from './semanticEditSourceRanges.js';
|
|
12
15
|
import { applySourceEdits, dedupeSourceEdits, validateSourceEdits } from './semanticSourceEditDedupe.js';
|
|
13
|
-
|
|
14
16
|
export function projectSemanticEditScriptToSource(input = {}) {
|
|
15
17
|
const script = input.script;
|
|
16
18
|
const workerSourceText = input.workerSourceText;
|
|
@@ -19,6 +21,15 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
19
21
|
if (!script) throw new Error('projectSemanticEditScriptToSource requires a script');
|
|
20
22
|
if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
|
|
21
23
|
if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
|
|
24
|
+
const language = normalizeNativeLanguageId(script.language);
|
|
25
|
+
const headSymbols = typeof headSourceText === 'string' && isJavaScriptLike(language)
|
|
26
|
+
? sourceSymbolIndex({
|
|
27
|
+
sourceText: headSourceText,
|
|
28
|
+
sourcePath: input.headSourcePath ?? script.sourcePath,
|
|
29
|
+
language,
|
|
30
|
+
parser: input.parser
|
|
31
|
+
})
|
|
32
|
+
: [];
|
|
22
33
|
const edits = [];
|
|
23
34
|
const coveredOperationIds = [];
|
|
24
35
|
const projectionCoveredOperationIds = projectionCoveredContainerOperationIds(script.operations ?? [], workerSourceText);
|
|
@@ -27,7 +38,10 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
27
38
|
coveredOperationIds.push(operation.id);
|
|
28
39
|
continue;
|
|
29
40
|
}
|
|
30
|
-
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index,
|
|
41
|
+
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index, {
|
|
42
|
+
headSourcePath: input.headSourcePath,
|
|
43
|
+
headSymbols
|
|
44
|
+
});
|
|
31
45
|
if (edit.ok) edits.push(edit.value);
|
|
32
46
|
else reasonCodes.push(...edit.reasonCodes);
|
|
33
47
|
}
|
|
@@ -67,20 +81,20 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
67
81
|
appliedEditCount: deduped.edits.filter((edit) => !edit.alreadyApplied).length,
|
|
68
82
|
alreadyAppliedEditCount: deduped.edits.filter((edit) => edit.alreadyApplied).length,
|
|
69
83
|
dedupedEditCount: deduped.skippedOperationIds.length,
|
|
84
|
+
anchorMode: headSymbols.length ? 'javascript-like-symbols' : 'offsets',
|
|
70
85
|
...input.metadata
|
|
71
86
|
})
|
|
72
87
|
};
|
|
73
88
|
return { ...core, hash: hashSemanticValue(core) };
|
|
74
89
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const identity = projectionIdentity(operation, headSourcePath);
|
|
90
|
+
function sourceEditForOperation(operation, workerSourceText, headSourceText, order, context) {
|
|
91
|
+
const identity = projectionIdentity(operation, context.headSourcePath);
|
|
78
92
|
if (operation.status === 'already-applied') {
|
|
79
93
|
return { ok: true, value: { ...identity, operationId: operation.id, order, start: 0, end: 0, replacement: '', current: '', alreadyApplied: true } };
|
|
80
94
|
}
|
|
81
95
|
if (operation.status !== 'portable') return { ok: false, reasonCodes: [`operation-not-portable:${operation.id}`] };
|
|
82
96
|
if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
|
|
83
|
-
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
|
|
97
|
+
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order, context);
|
|
84
98
|
}
|
|
85
99
|
if (operation.changeKind === 'removed' || String(operation.kind ?? '').startsWith('remove')) {
|
|
86
100
|
return removalEditForOperation(operation, identity, headSourceText, order);
|
|
@@ -131,7 +145,6 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
131
145
|
}
|
|
132
146
|
};
|
|
133
147
|
}
|
|
134
|
-
|
|
135
148
|
function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
136
149
|
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
137
150
|
const reasons = [];
|
|
@@ -158,12 +171,11 @@ function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
|
158
171
|
}
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
|
-
|
|
162
|
-
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
|
|
174
|
+
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order, context) {
|
|
163
175
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
164
176
|
const reasons = [];
|
|
165
177
|
if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
|
|
166
|
-
const insertion = insertionOffset(headSourceText, operation.insertion);
|
|
178
|
+
const insertion = insertionOffset(headSourceText, operation.insertion, { symbols: context.headSymbols });
|
|
167
179
|
if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
|
|
168
180
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
169
181
|
const spanText = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
|
|
@@ -189,7 +201,6 @@ function insertionEditForOperation(operation, identity, workerSourceText, headSo
|
|
|
189
201
|
}
|
|
190
202
|
};
|
|
191
203
|
}
|
|
192
|
-
|
|
193
204
|
function projectionIdentity(operation, headSourcePath) {
|
|
194
205
|
const identity = semanticEditIdentity(operation);
|
|
195
206
|
const sourcePath = operation.reanchor?.toSourcePath ?? headSourcePath ?? operation.insertion?.sourcePath ?? identity.sourcePath;
|
|
@@ -201,7 +212,6 @@ function projectionIdentity(operation, headSourcePath) {
|
|
|
201
212
|
: identity.targetSourcePath;
|
|
202
213
|
return { ...identity, sourcePath, originalSourcePath, targetSourcePath };
|
|
203
214
|
}
|
|
204
|
-
|
|
205
215
|
function projectionEditRecord(edit) {
|
|
206
216
|
const deletedTextHash = hashSemanticValue(edit.current);
|
|
207
217
|
const replacementTextHash = hashSemanticValue(edit.replacement);
|
|
@@ -255,10 +265,25 @@ function projectionEditRecord(edit) {
|
|
|
255
265
|
insertionAnchorKey: edit.insertion?.anchorKey,
|
|
256
266
|
insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
|
|
257
267
|
insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
|
|
268
|
+
insertionAnchorCandidates: edit.insertion?.anchorCandidates,
|
|
258
269
|
replacementText: edit.replacement
|
|
259
270
|
});
|
|
260
271
|
}
|
|
261
272
|
|
|
273
|
+
function sourceSymbolIndex(input) {
|
|
274
|
+
try {
|
|
275
|
+
const imported = normalizeNativeDiffImport({
|
|
276
|
+
language: input.language,
|
|
277
|
+
sourcePath: input.sourcePath,
|
|
278
|
+
sourceText: input.sourceText,
|
|
279
|
+
parser: input.parser
|
|
280
|
+
}, input, 'head');
|
|
281
|
+
return [...mapDiffSymbols(imported, createSemanticImportSidecar(imported)).values()];
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
262
287
|
function semanticEditIdentity(operation) {
|
|
263
288
|
const anchor = operation.anchor ?? {};
|
|
264
289
|
return compactRecord({
|
|
@@ -288,6 +313,7 @@ function projectedSourcePath(script, edits) {
|
|
|
288
313
|
return edits.map((edit) => edit.sourcePath).find(Boolean) ?? script.sourcePath;
|
|
289
314
|
}
|
|
290
315
|
|
|
316
|
+
function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
|
|
291
317
|
function compactRecord(value) {
|
|
292
318
|
return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
|
|
293
319
|
}
|
|
@@ -3,6 +3,7 @@ import { idFragment, normalizeNativeLanguageId, uniqueStrings } from '../../nati
|
|
|
3
3
|
import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
|
|
4
4
|
import { mapDiffSymbols } from './mapDiffSymbols.js';
|
|
5
5
|
import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
|
|
6
|
+
import { replayDiagnostics, replayEditDiagnostics, replayEditsWithOverlapDiagnostics } from './semanticEditReplayDiagnostics.js';
|
|
6
7
|
import { afterLineOffset, bodyContentRange, spanOffsets } from './semanticEditSourceRanges.js';
|
|
7
8
|
|
|
8
9
|
export function replaySemanticEditProjection(input = {}) {
|
|
@@ -17,11 +18,20 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
17
18
|
const currentSymbols = currentSourceText && isJavaScriptLike(language)
|
|
18
19
|
? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
|
|
19
20
|
: [];
|
|
20
|
-
const
|
|
21
|
+
const replayedEdits = projection.status === 'projected' && typeof currentSourceText === 'string'
|
|
21
22
|
? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), { currentSourceText, currentSymbols }))
|
|
22
23
|
: [];
|
|
24
|
+
const edits = replayEditsWithOverlapDiagnostics(replayedEdits);
|
|
23
25
|
const status = replayStatus(reasonCodes, edits, projection);
|
|
24
26
|
const outputSourceText = replayOutputSource(status, currentSourceText, edits);
|
|
27
|
+
const diagnostics = replayDiagnostics({
|
|
28
|
+
status,
|
|
29
|
+
reasonCodes,
|
|
30
|
+
edits,
|
|
31
|
+
sourcePath,
|
|
32
|
+
currentHash,
|
|
33
|
+
expectedCurrentHash: input.currentSourceHash
|
|
34
|
+
});
|
|
25
35
|
const core = {
|
|
26
36
|
kind: 'frontier.lang.semanticEditReplay',
|
|
27
37
|
version: 1,
|
|
@@ -38,6 +48,7 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
38
48
|
edits,
|
|
39
49
|
appliedOperations: edits.filter((edit) => edit.status === 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
40
50
|
skippedOperations: edits.filter((edit) => edit.status !== 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
51
|
+
diagnostics,
|
|
41
52
|
admission: replayAdmission(status, reasonCodes, edits),
|
|
42
53
|
outputSourceText,
|
|
43
54
|
summary: replaySummary(edits, reasonCodes),
|
|
@@ -52,8 +63,8 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
function replayProjectionEdit(edit, context) {
|
|
55
|
-
if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied']);
|
|
56
|
-
if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text']);
|
|
66
|
+
if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied'], context.currentSourceText);
|
|
67
|
+
if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text'], context.currentSourceText);
|
|
57
68
|
if (edit.editKind === 'insert') return replayInsertionEdit(edit, context);
|
|
58
69
|
const headRange = { start: edit.headStart, end: edit.headEnd };
|
|
59
70
|
const offset = checkRange(edit, headRange, context.currentSourceText, 'head-offset');
|
|
@@ -61,34 +72,34 @@ function replayProjectionEdit(edit, context) {
|
|
|
61
72
|
const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
|
|
62
73
|
if (symbol && spanRange && !sameRange(headRange, spanRange)) {
|
|
63
74
|
const moved = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
64
|
-
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol']);
|
|
75
|
+
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
65
76
|
if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
|
|
66
|
-
return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
|
|
77
|
+
return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
67
78
|
}
|
|
68
|
-
return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`]);
|
|
79
|
+
return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`], context.currentSourceText);
|
|
69
80
|
}
|
|
70
|
-
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
|
|
81
|
+
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
71
82
|
const anchored = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
72
|
-
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol']);
|
|
83
|
+
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
73
84
|
return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
|
|
74
85
|
symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
|
|
75
|
-
]);
|
|
86
|
+
], context.currentSourceText);
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
function replayInsertionEdit(edit, context) {
|
|
79
90
|
const inserted = findCurrentSymbol(edit, context.currentSymbols);
|
|
80
91
|
const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
|
|
81
92
|
const already = checkRange(edit, insertedRange, context.currentSourceText, 'current-inserted-symbol');
|
|
82
|
-
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason]);
|
|
93
|
+
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason], context.currentSourceText);
|
|
83
94
|
if (inserted && insertedRange) {
|
|
84
|
-
return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch']);
|
|
95
|
+
return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch'], context.currentSourceText);
|
|
85
96
|
}
|
|
86
|
-
const anchor =
|
|
87
|
-
const range = insertionRange(edit, anchor, context.currentSourceText);
|
|
88
|
-
if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`]);
|
|
97
|
+
const anchor = findInsertionAnchor(edit, context.currentSymbols);
|
|
98
|
+
const range = insertionRange(edit, anchor?.candidate, anchor?.symbol, context.currentSourceText);
|
|
99
|
+
if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`], context.currentSourceText);
|
|
89
100
|
return replayEditRecord(edit, anchor ? 'conflict' : 'stale', undefined, [
|
|
90
101
|
anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
|
|
91
|
-
]);
|
|
102
|
+
], context.currentSourceText);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
function checkRange(edit, range, sourceText, label) {
|
|
@@ -102,7 +113,8 @@ function checkRange(edit, range, sourceText, label) {
|
|
|
102
113
|
return undefined;
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
function replayEditRecord(edit, status, range, reasonCodes) {
|
|
116
|
+
function replayEditRecord(edit, status, range, reasonCodes, sourceText) {
|
|
117
|
+
const normalizedReasonCodes = reasonList(reasonCodes);
|
|
106
118
|
return compactRecord({
|
|
107
119
|
operationId: edit.operationId,
|
|
108
120
|
semanticKey: edit.semanticKey,
|
|
@@ -120,7 +132,8 @@ function replayEditRecord(edit, status, range, reasonCodes) {
|
|
|
120
132
|
end: range?.end,
|
|
121
133
|
replacementBytes: edit.replacementBytes,
|
|
122
134
|
replacementText: edit.replacementText,
|
|
123
|
-
reasonCodes:
|
|
135
|
+
reasonCodes: normalizedReasonCodes,
|
|
136
|
+
diagnostics: replayEditDiagnostics(edit, status, range, normalizedReasonCodes, sourceText)
|
|
124
137
|
});
|
|
125
138
|
}
|
|
126
139
|
|
|
@@ -146,18 +159,47 @@ function findCurrentSymbol(edit, symbols) {
|
|
|
146
159
|
return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
function findInsertionAnchor(edit, symbols) {
|
|
163
|
+
for (const candidate of insertionAnchorCandidates(edit)) {
|
|
164
|
+
const symbol = findInsertionAnchorSymbol(candidate, symbols);
|
|
165
|
+
if (symbol) return { candidate, symbol };
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findInsertionAnchorSymbol(candidate, symbols) {
|
|
171
|
+
const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
|
|
172
|
+
return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)))
|
|
173
|
+
?? symbols.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function insertionAnchorCandidates(edit) {
|
|
177
|
+
const primary = {
|
|
178
|
+
mode: edit.insertionMode,
|
|
179
|
+
anchorKey: edit.insertionAnchorKey,
|
|
180
|
+
anchorSymbolName: edit.insertionAnchorSymbolName,
|
|
181
|
+
anchorSymbolKind: edit.insertionAnchorSymbolKind
|
|
182
|
+
};
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
const result = [];
|
|
185
|
+
for (const candidate of [primary, ...(Array.isArray(edit.insertionAnchorCandidates) ? edit.insertionAnchorCandidates : [])]) {
|
|
186
|
+
if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
|
|
187
|
+
const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
|
|
188
|
+
if (seen.has(key)) continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
result.push(candidate);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
152
193
|
}
|
|
153
194
|
|
|
154
|
-
function insertionRange(edit, anchor, sourceText) {
|
|
195
|
+
function insertionRange(edit, candidate, anchor, sourceText) {
|
|
155
196
|
if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
|
|
156
197
|
if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
|
|
198
|
+
const mode = candidate?.mode ?? edit.insertionMode;
|
|
157
199
|
const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
|
|
158
200
|
if (!anchorRange) return undefined;
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
201
|
+
if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
|
|
202
|
+
if (mode === 'after') {
|
|
161
203
|
return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
|
|
162
204
|
}
|
|
163
205
|
return undefined;
|
|
@@ -7,13 +7,16 @@ export function semanticEditInsertionAnchor(region, context) {
|
|
|
7
7
|
.filter((symbol) => hasSymbol(context.baseSymbols, symbol));
|
|
8
8
|
const before = nearestBefore(workers, workerSymbol);
|
|
9
9
|
const after = nearestAfter(workers, workerSymbol);
|
|
10
|
-
const
|
|
11
|
-
? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol')
|
|
12
|
-
:
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const anchorCandidates = [
|
|
11
|
+
before ? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol') : undefined,
|
|
12
|
+
after ? insertionFromSymbol('before', after, context, 'nearest-next-base-symbol') : undefined
|
|
13
|
+
].filter(Boolean);
|
|
14
|
+
const anchor = anchorCandidates.find((candidate) => candidate.headSpan)
|
|
15
|
+
?? anchorCandidates[0]
|
|
16
|
+
?? fallbackInsertion(region, context, 'no-neighbor-base-symbol');
|
|
15
17
|
return compactRecord({
|
|
16
18
|
...anchor,
|
|
19
|
+
anchorCandidates,
|
|
17
20
|
insertedSymbolId: workerSymbol.id,
|
|
18
21
|
insertedSymbolName: workerSymbol.name,
|
|
19
22
|
insertedSymbolKind: workerSymbol.kind,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
|
|
4
|
+
export function replayEditDiagnostics(edit, status, range, reasonCodes, sourceText) {
|
|
5
|
+
if (status === 'applied' || status === 'already-applied') return [];
|
|
6
|
+
return reasonCodes.map((code) => replayDiagnostic(code, {
|
|
7
|
+
scope: 'edit',
|
|
8
|
+
status,
|
|
9
|
+
operationId: edit.operationId,
|
|
10
|
+
sourcePath: edit.targetSourcePath ?? edit.sourcePath,
|
|
11
|
+
symbolName: edit.targetSymbolName ?? edit.symbolName,
|
|
12
|
+
symbolKind: edit.targetSymbolKind ?? edit.symbolKind,
|
|
13
|
+
editKind: edit.editKind,
|
|
14
|
+
start: range?.start,
|
|
15
|
+
end: range?.end,
|
|
16
|
+
expectedHash: replayDiagnosticExpectedHash(code, edit),
|
|
17
|
+
actualHash: replayDiagnosticActualHash(range, sourceText),
|
|
18
|
+
replacementHash: edit.replacementTextHash ?? edit.replacementSpanTextHash
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function replayEditsWithOverlapDiagnostics(edits) {
|
|
23
|
+
const overlapDiagnostics = new Map();
|
|
24
|
+
const ordered = edits
|
|
25
|
+
.filter((edit) => edit.status === 'applied' && hasNumericReplayRange(edit))
|
|
26
|
+
.sort((left, right) => left.start - right.start || left.end - right.end || (left.editOrder ?? 0) - (right.editOrder ?? 0));
|
|
27
|
+
for (let leftIndex = 0; leftIndex < ordered.length; leftIndex += 1) {
|
|
28
|
+
for (let rightIndex = leftIndex + 1; rightIndex < ordered.length; rightIndex += 1) {
|
|
29
|
+
const left = ordered[leftIndex];
|
|
30
|
+
const right = ordered[rightIndex];
|
|
31
|
+
if (!rangesOverlap(left, right)) continue;
|
|
32
|
+
const operationIds = [left.operationId, right.operationId].filter(Boolean);
|
|
33
|
+
const fallbackId = `${left.editOrder ?? leftIndex}:${right.editOrder ?? rightIndex}`;
|
|
34
|
+
const code = `replay-edit-overlap:${operationIds.join(':') || fallbackId}`;
|
|
35
|
+
appendOverlapDiagnostic(overlapDiagnostics, left, code, operationIds);
|
|
36
|
+
appendOverlapDiagnostic(overlapDiagnostics, right, code, operationIds);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!overlapDiagnostics.size) return edits;
|
|
40
|
+
return edits.map((edit) => {
|
|
41
|
+
const diagnostics = overlapDiagnostics.get(edit);
|
|
42
|
+
if (!diagnostics) return edit;
|
|
43
|
+
return diagnostics.reduce((record, diagnostic) => appendReplayEditDiagnostic(record, diagnostic), edit);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function replayDiagnostics(input) {
|
|
48
|
+
return uniqueDiagnostics([
|
|
49
|
+
...reasonList(input.reasonCodes).map((code) => replayDiagnostic(code, {
|
|
50
|
+
scope: 'replay',
|
|
51
|
+
status: input.status,
|
|
52
|
+
sourcePath: input.sourcePath,
|
|
53
|
+
expectedHash: code === 'current-source-hash-mismatch' ? input.expectedCurrentHash : undefined,
|
|
54
|
+
actualHash: code === 'current-source-hash-mismatch' ? input.currentHash : undefined
|
|
55
|
+
})),
|
|
56
|
+
...input.edits.flatMap((edit) => edit.diagnostics ?? [])
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function appendOverlapDiagnostic(overlapDiagnostics, edit, code, operationIds) {
|
|
61
|
+
const diagnostics = overlapDiagnostics.get(edit) ?? [];
|
|
62
|
+
diagnostics.push(replayDiagnostic(code, {
|
|
63
|
+
scope: 'edit',
|
|
64
|
+
status: 'conflict',
|
|
65
|
+
operationId: edit.operationId,
|
|
66
|
+
sourcePath: edit.sourcePath,
|
|
67
|
+
symbolName: edit.symbolName,
|
|
68
|
+
symbolKind: edit.symbolKind,
|
|
69
|
+
editKind: edit.editKind,
|
|
70
|
+
start: edit.start,
|
|
71
|
+
end: edit.end,
|
|
72
|
+
overlapOperationIds: operationIds
|
|
73
|
+
}));
|
|
74
|
+
overlapDiagnostics.set(edit, diagnostics);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function appendReplayEditDiagnostic(edit, diagnostic) {
|
|
78
|
+
return compactRecord({
|
|
79
|
+
...edit,
|
|
80
|
+
status: edit.status === 'applied' ? 'conflict' : edit.status,
|
|
81
|
+
reasonCodes: reasonList([...(edit.reasonCodes ?? []), diagnostic.code]),
|
|
82
|
+
diagnostics: uniqueDiagnostics([...(edit.diagnostics ?? []), diagnostic])
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replayDiagnostic(code, context) {
|
|
87
|
+
const category = replayDiagnosticCategory(code, context.status);
|
|
88
|
+
return compactRecord({
|
|
89
|
+
code,
|
|
90
|
+
category,
|
|
91
|
+
severity: replayDiagnosticSeverity(code, category, context.status),
|
|
92
|
+
scope: context.scope,
|
|
93
|
+
status: context.status,
|
|
94
|
+
operationId: context.operationId,
|
|
95
|
+
sourcePath: context.sourcePath,
|
|
96
|
+
symbolName: context.symbolName,
|
|
97
|
+
symbolKind: context.symbolKind,
|
|
98
|
+
editKind: context.editKind,
|
|
99
|
+
start: context.start,
|
|
100
|
+
end: context.end,
|
|
101
|
+
expectedHash: context.expectedHash,
|
|
102
|
+
actualHash: context.actualHash,
|
|
103
|
+
replacementHash: context.replacementHash,
|
|
104
|
+
overlapOperationIds: context.overlapOperationIds
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function replayDiagnosticCategory(code, status) {
|
|
109
|
+
if (code.includes('overlap')) return 'overlap';
|
|
110
|
+
if (code.startsWith('missing-current-source') || code.startsWith('missing-head-source') || code.startsWith('missing-worker-source')) return 'missing-source';
|
|
111
|
+
if (code === 'current-symbol-anchor-missing' || code.includes('anchor-missing') || code.includes('anchor-unusable')) return 'stale-anchor';
|
|
112
|
+
if (code.includes('content-mismatch') || code.includes('hash-mismatch') || code.includes('span-not-resolvable') || code.startsWith('projection-not-') || code === 'missing-replacement-text') return 'projection-mismatch';
|
|
113
|
+
if (code.includes('reanchored')) return 'reanchored';
|
|
114
|
+
if (code.includes('matches-')) return 'matched-source';
|
|
115
|
+
if (status === 'stale') return 'stale-anchor';
|
|
116
|
+
if (status === 'conflict' || status === 'blocked') return 'projection-mismatch';
|
|
117
|
+
return 'replay';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function replayDiagnosticSeverity(code, category, status) {
|
|
121
|
+
if (code === 'current-source-hash-mismatch' && (status === 'accepted-clean' || status === 'already-applied')) return 'warning';
|
|
122
|
+
if (category === 'matched-source' || category === 'reanchored' || status === 'applied' || status === 'already-applied') return 'info';
|
|
123
|
+
if (category === 'overlap' || category === 'missing-source' || category === 'stale-anchor' || category === 'projection-mismatch') return 'error';
|
|
124
|
+
return status === 'accepted-clean' ? 'info' : 'warning';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replayDiagnosticExpectedHash(code, edit) {
|
|
128
|
+
if (code.includes('matches-replacement')) return edit.replacementTextHash ?? edit.replacementSpanTextHash;
|
|
129
|
+
if (code.includes('content-mismatch') || code.includes('matches-deleted')) return edit.deletedTextHash ?? edit.anchorDeletedTextHash;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function replayDiagnosticActualHash(range, sourceText) {
|
|
134
|
+
if (!range || typeof sourceText !== 'string') return undefined;
|
|
135
|
+
return hashSemanticValue(sourceText.slice(range.start, range.end));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function uniqueDiagnostics(diagnostics) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const diagnostic of diagnostics.filter(Boolean)) {
|
|
142
|
+
const key = [
|
|
143
|
+
diagnostic.scope,
|
|
144
|
+
diagnostic.operationId,
|
|
145
|
+
diagnostic.status,
|
|
146
|
+
diagnostic.code,
|
|
147
|
+
diagnostic.start,
|
|
148
|
+
diagnostic.end,
|
|
149
|
+
(diagnostic.overlapOperationIds ?? []).join('|')
|
|
150
|
+
].join(':');
|
|
151
|
+
if (seen.has(key)) continue;
|
|
152
|
+
seen.add(key);
|
|
153
|
+
result.push(diagnostic);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rangesOverlap(left, right) {
|
|
159
|
+
return Boolean(left && right && left.start < right.end && right.start < left.end);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasNumericReplayRange(edit) {
|
|
163
|
+
return typeof edit.start === 'number' && typeof edit.end === 'number';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function reasonList(values) { return uniqueStrings((values ?? []).filter(Boolean)); }
|
|
167
|
+
function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
|