@shapeshift-labs/frontier-lang-compiler 0.2.149 → 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 };
@@ -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))
@@ -28,6 +28,14 @@ function semanticEditFallbackResult(input, topLevelResult) {
28
28
  if (independentDeletionResult) return independentDeletionResult;
29
29
  const topLevelRenameAdmission = analyzeTopLevelRenameAdmission(input, topLevelResult);
30
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
+ }
31
39
  return topLevelRenameBlockedResult(input, topLevelResult, topLevelRenameAdmission);
32
40
  }
33
41
  if (topLevelRenameAdmission?.status === 'candidate') {
@@ -215,4 +223,37 @@ function semanticEditFallbackBlockedResult(input, topLevelResult, artifacts) {
215
223
  };
216
224
  }
217
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
+
218
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 };
@@ -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.149",
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",