@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.
- package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
- package/dist/declarations/semantic-edit-script.d.ts +34 -37
- package/dist/declarations/semantic-lineage.d.ts +63 -34
- package/dist/declarations/semantic-patch-bundle.d.ts +13 -0
- package/dist/internal/index-impl/declarationRecord.js +2 -2
- package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +56 -64
- package/dist/internal/index-impl/replaySemanticEditProjection.js +54 -22
- package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
- package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
- package/dist/internal/index-impl/semanticEditSourceRanges.js +204 -0
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +35 -1
- package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +150 -13
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +28 -1
- package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
- package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +6 -2
- package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
- package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
- package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
- package/dist/lightweight-dependency-relations.js +113 -7
- package/dist/native-import-utils.js +15 -1
- package/dist/native-region-scanner-js-helpers.js +61 -17
- package/dist/native-region-scanner-js.js +12 -4
- package/dist/semantic-import-regions.js +3 -3
- 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
|
|
58
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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(
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const startLine = Math.max(1, span.startLine);
|
|
215
|
-
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
216
|
-
const lineStart = starts[startLine - 1];
|
|
217
|
-
const endLineStart = starts[endLine - 1];
|
|
218
|
-
if (lineStart === undefined || endLineStart === undefined) return undefined;
|
|
219
|
-
const lineEnd = starts[endLine] === undefined ? sourceText.length : starts[endLine] - 1;
|
|
220
|
-
return { start: lineStart + Math.max(0, (span.startColumn ?? 1) - 1), end: endLineStart + (span.endColumn === undefined ? lineEnd - endLineStart : Math.max(0, span.endColumn - 1)) };
|
|
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
|
|
18
|
-
const
|
|
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
|
|
27
|
+
action: safeAction(input.action ?? options.action, status, positiveAutoApplyCandidate),
|
|
24
28
|
readiness,
|
|
25
|
-
reviewRequired: input.reviewRequired
|
|
26
|
-
autoApplyCandidate: input.autoApplyCandidate
|
|
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
|
|
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
|
+
}
|