@shapeshift-labs/frontier-lang-compiler 0.2.99 → 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 (27) hide show
  1. package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
  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.d.ts +13 -0
  5. package/dist/internal/index-impl/declarationRecord.js +2 -2
  6. package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
  7. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +56 -64
  8. package/dist/internal/index-impl/replaySemanticEditProjection.js +54 -22
  9. package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
  10. package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
  11. package/dist/internal/index-impl/semanticEditSourceRanges.js +204 -0
  12. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +35 -1
  13. package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
  14. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +150 -13
  15. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +28 -1
  16. package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
  17. package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
  18. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +6 -2
  19. package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
  20. package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
  21. package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
  22. package/dist/lightweight-dependency-relations.js +113 -7
  23. package/dist/native-import-utils.js +15 -1
  24. package/dist/native-region-scanner-js-helpers.js +61 -17
  25. package/dist/native-region-scanner-js.js +12 -4
  26. package/dist/semantic-import-regions.js +3 -3
  27. package/package.json +1 -1
@@ -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'; }
@@ -14,16 +14,20 @@ export function createSemanticEditBundleAdmission(input = {}, options = {}) {
14
14
  const scripts = array(input.semanticEditScripts ?? input.scripts ?? input.semanticEditScript);
15
15
  const projections = array(input.semanticEditProjections ?? input.projections ?? input.semanticEditProjection);
16
16
  const replays = array(input.semanticEditReplays ?? input.replays ?? input.semanticEditReplay);
17
- const summary = summarizeSemanticEditBundle(scripts, projections, replays);
18
- const status = input.status ?? options.status ?? semanticEditBundleStatus(summary);
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);
19
21
  const readiness = normalizeSemanticMergeReadiness(input.readiness ?? options.readiness ?? readinessForStatus(status))
20
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);
21
25
  return compactRecord({
22
26
  status,
23
- action: input.action ?? options.action ?? actionForStatus(status),
27
+ action: safeAction(input.action ?? options.action, status, positiveAutoApplyCandidate),
24
28
  readiness,
25
- reviewRequired: input.reviewRequired ?? !['ready', 'already-applied', 'none'].includes(status),
26
- autoApplyCandidate: input.autoApplyCandidate ?? status === 'ready',
29
+ reviewRequired: input.reviewRequired === true || computedReviewRequired,
30
+ autoApplyCandidate: input.autoApplyCandidate === false ? false : positiveAutoApplyCandidate,
27
31
  autoMergeClaim: false,
28
32
  semanticEquivalenceClaim: false,
29
33
  reasonCodes: uniqueStrings([
@@ -31,7 +35,7 @@ export function createSemanticEditBundleAdmission(input = {}, options = {}) {
31
35
  ...strings(options.reasonCodes),
32
36
  ...summary.reasonCodes,
33
37
  ...derivedReasonCodes(summary, status)
34
- ]),
38
+ ].filter(Boolean)),
35
39
  sourcePaths: summary.sourcePaths,
36
40
  scriptIds: summary.scriptIds,
37
41
  projectionIds: summary.projectionIds,
@@ -41,7 +45,7 @@ export function createSemanticEditBundleAdmission(input = {}, options = {}) {
41
45
  });
42
46
  }
43
47
 
