@shapeshift-labs/frontier-lang-compiler 0.2.94 → 0.2.96

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.
@@ -13,6 +13,15 @@ import type { SemanticLineageEvent, SemanticLineageResolution } from './semantic
13
13
  import type { SemanticPatchBundleRecord } from './semantic-patch-bundle.js';
14
14
 
15
15
  export type BidirectionalTargetChangeAnchorStatus = 'matched' | 'unmatched' | 'ambiguous' | 'deleted' | string;
16
+ export type BidirectionalTargetPortabilityStatus = 'portable' | 'needs-port' | 'stale' | 'conflict' | 'blocked' | 'evidence-only' | string;
17
+ export type BidirectionalTargetPortabilityAction =
18
+ | 'port-with-source-map-review'
19
+ | 'human-port'
20
+ | 'refresh-source-map'
21
+ | 'resolve-anchor-conflict'
22
+ | 'block'
23
+ | 'record-evidence'
24
+ | string;
16
25
 
17
26
  export interface BidirectionalTargetChangeSourceAnchorMapping {
18
27
  readonly targetAnchorKey?: string;
@@ -62,6 +71,20 @@ export interface BidirectionalTargetChangeAnchor {
62
71
  readonly metadata?: Record<string, unknown>;
63
72
  }
64
73
 
74
+ export interface BidirectionalTargetMatchPortability {
75
+ readonly status: BidirectionalTargetPortabilityStatus;
76
+ readonly action: BidirectionalTargetPortabilityAction;
77
+ readonly readiness: SemanticMergeReadiness | string;
78
+ readonly confidence?: number;
79
+ readonly reviewRequired: true;
80
+ readonly autoMergeClaim: false;
81
+ readonly semanticEquivalenceClaim: false;
82
+ readonly reasonCodes: readonly string[];
83
+ readonly sourceMapLinkIds: readonly string[];
84
+ readonly sourceMapMappingIds: readonly string[];
85
+ readonly staleSourceMapLinkIds: readonly string[];
86
+ }
87
+
65
88
  export interface BidirectionalTargetChangeSourceAnchorMatch {
66
89
  readonly kind: 'frontier.lang.bidirectionalTargetChangeSourceAnchorMatch';
67
90
  readonly version: 1;
@@ -70,6 +93,7 @@ export interface BidirectionalTargetChangeSourceAnchorMatch {
70
93
  readonly sourceAnchors: readonly BidirectionalTargetChangeAnchor[];
71
94
  readonly lineageResolutions: readonly SemanticLineageResolution[];
72
95
  readonly sourceMapLinks: readonly BidirectionalTargetChangeSourceMapLink[];
96
+ readonly portability?: BidirectionalTargetMatchPortability;
73
97
  readonly status: BidirectionalTargetChangeAnchorStatus;
74
98
  readonly confidence?: number;
75
99
  readonly reasonCodes: readonly string[];
@@ -79,6 +103,31 @@ export interface BidirectionalTargetChangeSourceAnchorMatch {
79
103
  readonly conflictKeys: readonly string[];
80
104
  }
81
105
 
106
+ export interface BidirectionalTargetPortabilityRecord {
107
+ readonly kind: 'frontier.lang.bidirectionalTargetPortability';
108
+ readonly version: 1;
109
+ readonly id?: string;
110
+ readonly status: BidirectionalTargetPortabilityStatus;
111
+ readonly action: BidirectionalTargetPortabilityAction;
112
+ readonly readiness: SemanticMergeReadiness | string;
113
+ readonly confidence?: number;
114
+ readonly reviewRequired: true;
115
+ readonly autoMergeClaim: false;
116
+ readonly semanticEquivalenceClaim: false;
117
+ readonly reasonCodes: readonly string[];
118
+ readonly conflictKeys: readonly string[];
119
+ readonly sourceAnchorMatchIds: readonly string[];
120
+ readonly sourceMapLinkIds: readonly string[];
121
+ readonly sourceMapMappingIds: readonly string[];
122
+ readonly staleSourceMapLinkIds: readonly string[];
123
+ readonly targetChangedRegions: number;
124
+ readonly matchedTargetRegions: number;
125
+ readonly sourceMapBackedRegions: number;
126
+ readonly unmatchedTargetRegions: number;
127
+ readonly ambiguousTargetRegions: number;
128
+ readonly deletedSourceAnchors: number;
129
+ }
130
+
82
131
  export interface CreateBidirectionalTargetChangeRecordOptions {
83
132
  readonly id?: string;
84
133
  readonly source?: NativeSourceImportResult | ImportNativeSourceOptions;
@@ -130,6 +179,7 @@ export interface BidirectionalTargetChangeRecord {
130
179
  readonly sourceImport?: NativeSourceImportResult;
131
180
  readonly targetChangeSet: NativeSourceChangeSet;
132
181
  readonly sourceAnchorMatches: readonly BidirectionalTargetChangeSourceAnchorMatch[];
182
+ readonly targetPortability: BidirectionalTargetPortabilityRecord;
133
183
  readonly sourcePatchBundle: SemanticPatchBundleRecord;
134
184
  readonly historyRecord: SemanticHistoryRecord;
135
185
  readonly evidence: readonly EvidenceRecord[];
@@ -143,6 +193,10 @@ export interface BidirectionalTargetChangeRecord {
143
193
  readonly deletedSourceAnchors: number;
144
194
  readonly sourceChangedRegions: number;
145
195
  readonly sourceMapBackedMatches: number;
196
+ readonly targetPortabilityStatus: BidirectionalTargetPortabilityStatus;
197
+ readonly portableTargetRegions: number;
198
+ readonly staleTargetRegions: number;
199
+ readonly conflictingTargetRegions: number;
146
200
  };
147
201
  readonly metadata: {
148
202
  readonly autoMergeClaim: false;
@@ -28,39 +28,32 @@ export interface SemanticEditScriptOperation {
28
28
  readonly kind: string;
29
29
  readonly changeKind?: string;
30
30
  readonly anchor: {
31
- readonly key?: string;
32
- readonly conflictKey?: string;
33
- readonly regionId?: string;
34
- readonly regionKind?: string;
35
- readonly granularity?: string;
31
+ readonly key?: string; readonly conflictKey?: string; readonly regionId?: string;
32
+ readonly regionKind?: string; readonly granularity?: string;
36
33
  readonly language?: FrontierSourceLanguage | string;
37
- readonly sourcePath?: string;
38
- readonly symbolId?: string;
39
- readonly symbolName?: string;
40
- readonly symbolKind?: string;
34
+ readonly sourcePath?: string; readonly symbolId?: string; readonly symbolName?: string; readonly symbolKind?: string;
41
35
  readonly sourceSpan?: SourceSpan;
42
36
  };
37
+ readonly insertion?: {
38
+ readonly mode?: 'before' | 'after' | 'file-start' | 'file-end' | string;
39
+ readonly anchorKey?: string; readonly anchorSymbolId?: string; readonly anchorSymbolName?: string; readonly anchorSymbolKind?: string;
40
+ readonly baseSpan?: SourceSpan; readonly workerAnchorSpan?: SourceSpan; readonly headSpan?: SourceSpan; readonly sourcePath?: string;
41
+ readonly insertedSymbolId?: string; readonly insertedSymbolName?: string; readonly insertedSymbolKind?: string;
42
+ readonly insertedSourceSpan?: SourceSpan; readonly insertedSourcePath?: string;
43
+ readonly reasonCodes?: readonly string[];
44
+ };
43
45
  readonly semanticKey?: string;
44
46
  readonly semanticIdentityHash?: string;
45
47
  readonly sourceIdentityHash?: string;
46
48
  readonly operationContentHash?: string;
47
49
  readonly spans?: {
48
- readonly base?: SourceSpan;
49
- readonly worker?: SourceSpan;
50
- readonly head?: SourceSpan;
50
+ readonly base?: SourceSpan; readonly worker?: SourceSpan; readonly head?: SourceSpan;
51
51
  };
52
52
  readonly hashes?: {
53
- readonly baseSourceHash?: string;
54
- readonly workerSourceHash?: string;
55
- readonly headSourceHash?: string;
56
- readonly baseSpanHash?: string;
57
- readonly workerSpanHash?: string;
58
- readonly headSpanHash?: string;
59
- readonly baseTextHash?: string;
60
- readonly workerTextHash?: string;
61
- readonly headTextHash?: string;
62
- readonly beforeSignatureHash?: string;
63
- readonly afterSignatureHash?: string;
53
+ readonly baseSourceHash?: string; readonly workerSourceHash?: string; readonly headSourceHash?: string;
54
+ readonly baseSpanHash?: string; readonly workerSpanHash?: string; readonly headSpanHash?: string;
55
+ readonly baseTextHash?: string; readonly workerTextHash?: string; readonly headTextHash?: string;
56
+ readonly beforeSignatureHash?: string; readonly afterSignatureHash?: string;
64
57
  };
65
58
  readonly status: SemanticEditScriptOperationStatus;
66
59
  readonly reanchor?: {
@@ -135,6 +128,7 @@ export interface SemanticEditProjectionEdit {
135
128
  readonly operationId?: string;
136
129
  readonly status: 'applied' | 'already-applied';
137
130
  readonly kind?: string;
131
+ readonly editKind?: 'replace' | 'insert' | string;
138
132
  readonly changeKind?: string;
139
133
  readonly anchorKey?: string;
140
134
  readonly conflictKey?: string;
@@ -162,6 +156,11 @@ export interface SemanticEditProjectionEdit {
162
156
  readonly replacementBytes: number;
163
157
  readonly deletedTextHash?: string;
164
158
  readonly replacementTextHash?: string;
159
+ readonly replacementSpanTextHash?: string;
160
+ readonly insertionMode?: string;
161
+ readonly insertionAnchorKey?: string;
162
+ readonly insertionAnchorSymbolName?: string;
163
+ readonly insertionAnchorSymbolKind?: string;
165
164
  readonly replacementText?: string;
166
165
  }
167
166
 
@@ -206,6 +205,7 @@ export interface SemanticEditReplayEdit {
206
205
  readonly semanticIdentityHash?: string;
207
206
  readonly sourceIdentityHash?: string;
208
207
  readonly editContentHash?: string;
208
+ readonly editKind?: 'replace' | 'insert' | string;
209
209
  readonly sourcePath?: string;
210
210
  readonly symbolName?: string;
211
211
  readonly symbolKind?: string;
@@ -36,6 +36,9 @@ export interface SemanticPatchBundleRecordIndex {
36
36
  readonly transformTargetLanguages: readonly string[];
37
37
  readonly transformSourcePaths: readonly string[];
38
38
  readonly transformTargetPaths: readonly string[];
39
+ readonly targetPortabilityStatuses: readonly string[];
40
+ readonly targetPortabilityActions: readonly string[];
41
+ readonly targetPortabilityReasonCodes: readonly string[];
39
42
  readonly patchIds: readonly string[];
40
43
  readonly mergeCandidateIds: readonly string[];
41
44
  readonly readinesses: readonly string[];
@@ -10,6 +10,7 @@ import type { NativeSourceChangeKind, NativeSourceChangeSet } from './native-dif
10
10
  import type { SemanticEditProjection, SemanticEditReplay, SemanticEditScript } from './semantic-edit-script.js';
11
11
  import type { SemanticPatchBundleRecordIndex } from './semantic-patch-bundle-index.js';
12
12
  import type { SemanticTransformIdentityRecord } from './semantic-transform-identity.js';
13
+ import type { BidirectionalTargetPortabilityRecord } from './bidirectional-target-change.js';
13
14
 
14
15
  export type { SemanticPatchBundleRecordIndex } from './semantic-patch-bundle-index.js';
15
16
 
@@ -56,6 +57,7 @@ export interface SemanticPatchBundleChangedRegion {
56
57
  readonly reasonCodes?: readonly string[];
57
58
  readonly conflictKeys?: readonly string[];
58
59
  };
60
+ readonly metadata?: Record<string, unknown>;
59
61
  }
60
62
 
61
63
  export interface SemanticPatchBundleSourceMapLink {
@@ -185,6 +187,7 @@ export interface CreateSemanticPatchBundleRecordOptions {
185
187
  readonly semanticEditReplays?: readonly SemanticEditReplay[] | SemanticEditReplay;
186
188
  readonly semanticTransformIdentity?: SemanticTransformIdentityRecord | Record<string, unknown>;
187
189
  readonly semanticTransformIdentities?: readonly (SemanticTransformIdentityRecord | Record<string, unknown>)[];
190
+ readonly targetPortability?: BidirectionalTargetPortabilityRecord | Record<string, unknown>;
188
191
  readonly sourceLanguage?: FrontierSourceLanguage | string;
189
192
  readonly targetLanguage?: FrontierSourceLanguage | string;
190
193
  readonly conflictKeys?: readonly string[] | string;
@@ -275,6 +278,12 @@ export interface SemanticPatchBundleRecordQuery {
275
278
  readonly transformSourcePaths?: readonly string[];
276
279
  readonly transformTargetPath?: string | readonly string[];
277
280
  readonly transformTargetPaths?: readonly string[];
281
+ readonly targetPortabilityStatus?: string | readonly string[];
282
+ readonly targetPortabilityStatuses?: readonly string[];
283
+ readonly targetPortabilityAction?: string | readonly string[];
284
+ readonly targetPortabilityActions?: readonly string[];
285
+ readonly targetPortabilityReasonCode?: string | readonly string[];
286
+ readonly targetPortabilityReasonCodes?: readonly string[];
278
287
  readonly readiness?: SemanticMergeReadiness | string | readonly string[];
279
288
  readonly readinesses?: readonly string[];
280
289
  readonly admissionStatus?: SemanticPatchBundleAdmissionStatus | readonly string[];
@@ -55,6 +55,7 @@ export function classifyBidirectionalReadiness(targetChangeSet, source, matches)
55
55
 
56
56
  export function sourceRegionsForMatch(match, readiness) {
57
57
  const anchors = match.sourceAnchors.length ? match.sourceAnchors : [undefined];
58
+ const portability = match.portability;
58
59
  return anchors.map((anchor, index) => compactRecord({
59
60
  id: `source_port_region_${idFragment(match.id)}_${index + 1}`,
60
61
  key: anchor?.key ?? `unmapped-target#${match.targetRegion.key ?? match.targetRegion.id}`,
@@ -70,9 +71,9 @@ export function sourceRegionsForMatch(match, readiness) {
70
71
  sourceSpan: anchor?.sourceSpan,
71
72
  sourceMapLinks: match.sourceMapLinks,
72
73
  admission: {
73
- readiness,
74
- action: 'review-port-from-target-change',
75
- reasonCodes: match.reasonCodes,
74
+ readiness: portability?.readiness ?? readiness,
75
+ action: portability?.action ?? 'review-port-from-target-change',
76
+ reasonCodes: uniqueStrings([...match.reasonCodes, ...array(portability?.reasonCodes)]),
76
77
  conflictKeys: match.conflictKeys
77
78
  },
78
79
  metadata: {
@@ -81,6 +82,7 @@ export function sourceRegionsForMatch(match, readiness) {
81
82
  targetRegion: match.targetRegion,
82
83
  sourceMapLinkIds: match.sourceMapLinks.map((link) => link.id),
83
84
  lineageResolutionIds: match.lineageResolutions.map((resolution) => resolution.id),
85
+ targetPortability: portability,
84
86
  reviewRequired: true,
85
87
  autoMergeClaim: false,
86
88
  semanticEquivalenceClaim: false
@@ -104,6 +106,10 @@ export function createBidirectionalEvidence(context) {
104
106
  sourceAnchorMatchIds: context.sourceAnchorMatches.map((match) => match.id),
105
107
  sourceMapBackedMatches: context.sourceAnchorMatches.filter((match) => match.sourceMapLinks.length > 0).length,
106
108
  sourceMapLinkIds: context.sourceAnchorMatches.flatMap((match) => match.sourceMapLinks.map((link) => link.id)),
109
+ targetPortabilityStatus: context.targetPortability?.status,
110
+ targetPortabilityAction: context.targetPortability?.action,
111
+ targetPortabilityReasonCodes: context.targetPortability?.reasonCodes,
112
+ targetPortabilitySourceMapLinkIds: context.targetPortability?.sourceMapLinkIds,
107
113
  readiness: context.readiness,
108
114
  reasons: context.reasons,
109
115
  autoMergeClaim: false,
@@ -0,0 +1,152 @@
1
+ import { uniqueStrings } from '../../native-import-utils.js';
2
+
3
+ export function classifyBidirectionalTargetPortability(context = {}) {
4
+ const matches = array(context.sourceAnchorMatches);
5
+ const targetRegions = Number(context.targetChangeSet?.changedRegions?.length ?? matches.length);
6
+ const links = matches.flatMap((match) => array(match.sourceMapLinks));
7
+ const sourceMapBackedMatches = matches.filter((match) => array(match.sourceMapLinks).length > 0).length;
8
+ const unmatched = matches.filter((match) => match.status === 'unmatched').length;
9
+ const ambiguous = matches.filter((match) => match.status === 'ambiguous').length;
10
+ const deleted = matches.filter((match) => match.status === 'deleted').length;
11
+ const staleLinks = links.filter((link) => sourceMapLinkIsStale(link, context));
12
+ const status = portabilityStatus({ context, targetRegions, matches, sourceMapBackedMatches, unmatched, ambiguous, deleted, staleLinks });
13
+ return compactRecord({
14
+ kind: 'frontier.lang.bidirectionalTargetPortability',
15
+ version: 1,
16
+ id: context.id ? `target_portability_${context.id}` : undefined,
17
+ status,
18
+ action: portabilityAction(status),
19
+ readiness: status === 'blocked' ? 'blocked' : 'needs-review',
20
+ confidence: portabilityConfidence({ status, targetRegions, sourceMapBackedMatches, unmatched, ambiguous, deleted, staleLinks }),
21
+ reviewRequired: true,
22
+ autoMergeClaim: false,
23
+ semanticEquivalenceClaim: false,
24
+ reasonCodes: portabilityReasons({ context, targetRegions, matches, sourceMapBackedMatches, unmatched, ambiguous, deleted, staleLinks, status }),
25
+ conflictKeys: uniqueStrings(matches.flatMap((match) => array(match.conflictKeys))),
26
+ sourceAnchorMatchIds: uniqueStrings(matches.map((match) => match.id)),
27
+ sourceMapLinkIds: uniqueStrings(links.map((link) => link.id)),
28
+ sourceMapMappingIds: uniqueStrings(links.map((link) => link.sourceMapMappingId)),
29
+ staleSourceMapLinkIds: uniqueStrings(staleLinks.map((link) => link.id)),
30
+ targetChangedRegions: targetRegions,
31
+ matchedTargetRegions: matches.filter((match) => match.status === 'matched').length,
32
+ sourceMapBackedRegions: sourceMapBackedMatches,
33
+ unmatchedTargetRegions: unmatched,
34
+ ambiguousTargetRegions: ambiguous,
35
+ deletedSourceAnchors: deleted
36
+ });
37
+ }
38
+
39
+ export function attachBidirectionalMatchPortability(match = {}, context = {}) {
40
+ return {
41
+ ...match,
42
+ portability: classifyBidirectionalMatchPortability(match, context)
43
+ };
44
+ }
45
+
46
+ export function classifyBidirectionalMatchPortability(match = {}, context = {}) {
47
+ const links = array(match.sourceMapLinks);
48
+ const staleLinks = links.filter((link) => sourceMapLinkIsStale(link, context));
49
+ const status = matchPortabilityStatus(match, links, staleLinks);
50
+ return compactRecord({
51
+ status,
52
+ action: portabilityAction(status),
53
+ readiness: status === 'blocked' ? 'blocked' : 'needs-review',
54
+ confidence: matchPortabilityConfidence(status, links),
55
+ reviewRequired: true,
56
+ autoMergeClaim: false,
57
+ semanticEquivalenceClaim: false,
58
+ reasonCodes: matchPortabilityReasons(match, links, staleLinks, status),
59
+ sourceMapLinkIds: uniqueStrings(links.map((link) => link.id)),
60
+ sourceMapMappingIds: uniqueStrings(links.map((link) => link.sourceMapMappingId)),
61
+ staleSourceMapLinkIds: uniqueStrings(staleLinks.map((link) => link.id))
62
+ });
63
+ }
64
+
65
+ function portabilityStatus(input) {
66
+ if (!input.context.source) return 'blocked';
67
+ if (input.targetRegions === 0 || input.matches.length === 0) return 'evidence-only';
68
+ if (input.deleted > 0) return 'blocked';
69
+ if (input.unmatched === input.targetRegions) return 'blocked';
70
+ if (input.staleLinks.length > 0) return 'stale';
71
+ if (input.ambiguous > 0) return 'conflict';
72
+ if (input.unmatched > 0) return 'needs-port';
73
+ if (input.sourceMapBackedMatches === input.targetRegions) return 'portable';
74
+ return 'needs-port';
75
+ }
76
+
77
+ function portabilityAction(status) {
78
+ if (status === 'portable') return 'port-with-source-map-review';
79
+ if (status === 'needs-port') return 'human-port';
80
+ if (status === 'stale') return 'refresh-source-map';
81
+ if (status === 'conflict') return 'resolve-anchor-conflict';
82
+ if (status === 'blocked') return 'block';
83
+ return 'record-evidence';
84
+ }
85
+
86
+ function matchPortabilityStatus(match, links, staleLinks) {
87
+ if (match.status === 'deleted' || match.status === 'unmatched') return 'blocked';
88
+ if (staleLinks.length > 0) return 'stale';
89
+ if (match.status === 'ambiguous') return 'conflict';
90
+ if (match.status === 'matched' && links.length > 0) return 'portable';
91
+ return 'needs-port';
92
+ }
93
+
94
+ function matchPortabilityConfidence(status, links) {
95
+ if (status === 'portable') return 0.72;
96
+ if (status === 'needs-port') return links.length > 0 ? 0.52 : 0.35;
97
+ return undefined;
98
+ }
99
+
100
+ function matchPortabilityReasons(match, links, staleLinks, status) {
101
+ return uniqueStrings([
102
+ status === 'portable' ? 'target-region-source-map-portable' : undefined,
103
+ links.length > 0 ? 'target-region-source-map-backed' : undefined,
104
+ links.length === 0 ? 'target-region-not-source-map-backed' : undefined,
105
+ match.status === 'unmatched' ? 'target-anchor-unmatched' : undefined,
106
+ match.status === 'ambiguous' ? 'target-anchor-ambiguous' : undefined,
107
+ match.status === 'deleted' ? 'target-anchor-deleted' : undefined,
108
+ staleLinks.length > 0 ? 'target-source-map-stale' : undefined,
109
+ 'target-region-is-portability-evidence-not-proof'
110
+ ]);
111
+ }
112
+
113
+ function portabilityConfidence(input) {
114
+ if (input.status === 'portable') return 0.74;
115
+ if (input.status === 'needs-port') return input.sourceMapBackedMatches > 0 ? 0.52 : 0.35;
116
+ if (input.status === 'evidence-only') return 0.25;
117
+ return undefined;
118
+ }
119
+
120
+ function portabilityReasons(input) {
121
+ const sourceMapBacked = input.sourceMapBackedMatches > 0;
122
+ return uniqueStrings([
123
+ input.context.source ? undefined : 'source-import-missing',
124
+ input.targetRegions === 0 ? 'target-change-empty' : undefined,
125
+ input.status === 'portable' ? 'target-change-source-map-portable' : undefined,
126
+ sourceMapBacked ? 'target-change-source-map-backed' : undefined,
127
+ input.sourceMapBackedMatches < input.targetRegions ? 'target-change-not-fully-source-map-backed' : undefined,
128
+ input.unmatched > 0 ? 'target-anchor-unmatched' : undefined,
129
+ input.ambiguous > 0 ? 'target-anchor-ambiguous' : undefined,
130
+ input.deleted > 0 ? 'target-anchor-deleted' : undefined,
131
+ input.staleLinks.length > 0 ? 'target-source-map-stale' : undefined,
132
+ 'target-change-is-portability-evidence-not-proof',
133
+ ...input.matches.flatMap((match) => array(match.reasonCodes))
134
+ ]);
135
+ }
136
+
137
+ function sourceMapLinkIsStale(link, context) {
138
+ const currentSourceHash = context.currentSourceHash;
139
+ const baseTargetHash = context.targetChangeSet?.beforeHash;
140
+ return Boolean(
141
+ (link.sourceHash && currentSourceHash && link.sourceHash !== currentSourceHash)
142
+ || (link.targetHash && baseTargetHash && link.targetHash !== baseTargetHash)
143
+ );
144
+ }
145
+
146
+ function array(value) {
147
+ return value === undefined || value === null ? [] : Array.isArray(value) ? value : [value];
148
+ }
149
+
150
+ function compactRecord(value) {
151
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
152
+ }
@@ -8,6 +8,7 @@ import { createSemanticPatchBundleRecord } from './semanticPatchBundleRecords.js
8
8
  import { createSemanticHistoryRecord } from './semanticHistoryRecords.js';
9
9
  import { diffNativeSourceImports } from './diffNativeSourceImports.js';
10
10
  import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
11
+ import { attachBidirectionalMatchPortability, classifyBidirectionalTargetPortability } from './bidirectionalTargetPortability.js';
11
12
  import {
12
13
  anchorsFromSourceSidecar,
13
14
  classifyBidirectionalReadiness,
@@ -64,12 +65,24 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
64
65
  mappings,
65
66
  sourceMaps,
66
67
  lineage
68
+ })).map((match) => attachBidirectionalMatchPortability(match, {
69
+ currentSourceHash: sourceHash(source),
70
+ targetChangeSet
67
71
  }));
72
+ const targetPortability = classifyBidirectionalTargetPortability({
73
+ id,
74
+ source,
75
+ currentSourceHash: sourceHash(source),
76
+ targetChangeSet,
77
+ sourceAnchorMatches
78
+ });
68
79
  const readiness = classifyBidirectionalReadiness(targetChangeSet, source, sourceAnchorMatches);
69
80
  const reasons = uniqueStrings([
70
81
  'source-port-review-required',
71
82
  'target-change-is-merge-evidence-not-proof',
83
+ `target-portability:${targetPortability.status}`,
72
84
  ...array(targetChangeSet.reasons),
85
+ ...array(targetPortability.reasonCodes),
73
86
  ...sourceAnchorMatches.flatMap((match) => match.reasonCodes)
74
87
  ]);
75
88
  const evidence = [createBidirectionalEvidence({
@@ -78,6 +91,7 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
78
91
  source,
79
92
  targetChangeSet,
80
93
  sourceAnchorMatches,
94
+ targetPortability,
81
95
  readiness,
82
96
  reasons
83
97
  })];
@@ -97,6 +111,7 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
97
111
  targetChangeSetId: targetChangeSet.id,
98
112
  targetPatchId: targetChangeSet.patch?.id,
99
113
  targetMergeCandidateId: targetChangeSet.mergeCandidate?.id,
114
+ targetPortability,
100
115
  sourceMapBackprojection: summarizeSourceMapBackprojection(sourceAnchorMatches)
101
116
  }
102
117
  }, {
@@ -106,6 +121,7 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
106
121
  admission: { status: readiness === 'blocked' ? 'blocked' : 'needs-review', readiness },
107
122
  metadata: {
108
123
  source: 'createBidirectionalTargetChangeRecord',
124
+ targetPortability,
109
125
  autoMergeClaim: false,
110
126
  semanticEquivalenceClaim: false
111
127
  }
@@ -138,6 +154,7 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
138
154
  bidirectionalTargetChangeId: id,
139
155
  sourcePatchBundleId: sourcePatchBundle.id,
140
156
  targetChangeSetId: targetChangeSet.id,
157
+ targetPortability,
141
158
  sourceMapBackprojection: summarizeSourceMapBackprojection(sourceAnchorMatches),
142
159
  autoMergeClaim: false,
143
160
  semanticEquivalenceClaim: false
@@ -154,6 +171,7 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
154
171
  sourceImport: source,
155
172
  targetChangeSet,
156
173
  sourceAnchorMatches,
174
+ targetPortability,
157
175
  sourcePatchBundle,
158
176
  historyRecord,
159
177
  evidence,
@@ -166,12 +184,17 @@ export function createBidirectionalTargetChangeRecord(input = {}, options = {})
166
184
  unmatchedTargetRegions: sourceAnchorMatches.filter((match) => match.status === 'unmatched').length,
167
185
  deletedSourceAnchors: sourceAnchorMatches.filter((match) => match.status === 'deleted').length,
168
186
  sourceChangedRegions: sourceChangedRegions.length,
169
- sourceMapBackedMatches: sourceAnchorMatches.filter((match) => match.sourceMapLinks.length > 0).length
187
+ sourceMapBackedMatches: sourceAnchorMatches.filter((match) => match.sourceMapLinks.length > 0).length,
188
+ targetPortabilityStatus: targetPortability.status,
189
+ portableTargetRegions: targetPortability.status === 'portable' ? targetPortability.targetChangedRegions : 0,
190
+ staleTargetRegions: targetPortability.status === 'stale' ? targetPortability.targetChangedRegions : 0,
191
+ conflictingTargetRegions: targetPortability.status === 'conflict' ? targetPortability.ambiguousTargetRegions : 0
170
192
  },
171
193
  metadata: {
172
194
  autoMergeClaim: false,
173
195
  semanticEquivalenceClaim: false,
174
196
  reviewRequired: true,
197
+ targetPortability,
175
198
  ...input.metadata
176
199
  }
177
200
  };
@@ -12,13 +12,15 @@ export function projectSemanticEditScriptToSource(input = {}) {
12
12
  if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
13
13
  if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
14
14
  const edits = [];
15
- for (const operation of script.operations ?? []) {
16
- const edit = sourceEditForOperation(operation, workerSourceText, headSourceText);
15
+ for (const [index, operation] of (script.operations ?? []).entries()) {
16
+ const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index);
17
17
  if (edit.ok) edits.push(edit.value);
18
18
  else reasonCodes.push(...edit.reasonCodes);
19
19
  }
20
+ const deduped = dedupeSourceEdits(edits);
21
+ reasonCodes.push(...validateSourceEdits(deduped.edits));
20
22
  const blocked = reasonCodes.length > 0;
21
- const sourceText = blocked ? undefined : applySourceEdits(headSourceText, edits);
23
+ const sourceText = blocked ? undefined : applySourceEdits(headSourceText, deduped.edits);
22
24
  const core = {
23
25
  kind: 'frontier.lang.semanticEditProjection',
24
26
  version: 1,
@@ -31,9 +33,9 @@ export function projectSemanticEditScriptToSource(input = {}) {
31
33
  workerHash: script.workerHash,
32
34
  headHash: script.headHash,
33
35
  projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
34
- appliedOperations: blocked ? [] : edits.map((edit) => edit.operationId),
35
- skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : [],
36
- edits: blocked ? [] : edits.map(projectionEditRecord),
36
+ appliedOperations: blocked ? [] : deduped.edits.map((edit) => edit.operationId),
37
+ skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : deduped.skippedOperationIds,
38
+ edits: blocked ? [] : deduped.edits.map(projectionEditRecord),
37
39
  sourceText,
38
40
  admission: {
39
41
  status: blocked ? 'blocked' : 'auto-merge-candidate',
@@ -45,20 +47,24 @@ export function projectSemanticEditScriptToSource(input = {}) {
45
47
  autoMergeClaim: false,
46
48
  semanticEquivalenceClaim: false,
47
49
  editCount: edits.length,
48
- appliedEditCount: edits.filter((edit) => !edit.alreadyApplied).length,
49
- alreadyAppliedEditCount: edits.filter((edit) => edit.alreadyApplied).length,
50
+ appliedEditCount: deduped.edits.filter((edit) => !edit.alreadyApplied).length,
51
+ alreadyAppliedEditCount: deduped.edits.filter((edit) => edit.alreadyApplied).length,
52
+ dedupedEditCount: deduped.skippedOperationIds.length,
50
53
  ...input.metadata
51
54
  })
52
55
  };
53
56
  return { ...core, hash: hashSemanticValue(core) };
54
57
  }
55
58
 
56
- function sourceEditForOperation(operation, workerSourceText, headSourceText) {
59
+ function sourceEditForOperation(operation, workerSourceText, headSourceText, order) {
57
60
  const identity = projectionIdentity(operation);
58
61
  if (operation.status === 'already-applied') {
59
- return { ok: true, value: { ...identity, operationId: operation.id, start: 0, end: 0, replacement: '', current: '', alreadyApplied: true } };
62
+ return { ok: true, value: { ...identity, operationId: operation.id, order, start: 0, end: 0, replacement: '', current: '', alreadyApplied: true } };
60
63
  }
61
64
  if (operation.status !== 'portable') return { ok: false, reasonCodes: [`operation-not-portable:${operation.id}`] };
65
+ if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
66
+ return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
67
+ }
62
68
  const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
63
69
  const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
64
70
  const reasons = [];
@@ -79,7 +85,9 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText) {
79
85
  ok: true,
80
86
  value: {
81
87
  operationId: operation.id,
88
+ order,
82
89
  ...identity,
90
+ editKind: 'replace',
83
91
  start: headOffsets.start,
84
92
  end: headOffsets.end,
85
93
  workerStart: workerOffsets.start,
@@ -90,6 +98,37 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText) {
90
98
  };
91
99
  }
92
100
 
101
+ function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
102
+ const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
103
+ const reasons = [];
104
+ if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
105
+ const insertion = insertionOffset(headSourceText, operation.insertion);
106
+ if (!insertion.ok) reasons.push(...insertion.reasonCodes.map((reason) => `${reason}:${operation.id}`));
107
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
108
+ const spanText = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
109
+ if (operation.hashes?.workerTextHash && hashSemanticValue(spanText) !== operation.hashes.workerTextHash) {
110
+ reasons.push(`worker-span-hash-mismatch:${operation.id}`);
111
+ }
112
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
113
+ return {
114
+ ok: true,
115
+ value: {
116
+ operationId: operation.id,
117
+ order,
118
+ ...identity,
119
+ editKind: 'insert',
120
+ insertion: operation.insertion,
121
+ start: insertion.offset,
122
+ end: insertion.offset,
123
+ workerStart: workerOffsets.start,
124
+ workerEnd: workerOffsets.end,
125
+ replacement: insertionReplacement(spanText, headSourceText, insertion.offset),
126
+ replacementSpanText: spanText,
127
+ current: ''
128
+ }
129
+ };
130
+ }
131
+
93
132
  function projectionIdentity(operation) {
94
133
  const identity = semanticEditIdentity(operation);
95
134
  return { ...identity, sourcePath: operation.reanchor?.toSourcePath ?? identity.sourcePath };
@@ -103,6 +142,7 @@ function projectionEditRecord(edit) {
103
142
  operationId: edit.operationId,
104
143
  status: edit.alreadyApplied ? 'already-applied' : 'applied',
105
144
  kind: edit.kind,
145
+ editKind: edit.editKind,
106
146
  changeKind: edit.changeKind,
107
147
  anchorKey: edit.anchorKey,
108
148
  conflictKey: edit.conflictKey,
@@ -133,6 +173,11 @@ function projectionEditRecord(edit) {
133
173
  replacementBytes: edit.replacement.length,
134
174
  deletedTextHash,
135
175
  replacementTextHash,
176
+ replacementSpanTextHash: hashSemanticValue(edit.replacementSpanText ?? edit.replacement),
177
+ insertionMode: edit.insertion?.mode,
178
+ insertionAnchorKey: edit.insertion?.anchorKey,
179
+ insertionAnchorSymbolName: edit.insertion?.anchorSymbolName,
180
+ insertionAnchorSymbolKind: edit.insertion?.anchorSymbolKind,
136
181
  replacementText: edit.replacement
137
182
  });
138
183
  }
@@ -164,10 +209,42 @@ function semanticEditIdentity(operation) {
164
209
 
165
210
  function applySourceEdits(sourceText, edits) {
166
211
  return edits.filter((edit) => !edit.alreadyApplied)
167
- .sort((left, right) => right.start - left.start)
212
+ .sort(sourceEditSort)
168
213
  .reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
169
214
  }
170
215
 
216
+ function dedupeSourceEdits(edits) {
217
+ const seen = new Map();
218
+ const result = [];
219
+ const skippedOperationIds = [];
220
+ for (const edit of edits) {
221
+ const key = duplicateEditKey(edit);
222
+ if (key && seen.has(key)) {
223
+ skippedOperationIds.push(edit.operationId);
224
+ continue;
225
+ }
226
+ if (key) seen.set(key, edit.operationId);
227
+ result.push(edit);
228
+ }
229
+ return { edits: result, skippedOperationIds };
230
+ }
231
+
232
+ function duplicateEditKey(edit) {
233
+ if (edit.editKind !== 'insert') return undefined;
234
+ return [
235
+ 'insert',
236
+ edit.start,
237
+ edit.end,
238
+ edit.insertion?.mode,
239
+ edit.insertion?.anchorKey,
240
+ hashSemanticValue(edit.replacementSpanText ?? edit.replacement)
241
+ ].join(':');
242
+ }
243
+
244
+ function sourceEditSort(left, right) {
245
+ return right.start - left.start || right.end - left.end || (right.order ?? 0) - (left.order ?? 0);
246
+ }
247
+
171
248
  function projectedSourcePath(script, edits) {
172
249
  return edits.map((edit) => edit.sourcePath).find(Boolean) ?? script.sourcePath;
173
250
  }
@@ -189,6 +266,48 @@ function spanOffsets(sourceText, span) {
189
266
  return { start: start + startColumn, end: endLineStart + endColumn };
190
267
  }
191
268
 
269
+ function insertionOffset(sourceText, insertion) {
270
+ if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
271
+ const mode = insertion?.mode;
272
+ if (mode === 'file-start') return { ok: true, offset: 0 };
273
+ if (mode === 'file-end') return { ok: true, offset: sourceText.length };
274
+ const range = spanOffsets(sourceText, insertion?.headSpan);
275
+ if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
276
+ if (mode === 'before') return { ok: true, offset: range.start };
277
+ if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
278
+ return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
279
+ }
280
+
281
+ function insertionReplacement(text, sourceText, offset) {
282
+ let replacement = String(text ?? '');
283
+ if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
284
+ if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
285
+ if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
286
+ if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
287
+ return replacement;
288
+ }
289
+
290
+ function afterLineOffset(sourceText, offset) {
291
+ return sourceText[offset] === '\n' ? offset + 1 : offset;
292
+ }
293
+
294
+ function validateSourceEdits(edits) {
295
+ const reasons = [];
296
+ const ordered = edits.filter((edit) => !edit.alreadyApplied).sort((left, right) => left.start - right.start || left.end - right.end);
297
+ for (let index = 1; index < ordered.length; index += 1) {
298
+ const previous = ordered[index - 1];
299
+ const current = ordered[index];
300
+ if (editsOverlap(previous, current)) reasons.push(`source-edit-overlap:${previous.operationId}:${current.operationId}`);
301
+ }
302
+ return uniqueStrings(reasons);
303
+ }
304
+
305
+ function editsOverlap(left, right) {
306
+ if (left.start === left.end) return right.start < left.start && left.start < right.end;
307
+ if (right.start === right.end) return left.start < right.start && right.start < left.end;
308
+ return left.start < right.end && right.start < left.end;
309
+ }
310
+
192
311
  function compactRecord(value) {
193
312
  return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
194
313
  }
@@ -53,6 +53,7 @@ export function replaySemanticEditProjection(input = {}) {
53
53
  function replayProjectionEdit(edit, context) {
54
54
  if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied']);
55
55
  if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text']);
56
+ if (edit.editKind === 'insert') return replayInsertionEdit(edit, context);
56
57
  const offset = checkRange(edit, { start: edit.headStart, end: edit.headEnd }, context.currentSourceText, 'head-offset');
57
58
  if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
58
59
  const symbol = findCurrentSymbol(edit, context.currentSymbols);
@@ -64,13 +65,27 @@ function replayProjectionEdit(edit, context) {
64
65
  ]);
65
66
  }
66
67
 
68
+ function replayInsertionEdit(edit, context) {
69
+ const inserted = findCurrentSymbol(edit, context.currentSymbols);
70
+ const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
71
+ const already = checkRange(edit, insertedRange, context.currentSourceText, 'current-inserted-symbol');
72
+ if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason]);
73
+ const anchor = findInsertionAnchorSymbol(edit, context.currentSymbols);
74
+ const range = insertionRange(edit, anchor, context.currentSourceText);
75
+ if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`]);
76
+ return replayEditRecord(edit, anchor ? 'conflict' : 'stale', undefined, [
77
+ anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
78
+ ]);
79
+ }
80
+
67
81
  function checkRange(edit, range, sourceText, label) {
68
82
  if (!range || range.end < range.start) return undefined;
69
83
  const current = sourceText.slice(range.start, range.end);
70
84
  const currentHash = hashSemanticValue(current);
71
- if (edit.deletedTextHash && currentHash === edit.deletedTextHash) return { status: 'applied', range, reason: `${label}-matches-deleted` };
85
+ if (edit.replacementSpanTextHash && currentHash === edit.replacementSpanTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement-span` };
72
86
  if (edit.replacementTextHash && currentHash === edit.replacementTextHash) return { status: 'already-applied', range, reason: `${label}-matches-replacement` };
73
87
  if (current === edit.replacementText) return { status: 'already-applied', range, reason: `${label}-matches-replacement-text` };
88
+ if (edit.deletedTextHash && currentHash === edit.deletedTextHash) return { status: 'applied', range, reason: `${label}-matches-deleted` };
74
89
  return undefined;
75
90
  }
76
91
 
@@ -81,6 +96,7 @@ function replayEditRecord(edit, status, range, reasonCodes) {
81
96
  semanticIdentityHash: edit.semanticIdentityHash,
82
97
  sourceIdentityHash: edit.sourceIdentityHash,
83
98
  editContentHash: edit.editContentHash,
99
+ editKind: edit.editKind,
84
100
  sourcePath: edit.targetSourcePath ?? edit.sourcePath,
85
101
  symbolName: edit.targetSymbolName ?? edit.symbolName,
86
102
  symbolKind: edit.targetSymbolKind ?? edit.symbolKind,
@@ -115,6 +131,24 @@ function findCurrentSymbol(edit, symbols) {
115
131
  return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
116
132
  }
117
133
 
134
+ function findInsertionAnchorSymbol(edit, symbols) {
135
+ return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && key === edit.insertionAnchorKey))
136
+ ?? symbols.find((symbol) => symbol.name === edit.insertionAnchorSymbolName && (!edit.insertionAnchorSymbolKind || symbol.kind === edit.insertionAnchorSymbolKind));
137
+ }
138
+
139
+ function insertionRange(edit, anchor, sourceText) {
140
+ if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
141
+ if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
142
+ const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
143
+ if (!anchorRange) return undefined;
144
+ if (edit.insertionMode === 'before') return { start: anchorRange.start, end: anchorRange.start };
145
+ if (edit.insertionMode === 'after') {
146
+ const offset = sourceText[anchorRange.end] === '\n' ? anchorRange.end + 1 : anchorRange.end;
147
+ return { start: offset, end: offset };
148
+ }
149
+ return undefined;
150
+ }
151
+
118
152
  function replayStatus(reasonCodes, edits, projection) {
119
153
  if (reasonCodes.some((reason) => reason !== 'current-source-hash-mismatch')) return 'blocked';
120
154
  if (!edits.length && !(projection.edits ?? []).length) return 'evidence-only';
@@ -0,0 +1,108 @@
1
+ export function semanticEditInsertionAnchor(region, context) {
2
+ if (region.changeKind !== 'added') return undefined;
3
+ const workerSymbol = symbolForRegion(context.workerSymbols, region);
4
+ if (!workerSymbol?.sourceSpan) return fallbackInsertion(region, context, 'worker-symbol-span-missing');
5
+ const workers = uniqueSymbols(context.workerSymbols)
6
+ .filter((symbol) => symbol.id !== workerSymbol.id && symbol.key !== workerSymbol.key)
7
+ .filter((symbol) => hasSymbol(context.baseSymbols, symbol));
8
+ const before = nearestBefore(workers, workerSymbol);
9
+ const after = nearestAfter(workers, workerSymbol);
10
+ const anchor = before
11
+ ? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol')
12
+ : after
13
+ ? insertionFromSymbol('before', after, context, 'nearest-next-base-symbol')
14
+ : fallbackInsertion(region, context, 'no-neighbor-base-symbol');
15
+ return compactRecord({
16
+ ...anchor,
17
+ insertedSymbolId: workerSymbol.id,
18
+ insertedSymbolName: workerSymbol.name,
19
+ insertedSymbolKind: workerSymbol.kind,
20
+ insertedSourceSpan: workerSymbol.sourceSpan,
21
+ insertedSourcePath: workerSymbol.sourcePath
22
+ });
23
+ }
24
+
25
+ function insertionFromSymbol(mode, symbol, context, reasonCode) {
26
+ const headSymbol = symbolForExisting(context.headSymbols, symbol);
27
+ return compactRecord({
28
+ mode,
29
+ anchorKey: symbol.key ?? symbol.ownershipKey ?? symbol.id,
30
+ anchorSymbolId: symbol.id,
31
+ anchorSymbolName: symbol.name,
32
+ anchorSymbolKind: symbol.kind,
33
+ baseSpan: symbolForExisting(context.baseSymbols, symbol)?.sourceSpan,
34
+ workerAnchorSpan: symbol.sourceSpan,
35
+ headSpan: headSymbol?.sourceSpan,
36
+ sourcePath: headSymbol?.sourcePath ?? symbol.sourcePath,
37
+ reasonCodes: [headSymbol ? reasonCode : `${reasonCode}:head-anchor-missing`]
38
+ });
39
+ }
40
+
41
+ function fallbackInsertion(region, context, reasonCode) {
42
+ const mode = region.regionKind === 'import' ? 'file-start' : 'file-end';
43
+ return compactRecord({
44
+ mode,
45
+ sourcePath: region.sourcePath ?? context.workerChangeSet.sourcePath,
46
+ reasonCodes: [reasonCode, `fallback-${mode}`]
47
+ });
48
+ }
49
+
50
+ function nearestBefore(symbols, target) {
51
+ return symbols
52
+ .filter((symbol) => spanEndLine(symbol.sourceSpan) <= spanStartLine(target.sourceSpan))
53
+ .sort((left, right) => spanEndLine(right.sourceSpan) - spanEndLine(left.sourceSpan))[0];
54
+ }
55
+
56
+ function nearestAfter(symbols, target) {
57
+ return symbols
58
+ .filter((symbol) => spanStartLine(symbol.sourceSpan) >= spanEndLine(target.sourceSpan))
59
+ .sort((left, right) => spanStartLine(left.sourceSpan) - spanStartLine(right.sourceSpan))[0];
60
+ }
61
+
62
+ function symbolForRegion(symbols, region) {
63
+ return symbolForKeys(symbols, [region.key, region.symbolId, region.symbolName].filter(Boolean));
64
+ }
65
+
66
+ function symbolForExisting(symbols, symbol) {
67
+ return symbolForKeys(symbols, symbolKeys(symbol));
68
+ }
69
+
70
+ function hasSymbol(symbols, symbol) {
71
+ return Boolean(symbolForExisting(symbols, symbol));
72
+ }
73
+
74
+ function symbolForKeys(symbols, keys) {
75
+ for (const key of keys) {
76
+ const symbol = symbols.get(key);
77
+ if (symbol) return symbol;
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ function uniqueSymbols(symbols) {
83
+ const seen = new Set();
84
+ const result = [];
85
+ for (const symbol of symbols.values()) {
86
+ const key = symbol.id ?? `${symbol.key}:${symbol.name}`;
87
+ if (seen.has(key)) continue;
88
+ seen.add(key);
89
+ result.push(symbol);
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function symbolKeys(symbol) {
95
+ return [symbol.key, symbol.ownershipKey, symbol.id, symbol.name].filter(Boolean);
96
+ }
97
+
98
+ function spanStartLine(span) {
99
+ return typeof span?.startLine === 'number' ? span.startLine : Number.MAX_SAFE_INTEGER;
100
+ }
101
+
102
+ function spanEndLine(span) {
103
+ return typeof span?.endLine === 'number' ? span.endLine : spanStartLine(span);
104
+ }
105
+
106
+ function compactRecord(value) {
107
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
108
+ }
@@ -12,6 +12,7 @@ import {
12
12
  semanticEditAdmission,
13
13
  summarizeSemanticEditOperations
14
14
  } from './semanticEditScriptClassification.js';
15
+ import { semanticEditInsertionAnchor } from './semanticEditInsertionAnchors.js';
15
16
  import { sourceTextForSpan } from './sourceTextForSpan.js';
16
17
  import { semanticEditIdentityFields, semanticEditOperationContentHash } from './semanticEditIdentityRecords.js';
17
18
 
@@ -168,10 +169,11 @@ function semanticEditOperation(region, index, context, input) {
168
169
  const identityRecord = semanticEditIdentityRecord({ kind, region, anchor });
169
170
  const identity = semanticEditIdentityFields(identityRecord);
170
171
  return compactRecord({
171
- id: `semantic_edit_op_${idFragment(firstString(input.id, anchorKey, index))}`,
172
+ id: `semantic_edit_op_${idFragment([input.id ?? 'semantic_edit', anchorKey, index].join(':'))}`,
172
173
  kind,
173
174
  changeKind: region.changeKind,
174
175
  anchor,
176
+ insertion: semanticEditInsertionAnchor(region, context),
175
177
  ...identity,
176
178
  spans: compactRecord({
177
179
  base: baseSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.before?.sourceSpan,
@@ -16,6 +16,7 @@ export function createSemanticPatchBundleRecord(input={},options={}){
16
16
  const transformContext={sourceLanguage:options.sourceLanguage??source.sourceLanguage??source.language,targetLanguage:options.targetLanguage??source.targetLanguage};
17
17
  const semanticTransformIdentities=normalizeSemanticTransformIdentityRecords(semanticTransformInputs(source,options),transformContext);
18
18
  const semanticTransformIndex=semanticTransformRecordIndex(semanticTransformIdentities,source);
19
+ const targetPortability=options.targetPortability??source.targetPortability??source.metadata?.targetPortability??options.metadata?.targetPortability;
19
20
  const regionInputs=array(options.changedRegions??source.changedRegions??source.regions);
20
21
  const sourceMapLinks=normalizeSourceMapLinks([
21
22
  ...array(options.sourceMapLinks??source.sourceMapLinks),
@@ -50,7 +51,7 @@ export function createSemanticPatchBundleRecord(input={},options={}){
50
51
  ??`semantic_patch_bundle_${idFragment(firstString(source.id,patchId,mergeCandidateId,source.sourcePath,source.language,'record'))}`;
51
52
  const language=options.language??source.language??mergeCandidate?.language??sources.find((item)=>item.language)?.language;
52
53
  const sourcePath=options.sourcePath??source.sourcePath??mergeCandidate?.sourcePath??sources.find((item)=>item.sourcePath)?.sourcePath;
53
- const index=recordIndex({baseHash,targetHash,sources,changedRegions,sourceMapLinks,evidenceIds,proofIds,historyIds,semanticOperationIds,patchId,mergeCandidateId,admission,semanticEditIndex,semanticTransformIndex});
54
+ const index=recordIndex({baseHash,targetHash,sources,changedRegions,sourceMapLinks,evidenceIds,proofIds,historyIds,semanticOperationIds,patchId,mergeCandidateId,admission,semanticEditIndex,semanticTransformIndex,targetPortability});
54
55
  return{
55
56
  kind:'frontier.lang.semanticPatchBundleRecord',
56
57
  version:1,
@@ -85,6 +86,7 @@ export function createSemanticPatchBundleRecord(input={},options={}){
85
86
  semanticMergeConflictSummary:source.metadata?.semanticMergeConflictSummary,
86
87
  semanticEditSummary:semanticEditSummary(semanticEditIndex),
87
88
  semanticTransformSummary:semanticTransformSummary(semanticTransformIndex),
89
+ targetPortability,
88
90
  ...options.metadata
89
91
  })
90
92
  };
@@ -154,11 +156,12 @@ function normalizeRegions(regions,context){
154
156
  sourceMapIds:uniqueStrings([...strings(region.sourceMapIds),...links.map((link)=>link.sourceMapId)]),
155
157
  sourceMapMappingIds:uniqueStrings([...strings(region.sourceMapMappingIds),...links.map((link)=>link.sourceMapMappingId)]),
156
158
  admission:compactRecord({
157
- readiness:projection?.admission?.readiness,
158
- action:projection?.admission?.action,
159
- reasonCodes:uniqueStrings(projection?.admission?.reasons),
160
- conflictKeys:uniqueStrings(projection?.admission?.conflictKeys)
161
- })
159
+ readiness:projection?.admission?.readiness??region.admission?.readiness,
160
+ action:projection?.admission?.action??region.admission?.action,
161
+ reasonCodes:uniqueStrings([...strings(region.admission?.reasonCodes),...strings(projection?.admission?.reasons)]),
162
+ conflictKeys:uniqueStrings([...strings(region.admission?.conflictKeys),...strings(projection?.admission?.conflictKeys)])
163
+ }),
164
+ metadata:region.metadata
162
165
  });
163
166
  });
164
167
  }
@@ -229,6 +232,9 @@ function recordIndex(parts){
229
232
  transformTargetLanguages:semanticTransformIndex.transformTargetLanguages,
230
233
  transformSourcePaths:semanticTransformIndex.transformSourcePaths,
231
234
  transformTargetPaths:semanticTransformIndex.transformTargetPaths,
235
+ targetPortabilityStatuses:uniqueStrings([parts.targetPortability?.status]),
236
+ targetPortabilityActions:uniqueStrings([parts.targetPortability?.action]),
237
+ targetPortabilityReasonCodes:uniqueStrings(parts.targetPortability?.reasonCodes),
232
238
  patchIds:uniqueStrings([parts.patchId]),
233
239
  mergeCandidateIds:uniqueStrings([parts.mergeCandidateId]),
234
240
  readinesses:uniqueStrings([parts.admission.readiness,...parts.changedRegions.map((region)=>region.admission?.readiness)]),
@@ -237,7 +243,7 @@ function recordIndex(parts){
237
243
  }
238
244
 
239
245
  function matchesRecord(record,query){
240
- const index=record.index??recordIndex({...record,baseHash:record.baseHash,targetHash:record.targetHash,sources:record.sources??[],changedRegions:record.changedRegions??[],sourceMapLinks:record.sourceMapLinks??[],evidenceIds:record.evidenceIds??[],proofIds:record.proofIds??[],historyIds:record.historyIds??[],semanticOperationIds:record.semanticOperationIds??[],patchId:record.patchId,mergeCandidateId:record.mergeCandidateId,admission:record.admission??{}});
246
+ const index=record.index??recordIndex({...record,baseHash:record.baseHash,targetHash:record.targetHash,sources:record.sources??[],changedRegions:record.changedRegions??[],sourceMapLinks:record.sourceMapLinks??[],evidenceIds:record.evidenceIds??[],proofIds:record.proofIds??[],historyIds:record.historyIds??[],semanticOperationIds:record.semanticOperationIds??[],patchId:record.patchId,mergeCandidateId:record.mergeCandidateId,admission:record.admission??{},targetPortability:record.metadata?.targetPortability});
241
247
  return matchAny(queryValues(query.id,query.ids),[record.id])
242
248
  &&matchAny(queryValues(query.patchId,query.patchIds),index.patchIds)
243
249
  &&matchAny(queryValues(query.mergeCandidateId,query.mergeCandidateIds),index.mergeCandidateIds)
@@ -278,6 +284,9 @@ function matchesRecord(record,query){
278
284
  &&matchAny(queryValues(query.transformTargetLanguage,query.transformTargetLanguages),index.transformTargetLanguages)
279
285
  &&matchAny(queryValues(query.transformSourcePath,query.transformSourcePaths),index.transformSourcePaths)
280
286
  &&matchAny(queryValues(query.transformTargetPath,query.transformTargetPaths),index.transformTargetPaths)
287
+ &&matchAny(queryValues(query.targetPortabilityStatus,query.targetPortabilityStatuses),index.targetPortabilityStatuses)
288
+ &&matchAny(queryValues(query.targetPortabilityAction,query.targetPortabilityActions),index.targetPortabilityActions)
289
+ &&matchAny(queryValues(query.targetPortabilityReasonCode,query.targetPortabilityReasonCodes),index.targetPortabilityReasonCodes)
281
290
  &&matchAny(queryValues(query.readiness,query.readinesses),index.readinesses)
282
291
  &&matchAny(queryValues(query.admissionStatus,query.admissionStatuses),index.admissionStatuses);
283
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.94",
3
+ "version": "0.2.96",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",