@shapeshift-labs/frontier-lang-compiler 0.2.97 → 0.2.98

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.
@@ -10,6 +10,7 @@ export type SemanticEditScriptOperationStatus =
10
10
  | 'candidate'
11
11
  | 'portable'
12
12
  | 'already-applied'
13
+ | 'covered'
13
14
  | 'needs-port'
14
15
  | 'conflict'
15
16
  | 'stale'
@@ -82,6 +83,7 @@ export interface SemanticEditScriptSummary {
82
83
  readonly conflicts: number;
83
84
  readonly stale: number;
84
85
  readonly blocked: number;
86
+ readonly covered?: number;
85
87
  readonly candidates: number;
86
88
  readonly autoMergeCandidates: number;
87
89
  readonly semanticKeys?: readonly string[];
@@ -13,7 +13,12 @@ export function projectSemanticEditScriptToSource(input = {}) {
13
13
  if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
14
14
  if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
15
15
  const edits = [];
16
+ const coveredOperationIds = [];
16
17
  for (const [index, operation] of (script.operations ?? []).entries()) {
18
+ if (operation.status === 'covered') {
19
+ coveredOperationIds.push(operation.id);
20
+ continue;
21
+ }
17
22
  const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index);
18
23
  if (edit.ok) edits.push(edit.value);
19
24
  else reasonCodes.push(...edit.reasonCodes);
@@ -35,7 +40,7 @@ export function projectSemanticEditScriptToSource(input = {}) {
35
40
  headHash: script.headHash,
36
41
  projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
37
42
  appliedOperations: blocked ? [] : deduped.edits.map((edit) => edit.operationId),
38
- skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : deduped.skippedOperationIds,
43
+ skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : uniqueStrings([...coveredOperationIds, ...deduped.skippedOperationIds]),
39
44
  edits: blocked ? [] : deduped.edits.map(projectionEditRecord),
40
45
  sourceText,
41
46
  admission: {
@@ -66,6 +71,9 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
66
71
  if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
67
72
  return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
68
73
  }
74
+ if (operation.changeKind === 'removed' || String(operation.kind ?? '').startsWith('remove')) {
75
+ return removalEditForOperation(operation, identity, headSourceText, order);
76
+ }
69
77
  const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
70
78
  const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
71
79
  const reasons = [];
@@ -99,6 +107,33 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
99
107
  };
100
108
  }
101
109
 
110
+ function removalEditForOperation(operation, identity, headSourceText, order) {
111
+ const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
112
+ const reasons = [];
113
+ if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
114
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
115
+ const rawCurrent = headSourceText.slice(headOffsets.start, headOffsets.end);
116
+ const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
117
+ if (expectedHeadHash && hashSemanticValue(rawCurrent) !== expectedHeadHash) {
118
+ reasons.push(`head-span-hash-mismatch:${operation.id}`);
119
+ }
120
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
121
+ const range = removalRange(headSourceText, headOffsets);
122
+ return {
123
+ ok: true,
124
+ value: {
125
+ operationId: operation.id,
126
+ order,
127
+ ...identity,
128
+ editKind: 'delete',
129
+ start: range.start,
130
+ end: range.end,
131
+ current: headSourceText.slice(range.start, range.end),
132
+ replacement: ''
133
+ }
134
+ };
135
+ }
136
+
102
137
  function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
103
138
  const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
104
139
  const reasons = [];
@@ -241,6 +276,13 @@ function insertionOffset(sourceText, insertion) {
241
276
  return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
242
277
  }
243
278
 
279
+ function removalRange(sourceText, span) {
280
+ const range = { ...span };
281
+ if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
282
+ else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
283
+ return range;
284
+ }
285
+
244
286
  function insertionReplacement(text, sourceText, offset) {
245
287
  let replacement = String(text ?? '');
246
288
  if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
@@ -0,0 +1,141 @@
1
+ import { uniqueStrings } from '../../native-import-utils.js';
2
+ import { nativeImportSourceText } from './nativeImportSourceText.js';
3
+
4
+ export function markCoveredSemanticEditOperations(operations, context) {
5
+ const sourceText = {
6
+ base: nativeImportSourceText(context.base),
7
+ worker: nativeImportSourceText(context.worker)
8
+ };
9
+ return (operations ?? []).map((operation) => {
10
+ const coveredBy = coveredByChildOperations(operation, operations, sourceText);
11
+ if (!coveredBy.length) return operation;
12
+ return {
13
+ ...operation,
14
+ status: 'covered',
15
+ readiness: 'ready',
16
+ confidence: Math.max(operation.confidence ?? 0, 0.82),
17
+ reasonCodes: uniqueStrings([...(operation.reasonCodes ?? []), 'container-covered-by-child-edits']),
18
+ evidenceIds: uniqueStrings(operation.evidenceIds ?? []),
19
+ metadata: {
20
+ ...(operation.metadata ?? {}),
21
+ coveredByOperationIds: coveredBy.map((child) => child.id)
22
+ }
23
+ };
24
+ });
25
+ }
26
+
27
+ function coveredByChildOperations(container, operations, sourceText) {
28
+ if (!isCoverableContainer(container)) return [];
29
+ const containerBase = spanOffsets(sourceText.base, container.spans?.base);
30
+ const containerWorker = spanOffsets(sourceText.worker, container.spans?.worker);
31
+ if (!containerBase || !containerWorker) return [];
32
+ const childEdits = (operations ?? [])
33
+ .filter((operation) => operation.id !== container.id)
34
+ .map((operation) => childEdit(operation, sourceText, containerBase))
35
+ .filter(Boolean);
36
+ if (!childEdits.length) return [];
37
+ const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
38
+ const workerText = sourceText.worker.slice(containerWorker.start, containerWorker.end);
39
+ return applyLocalEdits(baseText, childEdits) === workerText ? childEdits.map((edit) => edit.operation) : [];
40
+ }
41
+
42
+ function isCoverableContainer(operation) {
43
+ if (operation.changeKind !== 'modified') return false;
44
+ if (!operation.spans?.base || !operation.spans?.worker) return false;
45
+ const kind = String(operation.anchor?.regionKind ?? operation.regionKind ?? '');
46
+ return kind === 'type' || kind === 'config' || kind === 'content' || kind === 'route' || kind === 'property';
47
+ }
48
+
49
+ function childEdit(operation, sourceText, containerBase) {
50
+ if (!['portable', 'already-applied'].includes(operation.status)) return undefined;
51
+ if (operation.changeKind === 'modified') return replacementChildEdit(operation, sourceText, containerBase);
52
+ if (operation.changeKind === 'added') return insertionChildEdit(operation, sourceText, containerBase);
53
+ if (operation.changeKind === 'removed') return removalChildEdit(operation, sourceText, containerBase);
54
+ return undefined;
55
+ }
56
+
57
+ function replacementChildEdit(operation, sourceText, containerBase) {
58
+ const base = spanOffsets(sourceText.base, operation.spans?.base);
59
+ const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
60
+ if (!contained(base, containerBase) || !worker) return undefined;
61
+ return {
62
+ operation,
63
+ start: base.start - containerBase.start,
64
+ end: base.end - containerBase.start,
65
+ replacement: sourceText.worker.slice(worker.start, worker.end)
66
+ };
67
+ }
68
+
69
+ function insertionChildEdit(operation, sourceText, containerBase) {
70
+ const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
71
+ const offset = insertionOffset(sourceText.base, operation.insertion, containerBase);
72
+ if (!worker || offset === undefined) return undefined;
73
+ const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
74
+ const replacement = insertionReplacement(sourceText.worker.slice(worker.start, worker.end), baseText, offset);
75
+ return { operation, start: offset, end: offset, replacement };
76
+ }
77
+
78
+ function removalChildEdit(operation, sourceText, containerBase) {
79
+ const base = spanOffsets(sourceText.base, operation.spans?.base);
80
+ if (!contained(base, containerBase)) return undefined;
81
+ const range = removalRange(sourceText.base, base, containerBase);
82
+ return { operation, start: range.start - containerBase.start, end: range.end - containerBase.start, replacement: '' };
83
+ }
84
+
85
+ function insertionOffset(sourceText, insertion, containerBase) {
86
+ const mode = insertion?.mode;
87
+ if (mode === 'file-start') return 0;
88
+ if (mode === 'file-end') return containerBase.end - containerBase.start;
89
+ const span = spanOffsets(sourceText, insertion?.baseSpan ?? insertion?.headSpan);
90
+ if (!contained(span, containerBase)) return undefined;
91
+ if (mode === 'before') return span.start - containerBase.start;
92
+ if (mode === 'after') return afterLineOffset(sourceText, span.end) - containerBase.start;
93
+ return undefined;
94
+ }
95
+
96
+ function applyLocalEdits(sourceText, edits) {
97
+ return edits
98
+ .sort((left, right) => right.start - left.start || right.end - left.end)
99
+ .reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
100
+ }
101
+
102
+ function spanOffsets(sourceText, span) {
103
+ if (typeof sourceText !== 'string' || !span) return undefined;
104
+ if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
105
+ if (typeof span.startLine !== 'number') return undefined;
106
+ const lineStarts = [0];
107
+ for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
108
+ const startLine = Math.max(1, span.startLine);
109
+ const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
110
+ const start = lineStarts[startLine - 1];
111
+ const endLineStart = lineStarts[endLine - 1];
112
+ if (start === undefined || endLineStart === undefined) return undefined;
113
+ const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
114
+ const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
115
+ const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
116
+ return { start: start + startColumn, end: endLineStart + endColumn };
117
+ }
118
+
119
+ function contained(inner, outer) {
120
+ return Boolean(inner && outer && outer.start <= inner.start && inner.end <= outer.end);
121
+ }
122
+
123
+ function insertionReplacement(text, sourceText, offset) {
124
+ let replacement = String(text ?? '');
125
+ if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
126
+ if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
127
+ if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
128
+ if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
129
+ return replacement;
130
+ }
131
+
132
+ function removalRange(sourceText, span, container) {
133
+ const range = { ...span };
134
+ if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
135
+ else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
136
+ return range;
137
+ }
138
+
139
+ function afterLineOffset(sourceText, offset) {
140
+ return sourceText[offset] === '\n' ? offset + 1 : offset;
141
+ }
@@ -43,6 +43,7 @@ export function summarizeSemanticEditOperations(operations) {
43
43
  conflicts: byStatus.conflict ?? 0,
44
44
  stale: byStatus.stale ?? 0,
45
45
  blocked: byStatus.blocked ?? 0,
46
+ covered: byStatus.covered ?? 0,
46
47
  candidates: byStatus.candidate ?? 0,
47
48
  autoMergeCandidates: (byStatus.portable ?? 0) + (byStatus['already-applied'] ?? 0),
48
49
  semanticKeys: uniqueStrings(operations.map((operation) => operation.semanticKey).filter(Boolean)),
@@ -13,6 +13,7 @@ import {
13
13
  summarizeSemanticEditOperations
14
14
  } from './semanticEditScriptClassification.js';
15
15
  import { semanticEditInsertionAnchor } from './semanticEditInsertionAnchors.js';
16
+ import { markCoveredSemanticEditOperations } from './semanticEditOperationCoverage.js';
16
17
  import { sourceTextForSpan } from './sourceTextForSpan.js';
17
18
  import { semanticEditIdentityFields, semanticEditOperationContentHash } from './semanticEditIdentityRecords.js';
18
19
 
@@ -52,7 +53,10 @@ export function createSemanticEditScript(input = {}, options = {}) {
52
53
  metadata: { source: 'createSemanticEditScript' }
53
54
  }) : undefined;
54
55
  const context = createEditContext({ base, worker, head, workerChangeSet, headChangeSet, headLineage });
55
- const operations = workerChangeSet.changedRegions.map((region, index) => semanticEditOperation(region, index, context, input));
56
+ const operations = markCoveredSemanticEditOperations(
57
+ workerChangeSet.changedRegions.map((region, index) => semanticEditOperation(region, index, context, input)),
58
+ context
59
+ );
56
60
  const summary = summarizeSemanticEditOperations(operations);
57
61
  const admission = semanticEditAdmission({ operations, summary, head, workerChangeSet, headChangeSet, input });
58
62
  const evidence = semanticEditEvidence({ input, language, sourcePath, workerChangeSet, headChangeSet, headLineage, summary, admission });
@@ -169,7 +173,7 @@ function semanticEditOperation(region, index, context, input) {
169
173
  const identityRecord = semanticEditIdentityRecord({ kind, region, anchor });
170
174
  const identity = semanticEditIdentityFields(identityRecord);
171
175
  return compactRecord({
172
- id: `semantic_edit_op_${idFragment([input.id ?? 'semantic_edit', anchorKey, index].join(':'))}`,
176
+ id: `semantic_edit_op_${idFragment([index, anchorKey, input.id ?? 'semantic_edit'].join(':'))}`,
173
177
  kind,
174
178
  changeKind: region.changeKind,
175
179
  anchor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.97",
3
+ "version": "0.2.98",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",