@shapeshift-labs/frontier-lang-compiler 0.2.97 → 0.2.98
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-script.d.ts +2 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +43 -1
- package/dist/internal/index-impl/semanticEditOperationCoverage.js +141 -0
- package/dist/internal/index-impl/semanticEditScriptClassification.js +1 -0
- package/dist/internal/index-impl/semanticEditScripts.js +6 -2
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ export type SemanticEditScriptOperationStatus =
|
|
|
10
10
|
| 'candidate'
|
|
11
11
|
| 'portable'
|
|
12
12
|
| 'already-applied'
|
|
13
|
+
| 'covered'
|
|
13
14
|
| 'needs-port'
|
|
14
15
|
| 'conflict'
|
|
15
16
|
| 'stale'
|
|
@@ -82,6 +83,7 @@ export interface SemanticEditScriptSummary {
|
|
|
82
83
|
readonly conflicts: number;
|
|
83
84
|
readonly stale: number;
|
|
84
85
|
readonly blocked: number;
|
|
86
|
+
readonly covered?: number;
|
|
85
87
|
readonly candidates: number;
|
|
86
88
|
readonly autoMergeCandidates: number;
|
|
87
89
|
readonly semanticKeys?: readonly string[];
|
|
@@ -13,7 +13,12 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
13
13
|
if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
|
|
14
14
|
if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
|
|
15
15
|
const edits = [];
|
|
16
|
+
const coveredOperationIds = [];
|
|
16
17
|
for (const [index, operation] of (script.operations ?? []).entries()) {
|
|
18
|
+
if (operation.status === 'covered') {
|
|
19
|
+
coveredOperationIds.push(operation.id);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
17
22
|
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index);
|
|
18
23
|
if (edit.ok) edits.push(edit.value);
|
|
19
24
|
else reasonCodes.push(...edit.reasonCodes);
|
|
@@ -35,7 +40,7 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
35
40
|
headHash: script.headHash,
|
|
36
41
|
projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
|
|
37
42
|
appliedOperations: blocked ? [] : deduped.edits.map((edit) => edit.operationId),
|
|
38
|
-
skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : deduped.skippedOperationIds,
|
|
43
|
+
skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : uniqueStrings([...coveredOperationIds, ...deduped.skippedOperationIds]),
|
|
39
44
|
edits: blocked ? [] : deduped.edits.map(projectionEditRecord),
|
|
40
45
|
sourceText,
|
|
41
46
|
admission: {
|
|
@@ -66,6 +71,9 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
66
71
|
if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
|
|
67
72
|
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
|
|
68
73
|
}
|
|
74
|
+
if (operation.changeKind === 'removed' || String(operation.kind ?? '').startsWith('remove')) {
|
|
75
|
+
return removalEditForOperation(operation, identity, headSourceText, order);
|
|
76
|
+
}
|
|
69
77
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
70
78
|
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
71
79
|
const reasons = [];
|
|
@@ -99,6 +107,33 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
99
107
|
};
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
111
|
+
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
112
|
+
const reasons = [];
|
|
113
|
+
if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
|
|
114
|
+
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
115
|
+
const rawCurrent = headSourceText.slice(headOffsets.start, headOffsets.end);
|
|
116
|
+
const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
|
|
117
|
+
if (expectedHeadHash && hashSemanticValue(rawCurrent) !== expectedHeadHash) {
|
|
118
|
+
reasons.push(`head-span-hash-mismatch:${operation.id}`);
|
|
119
|
+
}
|
|
120
|
+
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
121
|
+
const range = removalRange(headSourceText, headOffsets);
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
value: {
|
|
125
|
+
operationId: operation.id,
|
|
126
|
+
order,
|
|
127
|
+
...identity,
|
|
128
|
+
editKind: 'delete',
|
|
129
|
+
start: range.start,
|
|
130
|
+
end: range.end,
|
|
131
|
+
current: headSourceText.slice(range.start, range.end),
|
|
132
|
+
replacement: ''
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
|
|
103
138
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
104
139
|
const reasons = [];
|
|
@@ -241,6 +276,13 @@ function insertionOffset(sourceText, insertion) {
|
|
|
241
276
|
return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
|
|
242
277
|
}
|
|
243
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
|
+
|
|
244
286
|
function insertionReplacement(text, sourceText, offset) {
|
|
245
287
|
let replacement = String(text ?? '');
|
|
246
288
|
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
2
|
+
import { nativeImportSourceText } from './nativeImportSourceText.js';
|
|
3
|
+
|
|
4
|
+
export function markCoveredSemanticEditOperations(operations, context) {
|
|
5
|
+
const sourceText = {
|
|
6
|
+
base: nativeImportSourceText(context.base),
|
|
7
|
+
worker: nativeImportSourceText(context.worker)
|
|
8
|
+
};
|
|
9
|
+
return (operations ?? []).map((operation) => {
|
|
10
|
+
const coveredBy = coveredByChildOperations(operation, operations, sourceText);
|
|
11
|
+
if (!coveredBy.length) return operation;
|
|
12
|
+
return {
|
|
13
|
+
...operation,
|
|
14
|
+
status: 'covered',
|
|
15
|
+
readiness: 'ready',
|
|
16
|
+
confidence: Math.max(operation.confidence ?? 0, 0.82),
|
|
17
|
+
reasonCodes: uniqueStrings([...(operation.reasonCodes ?? []), 'container-covered-by-child-edits']),
|
|
18
|
+
evidenceIds: uniqueStrings(operation.evidenceIds ?? []),
|
|
19
|
+
metadata: {
|
|
20
|
+
...(operation.metadata ?? {}),
|
|
21
|
+
coveredByOperationIds: coveredBy.map((child) => child.id)
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function coveredByChildOperations(container, operations, sourceText) {
|
|
28
|
+
if (!isCoverableContainer(container)) return [];
|
|
29
|
+
const containerBase = spanOffsets(sourceText.base, container.spans?.base);
|
|
30
|
+
const containerWorker = spanOffsets(sourceText.worker, container.spans?.worker);
|
|
31
|
+
if (!containerBase || !containerWorker) return [];
|
|
32
|
+
const childEdits = (operations ?? [])
|
|
33
|
+
.filter((operation) => operation.id !== container.id)
|
|
34
|
+
.map((operation) => childEdit(operation, sourceText, containerBase))
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
if (!childEdits.length) return [];
|
|
37
|
+
const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
|
|
38
|
+
const workerText = sourceText.worker.slice(containerWorker.start, containerWorker.end);
|
|
39
|
+
return applyLocalEdits(baseText, childEdits) === workerText ? childEdits.map((edit) => edit.operation) : [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isCoverableContainer(operation) {
|
|
43
|
+
if (operation.changeKind !== 'modified') return false;
|
|
44
|
+
if (!operation.spans?.base || !operation.spans?.worker) return false;
|
|
45
|
+
const kind = String(operation.anchor?.regionKind ?? operation.regionKind ?? '');
|
|
46
|
+
return kind === 'type' || kind === 'config' || kind === 'content' || kind === 'route' || kind === 'property';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function childEdit(operation, sourceText, containerBase) {
|
|
50
|
+
if (!['portable', 'already-applied'].includes(operation.status)) return undefined;
|
|
51
|
+
if (operation.changeKind === 'modified') return replacementChildEdit(operation, sourceText, containerBase);
|
|
52
|
+
if (operation.changeKind === 'added') return insertionChildEdit(operation, sourceText, containerBase);
|
|
53
|
+
if (operation.changeKind === 'removed') return removalChildEdit(operation, sourceText, containerBase);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function replacementChildEdit(operation, sourceText, containerBase) {
|
|
58
|
+
const base = spanOffsets(sourceText.base, operation.spans?.base);
|
|
59
|
+
const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
|
|
60
|
+
if (!contained(base, containerBase) || !worker) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
operation,
|
|
63
|
+
start: base.start - containerBase.start,
|
|
64
|
+
end: base.end - containerBase.start,
|
|
65
|
+
replacement: sourceText.worker.slice(worker.start, worker.end)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function insertionChildEdit(operation, sourceText, containerBase) {
|
|
70
|
+
const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
|
|
71
|
+
const offset = insertionOffset(sourceText.base, operation.insertion, containerBase);
|
|
72
|
+
if (!worker || offset === undefined) return undefined;
|
|
73
|
+
const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
|
|
74
|
+
const replacement = insertionReplacement(sourceText.worker.slice(worker.start, worker.end), baseText, offset);
|
|
75
|
+
return { operation, start: offset, end: offset, replacement };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function removalChildEdit(operation, sourceText, containerBase) {
|
|
79
|
+
const base = spanOffsets(sourceText.base, operation.spans?.base);
|
|
80
|
+
if (!contained(base, containerBase)) return undefined;
|
|
81
|
+
const range = removalRange(sourceText.base, base, containerBase);
|
|
82
|
+
return { operation, start: range.start - containerBase.start, end: range.end - containerBase.start, replacement: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function insertionOffset(sourceText, insertion, containerBase) {
|
|
86
|
+
const mode = insertion?.mode;
|
|
87
|
+
if (mode === 'file-start') return 0;
|
|
88
|
+
if (mode === 'file-end') return containerBase.end - containerBase.start;
|
|
89
|
+
const span = spanOffsets(sourceText, insertion?.baseSpan ?? insertion?.headSpan);
|
|
90
|
+
if (!contained(span, containerBase)) return undefined;
|
|
91
|
+
if (mode === 'before') return span.start - containerBase.start;
|
|
92
|
+
if (mode === 'after') return afterLineOffset(sourceText, span.end) - containerBase.start;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applyLocalEdits(sourceText, edits) {
|
|
97
|
+
return edits
|
|
98
|
+
.sort((left, right) => right.start - left.start || right.end - left.end)
|
|
99
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function spanOffsets(sourceText, span) {
|
|
103
|
+
if (typeof sourceText !== 'string' || !span) return undefined;
|
|
104
|
+
if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
|
|
105
|
+
if (typeof span.startLine !== 'number') return undefined;
|
|
106
|
+
const lineStarts = [0];
|
|
107
|
+
for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
|
|
108
|
+
const startLine = Math.max(1, span.startLine);
|
|
109
|
+
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
110
|
+
const start = lineStarts[startLine - 1];
|
|
111
|
+
const endLineStart = lineStarts[endLine - 1];
|
|
112
|
+
if (start === undefined || endLineStart === undefined) return undefined;
|
|
113
|
+
const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
|
|
114
|
+
const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
|
|
115
|
+
const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
|
|
116
|
+
return { start: start + startColumn, end: endLineStart + endColumn };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function contained(inner, outer) {
|
|
120
|
+
return Boolean(inner && outer && outer.start <= inner.start && inner.end <= outer.end);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function insertionReplacement(text, sourceText, offset) {
|
|
124
|
+
let replacement = String(text ?? '');
|
|
125
|
+
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
126
|
+
if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
127
|
+
if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
|
|
128
|
+
if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
129
|
+
return replacement;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function removalRange(sourceText, span, container) {
|
|
133
|
+
const range = { ...span };
|
|
134
|
+
if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
|
|
135
|
+
else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
136
|
+
return range;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function afterLineOffset(sourceText, offset) {
|
|
140
|
+
return sourceText[offset] === '\n' ? offset + 1 : offset;
|
|
141
|
+
}
|
|
@@ -43,6 +43,7 @@ export function summarizeSemanticEditOperations(operations) {
|
|
|
43
43
|
conflicts: byStatus.conflict ?? 0,
|
|
44
44
|
stale: byStatus.stale ?? 0,
|
|
45
45
|
blocked: byStatus.blocked ?? 0,
|
|
46
|
+
covered: byStatus.covered ?? 0,
|
|
46
47
|
candidates: byStatus.candidate ?? 0,
|
|
47
48
|
autoMergeCandidates: (byStatus.portable ?? 0) + (byStatus['already-applied'] ?? 0),
|
|
48
49
|
semanticKeys: uniqueStrings(operations.map((operation) => operation.semanticKey).filter(Boolean)),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
summarizeSemanticEditOperations
|
|
14
14
|
} from './semanticEditScriptClassification.js';
|
|
15
15
|
import { semanticEditInsertionAnchor } from './semanticEditInsertionAnchors.js';
|
|
16
|
+
import { markCoveredSemanticEditOperations } from './semanticEditOperationCoverage.js';
|
|
16
17
|
import { sourceTextForSpan } from './sourceTextForSpan.js';
|
|
17
18
|
import { semanticEditIdentityFields, semanticEditOperationContentHash } from './semanticEditIdentityRecords.js';
|
|
18
19
|
|
|
@@ -52,7 +53,10 @@ export function createSemanticEditScript(input = {}, options = {}) {
|
|
|
52
53
|
metadata: { source: 'createSemanticEditScript' }
|
|
53
54
|
}) : undefined;
|
|
54
55
|
const context = createEditContext({ base, worker, head, workerChangeSet, headChangeSet, headLineage });
|
|
55
|
-
const operations =
|
|
56
|
+
const operations = markCoveredSemanticEditOperations(
|
|
57
|
+
workerChangeSet.changedRegions.map((region, index) => semanticEditOperation(region, index, context, input)),
|
|
58
|
+
context
|
|
59
|
+
);
|
|
56
60
|
const summary = summarizeSemanticEditOperations(operations);
|
|
57
61
|
const admission = semanticEditAdmission({ operations, summary, head, workerChangeSet, headChangeSet, input });
|
|
58
62
|
const evidence = semanticEditEvidence({ input, language, sourcePath, workerChangeSet, headChangeSet, headLineage, summary, admission });
|
|
@@ -169,7 +173,7 @@ function semanticEditOperation(region, index, context, input) {
|
|
|
169
173
|
const identityRecord = semanticEditIdentityRecord({ kind, region, anchor });
|
|
170
174
|
const identity = semanticEditIdentityFields(identityRecord);
|
|
171
175
|
return compactRecord({
|
|
172
|
-
id: `semantic_edit_op_${idFragment([input.id ?? 'semantic_edit'
|
|
176
|
+
id: `semantic_edit_op_${idFragment([index, anchorKey, input.id ?? 'semantic_edit'].join(':'))}`,
|
|
173
177
|
kind,
|
|
174
178
|
changeKind: region.changeKind,
|
|
175
179
|
anchor,
|
package/package.json
CHANGED