@shapeshift-labs/frontier-lang-compiler 0.2.101 → 0.2.103

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 (23) hide show
  1. package/dist/declarations/import-adapter-core.d.ts +6 -0
  2. package/dist/declarations/native-project-admission-semantic-evidence.d.ts +34 -0
  3. package/dist/declarations/native-project-admission.d.ts +6 -10
  4. package/dist/declarations/semantic-edit-script.d.ts +3 -0
  5. package/dist/declarations/semantic-patch-bundle-overlaps.d.ts +1 -0
  6. package/dist/internal/index-impl/createLightweightNativeImport.js +9 -1
  7. package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +14 -2
  8. package/dist/internal/index-impl/diffNativeSymbols.js +82 -1
  9. package/dist/internal/index-impl/importNativeSource.js +14 -14
  10. package/dist/internal/index-impl/nativeImportSemanticIndex.js +33 -0
  11. package/dist/internal/index-impl/projectImportAdmissionImportEvidence.js +1 -1
  12. package/dist/internal/index-impl/projectImportAdmissionSemanticWarnings.js +178 -0
  13. package/dist/internal/index-impl/projectImportAdmissionSummaries.js +22 -3
  14. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +12 -62
  15. package/dist/internal/index-impl/replaySemanticEditProjection.js +43 -63
  16. package/dist/internal/index-impl/semanticEditImportProjection.js +53 -0
  17. package/dist/internal/index-impl/semanticEditProjectionRecord.js +79 -0
  18. package/dist/internal/index-impl/semanticEditReplayAnchors.js +63 -0
  19. package/dist/internal/index-impl/semanticEditSourceRanges.js +5 -0
  20. package/dist/internal/index-impl/semanticPatchBundleAdmission.js +51 -2
  21. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +33 -16
  22. package/dist/semantic-import-regions.js +12 -1
  23. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
1
  import{countBy,maxSemanticMergeReadiness,uniqueRecordsById,uniqueStrings}from'../../native-import-utils.js';
2
2
  import{createSemanticMergeCandidateAdmissionRecord,querySemanticMergeCandidateAdmissionOverlaps,sortSemanticMergeCandidateAdmissionRecords}from'./semanticMergeCandidateRecords.js';
3
- import{compactAdmissionSource,importLosses,sourceLossClasses,summarizeImportPreservation,summarizeParserEvidence}from'./projectImportAdmissionImportEvidence.js';
3
+ import{compactAdmissionSource,importLosses,sourceLossClasses,summarizeImportPreservation,summarizeParserEvidence,summarizeSemanticAdmissionWarnings}from'./projectImportAdmissionImportEvidence.js';
4
4
  import{sourceMissingEvidence,sourceMissingTasks,sourceSemanticMergeScore}from'./projectImportAdmissionTasks.js';
5
5
  import{candidateRisk,maxPreservationQuality,maxRisk,normalizeRisk}from'./projectImportAdmissionRanks.js';
6
6
 
7
7
  export{admissionLanguages}from'./projectImportAdmissionLanguageSummaries.js';
8
8
 
9
- export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
9
+ export function projectAdmissionImports(imports,sourceRows,mergeCandidates,projectResult){
10
10
  return imports.map((imported,index)=>{
11
11
  const source=sourceRows?.[index]??compactAdmissionSource(imported,index);
12
12
  const sourcePath=source.sourcePath??imported?.sourcePath;
@@ -22,6 +22,7 @@ export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
22
22
  };
23
23
  const readiness=source.readiness??imported?.metadata?.semanticMergeReadiness??candidates[0]?.readiness??'ready';
24
24
  const emptySemanticEvidence=Object.values(semanticCounts).reduce((sum,value)=>sum+value,0)===0;
25
+ const semanticAdmission=summarizeSemanticAdmissionWarnings(imported,{source,sourcePath,candidates,projectResult});
25
26
  const sourcePreservation=summarizeImportPreservation(imported,source);
26
27
  const losses=importLosses(imported);
27
28
  const lossClasses=sourceLossClasses(imported,losses);
@@ -55,6 +56,7 @@ export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
55
56
  readiness,
56
57
  semanticCounts,
57
58
  emptySemanticEvidence,
59
+ semanticAdmission,
58
60
  parserEvidence,
59
61
  lossClasses,
60
62
  missingEvidence,
@@ -90,6 +92,7 @@ export function admissionSemanticEvidence(projectResult,imports,importSummaries)
90
92
  .filter((entry)=>entry.emptySemanticEvidence)
91
93
  .map((entry)=>entry.sourcePath)
92
94
  .filter(Boolean));
95
+ const warnings=uniqueSemanticAdmissionWarnings(importSummaries.flatMap((entry)=>entry.semanticAdmission?.warnings??[]));
93
96
  return {
94
97
  empty:Object.values(totals).reduce((sum,value)=>sum+value,0)===0,
95
98
  emptySourceCount:importSummaries.filter((entry)=>entry.emptySemanticEvidence).length,
@@ -98,10 +101,26 @@ export function admissionSemanticEvidence(projectResult,imports,importSummaries)
98
101
  evidenceRecords:uniqueRecordsById([
99
102
  ...(projectResult?.evidence??[]),
100
103
  ...imports.flatMap((imported)=>imported?.evidence??[])
101
- ]).length
104
+ ]).length,
105
+ warningCount:warnings.length,
106
+ warningReasonCodes:uniqueStrings(warnings.map((warning)=>warning.reasonCode??warning.code)),
107
+ warningSourcePaths:uniqueStrings(warnings.flatMap((warning)=>warning.sourcePaths??[warning.sourcePath]).filter(Boolean)),
108
+ warnings
102
109
  };
