@shapeshift-labs/frontier-lang-compiler 0.2.101 → 0.2.103
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/import-adapter-core.d.ts +6 -0
- package/dist/declarations/native-project-admission-semantic-evidence.d.ts +34 -0
- package/dist/declarations/native-project-admission.d.ts +6 -10
- package/dist/declarations/semantic-edit-script.d.ts +3 -0
- package/dist/declarations/semantic-patch-bundle-overlaps.d.ts +1 -0
- package/dist/internal/index-impl/createLightweightNativeImport.js +9 -1
- package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +14 -2
- package/dist/internal/index-impl/diffNativeSymbols.js +82 -1
- package/dist/internal/index-impl/importNativeSource.js +14 -14
- package/dist/internal/index-impl/nativeImportSemanticIndex.js +33 -0
- package/dist/internal/index-impl/projectImportAdmissionImportEvidence.js +1 -1
- package/dist/internal/index-impl/projectImportAdmissionSemanticWarnings.js +178 -0
- package/dist/internal/index-impl/projectImportAdmissionSummaries.js +22 -3
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +12 -62
- package/dist/internal/index-impl/replaySemanticEditProjection.js +43 -63
- package/dist/internal/index-impl/semanticEditImportProjection.js +53 -0
- package/dist/internal/index-impl/semanticEditProjectionRecord.js +79 -0
- package/dist/internal/index-impl/semanticEditReplayAnchors.js +63 -0
- package/dist/internal/index-impl/semanticEditSourceRanges.js +5 -0
- package/dist/internal/index-impl/semanticPatchBundleAdmission.js +51 -2
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +33 -16
- package/dist/semantic-import-regions.js +12 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import{countBy,maxSemanticMergeReadiness,uniqueRecordsById,uniqueStrings}from'../../native-import-utils.js';
|
|
2
2
|
import{createSemanticMergeCandidateAdmissionRecord,querySemanticMergeCandidateAdmissionOverlaps,sortSemanticMergeCandidateAdmissionRecords}from'./semanticMergeCandidateRecords.js';
|
|
3
|
-
import{compactAdmissionSource,importLosses,sourceLossClasses,summarizeImportPreservation,summarizeParserEvidence}from'./projectImportAdmissionImportEvidence.js';
|
|
3
|
+
import{compactAdmissionSource,importLosses,sourceLossClasses,summarizeImportPreservation,summarizeParserEvidence,summarizeSemanticAdmissionWarnings}from'./projectImportAdmissionImportEvidence.js';
|
|
4
4
|
import{sourceMissingEvidence,sourceMissingTasks,sourceSemanticMergeScore}from'./projectImportAdmissionTasks.js';
|
|
5
5
|
import{candidateRisk,maxPreservationQuality,maxRisk,normalizeRisk}from'./projectImportAdmissionRanks.js';
|
|
6
6
|
|
|
7
7
|
export{admissionLanguages}from'./projectImportAdmissionLanguageSummaries.js';
|
|
8
8
|
|
|
9
|
-
export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
|
|
9
|
+
export function projectAdmissionImports(imports,sourceRows,mergeCandidates,projectResult){
|
|
10
10
|
return imports.map((imported,index)=>{
|
|
11
11
|
const source=sourceRows?.[index]??compactAdmissionSource(imported,index);
|
|
12
12
|
const sourcePath=source.sourcePath??imported?.sourcePath;
|
|
@@ -22,6 +22,7 @@ export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
|
|
|
22
22
|
};
|
|
23
23
|
const readiness=source.readiness??imported?.metadata?.semanticMergeReadiness??candidates[0]?.readiness??'ready';
|
|
24
24
|
const emptySemanticEvidence=Object.values(semanticCounts).reduce((sum,value)=>sum+value,0)===0;
|
|
25
|
+
const semanticAdmission=summarizeSemanticAdmissionWarnings(imported,{source,sourcePath,candidates,projectResult});
|
|
25
26
|
const sourcePreservation=summarizeImportPreservation(imported,source);
|
|
26
27
|
const losses=importLosses(imported);
|
|
27
28
|
const lossClasses=sourceLossClasses(imported,losses);
|
|
@@ -55,6 +56,7 @@ export function projectAdmissionImports(imports,sourceRows,mergeCandidates){
|
|
|
55
56
|
readiness,
|
|
56
57
|
semanticCounts,
|
|
57
58
|
emptySemanticEvidence,
|
|
59
|
+
semanticAdmission,
|
|
58
60
|
parserEvidence,
|
|
59
61
|
lossClasses,
|
|
60
62
|
missingEvidence,
|
|
@@ -90,6 +92,7 @@ export function admissionSemanticEvidence(projectResult,imports,importSummaries)
|
|
|
90
92
|
.filter((entry)=>entry.emptySemanticEvidence)
|
|
91
93
|
.map((entry)=>entry.sourcePath)
|
|
92
94
|
.filter(Boolean));
|
|
95
|
+
const warnings=uniqueSemanticAdmissionWarnings(importSummaries.flatMap((entry)=>entry.semanticAdmission?.warnings??[]));
|
|
93
96
|
return {
|
|
94
97
|
empty:Object.values(totals).reduce((sum,value)=>sum+value,0)===0,
|
|
95
98
|
emptySourceCount:importSummaries.filter((entry)=>entry.emptySemanticEvidence).length,
|
|
@@ -98,10 +101,26 @@ export function admissionSemanticEvidence(projectResult,imports,importSummaries)
|
|
|
98
101
|
evidenceRecords:uniqueRecordsById([
|
|
99
102
|
...(projectResult?.evidence??[]),
|
|
100
103
|
...imports.flatMap((imported)=>imported?.evidence??[])
|
|
101
|
-
]).length
|
|
104
|
+
]).length,
|
|
105
|
+
warningCount:warnings.length,
|
|
106
|
+
warningReasonCodes:uniqueStrings(warnings.map((warning)=>warning.reasonCode??warning.code)),
|
|
107
|
+
warningSourcePaths:uniqueStrings(warnings.flatMap((warning)=>warning.sourcePaths??[warning.sourcePath]).filter(Boolean)),
|
|
108
|
+
warnings
|
|
102
109
|
};
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
function uniqueSemanticAdmissionWarnings(warnings){
|
|
113
|
+
const seen=new Set();
|
|
114
|
+
const result=[];
|
|
115
|
+
for(const warning of warnings.filter(Boolean)){
|
|
116
|
+
const key=[warning.reasonCode??warning.code,(warning.sourcePaths??[warning.sourcePath]).join('|')].join('\u0000');
|
|
117
|
+
if(seen.has(key)) continue;
|
|
118
|
+
seen.add(key);
|
|
119
|
+
result.push(warning);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
export function admissionSourcePreservation(importSummaries,contract){
|
|
106
125
|
const qualities=importSummaries.map((entry)=>entry.sourcePreservation.quality);
|
|
107
126
|
const quality=qualities.length?qualities.reduce(maxPreservationQuality,'exact'):'empty';
|
|
@@ -3,7 +3,8 @@ 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 {
|
|
6
|
+
import { alreadyAppliedImportEditForOperation } from './semanticEditImportProjection.js';
|
|
7
|
+
import { projectionEditRecord } from './semanticEditProjectionRecord.js';
|
|
7
8
|
import {
|
|
8
9
|
insertionOffset,
|
|
9
10
|
insertionReplacement,
|
|
@@ -40,7 +41,8 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
40
41
|
}
|
|
41
42
|
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index, {
|
|
42
43
|
headSourcePath: input.headSourcePath,
|
|
43
|
-
headSymbols
|
|
44
|
+
headSymbols,
|
|
45
|
+
symbolIndexAvailable: isJavaScriptLike(language)
|
|
44
46
|
});
|
|
45
47
|
if (edit.ok) edits.push(edit.value);
|
|
46
48
|
else reasonCodes.push(...edit.reasonCodes);
|
|
@@ -175,14 +177,20 @@ function insertionEditForOperation(operation, identity, workerSourceText, headSo
|
|
|
175
177
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
176
178
|
const reasons = [];
|
|
177
179
|
if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
|
|
178
|
-
const insertion = insertionOffset(headSourceText, operation.insertion, { symbols: context.headSymbols });
|
|
179
|
-
if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
|
|
180
180
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
181
181
|
const spanText = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
|
|
182
182
|
if (operation.hashes?.workerTextHash && hashSemanticValue(spanText) !== operation.hashes.workerTextHash) {
|
|
183
183
|
reasons.push(`worker-span-hash-mismatch:${operation.id}`);
|
|
184
184
|
}
|
|
185
185
|
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
186
|
+
const alreadyAppliedImport = alreadyAppliedImportEditForOperation(operation, identity, spanText, headSourceText, workerOffsets, order, context);
|
|
187
|
+
if (alreadyAppliedImport) return { ok: true, value: alreadyAppliedImport };
|
|
188
|
+
const insertion = insertionOffset(headSourceText, operation.insertion, {
|
|
189
|
+
symbols: context.headSymbols,
|
|
190
|
+
symbolIndexAvailable: context.symbolIndexAvailable
|
|
191
|
+
});
|
|
192
|
+
if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
|
|
193
|
+
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
186
194
|
return {
|
|
187
195
|
ok: true,
|
|
188
196
|
value: {
|
|
@@ -212,64 +220,6 @@ function projectionIdentity(operation, headSourcePath) {
|
|
|
212
220
|
: identity.targetSourcePath;
|
|
213
221
|
return { ...identity, sourcePath, originalSourcePath, targetSourcePath };
|
|
214
222
|
}
|
|
215
|
-
function projectionEditRecord(edit) {
|
|
216
|
-
const deletedTextHash = hashSemanticValue(edit.current);
|
|
217
|
-
const replacementTextHash = hashSemanticValue(edit.replacement);
|
|
218
|
-
const identity = semanticEditIdentityFields(edit);
|
|
219
|
-
return compactRecord({
|
|
220
|
-
operationId: edit.operationId,
|
|
221
|
-
status: edit.alreadyApplied ? 'already-applied' : 'applied',
|
|
222
|
-
kind: edit.kind,
|
|
223
|
-
editKind: edit.editKind,
|
|
224
|
-
changeKind: edit.changeKind,
|
|
225
|
-
anchorKey: edit.anchorKey,
|
|
226
|
-
conflictKey: edit.conflictKey,
|
|
227
|
-
regionId: edit.regionId,
|
|
228
|
-
regionKind: edit.regionKind,
|
|
229
|
-
sourcePath: edit.sourcePath,
|
|
230
|
-
originalSourcePath: edit.originalSourcePath,
|
|
231
|
-
targetAnchorKey: edit.targetAnchorKey,
|
|
232
|
-
targetSourcePath: edit.targetSourcePath,
|
|
233
|
-
targetSymbolName: edit.targetSymbolName,
|
|
234
|
-
targetSymbolKind: edit.targetSymbolKind,
|
|
235
|
-
symbolId: edit.symbolId,
|
|
236
|
-
symbolName: edit.symbolName,
|
|
237
|
-
symbolKind: edit.symbolKind,
|
|
238
|
-
...identity,
|
|
239
|
-
operationContentHash: edit.operationContentHash,
|
|
240
|
-
editContentHash: hashSemanticValue(compactRecord({
|
|
241
|
-
semanticIdentityHash: identity.semanticIdentityHash,
|
|
242
|
-
sourceRangeKind: edit.sourceRangeKind,
|
|
243
|
-
deletedTextHash,
|
|
244
|
-
replacementTextHash,
|
|
245
|
-
status: edit.alreadyApplied ? 'already-applied' : 'applied'
|
|
246
|
-
})),
|
|
247
|
-
sourceRangeKind: edit.sourceRangeKind,
|
|
248
|
-
headStart: edit.start,
|
|
249
|
-
headEnd: edit.end,
|
|
250
|
-
workerStart: edit.workerStart,
|
|
251
|
-
workerEnd: edit.workerEnd,
|
|
252
|
-
editOrder: edit.order,
|
|
253
|
-
headAnchorStart: edit.headAnchorStart,
|
|
254
|
-
headAnchorEnd: edit.headAnchorEnd,
|
|
255
|
-
workerAnchorStart: edit.workerAnchorStart,
|
|
256
|
-
workerAnchorEnd: edit.workerAnchorEnd,
|
|
257
|
-
deletedBytes: edit.current.length,
|
|
258
|
-
replacementBytes: edit.replacement.length,
|
|
259
|
-
deletedTextHash,
|
|
260
|
-
replacementTextHash,
|
|
261
|
-
anchorDeletedTextHash: edit.anchorDeletedTextHash,
|
|
262
|
-
anchorReplacementTextHash: edit.anchorReplacementTextHash,
|
|
263
|
-
replacementSpanTextHash: hashSemanticValue(edit.replacementSpanText ?? edit.replacement),
|
|
264
|
-
insertionMode: edit.insertion?.mode,
|
|
265
|
-
insertionAnchorKey: edit.insertion?.anchorKey,
|
|
266
|
-
insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
|
|
267
|
-
insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
|
|
268
|
-
insertionAnchorCandidates: edit.insertion?.anchorCandidates,
|
|
269
|
-
replacementText: edit.replacement
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
223
|
function sourceSymbolIndex(input) {
|
|
274
224
|
try {
|
|
275
225
|
const imported = normalizeNativeDiffImport({
|
|
@@ -4,7 +4,14 @@ import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
|
|
|
4
4
|
import { mapDiffSymbols } from './mapDiffSymbols.js';
|
|
5
5
|
import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
|
|
6
6
|
import { replayDiagnostics, replayEditDiagnostics, replayEditsWithOverlapDiagnostics } from './semanticEditReplayDiagnostics.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
findCurrentSymbol,
|
|
9
|
+
findInsertionAnchor,
|
|
10
|
+
hasSymbolAnchorIdentity,
|
|
11
|
+
insertionAnchorCandidates,
|
|
12
|
+
insertionRange
|
|
13
|
+
} from './semanticEditReplayAnchors.js';
|
|
14
|
+
import { bodyContentRange, removalRange, spanOffsets } from './semanticEditSourceRanges.js';
|
|
8
15
|
|
|
9
16
|
export function replaySemanticEditProjection(input = {}) {
|
|
10
17
|
const projection = input.projection ?? input.semanticEditProjection;
|
|
@@ -19,7 +26,11 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
19
26
|
? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
|
|
20
27
|
: [];
|
|
21
28
|
const replayedEdits = projection.status === 'projected' && typeof currentSourceText === 'string'
|
|
22
|
-
? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), {
|
|
29
|
+
? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), {
|
|
30
|
+
currentSourceText,
|
|
31
|
+
currentSymbols,
|
|
32
|
+
symbolIndexAvailable: isJavaScriptLike(language)
|
|
33
|
+
}))
|
|
23
34
|
: [];
|
|
24
35
|
const edits = replayEditsWithOverlapDiagnostics(replayedEdits);
|
|
25
36
|
const status = replayStatus(reasonCodes, edits, projection);
|
|
@@ -72,7 +83,7 @@ function replayProjectionEdit(edit, context) {
|
|
|
72
83
|
const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
|
|
73
84
|
if (symbol && spanRange && !sameRange(headRange, spanRange)) {
|
|
74
85
|
const moved = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
75
|
-
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
86
|
+
if (moved) return replayEditRecord(edit, moved.status, replayAppliedRange(edit, moved.range, context.currentSourceText), [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
76
87
|
if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
|
|
77
88
|
return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
78
89
|
}
|
|
@@ -80,12 +91,17 @@ function replayProjectionEdit(edit, context) {
|
|
|
80
91
|
}
|
|
81
92
|
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
82
93
|
const anchored = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
83
|
-
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
94
|
+
if (anchored) return replayEditRecord(edit, anchored.status, replayAppliedRange(edit, anchored.range, context.currentSourceText), [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
84
95
|
return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
|
|
85
96
|
symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
|
|
86
97
|
], context.currentSourceText);
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
function replayAppliedRange(edit, range, sourceText) {
|
|
101
|
+
if (edit.editKind !== 'delete' || !range || typeof sourceText !== 'string') return range;
|
|
102
|
+
return removalRange(sourceText, range);
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
function replayInsertionEdit(edit, context) {
|
|
90
106
|
const inserted = findCurrentSymbol(edit, context.currentSymbols);
|
|
91
107
|
const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
|
|
@@ -97,7 +113,8 @@ function replayInsertionEdit(edit, context) {
|
|
|
97
113
|
const anchor = findInsertionAnchor(edit, context.currentSymbols);
|
|
98
114
|
const range = insertionRange(edit, anchor?.candidate, anchor?.symbol, context.currentSourceText);
|
|
99
115
|
if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`], context.currentSourceText);
|
|
100
|
-
|
|
116
|
+
const missingStableAnchor = context.symbolIndexAvailable && insertionAnchorCandidates(edit).some(hasSymbolAnchorIdentity);
|
|
117
|
+
return replayEditRecord(edit, anchor || missingStableAnchor ? 'conflict' : 'stale', undefined, [
|
|
101
118
|
anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
|
|
102
119
|
], context.currentSourceText);
|
|
103
120
|
}
|
|
@@ -106,10 +123,26 @@ function checkRange(edit, range, sourceText, label) {
|
|
|
106
123
|
if (!range || range.end < range.start) return undefined;
|
|
107
124
|
const current = sourceText.slice(range.start, range.end);
|
|
108
125
|
const currentHash = hashSemanticValue(current);
|
|
126
|
+
const currentLineEndingStableText = lineEndingStableText(current);
|
|
127
|
+
const currentLineEndingStableHash = currentLineEndingStableText === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: hashSemanticValue(currentLineEndingStableText);
|
|
109
130
|
if (edit.replacementSpanTextHash && currentHash === edit.replacementSpanTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement-span` };
|
|
110
131
|
if (edit.replacementTextHash && currentHash === edit.replacementTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement` };
|
|
111
132
|
if (current === edit.replacementText) return { status: 'already-applied', range, reason: `${label}-matches-replacement-text` };
|
|
133
|
+
if (edit.replacementSpanTextLineEndingStableHash && currentLineEndingStableHash === edit.replacementSpanTextLineEndingStableHash) {
|
|
134
|
+
return { status: 'already-applied', range, reason: `${label}-matches-replacement-span-line-ending-stable` };
|
|
135
|
+
}
|
|
136
|
+
if (edit.replacementTextLineEndingStableHash && currentLineEndingStableHash === edit.replacementTextLineEndingStableHash) {
|
|
137
|
+
return { status: 'already-applied', range, reason: `${label}-matches-replacement-line-ending-stable` };
|
|
138
|
+
}
|
|
139
|
+
if (typeof edit.replacementText === 'string' && currentLineEndingStableText === lineEndingStableText(edit.replacementText)) {
|
|
140
|
+
return { status: 'already-applied', range, reason: `${label}-matches-replacement-text-line-ending-stable` };
|
|
141
|
+
}
|
|
112
142
|
if (edit.deletedTextHash && currentHash === edit.deletedTextHash) return { status: 'applied', range, reason: `${label}-matches-deleted` };
|
|
143
|
+
if (edit.deletedTextLineEndingStableHash && currentLineEndingStableHash === edit.deletedTextLineEndingStableHash) {
|
|
144
|
+
return { status: 'applied', range, reason: `${label}-matches-deleted-line-ending-stable` };
|
|
145
|
+
}
|
|
113
146
|
return undefined;
|
|
114
147
|
}
|
|
115
148
|
|
|
@@ -147,64 +180,6 @@ function currentSymbolIndex(input) {
|
|
|
147
180
|
return [...mapDiffSymbols(imported, createSemanticImportSidecar(imported)).values()];
|
|
148
181
|
}
|
|
149
182
|
|
|
150
|
-
function findCurrentSymbol(edit, symbols) {
|
|
151
|
-
const exact = symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && [
|
|
152
|
-
edit.anchorKey,
|
|
153
|
-
edit.targetAnchorKey,
|
|
154
|
-
edit.symbolId
|
|
155
|
-
].includes(key)));
|
|
156
|
-
if (exact) return exact;
|
|
157
|
-
const name = edit.targetSymbolName ?? edit.symbolName;
|
|
158
|
-
const kind = edit.targetSymbolKind ?? edit.symbolKind;
|
|
159
|
-
return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function findInsertionAnchor(edit, symbols) {
|
|
163
|
-
for (const candidate of insertionAnchorCandidates(edit)) {
|
|
164
|
-
const symbol = findInsertionAnchorSymbol(candidate, symbols);
|
|
165
|
-
if (symbol) return { candidate, symbol };
|
|
166
|
-
}
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function findInsertionAnchorSymbol(candidate, symbols) {
|
|
171
|
-
const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
|
|
172
|
-
return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)))
|
|
173
|
-
?? symbols.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function insertionAnchorCandidates(edit) {
|
|
177
|
-
const primary = {
|
|
178
|
-
mode: edit.insertionMode,
|
|
179
|
-
anchorKey: edit.insertionAnchorKey,
|
|
180
|
-
anchorSymbolName: edit.insertionAnchorSymbolName,
|
|
181
|
-
anchorSymbolKind: edit.insertionAnchorSymbolKind
|
|
182
|
-
};
|
|
183
|
-
const seen = new Set();
|
|
184
|
-
const result = [];
|
|
185
|
-
for (const candidate of [primary, ...(Array.isArray(edit.insertionAnchorCandidates) ? edit.insertionAnchorCandidates : [])]) {
|
|
186
|
-
if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
|
|
187
|
-
const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
|
|
188
|
-
if (seen.has(key)) continue;
|
|
189
|
-
seen.add(key);
|
|
190
|
-
result.push(candidate);
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function insertionRange(edit, candidate, anchor, sourceText) {
|
|
196
|
-
if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
|
|
197
|
-
if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
|
|
198
|
-
const mode = candidate?.mode ?? edit.insertionMode;
|
|
199
|
-
const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
|
|
200
|
-
if (!anchorRange) return undefined;
|
|
201
|
-
if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
|
|
202
|
-
if (mode === 'after') {
|
|
203
|
-
return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
183
|
function currentSymbolEditRange(edit, symbolRange, sourceText) {
|
|
209
184
|
if (!symbolRange) return undefined;
|
|
210
185
|
if (edit.sourceRangeKind === 'body-content') return bodyContentRange(sourceText, symbolRange);
|
|
@@ -296,4 +271,9 @@ function rangesOverlap(left, right) {
|
|
|
296
271
|
|
|
297
272
|
function isJavaScriptLike(language) { return language === 'javascript' || language === 'typescript'; }
|
|
298
273
|
function reasonList(values) { return uniqueStrings((values ?? []).filter(Boolean)); }
|
|
274
|
+
function lineEndingStableText(value) {
|
|
275
|
+
if (typeof value !== 'string') return undefined;
|
|
276
|
+
const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
277
|
+
return normalized.length > 1 && normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
278
|
+
}
|
|
299
279
|
function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { spanOffsets } from './semanticEditSourceRanges.js';
|
|
3
|
+
|
|
4
|
+
export function alreadyAppliedImportEditForOperation(operation, identity, spanText, headSourceText, workerOffsets, order, context) {
|
|
5
|
+
if (!isAddImportOperation(operation) || typeof headSourceText !== 'string') return undefined;
|
|
6
|
+
const match = findHeadImportSymbol(operation, context.headSymbols);
|
|
7
|
+
const range = spanOffsets(headSourceText, match?.symbol?.sourceSpan);
|
|
8
|
+
if (!range) return undefined;
|
|
9
|
+
const current = headSourceText.slice(range.start, range.end);
|
|
10
|
+
if (!headImportMatchesOperation(operation, spanText, current, match.symbol)) return undefined;
|
|
11
|
+
return {
|
|
12
|
+
operationId: operation.id,
|
|
13
|
+
order,
|
|
14
|
+
...identity,
|
|
15
|
+
start: range.start,
|
|
16
|
+
end: range.end,
|
|
17
|
+
workerStart: workerOffsets.start,
|
|
18
|
+
workerEnd: workerOffsets.end,
|
|
19
|
+
replacement: current,
|
|
20
|
+
replacementSpanText: spanText,
|
|
21
|
+
current,
|
|
22
|
+
alreadyApplied: true
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findHeadImportSymbol(operation, symbols) {
|
|
27
|
+
const symbolList = Array.isArray(symbols) ? symbols : [];
|
|
28
|
+
const exactKeys = [
|
|
29
|
+
operation.anchor?.key,
|
|
30
|
+
operation.anchor?.symbolId,
|
|
31
|
+
operation.insertion?.insertedSymbolId
|
|
32
|
+
].filter(Boolean);
|
|
33
|
+
const exact = symbolList.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && exactKeys.includes(key)));
|
|
34
|
+
if (exact) return { symbol: exact, exact: true };
|
|
35
|
+
const name = operation.insertion?.insertedSymbolName ?? operation.anchor?.symbolName;
|
|
36
|
+
const kind = operation.insertion?.insertedSymbolKind ?? operation.anchor?.symbolKind;
|
|
37
|
+
const semantic = symbolList.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
38
|
+
return semantic ? { symbol: semantic, exact: false } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function headImportMatchesOperation(operation, spanText, current, symbol) {
|
|
42
|
+
const workerTextHash = operation.hashes?.workerTextHash ?? hashSemanticValue(spanText);
|
|
43
|
+
const workerSpanHash = operation.hashes?.workerSpanHash ?? workerTextHash;
|
|
44
|
+
const currentHash = hashSemanticValue(current);
|
|
45
|
+
if ([workerTextHash, workerSpanHash].includes(currentHash)) return true;
|
|
46
|
+
if ([workerTextHash, workerSpanHash].includes(symbol?.spanHash)) return true;
|
|
47
|
+
const signatureHash = operation.hashes?.afterSignatureHash;
|
|
48
|
+
return Boolean(signatureHash && symbol?.signatureHash === signatureHash);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAddImportOperation(operation) {
|
|
52
|
+
return operation?.kind === 'addImport' || (operation?.changeKind === 'added' && operation?.anchor?.regionKind === 'import');
|
|
53
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
|
|
3
|
+
|
|
4
|
+
export function projectionEditRecord(edit) {
|
|
5
|
+
const deletedTextHash = hashSemanticValue(edit.current);
|
|
6
|
+
const replacementTextHash = hashSemanticValue(edit.replacement);
|
|
7
|
+
const replacementSpanText = edit.replacementSpanText ?? edit.replacement;
|
|
8
|
+
const identity = semanticEditIdentityFields(edit);
|
|
9
|
+
return compactRecord({
|
|
10
|
+
operationId: edit.operationId,
|
|
11
|
+
status: edit.alreadyApplied ? 'already-applied' : 'applied',
|
|
12
|
+
kind: edit.kind,
|
|
13
|
+
editKind: edit.editKind,
|
|
14
|
+
changeKind: edit.changeKind,
|
|
15
|
+
anchorKey: edit.anchorKey,
|
|
16
|
+
conflictKey: edit.conflictKey,
|
|
17
|
+
regionId: edit.regionId,
|
|
18
|
+
regionKind: edit.regionKind,
|
|
19
|
+
sourcePath: edit.sourcePath,
|
|
20
|
+
originalSourcePath: edit.originalSourcePath,
|
|
21
|
+
targetAnchorKey: edit.targetAnchorKey,
|
|
22
|
+
targetSourcePath: edit.targetSourcePath,
|
|
23
|
+
targetSymbolName: edit.targetSymbolName,
|
|
24
|
+
targetSymbolKind: edit.targetSymbolKind,
|
|
25
|
+
symbolId: edit.symbolId,
|
|
26
|
+
symbolName: edit.symbolName,
|
|
27
|
+
symbolKind: edit.symbolKind,
|
|
28
|
+
...identity,
|
|
29
|
+
operationContentHash: edit.operationContentHash,
|
|
30
|
+
editContentHash: hashSemanticValue(compactRecord({
|
|
31
|
+
semanticIdentityHash: identity.semanticIdentityHash,
|
|
32
|
+
sourceRangeKind: edit.sourceRangeKind,
|
|
33
|
+
deletedTextHash,
|
|
34
|
+
replacementTextHash,
|
|
35
|
+
status: edit.alreadyApplied ? 'already-applied' : 'applied'
|
|
36
|
+
})),
|
|
37
|
+
sourceRangeKind: edit.sourceRangeKind,
|
|
38
|
+
headStart: edit.start,
|
|
39
|
+
headEnd: edit.end,
|
|
40
|
+
workerStart: edit.workerStart,
|
|
41
|
+
workerEnd: edit.workerEnd,
|
|
42
|
+
editOrder: edit.order,
|
|
43
|
+
headAnchorStart: edit.headAnchorStart,
|
|
44
|
+
headAnchorEnd: edit.headAnchorEnd,
|
|
45
|
+
workerAnchorStart: edit.workerAnchorStart,
|
|
46
|
+
workerAnchorEnd: edit.workerAnchorEnd,
|
|
47
|
+
deletedBytes: edit.current.length,
|
|
48
|
+
replacementBytes: edit.replacement.length,
|
|
49
|
+
deletedTextHash,
|
|
50
|
+
replacementTextHash,
|
|
51
|
+
deletedTextLineEndingStableHash: lineEndingStableTextHash(edit.current),
|
|
52
|
+
replacementTextLineEndingStableHash: lineEndingStableTextHash(edit.replacement),
|
|
53
|
+
anchorDeletedTextHash: edit.anchorDeletedTextHash,
|
|
54
|
+
anchorReplacementTextHash: edit.anchorReplacementTextHash,
|
|
55
|
+
replacementSpanTextHash: hashSemanticValue(replacementSpanText),
|
|
56
|
+
replacementSpanTextLineEndingStableHash: lineEndingStableTextHash(replacementSpanText),
|
|
57
|
+
insertionMode: edit.insertion?.mode,
|
|
58
|
+
insertionAnchorKey: edit.insertion?.anchorKey,
|
|
59
|
+
insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
|
|
60
|
+
insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
|
|
61
|
+
insertionAnchorCandidates: edit.insertion?.anchorCandidates,
|
|
62
|
+
replacementText: edit.replacement
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function lineEndingStableTextHash(value) {
|
|
67
|
+
const normalized = lineEndingStableText(value);
|
|
68
|
+
return normalized === undefined ? undefined : hashSemanticValue(normalized);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lineEndingStableText(value) {
|
|
72
|
+
if (typeof value !== 'string') return undefined;
|
|
73
|
+
const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
74
|
+
return normalized.length > 1 && normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactRecord(value) {
|
|
78
|
+
return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
|
|
79
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterLineOffset, spanOffsets } from './semanticEditSourceRanges.js';
|
|
2
|
+
|
|
3
|
+
export function findCurrentSymbol(edit, symbols) {
|
|
4
|
+
const exact = symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && [
|
|
5
|
+
edit.anchorKey,
|
|
6
|
+
edit.targetAnchorKey,
|
|
7
|
+
edit.symbolId
|
|
8
|
+
].includes(key)));
|
|
9
|
+
if (exact) return exact;
|
|
10
|
+
const name = edit.targetSymbolName ?? edit.symbolName;
|
|
11
|
+
const kind = edit.targetSymbolKind ?? edit.symbolKind;
|
|
12
|
+
return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findInsertionAnchor(edit, symbols) {
|
|
16
|
+
for (const candidate of insertionAnchorCandidates(edit)) {
|
|
17
|
+
const symbol = findInsertionAnchorSymbol(candidate, symbols);
|
|
18
|
+
if (symbol) return { candidate, symbol };
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function insertionAnchorCandidates(edit) {
|
|
24
|
+
const primary = {
|
|
25
|
+
mode: edit.insertionMode,
|
|
26
|
+
anchorKey: edit.insertionAnchorKey,
|
|
27
|
+
anchorSymbolName: edit.insertionAnchorSymbolName,
|
|
28
|
+
anchorSymbolKind: edit.insertionAnchorSymbolKind
|
|
29
|
+
};
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const result = [];
|
|
32
|
+
for (const candidate of [primary, ...(Array.isArray(edit.insertionAnchorCandidates) ? edit.insertionAnchorCandidates : [])]) {
|
|
33
|
+
if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
|
|
34
|
+
const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
|
|
35
|
+
if (seen.has(key)) continue;
|
|
36
|
+
seen.add(key);
|
|
37
|
+
result.push(candidate);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasSymbolAnchorIdentity(candidate) {
|
|
43
|
+
return Boolean(candidate.anchorKey || candidate.anchorSymbolId || candidate.anchorSymbolName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function insertionRange(edit, candidate, anchor, sourceText) {
|
|
47
|
+
if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
|
|
48
|
+
if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
|
|
49
|
+
const mode = candidate?.mode ?? edit.insertionMode;
|
|
50
|
+
const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
|
|
51
|
+
if (!anchorRange) return undefined;
|
|
52
|
+
if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
|
|
53
|
+
if (mode === 'after') {
|
|
54
|
+
return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findInsertionAnchorSymbol(candidate, symbols) {
|
|
60
|
+
const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
|
|
61
|
+
return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)))
|
|
62
|
+
?? symbols.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
|
|
63
|
+
}
|
|
@@ -168,6 +168,7 @@ function insertionAnchorResolution(sourceText, insertion, context) {
|
|
|
168
168
|
const range = spanOffsets(sourceText, symbol?.sourceSpan);
|
|
169
169
|
if (range) return { mode: candidate.mode, range };
|
|
170
170
|
}
|
|
171
|
+
if (context.symbolIndexAvailable && candidates.some(hasSymbolAnchorIdentity)) return undefined;
|
|
171
172
|
for (const candidate of candidates) {
|
|
172
173
|
const range = spanOffsets(sourceText, candidate.headSpan);
|
|
173
174
|
if (range) return { mode: candidate.mode, range };
|
|
@@ -175,6 +176,10 @@ function insertionAnchorResolution(sourceText, insertion, context) {
|
|
|
175
176
|
return undefined;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
function hasSymbolAnchorIdentity(candidate) {
|
|
180
|
+
return Boolean(candidate.anchorKey || candidate.anchorSymbolId || candidate.anchorSymbolName);
|
|
181
|
+
}
|
|
182
|
+
|
|
178
183
|
function insertionAnchorCandidates(insertion) {
|
|
179
184
|
const seen = new Set();
|
|
180
185
|
const result = [];
|
|
@@ -2,7 +2,9 @@ import { normalizeSemanticMergeReadiness, uniqueStrings } from '../../native-imp
|
|
|
2
2
|
|
|
3
3
|
export function createSemanticPatchBundleAdmission(input = {}, context = {}) {
|
|
4
4
|
const transformAdmission = semanticTransformAdmission(context);
|
|
5
|
-
const semanticEditAdmission =
|
|
5
|
+
const semanticEditAdmission = semanticEditAdmissionWithReplayRequirement(
|
|
6
|
+
context.semanticEditAdmission ?? { status: 'none', action: 'none', readiness: 'needs-review', reasonCodes: [] }
|
|
7
|
+
);
|
|
6
8
|
const evidenceAdmission = autoMergeEvidenceAdmission(context, { transformAdmission, semanticEditAdmission });
|
|
7
9
|
const fallbackReadiness = fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, evidenceAdmission, context.readiness);
|
|
8
10
|
const inputReadiness = normalizeSemanticMergeReadiness(input.readiness ?? fallbackReadiness) ?? input.readiness ?? fallbackReadiness;
|
|
@@ -35,7 +37,7 @@ export function createSemanticPatchBundleAdmission(input = {}, context = {}) {
|
|
|
35
37
|
...strings(semanticEditAdmission.reasonCodes),
|
|
36
38
|
...strings(evidenceAdmission.reasonCodes)
|
|
37
39
|
].filter(Boolean)),
|
|
38
|
-
conflictKeys: uniqueStrings([...strings(input.conflictKeys), ...context.conflictKeys]),
|
|
40
|
+
conflictKeys: uniqueStrings([...strings(input.conflictKeys), ...strings(context.conflictKeys)]),
|
|
39
41
|
admittedAt: input.admittedAt,
|
|
40
42
|
reviewerId: input.reviewerId,
|
|
41
43
|
evidenceIds: uniqueStrings([...strings(input.evidenceIds), ...strings(transformAdmission.evidenceIds), ...strings(evidenceAdmission.evidenceIds)]),
|
|
@@ -155,6 +157,7 @@ function fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, e
|
|
|
155
157
|
if ([transformAdmission.readiness, semanticEditAdmission.readiness, evidenceAdmission.readiness].includes('blocked')) return 'blocked';
|
|
156
158
|
if (hasSkipReadyAction(semanticEditAdmission)) return 'ready';
|
|
157
159
|
if (hasPositiveApplyAction(transformAdmission, semanticEditAdmission)) return evidenceAdmission.action === 'admit' ? 'ready' : evidenceAdmission.readiness;
|
|
160
|
+
if (semanticEditAdmission.action === 'review' || semanticEditAdmission.status === 'needs-review') return 'needs-review';
|
|
158
161
|
return fallback;
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -183,6 +186,51 @@ function hasSkipReadyAction(semanticEditAdmission) {
|
|
|
183
186
|
return semanticEditAdmission.action === 'skip' && semanticEditAdmission.readiness === 'ready' && semanticEditAdmission.reviewRequired === false;
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
function semanticEditAdmissionWithReplayRequirement(admission) {
|
|
190
|
+
if (!requiresSemanticEditReplay(admission) || hasAcceptedCleanSemanticEditReplay(admission)) return admission;
|
|
191
|
+
return compactRecord({
|
|
192
|
+
...admission,
|
|
193
|
+
status: 'needs-review',
|
|
194
|
+
action: 'review',
|
|
195
|
+
readiness: 'needs-review',
|
|
196
|
+
reviewRequired: true,
|
|
197
|
+
autoApplyCandidate: false,
|
|
198
|
+
reasonCodes: uniqueStrings([
|
|
199
|
+
...strings(admission.reasonCodes).filter((reason) => reason !== 'semantic-edit-positive-auto-merge-proof'),
|
|
200
|
+
...semanticEditReplayRequirementReasonCodes(admission)
|
|
201
|
+
])
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function requiresSemanticEditReplay(admission) {
|
|
206
|
+
return admission.action === 'admit' ||
|
|
207
|
+
admission.autoApplyCandidate === true ||
|
|
208
|
+
admission.status === 'ready' ||
|
|
209
|
+
strings(admission.reasonCodes).includes('semantic-edit-positive-auto-merge-proof');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hasAcceptedCleanSemanticEditReplay(admission) {
|
|
213
|
+
const summary = admission.summary ?? {};
|
|
214
|
+
const acceptedClean = count(summary.acceptedClean);
|
|
215
|
+
const alreadyApplied = count(summary.alreadyApplied);
|
|
216
|
+
const replays = count(summary.replays);
|
|
217
|
+
return acceptedClean > 0 && replays > 0 && acceptedClean + alreadyApplied === replays;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function semanticEditReplayRequirementReasonCodes(admission) {
|
|
221
|
+
const summary = admission.summary ?? {};
|
|
222
|
+
const scripts = count(summary.scripts);
|
|
223
|
+
const projections = count(summary.projections);
|
|
224
|
+
const replays = count(summary.replays);
|
|
225
|
+
const acceptedClean = count(summary.acceptedClean);
|
|
226
|
+
return [
|
|
227
|
+
scripts > 0 && projections === 0 ? 'semantic-edit-projection-missing' : undefined,
|
|
228
|
+
(scripts > 0 || projections > 0) && replays === 0 ? 'semantic-edit-replay-missing' : undefined,
|
|
229
|
+
replays > 0 && acceptedClean === 0 ? 'semantic-edit-replay-accepted-clean-missing' : undefined,
|
|
230
|
+
'semantic-edit-replay-required'
|
|
231
|
+
].filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
|
|
186
234
|
function hasCrossLanguageTransform(index) {
|
|
187
235
|
const source = new Set(strings(index.transformSourceLanguages));
|
|
188
236
|
return strings(index.transformTargetLanguages).some((target) => !source.has(target));
|
|
@@ -211,6 +259,7 @@ function uniqueEvidenceRecords(records) {
|
|
|
211
259
|
}
|
|
212
260
|
|
|
213
261
|
function evidenceIds(evidence) { return uniqueStrings(evidence.map((record) => record.id)); }
|
|
262
|
+
function count(value) { const number = Number(value ?? 0); return Number.isFinite(number) ? number : 0; }
|
|
214
263
|
function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
|
|
215
264
|
function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
|
|
216
265
|
function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
|