@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.
@@ -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(importSpecifierCanonical).join(', ')} }`);
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 { idFragment, uniqueStrings } from './native-import-utils.js';
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: selectedFallback ? ['top-level-neutralization', 'top-level-ledger', 'semantic-edit'] : ['top-level-ledger', 'semantic-edit'],
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) && Boolean(projectImportTargetName(edge)) && !edge.resolvedTargetSymbolId;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.148",
3
+ "version": "0.2.150",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",