@shapeshift-labs/frontier-lang-compiler 0.2.99 → 0.2.101
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/native-project-admission.d.ts +43 -22
- package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
- package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
- package/dist/declarations/semantic-edit-script.d.ts +53 -51
- package/dist/declarations/semantic-lineage.d.ts +62 -51
- package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
- package/dist/declarations/semantic-patch-bundle.d.ts +13 -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/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/declarationRecord.js +2 -2
- package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
- package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
- 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 +92 -74
- package/dist/internal/index-impl/replaySemanticEditProjection.js +114 -40
- package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
- package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
- 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 +283 -0
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +56 -3
- package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
- package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +158 -13
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +46 -2
- package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
- package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
- package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
- package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +29 -3
- package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
- package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
- package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
- package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
- package/dist/lightweight-dependency-relations.js +113 -7
- package/dist/native-import-language-profiles.js +10 -2
- package/dist/native-import-utils.js +15 -1
- package/dist/native-region-scanner-js-helpers.js +68 -18
- package/dist/native-region-scanner-js-imports.js +7 -0
- package/dist/native-region-scanner-js.js +16 -8
- package/dist/native-region-scanner.js +2 -1
- package/dist/semantic-import-regions.js +8 -6
- 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
|
@@ -1,30 +1,55 @@
|
|
|
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';
|
|
7
|
+
import {
|
|
8
|
+
insertionOffset,
|
|
9
|
+
insertionReplacement,
|
|
10
|
+
projectionCoveredContainerOperationIds,
|
|
11
|
+
removalRange,
|
|
12
|
+
scopedBodyReplacement,
|
|
13
|
+
spanOffsets
|
|
14
|
+
} from './semanticEditSourceRanges.js';
|
|
4
15
|
import { applySourceEdits, dedupeSourceEdits, validateSourceEdits } from './semanticSourceEditDedupe.js';
|
|
5
|
-
|
|
6
16
|
export function projectSemanticEditScriptToSource(input = {}) {
|
|
7
17
|
const script = input.script;
|
|
8
18
|
const workerSourceText = input.workerSourceText;
|
|
9
19
|
const headSourceText = input.headSourceText;
|
|
10
20
|
const reasonCodes = [];
|
|
11
21
|
if (!script) throw new Error('projectSemanticEditScriptToSource requires a script');
|
|
12
|
-
if (script.admission?.status !== 'auto-merge-candidate') reasonCodes.push('script-not-auto-merge-candidate');
|
|
13
22
|
if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
|
|
14
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
|
+
: [];
|
|
15
33
|
const edits = [];
|
|
16
34
|
const coveredOperationIds = [];
|
|
35
|
+
const projectionCoveredOperationIds = projectionCoveredContainerOperationIds(script.operations ?? [], workerSourceText);
|
|
17
36
|
for (const [index, operation] of (script.operations ?? []).entries()) {
|
|
18
|
-
if (operation.status === 'covered') {
|
|
37
|
+
if (operation.status === 'covered' || projectionCoveredOperationIds.has(operation.id)) {
|
|
19
38
|
coveredOperationIds.push(operation.id);
|
|
20
39
|
continue;
|
|
21
40
|
}
|
|
22
|
-
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index
|
|
41
|
+
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index, {
|
|
42
|
+
headSourcePath: input.headSourcePath,
|
|
43
|
+
headSymbols
|
|
44
|
+
});
|
|
23
45
|
if (edit.ok) edits.push(edit.value);
|
|
24
46
|
else reasonCodes.push(...edit.reasonCodes);
|
|
25
47
|
}
|
|
26
48
|
const deduped = dedupeSourceEdits(edits);
|
|
27
49
|
reasonCodes.push(...validateSourceEdits(deduped.edits));
|
|
50
|
+
if (script.admission?.status !== 'auto-merge-candidate' && (reasonCodes.length > 0 || (!edits.length && !coveredOperationIds.length))) {
|
|
51
|
+
reasonCodes.push('script-not-auto-merge-candidate');
|
|
52
|
+
}
|
|
28
53
|
const blocked = reasonCodes.length > 0;
|
|
29
54
|
const sourceText = blocked ? undefined : applySourceEdits(headSourceText, deduped.edits);
|
|
30
55
|
const core = {
|
|
@@ -56,20 +81,20 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
56
81
|
appliedEditCount: deduped.edits.filter((edit) => !edit.alreadyApplied).length,
|
|
57
82
|
alreadyAppliedEditCount: deduped.edits.filter((edit) => edit.alreadyApplied).length,
|
|
58
83
|
dedupedEditCount: deduped.skippedOperationIds.length,
|
|
84
|
+
anchorMode: headSymbols.length ? 'javascript-like-symbols' : 'offsets',
|
|
59
85
|
...input.metadata
|
|
60
86
|
})
|
|
61
87
|
};
|
|
62
88
|
return { ...core, hash: hashSemanticValue(core) };
|
|
63
89
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const identity = projectionIdentity(operation);
|
|
90
|
+
function sourceEditForOperation(operation, workerSourceText, headSourceText, order, context) {
|
|
91
|
+
const identity = projectionIdentity(operation, context.headSourcePath);
|
|
67
92
|
if (operation.status === 'already-applied') {
|
|
68
93
|
return { ok: true, value: { ...identity, operationId: operation.id, order, start: 0, end: 0, replacement: '', current: '', alreadyApplied: true } };
|
|
69
94
|
}
|
|
70
95
|
if (operation.status !== 'portable') return { ok: false, reasonCodes: [`operation-not-portable:${operation.id}`] };
|
|
71
96
|
if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
|
|
72
|
-
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
|
|
97
|
+
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order, context);
|
|
73
98
|
}
|
|
74
99
|
if (operation.changeKind === 'removed' || String(operation.kind ?? '').startsWith('remove')) {
|
|
75
100
|
return removalEditForOperation(operation, identity, headSourceText, order);
|
|
@@ -80,16 +105,23 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
80
105
|
if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
|
|
81
106
|
if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
|
|
82
107
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
if (operation.hashes?.workerTextHash && hashSemanticValue(
|
|
108
|
+
const anchorReplacement = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
|
|
109
|
+
const anchorCurrent = headSourceText.slice(headOffsets.start, headOffsets.end);
|
|
110
|
+
if (operation.hashes?.workerTextHash && hashSemanticValue(anchorReplacement) !== operation.hashes.workerTextHash) {
|
|
86
111
|
reasons.push(`worker-span-hash-mismatch:${operation.id}`);
|
|
87
112
|
}
|
|
88
113
|
const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
|
|
89
|
-
if (expectedHeadHash && hashSemanticValue(
|
|
114
|
+
if (expectedHeadHash && hashSemanticValue(anchorCurrent) !== expectedHeadHash) {
|
|
90
115
|
reasons.push(`head-span-hash-mismatch:${operation.id}`);
|
|
91
116
|
}
|
|
92
117
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
118
|
+
const scoped = scopedBodyReplacement(operation, headSourceText, workerSourceText, headOffsets, workerOffsets);
|
|
119
|
+
const replacement = scoped
|
|
120
|
+
? workerSourceText.slice(scoped.worker.start, scoped.worker.end)
|
|
121
|
+
: anchorReplacement;
|
|
122
|
+
const current = scoped
|
|
123
|
+
? headSourceText.slice(scoped.head.start, scoped.head.end)
|
|
124
|
+
: anchorCurrent;
|
|
93
125
|
return {
|
|
94
126
|
ok: true,
|
|
95
127
|
value: {
|
|
@@ -97,16 +129,22 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
97
129
|
order,
|
|
98
130
|
...identity,
|
|
99
131
|
editKind: 'replace',
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
132
|
+
sourceRangeKind: scoped?.sourceRangeKind,
|
|
133
|
+
start: scoped?.head.start ?? headOffsets.start,
|
|
134
|
+
end: scoped?.head.end ?? headOffsets.end,
|
|
135
|
+
workerStart: scoped?.worker.start ?? workerOffsets.start,
|
|
136
|
+
workerEnd: scoped?.worker.end ?? workerOffsets.end,
|
|
137
|
+
headAnchorStart: scoped ? headOffsets.start : undefined,
|
|
138
|
+
headAnchorEnd: scoped ? headOffsets.end : undefined,
|
|
139
|
+
workerAnchorStart: scoped ? workerOffsets.start : undefined,
|
|
140
|
+
workerAnchorEnd: scoped ? workerOffsets.end : undefined,
|
|
141
|
+
anchorDeletedTextHash: scoped ? hashSemanticValue(anchorCurrent) : undefined,
|
|
142
|
+
anchorReplacementTextHash: scoped ? hashSemanticValue(anchorReplacement) : undefined,
|
|
104
143
|
replacement,
|
|
105
144
|
current
|
|
106
145
|
}
|
|
107
146
|
};
|
|
108
147
|
}
|
|
109
|
-
|
|
110
148
|
function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
111
149
|
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
112
150
|
const reasons = [];
|
|
@@ -133,12 +171,11 @@ function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
|
133
171
|
}
|
|
134
172
|
};
|
|
135
173
|
}
|
|
136
|
-
|
|
137
|
-
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
|
|
174
|
+
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order, context) {
|
|
138
175
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
139
176
|
const reasons = [];
|
|
140
177
|
if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
|
|
141
|
-
const insertion = insertionOffset(headSourceText, operation.insertion);
|
|
178
|
+
const insertion = insertionOffset(headSourceText, operation.insertion, { symbols: context.headSymbols });
|
|
142
179
|
if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
|
|
143
180
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
144
181
|
const spanText = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
|
|
@@ -164,12 +201,17 @@ function insertionEditForOperation(operation, identity, workerSourceText, headSo
|
|
|
164
201
|
}
|
|
165
202
|
};
|
|
166
203
|
}
|
|
167
|
-
|
|
168
|
-
function projectionIdentity(operation) {
|
|
204
|
+
function projectionIdentity(operation, headSourcePath) {
|
|
169
205
|
const identity = semanticEditIdentity(operation);
|
|
170
|
-
|
|
206
|
+
const sourcePath = operation.reanchor?.toSourcePath ?? headSourcePath ?? operation.insertion?.sourcePath ?? identity.sourcePath;
|
|
207
|
+
const originalSourcePath = sourcePath && identity.sourcePath && sourcePath !== identity.sourcePath
|
|
208
|
+
? identity.sourcePath
|
|
209
|
+
: identity.originalSourcePath;
|
|
210
|
+
const targetSourcePath = sourcePath && sourcePath !== identity.sourcePath
|
|
211
|
+
? sourcePath
|
|
212
|
+
: identity.targetSourcePath;
|
|
213
|
+
return { ...identity, sourcePath, originalSourcePath, targetSourcePath };
|
|
171
214
|
}
|
|
172
|
-
|
|
173
215
|
function projectionEditRecord(edit) {
|
|
174
216
|
const deletedTextHash = hashSemanticValue(edit.current);
|
|
175
217
|
const replacementTextHash = hashSemanticValue(edit.replacement);
|
|
@@ -197,27 +239,51 @@ function projectionEditRecord(edit) {
|
|
|
197
239
|
operationContentHash: edit.operationContentHash,
|
|
198
240
|
editContentHash: hashSemanticValue(compactRecord({
|
|
199
241
|
semanticIdentityHash: identity.semanticIdentityHash,
|
|
242
|
+
sourceRangeKind: edit.sourceRangeKind,
|
|
200
243
|
deletedTextHash,
|
|
201
244
|
replacementTextHash,
|
|
202
245
|
status: edit.alreadyApplied ? 'already-applied' : 'applied'
|
|
203
246
|
})),
|
|
247
|
+
sourceRangeKind: edit.sourceRangeKind,
|
|
204
248
|
headStart: edit.start,
|
|
205
249
|
headEnd: edit.end,
|
|
206
250
|
workerStart: edit.workerStart,
|
|
207
251
|
workerEnd: edit.workerEnd,
|
|
252
|
+
editOrder: edit.order,
|
|
253
|
+
headAnchorStart: edit.headAnchorStart,
|
|
254
|
+
headAnchorEnd: edit.headAnchorEnd,
|
|
255
|
+
workerAnchorStart: edit.workerAnchorStart,
|
|
256
|
+
workerAnchorEnd: edit.workerAnchorEnd,
|
|
208
257
|
deletedBytes: edit.current.length,
|
|
209
258
|
replacementBytes: edit.replacement.length,
|
|
210
259
|
deletedTextHash,
|
|
211
260
|
replacementTextHash,
|
|
261
|
+
anchorDeletedTextHash: edit.anchorDeletedTextHash,
|
|
262
|
+
anchorReplacementTextHash: edit.anchorReplacementTextHash,
|
|
212
263
|
replacementSpanTextHash: hashSemanticValue(edit.replacementSpanText ?? edit.replacement),
|
|
213
264
|
insertionMode: edit.insertion?.mode,
|
|
214
265
|
insertionAnchorKey: edit.insertion?.anchorKey,
|
|
215
266
|
insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
|
|
216
267
|
insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
|
|
268
|
+
insertionAnchorCandidates: edit.insertion?.anchorCandidates,
|
|
217
269
|
replacementText: edit.replacement
|
|
218
270
|
});
|
|
219
271
|
}
|
|
220
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
|
+
|
|
221
287
|
function semanticEditIdentity(operation) {
|
|
222
288
|
const anchor = operation.anchor ?? {};
|
|
223
289
|
return compactRecord({
|
|
@@ -247,55 +313,7 @@ function projectedSourcePath(script, edits) {
|
|
|
247
313
|
return edits.map((edit) => edit.sourcePath).find(Boolean) ?? script.sourcePath;
|
|
248
314
|
}
|
|
249
315
|
|
|
250
|
-
function
|
|
251
|
-
if (typeof sourceText !== 'string' || !span) return undefined;
|
|
252
|
-
if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
|
|
253
|
-
if (typeof span.startLine !== 'number') return undefined;
|
|
254
|
-
const lineStarts = [0];
|
|
255
|
-
for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
|
|
256
|
-
const startLine = Math.max(1, span.startLine);
|
|
257
|
-
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
258
|
-
const start = lineStarts[startLine - 1];
|
|
259
|
-
const endLineStart = lineStarts[endLine - 1];
|
|
260
|
-
if (start === undefined || endLineStart === undefined) return undefined;
|
|
261
|
-
const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
|
|
262
|
-
const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
|
|
263
|
-
const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
|
|
264
|
-
return { start: start + startColumn, end: endLineStart + endColumn };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function insertionOffset(sourceText, insertion) {
|
|
268
|
-
if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
|
|
269
|
-
const mode = insertion?.mode;
|
|
270
|
-
if (mode === 'file-start') return { ok: true, offset: 0 };
|
|
271
|
-
if (mode === 'file-end') return { ok: true, offset: sourceText.length };
|
|
272
|
-
const range = spanOffsets(sourceText, insertion?.headSpan);
|
|
273
|
-
if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
|
|
274
|
-
if (mode === 'before') return { ok: true, offset: range.start };
|
|
275
|
-
if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
|
|
276
|
-
return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function removalRange(sourceText, span) {
|
|
280
|
-
const range = { ...span };
|
|
281
|
-
if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
|
|
282
|
-
else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
283
|
-
return range;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function insertionReplacement(text, sourceText, offset) {
|
|
287
|
-
let replacement = String(text ?? '');
|
|
288
|
-
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
289
|
-
if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
290
|
-
if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
|
|
291
|
-
if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
292
|
-
return replacement;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function afterLineOffset(sourceText, offset) {
|
|
296
|
-
return sourceText[offset] === '\n' ? offset + 1 : offset;
|
|
297
|
-
}
|
|
298
|
-
|
|
316
|
+
function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
|
|
299
317
|
function compactRecord(value) {
|
|
300
318
|
return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
|
|
301
319
|
}
|
|
@@ -3,6 +3,8 @@ 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';
|
|
7
|
+
import { afterLineOffset, bodyContentRange, spanOffsets } from './semanticEditSourceRanges.js';
|
|
6
8
|
|
|
7
9
|
export function replaySemanticEditProjection(input = {}) {
|
|
8
10
|
const projection = input.projection ?? input.semanticEditProjection;
|
|
@@ -16,11 +18,20 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
16
18
|
const currentSymbols = currentSourceText && isJavaScriptLike(language)
|
|
17
19
|
? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
|
|
18
20
|
: [];
|
|
19
|
-
const
|
|
20
|
-
? (projection.edits ?? []).map((edit) => replayProjectionEdit(edit, { currentSourceText, currentSymbols }))
|
|
21
|
+
const replayedEdits = projection.status === 'projected' && typeof currentSourceText === 'string'
|
|
22
|
+
? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), { currentSourceText, currentSymbols }))
|
|
21
23
|
: [];
|
|
24
|
+
const edits = replayEditsWithOverlapDiagnostics(replayedEdits);
|
|
22
25
|
const status = replayStatus(reasonCodes, edits, projection);
|
|
23
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
|
+
});
|
|
24
35
|
const core = {
|
|
25
36
|
kind: 'frontier.lang.semanticEditReplay',
|
|
26
37
|
version: 1,
|
|
@@ -37,6 +48,7 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
37
48
|
edits,
|
|
38
49
|
appliedOperations: edits.filter((edit) => edit.status === 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
39
50
|
skippedOperations: edits.filter((edit) => edit.status !== 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
51
|
+
diagnostics,
|
|
40
52
|
admission: replayAdmission(status, reasonCodes, edits),
|
|
41
53
|
outputSourceText,
|
|
42
54
|
summary: replaySummary(edits, reasonCodes),
|
|
@@ -51,31 +63,43 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
function replayProjectionEdit(edit, context) {
|
|
54
|
-
if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied']);
|
|
55
|
-
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);
|
|
56
68
|
if (edit.editKind === 'insert') return replayInsertionEdit(edit, context);
|
|
57
|
-
const
|
|
58
|
-
|
|
69
|
+
const headRange = { start: edit.headStart, end: edit.headEnd };
|
|
70
|
+
const offset = checkRange(edit, headRange, context.currentSourceText, 'head-offset');
|
|
59
71
|
const symbol = findCurrentSymbol(edit, context.currentSymbols);
|
|
60
|
-
const spanRange = spanOffsets(context.currentSourceText, symbol?.sourceSpan);
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
|
|
73
|
+
if (symbol && spanRange && !sameRange(headRange, spanRange)) {
|
|
74
|
+
const moved = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
75
|
+
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
76
|
+
if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
|
|
77
|
+
return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
78
|
+
}
|
|
79
|
+
return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`], context.currentSourceText);
|
|
80
|
+
}
|
|
81
|
+
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
82
|
+
const anchored = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
83
|
+
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
63
84
|
return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
|
|
64
|
-
symbol ?
|
|
65
|
-
]);
|
|
85
|
+
symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
|
|
86
|
+
], context.currentSourceText);
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function replayInsertionEdit(edit, context) {
|
|
69
90
|
const inserted = findCurrentSymbol(edit, context.currentSymbols);
|
|
70
91
|
const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
|
|
71
92
|
const already = checkRange(edit, insertedRange, context.currentSourceText, 'current-inserted-symbol');
|
|
72
|
-
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason]);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason], context.currentSourceText);
|
|
94
|
+
if (inserted && insertedRange) {
|
|
95
|
+
return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch'], context.currentSourceText);
|
|
96
|
+
}
|
|
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);
|
|
76
100
|
return replayEditRecord(edit, anchor ? 'conflict' : 'stale', undefined, [
|
|
77
101
|
anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
|
|
78
|
-
]);
|
|
102
|
+
], context.currentSourceText);
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
function checkRange(edit, range, sourceText, label) {
|
|
@@ -89,7 +113,8 @@ function checkRange(edit, range, sourceText, label) {
|
|
|
89
113
|
return undefined;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
function replayEditRecord(edit, status, range, reasonCodes) {
|
|
116
|
+
function replayEditRecord(edit, status, range, reasonCodes, sourceText) {
|
|
117
|
+
const normalizedReasonCodes = reasonList(reasonCodes);
|
|
93
118
|
return compactRecord({
|
|
94
119
|
operationId: edit.operationId,
|
|
95
120
|
semanticKey: edit.semanticKey,
|
|
@@ -97,6 +122,8 @@ function replayEditRecord(edit, status, range, reasonCodes) {
|
|
|
97
122
|
sourceIdentityHash: edit.sourceIdentityHash,
|
|
98
123
|
editContentHash: edit.editContentHash,
|
|
99
124
|
editKind: edit.editKind,
|
|
125
|
+
editOrder: edit.editOrder,
|
|
126
|
+
sourceRangeKind: edit.sourceRangeKind,
|
|
100
127
|
sourcePath: edit.targetSourcePath ?? edit.sourcePath,
|
|
101
128
|
symbolName: edit.targetSymbolName ?? edit.symbolName,
|
|
102
129
|
symbolKind: edit.targetSymbolKind ?? edit.symbolKind,
|
|
@@ -105,7 +132,8 @@ function replayEditRecord(edit, status, range, reasonCodes) {
|
|
|
105
132
|
end: range?.end,
|
|
106
133
|
replacementBytes: edit.replacementBytes,
|
|
107
134
|
replacementText: edit.replacementText,
|
|
108
|
-
reasonCodes:
|
|
135
|
+
reasonCodes: normalizedReasonCodes,
|
|
136
|
+
diagnostics: replayEditDiagnostics(edit, status, range, normalizedReasonCodes, sourceText)
|
|
109
137
|
});
|
|
110
138
|
}
|
|
111
139
|
|
|
@@ -131,24 +159,62 @@ function findCurrentSymbol(edit, symbols) {
|
|
|
131
159
|
return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
168
|
}
|
|
138
169
|
|
|
139
|
-
function
|
|
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;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function insertionRange(edit, candidate, anchor, sourceText) {
|
|
140
196
|
if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
|
|
141
197
|
if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
|
|
198
|
+
const mode = candidate?.mode ?? edit.insertionMode;
|
|
142
199
|
const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
|
|
143
200
|
if (!anchorRange) return undefined;
|
|
144
|
-
if (
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
return { start: offset, end: offset };
|
|
201
|
+
if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
|
|
202
|
+
if (mode === 'after') {
|
|
203
|
+
return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
|
|
148
204
|
}
|
|
149
205
|
return undefined;
|
|
150
206
|
}
|
|
151
207
|
|
|
208
|
+
function currentSymbolEditRange(edit, symbolRange, sourceText) {
|
|
209
|
+
if (!symbolRange) return undefined;
|
|
210
|
+
if (edit.sourceRangeKind === 'body-content') return bodyContentRange(sourceText, symbolRange);
|
|
211
|
+
return symbolRange;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function currentSymbolRangeLabel(edit) {
|
|
215
|
+
return edit.sourceRangeKind === 'body-content' ? 'current-symbol-body' : 'current-symbol-anchor';
|
|
216
|
+
}
|
|
217
|
+
|
|
152
218
|
function replayStatus(reasonCodes, edits, projection) {
|
|
153
219
|
if (reasonCodes.some((reason) => reason !== 'current-source-hash-mismatch')) return 'blocked';
|
|
154
220
|
if (!edits.length && !(projection.edits ?? []).length) return 'evidence-only';
|
|
@@ -189,10 +255,25 @@ function replayOutputSource(status, sourceText, edits) {
|
|
|
189
255
|
if (status === 'already-applied') return sourceText;
|
|
190
256
|
if (status !== 'accepted-clean') return undefined;
|
|
191
257
|
return edits.filter((edit) => edit.status === 'applied')
|
|
192
|
-
.sort(
|
|
258
|
+
.sort(replaySourceEditSort)
|
|
193
259
|
.reduce((text, edit) => text.slice(0, edit.start) + editReplacement(edit, edits) + text.slice(edit.end), sourceText);
|
|
194
260
|
}
|
|
195
261
|
|
|
262
|
+
function replaySourceEditSort(left, right) {
|
|
263
|
+
return right.start - left.start || right.end - left.end || (right.editOrder ?? 0) - (left.editOrder ?? 0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function projectionEditWithOrder(edit, index) {
|
|
267
|
+
return {
|
|
268
|
+
...edit,
|
|
269
|
+
editOrder: typeof edit.editOrder === 'number'
|
|
270
|
+
? edit.editOrder
|
|
271
|
+
: typeof edit.order === 'number'
|
|
272
|
+
? edit.order
|
|
273
|
+
: index
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
196
277
|
function editReplacement(edit, edits) {
|
|
197
278
|
return edits.find((candidate) => candidate.operationId === edit.operationId)?.replacementText ?? '';
|
|
198
279
|
}
|
|
@@ -205,19 +286,12 @@ function baseReasonCodes(projection, currentSourceText) {
|
|
|
205
286
|
]);
|
|
206
287
|
}
|
|
207
288
|
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const startLine = Math.max(1, span.startLine);
|
|
215
|
-
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
216
|
-
const lineStart = starts[startLine - 1];
|
|
217
|
-
const endLineStart = starts[endLine - 1];
|
|
218
|
-
if (lineStart === undefined || endLineStart === undefined) return undefined;
|
|
219
|
-
const lineEnd = starts[endLine] === undefined ? sourceText.length : starts[endLine] - 1;
|
|
220
|
-
return { start: lineStart + Math.max(0, (span.startColumn ?? 1) - 1), end: endLineStart + (span.endColumn === undefined ? lineEnd - endLineStart : Math.max(0, span.endColumn - 1)) };
|
|
289
|
+
function sameRange(left, right) {
|
|
290
|
+
return left?.start === right?.start && left?.end === right?.end;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function rangesOverlap(left, right) {
|
|
294
|
+
return Boolean(left && right && left.start < right.end && right.start < left.end);
|
|
221
295
|
}
|
|
222
296
|
|
|
223
297
|
function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
|