@shapeshift-labs/frontier-lang-compiler 0.2.98 → 0.2.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/declarations/semantic-edit-bundle.d.ts +90 -0
  2. package/dist/declarations/semantic-edit-script.d.ts +34 -37
  3. package/dist/declarations/semantic-lineage.d.ts +63 -34
  4. package/dist/declarations/semantic-patch-bundle-index.d.ts +3 -0
  5. package/dist/declarations/semantic-patch-bundle.d.ts +23 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/internal/index-impl/declarationRecord.js +2 -2
  9. package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
  10. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +56 -64
  11. package/dist/internal/index-impl/replaySemanticEditProjection.js +54 -22
  12. package/dist/internal/index-impl/semanticEditBundleAdmission.js +220 -0
  13. package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
  14. package/dist/internal/index-impl/semanticEditSourceRanges.js +204 -0
  15. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +35 -1
  16. package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
  17. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +150 -13
  18. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +28 -1
  19. package/dist/internal/index-impl/semanticPatchBundleAdmission.js +130 -11
  20. package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
  21. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +6 -2
  22. package/dist/internal/index-impl/semanticPatchBundleRecords.js +65 -126
  23. package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
  24. package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
  25. package/dist/lightweight-dependency-relations.js +113 -7
  26. package/dist/native-import-utils.js +15 -1
  27. package/dist/native-region-scanner-js-helpers.js +61 -17
  28. package/dist/native-region-scanner-js.js +12 -4
  29. package/dist/semantic-import-regions.js +3 -3
  30. package/package.json +1 -1
@@ -1,6 +1,14 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
2
  import { idFragment, uniqueStrings } from '../../native-import-utils.js';
