@shapeshift-labs/frontier-lang-compiler 0.2.133 → 0.2.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,7 +194,12 @@ generic semantic edit script path. The fallback admits the merge only after the
194
194
  script is an auto-merge candidate, the source projection succeeds, replay on
195
195
  current head is `accepted-clean`, and replay on the projected source is
196
196
  `already-applied`. Same-anchor head edits, stale anchors, and non-body conflicts
197
- remain blocked for review.
197
+ remain blocked for review. The same fallback composes with declared unordered
198
+ member-addition regions, so a verified body edit can still merge alongside safe
199
+ interface, type, class, or object member additions. Existing class/object method
200
+ or property body edits inside the declared member region are preserved for
201
+ semantic replay while added members are neutralized; object member additions are
202
+ re-emitted with safe commas when both sides add final properties.
198
203
 
199
204
  Project-level JS/TS safe merges compose the same file-level gates across a
200
205
  base/worker/head file set. They preserve head-only files, admit worker-only
@@ -6,6 +6,7 @@ import {
6
6
  normalizeKind,
7
7
  normalizeMemberText,
8
8
  parseMembers,
9
+ removePreparedMemberAdditions,
9
10
  uniqueStrings
10
11
  } from './js-ts-semantic-merge-parse.js';
11
12
  import { mergeResult } from './js-ts-safe-member-merge-result.js';
