@shapeshift-labs/frontier-lang-compiler 0.2.102 → 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/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/createProjectImportAdmissionRecord.js +14 -2
- package/dist/internal/index-impl/diffNativeSymbols.js +82 -1
- 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/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type NativeProjectAdmissionSemanticEvidenceWarningCode =
|
|
2
|
+
| 'missing-ownership-regions'
|
|
3
|
+
| 'missing-patch-hints'
|
|
4
|
+
| (string & {});
|
|
5
|
+
|
|
6
|
+
export interface NativeProjectAdmissionSemanticEvidenceWarning {
|
|
7
|
+
readonly code: NativeProjectAdmissionSemanticEvidenceWarningCode;
|
|
8
|
+
readonly reasonCode: NativeProjectAdmissionSemanticEvidenceWarningCode;
|
|
9
|
+
readonly severity: 'warning' | string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly action: string;
|
|
12
|
+
readonly sourcePath?: string;
|
|
13
|
+
readonly sourcePaths: readonly string[];
|
|
14
|
+
readonly semanticSymbols: number;
|
|
15
|
+
readonly ownershipRegions: number;
|
|
16
|
+
readonly patchHints: number;
|
|
17
|
+
readonly semanticImportExpected: boolean;
|
|
18
|
+
readonly changedSource: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NativeProjectAdmissionSemanticEvidence {
|
|
22
|
+
readonly empty: boolean;
|
|
23
|
+
readonly emptySourceCount: number;
|
|
24
|
+
readonly emptySourcePaths: readonly string[];
|
|
25
|
+
readonly symbols: number;
|
|
26
|
+
readonly occurrences: number;
|
|
27
|
+
readonly relations: number;
|
|
28
|
+
readonly facts: number;
|
|
29
|
+
readonly evidenceRecords: number;
|
|
30
|
+
readonly warningCount: number;
|
|
31
|
+
readonly warningReasonCodes: readonly NativeProjectAdmissionSemanticEvidenceWarningCode[];
|
|
32
|
+
readonly warningSourcePaths: readonly string[];
|
|
33
|
+
readonly warnings: readonly NativeProjectAdmissionSemanticEvidenceWarning[];
|
|
34
|
+
}
|
|
@@ -2,7 +2,13 @@ import type { FrontierSourceLanguage, SemanticMergeReadiness } from '@shapeshift
|
|
|
2
2
|
import type { NativeImportLossSummary } from './native-import-losses.js';
|
|
3
3
|
import type { NativeImportResultContract } from './native-import-contracts.js';
|
|
4
4
|
import type { NativeProjectImportResult } from './native-project.js';
|
|
5
|
+
import type { NativeProjectAdmissionSemanticEvidence } from './native-project-admission-semantic-evidence.js';
|
|
5
6
|
import type { SemanticMergeCandidateAdmissionRecord, SemanticMergeCandidateOverlapRecord, SemanticMergeCandidateProjectionRisk } from './semantic-merge-candidates.js';
|
|
7
|
+
export type {
|
|
8
|
+
NativeProjectAdmissionSemanticEvidence,
|
|
9
|
+
NativeProjectAdmissionSemanticEvidenceWarning,
|
|
10
|
+
NativeProjectAdmissionSemanticEvidenceWarningCode
|
|
11
|
+
} from './native-project-admission-semantic-evidence.js';
|
|
6
12
|
export type NativeProjectImportAdmissionAction = 'admit' | 'prioritize' | 'reject';
|
|
7
13
|
export type NativeProjectImportAdmissionPriority = 'low' | 'normal' | 'high' | 'critical' | 'blocker';
|
|
8
14
|
export type NativeProjectImportAdmissionRisk = 'low' | 'medium' | 'high' | 'unknown';
|
|
@@ -133,16 +139,6 @@ export interface NativeProjectAdmissionLanguages {
|
|
|
133
139
|
readonly readinessRows: readonly NativeProjectAdmissionLanguageReadinessSummary[];
|
|
134
140
|
readonly rows: readonly NativeProjectAdmissionLanguageSummary[];
|
|
135
141
|
}
|
|
136
|
-
export interface NativeProjectAdmissionSemanticEvidence {
|
|
137
|
-
readonly empty: boolean;
|
|
138
|
-
readonly emptySourceCount: number;
|
|
139
|
-
readonly emptySourcePaths: readonly string[];
|
|
140
|
-
readonly symbols: number;
|
|
141
|
-
readonly occurrences: number;
|
|
142
|
-
readonly relations: number;
|
|
143
|
-
readonly facts: number;
|
|
144
|
-
readonly evidenceRecords: number;
|
|
145
|
-
}
|
|
146
142
|
export type NativeProjectAdmissionSourceStalenessReason =
|
|
147
143
|
| 'source-hash-mismatch'
|
|
148
144
|
| 'content-hash-mismatch'
|
|
@@ -147,7 +147,10 @@ export interface SemanticEditProjectionEdit {
|
|
|
147
147
|
readonly replacementBytes: number;
|
|
148
148
|
readonly deletedTextHash?: string;
|
|
149
149
|
readonly replacementTextHash?: string;
|
|
150
|
+
readonly deletedTextLineEndingStableHash?: string;
|
|
151
|
+
readonly replacementTextLineEndingStableHash?: string;
|
|
150
152
|
readonly replacementSpanTextHash?: string;
|
|
153
|
+
readonly replacementSpanTextLineEndingStableHash?: string;
|
|
151
154
|
readonly insertionMode?: string;
|
|
152
155
|
readonly insertionAnchorKey?: string;
|
|
153
156
|
readonly insertionAnchorSymbolName?: string;
|
|
@@ -71,6 +71,7 @@ export interface SemanticPatchBundleOverlapRecord {
|
|
|
71
71
|
readonly sourceSignals: number;
|
|
72
72
|
readonly baseHashMismatch: boolean;
|
|
73
73
|
readonly targetHashMismatch: boolean;
|
|
74
|
+
readonly replayOutputHashMismatch: boolean;
|
|
74
75
|
};
|
|
75
76
|
readonly metadata?: Record<string, unknown>;
|
|
76
77
|
}
|
|
@@ -19,7 +19,7 @@ export function createProjectImportAdmissionRecord(projectResult,options={}){
|
|
|
19
19
|
...(projectResult?.mergeCandidates??[]),
|
|
20
20
|
...imports.flatMap((imported)=>imported?.mergeCandidates??[])
|
|
21
21
|
]);
|
|
22
|
-
const importSummaries=projectAdmissionImports(imports,contract?.sources??[],mergeCandidates);
|
|
22
|
+
const importSummaries=projectAdmissionImports(imports,contract?.sources??[],mergeCandidates,projectResult);
|
|
23
23
|
const languages=admissionLanguages(importSummaries);
|
|
24
24
|
const semanticEvidence=admissionSemanticEvidence(projectResult,imports,importSummaries);
|
|
25
25
|
const sourceStaleness=admissionSourceStaleness(imports,importSummaries,contract);
|
|
@@ -42,7 +42,10 @@ export function createProjectImportAdmissionRecord(projectResult,options={}){
|
|
|
42
42
|
failedEvidenceIds,
|
|
43
43
|
blockingLossIds:contract?.readiness?.blockingLossIds??lossSummary?.blockingLossIds??[]
|
|
44
44
|
});
|
|
45
|
-
const priorityReasons=
|
|
45
|
+
const priorityReasons=uniqueStrings([
|
|
46
|
+
...admissionPriorityReasons({readiness,semanticEvidence,sourcePreservation,ownership,mergeCandidateRisk}),
|
|
47
|
+
...semanticAdmissionWarningReasons(semanticEvidence)
|
|
48
|
+
]);
|
|
46
49
|
const action=rejectionReasons.length?'reject':priorityReasons.length?'prioritize':'admit';
|
|
47
50
|
const priority=admissionPriority(action,readiness,sourcePreservation,mergeCandidateRisk);
|
|
48
51
|
const mergeScore=admissionMergeScore({
|
|
@@ -221,6 +224,15 @@ function admissionSourcePreservationWithStaleness(sourcePreservation,sourceStale
|
|
|
221
224
|
};
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
function semanticAdmissionWarningReasons(semanticEvidence){
|
|
228
|
+
return (semanticEvidence?.warnings??[]).map((warning)=>{
|
|
229
|
+
const reasonCode=warning.reasonCode??warning.code;
|
|
230
|
+
const sourcePaths=warning.sourcePaths?.length?warning.sourcePaths:warning.sourcePath?[warning.sourcePath]:[];
|
|
231
|
+
const sourceText=sourcePaths.length?` for ${sourcePaths.join(', ')}`:'';
|
|
232
|
+
return `Project import semantic admission warning ${reasonCode}${sourceText}.`;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
224
236
|
function sourcePaths(records){
|
|
225
237
|
return uniqueStrings(records.map((record)=>record.sourcePath).filter(Boolean));
|
|
226
238
|
}
|
|
@@ -37,5 +37,86 @@ export function diffNativeSymbols(beforeSymbols, afterSymbols) {
|
|
|
37
37
|
readiness: maxSemanticMergeReadiness(before?.readiness ?? 'ready', after?.readiness ?? 'ready')
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
return changed;
|
|
40
|
+
return downgradeCoveredContainerSymbols(changed);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
function downgradeCoveredContainerSymbols(symbols) {
|
|
44
|
+
return symbols.filter((symbol) => !nativeDiffContainerCoveredByMorePreciseChange(symbol, symbols));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nativeDiffContainerCoveredByMorePreciseChange(symbol, symbols) {
|
|
48
|
+
if (symbol.changeKind !== 'modified') return false;
|
|
49
|
+
if (!nativeDiffSymbolIsContainer(symbol)) return false;
|
|
50
|
+
if (nativeDiffSymbolHasOwnChange(symbol)) return false;
|
|
51
|
+
return symbols.some((candidate) => candidate !== symbol && nativeDiffSymbolIsMorePreciseNestedChange(candidate, symbol));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nativeDiffSymbolHasOwnChange(symbol) {
|
|
55
|
+
return ((symbol.beforeSignatureHash ?? '') !== (symbol.afterSignatureHash ?? ''))
|
|
56
|
+
|| ((symbol.beforeOwnershipKey ?? '') !== (symbol.afterOwnershipKey ?? ''))
|
|
57
|
+
|| ((symbol.beforeNativeAstNodeId ?? '') !== (symbol.afterNativeAstNodeId ?? ''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nativeDiffSymbolIsMorePreciseNestedChange(candidate, container) {
|
|
61
|
+
if (candidate.changeKind === 'unchanged') return false;
|
|
62
|
+
if (nativeDiffSymbolIsContainer(candidate)) return false;
|
|
63
|
+
if ((candidate.ownershipKey ?? '') === (container.ownershipKey ?? '')) return false;
|
|
64
|
+
if (!nativeDiffAnySpanContains(container, candidate)) return false;
|
|
65
|
+
return nativeDiffNestedSymbolName(candidate, container) || nativeDiffSymbolIsMember(candidate);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nativeDiffSymbolIsContainer(symbol) {
|
|
69
|
+
return nativeDiffContainerKinds.has(nativeDiffKind(symbol.ownershipRegionKind ?? symbol.kind));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nativeDiffSymbolIsMember(symbol) {
|
|
73
|
+
return nativeDiffMemberKinds.has(nativeDiffKind(symbol.ownershipRegionKind ?? symbol.kind));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function nativeDiffNestedSymbolName(candidate, container) {
|
|
77
|
+
const candidateName = String(candidate.name ?? '');
|
|
78
|
+
const containerName = String(container.name ?? '');
|
|
79
|
+
return Boolean(containerName && candidateName && candidateName !== containerName && candidateName.startsWith(`${containerName}.`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nativeDiffAnySpanContains(container, candidate) {
|
|
83
|
+
return nativeDiffSymbolSpans(container).some((containerSpan) => nativeDiffSymbolSpans(candidate).some((candidateSpan) => (
|
|
84
|
+
nativeDiffSameSourcePath(candidateSpan, containerSpan) && nativeDiffSpanContains(containerSpan, candidateSpan)
|
|
85
|
+
)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function nativeDiffSymbolSpans(symbol) {
|
|
89
|
+
return [symbol.sourceSpan, symbol.beforeSourceSpan, symbol.afterSourceSpan].filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function nativeDiffSameSourcePath(left, right) {
|
|
93
|
+
const leftPath = left?.path;
|
|
94
|
+
const rightPath = right?.path;
|
|
95
|
+
return !leftPath || !rightPath || leftPath === rightPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nativeDiffSpanContains(containerSpan, candidateSpan) {
|
|
99
|
+
const container = nativeDiffSpanRange(containerSpan);
|
|
100
|
+
const candidate = nativeDiffSpanRange(candidateSpan);
|
|
101
|
+
if (!container || !candidate) return false;
|
|
102
|
+
return container.start <= candidate.start && candidate.end <= container.end && (container.start !== candidate.start || container.end !== candidate.end);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function nativeDiffSpanRange(span) {
|
|
106
|
+
const startLine = Number(span?.startLine ?? span?.line ?? span?.start?.line);
|
|
107
|
+
const endLine = Number(span?.endLine ?? span?.end?.line ?? startLine);
|
|
108
|
+
if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) return undefined;
|
|
109
|
+
const startColumn = Number(span?.startColumn ?? span?.column ?? span?.start?.column ?? 0);
|
|
110
|
+
const endColumn = Number(span?.endColumn ?? span?.end?.column ?? startColumn);
|
|
111
|
+
return {
|
|
112
|
+
start: startLine * 100000 + (Number.isFinite(startColumn) ? startColumn : 0),
|
|
113
|
+
end: endLine * 100000 + Math.max(Number.isFinite(startColumn) ? startColumn : 0, Number.isFinite(endColumn) ? endColumn : 0)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function nativeDiffKind(value) {
|
|
118
|
+
return String(value ?? '').toLowerCase();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nativeDiffContainerKinds = new Set(['type', 'class', 'interface', 'trait', 'protocol', 'struct', 'enum', 'record']);
|
|
122
|
+
const nativeDiffMemberKinds = new Set(['body', 'method', 'function', 'property', 'declaration']);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import{uniqueStrings}from'../../native-import-utils.js';
|
|
2
2
|
import{nativeImportCategoryForLossKind}from'./nativeImportCategoryForLossKind.js';
|
|
3
|
+
export{summarizeSemanticAdmissionWarnings}from'./projectImportAdmissionSemanticWarnings.js';
|
|
3
4
|
|
|
4
5
|
export function importLosses(imported){
|
|
5
6
|
const nativeAst=imported?.nativeAst??imported?.nativeSource?.ast;
|
|
@@ -157,4 +158,3 @@ export function summarizeImportPreservation(imported,source){
|
|
|
157
158
|
const quality=stale?'stale':missing?'missing':truncated||!exactSourceAvailable||sourcePreservationLosses.length?'lossy':'exact';
|
|
158
159
|
return {quality,missing,stale,truncated,exactSourceAvailable,lossCount:sourcePreservationLosses.length,id:record?.id};
|
|
159
160
|
}
|
|
160
|
-
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { uniqueRecordsById, uniqueStrings } from '../../native-import-utils.js';
|
|
2
|
+
import { semanticOwnershipRegionsFromSemanticIndex } from '../../semantic-import-regions.js';
|
|
3
|
+
|
|
4
|
+
export function summarizeSemanticAdmissionWarnings(imported, context = {}) {
|
|
5
|
+
const semanticIndex = imported?.semanticIndex ?? imported?.universalAst?.semanticIndex;
|
|
6
|
+
const symbols = semanticSymbolsForImport(imported, semanticIndex);
|
|
7
|
+
const ownershipRegions = semanticOwnershipRegionsForImport(imported, semanticIndex);
|
|
8
|
+
const patchHints = semanticPatchHintsForImport(imported, semanticIndex);
|
|
9
|
+
const sourcePath = firstString(context.sourcePath, context.source?.sourcePath, imported?.sourcePath, imported?.nativeSource?.sourcePath, imported?.nativeAst?.sourcePath, semanticIndex?.documents?.[0]?.path);
|
|
10
|
+
const semanticImportExpected = isSemanticImportExpected(imported, context);
|
|
11
|
+
const semanticImportExpectedEmpty = isSemanticImportExpectedEmpty(imported, context);
|
|
12
|
+
const changedSource = isChangedSemanticAdmissionSource(imported, { ...context, sourcePath });
|
|
13
|
+
const warnings = [];
|
|
14
|
+
if (semanticImportExpected && !semanticImportExpectedEmpty && changedSource && symbols.length > 0 && ownershipRegions.length === 0) {
|
|
15
|
+
warnings.push(semanticAdmissionWarning({
|
|
16
|
+
code: 'missing-ownership-regions',
|
|
17
|
+
message: 'Semantic import was expected for a changed source and produced symbols, but no ownership regions were available.',
|
|
18
|
+
action: 'rerun-sidecar-generation-with-ownership-regions',
|
|
19
|
+
sourcePath,
|
|
20
|
+
symbols,
|
|
21
|
+
ownershipRegions,
|
|
22
|
+
patchHints,
|
|
23
|
+
semanticImportExpected,
|
|
24
|
+
changedSource
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
if (semanticImportExpected && !semanticImportExpectedEmpty && changedSource && symbols.length > 0 && patchHints.length === 0) {
|
|
28
|
+
warnings.push(semanticAdmissionWarning({
|
|
29
|
+
code: 'missing-patch-hints',
|
|
30
|
+
message: 'Semantic import was expected for a changed source and produced symbols, but no patch hints were available.',
|
|
31
|
+
action: 'generate-semantic-patch-hints',
|
|
32
|
+
sourcePath,
|
|
33
|
+
symbols,
|
|
34
|
+
ownershipRegions,
|
|
35
|
+
patchHints,
|
|
36
|
+
semanticImportExpected,
|
|
37
|
+
changedSource
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
semanticImportExpected,
|
|
42
|
+
semanticImportExpectedEmpty,
|
|
43
|
+
changedSource,
|
|
44
|
+
symbolCount: symbols.length,
|
|
45
|
+
ownershipRegionCount: ownershipRegions.length,
|
|
46
|
+
patchHintCount: patchHints.length,
|
|
47
|
+
warningCount: warnings.length,
|
|
48
|
+
reasonCodes: uniqueStrings(warnings.map((warning) => warning.reasonCode)),
|
|
49
|
+
warnings
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function semanticAdmissionWarning(input) {
|
|
54
|
+
return {
|
|
55
|
+
code: input.code,
|
|
56
|
+
reasonCode: input.code,
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
message: input.message,
|
|
59
|
+
action: input.action,
|
|
60
|
+
...(input.sourcePath ? { sourcePath: input.sourcePath } : {}),
|
|
61
|
+
sourcePaths: uniqueStrings([input.sourcePath].filter(Boolean)),
|
|
62
|
+
semanticSymbols: input.symbols.length,
|
|
63
|
+
ownershipRegions: input.ownershipRegions.length,
|
|
64
|
+
patchHints: input.patchHints.length,
|
|
65
|
+
semanticImportExpected: input.semanticImportExpected,
|
|
66
|
+
changedSource: input.changedSource
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function semanticSymbolsForImport(imported, semanticIndex) {
|
|
71
|
+
if (Array.isArray(semanticIndex?.symbols)) return semanticIndex.symbols;
|
|
72
|
+
for (const symbols of [
|
|
73
|
+
imported?.semanticSymbols,
|
|
74
|
+
imported?.symbols,
|
|
75
|
+
imported?.metadata?.semanticSymbols,
|
|
76
|
+
imported?.metadata?.symbols
|
|
77
|
+
]) {
|
|
78
|
+
if (Array.isArray(symbols)) return symbols;
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function semanticOwnershipRegionsForImport(imported, semanticIndex) {
|
|
84
|
+
return uniqueRecordsById([
|
|
85
|
+
...(Array.isArray(imported?.ownershipRegions) ? imported.ownershipRegions : []),
|
|
86
|
+
...(Array.isArray(imported?.semanticOwnershipRegions) ? imported.semanticOwnershipRegions : []),
|
|
87
|
+
...semanticOwnershipRegionsFromSemanticIndex(semanticIndex),
|
|
88
|
+
...(Array.isArray(imported?.universalAst?.ownershipRegions) ? imported.universalAst.ownershipRegions : []),
|
|
89
|
+
...(Array.isArray(imported?.metadata?.ownershipRegions) ? imported.metadata.ownershipRegions : [])
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function semanticPatchHintsForImport(imported, semanticIndex) {
|
|
94
|
+
return uniqueRecordsById([
|
|
95
|
+
...(Array.isArray(imported?.patchHints) ? imported.patchHints : []),
|
|
96
|
+
...(Array.isArray(imported?.semanticPatchHints) ? imported.semanticPatchHints : []),
|
|
97
|
+
...(Array.isArray(semanticIndex?.patchHints) ? semanticIndex.patchHints : []),
|
|
98
|
+
...(Array.isArray(imported?.universalAst?.patchHints) ? imported.universalAst.patchHints : []),
|
|
99
|
+
...(Array.isArray(imported?.metadata?.patchHints) ? imported.metadata.patchHints : [])
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isSemanticImportExpected(imported, context) {
|
|
104
|
+
return semanticExpectationRecords(imported, context).some((entry) =>
|
|
105
|
+
entry.semanticImportExpected === true
|
|
106
|
+
|| entry.expectedSemanticImport === true
|
|
107
|
+
|| entry.semanticSidecarExpected === true
|
|
108
|
+
|| entry.expected === true && looksLikeSemanticSidecarQuality(entry)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isSemanticImportExpectedEmpty(imported, context) {
|
|
113
|
+
return semanticExpectationRecords(imported, context).some((entry) =>
|
|
114
|
+
entry.semanticImportExpectedEmpty === true
|
|
115
|
+
|| entry.expectedSemanticImportEmpty === true
|
|
116
|
+
|| entry.semanticSidecarExpectedEmpty === true
|
|
117
|
+
|| entry.expectedEmpty === true && looksLikeSemanticSidecarQuality(entry)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isChangedSemanticAdmissionSource(imported, context) {
|
|
122
|
+
const sourcePath = context.sourcePath;
|
|
123
|
+
const changedSourcePaths = uniqueStrings([
|
|
124
|
+
...(context.projectResult?.changedSourcePaths ?? []),
|
|
125
|
+
...(context.projectResult?.metadata?.changedSourcePaths ?? []),
|
|
126
|
+
...(context.projectResult?.metadata?.semanticChangedSourcePaths ?? []),
|
|
127
|
+
...(context.projectResult?.metadata?.semanticImportChangedSourcePaths ?? [])
|
|
128
|
+
]);
|
|
129
|
+
return Boolean(
|
|
130
|
+
(context.candidates?.length ?? 0) > 0
|
|
131
|
+
|| (sourcePath && changedSourcePaths.includes(sourcePath))
|
|
132
|
+
|| semanticExpectationRecords(imported, context).some((entry) =>
|
|
133
|
+
entry.changedSource === true
|
|
134
|
+
|| entry.sourceChanged === true
|
|
135
|
+
|| entry.semanticSourceChanged === true
|
|
136
|
+
|| entry.semanticImportChangedSource === true
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function semanticExpectationRecords(imported, context) {
|
|
142
|
+
const nativeAst = imported?.nativeAst ?? imported?.nativeSource?.ast;
|
|
143
|
+
const semanticIndex = imported?.semanticIndex ?? imported?.universalAst?.semanticIndex;
|
|
144
|
+
return [
|
|
145
|
+
context.projectResult?.metadata,
|
|
146
|
+
context.source?.metadata,
|
|
147
|
+
imported?.metadata,
|
|
148
|
+
imported?.nativeSource?.metadata,
|
|
149
|
+
nativeAst?.metadata,
|
|
150
|
+
imported?.universalAst?.metadata,
|
|
151
|
+
imported?.patch?.metadata,
|
|
152
|
+
semanticIndex?.metadata,
|
|
153
|
+
imported?.semanticSidecarQuality,
|
|
154
|
+
imported?.sidecarQuality,
|
|
155
|
+
imported?.semanticSidecar?.quality,
|
|
156
|
+
imported?.sidecar?.quality,
|
|
157
|
+
imported?.metadata?.semanticSidecarQuality,
|
|
158
|
+
imported?.metadata?.sidecarQuality,
|
|
159
|
+
imported?.patch?.metadata?.semanticSidecarQuality
|
|
160
|
+
].filter((entry) => entry && typeof entry === 'object');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function looksLikeSemanticSidecarQuality(value) {
|
|
164
|
+
return value && typeof value === 'object' && (
|
|
165
|
+
value.schema === 'frontier.lang.semanticSidecarQuality.v1'
|
|
166
|
+
|| value.imported !== undefined
|
|
167
|
+
|| value.expectedSatisfied !== undefined
|
|
168
|
+
|| value.warningCount !== undefined
|
|
169
|
+
|| value.proofSummary !== undefined
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function firstString(...values) {
|
|
174
|
+
for (const value of values) {
|
|
175
|
+
if (value !== undefined && value !== null && String(value)) return String(value);
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
@@ -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))); }
|
|
@@ -65,7 +65,8 @@ export function compareSemanticPatchBundleRecords(left={},right={},options={}){
|
|
|
65
65
|
semanticSignals:shared.semanticEditKeys.length+shared.semanticIdentityHashes.length+shared.sourceIdentityHashes.length+shared.semanticTransformIdentityHashes.length+shared.projectionIdentityHashes.length,
|
|
66
66
|
sourceSignals:shared.regionKeys.length+shared.conflictKeys.length+shared.sourcePaths.length+shared.semanticEditReplayCurrentHashes.length,
|
|
67
67
|
baseHashMismatch:admission.reasonCodes.includes('base-hash-mismatch'),
|
|
68
|
-
targetHashMismatch:admission.reasonCodes.includes('target-hash-mismatch')
|
|
68
|
+
targetHashMismatch:admission.reasonCodes.includes('target-hash-mismatch'),
|
|
69
|
+
replayOutputHashMismatch:admission.reasonCodes.includes('replay-output-hash-mismatch')
|
|
69
70
|
},
|
|
70
71
|
metadata:compactRecord(options.metadata)
|
|
71
72
|
};
|
|
@@ -110,21 +111,32 @@ function sharedIndex(left,right,options){
|
|
|
110
111
|
baseHashes:intersect(left.baseHashes,right.baseHashes),
|
|
111
112
|
targetHashes:intersect(left.targetHashes,right.targetHashes)
|
|
112
113
|
};
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
return{
|
|
114
|
+
const semanticKeyIndependent=hasDisjointSemanticEditScope(left,right,shared);
|
|
115
|
+
const scopedShared={
|
|
116
116
|
...shared,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
sourcePaths:semanticKeyIndependent?[]:shared.sourcePaths
|
|
118
|
+
};
|
|
119
|
+
const scopedEdit=hasSharedEditScope(scopedShared);
|
|
120
|
+
const scopedSource=hasSharedSourceScope(scopedShared);
|
|
121
|
+
return{
|
|
122
|
+
...scopedShared,
|
|
123
|
+
operationContentHashes:scopedEdit?scopedShared.operationContentHashes:[],
|
|
124
|
+
editContentHashes:scopedEdit?scopedShared.editContentHashes:[],
|
|
125
|
+
semanticEditKeys:scopedSource?scopedShared.semanticEditKeys:[],
|
|
126
|
+
semanticIdentityHashes:scopedSource?scopedShared.semanticIdentityHashes:[],
|
|
127
|
+
semanticEditReplayCurrentHashes:scopedSource?scopedShared.semanticEditReplayCurrentHashes:[],
|
|
128
|
+
semanticEditReplayOutputHashes:scopedEdit?scopedShared.semanticEditReplayOutputHashes:[],
|
|
129
|
+
semanticTransformContentHashes:scopedShared.projectionIdentityHashes.length?scopedShared.semanticTransformContentHashes:[],
|
|
130
|
+
semanticTransformIdentityHashes:scopedShared.projectionIdentityHashes.length?scopedShared.semanticTransformIdentityHashes:[]
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
function hasDisjointSemanticEditScope(left,right,shared){
|
|
135
|
+
return Boolean(shared.sourcePaths.length&&left.semanticEditKeys.length&&right.semanticEditKeys.length
|
|
136
|
+
&&shared.semanticEditKeys.length===0&&shared.regionKeys.length===0&&shared.conflictKeys.length===0
|
|
137
|
+
&&shared.semanticIdentityHashes.length===0&&shared.sourceIdentityHashes.length===0);
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
function hasSharedEditScope(shared){
|
|
129
141
|
return Boolean(shared.regionKeys.length||shared.conflictKeys.length||shared.sourceIdentityHashes.length
|
|
130
142
|
||(shared.sourcePaths.length&&(shared.semanticEditKeys.length||shared.semanticIdentityHashes.length)));
|
|
@@ -135,7 +147,10 @@ function hasSharedSourceScope(shared){
|
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
function overlapAdmission(shared,{leftIndex,rightIndex,options}){
|
|
138
|
-
const
|
|
150
|
+
const baseHashMismatch=disjointNonEmpty(leftIndex.baseHashes,rightIndex.baseHashes);
|
|
151
|
+
const targetHashMismatch=disjointNonEmpty(leftIndex.targetHashes,rightIndex.targetHashes);
|
|
152
|
+
const replayOutputHashMismatch=disjointNonEmpty(leftIndex.semanticEditReplayOutputHashes,rightIndex.semanticEditReplayOutputHashes);
|
|
153
|
+
const hashMismatch=baseHashMismatch||targetHashMismatch||replayOutputHashMismatch;
|
|
139
154
|
const sourceRelated=shared.sourcePaths.length||shared.regionKeys.length||shared.conflictKeys.length||shared.sourceIdentityHashes.length;
|
|
140
155
|
const editContent=shared.operationContentHashes.length||shared.editContentHashes.length||shared.semanticEditReplayOutputHashes.length;
|
|
141
156
|
const transformContent=shared.semanticTransformContentHashes.length&&shared.projectionIdentityHashes.length;
|
|
@@ -158,8 +173,9 @@ function overlapAdmission(shared,{leftIndex,rightIndex,options}){
|
|
|
158
173
|
shared.regionKeys.length?'same-region-key':undefined,
|
|
159
174
|
shared.conflictKeys.length?'same-conflict-key':undefined,
|
|
160
175
|
shared.sourcePaths.length?'same-source-path':undefined,
|
|
161
|
-
status!=='independent'&&
|
|
162
|
-
status!=='independent'&&
|
|
176
|
+
status!=='independent'&&baseHashMismatch?'base-hash-mismatch':undefined,
|
|
177
|
+
status!=='independent'&&targetHashMismatch?'target-hash-mismatch':undefined,
|
|
178
|
+
status!=='independent'&&replayOutputHashMismatch?'replay-output-hash-mismatch':undefined
|
|
163
179
|
]);
|
|
164
180
|
return{
|
|
165
181
|
status,
|
|
@@ -223,7 +239,8 @@ function matchesOverlap(overlap,query){
|
|
|
223
239
|
function overlapScore(status,shared,reasonCodes){
|
|
224
240
|
const base=status==='duplicate'?100:status==='semantic-overlap'?75:status==='source-overlap'?35:0;
|
|
225
241
|
const sharedBonus=Math.min(20,countShared(shared));
|
|
226
|
-
const stalePenalty=reasonCodes.includes('base-hash-mismatch')||reasonCodes.includes('target-hash-mismatch')
|
|
242
|
+
const stalePenalty=reasonCodes.includes('base-hash-mismatch')||reasonCodes.includes('target-hash-mismatch')
|
|
243
|
+
||reasonCodes.includes('replay-output-hash-mismatch')?15:0;
|
|
227
244
|
return Math.max(0,base+sharedBonus-stalePenalty);
|
|
228
245
|
}
|
|
229
246
|
|
package/package.json
CHANGED