@shapeshift-labs/frontier-lang-compiler 0.2.148 → 0.2.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/declarations/semantic-patch-bundle-composition.d.ts +62 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/index-impl/semanticPatchBundleComposition.js +166 -0
- package/dist/js-ts-safe-merge-constants.js +1 -0
- package/dist/js-ts-safe-merge-context.js +1 -0
- package/dist/js-ts-safe-merge-jsx-attribute-fallback.js +320 -0
- package/dist/js-ts-safe-merge-plan.js +3 -1
- package/dist/js-ts-safe-merge-semantic-edit-artifacts.js +111 -0
- package/dist/js-ts-safe-merge-semantic-edit-fallback-utils.js +11 -0
- package/dist/js-ts-safe-merge-semantic-edit-fallback.js +67 -121
- package/dist/js-ts-safe-merge-source-shape-fallbacks.js +3 -1
- package/dist/js-ts-safe-merge-top-level-rename-fallback.js +149 -0
- package/dist/js-ts-safe-merge-top-level-rename-result.js +44 -0
- package/dist/js-ts-safe-project-merge-graph-conflicts.js +24 -3
- package/dist/js-ts-safe-project-merge.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EvidenceRecord, FrontierSourceLanguage } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import type { SemanticEditProjection, SemanticEditReplay } from './semantic-edit-script.js';
|
|
3
|
+
import type { SemanticPatchBundleRecord } from './semantic-patch-bundle.js';
|
|
4
|
+
import type { CompareSemanticPatchBundleRecordsOptions, SemanticPatchBundleOverlapRecord } from './semantic-patch-bundle-overlaps.js';
|
|
5
|
+
|
|
6
|
+
export type SemanticPatchBundleCompositionStatus = 'verified' | 'blocked' | string;
|
|
7
|
+
|
|
8
|
+
export interface ComposeSemanticPatchBundleProjectionsInput {
|
|
9
|
+
readonly id?: string;
|
|
10
|
+
readonly sourcePath?: string;
|
|
11
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
12
|
+
readonly currentSourceText?: string;
|
|
13
|
+
readonly headSourceText?: string;
|
|
14
|
+
readonly bundles?: readonly SemanticPatchBundleRecord[] | SemanticPatchBundleRecord;
|
|
15
|
+
readonly semanticPatchBundles?: readonly SemanticPatchBundleRecord[] | SemanticPatchBundleRecord;
|
|
16
|
+
readonly projections?: readonly SemanticEditProjection[] | SemanticEditProjection;
|
|
17
|
+
readonly semanticEditProjections?: readonly SemanticEditProjection[] | SemanticEditProjection;
|
|
18
|
+
readonly overlapOptions?: CompareSemanticPatchBundleRecordsOptions;
|
|
19
|
+
readonly parser?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SemanticPatchBundleComposition {
|
|
23
|
+
readonly kind: 'frontier.lang.semanticPatchBundleComposition';
|
|
24
|
+
readonly version: 1;
|
|
25
|
+
readonly schema: 'frontier.lang.semanticPatchBundleComposition.v1';
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
readonly status: SemanticPatchBundleCompositionStatus;
|
|
29
|
+
readonly sourcePath?: string;
|
|
30
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
31
|
+
readonly bundleIds: readonly string[];
|
|
32
|
+
readonly projectionIds: readonly string[];
|
|
33
|
+
readonly currentHash?: string;
|
|
34
|
+
readonly outputHash?: string;
|
|
35
|
+
readonly outputSourceText?: string;
|
|
36
|
+
readonly replays: readonly SemanticEditReplay[];
|
|
37
|
+
readonly verificationReplays: readonly SemanticEditReplay[];
|
|
38
|
+
readonly overlapRecords: readonly SemanticPatchBundleOverlapRecord[];
|
|
39
|
+
readonly admission: {
|
|
40
|
+
readonly status: 'auto-merge-candidate' | 'blocked' | string;
|
|
41
|
+
readonly action: 'apply' | 'human-review' | string;
|
|
42
|
+
readonly reviewRequired: boolean;
|
|
43
|
+
readonly autoApplyCandidate: boolean;
|
|
44
|
+
readonly autoMergeClaim: false;
|
|
45
|
+
readonly semanticEquivalenceClaim: false;
|
|
46
|
+
readonly reasonCodes: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
readonly summary: {
|
|
49
|
+
readonly bundles: number;
|
|
50
|
+
readonly projections: number;
|
|
51
|
+
readonly replays: number;
|
|
52
|
+
readonly verificationReplays: number;
|
|
53
|
+
readonly appliedEdits: number;
|
|
54
|
+
readonly overlapRecords: number;
|
|
55
|
+
readonly blockedOverlaps: number;
|
|
56
|
+
};
|
|
57
|
+
readonly evidence: readonly EvidenceRecord[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare function composeSemanticPatchBundleProjections(
|
|
61
|
+
input?: ComposeSemanticPatchBundleProjectionsInput
|
|
62
|
+
): SemanticPatchBundleComposition;
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './declarations/semantic-edit-bundle.js';
|
|
|
24
24
|
export * from './declarations/semantic-patch-bundle-index.js';
|
|
25
25
|
export * from './declarations/semantic-patch-bundle.js';
|
|
26
26
|
export * from './declarations/semantic-patch-bundle-overlaps.js';
|
|
27
|
+
export * from './declarations/semantic-patch-bundle-composition.js';
|
|
27
28
|
export * from './declarations/semantic-transform-identity.js';
|
|
28
29
|
export * from './declarations/bidirectional-target-change.js';
|
|
29
30
|
export * from './declarations/bidirectional-target-change-source-edit.js';
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export { queryProjectionReadinessMatrix } from './internal/index-impl/queryProje
|
|
|
72
72
|
export { createSemanticPatchBundleRecord, querySemanticPatchBundleRecords, SemanticPatchBundleAdmissionStatuses } from './internal/index-impl/semanticPatchBundleRecords.js';
|
|
73
73
|
export { createSemanticEditBundleAdmission, SemanticEditBundleAdmissionStatuses } from './internal/index-impl/semanticEditBundleAdmission.js';
|
|
74
74
|
export { compareSemanticPatchBundleRecords, querySemanticPatchBundleOverlaps, SemanticPatchBundleOverlapKinds, SemanticPatchBundleOverlapStatuses } from './internal/index-impl/semanticPatchBundleOverlaps.js';
|
|
75
|
+
export { composeSemanticPatchBundleProjections } from './internal/index-impl/semanticPatchBundleComposition.js';
|
|
75
76
|
export { createSemanticTransformIdentityRecord, deriveSemanticTransformIdentityRecords, semanticTransformIdentityFields } from './internal/index-impl/semanticTransformIdentityRecords.js';
|
|
76
77
|
export { createSemanticMergeCandidateAdmissionRecord, decorateSemanticMergeCandidateForAdmission, querySemanticMergeCandidateAdmissionOverlaps, SemanticMergeCandidateProjectionRisks, semanticMergeCandidateReadinessSortKey, sortSemanticMergeCandidateAdmissionRecords } from './internal/index-impl/semanticMergeCandidateRecords.js';
|
|
77
78
|
export { querySemanticMergeConflictClasses, SemanticMergeConflictClasses, semanticMergeConflictRiskScore, sortSemanticMergeCandidatesByConflictRisk, summarizeSemanticMergeConflicts } from './internal/index-impl/semanticMergeConflicts.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { idFragment, uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
import { compareSemanticPatchBundleRecords } from './semanticPatchBundleOverlaps.js';
|
|
4
|
+
import { replaySemanticEditProjection } from './replaySemanticEditProjection.js';
|
|
5
|
+
|
|
6
|
+
function composeSemanticPatchBundleProjections(input = {}) {
|
|
7
|
+
const id = String(input.id ?? 'semantic_patch_bundle_composition');
|
|
8
|
+
const currentSourceText = input.currentSourceText ?? input.headSourceText;
|
|
9
|
+
const projections = array(input.projections ?? input.semanticEditProjections);
|
|
10
|
+
const bundles = array(input.bundles ?? input.semanticPatchBundles);
|
|
11
|
+
const sourcePath = input.sourcePath ?? projections.find((projection) => projection?.sourcePath)?.sourcePath;
|
|
12
|
+
const language = input.language ?? projections.find((projection) => projection?.language)?.language;
|
|
13
|
+
const reasonCodes = baseReasonCodes({ currentSourceText, projections, bundles });
|
|
14
|
+
const overlapRecords = bundleOverlapRecords(bundles, input.overlapOptions);
|
|
15
|
+
reasonCodes.push(...overlapRecords.flatMap((record) => record.admission.status === 'independent'
|
|
16
|
+
? []
|
|
17
|
+
: [`bundle-overlap:${record.admission.status}`]));
|
|
18
|
+
const replays = reasonCodes.length ? [] : projections.map((projection, index) => replaySemanticEditProjection({
|
|
19
|
+
id: `${id}_replay_${index + 1}`,
|
|
20
|
+
projection,
|
|
21
|
+
currentSourceText,
|
|
22
|
+
currentSourcePath: sourcePath ?? projection.sourcePath,
|
|
23
|
+
language: language ?? projection.language,
|
|
24
|
+
parser: input.parser
|
|
25
|
+
}));
|
|
26
|
+
reasonCodes.push(...replayReasonCodes(replays));
|
|
27
|
+
const edits = reasonCodes.length ? [] : appliedReplayEdits(replays);
|
|
28
|
+
reasonCodes.push(...sourceEditOverlapReasons(edits));
|
|
29
|
+
const outputSourceText = reasonCodes.length ? undefined : applyReplayEdits(currentSourceText, edits);
|
|
30
|
+
const verificationReplays = outputSourceText === undefined ? [] : projections.map((projection, index) => replaySemanticEditProjection({
|
|
31
|
+
id: `${id}_already_applied_${index + 1}`,
|
|
32
|
+
projection,
|
|
33
|
+
currentSourceText: outputSourceText,
|
|
34
|
+
currentSourcePath: sourcePath ?? projection.sourcePath,
|
|
35
|
+
language: language ?? projection.language,
|
|
36
|
+
parser: input.parser
|
|
37
|
+
}));
|
|
38
|
+
reasonCodes.push(...verificationReasonCodes(verificationReplays));
|
|
39
|
+
const status = reasonCodes.length ? 'blocked' : 'verified';
|
|
40
|
+
const core = {
|
|
41
|
+
kind: 'frontier.lang.semanticPatchBundleComposition',
|
|
42
|
+
version: 1,
|
|
43
|
+
schema: 'frontier.lang.semanticPatchBundleComposition.v1',
|
|
44
|
+
id,
|
|
45
|
+
status,
|
|
46
|
+
sourcePath,
|
|
47
|
+
language,
|
|
48
|
+
bundleIds: bundles.map((bundle) => bundle?.id).filter(Boolean),
|
|
49
|
+
projectionIds: projections.map((projection) => projection?.id).filter(Boolean),
|
|
50
|
+
currentHash: typeof currentSourceText === 'string' ? hashSemanticValue(currentSourceText) : undefined,
|
|
51
|
+
outputHash: outputSourceText === undefined ? undefined : hashSemanticValue(outputSourceText),
|
|
52
|
+
outputSourceText,
|
|
53
|
+
replays,
|
|
54
|
+
verificationReplays,
|
|
55
|
+
overlapRecords,
|
|
56
|
+
admission: {
|
|
57
|
+
status: status === 'verified' ? 'auto-merge-candidate' : 'blocked',
|
|
58
|
+
action: status === 'verified' ? 'apply' : 'human-review',
|
|
59
|
+
reviewRequired: status !== 'verified',
|
|
60
|
+
autoApplyCandidate: status === 'verified',
|
|
61
|
+
autoMergeClaim: false,
|
|
62
|
+
semanticEquivalenceClaim: false,
|
|
63
|
+
reasonCodes: uniqueStrings(reasonCodes)
|
|
64
|
+
},
|
|
65
|
+
summary: {
|
|
66
|
+
bundles: bundles.length,
|
|
67
|
+
projections: projections.length,
|
|
68
|
+
replays: replays.length,
|
|
69
|
+
verificationReplays: verificationReplays.length,
|
|
70
|
+
appliedEdits: edits.length,
|
|
71
|
+
overlapRecords: overlapRecords.length,
|
|
72
|
+
blockedOverlaps: overlapRecords.filter((record) => record.admission.status !== 'independent').length
|
|
73
|
+
},
|
|
74
|
+
evidence: [{
|
|
75
|
+
id: `evidence_${idFragment(id)}_semantic_patch_bundle_composition`,
|
|
76
|
+
kind: 'semantic-patch-bundle-composition',
|
|
77
|
+
status: status === 'verified' ? 'passed' : 'needs-review',
|
|
78
|
+
path: sourcePath,
|
|
79
|
+
summary: status === 'verified'
|
|
80
|
+
? `Composed ${edits.length} replayed semantic edit(s) from ${projections.length} projection(s).`
|
|
81
|
+
: `Semantic patch bundle composition blocked: ${uniqueStrings(reasonCodes).join(', ')}.`,
|
|
82
|
+
metadata: {
|
|
83
|
+
bundleIds: bundles.map((bundle) => bundle?.id).filter(Boolean),
|
|
84
|
+
projectionIds: projections.map((projection) => projection?.id).filter(Boolean),
|
|
85
|
+
autoMergeClaim: false,
|
|
86
|
+
semanticEquivalenceClaim: false
|
|
87
|
+
}
|
|
88
|
+
}]
|
|
89
|
+
};
|
|
90
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function baseReasonCodes(input) {
|
|
94
|
+
return uniqueStrings([
|
|
95
|
+
typeof input.currentSourceText === 'string' ? undefined : 'missing-current-source-text',
|
|
96
|
+
input.projections.length ? undefined : 'missing-semantic-edit-projections',
|
|
97
|
+
input.bundles.length && input.bundles.length !== input.projections.length ? 'bundle-projection-count-mismatch' : undefined,
|
|
98
|
+
...input.projections.map((projection, index) => projection?.status === 'projected' ? undefined : `projection-not-projected:${index + 1}`)
|
|
99
|
+
].filter(Boolean));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function bundleOverlapRecords(bundles, options = {}) {
|
|
103
|
+
const records = [];
|
|
104
|
+
for (let left = 0; left < bundles.length; left += 1) {
|
|
105
|
+
for (let right = left + 1; right < bundles.length; right += 1) {
|
|
106
|
+
records.push(compareSemanticPatchBundleRecords(bundles[left], bundles[right], options));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return records;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function replayReasonCodes(replays) {
|
|
113
|
+
return uniqueStrings(replays.flatMap((replay, index) => {
|
|
114
|
+
if (replay.status === 'accepted-clean' || replay.status === 'already-applied') return [];
|
|
115
|
+
return [`replay-not-clean:${index + 1}:${replay.status}`, ...(replay.admission?.reasonCodes ?? replay.summary?.reasonCodes ?? [])];
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function appliedReplayEdits(replays) {
|
|
120
|
+
return replays.flatMap((replay, replayIndex) => (replay.edits ?? [])
|
|
121
|
+
.filter((edit) => edit.status === 'applied')
|
|
122
|
+
.map((edit, editIndex) => ({
|
|
123
|
+
replayIndex,
|
|
124
|
+
editIndex,
|
|
125
|
+
operationId: edit.operationId,
|
|
126
|
+
start: edit.start,
|
|
127
|
+
end: edit.end,
|
|
128
|
+
replacementText: edit.replacementText
|
|
129
|
+
})));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sourceEditOverlapReasons(edits) {
|
|
133
|
+
const reasons = [];
|
|
134
|
+
const ordered = edits.slice().sort((left, right) => left.start - right.start || left.end - right.end);
|
|
135
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
136
|
+
const previous = ordered[index - 1];
|
|
137
|
+
const current = ordered[index];
|
|
138
|
+
if (sourceEditsOverlap(previous, current)) reasons.push(`composed-source-edit-overlap:${previous.operationId}:${current.operationId}`);
|
|
139
|
+
}
|
|
140
|
+
return uniqueStrings(reasons);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function applyReplayEdits(sourceText, edits) {
|
|
144
|
+
return edits.slice().sort((left, right) => right.start - left.start || right.end - left.end)
|
|
145
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacementText + text.slice(edit.end), sourceText);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function verificationReasonCodes(replays) {
|
|
149
|
+
return uniqueStrings(replays.flatMap((replay, index) => {
|
|
150
|
+
if (replay.status === 'already-applied') return [];
|
|
151
|
+
return [`verification-replay-not-already-applied:${index + 1}:${replay.status}`, ...(replay.admission?.reasonCodes ?? [])];
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sourceEditsOverlap(left, right) {
|
|
156
|
+
if (left.start === left.end) return right.start < left.start && left.start < right.end;
|
|
157
|
+
if (right.start === right.end) return left.start < right.start && right.start < left.end;
|
|
158
|
+
return left.start < right.end && right.start < left.end;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function array(value) {
|
|
162
|
+
if (value === undefined || value === null) return [];
|
|
163
|
+
return Array.isArray(value) ? value : [value];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { composeSemanticPatchBundleProjections };
|
|
@@ -19,6 +19,7 @@ export const JsTsSafeMergeConflictCodes = Object.freeze({
|
|
|
19
19
|
malformedSyntax: 'malformed-syntax',
|
|
20
20
|
sideEffectImportReorder: 'side-effect-import-reorder',
|
|
21
21
|
topLevelOrderChanged: 'top-level-order-changed',
|
|
22
|
+
topLevelRenamePublicExportContract: 'top-level-rename-public-export-contract',
|
|
22
23
|
changedExistingDeclaration: 'changed-existing-declaration',
|
|
23
24
|
typeAliasConflict: 'type-alias-conflict',
|
|
24
25
|
importShapeChanged: 'import-shape-changed',
|
|
@@ -6,6 +6,7 @@ export function createMergeContext(input) {
|
|
|
6
6
|
sourcePath: input.sourcePath,
|
|
7
7
|
language: input.language ?? 'typescript',
|
|
8
8
|
deferReExportIdentityConflictsToProjectGraph: input.deferReExportIdentityConflictsToProjectGraph === true,
|
|
9
|
+
deferTopLevelRenamePublicExportContractToProjectGraph: input.deferTopLevelRenamePublicExportContractToProjectGraph === true,
|
|
9
10
|
conflicts: [],
|
|
10
11
|
blockedGateIds: new Set(),
|
|
11
12
|
gateReasonCodes: new Map()
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { JsTsSafeMergeStatuses } from './js-ts-safe-merge-constants.js';
|
|
2
|
+
import { createJsTsSafeMergeSemanticArtifacts } from './js-ts-safe-merge-semantic-artifacts.js';
|
|
3
|
+
import { uniqueStrings } from './js-ts-safe-merge-context.js';
|
|
4
|
+
import { semanticFallbackChangedExistingDeclarations } from './js-ts-safe-merge-semantic-edit-fallback-utils.js';
|
|
5
|
+
|
|
6
|
+
function createJsxAttributeSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
7
|
+
const currentSourceText = stagedFallback?.directReplayCurrentSourceText
|
|
8
|
+
?? stagedFallback?.replayCurrentSourceText
|
|
9
|
+
?? input.headSourceText;
|
|
10
|
+
const merge = mergeJsxAttributeSources({
|
|
11
|
+
baseSourceText: input.baseSourceText,
|
|
12
|
+
workerSourceText: input.workerSourceText,
|
|
13
|
+
headSourceText: input.headSourceText,
|
|
14
|
+
currentSourceText
|
|
15
|
+
});
|
|
16
|
+
if (!merge.ok || merge.sourceText === currentSourceText) return undefined;
|
|
17
|
+
const resultBase = stagedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
18
|
+
const language = input.language ?? topLevelResult.language ?? 'tsx';
|
|
19
|
+
const sourcePath = input.sourcePath ?? topLevelResult.sourcePath ?? 'inline.tsx';
|
|
20
|
+
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
21
|
+
const artifacts = createJsTsSafeMergeSemanticArtifacts({
|
|
22
|
+
...input,
|
|
23
|
+
id: `${id}_jsx_attribute`,
|
|
24
|
+
language,
|
|
25
|
+
sourcePath,
|
|
26
|
+
headSourceText: currentSourceText,
|
|
27
|
+
headHash: undefined,
|
|
28
|
+
currentSourceHash: undefined
|
|
29
|
+
}, {
|
|
30
|
+
...resultBase,
|
|
31
|
+
id: `${String(input.id ?? resultBase.id ?? 'js_ts_safe_merge')}_jsx_attribute`,
|
|
32
|
+
language,
|
|
33
|
+
sourcePath,
|
|
34
|
+
mergedSourceText: merge.sourceText,
|
|
35
|
+
outputSourceText: merge.sourceText
|
|
36
|
+
});
|
|
37
|
+
if (artifacts.status !== 'verified') return undefined;
|
|
38
|
+
const gates = semanticArtifactGates(artifacts);
|
|
39
|
+
return {
|
|
40
|
+
...resultBase,
|
|
41
|
+
id: String(input.id ?? resultBase.id ?? topLevelResult.id),
|
|
42
|
+
status: JsTsSafeMergeStatuses.merged,
|
|
43
|
+
mergedSourceText: merge.sourceText,
|
|
44
|
+
outputSourceText: merge.sourceText,
|
|
45
|
+
conflicts: [],
|
|
46
|
+
gates,
|
|
47
|
+
admission: {
|
|
48
|
+
status: 'auto-merge-candidate',
|
|
49
|
+
action: 'apply',
|
|
50
|
+
reviewRequired: false,
|
|
51
|
+
autoApplyCandidate: true,
|
|
52
|
+
autoMergeClaim: false,
|
|
53
|
+
semanticEquivalenceClaim: false,
|
|
54
|
+
reasonCodes: []
|
|
55
|
+
},
|
|
56
|
+
summary: {
|
|
57
|
+
...resultBase.summary,
|
|
58
|
+
changedExistingDeclarations: semanticFallbackChangedExistingDeclarations(topLevelResult, resultBase, stagedFallback),
|
|
59
|
+
conflicts: 0,
|
|
60
|
+
gatesPassed: gates.filter((gate) => gate.status === 'passed').length,
|
|
61
|
+
semanticEditOperations: artifacts.script.summary.operations,
|
|
62
|
+
semanticEditAppliedOperations: artifacts.replay.summary.applied,
|
|
63
|
+
semanticEditReplayStatus: artifacts.replay.status,
|
|
64
|
+
jsxAttributeTags: merge.summary.tags,
|
|
65
|
+
jsxAttributeEdits: merge.summary.edits,
|
|
66
|
+
composedPhases: 2
|
|
67
|
+
},
|
|
68
|
+
metadata: {
|
|
69
|
+
...resultBase.metadata,
|
|
70
|
+
composed: {
|
|
71
|
+
phase: stagedFallback
|
|
72
|
+
? 'staged-top-level-jsx-attribute-semantic-fallback'
|
|
73
|
+
: 'jsx-attribute-semantic-fallback',
|
|
74
|
+
phases: stagedFallback
|
|
75
|
+
? ['top-level-neutralization', 'top-level-ledger', 'jsx-attribute']
|
|
76
|
+
: ['top-level-ledger', 'jsx-attribute'],
|
|
77
|
+
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? [],
|
|
78
|
+
stagedTopLevelSummary: stagedFallback?.stagedTopLevelResult?.summary,
|
|
79
|
+
neutralization: stagedFallback?.neutralization?.summary,
|
|
80
|
+
jsxAttributeFallback: merge.summary
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
semanticArtifacts: artifacts
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeJsxAttributeSources(input) {
|
|
88
|
+
if (![input.baseSourceText, input.workerSourceText, input.headSourceText, input.currentSourceText].every(isString)) {
|
|
89
|
+
return blocked('missing-source-text');
|
|
90
|
+
}
|
|
91
|
+
const parsed = ['base', 'worker', 'head', 'current'].map((side) => parseJsxTags(input[`${side}SourceText`]));
|
|
92
|
+
if (parsed.some((source) => source.reasonCodes.length)) return blocked('jsx-attribute-parse-blocked');
|
|
93
|
+
const [base, worker, head, current] = parsed;
|
|
94
|
+
const edits = [];
|
|
95
|
+
let changedTags = 0;
|
|
96
|
+
for (const baseTag of base.tags) {
|
|
97
|
+
const workerTag = worker.byKey.get(baseTag.key);
|
|
98
|
+
const headTag = head.byKey.get(baseTag.key);
|
|
99
|
+
const currentTag = current.byKey.get(baseTag.key);
|
|
100
|
+
if (!workerTag || !headTag || !currentTag) continue;
|
|
101
|
+
if (sameTagText(baseTag, workerTag)) continue;
|
|
102
|
+
const merged = mergeTagAttributes(baseTag, workerTag, headTag, currentTag);
|
|
103
|
+
if (merged.status === 'blocked') return blocked(...merged.reasonCodes);
|
|
104
|
+
for (const edit of merged.edits) edits.push(edit);
|
|
105
|
+
if (merged.edits.length) changedTags += 1;
|
|
106
|
+
}
|
|
107
|
+
if (!edits.length) return blocked('no-jsx-attribute-merge-candidate');
|
|
108
|
+
const sourceText = edits.sort((left, right) => right.start - left.start)
|
|
109
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), input.currentSourceText);
|
|
110
|
+
return { ok: true, sourceText, summary: { tags: changedTags, edits: edits.length } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mergeTagAttributes(base, worker, head, current) {
|
|
114
|
+
if (![worker, head, current].every((tag) => tag.tagName === base.tagName)) return blockedTag('jsx-tag-name-changed');
|
|
115
|
+
const maps = [base, worker, head, current].map(attributeMap);
|
|
116
|
+
if (maps.some((map) => map.reasonCodes.length)) return blockedTag('jsx-attribute-duplicate-name');
|
|
117
|
+
if (![worker, head, current].every((tag) => sameAttributeNames(base, tag))) {
|
|
118
|
+
return blockedTag('jsx-attribute-shape-changed');
|
|
119
|
+
}
|
|
120
|
+
const [, workerAttrs, headAttrs, currentAttrs] = maps.map((map) => map.byName);
|
|
121
|
+
const edits = [];
|
|
122
|
+
for (const baseAttr of base.attributes) {
|
|
123
|
+
const workerAttr = workerAttrs.get(baseAttr.name);
|
|
124
|
+
const headAttr = headAttrs.get(baseAttr.name);
|
|
125
|
+
const currentAttr = currentAttrs.get(baseAttr.name);
|
|
126
|
+
const workerChanged = !sameAttrText(baseAttr, workerAttr);
|
|
127
|
+
const headChanged = !sameAttrText(baseAttr, headAttr);
|
|
128
|
+
if (workerChanged && headChanged && !sameAttrText(workerAttr, headAttr)) {
|
|
129
|
+
return blockedTag('jsx-attribute-conflict');
|
|
130
|
+
}
|
|
131
|
+
if (!sameAttrText(currentAttr, headAttr) && !sameAttrText(currentAttr, workerAttr)) {
|
|
132
|
+
return blockedTag('jsx-attribute-current-diverged');
|
|
133
|
+
}
|
|
134
|
+
if (workerChanged && !sameAttrText(currentAttr, workerAttr)) {
|
|
135
|
+
edits.push({ start: currentAttr.start, end: currentAttr.end, replacement: workerAttr.text });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { status: 'merged', edits };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseJsxTags(sourceText) {
|
|
142
|
+
const tags = [];
|
|
143
|
+
const reasonCodes = [];
|
|
144
|
+
const ordinals = new Map();
|
|
145
|
+
let index = 0;
|
|
146
|
+
while (index < sourceText.length) {
|
|
147
|
+
const start = sourceText.indexOf('<', index);
|
|
148
|
+
if (start === -1) break;
|
|
149
|
+
const parsed = parseOpeningTag(sourceText, start);
|
|
150
|
+
if (!parsed) {
|
|
151
|
+
index = start + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (parsed.reasonCodes.length) reasonCodes.push(...parsed.reasonCodes);
|
|
155
|
+
const ordinal = (ordinals.get(parsed.tagName) ?? 0) + 1;
|
|
156
|
+
ordinals.set(parsed.tagName, ordinal);
|
|
157
|
+
tags.push({ ...parsed, key: `${parsed.tagName}#${ordinal}` });
|
|
158
|
+
index = parsed.end;
|
|
159
|
+
}
|
|
160
|
+
return { tags, byKey: new Map(tags.map((tag) => [tag.key, tag])), reasonCodes: uniqueStrings(reasonCodes) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseOpeningTag(sourceText, start) {
|
|
164
|
+
const afterOpen = start + 1;
|
|
165
|
+
if (/[/!?>]/.test(sourceText[afterOpen] ?? '')) return undefined;
|
|
166
|
+
const nameMatch = /^[A-Za-z_$][\w$]*(?:[.:][A-Za-z_$][\w$]*|-[\w$]+)*/.exec(sourceText.slice(afterOpen));
|
|
167
|
+
if (!nameMatch) return undefined;
|
|
168
|
+
const tagName = nameMatch[0];
|
|
169
|
+
const nameEnd = afterOpen + tagName.length;
|
|
170
|
+
const end = openingTagEnd(sourceText, nameEnd);
|
|
171
|
+
if (end === undefined) return undefined;
|
|
172
|
+
const attributes = parseAttributes(sourceText, nameEnd, end - 1);
|
|
173
|
+
return {
|
|
174
|
+
tagName,
|
|
175
|
+
start,
|
|
176
|
+
end,
|
|
177
|
+
text: sourceText.slice(start, end),
|
|
178
|
+
attributes: attributes.values,
|
|
179
|
+
reasonCodes: attributes.reasonCodes
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseAttributes(sourceText, start, end) {
|
|
184
|
+
const values = [];
|
|
185
|
+
const reasonCodes = [];
|
|
186
|
+
let cursor = start;
|
|
187
|
+
while (cursor < end) {
|
|
188
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
189
|
+
if (sourceText[cursor] === '/') {
|
|
190
|
+
cursor += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const attrStart = cursor;
|
|
194
|
+
const nameMatch = /^[A-Za-z_$][\w$:-]*/.exec(sourceText.slice(cursor, end));
|
|
195
|
+
if (!nameMatch) {
|
|
196
|
+
reasonCodes.push('jsx-attribute-token-unsupported');
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const name = nameMatch[0];
|
|
200
|
+
cursor += name.length;
|
|
201
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
202
|
+
if (sourceText[cursor] === '=') {
|
|
203
|
+
cursor += 1;
|
|
204
|
+
while (cursor < end && /\s/.test(sourceText[cursor])) cursor += 1;
|
|
205
|
+
cursor = attributeValueEnd(sourceText, cursor, end);
|
|
206
|
+
if (cursor === undefined) return { values, reasonCodes: ['jsx-attribute-value-unterminated'] };
|
|
207
|
+
}
|
|
208
|
+
values.push({ name, start: attrStart, end: cursor, text: sourceText.slice(attrStart, cursor) });
|
|
209
|
+
}
|
|
210
|
+
return { values, reasonCodes: uniqueStrings(reasonCodes) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function openingTagEnd(sourceText, start) {
|
|
214
|
+
let quote;
|
|
215
|
+
let escaped = false;
|
|
216
|
+
let braceDepth = 0;
|
|
217
|
+
for (let index = start; index < sourceText.length; index += 1) {
|
|
218
|
+
const char = sourceText[index];
|
|
219
|
+
if (quote) {
|
|
220
|
+
if (escaped) escaped = false;
|
|
221
|
+
else if (char === '\\') escaped = true;
|
|
222
|
+
else if (char === quote) quote = undefined;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === '"' || char === '\'' || char === '`') quote = char;
|
|
226
|
+
else if (char === '{') braceDepth += 1;
|
|
227
|
+
else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
|
|
228
|
+
else if (char === '>' && braceDepth === 0) return index + 1;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function attributeValueEnd(sourceText, start, end) {
|
|
234
|
+
const first = sourceText[start];
|
|
235
|
+
if (first === '"' || first === '\'') return quotedValueEnd(sourceText, start, end, first);
|
|
236
|
+
if (first === '{') return bracedValueEnd(sourceText, start, end);
|
|
237
|
+
let cursor = start;
|
|
238
|
+
while (cursor < end && !/[\s/]/.test(sourceText[cursor])) cursor += 1;
|
|
239
|
+
return cursor;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function quotedValueEnd(sourceText, start, end, quote) {
|
|
243
|
+
let escaped = false;
|
|
244
|
+
for (let cursor = start + 1; cursor < end; cursor += 1) {
|
|
245
|
+
const char = sourceText[cursor];
|
|
246
|
+
if (escaped) escaped = false;
|
|
247
|
+
else if (char === '\\') escaped = true;
|
|
248
|
+
else if (char === quote) return cursor + 1;
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function bracedValueEnd(sourceText, start, end) {
|
|
254
|
+
let depth = 0;
|
|
255
|
+
let quote;
|
|
256
|
+
let escaped = false;
|
|
257
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
258
|
+
const char = sourceText[cursor];
|
|
259
|
+
if (quote) {
|
|
260
|
+
if (escaped) escaped = false;
|
|
261
|
+
else if (char === '\\') escaped = true;
|
|
262
|
+
else if (char === quote) quote = undefined;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === '"' || char === '\'' || char === '`') quote = char;
|
|
266
|
+
else if (char === '{') depth += 1;
|
|
267
|
+
else if (char === '}') {
|
|
268
|
+
depth -= 1;
|
|
269
|
+
if (depth === 0) return cursor + 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function attributeMap(tag) {
|
|
276
|
+
const byName = new Map();
|
|
277
|
+
const duplicateNames = [];
|
|
278
|
+
for (const attribute of tag.attributes) {
|
|
279
|
+
if (byName.has(attribute.name)) duplicateNames.push(attribute.name);
|
|
280
|
+
byName.set(attribute.name, attribute);
|
|
281
|
+
}
|
|
282
|
+
return { byName, reasonCodes: duplicateNames.length ? ['jsx-attribute-duplicate-name'] : [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function sameAttributeNames(left, right) {
|
|
286
|
+
return left.attributes.map((attr) => attr.name).join('\0') === right.attributes.map((attr) => attr.name).join('\0');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sameTagText(left, right) {
|
|
290
|
+
return String(left?.text ?? '').trim() === String(right?.text ?? '').trim();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sameAttrText(left, right) {
|
|
294
|
+
return String(left?.text ?? '').trim() === String(right?.text ?? '').trim();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function semanticArtifactGates(artifacts) {
|
|
298
|
+
return [
|
|
299
|
+
gate('semantic-edit-script', artifacts.script?.admission?.status === 'auto-merge-candidate', artifacts.script?.admission?.reasonCodes),
|
|
300
|
+
gate('semantic-edit-projection', artifacts.projection?.status === 'projected', artifacts.projection?.admission?.reasonCodes),
|
|
301
|
+
gate('semantic-edit-replay', artifacts.replay?.status === 'accepted-clean', artifacts.replay?.admission?.reasonCodes),
|
|
302
|
+
gate('semantic-edit-already-applied', artifacts.alreadyAppliedReplay?.status === 'already-applied', artifacts.alreadyAppliedReplay?.admission?.reasonCodes)
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function gate(id, passed, reasonCodes = []) {
|
|
307
|
+
return { id, status: passed ? 'passed' : 'blocked', reasonCodes: passed ? [] : uniqueStrings(reasonCodes) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function blocked(...reasonCodes) {
|
|
311
|
+
return { ok: false, reasonCodes: uniqueStrings(reasonCodes) };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function blockedTag(...reasonCodes) {
|
|
315
|
+
return { status: 'blocked', reasonCodes: uniqueStrings(reasonCodes) };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isString(value) { return typeof value === 'string'; }
|
|
319
|
+
|
|
320
|
+
export { createJsxAttributeSemanticFallbackResult };
|
|
@@ -225,11 +225,13 @@ function renderImportStatement(importInfo, specifiers) {
|
|
|
225
225
|
const clause = [];
|
|
226
226
|
if (importInfo.defaultLocalName) clause.push(importInfo.defaultLocalName);
|
|
227
227
|
if (importInfo.namespaceLocalName) clause.push(`* as ${importInfo.namespaceLocalName}`);
|
|
228
|
-
if (specifiers.length) clause.push(`{ ${specifiers.map(
|
|
228
|
+
if (specifiers.length) clause.push(`{ ${specifiers.map((specifier) => renderImportSpecifier(specifier, importInfo)).join(', ')} }`);
|
|
229
229
|
const importType = importInfo.typeOnly ? 'type ' : '';
|
|
230
230
|
return `import ${importType}${clause.join(', ')} from ${importInfo.quote}${importInfo.moduleSpecifier}${importInfo.quote};`;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
const renderImportSpecifier = (specifier, importInfo) => importSpecifierCanonical(importInfo.typeOnly ? { ...specifier, typeOnly: false } : specifier);
|
|
234
|
+
|
|
233
235
|
function importInsertionText(entries, lineEnding) {
|
|
234
236
|
return entries
|
|
235
237
|
.map((entry) => normalizeLineEndings(entry.text.trimEnd(), lineEnding))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { semanticFallbackPhase } from './js-ts-safe-merge-semantic-edit-fallback-utils.js';
|
|
3
|
+
import { idFragment, uniqueStrings } from './native-import-utils.js';
|
|
4
|
+
|
|
5
|
+
function semanticEditArtifacts(input) {
|
|
6
|
+
const reasonCodes = semanticEditArtifactReasonCodes(input);
|
|
7
|
+
const status = reasonCodes.length ? 'blocked' : 'verified';
|
|
8
|
+
const core = {
|
|
9
|
+
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
10
|
+
version: 1,
|
|
11
|
+
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
12
|
+
id: `js_ts_safe_merge_semantic_edit_artifacts_${idFragment(input.id)}`,
|
|
13
|
+
sourcePath: input.sourcePath,
|
|
14
|
+
language: input.language,
|
|
15
|
+
status,
|
|
16
|
+
script: input.script,
|
|
17
|
+
projection: input.projection,
|
|
18
|
+
replay: input.replay,
|
|
19
|
+
alreadyAppliedReplay: input.alreadyAppliedReplay,
|
|
20
|
+
admission: {
|
|
21
|
+
status: status === 'verified' ? 'auto-merge-candidate' : 'blocked',
|
|
22
|
+
action: status === 'verified' ? 'apply' : 'human-review',
|
|
23
|
+
reviewRequired: status !== 'verified',
|
|
24
|
+
autoApplyCandidate: status === 'verified',
|
|
25
|
+
autoMergeClaim: false,
|
|
26
|
+
semanticEquivalenceClaim: false,
|
|
27
|
+
reasonCodes
|
|
28
|
+
},
|
|
29
|
+
summary: {
|
|
30
|
+
operations: input.script.summary.operations,
|
|
31
|
+
edits: input.projection.edits.length,
|
|
32
|
+
replayStatus: input.replay.status,
|
|
33
|
+
alreadyAppliedReplayStatus: input.alreadyAppliedReplay.status,
|
|
34
|
+
projectedSourceMatchesMerged: input.projection.sourceText === input.replay.outputSourceText,
|
|
35
|
+
replayOutputMatchesMerged: input.replay.outputSourceText === input.projection.sourceText
|
|
36
|
+
},
|
|
37
|
+
evidence: [{
|
|
38
|
+
id: `evidence_${idFragment(input.id)}_js_ts_semantic_edit_replay`,
|
|
39
|
+
kind: 'js-ts-semantic-edit-replay',
|
|
40
|
+
status: status === 'verified' ? 'passed' : 'needs-review',
|
|
41
|
+
path: input.sourcePath,
|
|
42
|
+
summary: status === 'verified'
|
|
43
|
+
? `JS/TS semantic edit replay verified ${input.script.summary.operations} operation(s).`
|
|
44
|
+
: `JS/TS semantic edit replay requires review: ${reasonCodes.join(', ')}.`
|
|
45
|
+
}],
|
|
46
|
+
metadata: {
|
|
47
|
+
autoMergeClaim: false,
|
|
48
|
+
semanticEquivalenceClaim: false,
|
|
49
|
+
source: input.stagedFallback ? semanticFallbackPhase(input.stagedFallback) : 'js-ts-semantic-edit-fallback',
|
|
50
|
+
originalReasonCodes: input.topLevelResult.admission?.reasonCodes ?? [],
|
|
51
|
+
stagedTopLevelSummary: input.stagedFallback?.stagedTopLevelResult?.summary,
|
|
52
|
+
neutralization: input.stagedFallback?.neutralization?.summary
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function blockedSemanticEditArtifacts(input, topLevelResult, reasonCodes, error) {
|
|
59
|
+
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
60
|
+
const core = {
|
|
61
|
+
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
62
|
+
version: 1,
|
|
63
|
+
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
64
|
+
id: `js_ts_safe_merge_semantic_edit_artifacts_${idFragment(id)}`,
|
|
65
|
+
sourcePath: input.sourcePath ?? topLevelResult.sourcePath,
|
|
66
|
+
language: input.language ?? topLevelResult.language ?? 'typescript',
|
|
67
|
+
status: 'blocked',
|
|
68
|
+
admission: {
|
|
69
|
+
status: 'blocked',
|
|
70
|
+
action: 'human-review',
|
|
71
|
+
reviewRequired: true,
|
|
72
|
+
autoApplyCandidate: false,
|
|
73
|
+
autoMergeClaim: false,
|
|
74
|
+
semanticEquivalenceClaim: false,
|
|
75
|
+
reasonCodes
|
|
76
|
+
},
|
|
77
|
+
summary: {
|
|
78
|
+
operations: 0,
|
|
79
|
+
edits: 0,
|
|
80
|
+
replayStatus: 'blocked',
|
|
81
|
+
alreadyAppliedReplayStatus: 'blocked',
|
|
82
|
+
projectedSourceMatchesMerged: false,
|
|
83
|
+
replayOutputMatchesMerged: false
|
|
84
|
+
},
|
|
85
|
+
metadata: {
|
|
86
|
+
source: 'js-ts-semantic-edit-fallback',
|
|
87
|
+
error: error?.message
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function semanticEditArtifactReasonCodes(input) {
|
|
94
|
+
const scriptReady = input.script.admission.status === 'auto-merge-candidate';
|
|
95
|
+
const projectionReady = input.projection.status === 'projected';
|
|
96
|
+
const replayReady = input.replay.status === 'accepted-clean';
|
|
97
|
+
const alreadyAppliedReady = input.alreadyAppliedReplay.status === 'already-applied';
|
|
98
|
+
return uniqueStrings([
|
|
99
|
+
scriptReady ? undefined : `semantic-edit-script-${input.script.admission.status}`,
|
|
100
|
+
projectionReady ? undefined : `semantic-edit-projection-${input.projection.status}`,
|
|
101
|
+
replayReady ? undefined : `semantic-edit-replay-${input.replay.status}`,
|
|
102
|
+
input.replay.outputSourceText !== input.projection.sourceText ? 'semantic-edit-replay-output-mismatch' : undefined,
|
|
103
|
+
alreadyAppliedReady ? undefined : `semantic-edit-already-applied-${input.alreadyAppliedReplay.status}`,
|
|
104
|
+
...(scriptReady ? [] : input.script.admission.reasonCodes),
|
|
105
|
+
...(projectionReady ? [] : input.projection.admission.reasonCodes),
|
|
106
|
+
...(replayReady ? [] : input.replay.admission.reasonCodes),
|
|
107
|
+
...(alreadyAppliedReady ? [] : input.alreadyAppliedReplay.admission.reasonCodes)
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { blockedSemanticEditArtifacts, semanticEditArtifacts };
|
|
@@ -33,7 +33,18 @@ function semanticFallbackPhase(fallback) {
|
|
|
33
33
|
: 'staged-top-level-semantic-edit-fallback';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function semanticFallbackCandidates(stagedFallback) {
|
|
37
|
+
if (!stagedFallback) return [undefined];
|
|
38
|
+
const headChanged = (stagedFallback.neutralization?.summary?.headChangedExistingDeclarations ?? 0) > 0;
|
|
39
|
+
const directFallback = stagedFallback.directProjectionHeadSourceText && (headChanged || stagedFallback.safeTopLevelChanges > 0)
|
|
40
|
+
? { ...stagedFallback, projectionMode: 'direct' }
|
|
41
|
+
: undefined;
|
|
42
|
+
if (headChanged) return directFallback ? [directFallback, undefined] : [undefined];
|
|
43
|
+
return directFallback ? [stagedFallback, directFallback, undefined] : [stagedFallback];
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export {
|
|
47
|
+
semanticFallbackCandidates,
|
|
37
48
|
semanticFallbackChangedExistingDeclarations,
|
|
38
49
|
semanticFallbackConflictCode,
|
|
39
50
|
semanticFallbackPhase,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
1
|
import { createSemanticEditScript } from './internal/index-impl/semanticEditScripts.js';
|
|
3
2
|
import { projectSemanticEditScriptToSource } from './internal/index-impl/projectSemanticEditScriptToSource.js';
|
|
4
3
|
import { replaySemanticEditProjection } from './internal/index-impl/replaySemanticEditProjection.js';
|
|
5
4
|
import { JsTsSafeMergeStatuses } from './js-ts-safe-merge-constants.js';
|
|
6
5
|
import { independentTopLevelDeletionFallbackResult } from './js-ts-safe-merge-independent-deletion-fallback.js';
|
|
7
6
|
import { normalizeAlreadyAppliedDeleteReplay } from './js-ts-safe-merge-semantic-edit-already-applied.js';
|
|
7
|
+
import { blockedSemanticEditArtifacts, semanticEditArtifacts } from './js-ts-safe-merge-semantic-edit-artifacts.js';
|
|
8
8
|
import {
|
|
9
|
+
semanticFallbackCandidates,
|
|
9
10
|
semanticFallbackChangedExistingDeclarations,
|
|
10
11
|
semanticFallbackConflictCode,
|
|
11
12
|
semanticFallbackPhase,
|
|
@@ -19,11 +20,31 @@ import {
|
|
|
19
20
|
} from './js-ts-safe-merge-staged-declaration-replay.js';
|
|
20
21
|
import { createStagedTopLevelSemanticFallback } from './js-ts-safe-merge-staged-top-level-fallback.js';
|
|
21
22
|
import { createSourceShapeSemanticFallbackResult } from './js-ts-safe-merge-source-shape-fallbacks.js';
|
|
22
|
-
import {
|
|
23
|
+
import { analyzeTopLevelRenameAdmission } from './js-ts-safe-merge-top-level-rename-fallback.js';
|
|
24
|
+
import { topLevelRenameBlockedResult } from './js-ts-safe-merge-top-level-rename-result.js';
|
|
23
25
|
|
|
24
26
|
function semanticEditFallbackResult(input, topLevelResult) {
|
|
25
27
|
const independentDeletionResult = independentTopLevelDeletionFallbackResult(input, topLevelResult);
|
|
26
28
|
if (independentDeletionResult) return independentDeletionResult;
|
|
29
|
+
const topLevelRenameAdmission = analyzeTopLevelRenameAdmission(input, topLevelResult);
|
|
30
|
+
if (topLevelRenameAdmission?.status === 'blocked') {
|
|
31
|
+
if (shouldDeferTopLevelRenamePublicContract(input, topLevelRenameAdmission)) {
|
|
32
|
+
const deferredAdmission = deferredTopLevelRenameAdmission(topLevelRenameAdmission);
|
|
33
|
+
const artifacts = createSemanticEditFallbackArtifacts(input, topLevelResult);
|
|
34
|
+
if (artifacts.status !== 'verified') {
|
|
35
|
+
return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts, deferredAdmission);
|
|
36
|
+
}
|
|
37
|
+
return semanticEditFallbackMergedResult(input, topLevelResult, undefined, artifacts, deferredAdmission);
|
|
38
|
+
}
|
|
39
|
+
return topLevelRenameBlockedResult(input, topLevelResult, topLevelRenameAdmission);
|
|
40
|
+
}
|
|
41
|
+
if (topLevelRenameAdmission?.status === 'candidate') {
|
|
42
|
+
const artifacts = createSemanticEditFallbackArtifacts(input, topLevelResult);
|
|
43
|
+
if (artifacts.status !== 'verified') {
|
|
44
|
+
return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts, topLevelRenameAdmission);
|
|
45
|
+
}
|
|
46
|
+
return semanticEditFallbackMergedResult(input, topLevelResult, undefined, artifacts, topLevelRenameAdmission);
|
|
47
|
+
}
|
|
27
48
|
if (!shouldTrySemanticEditFallback(topLevelResult)) return topLevelResult;
|
|
28
49
|
const stagedFallback = createStagedTopLevelSemanticFallback(input, topLevelResult);
|
|
29
50
|
const candidates = semanticFallbackCandidates(stagedFallback);
|
|
@@ -40,6 +61,10 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
40
61
|
if (sourceShapeResult) return sourceShapeResult;
|
|
41
62
|
return semanticEditFallbackBlockedResult(input, topLevelResult, artifacts);
|
|
42
63
|
}
|
|
64
|
+
return semanticEditFallbackMergedResult(input, topLevelResult, selectedFallback, artifacts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function semanticEditFallbackMergedResult(input, topLevelResult, selectedFallback, artifacts, topLevelRenameAdmission) {
|
|
43
68
|
const resultBase = selectedFallback?.stagedTopLevelResult ?? topLevelResult;
|
|
44
69
|
const mergedSourceText = artifacts.projection.sourceText;
|
|
45
70
|
const gates = semanticEditGates(artifacts);
|
|
@@ -68,32 +93,26 @@ function semanticEditFallbackResult(input, topLevelResult) {
|
|
|
68
93
|
semanticEditOperations: artifacts.script.summary.operations,
|
|
69
94
|
semanticEditAppliedOperations: artifacts.replay.summary.applied,
|
|
70
95
|
semanticEditReplayStatus: artifacts.replay.status,
|
|
96
|
+
topLevelDeclarationRenames: topLevelRenameAdmission ? 1 : resultBase.summary?.topLevelDeclarationRenames,
|
|
71
97
|
composedPhases: 2
|
|
72
98
|
},
|
|
73
99
|
metadata: {
|
|
74
100
|
...resultBase.metadata,
|
|
75
101
|
composed: {
|
|
76
|
-
phase: semanticFallbackPhase(selectedFallback),
|
|
77
|
-
phases:
|
|
102
|
+
phase: topLevelRenameAdmission ? 'top-level-rename-semantic-edit-fallback' : semanticFallbackPhase(selectedFallback),
|
|
103
|
+
phases: topLevelRenameAdmission
|
|
104
|
+
? ['top-level-rename-admission', 'semantic-edit']
|
|
105
|
+
: selectedFallback ? ['top-level-neutralization', 'top-level-ledger', 'semantic-edit'] : ['top-level-ledger', 'semantic-edit'],
|
|
78
106
|
originalReasonCodes: topLevelResult.admission?.reasonCodes ?? [],
|
|
79
107
|
stagedTopLevelSummary: selectedFallback?.stagedTopLevelResult?.summary,
|
|
80
|
-
neutralization: selectedFallback?.neutralization?.summary
|
|
108
|
+
neutralization: selectedFallback?.neutralization?.summary,
|
|
109
|
+
topLevelRenameAdmission: topLevelRenameAdmission?.summary
|
|
81
110
|
}
|
|
82
111
|
},
|
|
83
112
|
semanticArtifacts: artifacts
|
|
84
113
|
};
|
|
85
114
|
}
|
|
86
115
|
|
|
87
|
-
function semanticFallbackCandidates(stagedFallback) {
|
|
88
|
-
if (!stagedFallback) return [undefined];
|
|
89
|
-
const headChanged = (stagedFallback.neutralization?.summary?.headChangedExistingDeclarations ?? 0) > 0;
|
|
90
|
-
const directFallback = stagedFallback.directProjectionHeadSourceText && (headChanged || stagedFallback.safeTopLevelChanges > 0)
|
|
91
|
-
? { ...stagedFallback, projectionMode: 'direct' }
|
|
92
|
-
: undefined;
|
|
93
|
-
if (headChanged) return directFallback ? [directFallback, undefined] : [undefined];
|
|
94
|
-
return directFallback ? [stagedFallback, directFallback, undefined] : [stagedFallback];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
116
|
function createSemanticEditFallbackArtifacts(input, topLevelResult, stagedFallback) {
|
|
98
117
|
try {
|
|
99
118
|
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
@@ -166,112 +185,6 @@ function createSemanticEditFallbackArtifacts(input, topLevelResult, stagedFallba
|
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
|
|
169
|
-
function semanticEditArtifacts(input) {
|
|
170
|
-
const reasonCodes = semanticEditArtifactReasonCodes(input);
|
|
171
|
-
const status = reasonCodes.length ? 'blocked' : 'verified';
|
|
172
|
-
const core = {
|
|
173
|
-
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
174
|
-
version: 1,
|
|
175
|
-
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
176
|
-
id: `js_ts_safe_merge_semantic_edit_artifacts_${idFragment(input.id)}`,
|
|
177
|
-
sourcePath: input.sourcePath,
|
|
178
|
-
language: input.language,
|
|
179
|
-
status,
|
|
180
|
-
script: input.script,
|
|
181
|
-
projection: input.projection,
|
|
182
|
-
replay: input.replay,
|
|
183
|
-
alreadyAppliedReplay: input.alreadyAppliedReplay,
|
|
184
|
-
admission: {
|
|
185
|
-
status: status === 'verified' ? 'auto-merge-candidate' : 'blocked',
|
|
186
|
-
action: status === 'verified' ? 'apply' : 'human-review',
|
|
187
|
-
reviewRequired: status !== 'verified',
|
|
188
|
-
autoApplyCandidate: status === 'verified',
|
|
189
|
-
autoMergeClaim: false,
|
|
190
|
-
semanticEquivalenceClaim: false,
|
|
191
|
-
reasonCodes
|
|
192
|
-
},
|
|
193
|
-
summary: {
|
|
194
|
-
operations: input.script.summary.operations,
|
|
195
|
-
edits: input.projection.edits.length,
|
|
196
|
-
replayStatus: input.replay.status,
|
|
197
|
-
alreadyAppliedReplayStatus: input.alreadyAppliedReplay.status,
|
|
198
|
-
projectedSourceMatchesMerged: input.projection.sourceText === input.replay.outputSourceText,
|
|
199
|
-
replayOutputMatchesMerged: input.replay.outputSourceText === input.projection.sourceText
|
|
200
|
-
},
|
|
201
|
-
evidence: [{
|
|
202
|
-
id: `evidence_${idFragment(input.id)}_js_ts_semantic_edit_replay`,
|
|
203
|
-
kind: 'js-ts-semantic-edit-replay',
|
|
204
|
-
status: status === 'verified' ? 'passed' : 'needs-review',
|
|
205
|
-
path: input.sourcePath,
|
|
206
|
-
summary: status === 'verified'
|
|
207
|
-
? `JS/TS semantic edit replay verified ${input.script.summary.operations} operation(s).`
|
|
208
|
-
: `JS/TS semantic edit replay requires review: ${reasonCodes.join(', ')}.`
|
|
209
|
-
}],
|
|
210
|
-
metadata: {
|
|
211
|
-
autoMergeClaim: false,
|
|
212
|
-
semanticEquivalenceClaim: false,
|
|
213
|
-
source: input.stagedFallback ? semanticFallbackPhase(input.stagedFallback) : 'js-ts-semantic-edit-fallback',
|
|
214
|
-
originalReasonCodes: input.topLevelResult.admission?.reasonCodes ?? [],
|
|
215
|
-
stagedTopLevelSummary: input.stagedFallback?.stagedTopLevelResult?.summary,
|
|
216
|
-
neutralization: input.stagedFallback?.neutralization?.summary
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
return { ...core, hash: hashSemanticValue(core) };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function semanticEditArtifactReasonCodes(input) {
|
|
223
|
-
const scriptReady = input.script.admission.status === 'auto-merge-candidate';
|
|
224
|
-
const projectionReady = input.projection.status === 'projected';
|
|
225
|
-
const replayReady = input.replay.status === 'accepted-clean';
|
|
226
|
-
const alreadyAppliedReady = input.alreadyAppliedReplay.status === 'already-applied';
|
|
227
|
-
return uniqueStrings([
|
|
228
|
-
scriptReady ? undefined : `semantic-edit-script-${input.script.admission.status}`,
|
|
229
|
-
projectionReady ? undefined : `semantic-edit-projection-${input.projection.status}`,
|
|
230
|
-
replayReady ? undefined : `semantic-edit-replay-${input.replay.status}`,
|
|
231
|
-
input.replay.outputSourceText !== input.projection.sourceText ? 'semantic-edit-replay-output-mismatch' : undefined,
|
|
232
|
-
alreadyAppliedReady ? undefined : `semantic-edit-already-applied-${input.alreadyAppliedReplay.status}`,
|
|
233
|
-
...(scriptReady ? [] : input.script.admission.reasonCodes),
|
|
234
|
-
...(projectionReady ? [] : input.projection.admission.reasonCodes),
|
|
235
|
-
...(replayReady ? [] : input.replay.admission.reasonCodes),
|
|
236
|
-
...(alreadyAppliedReady ? [] : input.alreadyAppliedReplay.admission.reasonCodes)
|
|
237
|
-
]);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function blockedSemanticEditArtifacts(input, topLevelResult, reasonCodes, error) {
|
|
241
|
-
const id = String(input.id ?? topLevelResult.id ?? 'js_ts_safe_merge');
|
|
242
|
-
const core = {
|
|
243
|
-
kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts',
|
|
244
|
-
version: 1,
|
|
245
|
-
schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1',
|
|
246
|
-
id: `js_ts_safe_merge_semantic_edit_artifacts_${idFragment(id)}`,
|
|
247
|
-
sourcePath: input.sourcePath ?? topLevelResult.sourcePath,
|
|
248
|
-
language: input.language ?? topLevelResult.language ?? 'typescript',
|
|
249
|
-
status: 'blocked',
|
|
250
|
-
admission: {
|
|
251
|
-
status: 'blocked',
|
|
252
|
-
action: 'human-review',
|
|
253
|
-
reviewRequired: true,
|
|
254
|
-
autoApplyCandidate: false,
|
|
255
|
-
autoMergeClaim: false,
|
|
256
|
-
semanticEquivalenceClaim: false,
|
|
257
|
-
reasonCodes
|
|
258
|
-
},
|
|
259
|
-
summary: {
|
|
260
|
-
operations: 0,
|
|
261
|
-
edits: 0,
|
|
262
|
-
replayStatus: 'blocked',
|
|
263
|
-
alreadyAppliedReplayStatus: 'blocked',
|
|
264
|
-
projectedSourceMatchesMerged: false,
|
|
265
|
-
replayOutputMatchesMerged: false
|
|
266
|
-
},
|
|
267
|
-
metadata: {
|
|
268
|
-
source: 'js-ts-semantic-edit-fallback',
|
|
269
|
-
error: error?.message
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
return { ...core, hash: hashSemanticValue(core) };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
188
|
function semanticEditFallbackBlockedResult(input, topLevelResult, artifacts) {
|
|
276
189
|
const reasonCodes = artifacts.admission.reasonCodes.length
|
|
277
190
|
? artifacts.admission.reasonCodes
|
|
@@ -310,4 +223,37 @@ function semanticEditFallbackBlockedResult(input, topLevelResult, artifacts) {
|
|
|
310
223
|
};
|
|
311
224
|
}
|
|
312
225
|
|
|
226
|
+
function shouldDeferTopLevelRenamePublicContract(input, admission) {
|
|
227
|
+
return input.deferTopLevelRenamePublicExportContractToProjectGraph === true
|
|
228
|
+
&& admission.reasonCodes?.length === 1
|
|
229
|
+
&& admission.reasonCodes.includes('top-level-rename-public-export-contract')
|
|
230
|
+
&& admission.summary?.exported !== true
|
|
231
|
+
&& workerPreservesRenamedExportAlias(input.workerSourceText, admission.summary);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function deferredTopLevelRenameAdmission(admission) {
|
|
235
|
+
return {
|
|
236
|
+
...admission,
|
|
237
|
+
status: 'candidate',
|
|
238
|
+
reasonCodes: ['top-level-rename-public-export-contract-deferred-to-project-graph'],
|
|
239
|
+
summary: {
|
|
240
|
+
...admission.summary,
|
|
241
|
+
deferredToProjectGraph: true,
|
|
242
|
+
reasonCodes: ['top-level-rename-public-export-contract-deferred-to-project-graph']
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function workerPreservesRenamedExportAlias(sourceText, summary) {
|
|
248
|
+
const fromName = summary?.fromName;
|
|
249
|
+
const toName = summary?.toName;
|
|
250
|
+
if (typeof sourceText !== 'string' || !fromName || !toName) return false;
|
|
251
|
+
const exportListPattern = new RegExp(`export\\s*\\{[^}]*\\b${escapeRegExp(toName)}\\s+as\\s+${escapeRegExp(fromName)}\\b[^}]*\\}`);
|
|
252
|
+
return exportListPattern.test(sourceText);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function escapeRegExp(value) {
|
|
256
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
+
}
|
|
258
|
+
|
|
313
259
|
export { semanticEditFallbackResult };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createEnumMemberSemanticFallbackResult } from './js-ts-safe-merge-enum-member-fallback.js';
|
|
2
|
+
import { createJsxAttributeSemanticFallbackResult } from './js-ts-safe-merge-jsx-attribute-fallback.js';
|
|
2
3
|
import { createVariableDeclaratorSemanticFallbackResult } from './js-ts-safe-merge-variable-declarator-fallback.js';
|
|
3
4
|
|
|
4
5
|
function createSourceShapeSemanticFallbackResult(input, topLevelResult, stagedFallback) {
|
|
5
6
|
return createVariableDeclaratorSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
6
|
-
?? createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
7
|
+
?? createEnumMemberSemanticFallbackResult(input, topLevelResult, stagedFallback)
|
|
8
|
+
?? createJsxAttributeSemanticFallbackResult(input, topLevelResult, stagedFallback);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export { createSourceShapeSemanticFallbackResult };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { JsTsSafeMergeConflictCodes } from './js-ts-safe-merge-constants.js';
|
|
2
|
+
import { createMergeContext, sameStatementText } from './js-ts-safe-merge-context.js';
|
|
3
|
+
import { scanJsTsTopLevelLedger, validateLedgerUniqueness } from './js-ts-safe-merge-ledger.js';
|
|
4
|
+
import { uniqueStrings } from './native-import-utils.js';
|
|
5
|
+
|
|
6
|
+
const supportedRenameDeclarationKinds = new Set(['function', 'class', 'type']);
|
|
7
|
+
|
|
8
|
+
function analyzeTopLevelRenameAdmission(input, topLevelResult) {
|
|
9
|
+
const originalReasonCodes = topLevelResult?.admission?.reasonCodes ?? [];
|
|
10
|
+
if (!originalReasonCodes.includes(JsTsSafeMergeConflictCodes.topLevelOrderChanged)) return undefined;
|
|
11
|
+
if (typeof input.baseSourceText !== 'string'
|
|
12
|
+
|| typeof input.workerSourceText !== 'string'
|
|
13
|
+
|| typeof input.headSourceText !== 'string') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const context = createMergeContext(input);
|
|
18
|
+
const base = scanJsTsTopLevelLedger(input.baseSourceText, 'base', context);
|
|
19
|
+
const worker = scanJsTsTopLevelLedger(input.workerSourceText, 'worker', context);
|
|
20
|
+
const head = scanJsTsTopLevelLedger(input.headSourceText, 'head', context);
|
|
21
|
+
if (!context.conflicts.length) {
|
|
22
|
+
validateLedgerUniqueness(base, context);
|
|
23
|
+
validateLedgerUniqueness(worker, context);
|
|
24
|
+
validateLedgerUniqueness(head, context);
|
|
25
|
+
}
|
|
26
|
+
if (context.conflicts.length) return undefined;
|
|
27
|
+
|
|
28
|
+
const candidate = topLevelRenameCandidate(base, worker, head);
|
|
29
|
+
if (!candidate) return undefined;
|
|
30
|
+
|
|
31
|
+
const publicContractReasonCodes = publicContractRenameReasonCodes(base, worker, head, candidate);
|
|
32
|
+
if (publicContractReasonCodes.length) {
|
|
33
|
+
return {
|
|
34
|
+
status: 'blocked',
|
|
35
|
+
reasonCodes: publicContractReasonCodes,
|
|
36
|
+
summary: candidateSummary(candidate, publicContractReasonCodes),
|
|
37
|
+
ledgers: { base, worker, head }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
status: 'candidate',
|
|
43
|
+
reasonCodes: ['top-level-rename-source-shape-matches'],
|
|
44
|
+
summary: candidateSummary(candidate, ['top-level-rename-source-shape-matches']),
|
|
45
|
+
ledgers: { base, worker, head }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function topLevelRenameCandidate(base, worker, head) {
|
|
50
|
+
const baseByKey = entriesByKey(base.entries);
|
|
51
|
+
const workerByKey = entriesByKey(worker.entries);
|
|
52
|
+
const baseKeys = base.entries.map((entry) => entry.key);
|
|
53
|
+
const missingBaseDeclarations = base.entries
|
|
54
|
+
.filter((entry) => !workerByKey.has(entry.key))
|
|
55
|
+
.filter(isSupportedRenameDeclaration);
|
|
56
|
+
const addedWorkerDeclarations = worker.entries
|
|
57
|
+
.filter((entry) => !baseByKey.has(entry.key))
|
|
58
|
+
.filter(isSupportedRenameDeclaration);
|
|
59
|
+
|
|
60
|
+
if (missingBaseDeclarations.length !== 1 || addedWorkerDeclarations.length !== 1) return undefined;
|
|
61
|
+
const fromEntry = missingBaseDeclarations[0];
|
|
62
|
+
const toEntry = addedWorkerDeclarations[0];
|
|
63
|
+
const fromName = fromEntry.names?.[0];
|
|
64
|
+
const toName = toEntry.names?.[0];
|
|
65
|
+
if (!fromName || !toName || fromName === toName) return undefined;
|
|
66
|
+
if (fromEntry.declarationInfo?.declarationKind !== toEntry.declarationInfo?.declarationKind) return undefined;
|
|
67
|
+
if (!sameStatementText(renameDeclarationText(fromEntry.text, fromEntry.declarationInfo.declarationKind, fromName, toName), toEntry.text)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workerProjectedBaseKeys = worker.entries.map((entry) => entry.key === toEntry.key ? fromEntry.key : entry.key);
|
|
72
|
+
if (!sameStringList(workerProjectedBaseKeys, baseKeys)) return undefined;
|
|
73
|
+
const headProjectedBaseKeys = head.entries
|
|
74
|
+
.filter((entry) => baseByKey.has(entry.key))
|
|
75
|
+
.map((entry) => entry.key);
|
|
76
|
+
if (!sameStringList(headProjectedBaseKeys, baseKeys)) return undefined;
|
|
77
|
+
if (head.entries.length !== base.entries.length) return undefined;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
fromEntry,
|
|
81
|
+
toEntry,
|
|
82
|
+
fromName,
|
|
83
|
+
toName,
|
|
84
|
+
declarationKind: fromEntry.declarationInfo.declarationKind
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isSupportedRenameDeclaration(entry) {
|
|
89
|
+
return entry?.kind === 'declaration'
|
|
90
|
+
&& entry.names?.length === 1
|
|
91
|
+
&& supportedRenameDeclarationKinds.has(entry.declarationInfo?.declarationKind)
|
|
92
|
+
&& !entry.declarationInfo.defaultExport
|
|
93
|
+
&& !/^\s*(?:export\s+)?declare\b/.test(entry.text ?? '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function publicContractRenameReasonCodes(base, worker, head, candidate) {
|
|
97
|
+
const directExport = candidate.fromEntry.declarationInfo?.exported === true
|
|
98
|
+
|| candidate.toEntry.declarationInfo?.exported === true;
|
|
99
|
+
const exportListMention = [base, worker, head]
|
|
100
|
+
.some((ledger) => ledger.entries
|
|
101
|
+
.some((entry) => entry.kind === 'export' && mentionsName(entry.text, candidate.fromName, candidate.toName)));
|
|
102
|
+
return directExport || exportListMention
|
|
103
|
+
? [JsTsSafeMergeConflictCodes.topLevelRenamePublicExportContract]
|
|
104
|
+
: [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mentionsName(text, ...names) {
|
|
108
|
+
return names.some((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text ?? ''));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renameDeclarationText(text, declarationKind, fromName, toName) {
|
|
112
|
+
if (typeof text !== 'string') return undefined;
|
|
113
|
+
const escaped = escapeRegExp(fromName);
|
|
114
|
+
const replacement = `$1${toName}`;
|
|
115
|
+
if (declarationKind === 'function') {
|
|
116
|
+
return text.replace(new RegExp(`^((?:export\\s+)?(?:async\\s+)?function\\*?\\s+)${escaped}\\b`), replacement);
|
|
117
|
+
}
|
|
118
|
+
if (declarationKind === 'class') {
|
|
119
|
+
return text.replace(new RegExp(`^((?:export\\s+)?(?:abstract\\s+)?class\\s+)${escaped}\\b`), replacement);
|
|
120
|
+
}
|
|
121
|
+
if (declarationKind === 'type') {
|
|
122
|
+
return text.replace(new RegExp(`^((?:export\\s+)?type\\s+)${escaped}\\b`), replacement);
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function candidateSummary(candidate, reasonCodes) {
|
|
128
|
+
return {
|
|
129
|
+
fromName: candidate.fromName,
|
|
130
|
+
toName: candidate.toName,
|
|
131
|
+
declarationKind: candidate.declarationKind,
|
|
132
|
+
exported: candidate.fromEntry.declarationInfo?.exported === true || candidate.toEntry.declarationInfo?.exported === true,
|
|
133
|
+
reasonCodes: uniqueStrings(reasonCodes)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function entriesByKey(entries) {
|
|
138
|
+
return new Map(entries.map((entry) => [entry.key, entry]));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sameStringList(left, right) {
|
|
142
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function escapeRegExp(value) {
|
|
146
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { analyzeTopLevelRenameAdmission };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JsTsSafeMergeConflictCodes, JsTsSafeMergeGateIds } from './js-ts-safe-merge-constants.js';
|
|
2
|
+
import { uniqueStrings } from './native-import-utils.js';
|
|
3
|
+
|
|
4
|
+
function topLevelRenameBlockedResult(input, topLevelResult, topLevelRenameAdmission) {
|
|
5
|
+
const reasonCodes = uniqueStrings([
|
|
6
|
+
...(topLevelResult.admission?.reasonCodes ?? []),
|
|
7
|
+
...(topLevelRenameAdmission.reasonCodes ?? [])
|
|
8
|
+
]);
|
|
9
|
+
const conflict = {
|
|
10
|
+
code: JsTsSafeMergeConflictCodes.topLevelRenamePublicExportContract,
|
|
11
|
+
gateId: JsTsSafeMergeGateIds.stableExistingDeclarations,
|
|
12
|
+
message: 'Top-level rename changes a public export contract without project-level evidence.',
|
|
13
|
+
side: 'worker',
|
|
14
|
+
sourcePath: input.sourcePath ?? topLevelResult.sourcePath,
|
|
15
|
+
details: {
|
|
16
|
+
...topLevelRenameAdmission.summary,
|
|
17
|
+
reasonCodes: topLevelRenameAdmission.reasonCodes
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
...topLevelResult,
|
|
22
|
+
conflicts: [...(topLevelResult.conflicts ?? []), conflict],
|
|
23
|
+
admission: {
|
|
24
|
+
status: 'blocked',
|
|
25
|
+
action: 'human-review',
|
|
26
|
+
reviewRequired: true,
|
|
27
|
+
autoApplyCandidate: false,
|
|
28
|
+
autoMergeClaim: false,
|
|
29
|
+
semanticEquivalenceClaim: false,
|
|
30
|
+
reasonCodes
|
|
31
|
+
},
|
|
32
|
+
summary: {
|
|
33
|
+
...topLevelResult.summary,
|
|
34
|
+
conflicts: (topLevelResult.conflicts?.length ?? 0) + 1,
|
|
35
|
+
topLevelDeclarationRenames: 1
|
|
36
|
+
},
|
|
37
|
+
metadata: {
|
|
38
|
+
...topLevelResult.metadata,
|
|
39
|
+
topLevelRenameAdmission: topLevelRenameAdmission.summary
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { topLevelRenameBlockedResult };
|
|
@@ -6,6 +6,7 @@ function outputProjectGraphConflicts(projectSymbolGraph) {
|
|
|
6
6
|
const limitConflicts = Array.isArray(projectSymbolGraph?.limitConflicts) ? projectSymbolGraph.limitConflicts : [];
|
|
7
7
|
projectSymbolGraph = projectSymbolGraph?.projectSymbolGraph ?? projectSymbolGraph;
|
|
8
8
|
const importEdges = Array.isArray(projectSymbolGraph?.importEdges) ? projectSymbolGraph.importEdges : [];
|
|
9
|
+
const exportEdges = Array.isArray(projectSymbolGraph?.exportEdges) ? projectSymbolGraph.exportEdges : [];
|
|
9
10
|
const missingModuleGroups = new Map();
|
|
10
11
|
const missingSymbolGroups = new Map();
|
|
11
12
|
for (const edge of importEdges) {
|
|
@@ -16,7 +17,7 @@ function outputProjectGraphConflicts(projectSymbolGraph) {
|
|
|
16
17
|
missingModuleGroups.set(key, group);
|
|
17
18
|
continue;
|
|
18
19
|
}
|
|
19
|
-
if (isMissingProjectImportTargetEdge(edge)) {
|
|
20
|
+
if (isMissingProjectImportTargetEdge(edge, exportEdges)) {
|
|
20
21
|
const key = [edge.sourcePath, edge.moduleSpecifier, projectImportTargetName(edge), edge.resolvedModulePath].join('\u0000');
|
|
21
22
|
const group = missingSymbolGroups.get(key) ?? [];
|
|
22
23
|
group.push(edge);
|
|
@@ -81,8 +82,11 @@ function isMissingProjectImportEdge(edge) {
|
|
|
81
82
|
return typeof edge?.resolutionKind === 'string' && edge.resolutionKind.endsWith('-missing');
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function isMissingProjectImportTargetEdge(edge) {
|
|
85
|
-
return hasResolvedProjectModule(edge)
|
|
85
|
+
function isMissingProjectImportTargetEdge(edge, exportEdges = []) {
|
|
86
|
+
return hasResolvedProjectModule(edge)
|
|
87
|
+
&& Boolean(projectImportTargetName(edge))
|
|
88
|
+
&& !edge.resolvedTargetSymbolId
|
|
89
|
+
&& !commonJsRequireResolvedByExportAssignment(edge, exportEdges);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
function hasResolvedProjectModule(edge) {
|
|
@@ -96,6 +100,23 @@ function projectImportTargetName(edge) {
|
|
|
96
100
|
return String(name);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
function commonJsRequireResolvedByExportAssignment(edge, exportEdges) {
|
|
104
|
+
if (edge?.importKind !== 'commonjs-require' || projectImportTargetName(edge) !== 'default') return false;
|
|
105
|
+
return exportEdges.some((exportEdge) => exportEdge?.exportKind === 'assignment'
|
|
106
|
+
&& exportEdge.exportedName === 'module.exports'
|
|
107
|
+
&& sameProjectDocument(edge, exportEdge));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sameProjectDocument(importEdge, exportEdge) {
|
|
111
|
+
if (importEdge?.targetDocumentId && exportEdge?.sourceDocumentId) {
|
|
112
|
+
return importEdge.targetDocumentId === exportEdge.sourceDocumentId;
|
|
113
|
+
}
|
|
114
|
+
if (importEdge?.resolvedModulePath && exportEdge?.sourcePath) {
|
|
115
|
+
return importEdge.resolvedModulePath === exportEdge.sourcePath;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
function duplicateReExportIdentityConflicts(records = []) {
|
|
100
121
|
const groups = new Map();
|
|
101
122
|
for (const record of records) {
|
|
@@ -112,6 +112,7 @@ function mergeProjectFile(file, input, projectId) {
|
|
|
112
112
|
...input,
|
|
113
113
|
...context,
|
|
114
114
|
deferReExportIdentityConflictsToProjectGraph: input.includeProjectGraphDelta === true || input.includeOutputProjectSymbolGraph === true,
|
|
115
|
+
deferTopLevelRenamePublicExportContractToProjectGraph: input.includeProjectGraphDelta === true || input.includeOutputProjectSymbolGraph === true,
|
|
115
116
|
id: `${projectId}_${safeId(file.sourcePath)}`,
|
|
116
117
|
baseSourceText: base,
|
|
117
118
|
workerSourceText: worker,
|
|
@@ -304,7 +305,6 @@ function blockedAdmission(reasonCode) {
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
function hashText(text) { return typeof text === 'string' ? hashSemanticValue(text) : undefined; }
|
|
307
|
-
|
|
308
308
|
function stringOrUndefined(value) { return typeof value === 'string' ? value : undefined; }
|
|
309
309
|
|
|
310
310
|
function safeId(value) {
|
package/package.json
CHANGED