@@ -43,7 +44,8 @@ function analyzeJsTsSafeMemberAdditions(input = {}) {
43
44
  function neutralizeJsTsSafeMemberMergeSources(input = {}) {
44
45
  const analysis = analyzeJsTsSafeMemberAdditions({
45
46
  ...input,
46
- allowNonPolicySourceChanges: true
47
+ allowNonPolicySourceChanges: true,
48
+ allowExistingMemberChanges: true
47
49
  });
48
50
  if (analysis.reasonCodes.length) {
49
51
  return {
@@ -56,8 +58,8 @@ function neutralizeJsTsSafeMemberMergeSources(input = {}) {
56
58
  ok: true,
57
59
  analysis,
58
60
  baseSourceText: analysis.baseSourceText,
59
- workerSourceText: canonicalizeSourceBodies(analysis.workerSourceText, analysis.preparedRegions, 'worker'),
60
- headSourceText: canonicalizeSourceBodies(analysis.headSourceText, analysis.preparedRegions, 'head')
61
+ workerSourceText: removePreparedMemberAdditions(analysis.workerSourceText, analysis.preparedRegions, 'worker'),
62
+ headSourceText: removePreparedMemberAdditions(analysis.headSourceText, analysis.preparedRegions, 'head')
61
63
  };
62
64
  }
63
65
 
@@ -81,7 +83,7 @@ function prepareJsTsSafeMemberAdditions(input = {}) {
81
83
  reasonCodes.push(...policyReasons);
82
84
  if (policyReasons.length) continue;
83
85
  if (typeof baseSourceText !== 'string' || typeof workerSourceText !== 'string' || typeof headSourceText !== 'string') continue;
84
- const prepared = prepareRegion({ region, baseSourceText, workerSourceText, headSourceText });
86
+ const prepared = prepareRegion({ region, baseSourceText, workerSourceText, headSourceText, allowExistingMemberChanges: input.allowExistingMemberChanges });
85
87
  reasonCodes.push(...prepared.reasonCodes);
86
88
  if (prepared.ok) preparedRegions.push(prepared.value);
87
89
  }
@@ -163,8 +165,8 @@ function prepareRegion(input) {
163
165
  const baseByKey = membersByKey(baseMembers.members);
164
166
  const workerByKey = membersByKey(workerMembers.members);
165
167
  const headByKey = membersByKey(headMembers.members);
166
- reasonCodes.push(...existingMemberReasons(input.region, 'worker', baseMembers.members, workerMembers.members, workerByKey));
167
- reasonCodes.push(...existingMemberReasons(input.region, 'head', baseMembers.members, headMembers.members, headByKey));
168
+ reasonCodes.push(...existingMemberReasons(input.region, 'worker', baseMembers.members, workerMembers.members, workerByKey, input.allowExistingMemberChanges));
169
+ reasonCodes.push(...existingMemberReasons(input.region, 'head', baseMembers.members, headMembers.members, headByKey, input.allowExistingMemberChanges));
168
170
  const workerAddedKeys = workerMembers.members.map((member) => member.key).filter((key) => !baseByKey.has(key));
169
171
  const headAddedKeys = headMembers.members.map((member) => member.key).filter((key) => !baseByKey.has(key));
170
172
  reasonCodes.push(...duplicateAddedReasons(input.region, workerAddedKeys, workerByKey, headByKey));
@@ -212,7 +214,7 @@ function duplicateReasons(region, side, members) {
212
214
  });
213
215
  }
214
216
 
215
- function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey) {
217
+ function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey, allowExistingMemberChanges) {
216
218
  const reasonCodes = [];
217
219
  const sideBaseOrder = sideMembers.map((member) => member.key).filter((key) => baseMembers.some((baseMember) => baseMember.key === key));
218
220
  const baseOrder = baseMembers.map((member) => member.key);
@@ -225,7 +227,7 @@ function existingMemberReasons(region, side, baseMembers, sideMembers, sideByKey
225
227
  reasonCodes.push(regionReason(region, `existing-member-removed:${side}:${baseMember.key}`));
226
228
  continue;
227
229
  }
228
- if (normalizeMemberText(sideMember.text) !== normalizeMemberText(baseMember.text)) {
230
+ if (!allowExistingMemberChanges && normalizeMemberText(sideMember.text, normalizeKind(region.kind)) !== normalizeMemberText(baseMember.text, normalizeKind(region.kind))) {
229
231
  reasonCodes.push(regionReason(region, `existing-member-changed:${side}:${baseMember.key}`));
230
232
  }
231
233
  }
@@ -25,12 +25,16 @@ function safeMergeJsTsSource(input = {}) {
25
25
  return composedBlockedResult(input, 'member-analysis', memberNeutralization.result, memberNeutralization.analysis);
26
26
  }
27
27
 
28
- const topLevelResult = safeMergeJsTsImportsAndDeclarations({
28
+ const topLevelInput = {
29
29
  ...input,
30
30
  baseSourceText: memberNeutralization.baseSourceText,
31
31
  workerSourceText: memberNeutralization.workerSourceText,
32
32
  headSourceText: memberNeutralization.headSourceText
33
- });
33
+ };
34
+ const topLevelLedgerResult = safeMergeJsTsImportsAndDeclarations(topLevelInput);
35
+ const topLevelResult = topLevelLedgerResult.status === JsTsSafeMergeStatuses.merged
36
+ ? topLevelLedgerResult
37
+ : semanticEditFallbackResult(topLevelInput, topLevelLedgerResult);
34
38
  if (topLevelResult.status !== JsTsSafeMergeStatuses.merged) {
35
39
  return composedBlockedResult(input, 'top-level', topLevelResult, memberNeutralization.analysis);
36
40
  }
@@ -61,7 +65,7 @@ function safeMergeJsTsSource(input = {}) {
61
65
  metadata: {
62
66
  ...topLevelResult.metadata,
63
67
  composed: {
64
- phases: ['top-level', 'member'],
68
+ phases: composedPhaseList(topLevelResult),
65
69
  memberRegions: memberNeutralization.analysis.preparedRegions.map((region) => ({
66
70
  kind: region.kind,
67
71
  name: region.name,
@@ -78,6 +82,13 @@ function safeMergeJsTsSource(input = {}) {
78
82
  };
79
83
  }
80
84
 
85
+ function composedPhaseList(topLevelResult) {
86
+ const topLevelPhases = topLevelResult.metadata?.composed?.phases;
87
+ if (Array.isArray(topLevelPhases) && topLevelPhases.length) return [...topLevelPhases, 'member'];
88
+ const topLevelPhase = topLevelResult.metadata?.composed?.phase;
89
+ return topLevelPhase ? [topLevelPhase, 'member'] : ['top-level', 'member'];
90
+ }
91
+
81
92
  function hasMemberMergePolicy(input) {
82
93
  const policy = input.policy ?? input.mergePolicy ?? input;
83
94
  const regions = Array.isArray(policy)
@@ -119,14 +130,20 @@ function composedBlockedResult(input, phase, result, memberAnalysis) {
119
130
  gatesPassed: result.gates?.filter((gate) => gate.status === 'passed').length ?? 0,
120
131
  memberRegions: memberAnalysis?.preparedRegions?.length ?? 0,
121
132
  memberAdditions: 0,
133
+ semanticEditOperations: result.summary?.semanticEditOperations,
134
+ semanticEditAppliedOperations: result.summary?.semanticEditAppliedOperations,
135
+ semanticEditReplayStatus: result.summary?.semanticEditReplayStatus,
122
136
  composedPhases: phase === 'top-level' ? 2 : 1
123
137
  },
124
138
  metadata: {
125
139
  composed: {
126
140
  phase,
127
- sourceKind: result.kind
141
+ sourceKind: result.kind,
142
+ topLevelPhase: result.metadata?.composed?.phase,
143
+ topLevelPhases: result.metadata?.composed?.phases
128
144
  }
129
- }
145
+ },
146
+ semanticArtifacts: result.semanticArtifacts
130
147
  };
131
148
  }
132
149
 
@@ -12,6 +12,20 @@ function canonicalizeSourceBodies(sourceText, preparedRegions, side) {
12
12
  return output;
13
13
  }
14
14
 
15
+ function removePreparedMemberAdditions(sourceText, preparedRegions, side) {
16
+ let output = sourceText;
17
+ const replacements = preparedRegions
18
+ .map((region) => ({
19
+ range: region[side],
20
+ replacement: removeMembersFromBody(region[side].body, region[`${side}AddedMembers`] ?? [], region.kind)
21
+ }))
22
+ .sort((left, right) => right.range.bodyStart - left.range.bodyStart);
23
+ for (const { range, replacement } of replacements) {
24
+ output = `${output.slice(0, range.bodyStart)}${replacement}${output.slice(range.bodyEnd)}`;
25
+ }
26
+ return output;
27
+ }
28
+
15
29
  function applyMemberAdditions(headSourceText, preparedRegions) {
16
30
  return applyPreparedMemberAdditions(headSourceText, preparedRegions, ['worker']).sourceText;
17
31
  }
@@ -28,7 +42,7 @@ function applyPreparedMemberAdditions(sourceText, preparedRegions, sides = ['wor
28
42
  }
29
43
  const members = sides.flatMap((side) => region[`${side}AddedMembers`] ?? []);
30
44
  if (!members.length) return undefined;
31
- return { range: match.value, replacement: appendMembersToBody(match.value.body, members) };
45
+ return { range: match.value, replacement: appendMembersToBody(match.value.body, members, region.kind) };
32
46
  })
33
47
  .filter(Boolean)
34
48
  .sort((left, right) => right.range.bodyStart - left.range.bodyStart);
@@ -38,32 +52,50 @@ function applyPreparedMemberAdditions(sourceText, preparedRegions, sides = ['wor
38
52
  return { sourceText: output, reasonCodes: uniqueStrings(reasonCodes) };
39
53
  }
40
54
 
41
- function appendMembersToBody(body, members) {
55
+ function appendMembersToBody(body, members, kind) {
42
56
  if (!members.length) return body;
43
57
  const indent = inferMemberIndent(body) ?? inferMemberIndent(members.map((member) => member.text).join('\n')) ?? ' ';
44
- const addedText = members.map((member) => normalizeMemberForInsertion(member.text, indent)).join('\n');
58
+ const addedText = members.map((member, index) => normalizeMemberForInsertion(member.text, indent, kind, index < members.length - 1)).join('\n');
45
59
  const trailing = body.match(/\s*$/)?.[0] ?? '';
46
60
  const before = body.slice(0, body.length - trailing.length);
47
61
  if (!before.trim()) {
48
62
  const closingIndent = trailing.includes('\n') ? trailing.slice(trailing.lastIndexOf('\n') + 1) : '';
49
63
  return `\n${addedText}\n${closingIndent}`;
50
64
  }
51
- return `${before}${before.endsWith('\n') ? '' : '\n'}${addedText}${trailing.includes('\n') ? trailing : `\n${trailing}`}`;
65
+ const appendableBefore = appendReadyBody(before, kind);
66
+ return `${appendableBefore}${appendableBefore.endsWith('\n') ? '' : '\n'}${addedText}${trailing.includes('\n') ? trailing : `\n${trailing}`}`;
52
67
  }
53
68
 
54
69
  function inferMemberIndent(text) {
55
70
  return String(text ?? '').match(/\n([ \t]*)\S/)?.[1];
56
71
  }
57
72
 
58
- function normalizeMemberForInsertion(text, indent) {
73
+ function normalizeMemberForInsertion(text, indent, kind, needsDelimiter) {
59
74
  const lines = String(text ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
60
75
  while (lines.length && !lines[0].trim()) lines.shift();
61
76
  while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
62
77
  const commonIndent = minimumIndent(lines);
63
- return lines.map((line) => {
78
+ const normalized = lines.map((line) => {
64
79
  const normalized = commonIndent ? line.slice(Math.min(commonIndent, leadingWhitespace(line))) : line;
65
80
  return normalized.trim() ? `${indent}${normalized}` : normalized;
66
81
  }).join('\n');
82
+ if (kind !== 'object' || !needsDelimiter || /,\s*$/.test(normalized)) return normalized;
83
+ return `${normalized},`;
84
+ }
85
+
86
+ function appendReadyBody(before, kind) {
87
+ if (kind !== 'object' || !before.trim() || /,\s*$/.test(before)) return before;
88
+ return `${before.replace(/\s*$/, '')},`;
89
+ }
90
+
91
+ function removeMembersFromBody(body, members, kind) {
92
+ let output = body;
93
+ for (const member of [...members].sort((left, right) => right.start - left.start || right.end - left.end)) {
94
+ output = `${output.slice(0, member.start)}${output.slice(member.end)}`;
95
+ }
96
+ if (kind !== 'object') return output;
97
+ const trailing = String(body ?? '').match(/\s*$/)?.[0] ?? '';
98
+ return output.replace(/,\s*$/, trailing);
67
99
  }
68
100
 
69
101
  function minimumIndent(lines) {
@@ -78,5 +110,6 @@ function leadingWhitespace(line) {
78
110
  export {
79
111
  applyMemberAdditions,
80
112
  applyPreparedMemberAdditions,
81
- canonicalizeSourceBodies
113
+ canonicalizeSourceBodies,
114
+ removePreparedMemberAdditions
82
115
  };
@@ -1,11 +1,14 @@
1
- function normalizeMemberText(text) {
2
- return String(text ?? '')
1
+ function normalizeMemberText(text, kind) {
2
+ const normalized = String(text ?? '')
3
3
  .replace(/\r\n/g, '\n')
4
4
  .replace(/\r/g, '\n')
5
5
  .split('\n')
6
6
  .map((line) => line.trimEnd())
7
7
  .join('\n')
8
8
  .trim();
9
+ if (kind === 'object') return normalized.replace(/,\s*$/, '');
10
+ if (kind === 'interface' || kind === 'type' || kind === 'class') return normalized.replace(/;\s*$/, '');
11
+ return normalized;
9
12
  }
10
13
 
11
14
  function uniqueStrings(values) {
@@ -8,7 +8,8 @@ export {
8
8
  export {
9
9
  applyMemberAdditions,
10
10
  applyPreparedMemberAdditions,
11
- canonicalizeSourceBodies
11
+ canonicalizeSourceBodies,
12
+ removePreparedMemberAdditions
12
13
  } from './js-ts-semantic-merge-member-source.js';
13
14
  export {
14
15
  normalizeMemberText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.133",
3
+ "version": "0.2.135",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",