@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.
Files changed (47) hide show
  1. package/dist/declarations/bidirectional-target-change-evidence.d.ts +299 -0
  2. package/dist/declarations/bidirectional-target-change.d.ts +19 -120
  3. package/dist/declarations/import-adapter-core.d.ts +6 -0
  4. package/dist/declarations/native-project-admission.d.ts +43 -22
  5. package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
  6. package/dist/declarations/semantic-edit-script.d.ts +20 -15
  7. package/dist/declarations/semantic-lineage.d.ts +3 -21
  8. package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
  9. package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
  10. package/dist/declarations/semantic-sidecar.d.ts +12 -14
  11. package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
  12. package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
  13. package/dist/internal/index-impl/createLightweightNativeImport.js +9 -1
  14. package/dist/internal/index-impl/createNativeSourcePreservation.js +16 -1
  15. package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +151 -1
  16. package/dist/internal/index-impl/createSemanticImportSidecar.js +5 -0
  17. package/dist/internal/index-impl/createSemanticImportSidecarAdmission.js +29 -11
  18. package/dist/internal/index-impl/importNativeSource.js +14 -14
  19. package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
  20. package/dist/internal/index-impl/nativeImportSemanticIndex.js +33 -0
  21. package/dist/internal/index-impl/projectImportAdmissionMergeScore.js +26 -74
  22. package/dist/internal/index-impl/projectImportAdmissionProjectionCoverage.js +74 -0
  23. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +39 -13
  24. package/dist/internal/index-impl/replaySemanticEditProjection.js +65 -23
  25. package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
  26. package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
  27. package/dist/internal/index-impl/semanticEditSourceRanges.js +94 -15
  28. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +21 -2
  29. package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
  30. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +8 -0
  31. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +18 -1
  32. package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
  33. package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
  34. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +23 -1
  35. package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
  36. package/dist/native-import-language-profiles.js +10 -2
  37. package/dist/native-region-scanner-js-helpers.js +8 -2
  38. package/dist/native-region-scanner-js-imports.js +7 -0
  39. package/dist/native-region-scanner-js.js +4 -4
  40. package/dist/native-region-scanner.js +2 -1
  41. package/dist/semantic-import-regions.js +18 -5
  42. package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
  43. package/dist/semantic-import-sidecar-entry.js +151 -7
  44. package/dist/semantic-import-sidecar-types.d.ts +18 -13
  45. package/dist/semantic-import-source-preservation-utils.js +55 -0
  46. package/dist/semantic-import-source-preservation.js +98 -3
  47. 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, input.headSourcePath);
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
- function sourceEditForOperation(operation, workerSourceText, headSourceText, order, headSourcePath) {
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 edits = projection.status === 'projected' && typeof currentSourceText === 'string'
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 = findInsertionAnchorSymbol(edit, context.currentSymbols);
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: reasonList(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 findInsertionAnchorSymbol(edit, symbols) {
150
- return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && key === edit.insertionAnchorKey))
151
- ?? symbols.find((symbol) => symbol.name === edit.insertionAnchorSymbolName && (!edit.insertionAnchorSymbolKind || symbol.kind === edit.insertionAnchorSymbolKind));
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 (edit.insertionMode === 'before') return { start: anchorRange.start, end: anchorRange.start };
160
- if (edit.insertionMode === 'after') {
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 anchor = before
11
- ? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol')
12
- : after
13
- ? insertionFromSymbol('before', after, context, 'nearest-next-base-symbol')
14
- : fallbackInsertion(region, context, 'no-neighbor-base-symbol');
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))); }