44
- function summarizeSemanticEditBundle(scripts, projections, replays) {
48
+ function summarizeSemanticEditBundle(scripts, projections, replays, evidence) {
45
49
  const scriptStatusEntries = scripts.map((script) => script.admission?.status);
46
50
  const projectionStatusEntries = projections.flatMap((projection) => [projection.status, projection.admission?.status]);
47
51
  const replayStatusEntries = replays.map((replay) => replay.status);
@@ -49,11 +53,14 @@ function summarizeSemanticEditBundle(scripts, projections, replays) {
49
53
  const projectionStatuses = uniqueStrings(strings(projectionStatusEntries));
50
54
  const replayStatuses = uniqueStrings(strings(replayStatusEntries));
51
55
  const replayActions = uniqueStrings(strings(replays.map((replay) => replay.admission?.action)));
56
+ const evidenceSummary = summarizeEvidence(evidence);
52
57
  return {
53
58
  scripts: scripts.length,
54
59
  projections: projections.length,
55
60
  replays: replays.length,
56
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,
57
64
  acceptedClean: replays.filter((replay) => replay.status === 'accepted-clean').length,
58
65
  alreadyApplied: replays.filter((replay) => replay.status === 'already-applied').length,
59
66
  conflicts: countStatuses(scriptStatusEntries, replayStatusEntries, ['conflict']),
@@ -70,30 +77,43 @@ function summarizeSemanticEditBundle(scripts, projections, replays) {
70
77
  scriptIds: uniqueStrings(scripts.map((script) => script.id)),
71
78
  projectionIds: uniqueStrings(projections.map((projection) => projection.id)),
72
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,
73
85
  reasonCodes: uniqueStrings([
74
86
  ...scripts.flatMap((script) => strings(script.admission?.reasonCodes)),
75
87
  ...projections.flatMap((projection) => strings(projection.admission?.reasonCodes)),
76
- ...replays.flatMap((replay) => strings(replay.admission?.reasonCodes))
88
+ ...replays.flatMap((replay) => strings(replay.admission?.reasonCodes)),
89
+ ...evidenceSummary.reasonCodes
77
90
  ])
78
91
  };
79
92
  }
80
93
 
81
94
  function semanticEditBundleStatus(summary) {
82
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';
83
99
  if (total === 0) return 'none';
84
- if (summary.blocked || summary.projectionBlocked) return 'blocked';
85
- if (summary.conflicts) return 'conflict';
86
- if (summary.stale) return 'stale';
87
100
  if (!summary.replays || summary.needsReview) return 'needs-review';
88
101
  if (summary.acceptedClean === 0 && summary.alreadyApplied === summary.replays) return 'already-applied';
89
- return summary.acceptedClean + summary.alreadyApplied === summary.replays ? 'ready' : 'needs-review';
102
+ return hasPositiveAutoMergeProof(summary) ? 'ready' : 'needs-review';
90
103
  }
91
104
 
92
105
  function derivedReasonCodes(summary, status) {
93
106
  return [
94
107
  summary.scripts && !summary.projections ? 'semantic-edit-projection-missing' : undefined,
95
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,
96
115
  status === 'ready' ? 'semantic-edit-replay-accepted-clean' : undefined,
116
+ status === 'ready' ? 'semantic-edit-positive-auto-merge-proof' : undefined,
97
117
  status === 'already-applied' ? 'semantic-edit-replay-already-applied' : undefined,
98
118
  status === 'blocked' ? 'semantic-edit-blocked' : undefined,
99
119
  status === 'conflict' ? 'semantic-edit-conflict' : undefined,
@@ -101,6 +121,19 @@ function derivedReasonCodes(summary, status) {
101
121
  ];
102
122
  }
103
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
+
104
137
  function readinessForStatus(status) {
105
138
  if (['ready', 'already-applied'].includes(status)) return 'ready';
106
139
  if (['blocked', 'conflict'].includes(status)) return 'blocked';
@@ -116,6 +149,20 @@ function actionForStatus(status) {
116
149
  return 'review';
117
150
  }
118
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
+
119
166
  function sourcePaths(scripts, projections, replays) {
120
167
  return uniqueStrings(strings([
121
168
  ...scripts.map((script) => script.sourcePath),
@@ -132,6 +179,42 @@ function countStatuses(...args) {
132
179
  return statuses.filter((status) => needles.has(status)).length;
133
180
  }
134
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
+
135
218
  function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
136
219
  function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
137
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
  });
@@ -0,0 +1,204 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+
3
+ export function projectionCoveredContainerOperationIds(operations, workerSourceText) {
4
+ if (typeof workerSourceText !== 'string') return new Set();
5
+ const result = new Set();
6
+ for (const operation of operations ?? []) {
7
+ if (!isProjectionCoverableContainer(operation)) continue;
8
+ if (workerContainerCoveredByInsertedChildren(operation, operations, workerSourceText)) result.add(operation.id);
9
+ }
10
+ return result;
11
+ }
12
+
13
+ export function spanOffsets(sourceText, span) {
14
+ if (typeof sourceText !== 'string' || !span) return undefined;
15
+ if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) {
16
+ return { start: span.start, end: span.end };
17
+ }
18
+ if (typeof span.startLine !== 'number') return undefined;
19
+ const lineStarts = [0];
20
+ for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
21
+ const startLine = Math.max(1, span.startLine);
22
+ const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
23
+ const start = lineStarts[startLine - 1];
24
+ const endLineStart = lineStarts[endLine - 1];
25
+ if (start === undefined || endLineStart === undefined) return undefined;
26
+ const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
27
+ const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
28
+ const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
29
+ return { start: start + startColumn, end: endLineStart + endColumn };
30
+ }
31
+
32
+ export function scopedBodyReplacement(operation, headSourceText, workerSourceText, headOffsets, workerOffsets) {
33
+ if (!isBodyReplacement(operation)) return undefined;
34
+ const head = bodyContentRange(headSourceText, headOffsets);
35
+ const worker = bodyContentRange(workerSourceText, workerOffsets);
36
+ if (!head || !worker) return undefined;
37
+ const headPrefix = headSourceText.slice(headOffsets.start, head.start);
38
+ const workerPrefix = workerSourceText.slice(workerOffsets.start, worker.start);
39
+ const headSuffix = headSourceText.slice(head.end, headOffsets.end);
40
+ const workerSuffix = workerSourceText.slice(worker.end, workerOffsets.end);
41
+ if (headPrefix !== workerPrefix || headSuffix !== workerSuffix) return undefined;
42
+ return { sourceRangeKind: 'body-content', head, worker };
43
+ }
44
+
45
+ export function bodyContentRange(sourceText, range) {
46
+ const pairs = bracePairs(sourceText, range);
47
+ const close = trailingBodyCloseOffset(sourceText, range);
48
+ const pair = close === undefined ? undefined : pairs.find((candidate) => candidate.close === close);
49
+ return pair ? { start: pair.open + 1, end: pair.close } : undefined;
50
+ }
51
+
52
+ export function insertionOffset(sourceText, insertion) {
53
+ if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
54
+ const mode = insertion?.mode;
55
+ if (mode === 'file-start') return { ok: true, offset: 0 };
56
+ if (mode === 'file-end') return { ok: true, offset: sourceText.length };
57
+ const range = spanOffsets(sourceText, insertion?.headSpan);
58
+ if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
59
+ if (mode === 'before') return { ok: true, offset: range.start };
60
+ if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
61
+ return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
62
+ }
63
+
64
+ export function removalRange(sourceText, span) {
65
+ const range = { ...span };
66
+ if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
67
+ else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
68
+ return range;
69
+ }
70
+
71
+ export function insertionReplacement(text, sourceText, offset) {
72
+ let replacement = String(text ?? '');
73
+ if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
74
+ if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
75
+ if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
76
+ if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
77
+ return replacement;
78
+ }
79
+
80
+ export function afterLineOffset(sourceText, offset) {
81
+ return sourceText[offset] === '\n' ? offset + 1 : offset;
82
+ }
83
+
84
+ function isProjectionCoverableContainer(operation) {
85
+ if (['portable', 'already-applied', 'covered'].includes(operation.status)) return false;
86
+ if (operation.changeKind !== 'modified') return false;
87
+ if (!operation.spans?.worker || !operation.hashes?.baseTextHash) return false;
88
+ const kind = String(operation.anchor?.regionKind ?? operation.regionKind ?? '');
89
+ return kind === 'type' || kind === 'config' || kind === 'content' || kind === 'route' || kind === 'property';
90
+ }
91
+
92
+ function workerContainerCoveredByInsertedChildren(container, operations, workerSourceText) {
93
+ const containerWorker = spanOffsets(workerSourceText, container.spans?.worker);
94
+ if (!containerWorker) return false;
95
+ const childRanges = (operations ?? [])
96
+ .filter((operation) => operation.id !== container.id)
97
+ .filter((operation) => operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add'))
98
+ .filter((operation) => ['portable', 'already-applied'].includes(operation.status))
99
+ .map((operation) => spanOffsets(workerSourceText, operation.spans?.worker))
100
+ .filter((range) => containedRange(range, containerWorker))
101
+ .map((range) => insertionRemovalRange(workerSourceText, range, containerWorker));
102
+ if (!childRanges.length) return false;
103
+ const stripped = childRanges
104
+ .sort((left, right) => right.start - left.start || right.end - left.end)
105
+ .reduce((text, range) => text.slice(0, range.start - containerWorker.start) + text.slice(range.end - containerWorker.start), workerSourceText.slice(containerWorker.start, containerWorker.end));
106
+ return hashSemanticValue(stripped) === container.hashes.baseTextHash;
107
+ }
108
+
109
+ function containedRange(inner, outer) {
110
+ return Boolean(inner && outer && outer.start <= inner.start && inner.end <= outer.end);
111
+ }
112
+
113
+ function insertionRemovalRange(sourceText, span, container) {
114
+ const range = { ...span };
115
+ if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
116
+ else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
117
+ return range;
118
+ }
119
+
120
+ function isBodyReplacement(operation) {
121
+ return operation.changeKind === 'modified' && (operation.kind === 'replaceBody' || operation.anchor?.regionKind === 'body');
122
+ }
123
+
124
+ function trailingBodyCloseOffset(sourceText, range) {
125
+ if (typeof sourceText !== 'string' || !range) return undefined;
126
+ let index = range.end - 1;
127
+ index = previousCodeOffset(sourceText, index, range.start);
128
+ if (sourceText[index] === ';' || sourceText[index] === ',') {
129
+ index -= 1;
130
+ index = previousCodeOffset(sourceText, index, range.start);
131
+ }
132
+ return sourceText[index] === '}' ? index : undefined;
133
+ }
134
+
135
+ function previousCodeOffset(sourceText, index, minIndex) {
136
+ let cursor = index;
137
+ while (cursor >= minIndex) {
138
+ while (cursor >= minIndex && /\s/.test(sourceText[cursor])) cursor -= 1;
139
+ const blockStart = sourceText.lastIndexOf('/*', cursor);
140
+ if (sourceText[cursor] === '/' && sourceText[cursor - 1] === '*' && blockStart >= minIndex) {
141
+ cursor = blockStart - 1;
142
+ continue;
143
+ }
144
+ const lineStart = Math.max(minIndex, sourceText.lastIndexOf('\n', cursor) + 1);
145
+ const lineComment = sourceText.lastIndexOf('//', cursor);
146
+ if (lineComment >= lineStart) {
147
+ cursor = lineComment - 1;
148
+ continue;
149
+ }
150
+ return cursor;
151
+ }
152
+ return cursor;
153
+ }
154
+
155
+ function bracePairs(sourceText, range) {
156
+ if (typeof sourceText !== 'string' || !range || range.end <= range.start) return [];
157
+ const stack = [];
158
+ const pairs = [];
159
+ let quote;
160
+ let escaped = false;
161
+ let lineComment = false;
162
+ let blockComment = false;
163
+ for (let index = range.start; index < range.end; index += 1) {
164
+ const char = sourceText[index];
165
+ const next = sourceText[index + 1];
166
+ if (lineComment) {
167
+ if (char === '\n' || char === '\r') lineComment = false;
168
+ continue;
169
+ }
170
+ if (blockComment) {
171
+ if (char === '*' && next === '/') {
172
+ blockComment = false;
173
+ index += 1;
174
+ }
175
+ continue;
176
+ }
177
+ if (quote) {
178
+ if (escaped) escaped = false;
179
+ else if (char === '\\') escaped = true;
180
+ else if (char === quote) quote = undefined;
181
+ continue;
182
+ }
183
+ if (char === '/' && next === '/') {
184
+ lineComment = true;
185
+ index += 1;
186
+ continue;
187
+ }
188
+ if (char === '/' && next === '*') {
189
+ blockComment = true;
190
+ index += 1;
191
+ continue;
192
+ }
193
+ if (char === '\'' || char === '"' || char === '`') {
194
+ quote = char;
195
+ continue;
196
+ }
197
+ if (char === '{') stack.push(index);
198
+ else if (char === '}') {
199
+ const open = stack.pop();
200
+ if (open !== undefined) pairs.push({ open, close: index });
201
+ }
202
+ }
203
+ return pairs;
204
+ }