3
3
  import { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
4
+ import {
5
+ insertionOffset,
6
+ insertionReplacement,
7
+ projectionCoveredContainerOperationIds,
8
+ removalRange,
9
+ scopedBodyReplacement,
10
+ spanOffsets
11
+ } from './semanticEditSourceRanges.js';
4
12
  import { applySourceEdits, dedupeSourceEdits, validateSourceEdits } from './semanticSourceEditDedupe.js';
5
13
 
6
14
  export function projectSemanticEditScriptToSource(input = {}) {
@@ -9,22 +17,25 @@ export function projectSemanticEditScriptToSource(input = {}) {
9
17
  const headSourceText = input.headSourceText;
10
18
  const reasonCodes = [];
11
19
  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
20
  if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
14
21
  if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
15
22
  const edits = [];
16
23
  const coveredOperationIds = [];
24
+ const projectionCoveredOperationIds = projectionCoveredContainerOperationIds(script.operations ?? [], workerSourceText);
17
25
  for (const [index, operation] of (script.operations ?? []).entries()) {
18
- if (operation.status === 'covered') {
26
+ if (operation.status === 'covered' || projectionCoveredOperationIds.has(operation.id)) {
19
27
  coveredOperationIds.push(operation.id);
20
28
  continue;
21
29
  }
22
- const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index);
30
+ const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index, input.headSourcePath);
23
31
  if (edit.ok) edits.push(edit.value);
24
32
  else reasonCodes.push(...edit.reasonCodes);
25
33
  }
26
34
  const deduped = dedupeSourceEdits(edits);
27
35
  reasonCodes.push(...validateSourceEdits(deduped.edits));
36
+ if (script.admission?.status !== 'auto-merge-candidate' && (reasonCodes.length > 0 || (!edits.length && !coveredOperationIds.length))) {
37
+ reasonCodes.push('script-not-auto-merge-candidate');
38
+ }
28
39
  const blocked = reasonCodes.length > 0;
29
40
  const sourceText = blocked ? undefined : applySourceEdits(headSourceText, deduped.edits);
30
41
  const core = {
@@ -62,8 +73,8 @@ export function projectSemanticEditScriptToSource(input = {}) {
62
73
  return { ...core, hash: hashSemanticValue(core) };
63
74
  }
64
75
 
65
- function sourceEditForOperation(operation, workerSourceText, headSourceText, order) {
66
- const identity = projectionIdentity(operation);
76
+ function sourceEditForOperation(operation, workerSourceText, headSourceText, order, headSourcePath) {
77
+ const identity = projectionIdentity(operation, headSourcePath);
67
78
  if (operation.status === 'already-applied') {
68
79
  return { ok: true, value: { ...identity, operationId: operation.id, order, start: 0, end: 0, replacement: '', current: '', alreadyApplied: true } };
69
80
  }
@@ -80,16 +91,23 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
80
91
  if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
81
92
  if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
82
93
  if (reasons.length) return { ok: false, reasonCodes: reasons };
83
- const replacement = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
84
- const current = headSourceText.slice(headOffsets.start, headOffsets.end);
85
- if (operation.hashes?.workerTextHash && hashSemanticValue(replacement) !== operation.hashes.workerTextHash) {
94
+ const anchorReplacement = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
95
+ const anchorCurrent = headSourceText.slice(headOffsets.start, headOffsets.end);
96
+ if (operation.hashes?.workerTextHash && hashSemanticValue(anchorReplacement) !== operation.hashes.workerTextHash) {
86
97
  reasons.push(`worker-span-hash-mismatch:${operation.id}`);
87
98
  }
88
99
  const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
89
- if (expectedHeadHash && hashSemanticValue(current) !== expectedHeadHash) {
100
+ if (expectedHeadHash && hashSemanticValue(anchorCurrent) !== expectedHeadHash) {
90
101
  reasons.push(`head-span-hash-mismatch:${operation.id}`);
91
102
  }
92
103
  if (reasons.length) return { ok: false, reasonCodes: reasons };
104
+ const scoped = scopedBodyReplacement(operation, headSourceText, workerSourceText, headOffsets, workerOffsets);
105
+ const replacement = scoped
106
+ ? workerSourceText.slice(scoped.worker.start, scoped.worker.end)
107
+ : anchorReplacement;
108
+ const current = scoped
109
+ ? headSourceText.slice(scoped.head.start, scoped.head.end)
110
+ : anchorCurrent;
93
111
  return {
94
112
  ok: true,
95
113
  value: {
@@ -97,10 +115,17 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
97
115
  order,
98
116
  ...identity,
99
117
  editKind: 'replace',
100
- start: headOffsets.start,
101
- end: headOffsets.end,
102
- workerStart: workerOffsets.start,
103
- workerEnd: workerOffsets.end,
118
+ sourceRangeKind: scoped?.sourceRangeKind,
119
+ start: scoped?.head.start ?? headOffsets.start,
120
+ end: scoped?.head.end ?? headOffsets.end,
121
+ workerStart: scoped?.worker.start ?? workerOffsets.start,
122
+ workerEnd: scoped?.worker.end ?? workerOffsets.end,
123
+ headAnchorStart: scoped ? headOffsets.start : undefined,
124
+ headAnchorEnd: scoped ? headOffsets.end : undefined,
125
+ workerAnchorStart: scoped ? workerOffsets.start : undefined,
126
+ workerAnchorEnd: scoped ? workerOffsets.end : undefined,
127
+ anchorDeletedTextHash: scoped ? hashSemanticValue(anchorCurrent) : undefined,
128
+ anchorReplacementTextHash: scoped ? hashSemanticValue(anchorReplacement) : undefined,
104
129
  replacement,
105
130
  current
106
131
  }
@@ -165,9 +190,16 @@ function insertionEditForOperation(operation, identity, workerSourceText, headSo
165
190
  };
166
191
  }
167
192
 
168
- function projectionIdentity(operation) {
193
+ function projectionIdentity(operation, headSourcePath) {
169
194
  const identity = semanticEditIdentity(operation);
170
- return { ...identity, sourcePath: operation.reanchor?.toSourcePath ?? identity.sourcePath };
195
+ const sourcePath = operation.reanchor?.toSourcePath ?? headSourcePath ?? operation.insertion?.sourcePath ?? identity.sourcePath;
196
+ const originalSourcePath = sourcePath && identity.sourcePath && sourcePath !== identity.sourcePath
197
+ ? identity.sourcePath
198
+ : identity.originalSourcePath;
199
+ const targetSourcePath = sourcePath && sourcePath !== identity.sourcePath
200
+ ? sourcePath
201
+ : identity.targetSourcePath;
202
+ return { ...identity, sourcePath, originalSourcePath, targetSourcePath };
171
203
  }
172
204
 
173
205
  function projectionEditRecord(edit) {
@@ -197,18 +229,27 @@ function projectionEditRecord(edit) {
197
229
  operationContentHash: edit.operationContentHash,
198
230
  editContentHash: hashSemanticValue(compactRecord({
199
231
  semanticIdentityHash: identity.semanticIdentityHash,
232
+ sourceRangeKind: edit.sourceRangeKind,
200
233
  deletedTextHash,
201
234
  replacementTextHash,
202
235
  status: edit.alreadyApplied ? 'already-applied' : 'applied'
203
236
  })),
237
+ sourceRangeKind: edit.sourceRangeKind,
204
238
  headStart: edit.start,
205
239
  headEnd: edit.end,
206
240
  workerStart: edit.workerStart,
207
241
  workerEnd: edit.workerEnd,
242
+ editOrder: edit.order,
243
+ headAnchorStart: edit.headAnchorStart,
244
+ headAnchorEnd: edit.headAnchorEnd,
245
+ workerAnchorStart: edit.workerAnchorStart,
246
+ workerAnchorEnd: edit.workerAnchorEnd,
208
247
  deletedBytes: edit.current.length,
209
248
  replacementBytes: edit.replacement.length,
210
249
  deletedTextHash,
211
250
  replacementTextHash,
251
+ anchorDeletedTextHash: edit.anchorDeletedTextHash,
252
+ anchorReplacementTextHash: edit.anchorReplacementTextHash,
212
253
  replacementSpanTextHash: hashSemanticValue(edit.replacementSpanText ?? edit.replacement),
213
254
  insertionMode: edit.insertion?.mode,
214
255
  insertionAnchorKey: edit.insertion?.anchorKey,
@@ -247,55 +288,6 @@ function projectedSourcePath(script, edits) {
247
288
  return edits.map((edit) => edit.sourcePath).find(Boolean) ?? script.sourcePath;
248
289
  }
249
290
 
250
- function spanOffsets(sourceText, span) {
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
-
299
291
  function compactRecord(value) {
300
292
  return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
301
293
  }
@@ -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 { afterLineOffset, bodyContentRange, spanOffsets } from './semanticEditSourceRanges.js';
6
7
 
7
8
  export function replaySemanticEditProjection(input = {}) {
8
9
  const projection = input.projection ?? input.semanticEditProjection;
@@ -17,7 +18,7 @@ export function replaySemanticEditProjection(input = {}) {
17
18
  ? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
18
19
  : [];
19
20
  const edits = projection.status === 'projected' && typeof currentSourceText === 'string'
20
- ? (projection.edits ?? []).map((edit) => replayProjectionEdit(edit, { currentSourceText, currentSymbols }))
21
+ ? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), { currentSourceText, currentSymbols }))
21
22
  : [];
22
23
  const status = replayStatus(reasonCodes, edits, projection);
23
24
  const outputSourceText = replayOutputSource(status, currentSourceText, edits);
@@ -54,14 +55,23 @@ function replayProjectionEdit(edit, context) {
54
55
  if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied']);
55
56
  if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text']);
56
57
  if (edit.editKind === 'insert') return replayInsertionEdit(edit, context);
57
- const offset = checkRange(edit, { start: edit.headStart, end: edit.headEnd }, context.currentSourceText, 'head-offset');
58
- if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
58
+ const headRange = { start: edit.headStart, end: edit.headEnd };
59
+ const offset = checkRange(edit, headRange, context.currentSourceText, 'head-offset');
59
60
  const symbol = findCurrentSymbol(edit, context.currentSymbols);
60
- const spanRange = spanOffsets(context.currentSourceText, symbol?.sourceSpan);
61
- const anchored = checkRange(edit, spanRange, context.currentSourceText, 'current-symbol-anchor');
61
+ const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
62
+ if (symbol && spanRange && !sameRange(headRange, spanRange)) {
63
+ 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']);
65
+ if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
66
+ return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
67
+ }
68
+ return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`]);
69
+ }
70
+ if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
71
+ const anchored = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
62
72
  if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol']);
63
73
  return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
64
- symbol ? 'current-symbol-anchor-content-mismatch' : 'current-symbol-anchor-missing'
74
+ symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
65
75
  ]);
66
76
  }
67
77
 
@@ -70,6 +80,9 @@ function replayInsertionEdit(edit, context) {
70
80
  const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
71
81
  const already = checkRange(edit, insertedRange, context.currentSourceText, 'current-inserted-symbol');
72
82
  if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason]);
83
+ if (inserted && insertedRange) {
84
+ return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch']);
85
+ }
73
86
  const anchor = findInsertionAnchorSymbol(edit, context.currentSymbols);
74
87
  const range = insertionRange(edit, anchor, context.currentSourceText);
75
88
  if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`]);
@@ -97,6 +110,8 @@ function replayEditRecord(edit, status, range, reasonCodes) {
97
110
  sourceIdentityHash: edit.sourceIdentityHash,
98
111
  editContentHash: edit.editContentHash,
99
112
  editKind: edit.editKind,
113
+ editOrder: edit.editOrder,
114
+ sourceRangeKind: edit.sourceRangeKind,
100
115
  sourcePath: edit.targetSourcePath ?? edit.sourcePath,
101
116
  symbolName: edit.targetSymbolName ?? edit.symbolName,
102
117
  symbolKind: edit.targetSymbolKind ?? edit.symbolKind,
@@ -143,12 +158,21 @@ function insertionRange(edit, anchor, sourceText) {
143
158
  if (!anchorRange) return undefined;
144
159
  if (edit.insertionMode === 'before') return { start: anchorRange.start, end: anchorRange.start };
145
160
  if (edit.insertionMode === 'after') {
146
- const offset = sourceText[anchorRange.end] === '\n' ? anchorRange.end + 1 : anchorRange.end;
147
- return { start: offset, end: offset };
161
+ return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
148
162
  }
149
163
  return undefined;
150
164
  }
151
165
 
166
+ function currentSymbolEditRange(edit, symbolRange, sourceText) {
167
+ if (!symbolRange) return undefined;
168
+ if (edit.sourceRangeKind === 'body-content') return bodyContentRange(sourceText, symbolRange);
169
+ return symbolRange;
170
+ }
171
+
172
+ function currentSymbolRangeLabel(edit) {
173
+ return edit.sourceRangeKind === 'body-content' ? 'current-symbol-body' : 'current-symbol-anchor';
174
+ }
175
+
152
176
  function replayStatus(reasonCodes, edits, projection) {
153
177
  if (reasonCodes.some((reason) => reason !== 'current-source-hash-mismatch')) return 'blocked';
154
178
  if (!edits.length && !(projection.edits ?? []).length) return 'evidence-only';
@@ -189,10 +213,25 @@ function replayOutputSource(status, sourceText, edits) {
189
213
  if (status === 'already-applied') return sourceText;
190
214
  if (status !== 'accepted-clean') return undefined;
191
215
  return edits.filter((edit) => edit.status === 'applied')
192
- .sort((left, right) => right.start - left.start)
216
+ .sort(replaySourceEditSort)
193
217
  .reduce((text, edit) => text.slice(0, edit.start) + editReplacement(edit, edits) + text.slice(edit.end), sourceText);
194
218
  }
195
219
 
220
+ function replaySourceEditSort(left, right) {
221
+ return right.start - left.start || right.end - left.end || (right.editOrder ?? 0) - (left.editOrder ?? 0);
222
+ }
223
+
224
+ function projectionEditWithOrder(edit, index) {
225
+ return {
226
+ ...edit,
227
+ editOrder: typeof edit.editOrder === 'number'
228
+ ? edit.editOrder
229
+ : typeof edit.order === 'number'
230
+ ? edit.order
231
+ : index
232
+ };
233
+ }
234
+
196
235
  function editReplacement(edit, edits) {
197
236
  return edits.find((candidate) => candidate.operationId === edit.operationId)?.replacementText ?? '';
198
237
  }
@@ -205,19 +244,12 @@ function baseReasonCodes(projection, currentSourceText) {
205
244
  ]);
206
245
  }
207
246
 
208
- function spanOffsets(sourceText, span) {
209
- if (typeof sourceText !== 'string' || !span) return undefined;
210
- if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
211
- if (typeof span.startLine !== 'number') return undefined;
212
- const starts = [0];
213
- for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') starts.push(index + 1);
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)) };
247
+ function sameRange(left, right) {
248
+ return left?.start === right?.start && left?.end === right?.end;
249
+ }
250
+
251
+ function rangesOverlap(left, right) {
252
+ return Boolean(left && right && left.start < right.end && right.start < left.end);
221
253
  }
222
254
 
223
255
  function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
@@ -0,0 +1,220 @@
1
+ import { normalizeSemanticMergeReadiness, uniqueStrings } from '../../native-import-utils.js';
2
+
3
+ export const SemanticEditBundleAdmissionStatuses = Object.freeze([
4
+ 'none',
5
+ 'ready',
6
+ 'already-applied',
7
+ 'needs-review',
8
+ 'stale',
9
+ 'conflict',
10
+ 'blocked'
11
+ ]);
12
+
13
+ export function createSemanticEditBundleAdmission(input = {}, options = {}) {
14
+ const scripts = array(input.semanticEditScripts ?? input.scripts ?? input.semanticEditScript);
15
+ const projections = array(input.semanticEditProjections ?? input.projections ?? input.semanticEditProjection);
16
+ const replays = array(input.semanticEditReplays ?? input.replays ?? input.semanticEditReplay);
17
+ const evidence = evidenceRecords(input, options);
18
+ const summary = summarizeSemanticEditBundle(scripts, projections, replays, evidence);
19
+ const computedStatus = semanticEditBundleStatus(summary);
20
+ const status = safeStatus(input.status ?? options.status, computedStatus, summary);
21
+ const readiness = normalizeSemanticMergeReadiness(input.readiness ?? options.readiness ?? readinessForStatus(status))
22
+ ?? input.readiness ?? options.readiness ?? readinessForStatus(status);
23
+ const positiveAutoApplyCandidate = status === 'ready' && hasPositiveAutoMergeProof(summary);
24
+ const computedReviewRequired = !['ready', 'already-applied', 'none'].includes(status) || (status === 'ready' && !positiveAutoApplyCandidate);
25
+ return compactRecord({
26
+ status,
27
+ action: safeAction(input.action ?? options.action, status, positiveAutoApplyCandidate),
28
+ readiness,
29
+ reviewRequired: input.reviewRequired === true || computedReviewRequired,
30
+ autoApplyCandidate: input.autoApplyCandidate === false ? false : positiveAutoApplyCandidate,
31
+ autoMergeClaim: false,
32
+ semanticEquivalenceClaim: false,
33
+ reasonCodes: uniqueStrings([
34
+ ...strings(input.reasonCodes),
35
+ ...strings(options.reasonCodes),
36
+ ...summary.reasonCodes,
37
+ ...derivedReasonCodes(summary, status)
38
+ ].filter(Boolean)),
39
+ sourcePaths: summary.sourcePaths,
40
+ scriptIds: summary.scriptIds,
41
+ projectionIds: summary.projectionIds,
42
+ replayIds: summary.replayIds,
43
+ summary,
44
+ metadata: input.metadata ?? options.metadata
45
+ });
46
+ }
47
+
48
+ function summarizeSemanticEditBundle(scripts, projections, replays, evidence) {
49
+ const scriptStatusEntries = scripts.map((script) => script.admission?.status);
50
+ const projectionStatusEntries = projections.flatMap((projection) => [projection.status, projection.admission?.status]);
51
+ const replayStatusEntries = replays.map((replay) => replay.status);
52
+ const scriptStatuses = uniqueStrings(strings(scriptStatusEntries));
53
+ const projectionStatuses = uniqueStrings(strings(projectionStatusEntries));
54
+ const replayStatuses = uniqueStrings(strings(replayStatusEntries));
55
+ const replayActions = uniqueStrings(strings(replays.map((replay) => replay.admission?.action)));
56
+ const evidenceSummary = summarizeEvidence(evidence);
57
+ return {
58
+ scripts: scripts.length,
59
+ projections: projections.length,
60
+ replays: replays.length,
61
+ files: sourcePaths(scripts, projections, replays).length,
62
+ portableScripts: scripts.filter((script) => script.admission?.status === 'auto-merge-candidate').length,
63
+ portableProjections: projections.filter((projection) => projection.status === 'projected' && projection.admission?.status === 'auto-merge-candidate').length,
64
+ acceptedClean: replays.filter((replay) => replay.status === 'accepted-clean').length,
65
+ alreadyApplied: replays.filter((replay) => replay.status === 'already-applied').length,
66
+ conflicts: countStatuses(scriptStatusEntries, replayStatusEntries, ['conflict']),
67
+ stale: countStatuses(scriptStatusEntries, replayStatusEntries, ['stale']),
68
+ blocked: countStatuses(scriptStatusEntries, projectionStatusEntries, replayStatusEntries, ['blocked']),
69
+ needsReview: countStatuses(scriptStatusEntries, replayStatusEntries, ['needs-port', 'evidence-only']),
70
+ projected: projections.filter((projection) => projection.status === 'projected').length,
71
+ projectionBlocked: projections.filter((projection) => projection.status === 'blocked').length,
72
+ scriptStatuses,
73
+ projectionStatuses,
74
+ replayStatuses,
75
+ replayActions,
76
+ sourcePaths: sourcePaths(scripts, projections, replays),
77
+ scriptIds: uniqueStrings(scripts.map((script) => script.id)),
78
+ projectionIds: uniqueStrings(projections.map((projection) => projection.id)),
79
+ replayIds: uniqueStrings(replays.map((replay) => replay.id)),
80
+ evidenceIds: evidenceSummary.evidenceIds,
81
+ passedTestEvidence: evidenceSummary.passed,
82
+ failedTestEvidence: evidenceSummary.failed,
83
+ conflictEvidence: evidenceSummary.conflict,
84
+ staleEvidence: evidenceSummary.stale,
85
+ reasonCodes: uniqueStrings([
86
+ ...scripts.flatMap((script) => strings(script.admission?.reasonCodes)),
87
+ ...projections.flatMap((projection) => strings(projection.admission?.reasonCodes)),
88
+ ...replays.flatMap((replay) => strings(replay.admission?.reasonCodes)),
89
+ ...evidenceSummary.reasonCodes
90
+ ])
91
+ };
92
+ }
93
+
94
+ function semanticEditBundleStatus(summary) {
95
+ const total = summary.scripts + summary.projections + summary.replays;
96
+ if (summary.blocked || summary.projectionBlocked || summary.failedTestEvidence) return 'blocked';
97
+ if (summary.conflicts || summary.conflictEvidence) return 'conflict';
98
+ if (summary.stale || summary.staleEvidence) return 'stale';
99
+ if (total === 0) return 'none';
100
+ if (!summary.replays || summary.needsReview) return 'needs-review';
101
+ if (summary.acceptedClean === 0 && summary.alreadyApplied === summary.replays) return 'already-applied';
102
+ return hasPositiveAutoMergeProof(summary) ? 'ready' : 'needs-review';
103
+ }
104
+
105
+ function derivedReasonCodes(summary, status) {
106
+ return [
107
+ summary.scripts && !summary.projections ? 'semantic-edit-projection-missing' : undefined,
108
+ (summary.scripts || summary.projections) && !summary.replays ? 'semantic-edit-replay-missing' : undefined,
109
+ summary.scripts && summary.portableScripts !== summary.scripts ? 'semantic-edit-script-not-portable' : undefined,
110
+ summary.projections && summary.portableProjections !== summary.projections ? 'semantic-edit-projection-not-portable' : undefined,
111
+ summary.acceptedClean && !summary.passedTestEvidence ? 'semantic-edit-tests-passed-evidence-missing' : undefined,
112
+ summary.failedTestEvidence ? 'semantic-edit-tests-failed' : undefined,
113
+ summary.conflictEvidence ? 'semantic-edit-conflict-evidence' : undefined,
114
+ summary.staleEvidence ? 'semantic-edit-stale-evidence' : undefined,
115
+ status === 'ready' ? 'semantic-edit-replay-accepted-clean' : undefined,
116
+ status === 'ready' ? 'semantic-edit-positive-auto-merge-proof' : undefined,
117
+ status === 'already-applied' ? 'semantic-edit-replay-already-applied' : undefined,
118
+ status === 'blocked' ? 'semantic-edit-blocked' : undefined,
119
+ status === 'conflict' ? 'semantic-edit-conflict' : undefined,
120
+ status === 'stale' ? 'semantic-edit-stale' : undefined
121
+ ];
122
+ }
123
+
124
+ function hasPositiveAutoMergeProof(summary) {
125
+ return summary.acceptedClean > 0 &&
126
+ summary.acceptedClean + summary.alreadyApplied === summary.replays &&
127
+ summary.scripts > 0 &&
128
+ summary.projections > 0 &&
129
+ summary.portableScripts === summary.scripts &&
130
+ summary.portableProjections === summary.projections &&
131
+ summary.passedTestEvidence > 0 &&
132
+ summary.failedTestEvidence === 0 &&
133
+ summary.conflictEvidence === 0 &&
134
+ summary.staleEvidence === 0;
135
+ }
136
+
137
+ function readinessForStatus(status) {
138
+ if (['ready', 'already-applied'].includes(status)) return 'ready';
139
+ if (['blocked', 'conflict'].includes(status)) return 'blocked';
140
+ return 'needs-review';
141
+ }
142
+
143
+ function actionForStatus(status) {
144
+ if (status === 'ready') return 'admit';
145
+ if (status === 'already-applied') return 'skip';
146
+ if (status === 'none') return 'none';
147
+ if (status === 'stale') return 'rerun-semantic-import';
148
+ if (status === 'blocked' || status === 'conflict') return 'block';
149
+ return 'review';
150
+ }
151
+
152
+ function safeStatus(requested, computed, summary) {
153
+ if (!requested) return computed;
154
+ if (requested === 'ready' && !hasPositiveAutoMergeProof(summary)) return computed;
155
+ if (requested === 'already-applied' && computed !== 'already-applied') return computed;
156
+ if (['blocked', 'conflict', 'stale'].includes(requested)) return requested;
157
+ return computed;
158
+ }
159
+
160
+ function safeAction(requested, status, positiveAutoApplyCandidate) {
161
+ if (requested === 'admit' && !positiveAutoApplyCandidate) return actionForStatus(status);
162
+ if (requested === 'skip' && status !== 'already-applied') return actionForStatus(status);
163
+ return requested ?? actionForStatus(status);
164
+ }
165
+
166
+ function sourcePaths(scripts, projections, replays) {
167
+ return uniqueStrings(strings([
168
+ ...scripts.map((script) => script.sourcePath),
169
+ ...projections.map((projection) => projection.sourcePath),
170
+ ...projections.flatMap((projection) => array(projection.edits).flatMap((edit) => [edit.sourcePath, edit.targetSourcePath])),
171
+ ...replays.map((replay) => replay.sourcePath),
172
+ ...replays.flatMap((replay) => array(replay.edits).map((edit) => edit.sourcePath))
173
+ ]));
174
+ }
175
+
176
+ function countStatuses(...args) {
177
+ const statuses = args.slice(0, -1).flatMap((value) => strings(value));
178
+ const needles = new Set(args.at(-1));
179
+ return statuses.filter((status) => needles.has(status)).length;
180
+ }
181
+
182
+ function evidenceRecords(...sources) {
183
+ return sources.flatMap((source) => [
184
+ ...array(source?.evidence),
185
+ ...array(source?.testEvidence),
186
+ ...array(source?.testResults),
187
+ ...array(source?.gateEvidence),
188
+ ...array(source?.proofEvidence)
189
+ ]).filter(Boolean);
190
+ }
191
+
192
+ function summarizeEvidence(evidence) {
193
+ const testLike = evidence.filter(isAutoMergeTestEvidence);
194
+ const conflict = evidence.filter((record) => evidenceStatus(record, ['conflict', 'conflicted']) || record?.metadata?.conflict === true || strings(record?.reasonCodes ?? record?.reasons).some((reason) => reason.toLowerCase().includes('conflict')));
195
+ const stale = evidence.filter((record) => evidenceStatus(record, ['stale']) || record?.metadata?.stale === true || strings(record?.reasonCodes ?? record?.reasons).some((reason) => reason.toLowerCase().includes('stale')));
196
+ const failed = testLike.filter((record) => evidenceStatus(record, ['failed', 'failure', 'error', 'blocked', 'rejected']));
197
+ const passed = testLike.filter((record) => evidenceStatus(record, ['passed', 'ok', 'success', 'succeeded', 'accepted', 'verified']));
198
+ return {
199
+ evidenceIds: uniqueStrings(evidence.map((record) => record.id)),
200
+ passed: passed.length,
201
+ failed: failed.length,
202
+ conflict: conflict.length,
203
+ stale: stale.length,
204
+ reasonCodes: uniqueStrings(evidence.flatMap((record) => strings(record.reasonCodes ?? record.reasons)))
205
+ };
206
+ }
207
+
208
+ function isAutoMergeTestEvidence(record) {
209
+ const kind = String(record?.kind ?? record?.type ?? '').toLowerCase();
210
+ return ['test', 'tests', 'proof', 'gate', 'verification', 'check'].includes(kind);
211
+ }
212
+
213
+ function evidenceStatus(record, statuses) {
214
+ const status = String(record?.status ?? record?.outcome ?? '').toLowerCase();
215
+ return statuses.includes(status);
216
+ }
217
+
218
+ function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
219
+ function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
220
+ function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
@@ -15,16 +15,16 @@ export function semanticEditRecordIndex(scripts, projections, replays, source =
15
15
  semanticEditReplayEditCount: replayEdits.length,
16
16
  semanticEditReplayStatuses: uniqueStrings([...strings(source.semanticEditReplayStatuses), ...strings(index.semanticEditReplayStatuses), ...strings(summary.replayStatuses), ...replays.map((replay) => replay.status)]),
17
17
  semanticEditReplayActions: uniqueStrings([...strings(source.semanticEditReplayActions), ...strings(index.semanticEditReplayActions), ...strings(summary.replayActions), ...replays.map((replay) => replay.admission?.action)]),
18
- semanticEditReplayCurrentHashes: uniqueStrings([...strings(source.semanticEditReplayCurrentHashes), ...strings(index.semanticEditReplayCurrentHashes), ...replays.map((replay) => replay.currentHash)]),
19
- semanticEditReplayOutputHashes: uniqueStrings([...strings(source.semanticEditReplayOutputHashes), ...strings(index.semanticEditReplayOutputHashes), ...replays.map((replay) => replay.outputHash)]),
20
- semanticEditKeys: uniqueStrings([...strings(source.semanticEditKeys), ...strings(index.semanticEditKeys), ...operations.map((operation) => operation.semanticKey), ...edits.map((edit) => edit.semanticKey), ...replayEdits.map((edit) => edit.semanticKey)]),
21
- semanticIdentityHashes: uniqueStrings([...strings(source.semanticIdentityHashes), ...strings(index.semanticIdentityHashes), ...operations.map((operation) => operation.semanticIdentityHash), ...edits.map((edit) => edit.semanticIdentityHash), ...replayEdits.map((edit) => edit.semanticIdentityHash)]),
22
- sourceIdentityHashes: uniqueStrings([...strings(source.sourceIdentityHashes), ...strings(index.sourceIdentityHashes), ...operations.map((operation) => operation.sourceIdentityHash), ...edits.map((edit) => edit.sourceIdentityHash), ...replayEdits.map((edit) => edit.sourceIdentityHash)]),
23
- operationContentHashes: uniqueStrings([...strings(source.operationContentHashes), ...strings(index.operationContentHashes), ...operations.map((operation) => operation.operationContentHash), ...edits.map((edit) => edit.operationContentHash)]),
24
- editContentHashes: uniqueStrings([...strings(source.editContentHashes), ...strings(index.editContentHashes), ...edits.map((edit) => edit.editContentHash), ...replayEdits.map((edit) => edit.editContentHash)]),
25
- anchorKeys: uniqueStrings([...operations.map((operation) => operation.anchor?.key), ...edits.map((edit) => edit.anchorKey)]),
26
- conflictKeys: uniqueStrings([...operations.map((operation) => operation.anchor?.conflictKey), ...edits.map((edit) => edit.conflictKey)]),
27
- projectedSourcePaths: uniqueStrings([...projections.map((projection) => projection.sourcePath), ...edits.flatMap((edit) => [edit.sourcePath, edit.targetSourcePath]), ...replays.map((replay) => replay.sourcePath), ...replayEdits.map((edit) => edit.sourcePath)])
18
+ semanticEditReplayCurrentHashes: uniqueStrings([...strings(source.semanticEditReplayCurrentHashes), ...strings(index.semanticEditReplayCurrentHashes), ...strings(summary.replayCurrentHashes), ...strings(summary.semanticEditReplayCurrentHashes), ...replays.map((replay) => replay.currentHash)]),
19
+ semanticEditReplayOutputHashes: uniqueStrings([...strings(source.semanticEditReplayOutputHashes), ...strings(index.semanticEditReplayOutputHashes), ...strings(summary.replayOutputHashes), ...strings(summary.semanticEditReplayOutputHashes), ...replays.map((replay) => replay.outputHash)]),
20
+ semanticEditKeys: uniqueStrings([...strings(source.semanticEditKeys), ...strings(index.semanticEditKeys), ...strings(summary.semanticEditKeys), ...operations.map((operation) => operation.semanticKey), ...edits.map((edit) => edit.semanticKey), ...replayEdits.map((edit) => edit.semanticKey)]),
21
+ semanticIdentityHashes: uniqueStrings([...strings(source.semanticIdentityHashes), ...strings(index.semanticIdentityHashes), ...strings(summary.semanticIdentityHashes), ...operations.map((operation) => operation.semanticIdentityHash), ...edits.map((edit) => edit.semanticIdentityHash), ...replayEdits.map((edit) => edit.semanticIdentityHash)]),
22
+ sourceIdentityHashes: uniqueStrings([...strings(source.sourceIdentityHashes), ...strings(index.sourceIdentityHashes), ...strings(summary.sourceIdentityHashes), ...operations.map((operation) => operation.sourceIdentityHash), ...edits.map((edit) => edit.sourceIdentityHash), ...replayEdits.map((edit) => edit.sourceIdentityHash)]),
23
+ operationContentHashes: uniqueStrings([...strings(source.operationContentHashes), ...strings(index.operationContentHashes), ...strings(summary.operationContentHashes), ...operations.map((operation) => operation.operationContentHash), ...edits.map((edit) => edit.operationContentHash), ...replayEdits.map((edit) => edit.operationContentHash)]),
24
+ editContentHashes: uniqueStrings([...strings(source.editContentHashes), ...strings(index.editContentHashes), ...strings(summary.editContentHashes), ...edits.map((edit) => edit.editContentHash), ...replayEdits.map((edit) => edit.editContentHash)]),
25
+ anchorKeys: uniqueStrings([...strings(source.anchorKeys), ...strings(index.anchorKeys), ...strings(summary.anchorKeys), ...operations.map((operation) => operation.anchor?.key), ...edits.map((edit) => edit.anchorKey), ...replayEdits.map((edit) => edit.anchorKey)]),
26
+ conflictKeys: uniqueStrings([...strings(source.conflictKeys), ...strings(index.conflictKeys), ...strings(summary.conflictKeys), ...operations.map((operation) => operation.anchor?.conflictKey), ...edits.map((edit) => edit.conflictKey), ...replayEdits.map((edit) => edit.conflictKey)]),
27
+ projectedSourcePaths: uniqueStrings([...strings(source.projectedSourcePaths), ...strings(index.projectedSourcePaths), ...strings(summary.projectedSourcePaths), ...projections.map((projection) => projection.sourcePath), ...edits.flatMap((edit) => [edit.sourcePath, edit.targetSourcePath]), ...replays.map((replay) => replay.sourcePath), ...replayEdits.map((edit) => edit.sourcePath)])
28
28
  };
29
29
  }
30
30
 
@@ -36,9 +36,15 @@ export function semanticEditSummary(index) {
36
36
  replayIds: index.semanticEditReplayIds,
37
37
  replayStatuses: index.semanticEditReplayStatuses,
38
38
  replayActions: index.semanticEditReplayActions,
39
+ replayCurrentHashes: index.semanticEditReplayCurrentHashes,
40
+ replayOutputHashes: index.semanticEditReplayOutputHashes,
39
41
  semanticEditKeys: index.semanticEditKeys,
42
+ semanticIdentityHashes: index.semanticIdentityHashes,
43
+ sourceIdentityHashes: index.sourceIdentityHashes,
40
44
  operationContentHashes: index.operationContentHashes,
41
45
  editContentHashes: index.editContentHashes,
46
+ anchorKeys: index.anchorKeys,
47
+ conflictKeys: index.conflictKeys,
42
48
  projectedSourcePaths: index.projectedSourcePaths,
43
49
  replayEditCount: index.semanticEditReplayEditCount
44
50
  });