@shapeshift-labs/frontier-lang-compiler 0.2.104 → 0.2.106
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/README.md +13 -1
- package/dist/declarations/js-ts-safe-member-merge.d.ts +29 -1
- package/dist/declarations/js-ts-safe-merge.d.ts +47 -0
- package/dist/declarations/native-project.d.ts +110 -0
- package/dist/internal/index-impl/createNativeProjectImportResult.js +177 -3
- package/dist/internal/index-impl/replaySemanticEditProjection.js +1 -1
- package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +200 -15
- package/dist/js-ts-safe-member-merge.js +69 -6
- package/dist/js-ts-safe-merge-composed.js +175 -0
- package/dist/js-ts-safe-merge-semantic-artifact-ledger.js +197 -0
- package/dist/js-ts-safe-merge-semantic-artifacts.js +266 -0
- package/dist/js-ts-safe-merge.js +6 -1
- package/dist/js-ts-semantic-merge-member-source.js +22 -4
- package/dist/js-ts-semantic-merge-parse.js +1 -0
- package/dist/js-ts-semantic-merge.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { idFragment } from './native-import-utils.js';
|
|
3
|
+
import { detectLineEnding, normalizeLineEndings } from './js-ts-safe-merge-context.js';
|
|
4
|
+
|
|
5
|
+
function createOperationsFromLedgers(context) {
|
|
6
|
+
const headByKey = new Map(context.head.entries.map((entry) => [entry.key, entry]));
|
|
7
|
+
return context.merged.entries.flatMap((entry, index) => {
|
|
8
|
+
const headEntry = headByKey.get(entry.key);
|
|
9
|
+
if (headEntry) {
|
|
10
|
+
if (sameEntryText(headEntry.text, entry.text)) return [];
|
|
11
|
+
return [operationRecord({
|
|
12
|
+
...context,
|
|
13
|
+
index,
|
|
14
|
+
kind: entry.kind === 'import' ? 'jsTsReplaceImport' : 'jsTsReplaceDeclaration',
|
|
15
|
+
changeKind: 'modified',
|
|
16
|
+
entry,
|
|
17
|
+
headEntry,
|
|
18
|
+
insertion: undefined,
|
|
19
|
+
currentText: headEntry.text,
|
|
20
|
+
replacementText: entry.text
|
|
21
|
+
})];
|
|
22
|
+
}
|
|
23
|
+
return [operationRecord({
|
|
24
|
+
...context,
|
|
25
|
+
index,
|
|
26
|
+
kind: entry.kind === 'import' ? 'jsTsInsertImport' : 'jsTsInsertDeclaration',
|
|
27
|
+
changeKind: 'added',
|
|
28
|
+
entry,
|
|
29
|
+
headEntry: undefined,
|
|
30
|
+
insertion: insertionAnchorForMergedEntry(entry, index, context),
|
|
31
|
+
currentText: '',
|
|
32
|
+
replacementText: insertionText(entry.text, context.head.sourceText)
|
|
33
|
+
})];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function operationRecord(input) {
|
|
38
|
+
const anchor = entryAnchor(input.entry, input.sourcePath, input.language);
|
|
39
|
+
const operation = {
|
|
40
|
+
id: `js_ts_safe_merge_op_${idFragment([input.id, input.index, input.entry.key].join(':'))}`,
|
|
41
|
+
kind: input.kind,
|
|
42
|
+
changeKind: input.changeKind,
|
|
43
|
+
anchor,
|
|
44
|
+
insertion: input.insertion,
|
|
45
|
+
spans: {
|
|
46
|
+
head: input.headEntry ? spanForEntry(input.headEntry) : undefined,
|
|
47
|
+
worker: spanForEntry(input.entry)
|
|
48
|
+
},
|
|
49
|
+
hashes: {
|
|
50
|
+
baseSourceHash: input.input.baseHash,
|
|
51
|
+
workerSourceHash: input.input.workerHash,
|
|
52
|
+
headSourceHash: input.input.headHash,
|
|
53
|
+
headTextHash: hashSemanticValue(input.currentText),
|
|
54
|
+
workerTextHash: hashSemanticValue(input.replacementText)
|
|
55
|
+
},
|
|
56
|
+
status: 'portable',
|
|
57
|
+
readiness: 'ready',
|
|
58
|
+
confidence: 1,
|
|
59
|
+
reasonCodes: ['js-ts-safe-merge-gates-passed', 'js-ts-ledger-source-edit'],
|
|
60
|
+
evidenceIds: [`evidence_${idFragment(input.id)}_js_ts_safe_merge_semantic_replay`],
|
|
61
|
+
metadata: {
|
|
62
|
+
autoMergeClaim: false,
|
|
63
|
+
semanticEquivalenceClaim: false,
|
|
64
|
+
ledgerKey: input.entry.key
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return { ...operation, operationContentHash: hashSemanticValue(operation) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sourceEditForOperation(operation, order, headSourceText, mergedSourceText) {
|
|
71
|
+
const headStart = operation.spans.head?.start ?? insertionOffsetFromAnchor(operation.insertion, headSourceText);
|
|
72
|
+
const headEnd = operation.spans.head?.end ?? headStart;
|
|
73
|
+
const workerStart = operation.spans.worker?.start ?? 0;
|
|
74
|
+
const workerEnd = operation.spans.worker?.end ?? workerStart;
|
|
75
|
+
const current = headSourceText.slice(headStart, headEnd);
|
|
76
|
+
const replacementSpanText = mergedSourceText.slice(workerStart, workerEnd);
|
|
77
|
+
const replacement = operation.changeKind === 'added'
|
|
78
|
+
? insertionText(replacementSpanText, headSourceText)
|
|
79
|
+
: replacementSpanText;
|
|
80
|
+
return {
|
|
81
|
+
operationId: operation.id,
|
|
82
|
+
order,
|
|
83
|
+
kind: operation.kind,
|
|
84
|
+
editKind: operation.changeKind === 'added' ? 'insert' : 'replace',
|
|
85
|
+
changeKind: operation.changeKind,
|
|
86
|
+
anchorKey: operation.anchor.key,
|
|
87
|
+
conflictKey: operation.anchor.conflictKey,
|
|
88
|
+
regionId: operation.anchor.regionId,
|
|
89
|
+
regionKind: operation.anchor.regionKind,
|
|
90
|
+
sourcePath: operation.anchor.sourcePath,
|
|
91
|
+
symbolId: operation.anchor.symbolId,
|
|
92
|
+
symbolName: operation.anchor.symbolName,
|
|
93
|
+
symbolKind: operation.anchor.symbolKind,
|
|
94
|
+
operationContentHash: operation.operationContentHash,
|
|
95
|
+
insertion: operation.insertion,
|
|
96
|
+
start: headStart,
|
|
97
|
+
end: headEnd,
|
|
98
|
+
workerStart,
|
|
99
|
+
workerEnd,
|
|
100
|
+
current,
|
|
101
|
+
replacement,
|
|
102
|
+
replacementSpanText
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function insertionAnchorForMergedEntry(entry, index, context) {
|
|
107
|
+
const previous = nearestHeadEntry(context.merged.entries, context.head.entries, index, -1);
|
|
108
|
+
if (previous) return {
|
|
109
|
+
mode: 'after',
|
|
110
|
+
anchorKey: previous.key,
|
|
111
|
+
anchorSymbolName: entryName(previous),
|
|
112
|
+
anchorSymbolKind: entrySymbolKind(previous),
|
|
113
|
+
headSpan: spanForEntry(previous),
|
|
114
|
+
sourcePath: context.sourcePath
|
|
115
|
+
};
|
|
116
|
+
const next = nearestHeadEntry(context.merged.entries, context.head.entries, index, 1);
|
|
117
|
+
if (next) return {
|
|
118
|
+
mode: 'before',
|
|
119
|
+
anchorKey: next.key,
|
|
120
|
+
anchorSymbolName: entryName(next),
|
|
121
|
+
anchorSymbolKind: entrySymbolKind(next),
|
|
122
|
+
headSpan: spanForEntry(next),
|
|
123
|
+
sourcePath: context.sourcePath
|
|
124
|
+
};
|
|
125
|
+
return { mode: 'file-end', sourcePath: context.sourcePath };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function nearestHeadEntry(mergedEntries, headEntries, startIndex, step) {
|
|
129
|
+
const headByKey = new Map(headEntries.map((entry) => [entry.key, entry]));
|
|
130
|
+
for (let index = startIndex + step; index >= 0 && index < mergedEntries.length; index += step) {
|
|
131
|
+
const headEntry = headByKey.get(mergedEntries[index].key);
|
|
132
|
+
if (headEntry) return headEntry;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function insertionOffsetFromAnchor(insertion, sourceText) {
|
|
138
|
+
if (insertion?.mode === 'file-start') return 0;
|
|
139
|
+
if (insertion?.mode === 'file-end') return sourceText.length;
|
|
140
|
+
const span = insertion?.headSpan;
|
|
141
|
+
if (!span) return sourceText.length;
|
|
142
|
+
if (insertion.mode === 'before') return lineStartOffset(sourceText, span.start);
|
|
143
|
+
return lineEndOffset(sourceText, span.end);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function entryAnchor(entry, sourcePath, language) {
|
|
147
|
+
const name = entryName(entry);
|
|
148
|
+
const key = `source#${sourcePath ?? 'unknown'}#${entry.kind}#${name ?? entry.key}`;
|
|
149
|
+
return {
|
|
150
|
+
key,
|
|
151
|
+
conflictKey: key,
|
|
152
|
+
regionId: key,
|
|
153
|
+
regionKind: entry.kind === 'import' ? 'import' : 'declaration',
|
|
154
|
+
granularity: 'js-ts-ledger-entry',
|
|
155
|
+
language,
|
|
156
|
+
sourcePath,
|
|
157
|
+
symbolId: key,
|
|
158
|
+
symbolName: name,
|
|
159
|
+
symbolKind: entrySymbolKind(entry),
|
|
160
|
+
sourceSpan: spanForEntry(entry)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function entryName(entry) {
|
|
165
|
+
if (entry.kind === 'import') return entry.importInfo?.moduleSpecifier;
|
|
166
|
+
return entry.names?.join('|') ?? entry.key;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function entrySymbolKind(entry) {
|
|
170
|
+
if (entry.kind === 'import') return 'import';
|
|
171
|
+
return entry.declarationInfo?.declarationKind ?? entry.kind;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function spanForEntry(entry) {
|
|
175
|
+
return { start: entry.start, end: entry.end };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function insertionText(text, sourceText) {
|
|
179
|
+
const lineEnding = detectLineEnding(sourceText);
|
|
180
|
+
const normalized = normalizeLineEndings(String(text ?? '').trimEnd(), lineEnding);
|
|
181
|
+
return normalized.endsWith(lineEnding) ? normalized : `${normalized}${lineEnding}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sameEntryText(left, right) {
|
|
185
|
+
return normalizeLineEndings(String(left ?? '').trim(), '\n') === normalizeLineEndings(String(right ?? '').trim(), '\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function lineStartOffset(sourceText, offset) {
|
|
189
|
+
return sourceText.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function lineEndOffset(sourceText, offset) {
|
|
193
|
+
const lineEnd = sourceText.indexOf('\n', offset);
|
|
194
|
+
return lineEnd === -1 ? sourceText.length : lineEnd + 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { createOperationsFromLedgers, sourceEditForOperation };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { idFragment, uniqueStrings } from './native-import-utils.js';
|
|
3
|
+
import { replaySemanticEditProjection } from './internal/index-impl/replaySemanticEditProjection.js';
|
|
4
|
+
import { projectionEditRecord } from './internal/index-impl/semanticEditProjectionRecord.js';
|
|
5
|
+
import { applySourceEdits, dedupeSourceEdits, validateSourceEdits } from './internal/index-impl/semanticSourceEditDedupe.js';
|
|
6
|
+
import { createMergeContext } from './js-ts-safe-merge-context.js';
|
|
7
|
+
import { scanJsTsTopLevelLedger } from './js-ts-safe-merge-ledger.js';
|
|
8
|
+
import { createOperationsFromLedgers, sourceEditForOperation } from './js-ts-safe-merge-semantic-artifact-ledger.js';
|
|
9
|
+
|
|
10
|
+
function createJsTsSafeMergeSemanticArtifacts(input = {}, merge = {}) {
|
|
11
|
+
const headSourceText = input.headSourceText;
|
|
12
|
+
const mergedSourceText = merge.mergedSourceText ?? merge.outputSourceText;
|
|
13
|
+
const language = input.language ?? merge.language ?? 'typescript';
|
|
14
|
+
const sourcePath = input.sourcePath ?? merge.sourcePath ?? 'inline.js';
|
|
15
|
+
const id = String(input.id ?? merge.id ?? 'js_ts_safe_merge');
|
|
16
|
+
const baseReasonCodes = [];
|
|
17
|
+
if (typeof headSourceText !== 'string') baseReasonCodes.push('missing-head-source-text');
|
|
18
|
+
if (typeof mergedSourceText !== 'string') baseReasonCodes.push('missing-merged-source-text');
|
|
19
|
+
if (baseReasonCodes.length) return blockedArtifacts({ id, language, sourcePath, reasonCodes: baseReasonCodes, merge });
|
|
20
|
+
|
|
21
|
+
const ledgerContext = createMergeContext({ ...input, id: `${id}_semantic_artifacts`, language, sourcePath });
|
|
22
|
+
const head = scanJsTsTopLevelLedger(headSourceText, 'head', ledgerContext);
|
|
23
|
+
const merged = scanJsTsTopLevelLedger(mergedSourceText, 'merged', ledgerContext);
|
|
24
|
+
if (ledgerContext.conflicts.length) {
|
|
25
|
+
return blockedArtifacts({
|
|
26
|
+
id,
|
|
27
|
+
language,
|
|
28
|
+
sourcePath,
|
|
29
|
+
reasonCodes: uniqueStrings(ledgerContext.conflicts.map((conflict) => conflict.code)),
|
|
30
|
+
merge,
|
|
31
|
+
ledgers: { head, merged }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const operations = createOperationsFromLedgers({ id, language, sourcePath, head, merged, input, merge });
|
|
36
|
+
const rawEdits = operations.map((operation, order) => sourceEditForOperation(operation, order, headSourceText, mergedSourceText));
|
|
37
|
+
const deduped = dedupeSourceEdits(rawEdits);
|
|
38
|
+
const reasonCodes = uniqueStrings([
|
|
39
|
+
...validateSourceEdits(deduped.edits),
|
|
40
|
+
...deduped.skippedOperationIds.map((operationId) => `source-edit-deduped:${operationId}`)
|
|
41
|
+
]);
|
|
42
|
+
const sourceText = reasonCodes.length ? undefined : applySourceEdits(headSourceText, deduped.edits);
|
|
43
|
+
if (sourceText !== mergedSourceText) reasonCodes.push('projected-source-mismatch');
|
|
44
|
+
const blocked = reasonCodes.length > 0;
|
|
45
|
+
const script = createScript({ id, language, sourcePath, input, merge, operations, blocked, reasonCodes });
|
|
46
|
+
const projection = createProjection({
|
|
47
|
+
id,
|
|
48
|
+
language,
|
|
49
|
+
sourcePath,
|
|
50
|
+
input,
|
|
51
|
+
merge,
|
|
52
|
+
script,
|
|
53
|
+
edits: blocked ? [] : deduped.edits,
|
|
54
|
+
skippedOperationIds: deduped.skippedOperationIds,
|
|
55
|
+
sourceText: blocked ? undefined : sourceText,
|
|
56
|
+
reasonCodes
|
|
57
|
+
});
|
|
58
|
+
const replay = replaySemanticEditProjection({
|
|
59
|
+
id: `js_ts_safe_merge_replay_${idFragment(id)}`,
|
|
60
|
+
projection,
|
|
61
|
+
currentSourceText: headSourceText,
|
|
62
|
+
currentSourcePath: sourcePath,
|
|
63
|
+
language
|
|
64
|
+
});
|
|
65
|
+
const alreadyAppliedReplay = replaySemanticEditProjection({
|
|
66
|
+
id: `js_ts_safe_merge_replay_already_applied_${idFragment(id)}`,
|
|
67
|
+
projection,
|
|
68
|
+
currentSourceText: mergedSourceText,
|
|
69
|
+
currentSourcePath: sourcePath,
|
|
70
|
+
language
|
|
71
|
+
});
|
|
72
|
+
const replayReady = replay.status === 'accepted-clean' && replay.outputSourceText === mergedSourceText;
|
|
73
|
+
const alreadyAppliedReady = alreadyAppliedReplay.status === 'already-applied';
|
|
74
|
+
const status = !blocked && replayReady && alreadyAppliedReady ? 'verified' : 'blocked';
|
|
75
|
+
const finalReasonCodes = uniqueStrings([
|
|
76
|
+
...reasonCodes,
|
|
77
|
+
replayReady ? undefined : `semantic-replay-${replay.status}`,
|
|
78
|
+
alreadyAppliedReady ? undefined : `semantic-replay-already-applied-${alreadyAppliedReplay.status}`
|
|
79
|
+
]);
|
|
80
|
+
const core = {
|
|
81
|
+
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
82
|
+
version: 1,
|
|
83
|
+
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
84
|
+
id: `js_ts_safe_merge_semantic_artifacts_${idFragment(id)}`,
|
|
85
|
+
sourcePath,
|
|
86
|
+
language,
|
|
87
|
+
status,
|
|
88
|
+
script,
|
|
89
|
+
projection,
|
|
90
|
+
replay,
|
|
91
|
+
alreadyAppliedReplay,
|
|
92
|
+
admission: {
|
|
93
|
+
status: status === 'verified' ? 'auto-merge-candidate' : 'blocked',
|
|
94
|
+
action: status === 'verified' ? 'apply' : 'human-review',
|
|
95
|
+
reviewRequired: status !== 'verified',
|
|
96
|
+
autoApplyCandidate: status === 'verified',
|
|
97
|
+
autoMergeClaim: false,
|
|
98
|
+
semanticEquivalenceClaim: false,
|
|
99
|
+
reasonCodes: finalReasonCodes
|
|
100
|
+
},
|
|
101
|
+
summary: {
|
|
102
|
+
operations: operations.length,
|
|
103
|
+
edits: projection.edits.length,
|
|
104
|
+
replayStatus: replay.status,
|
|
105
|
+
alreadyAppliedReplayStatus: alreadyAppliedReplay.status,
|
|
106
|
+
projectedSourceMatchesMerged: projection.sourceText === mergedSourceText,
|
|
107
|
+
replayOutputMatchesMerged: replay.outputSourceText === mergedSourceText
|
|
108
|
+
},
|
|
109
|
+
evidence: [{
|
|
110
|
+
id: `evidence_${idFragment(id)}_js_ts_safe_merge_semantic_replay`,
|
|
111
|
+
kind: 'js-ts-safe-merge-semantic-replay',
|
|
112
|
+
status: status === 'verified' ? 'passed' : 'needs-review',
|
|
113
|
+
path: sourcePath,
|
|
114
|
+
summary: status === 'verified'
|
|
115
|
+
? `JS/TS safe merge replay verified ${operations.length} semantic source edit(s).`
|
|
116
|
+
: `JS/TS safe merge semantic replay requires review: ${finalReasonCodes.join(', ')}.`,
|
|
117
|
+
metadata: {
|
|
118
|
+
scriptId: script.id,
|
|
119
|
+
projectionId: projection.id,
|
|
120
|
+
replayId: replay.id,
|
|
121
|
+
alreadyAppliedReplayId: alreadyAppliedReplay.id,
|
|
122
|
+
autoMergeClaim: false,
|
|
123
|
+
semanticEquivalenceClaim: false
|
|
124
|
+
}
|
|
125
|
+
}],
|
|
126
|
+
metadata: {
|
|
127
|
+
autoMergeClaim: false,
|
|
128
|
+
semanticEquivalenceClaim: false,
|
|
129
|
+
source: 'js-ts-safe-merge-ledger',
|
|
130
|
+
mergeGateIds: (merge.gates ?? []).map((gate) => gate.id)
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createScript(input) {
|
|
137
|
+
const statuses = countBy(input.operations, (operation) => operation.status);
|
|
138
|
+
const kinds = countBy(input.operations, (operation) => operation.kind);
|
|
139
|
+
const reasonCodes = uniqueStrings(input.reasonCodes);
|
|
140
|
+
const core = {
|
|
141
|
+
kind: 'frontier.lang.semanticEditScript',
|
|
142
|
+
version: 1,
|
|
143
|
+
schema: 'frontier.lang.semanticEditScript.v1',
|
|
144
|
+
id: `js_ts_safe_merge_script_${idFragment(input.id)}`,
|
|
145
|
+
stableId: `js_ts_safe_merge_script_${idFragment([input.id, input.merge.mergedSourceText].join(':'))}`,
|
|
146
|
+
language: input.language,
|
|
147
|
+
sourcePath: input.sourcePath,
|
|
148
|
+
baseHash: input.input.baseHash,
|
|
149
|
+
workerHash: input.input.workerHash,
|
|
150
|
+
headHash: input.input.headHash,
|
|
151
|
+
workerChangeSetId: input.input.workerChangeSetId,
|
|
152
|
+
headChangeSetId: input.input.headChangeSetId,
|
|
153
|
+
operations: input.operations,
|
|
154
|
+
summary: {
|
|
155
|
+
operations: input.operations.length,
|
|
156
|
+
byStatus: statuses,
|
|
157
|
+
byKind: kinds,
|
|
158
|
+
portable: statuses.portable ?? 0,
|
|
159
|
+
alreadyApplied: 0,
|
|
160
|
+
needsPort: 0,
|
|
161
|
+
conflicts: 0,
|
|
162
|
+
stale: 0,
|
|
163
|
+
blocked: input.blocked ? input.operations.length : 0,
|
|
164
|
+
candidates: 0,
|
|
165
|
+
autoMergeCandidates: input.blocked ? 0 : input.operations.length,
|
|
166
|
+
operationContentHashes: input.operations.map((operation) => operation.operationContentHash)
|
|
167
|
+
},
|
|
168
|
+
admission: {
|
|
169
|
+
status: input.blocked ? 'blocked' : 'auto-merge-candidate',
|
|
170
|
+
action: input.blocked ? 'block' : 'run-gates-and-apply',
|
|
171
|
+
reviewRequired: input.blocked,
|
|
172
|
+
autoApplyCandidate: !input.blocked,
|
|
173
|
+
autoMergeClaim: false,
|
|
174
|
+
semanticEquivalenceClaim: false,
|
|
175
|
+
reasonCodes,
|
|
176
|
+
conflictKeys: input.operations.map((operation) => operation.anchor.conflictKey),
|
|
177
|
+
evidenceIds: input.operations.flatMap((operation) => operation.evidenceIds ?? [])
|
|
178
|
+
},
|
|
179
|
+
evidence: [],
|
|
180
|
+
metadata: {
|
|
181
|
+
autoMergeClaim: false,
|
|
182
|
+
semanticEquivalenceClaim: false,
|
|
183
|
+
source: 'js-ts-safe-merge-ledger'
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function createProjection(input) {
|
|
190
|
+
const blocked = input.reasonCodes.length > 0;
|
|
191
|
+
const edits = blocked ? [] : input.edits.map(projectionEditRecord);
|
|
192
|
+
const core = {
|
|
193
|
+
kind: 'frontier.lang.semanticEditProjection',
|
|
194
|
+
version: 1,
|
|
195
|
+
id: `js_ts_safe_merge_projection_${idFragment(input.id)}`,
|
|
196
|
+
scriptId: input.script.id,
|
|
197
|
+
status: blocked ? 'blocked' : 'projected',
|
|
198
|
+
sourcePath: input.sourcePath,
|
|
199
|
+
language: input.language,
|
|
200
|
+
baseHash: input.input.baseHash,
|
|
201
|
+
workerHash: input.input.workerHash,
|
|
202
|
+
headHash: input.input.headHash,
|
|
203
|
+
projectedHash: input.sourceText === undefined ? undefined : hashSemanticValue(input.sourceText),
|
|
204
|
+
appliedOperations: edits.map((edit) => edit.operationId).filter(Boolean),
|
|
205
|
+
skippedOperations: blocked ? input.script.operations.map((operation) => operation.id) : input.skippedOperationIds,
|
|
206
|
+
edits,
|
|
207
|
+
sourceText: input.sourceText,
|
|
208
|
+
admission: {
|
|
209
|
+
status: blocked ? 'blocked' : 'auto-merge-candidate',
|
|
210
|
+
autoMergeClaim: false,
|
|
211
|
+
semanticEquivalenceClaim: false,
|
|
212
|
+
reasonCodes: uniqueStrings(input.reasonCodes)
|
|
213
|
+
},
|
|
214
|
+
metadata: {
|
|
215
|
+
autoMergeClaim: false,
|
|
216
|
+
semanticEquivalenceClaim: false,
|
|
217
|
+
source: 'js-ts-safe-merge-ledger'
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function blockedArtifacts(input) {
|
|
224
|
+
const core = {
|
|
225
|
+
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
226
|
+
version: 1,
|
|
227
|
+
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
228
|
+
id: `js_ts_safe_merge_semantic_artifacts_${idFragment(input.id)}`,
|
|
229
|
+
sourcePath: input.sourcePath,
|
|
230
|
+
language: input.language,
|
|
231
|
+
status: 'blocked',
|
|
232
|
+
admission: {
|
|
233
|
+
status: 'blocked',
|
|
234
|
+
action: 'human-review',
|
|
235
|
+
reviewRequired: true,
|
|
236
|
+
autoApplyCandidate: false,
|
|
237
|
+
autoMergeClaim: false,
|
|
238
|
+
semanticEquivalenceClaim: false,
|
|
239
|
+
reasonCodes: uniqueStrings(input.reasonCodes)
|
|
240
|
+
},
|
|
241
|
+
summary: {
|
|
242
|
+
operations: 0,
|
|
243
|
+
edits: 0,
|
|
244
|
+
projectedSourceMatchesMerged: false,
|
|
245
|
+
replayOutputMatchesMerged: false
|
|
246
|
+
},
|
|
247
|
+
evidence: [],
|
|
248
|
+
metadata: {
|
|
249
|
+
autoMergeClaim: false,
|
|
250
|
+
semanticEquivalenceClaim: false,
|
|
251
|
+
source: 'js-ts-safe-merge-ledger'
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function countBy(values, keyFor) {
|
|
258
|
+
const result = {};
|
|
259
|
+
for (const value of values) {
|
|
260
|
+
const key = keyFor(value);
|
|
261
|
+
result[key] = (result[key] ?? 0) + 1;
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { createJsTsSafeMergeSemanticArtifacts };
|
package/dist/js-ts-safe-merge.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from './js-ts-safe-merge-constants.js';
|
|
9
9
|
import { indexBaseLedger, scanJsTsTopLevelLedger, validateLedgerUniqueness } from './js-ts-safe-merge-ledger.js';
|
|
10
10
|
import { applySourceMergePlan, createSourceMergePlan } from './js-ts-safe-merge-plan.js';
|
|
11
|
+
import { createJsTsSafeMergeSemanticArtifacts } from './js-ts-safe-merge-semantic-artifacts.js';
|
|
11
12
|
|
|
12
13
|
export { JsTsSafeMergeConflictCodes, JsTsSafeMergeGateIds, JsTsSafeMergeStatuses };
|
|
13
14
|
|
|
@@ -61,7 +62,7 @@ export function safeMergeJsTsImportsAndDeclarations(input = {}) {
|
|
|
61
62
|
if (!context.conflicts.length) validateLedgerUniqueness(merged, context);
|
|
62
63
|
if (context.conflicts.length) return blockedResult(context, { base, worker, head, merged });
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
const result = {
|
|
65
66
|
kind: 'frontier.lang.jsTsSafeMerge',
|
|
66
67
|
version: 1,
|
|
67
68
|
schema: 'frontier.lang.jsTsSafeMerge.v1',
|
|
@@ -99,6 +100,10 @@ export function safeMergeJsTsImportsAndDeclarations(input = {}) {
|
|
|
99
100
|
currentSourceHash: input.currentSourceHash
|
|
100
101
|
})
|
|
101
102
|
};
|
|
103
|
+
return {
|
|
104
|
+
...result,
|
|
105
|
+
semanticArtifacts: createJsTsSafeMergeSemanticArtifacts(input, result)
|
|
106
|
+
};
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
function validateStaleSourceHashes(input, context) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { findContainer } from './js-ts-semantic-merge-member-containers.js';
|
|
2
|
+
import { uniqueStrings } from './js-ts-semantic-merge-member-utils.js';
|
|
3
|
+
|
|
1
4
|
function canonicalizeSourceBodies(sourceText, preparedRegions, side) {
|
|
2
5
|
let output = sourceText;
|
|
3
6
|
const replacements = preparedRegions
|
|
@@ -10,15 +13,29 @@ function canonicalizeSourceBodies(sourceText, preparedRegions, side) {
|
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
function applyMemberAdditions(headSourceText, preparedRegions) {
|
|
13
|
-
|
|
16
|
+
return applyPreparedMemberAdditions(headSourceText, preparedRegions, ['worker']).sourceText;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyPreparedMemberAdditions(sourceText, preparedRegions, sides = ['worker']) {
|
|
20
|
+
let output = sourceText;
|
|
21
|
+
const reasonCodes = [];
|
|
14
22
|
const replacements = preparedRegions
|
|
15
|
-
.
|
|
16
|
-
|
|
23
|
+
.map((region) => {
|
|
24
|
+
const match = findContainer(sourceText, region.policy);
|
|
25
|
+
if (match.reasonCodes.length) {
|
|
26
|
+
reasonCodes.push(...match.reasonCodes.map((reason) => `target-${reason}:${region.kind}:${region.name}`));
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const members = sides.flatMap((side) => region[`${side}AddedMembers`] ?? []);
|
|
30
|
+
if (!members.length) return undefined;
|
|
31
|
+
return { range: match.value, replacement: appendMembersToBody(match.value.body, members) };
|
|
32
|
+
})
|
|
33
|
+
.filter(Boolean)
|
|
17
34
|
.sort((left, right) => right.range.bodyStart - left.range.bodyStart);
|
|
18
35
|
for (const { range, replacement } of replacements) {
|
|
19
36
|
output = `${output.slice(0, range.bodyStart)}${replacement}${output.slice(range.bodyEnd)}`;
|
|
20
37
|
}
|
|
21
|
-
return output;
|
|
38
|
+
return { sourceText: output, reasonCodes: uniqueStrings(reasonCodes) };
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
function appendMembersToBody(body, members) {
|
|
@@ -60,5 +77,6 @@ function leadingWhitespace(line) {
|
|
|
60
77
|
|
|
61
78
|
export {
|
|
62
79
|
applyMemberAdditions,
|
|
80
|
+
applyPreparedMemberAdditions,
|
|
63
81
|
canonicalizeSourceBodies
|
|
64
82
|
};
|
package/package.json
CHANGED