103
110
  }
104
111
 
112
+ function uniqueSemanticAdmissionWarnings(warnings){
113
+ const seen=new Set();
114
+ const result=[];
115
+ for(const warning of warnings.filter(Boolean)){
116
+ const key=[warning.reasonCode??warning.code,(warning.sourcePaths??[warning.sourcePath]).join('|')].join('\u0000');
117
+ if(seen.has(key)) continue;
118
+ seen.add(key);
119
+ result.push(warning);
120
+ }
121
+ return result;
122
+ }
123
+
105
124
  export function admissionSourcePreservation(importSummaries,contract){
106
125
  const qualities=importSummaries.map((entry)=>entry.sourcePreservation.quality);
107
126
  const quality=qualities.length?qualities.reduce(maxPreservationQuality,'exact'):'empty';
@@ -3,7 +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 { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
6
+ import { alreadyAppliedImportEditForOperation } from './semanticEditImportProjection.js';
7
+ import { projectionEditRecord } from './semanticEditProjectionRecord.js';
7
8
  import {
8
9
  insertionOffset,
9
10
  insertionReplacement,
@@ -40,7 +41,8 @@ export function projectSemanticEditScriptToSource(input = {}) {
40
41
  }
41
42
  const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index, {
42
43
  headSourcePath: input.headSourcePath,
43
- headSymbols
44
+ headSymbols,
45
+ symbolIndexAvailable: isJavaScriptLike(language)
44
46
  });
45
47
  if (edit.ok) edits.push(edit.value);
46
48
  else reasonCodes.push(...edit.reasonCodes);
@@ -175,14 +177,20 @@ function insertionEditForOperation(operation, identity, workerSourceText, headSo
175
177
  const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
176
178
  const reasons = [];
177
179
  if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
178
- const insertion = insertionOffset(headSourceText, operation.insertion, { symbols: context.headSymbols });
179
- if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
180
180
  if (reasons.length) return { ok: false, reasonCodes: reasons };
181
181
  const spanText = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
182
182
  if (operation.hashes?.workerTextHash && hashSemanticValue(spanText) !== operation.hashes.workerTextHash) {
183
183
  reasons.push(`worker-span-hash-mismatch:${operation.id}`);
184
184
  }
185
185
  if (reasons.length) return { ok: false, reasonCodes: reasons };
186
+ const alreadyAppliedImport = alreadyAppliedImportEditForOperation(operation, identity, spanText, headSourceText, workerOffsets, order, context);
187
+ if (alreadyAppliedImport) return { ok: true, value: alreadyAppliedImport };
188
+ const insertion = insertionOffset(headSourceText, operation.insertion, {
189
+ symbols: context.headSymbols,
190
+ symbolIndexAvailable: context.symbolIndexAvailable
191
+ });
192
+ if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
193
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
186
194
  return {
187
195
  ok: true,
188
196
  value: {
@@ -212,64 +220,6 @@ function projectionIdentity(operation, headSourcePath) {
212
220
  : identity.targetSourcePath;
213
221
  return { ...identity, sourcePath, originalSourcePath, targetSourcePath };
214
222
  }
215
- function projectionEditRecord(edit) {
216
- const deletedTextHash = hashSemanticValue(edit.current);
217
- const replacementTextHash = hashSemanticValue(edit.replacement);
218
- const identity = semanticEditIdentityFields(edit);
219
- return compactRecord({
220
- operationId: edit.operationId,
221
- status: edit.alreadyApplied ? 'already-applied' : 'applied',
222
- kind: edit.kind,
223
- editKind: edit.editKind,
224
- changeKind: edit.changeKind,
225
- anchorKey: edit.anchorKey,
226
- conflictKey: edit.conflictKey,
227
- regionId: edit.regionId,
228
- regionKind: edit.regionKind,
229
- sourcePath: edit.sourcePath,
230
- originalSourcePath: edit.originalSourcePath,
231
- targetAnchorKey: edit.targetAnchorKey,
232
- targetSourcePath: edit.targetSourcePath,
233
- targetSymbolName: edit.targetSymbolName,
234
- targetSymbolKind: edit.targetSymbolKind,
235
- symbolId: edit.symbolId,
236
- symbolName: edit.symbolName,
237
- symbolKind: edit.symbolKind,
238
- ...identity,
239
- operationContentHash: edit.operationContentHash,
240
- editContentHash: hashSemanticValue(compactRecord({
241
- semanticIdentityHash: identity.semanticIdentityHash,
242
- sourceRangeKind: edit.sourceRangeKind,
243
- deletedTextHash,
244
- replacementTextHash,
245
- status: edit.alreadyApplied ? 'already-applied' : 'applied'
246
- })),
247
- sourceRangeKind: edit.sourceRangeKind,
248
- headStart: edit.start,
249
- headEnd: edit.end,
250
- workerStart: edit.workerStart,
251
- workerEnd: edit.workerEnd,
252
- editOrder: edit.order,
253
- headAnchorStart: edit.headAnchorStart,
254
- headAnchorEnd: edit.headAnchorEnd,
255
- workerAnchorStart: edit.workerAnchorStart,
256
- workerAnchorEnd: edit.workerAnchorEnd,
257
- deletedBytes: edit.current.length,
258
- replacementBytes: edit.replacement.length,
259
- deletedTextHash,
260
- replacementTextHash,
261
- anchorDeletedTextHash: edit.anchorDeletedTextHash,
262
- anchorReplacementTextHash: edit.anchorReplacementTextHash,
263
- replacementSpanTextHash: hashSemanticValue(edit.replacementSpanText ?? edit.replacement),
264
- insertionMode: edit.insertion?.mode,
265
- insertionAnchorKey: edit.insertion?.anchorKey,
266
- insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
267
- insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
268
- insertionAnchorCandidates: edit.insertion?.anchorCandidates,
269
- replacementText: edit.replacement
270
- });
271
- }
272
-
273
223
  function sourceSymbolIndex(input) {
274
224
  try {
275
225
  const imported = normalizeNativeDiffImport({
@@ -4,7 +4,14 @@ import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
4
4
  import { mapDiffSymbols } from './mapDiffSymbols.js';
5
5
  import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
6
6
  import { replayDiagnostics, replayEditDiagnostics, replayEditsWithOverlapDiagnostics } from './semanticEditReplayDiagnostics.js';
7
- import { afterLineOffset, bodyContentRange, spanOffsets } from './semanticEditSourceRanges.js';
7
+ import {
8
+ findCurrentSymbol,
9
+ findInsertionAnchor,
10
+ hasSymbolAnchorIdentity,
11
+ insertionAnchorCandidates,
12
+ insertionRange
13
+ } from './semanticEditReplayAnchors.js';
14
+ import { bodyContentRange, removalRange, spanOffsets } from './semanticEditSourceRanges.js';
8
15
 
9
16
  export function replaySemanticEditProjection(input = {}) {
10
17
  const projection = input.projection ?? input.semanticEditProjection;
@@ -19,7 +26,11 @@ export function replaySemanticEditProjection(input = {}) {
19
26
  ? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
20
27
  : [];
21
28
  const replayedEdits = projection.status === 'projected' && typeof currentSourceText === 'string'
22
- ? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), { currentSourceText, currentSymbols }))
29
+ ? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), {
30
+ currentSourceText,
31
+ currentSymbols,
32
+ symbolIndexAvailable: isJavaScriptLike(language)
33
+ }))
23
34
  : [];
24
35
  const edits = replayEditsWithOverlapDiagnostics(replayedEdits);
25
36
  const status = replayStatus(reasonCodes, edits, projection);
@@ -72,7 +83,7 @@ function replayProjectionEdit(edit, context) {
72
83
  const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
73
84
  if (symbol && spanRange && !sameRange(headRange, spanRange)) {
74
85
  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);
86
+ if (moved) return replayEditRecord(edit, moved.status, replayAppliedRange(edit, moved.range, context.currentSourceText), [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
76
87
  if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
77
88
  return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
78
89
  }
@@ -80,12 +91,17 @@ function replayProjectionEdit(edit, context) {
80
91
  }
81
92
  if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
82
93
  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);
94
+ if (anchored) return replayEditRecord(edit, anchored.status, replayAppliedRange(edit, anchored.range, context.currentSourceText), [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
84
95
  return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
85
96
  symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
86
97
  ], context.currentSourceText);
87
98
  }
88
99
 
100
+ function replayAppliedRange(edit, range, sourceText) {
101
+ if (edit.editKind !== 'delete' || !range || typeof sourceText !== 'string') return range;
102
+ return removalRange(sourceText, range);
103
+ }
104
+
89
105
  function replayInsertionEdit(edit, context) {
90
106
  const inserted = findCurrentSymbol(edit, context.currentSymbols);
91
107
  const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
@@ -97,7 +113,8 @@ function replayInsertionEdit(edit, context) {
97
113
  const anchor = findInsertionAnchor(edit, context.currentSymbols);
98
114
  const range = insertionRange(edit, anchor?.candidate, anchor?.symbol, context.currentSourceText);
99
115
  if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`], context.currentSourceText);
100
- return replayEditRecord(edit, anchor ? 'conflict' : 'stale', undefined, [
116
+ const missingStableAnchor = context.symbolIndexAvailable && insertionAnchorCandidates(edit).some(hasSymbolAnchorIdentity);
117
+ return replayEditRecord(edit, anchor || missingStableAnchor ? 'conflict' : 'stale', undefined, [
101
118
  anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
102
119
  ], context.currentSourceText);
103
120
  }
@@ -106,10 +123,26 @@ function checkRange(edit, range, sourceText, label) {
106
123
  if (!range || range.end < range.start) return undefined;
107
124
  const current = sourceText.slice(range.start, range.end);
108
125
  const currentHash = hashSemanticValue(current);
126
+ const currentLineEndingStableText = lineEndingStableText(current);
127
+ const currentLineEndingStableHash = currentLineEndingStableText === undefined
128
+ ? undefined
129
+ : hashSemanticValue(currentLineEndingStableText);
109
130
  if (edit.replacementSpanTextHash && currentHash === edit.replacementSpanTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement-span` };
110
131
  if (edit.replacementTextHash && currentHash === edit.replacementTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement` };
111
132
  if (current === edit.replacementText) return { status: 'already-applied', range, reason: `${label}-matches-replacement-text` };
133
+ if (edit.replacementSpanTextLineEndingStableHash && currentLineEndingStableHash === edit.replacementSpanTextLineEndingStableHash) {
134
+ return { status: 'already-applied', range, reason: `${label}-matches-replacement-span-line-ending-stable` };
135
+ }
136
+ if (edit.replacementTextLineEndingStableHash && currentLineEndingStableHash === edit.replacementTextLineEndingStableHash) {
137
+ return { status: 'already-applied', range, reason: `${label}-matches-replacement-line-ending-stable` };
138
+ }
139
+ if (typeof edit.replacementText === 'string' && currentLineEndingStableText === lineEndingStableText(edit.replacementText)) {
140
+ return { status: 'already-applied', range, reason: `${label}-matches-replacement-text-line-ending-stable` };
141
+ }
112
142
  if (edit.deletedTextHash && currentHash === edit.deletedTextHash) return { status: 'applied', range, reason: `${label}-matches-deleted` };
143
+ if (edit.deletedTextLineEndingStableHash && currentLineEndingStableHash === edit.deletedTextLineEndingStableHash) {
144
+ return { status: 'applied', range, reason: `${label}-matches-deleted-line-ending-stable` };
145
+ }
113
146
  return undefined;
114
147
  }
115
148
 
@@ -147,64 +180,6 @@ function currentSymbolIndex(input) {
147
180
  return [...mapDiffSymbols(imported, createSemanticImportSidecar(imported)).values()];
148
181
  }
149
182
 
150
- function findCurrentSymbol(edit, symbols) {
151
- const exact = symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && [
152
- edit.anchorKey,
153
- edit.targetAnchorKey,
154
- edit.symbolId
155
- ].includes(key)));
156
- if (exact) return exact;
157
- const name = edit.targetSymbolName ?? edit.symbolName;
158
- const kind = edit.targetSymbolKind ?? edit.symbolKind;
159
- return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
160
- }
161
-
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;
193
- }
194
-
195
- function insertionRange(edit, candidate, anchor, sourceText) {
196
- if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
197
- if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
198
- const mode = candidate?.mode ?? edit.insertionMode;
199
- const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
200
- if (!anchorRange) return undefined;
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) };
204
- }
205
- return undefined;
206
- }
207
-
208
183
  function currentSymbolEditRange(edit, symbolRange, sourceText) {
209
184
  if (!symbolRange) return undefined;
210
185
  if (edit.sourceRangeKind === 'body-content') return bodyContentRange(sourceText, symbolRange);
@@ -296,4 +271,9 @@ function rangesOverlap(left, right) {
296
271
 
297
272
  function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
298
273
  function reasonList(values) { return uniqueStrings((values ?? []).filter(Boolean)); }
274
+ function lineEndingStableText(value) {
275
+ if (typeof value !== 'string') return undefined;
276
+ const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
277
+ return normalized.length > 1 && normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
278
+ }
299
279
  function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
@@ -0,0 +1,53 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { spanOffsets } from './semanticEditSourceRanges.js';
3
+
4
+ export function alreadyAppliedImportEditForOperation(operation, identity, spanText, headSourceText, workerOffsets, order, context) {
5
+ if (!isAddImportOperation(operation) || typeof headSourceText !== 'string') return undefined;
6
+ const match = findHeadImportSymbol(operation, context.headSymbols);
7
+ const range = spanOffsets(headSourceText, match?.symbol?.sourceSpan);
8
+ if (!range) return undefined;
9
+ const current = headSourceText.slice(range.start, range.end);
10
+ if (!headImportMatchesOperation(operation, spanText, current, match.symbol)) return undefined;
11
+ return {
12
+ operationId: operation.id,
13
+ order,
14
+ ...identity,
15
+ start: range.start,
16
+ end: range.end,
17
+ workerStart: workerOffsets.start,
18
+ workerEnd: workerOffsets.end,
19
+ replacement: current,
20
+ replacementSpanText: spanText,
21
+ current,
22
+ alreadyApplied: true
23
+ };
24
+ }
25
+
26
+ function findHeadImportSymbol(operation, symbols) {
27
+ const symbolList = Array.isArray(symbols) ? symbols : [];
28
+ const exactKeys = [
29
+ operation.anchor?.key,
30
+ operation.anchor?.symbolId,
31
+ operation.insertion?.insertedSymbolId
32
+ ].filter(Boolean);
33
+ const exact = symbolList.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && exactKeys.includes(key)));
34
+ if (exact) return { symbol: exact, exact: true };
35
+ const name = operation.insertion?.insertedSymbolName ?? operation.anchor?.symbolName;
36
+ const kind = operation.insertion?.insertedSymbolKind ?? operation.anchor?.symbolKind;
37
+ const semantic = symbolList.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
38
+ return semantic ? { symbol: semantic, exact: false } : undefined;
39
+ }
40
+
41
+ function headImportMatchesOperation(operation, spanText, current, symbol) {
42
+ const workerTextHash = operation.hashes?.workerTextHash ?? hashSemanticValue(spanText);
43
+ const workerSpanHash = operation.hashes?.workerSpanHash ?? workerTextHash;
44
+ const currentHash = hashSemanticValue(current);
45
+ if ([workerTextHash, workerSpanHash].includes(currentHash)) return true;
46
+ if ([workerTextHash, workerSpanHash].includes(symbol?.spanHash)) return true;
47
+ const signatureHash = operation.hashes?.afterSignatureHash;
48
+ return Boolean(signatureHash && symbol?.signatureHash === signatureHash);
49
+ }
50
+
51
+ function isAddImportOperation(operation) {
52
+ return operation?.kind === 'addImport' || (operation?.changeKind === 'added' && operation?.anchor?.regionKind === 'import');
53
+ }
@@ -0,0 +1,79 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
3
+
4
+ export function projectionEditRecord(edit) {
5
+ const deletedTextHash = hashSemanticValue(edit.current);
6
+ const replacementTextHash = hashSemanticValue(edit.replacement);
7
+ const replacementSpanText = edit.replacementSpanText ?? edit.replacement;
8
+ const identity = semanticEditIdentityFields(edit);
9
+ return compactRecord({
10
+ operationId: edit.operationId,
11
+ status: edit.alreadyApplied ? 'already-applied' : 'applied',
12
+ kind: edit.kind,
13
+ editKind: edit.editKind,
14
+ changeKind: edit.changeKind,
15
+ anchorKey: edit.anchorKey,
16
+ conflictKey: edit.conflictKey,
17
+ regionId: edit.regionId,
18
+ regionKind: edit.regionKind,
19
+ sourcePath: edit.sourcePath,
20
+ originalSourcePath: edit.originalSourcePath,
21
+ targetAnchorKey: edit.targetAnchorKey,
22
+ targetSourcePath: edit.targetSourcePath,
23
+ targetSymbolName: edit.targetSymbolName,
24
+ targetSymbolKind: edit.targetSymbolKind,
25
+ symbolId: edit.symbolId,
26
+ symbolName: edit.symbolName,
27
+ symbolKind: edit.symbolKind,
28
+ ...identity,
29
+ operationContentHash: edit.operationContentHash,
30
+ editContentHash: hashSemanticValue(compactRecord({
31
+ semanticIdentityHash: identity.semanticIdentityHash,
32
+ sourceRangeKind: edit.sourceRangeKind,
33
+ deletedTextHash,
34
+ replacementTextHash,
35
+ status: edit.alreadyApplied ? 'already-applied' : 'applied'
36
+ })),
37
+ sourceRangeKind: edit.sourceRangeKind,
38
+ headStart: edit.start,
39
+ headEnd: edit.end,
40
+ workerStart: edit.workerStart,
41
+ workerEnd: edit.workerEnd,
42
+ editOrder: edit.order,
43
+ headAnchorStart: edit.headAnchorStart,
44
+ headAnchorEnd: edit.headAnchorEnd,
45
+ workerAnchorStart: edit.workerAnchorStart,
46
+ workerAnchorEnd: edit.workerAnchorEnd,
47
+ deletedBytes: edit.current.length,
48
+ replacementBytes: edit.replacement.length,
49
+ deletedTextHash,
50
+ replacementTextHash,
51
+ deletedTextLineEndingStableHash: lineEndingStableTextHash(edit.current),
52
+ replacementTextLineEndingStableHash: lineEndingStableTextHash(edit.replacement),
53
+ anchorDeletedTextHash: edit.anchorDeletedTextHash,
54
+ anchorReplacementTextHash: edit.anchorReplacementTextHash,
55
+ replacementSpanTextHash: hashSemanticValue(replacementSpanText),
56
+ replacementSpanTextLineEndingStableHash: lineEndingStableTextHash(replacementSpanText),
57
+ insertionMode: edit.insertion?.mode,
58
+ insertionAnchorKey: edit.insertion?.anchorKey,
59
+ insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
60
+ insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
61
+ insertionAnchorCandidates: edit.insertion?.anchorCandidates,
62
+ replacementText: edit.replacement
63
+ });
64
+ }
65
+
66
+ function lineEndingStableTextHash(value) {
67
+ const normalized = lineEndingStableText(value);
68
+ return normalized === undefined ? undefined : hashSemanticValue(normalized);
69
+ }
70
+
71
+ function lineEndingStableText(value) {
72
+ if (typeof value !== 'string') return undefined;
73
+ const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
74
+ return normalized.length > 1 && normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
75
+ }
76
+
77
+ function compactRecord(value) {
78
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
79
+ }
@@ -0,0 +1,63 @@
1
+ import { afterLineOffset, spanOffsets } from './semanticEditSourceRanges.js';
2
+
3
+ export function findCurrentSymbol(edit, symbols) {
4
+ const exact = symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && [
5
+ edit.anchorKey,
6
+ edit.targetAnchorKey,
7
+ edit.symbolId
8
+ ].includes(key)));
9
+ if (exact) return exact;
10
+ const name = edit.targetSymbolName ?? edit.symbolName;
11
+ const kind = edit.targetSymbolKind ?? edit.symbolKind;
12
+ return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
13
+ }
14
+
15
+ export function findInsertionAnchor(edit, symbols) {
16
+ for (const candidate of insertionAnchorCandidates(edit)) {
17
+ const symbol = findInsertionAnchorSymbol(candidate, symbols);
18
+ if (symbol) return { candidate, symbol };
19
+ }
20
+ return undefined;
21
+ }
22
+
23
+ export function insertionAnchorCandidates(edit) {
24
+ const primary = {
25
+ mode: edit.insertionMode,
26
+ anchorKey: edit.insertionAnchorKey,
27
+ anchorSymbolName: edit.insertionAnchorSymbolName,
28
+ anchorSymbolKind: edit.insertionAnchorSymbolKind
29
+ };
30
+ const seen = new Set();
31
+ const result = [];
32
+ for (const candidate of [primary, ...(Array.isArray(edit.insertionAnchorCandidates) ? edit.insertionAnchorCandidates : [])]) {
33
+ if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
34
+ const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
35
+ if (seen.has(key)) continue;
36
+ seen.add(key);
37
+ result.push(candidate);
38
+ }
39
+ return result;
40
+ }
41
+
42
+ export function hasSymbolAnchorIdentity(candidate) {
43
+ return Boolean(candidate.anchorKey || candidate.anchorSymbolId || candidate.anchorSymbolName);
44
+ }
45
+
46
+ export function insertionRange(edit, candidate, anchor, sourceText) {
47
+ if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
48
+ if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
49
+ const mode = candidate?.mode ?? edit.insertionMode;
50
+ const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
51
+ if (!anchorRange) return undefined;
52
+ if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
53
+ if (mode === 'after') {
54
+ return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function findInsertionAnchorSymbol(candidate, symbols) {
60
+ const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
61
+ return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)))
62
+ ?? symbols.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
63
+ }
@@ -168,6 +168,7 @@ function insertionAnchorResolution(sourceText, insertion, context) {
168
168
  const range = spanOffsets(sourceText, symbol?.sourceSpan);
169
169
  if (range) return { mode: candidate.mode, range };
170
170
  }
171
+ if (context.symbolIndexAvailable && candidates.some(hasSymbolAnchorIdentity)) return undefined;
171
172
  for (const candidate of candidates) {
172
173
  const range = spanOffsets(sourceText, candidate.headSpan);
173
174
  if (range) return { mode: candidate.mode, range };
@@ -175,6 +176,10 @@ function insertionAnchorResolution(sourceText, insertion, context) {
175
176
  return undefined;
176
177
  }
177
178
 
179
+ function hasSymbolAnchorIdentity(candidate) {
180
+ return Boolean(candidate.anchorKey || candidate.anchorSymbolId || candidate.anchorSymbolName);
181
+ }
182
+
178
183
  function insertionAnchorCandidates(insertion) {
179
184
  const seen = new Set();
180
185
  const result = [];
@@ -2,7 +2,9 @@ import { normalizeSemanticMergeReadiness, uniqueStrings } from '../../native-imp
2
2
 
3
3
  export function createSemanticPatchBundleAdmission(input = {}, context = {}) {
4
4
  const transformAdmission = semanticTransformAdmission(context);
5
- const semanticEditAdmission = context.semanticEditAdmission ?? { status: 'none', action: 'none', readiness: 'needs-review', reasonCodes: [] };
5
+ const semanticEditAdmission = semanticEditAdmissionWithReplayRequirement(
6
+ context.semanticEditAdmission ?? { status: 'none', action: 'none', readiness: 'needs-review', reasonCodes: [] }
7
+ );
6
8
  const evidenceAdmission = autoMergeEvidenceAdmission(context, { transformAdmission, semanticEditAdmission });
7
9
  const fallbackReadiness = fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, evidenceAdmission, context.readiness);
8
10
  const inputReadiness = normalizeSemanticMergeReadiness(input.readiness ?? fallbackReadiness) ?? input.readiness ?? fallbackReadiness;
@@ -35,7 +37,7 @@ export function createSemanticPatchBundleAdmission(input = {}, context = {}) {
35
37
  ...strings(semanticEditAdmission.reasonCodes),
36
38
  ...strings(evidenceAdmission.reasonCodes)
37
39
  ].filter(Boolean)),
38
- conflictKeys: uniqueStrings([...strings(input.conflictKeys), ...context.conflictKeys]),
40
+ conflictKeys: uniqueStrings([...strings(input.conflictKeys), ...strings(context.conflictKeys)]),
39
41
  admittedAt: input.admittedAt,
40
42
  reviewerId: input.reviewerId,
41
43
  evidenceIds: uniqueStrings([...strings(input.evidenceIds), ...strings(transformAdmission.evidenceIds), ...strings(evidenceAdmission.evidenceIds)]),
@@ -155,6 +157,7 @@ function fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, e
155
157
  if ([transformAdmission.readiness, semanticEditAdmission.readiness, evidenceAdmission.readiness].includes('blocked')) return 'blocked';
156
158
  if (hasSkipReadyAction(semanticEditAdmission)) return 'ready';
157
159
  if (hasPositiveApplyAction(transformAdmission, semanticEditAdmission)) return evidenceAdmission.action === 'admit' ? 'ready' : evidenceAdmission.readiness;
160
+ if (semanticEditAdmission.action === 'review' || semanticEditAdmission.status === 'needs-review') return 'needs-review';
158
161
  return fallback;
159
162
  }
160
163
 
@@ -183,6 +186,51 @@ function hasSkipReadyAction(semanticEditAdmission) {
183
186
  return semanticEditAdmission.action === 'skip' && semanticEditAdmission.readiness === 'ready' && semanticEditAdmission.reviewRequired === false;
184
187
  }
185
188
 
189
+ function semanticEditAdmissionWithReplayRequirement(admission) {
190
+ if (!requiresSemanticEditReplay(admission) || hasAcceptedCleanSemanticEditReplay(admission)) return admission;
191
+ return compactRecord({
192
+ ...admission,
193
+ status: 'needs-review',
194
+ action: 'review',
195
+ readiness: 'needs-review',
196
+ reviewRequired: true,
197
+ autoApplyCandidate: false,
198
+ reasonCodes: uniqueStrings([
199
+ ...strings(admission.reasonCodes).filter((reason) => reason !== 'semantic-edit-positive-auto-merge-proof'),
200
+ ...semanticEditReplayRequirementReasonCodes(admission)
201
+ ])
202
+ });
203
+ }
204
+
205
+ function requiresSemanticEditReplay(admission) {
206
+ return admission.action === 'admit' ||
207
+ admission.autoApplyCandidate === true ||
208
+ admission.status === 'ready' ||
209
+ strings(admission.reasonCodes).includes('semantic-edit-positive-auto-merge-proof');
210
+ }
211
+
212
+ function hasAcceptedCleanSemanticEditReplay(admission) {
213
+ const summary = admission.summary ?? {};
214
+ const acceptedClean = count(summary.acceptedClean);
215
+ const alreadyApplied = count(summary.alreadyApplied);
216
+ const replays = count(summary.replays);
217
+ return acceptedClean > 0 && replays > 0 && acceptedClean + alreadyApplied === replays;
218
+ }
219
+
220
+ function semanticEditReplayRequirementReasonCodes(admission) {
221
+ const summary = admission.summary ?? {};
222
+ const scripts = count(summary.scripts);
223
+ const projections = count(summary.projections);
224
+ const replays = count(summary.replays);
225
+ const acceptedClean = count(summary.acceptedClean);
226
+ return [
227
+ scripts > 0 && projections === 0 ? 'semantic-edit-projection-missing' : undefined,
228
+ (scripts > 0 || projections > 0) && replays === 0 ? 'semantic-edit-replay-missing' : undefined,
229
+ replays > 0 && acceptedClean === 0 ? 'semantic-edit-replay-accepted-clean-missing' : undefined,
230
+ 'semantic-edit-replay-required'
231
+ ].filter(Boolean);
232
+ }
233
+
186
234
  function hasCrossLanguageTransform(index) {
187
235
  const source = new Set(strings(index.transformSourceLanguages));
188
236
  return strings(index.transformTargetLanguages).some((target) => !source.has(target));
@@ -211,6 +259,7 @@ function uniqueEvidenceRecords(records) {
211
259
  }
212
260
 
213
261
  function evidenceIds(evidence) { return uniqueStrings(evidence.map((record) => record.id)); }
262
+ function count(value) { const number = Number(value ?? 0); return Number.isFinite(number) ? number : 0; }
214
263
  function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
215
264
  function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
216
265
  function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }