@shapeshift-labs/frontier-lang-compiler 0.2.100 → 0.2.102

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.
Files changed (47) hide show
  1. package/dist/declarations/bidirectional-target-change-evidence.d.ts +299 -0
  2. package/dist/declarations/bidirectional-target-change.d.ts +19 -120
  3. package/dist/declarations/import-adapter-core.d.ts +6 -0
  4. package/dist/declarations/native-project-admission.d.ts +43 -22
  5. package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
  6. package/dist/declarations/semantic-edit-script.d.ts +20 -15
  7. package/dist/declarations/semantic-lineage.d.ts +3 -21
  8. package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
  9. package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
  10. package/dist/declarations/semantic-sidecar.d.ts +12 -14
  11. package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
  12. package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
  13. package/dist/internal/index-impl/createLightweightNativeImport.js +9 -1
  14. package/dist/internal/index-impl/createNativeSourcePreservation.js +16 -1
  15. package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +151 -1
  16. package/dist/internal/index-impl/createSemanticImportSidecar.js +5 -0
  17. package/dist/internal/index-impl/createSemanticImportSidecarAdmission.js +29 -11
  18. package/dist/internal/index-impl/importNativeSource.js +14 -14
  19. package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
  20. package/dist/internal/index-impl/nativeImportSemanticIndex.js +33 -0
  21. package/dist/internal/index-impl/projectImportAdmissionMergeScore.js +26 -74
  22. package/dist/internal/index-impl/projectImportAdmissionProjectionCoverage.js +74 -0
  23. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +39 -13
  24. package/dist/internal/index-impl/replaySemanticEditProjection.js +65 -23
  25. package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
  26. package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
  27. package/dist/internal/index-impl/semanticEditSourceRanges.js +94 -15
  28. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +21 -2
  29. package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
  30. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +8 -0
  31. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +18 -1
  32. package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
  33. package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
  34. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +23 -1
  35. package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
  36. package/dist/native-import-language-profiles.js +10 -2
  37. package/dist/native-region-scanner-js-helpers.js +8 -2
  38. package/dist/native-region-scanner-js-imports.js +7 -0
  39. package/dist/native-region-scanner-js.js +4 -4
  40. package/dist/native-region-scanner.js +2 -1
  41. package/dist/semantic-import-regions.js +18 -5
  42. package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
  43. package/dist/semantic-import-sidecar-entry.js +151 -7
  44. package/dist/semantic-import-sidecar-types.d.ts +18 -13
  45. package/dist/semantic-import-source-preservation-utils.js +55 -0
  46. package/dist/semantic-import-source-preservation.js +98 -3
  47. package/package.json +1 -1
@@ -24,7 +24,7 @@ export function spanOffsets(sourceText, span) {
24
24
  const endLineStart = lineStarts[endLine - 1];
25
25
  if (start === undefined || endLineStart === undefined) return undefined;
26
26
  const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
27
- const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
27
+ const lineEnd = lineContentEndOffset(sourceText, lineStarts[endLine]);
28
28
  const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
29
29
  return { start: start + startColumn, end: endLineStart + endColumn };
30
30
  }
@@ -49,36 +49,41 @@ export function bodyContentRange(sourceText, range) {
49
49
  return pair ? { start: pair.open + 1, end: pair.close } : undefined;
50
50
  }
51
51
 
52
- export function insertionOffset(sourceText, insertion) {
52
+ export function insertionOffset(sourceText, insertion, context = {}) {
53
53
  if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
54
54
  const mode = insertion?.mode;
55
55
  if (mode === 'file-start') return { ok: true, offset: 0 };
56
56
  if (mode === 'file-end') return { ok: true, offset: sourceText.length };
57
- const range = spanOffsets(sourceText, insertion?.headSpan);
58
- if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
59
- if (mode === 'before') return { ok: true, offset: range.start };
60
- if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
57
+ const resolved = insertionAnchorResolution(sourceText, insertion, context);
58
+ if (!resolved?.range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
59
+ if (resolved.mode === 'before') return { ok: true, offset: resolved.range.start };
60
+ if (resolved.mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, resolved.range.end) };
61
61
  return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
62
62
  }
63
63
 
64
64
  export function removalRange(sourceText, span) {
65
65
  const range = { ...span };
66
- if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
67
- else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
66
+ const next = lineBreakEndOffset(sourceText, range.end);
67
+ if (next !== range.end) range.end = next;
68
+ else {
69
+ const previous = previousLineBreakStartOffset(sourceText, range.start);
70
+ if (previous !== range.start) range.start = previous;
71
+ }
68
72
  return range;
69
73
  }
70
74
 
71
75
  export function insertionReplacement(text, sourceText, offset) {
72
76
  let replacement = String(text ?? '');
73
- if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
74
- if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
75
- if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
76
- if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
77
+ const newline = sourceLineEnding(sourceText);
78
+ if (offset > 0 && !isLineBreak(sourceText[offset - 1])) replacement = `${newline}${replacement}`;
79
+ if (offset < sourceText.length && !endsWithLineBreak(replacement)) replacement += newline;
80
+ if (offset === sourceText.length && sourceText && !endsWithLineBreak(sourceText)) replacement = `${newline}${replacement}`;
81
+ if (offset === sourceText.length && !endsWithLineBreak(replacement)) replacement += newline;
77
82
  return replacement;
78
83
  }
79
84
 
80
85
  export function afterLineOffset(sourceText, offset) {
81
- return sourceText[offset] === '\n' ? offset + 1 : offset;
86
+ return lineBreakEndOffset(sourceText, offset);
82
87
  }
83
88
 
84
89
  function isProjectionCoverableContainer(operation) {
@@ -112,15 +117,89 @@ function containedRange(inner, outer) {
112
117
 
113
118
  function insertionRemovalRange(sourceText, span, container) {
114
119
  const range = { ...span };
115
- if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
116
- else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
120
+ const next = lineBreakEndOffset(sourceText, range.end);
121
+ if (next !== range.end && next <= container.end) range.end = next;
122
+ else {
123
+ const previous = previousLineBreakStartOffset(sourceText, range.start);
124
+ if (previous !== range.start && previous >= container.start) range.start = previous;
125
+ }
117
126
  return range;
118
127
  }
119
128
 
129
+ function lineContentEndOffset(sourceText, nextLineStart) {
130
+ if (nextLineStart === undefined) return sourceText.length;
131
+ const lineBreakStart = sourceText[nextLineStart - 2] === '\r' ? nextLineStart - 2 : nextLineStart - 1;
132
+ return Math.max(0, lineBreakStart);
133
+ }
134
+
135
+ function lineBreakEndOffset(sourceText, offset) {
136
+ if (sourceText[offset] === '\r' && sourceText[offset + 1] === '\n') return offset + 2;
137
+ if (isLineBreak(sourceText[offset])) return offset + 1;
138
+ return offset;
139
+ }
140
+
141
+ function previousLineBreakStartOffset(sourceText, offset) {
142
+ if (sourceText[offset - 1] === '\n') return sourceText[offset - 2] === '\r' ? offset - 2 : offset - 1;
143
+ if (sourceText[offset - 1] === '\r') return offset - 1;
144
+ return offset;
145
+ }
146
+
147
+ function sourceLineEnding(sourceText) {
148
+ if (sourceText.includes('\r\n')) return '\r\n';
149
+ return sourceText.includes('\r') ? '\r' : '\n';
150
+ }
151
+
152
+ function endsWithLineBreak(value) {
153
+ return isLineBreak(value[value.length - 1]);
154
+ }
155
+
156
+ function isLineBreak(char) {
157
+ return char === '\n' || char === '\r';
158
+ }
159
+
120
160
  function isBodyReplacement(operation) {
121
161
  return operation.changeKind === 'modified' && (operation.kind === 'replaceBody' || operation.anchor?.regionKind === 'body');
122
162
  }
123
163
 
164
+ function insertionAnchorResolution(sourceText, insertion, context) {
165
+ const candidates = insertionAnchorCandidates(insertion);
166
+ for (const candidate of candidates) {
167
+ const symbol = insertionAnchorSymbol(candidate, context.symbols);
168
+ const range = spanOffsets(sourceText, symbol?.sourceSpan);
169
+ if (range) return { mode: candidate.mode, range };
170
+ }
171
+ for (const candidate of candidates) {
172
+ const range = spanOffsets(sourceText, candidate.headSpan);
173
+ if (range) return { mode: candidate.mode, range };
174
+ }
175
+ return undefined;
176
+ }
177
+
178
+ function insertionAnchorCandidates(insertion) {
179
+ const seen = new Set();
180
+ const result = [];
181
+ for (const candidate of [insertion, ...(Array.isArray(insertion?.anchorCandidates) ? insertion.anchorCandidates : [])]) {
182
+ if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
183
+ const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
184
+ if (seen.has(key)) continue;
185
+ seen.add(key);
186
+ result.push(candidate);
187
+ }
188
+ return result;
189
+ }
190
+
191
+ function insertionAnchorSymbol(candidate, symbols) {
192
+ const symbolList = Array.isArray(symbols)
193
+ ? symbols
194
+ : symbols?.values
195
+ ? [...symbols.values()]
196
+ : [];
197
+ const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
198
+ const exact = symbolList.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)));
199
+ if (exact) return exact;
200
+ return symbolList.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
201
+ }
202
+
124
203
  function trailingBodyCloseOffset(sourceText, range) {
125
204
  if (typeof sourceText !== 'string' || !range) return undefined;
126
205
  let index = range.end - 1;
@@ -137,12 +137,17 @@ function resolveIndexKeys(values, resolutions, options) {
137
137
  const key = String(value);
138
138
  const resolution = resolutions.get(key);
139
139
  if (!resolution) return [key];
140
+ const current = resolution.currentAnchors.map((anchor) => anchor.key).filter(Boolean);
140
141
  if (resolution.status === 'deleted' && options.keepDeletedAnchors !== true) return [];
141
142
  if (resolution.status === 'cycle' && options.keepBlockedAnchors !== true) return [];
142
143
  if (resolution.status === 'max-depth' && options.keepBlockedAnchors !== true) return [];
143
144
  if (resolution.status === 'not-found' && options.keepUnresolvedAnchors !== true) return [];
145
+ if (resolution.status === 'ambiguous' && resolutionHasInactiveTerminal(resolution)) {
146
+ if (options.keepCandidateAnchors === true) return current.length ? current : [key];
147
+ if (options.keepDeletedAnchors === true || options.keepInactiveAnchors === true) return [key];
148
+ return [];
149
+ }
144
150
  if (resolution.status === 'ambiguous' && options.keepCandidateAnchors === false) return [];
145
- const current = resolution.currentAnchors.map((anchor) => anchor.key).filter(Boolean);
146
151
  return current.length ? current : [key];
147
152
  }));
148
153
  }
@@ -193,7 +198,10 @@ function createAnchorInventory(resolutions) {
193
198
  const current = resolution.currentAnchors.map((anchor) => anchorEntry(anchor, resolution));
194
199
  if (resolution.status === 'ambiguous') {
195
200
  inventory.candidate.push(...current);
196
- if (start) inventory.inactive.push(start);
201
+ if (start) {
202
+ inventory.inactive.push(start);
203
+ if (resolutionHasDeletedTerminal(resolution)) inventory.deleted.push(start);
204
+ }
197
205
  continue;
198
206
  }
199
207
  if (resolution.status === 'deleted') {
@@ -254,6 +262,17 @@ function anchorEntry(anchor, resolution) {
254
262
  });
255
263
  }
256
264
 
265
+ function resolutionHasInactiveTerminal(resolution) {
266
+ return array(resolution.terminalEventIds).length > 0
267
+ || resolutionHasDeletedTerminal(resolution)
268
+ || array(resolution.reasonCodes).some((code) => code === 'lineage-event-without-target-anchor' || code === 'inactive-anchor-has-active-candidates');
269
+ }
270
+
271
+ function resolutionHasDeletedTerminal(resolution) {
272
+ return array(resolution.lineageEventKinds).includes('deleted')
273
+ || array(resolution.reasonCodes).includes('anchor-deleted');
274
+ }
275
+
257
276
  function queryAnchor(resolution) {
258
277
  return compactRecord({
259
278
  key: resolution.query?.anchorKey,
@@ -0,0 +1,97 @@
1
+ import { uniqueStrings } from '../../native-import-utils.js';
2
+
3
+ export function addIdentityHashEvidence(before, after, add, note) {
4
+ const matches = matchingIdentityHashReasons(before, after);
5
+ if (matches.length === 0) return;
6
+ if (!compatibleLineageSurface(before, after)) {
7
+ note('identity-hash-match-surface-mismatch');
8
+ return;
9
+ }
10
+ const primary = matches.includes('semantic-identity-hash-match')
11
+ ? 'semantic-identity-hash-match'
12
+ : matches.includes('source-identity-hash-match')
13
+ ? 'source-identity-hash-match'
14
+ : 'identity-hash-match';
15
+ add(0.62, primary);
16
+ for (const reason of matches) {
17
+ if (reason !== primary) note(reason);
18
+ }
19
+ }
20
+
21
+ export function addSourceHashEvidence(before, after, add, note, reasons, sameSymbolSurface) {
22
+ const beforeHash = firstString(before.anchor.sourceHash, before.sourceHash);
23
+ const afterHash = firstString(after.anchor.sourceHash, after.sourceHash);
24
+ if (!beforeHash || !afterHash) return;
25
+ if (beforeHash !== afterHash) {
26
+ note('source-hash-changed');
27
+ return;
28
+ }
29
+ if (!hasSourceHashSupport(before, after, reasons, sameSymbolSurface)) {
30
+ note('source-hash-match-without-lineage-support');
31
+ return;
32
+ }
33
+ add(0.04, 'source-hash-match');
34
+ if (before.anchor.sourcePath && after.anchor.sourcePath && before.anchor.sourcePath !== after.anchor.sourcePath) {
35
+ note('source-hash-preserved-across-path');
36
+ }
37
+ }
38
+
39
+ export function hashEvidenceSummary(reasons) {
40
+ return {
41
+ semanticIdentityHashMatch: reasons.includes('semantic-identity-hash-match'),
42
+ sourceIdentityHashMatch: reasons.includes('source-identity-hash-match'),
43
+ identityHashMatch: reasons.includes('identity-hash-match'),
44
+ sourceHashMatch: reasons.includes('source-hash-match'),
45
+ signatureHashMatch: reasons.includes('signature-hash-match'),
46
+ bodyHashMatch: reasons.includes('body-hash-match')
47
+ };
48
+ }
49
+
50
+ function matchingIdentityHashReasons(before, after) {
51
+ const beforeHashes = identityHashEntries(before);
52
+ const afterHashes = new Map(identityHashEntries(after).map((entry) => [entry.value, entry.reason]));
53
+ const reasons = [];
54
+ for (const entry of beforeHashes) {
55
+ const afterReason = afterHashes.get(entry.value);
56
+ if (!afterReason) continue;
57
+ reasons.push(identityHashMatchReason(entry.reason, afterReason));
58
+ }
59
+ return uniqueStrings(reasons);
60
+ }
61
+
62
+ function identityHashEntries(symbol) {
63
+ return [
64
+ { reason: 'semantic-identity-hash-match', value: firstString(symbol.semanticIdentityHash, symbol.anchor.metadata?.semanticIdentityHash) },
65
+ { reason: 'source-identity-hash-match', value: firstString(symbol.sourceIdentityHash, symbol.anchor.metadata?.sourceIdentityHash) },
66
+ { reason: 'identity-hash-match', value: firstString(symbol.identityHash, symbol.anchor.metadata?.identityHash) }
67
+ ].filter((entry) => entry.value);
68
+ }
69
+
70
+ function identityHashMatchReason(beforeReason, afterReason) {
71
+ if (beforeReason === afterReason) return beforeReason;
72
+ if (beforeReason === 'semantic-identity-hash-match' || afterReason === 'semantic-identity-hash-match') return 'semantic-identity-hash-match';
73
+ if (beforeReason === 'source-identity-hash-match' || afterReason === 'source-identity-hash-match') return 'source-identity-hash-match';
74
+ return 'identity-hash-match';
75
+ }
76
+
77
+ function compatibleLineageSurface(before, after) {
78
+ return (!before.language || !after.language || before.language === after.language)
79
+ && (!before.kind || !after.kind || before.kind === after.kind)
80
+ && (!before.anchor.kind || !after.anchor.kind || before.anchor.kind === after.anchor.kind);
81
+ }
82
+
83
+ function hasSourceHashSupport(before, after, reasons, sameSymbolSurface) {
84
+ return reasons.some((reason) => [
85
+ 'semantic-identity-hash-match',
86
+ 'source-identity-hash-match',
87
+ 'identity-hash-match',
88
+ 'signature-hash-match',
89
+ 'body-hash-match',
90
+ 'symbol-name-match'
91
+ ].includes(reason))
92
+ || sameSymbolSurface(before, after);
93
+ }
94
+
95
+ function firstString(...values) {
96
+ return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
97
+ }
@@ -1,4 +1,5 @@
1
1
  import { idFragment, uniqueStrings } from '../../native-import-utils.js';
2
+ import { addIdentityHashEvidence, addSourceHashEvidence, hashEvidenceSummary } from './semanticLineageHashEvidence.js';
2
3
  import { createSemanticLineageEvent } from './semanticLineageRecords.js';
3
4
 
4
5
  export function matchExactAnchors(beforeSymbols, afterSymbols) {
@@ -88,6 +89,9 @@ export function symbolSummary(symbol) {
88
89
  language: symbol.language,
89
90
  sourcePath: symbol.anchor.sourcePath,
90
91
  sourceHash: symbol.anchor.sourceHash,
92
+ identityHash: firstString(symbol.identityHash, symbol.anchor.metadata?.identityHash),
93
+ semanticIdentityHash: firstString(symbol.semanticIdentityHash, symbol.anchor.metadata?.semanticIdentityHash),
94
+ sourceIdentityHash: firstString(symbol.sourceIdentityHash, symbol.anchor.metadata?.sourceIdentityHash),
91
95
  sourceSpan: symbol.anchor.sourceSpan,
92
96
  signatureHash: symbol.signatureHash,
93
97
  bodyHash: symbol.spanHash,
@@ -144,6 +148,7 @@ function inferredEvent(before, after, score, input) {
144
148
  inferred: true,
145
149
  algorithm: 'frontier.semantic-lineage-inference.v1',
146
150
  reasonCodes: score.reasons,
151
+ hashEvidence: hashEvidenceSummary(score.reasons),
147
152
  moved: pathChanged || spanMoved,
148
153
  renamed: nameChanged,
149
154
  recreated,
@@ -190,6 +195,7 @@ function inferredSplitEvent(before, candidates, input) {
190
195
  inferred: true,
191
196
  algorithm: 'frontier.semantic-lineage-inference.v1',
192
197
  reasonCodes: reasons,
198
+ hashEvidence: hashEvidenceSummary(reasons),
193
199
  split: true,
194
200
  targetCount: targets.length,
195
201
  candidateConfidences: candidates.map((candidate) => candidate.score.confidence),
@@ -210,10 +216,12 @@ function scoreLineagePair(before, after) {
210
216
  if (before.anchor.key && before.anchor.key === after.anchor.key) add(0.4, 'anchor-key-match');
211
217
  if (before.name && before.name === after.name) add(0.28, 'symbol-name-match');
212
218
  if (before.kind && before.kind === after.kind) add(0.12, 'symbol-kind-match');
219
+ addIdentityHashEvidence(before, after, add, note);
213
220
  if (before.signatureHash && before.signatureHash === after.signatureHash) add(0.52, 'signature-hash-match');
214
221
  if (before.spanHash && before.spanHash === after.spanHash) add(0.22, 'body-hash-match');
215
222
  if (before.anchor.kind && before.anchor.kind === after.anchor.kind) add(0.06, 'anchor-kind-match');
216
223
  if (before.anchor.sourcePath && before.anchor.sourcePath === after.anchor.sourcePath) add(0.04, 'source-path-match');
224
+ addSourceHashEvidence(before, after, add, note, reasons, sameSymbolSurface);
217
225
  if (sourceSpanRangeSame(before.anchor.sourceSpan, after.anchor.sourceSpan)) add(0.18, 'source-span-range-match');
218
226
  if (before.ownershipRegionKind && before.ownershipRegionKind === after.ownershipRegionKind) add(0.04, 'ownership-kind-match');
219
227
  if (before.nativeAstNodeId && before.nativeAstNodeId === after.nativeAstNodeId) add(0.06, 'native-node-id-match');
@@ -36,7 +36,12 @@ export function resolveSemanticLineage(eventsOrMap = [], query = {}, options = {
36
36
  state.status = 'max-depth';
37
37
  state.reasonCodes.push('max-depth');
38
38
  }
39
- if (!state.cycle && !state.maxDepthHit) state.status = classifyResolutionStatus(state);
39
+ if (!state.cycle && !state.maxDepthHit) {
40
+ state.status = classifyResolutionStatus(state);
41
+ if (state.status === 'ambiguous' && state.terminal.length > 0 && state.current.length > 0) {
42
+ state.reasonCodes.push('inactive-anchor-has-active-candidates');
43
+ }
44
+ }
40
45
  return buildResolutionRecord(state, start, maxDepth, resolutionQuery, options);
41
46
  }
42
47
 
@@ -105,6 +110,7 @@ function applyLineageEvent(state, event, visitedEvents) {
105
110
  state.operationIds.push(event.crdt?.operationId);
106
111
  state.heads.push(...(event.crdt?.heads ?? []));
107
112
  state.eventKinds.push(event.eventKind);
113
+ state.reasonCodes.push(...lineageEventReasonCodes(event));
108
114
  state.sourcePaths.push(event.from?.sourcePath, ...event.to.map((anchor) => anchor.sourcePath));
109
115
  if (event.confidence !== undefined) state.confidence = state.confidence === undefined ? event.confidence : Math.min(state.confidence, event.confidence);
110
116
  const matched = state.current.filter((anchor) => anchorsMatch(anchor, event.from));
@@ -142,6 +148,7 @@ function nextLineageEvents(anchors, byFromKey, byFromId) {
142
148
 
143
149
  function classifyResolutionStatus(state) {
144
150
  if (state.current.length === 0 && state.traversed.length > 0) return 'deleted';
151
+ if (state.terminal.length > 0 && state.current.length > 0) return 'ambiguous';
145
152
  if (state.eventKinds.includes('recreated')) return 'recreated';
146
153
  if (state.current.length > 1 || state.eventKinds.includes('split') || state.eventKinds.includes('merged')) return 'ambiguous';
147
154
  if (state.traversed.length > 0) return 'resolved';
@@ -205,6 +212,16 @@ function lineageSourcePaths(state, start, query, anchors = []) {
205
212
  ...array(anchors).flatMap((anchor) => anchor?.lineageSourcePaths ?? [])
206
213
  ]);
207
214
  }
215
+ function lineageEventReasonCodes(event) {
216
+ return uniqueStrings([
217
+ ...array(event.reasonCodes),
218
+ ...array(event.metadata?.reasonCodes),
219
+ event.evidence?.signatureHashMatch ? 'signature-hash-match' : undefined,
220
+ event.evidence?.bodyHashMatch ? 'body-hash-match' : undefined,
221
+ event.evidence?.pathMatch ? 'source-path-match' : undefined,
222
+ event.evidence?.sourceSpanMoved ? 'source-span-moved' : undefined
223
+ ]);
224
+ }
208
225
  function positiveInteger(value, fallback) {
209
226
  const number = Number(value);
210
227
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
@@ -1,6 +1,7 @@
1
1
  import{idFragment,normalizeSemanticMergeReadiness,uniqueStrings}from'../../native-import-utils.js';
2
2
  import{semanticMergeConflictRiskScore}from'./semanticMergeConflicts.js';
3
3
  import{inferProjectionRisk,internalOverlaps,normalizeChangedSemanticRegions,normalizeProjectionRisk,projectionRiskRank,queryAdmissionOverlaps,summarizeOverlaps}from'./semanticMergeCandidateRecordInternals.js';
4
+ import{semanticMergeCandidateScoreFacets}from'./semanticMergeCandidateScoreFacets.js';
4
5
 
5
6
  export const SemanticMergeCandidateProjectionRisks=Object.freeze(['low','medium','high','unknown']);
6
7
 
@@ -74,6 +75,21 @@ export function createSemanticMergeCandidateAdmissionRecord(input={},options={})
74
75
  const projectionRisk=normalizeProjectionRisk(options.projectionRisk??candidate.projectionRisk??candidate.risk)
75
76
  ??inferProjectionRisk({readiness,candidate,patch,changedSemanticRegions,conflictKeys});
76
77
  const overlaps=internalOverlaps(changedSemanticRegions);
78
+ const conflictRiskScore=semanticMergeConflictRiskScore(candidate);
79
+ const scoreFacets=semanticMergeCandidateScoreFacets({
80
+ source,
81
+ candidate,
82
+ patch,
83
+ readiness,
84
+ projectionRisk,
85
+ evidenceRecords,
86
+ evidenceIds,
87
+ proofIds,
88
+ changedSemanticRegions,
89
+ conflictKeys,
90
+ overlaps,
91
+ conflictRiskScore
92
+ });
77
93
  const readinessSortKey=semanticMergeCandidateReadinessSortKey({
78
94
  readiness,
79
95
  projectionRisk,
@@ -106,11 +122,13 @@ export function createSemanticMergeCandidateAdmissionRecord(input={},options={})
106
122
  evidenceIds,
107
123
  proofIds,
108
124
  overlapSummary:summarizeOverlaps(overlaps),
125
+ scoreFacets,
109
126
  admission:{
110
127
  readiness,
111
128
  reviewRequired:readiness!=='ready'||projectionRisk!=='low'||overlaps.length>0,
112
129
  action:admissionAction({readiness,projectionRisk,overlaps}),
113
130
  sortKey:readinessSortKey,
131
+ scoreFacets,
114
132
  reasonCodes:uniqueStrings([
115
133
  ...strings(options.reasonCodes),
116
134
  ...strings(source.reasons),
@@ -145,11 +163,12 @@ export function createSemanticMergeCandidateAdmissionRecord(input={},options={})
145
163
  overlaps:overlaps.length,
146
164
  readiness,
147
165
  projectionRisk,
148
- reviewRequired:readiness!=='ready'||projectionRisk!=='low'||overlaps.length>0
166
+ reviewRequired:readiness!=='ready'||projectionRisk!=='low'||overlaps.length>0,
167
+ scoreFacets:scoreFacets.summary
149
168
  },
150
169
  metadata:compactRecord({
151
170
  sourceChangeSetId:source.kind==='frontier.lang.nativeSourceChangeSet'?source.id:undefined,
152
- conflictRiskScore:semanticMergeConflictRiskScore(candidate),
171
+ conflictRiskScore,
153
172
  conflictSummary:candidate.conflictSummary??candidate.metadata?.conflictSummary??source.metadata?.semanticMergeConflictSummary,
154
173
  changedRegionProjectionSummary:source.metadata?.changedRegionProjectionSummary??candidate.metadata?.changedRegionProjectionSummary,
155
174
  compact:true,
@@ -170,6 +189,7 @@ export function decorateSemanticMergeCandidateForAdmission(input={},options={}){
170
189
  proofIds:admissionRecord.proofIds,
171
190
  projectionRisk:admissionRecord.projectionRisk,
172
191
  readinessSortKey:admissionRecord.readinessSortKey,
192
+ scoreFacets:admissionRecord.scoreFacets,
173
193
  mergeAdmission:admissionRecord
174
194
  };
175
195
  }