@shapeshift-labs/frontier-lang-compiler 0.2.99 → 0.2.101

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 (57) 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/native-project-admission.d.ts +43 -22
  4. package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
  5. package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
  6. package/dist/declarations/semantic-edit-script.d.ts +53 -51
  7. package/dist/declarations/semantic-lineage.d.ts +62 -51
  8. package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
  9. package/dist/declarations/semantic-patch-bundle.d.ts +13 -0
  10. package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
  11. package/dist/declarations/semantic-sidecar.d.ts +12 -14
  12. package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
  13. package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
  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/declarationRecord.js +2 -2
  19. package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
  20. package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
  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 +92 -74
  24. package/dist/internal/index-impl/replaySemanticEditProjection.js +114 -40
  25. package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
  26. package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
  27. package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
  28. package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
  29. package/dist/internal/index-impl/semanticEditSourceRanges.js +283 -0
  30. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +56 -3
  31. package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
  32. package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
  33. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +158 -13
  34. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +46 -2
  35. package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
  36. package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
  37. package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
  38. package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
  39. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +29 -3
  40. package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
  41. package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
  42. package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
  43. package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
  44. package/dist/lightweight-dependency-relations.js +113 -7
  45. package/dist/native-import-language-profiles.js +10 -2
  46. package/dist/native-import-utils.js +15 -1
  47. package/dist/native-region-scanner-js-helpers.js +68 -18
  48. package/dist/native-region-scanner-js-imports.js +7 -0
  49. package/dist/native-region-scanner-js.js +16 -8
  50. package/dist/native-region-scanner.js +2 -1
  51. package/dist/semantic-import-regions.js +8 -6
  52. package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
  53. package/dist/semantic-import-sidecar-entry.js +151 -7
  54. package/dist/semantic-import-sidecar-types.d.ts +18 -13
  55. package/dist/semantic-import-source-preservation-utils.js +55 -0
  56. package/dist/semantic-import-source-preservation.js +98 -3
  57. package/package.json +1 -1
@@ -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) {
@@ -8,8 +9,12 @@ export function matchExactAnchors(beforeSymbols, afterSymbols) {
8
9
  const matchedAfterKeys = new Set();
9
10
  for (const before of beforeSymbols) {
10
11
  const after = afterByKey.get(before.anchor.key);
11
- if (after && anchorsSameLocation(before.anchor, after.anchor)) {
12
- matched.push({ before: symbolSummary(before), after: symbolSummary(after) });
12
+ if (after) {
13
+ matched.push({
14
+ before: symbolSummary(before),
15
+ after: symbolSummary(after),
16
+ sourceSpanMoved: !anchorsSameLocation(before.anchor, after.anchor)
17
+ });
13
18
  matchedAfterKeys.add(after.anchor.key);
14
19
  } else {
15
20
  unmatchedBefore.push(before);
@@ -27,21 +32,33 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
27
32
  const events = [];
28
33
  const ambiguous = [];
29
34
  const unmatchedBefore = [];
30
- for (const before of beforeSymbols) {
31
- const ranked = afterSymbols
32
- .filter((after) => !claimedAfter.has(after.anchor.key))
33
- .map((after) => ({ after, score: scoreLineagePair(before, after) }))
34
- .filter((candidate) => candidate.score.confidence >= options.minConfidence)
35
- .sort(compareCandidateScores);
35
+ const rankedByBefore = beforeSymbols.map((before) => ({
36
+ before,
37
+ ranked: rankLineageCandidates(before, afterSymbols, options)
38
+ }));
39
+ const contendersByAfter = afterContenderIndex(rankedByBefore);
40
+ for (const entry of rankedByBefore) {
41
+ const before = entry.before;
42
+ const ranked = entry.ranked.filter((candidate) => !claimedAfter.has(candidate.after.anchor.key));
36
43
  const best = ranked[0];
37
44
  const runnerUp = ranked[1];
38
45
  if (!best) {
39
46
  unmatchedBefore.push(before);
40
47
  continue;
41
48
  }
49
+ const splitTargets = splitLineageCandidates(before, ranked, contendersByAfter, options);
50
+ if (splitTargets.length > 1) {
51
+ for (const candidate of splitTargets) claimedAfter.add(candidate.after.anchor.key);
52
+ events.push(inferredSplitEvent(before, splitTargets, input));
53
+ continue;
54
+ }
55
+ const targetContention = ambiguousTargetContention(before, best, contendersByAfter, options);
56
+ if (targetContention.length > 0) {
57
+ ambiguous.push(ambiguousMatch(before, ranked, ['ambiguous-lineage-candidates', 'ambiguous-target-lineage-candidates']));
58
+ continue;
59
+ }
42
60
  if (runnerUp && best.score.confidence - runnerUp.score.confidence < options.ambiguityMargin) {
43
61
  ambiguous.push(ambiguousMatch(before, ranked));
44
- unmatchedBefore.push(before);
45
62
  continue;
46
63
  }
47
64
  claimedAfter.add(best.after.anchor.key);
@@ -55,6 +72,14 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
55
72
  };
56
73
  }
57
74
 
75
+ function rankLineageCandidates(before, afterSymbols, options) {
76
+ return afterSymbols
77
+ .filter((after) => before.anchor.key !== after.anchor.key)
78
+ .map((after) => ({ before, after, score: scoreLineagePair(before, after) }))
79
+ .filter((candidate) => candidate.score.confidence >= options.minConfidence)
80
+ .sort(compareCandidateScores);
81
+ }
82
+
58
83
  export function symbolSummary(symbol) {
59
84
  return {
60
85
  key: symbol.anchor.key,
@@ -64,6 +89,9 @@ export function symbolSummary(symbol) {
64
89
  language: symbol.language,
65
90
  sourcePath: symbol.anchor.sourcePath,
66
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),
67
95
  sourceSpan: symbol.anchor.sourceSpan,
68
96
  signatureHash: symbol.signatureHash,
69
97
  bodyHash: symbol.spanHash,
@@ -71,7 +99,7 @@ export function symbolSummary(symbol) {
71
99
  };
72
100
  }
73
101
 
74
- function ambiguousMatch(before, ranked) {
102
+ function ambiguousMatch(before, ranked, reasonCodes = ['ambiguous-lineage-candidates']) {
75
103
  return {
76
104
  before: symbolSummary(before),
77
105
  candidates: ranked.slice(0, 4).map((candidate) => ({
@@ -79,7 +107,7 @@ function ambiguousMatch(before, ranked) {
79
107
  confidence: candidate.score.confidence,
80
108
  reasons: candidate.score.reasons
81
109
  })),
82
- reasonCodes: ['ambiguous-lineage-candidates']
110
+ reasonCodes: uniqueStrings(reasonCodes)
83
111
  };
84
112
  }
85
113
 
@@ -94,7 +122,8 @@ function inferredEvent(before, after, score, input) {
94
122
  && before.anchor.symbolName !== after.anchor.symbolName;
95
123
  const pathChanged = before.anchor.sourcePath !== after.anchor.sourcePath;
96
124
  const spanMoved = JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(after.anchor.sourceSpan ?? null);
97
- const eventKind = nameChanged ? 'renamed' : 'moved';
125
+ const recreated = !nameChanged && score.reasons.includes('delete-recreate-candidate');
126
+ const eventKind = nameChanged ? 'renamed' : recreated ? 'recreated' : 'moved';
98
127
  return createSemanticLineageEvent({
99
128
  id: `lineage_inferred_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(after.anchor.key)}`,
100
129
  createdAt: input.generatedAt,
@@ -119,8 +148,57 @@ function inferredEvent(before, after, score, input) {
119
148
  inferred: true,
120
149
  algorithm: 'frontier.semantic-lineage-inference.v1',
121
150
  reasonCodes: score.reasons,
151
+ hashEvidence: hashEvidenceSummary(score.reasons),
122
152
  moved: pathChanged || spanMoved,
123
153
  renamed: nameChanged,
154
+ recreated,
155
+ anchorKeyChanged: before.anchor.key !== after.anchor.key,
156
+ autoMergeClaim: false,
157
+ semanticEquivalenceClaim: false
158
+ }
159
+ });
160
+ }
161
+
162
+ function inferredSplitEvent(before, candidates, input) {
163
+ const targets = candidates.map((candidate) => candidate.after);
164
+ const reasons = uniqueStrings([
165
+ ...candidates.flatMap((candidate) => candidate.score.reasons),
166
+ 'split-lineage-candidate'
167
+ ]);
168
+ const confidence = Math.min(...candidates.map((candidate) => candidate.score.confidence));
169
+ const pathMatch = targets.every((target) => before.anchor.sourcePath === target.anchor.sourcePath);
170
+ const spanMoved = targets.some((target) => (
171
+ before.anchor.sourcePath !== target.anchor.sourcePath
172
+ || JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(target.anchor.sourceSpan ?? null)
173
+ ));
174
+ return createSemanticLineageEvent({
175
+ id: `lineage_split_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(targets.map((target) => target.anchor.key).join('_'))}`,
176
+ createdAt: input.generatedAt,
177
+ eventKind: 'split',
178
+ from: before.anchor,
179
+ to: targets.map((target) => target.anchor),
180
+ confidence,
181
+ actor: input.actor,
182
+ actorId: input.actorId,
183
+ actorRole: input.actorRole ?? 'semantic-lineage-inference',
184
+ operationId: input.operationId,
185
+ deps: input.deps,
186
+ heads: input.heads,
187
+ stateVector: input.stateVector,
188
+ evidenceIds: [input.evidenceId ?? `evidence_${idFragment(input.id ?? before.anchor.key)}_lineage_inference`],
189
+ signatureHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('signature-hash-match')),
190
+ bodyHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('body-hash-match')),
191
+ pathMatch,
192
+ sourceSpanMoved: spanMoved,
193
+ conflictKeys: uniqueStrings([before.anchor.key, ...targets.map((target) => target.anchor.key)]),
194
+ metadata: {
195
+ inferred: true,
196
+ algorithm: 'frontier.semantic-lineage-inference.v1',
197
+ reasonCodes: reasons,
198
+ hashEvidence: hashEvidenceSummary(reasons),
199
+ split: true,
200
+ targetCount: targets.length,
201
+ candidateConfidences: candidates.map((candidate) => candidate.score.confidence),
124
202
  autoMergeClaim: false,
125
203
  semanticEquivalenceClaim: false
126
204
  }
@@ -134,18 +212,81 @@ function scoreLineagePair(before, after) {
134
212
  score += value;
135
213
  reasons.push(reason);
136
214
  };
215
+ const note = (reason) => reasons.push(reason);
137
216
  if (before.anchor.key && before.anchor.key === after.anchor.key) add(0.4, 'anchor-key-match');
138
217
  if (before.name && before.name === after.name) add(0.28, 'symbol-name-match');
139
218
  if (before.kind && before.kind === after.kind) add(0.12, 'symbol-kind-match');
219
+ addIdentityHashEvidence(before, after, add, note);
140
220
  if (before.signatureHash && before.signatureHash === after.signatureHash) add(0.52, 'signature-hash-match');
141
221
  if (before.spanHash && before.spanHash === after.spanHash) add(0.22, 'body-hash-match');
142
222
  if (before.anchor.kind && before.anchor.kind === after.anchor.kind) add(0.06, 'anchor-kind-match');
143
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);
144
225
  if (sourceSpanRangeSame(before.anchor.sourceSpan, after.anchor.sourceSpan)) add(0.18, 'source-span-range-match');
145
226
  if (before.ownershipRegionKind && before.ownershipRegionKind === after.ownershipRegionKind) add(0.04, 'ownership-kind-match');
146
227
  if (before.nativeAstNodeId && before.nativeAstNodeId === after.nativeAstNodeId) add(0.06, 'native-node-id-match');
147
228
  if (before.anchor.sourcePath !== after.anchor.sourcePath && (before.name === after.name || before.signatureHash === after.signatureHash)) add(0.04, 'source-path-moved');
148
- return { confidence: Math.max(0, Math.min(1, Number(score.toFixed(3)))), reasons };
229
+ if (before.anchor.key && after.anchor.key && before.anchor.key !== after.anchor.key) note('anchor-key-changed');
230
+ if (sameSymbolSurface(before, after)) note('same-symbol-surface');
231
+ if (deleteRecreateCandidate(before, after, reasons)) note('delete-recreate-candidate');
232
+ return { confidence: Math.max(0, Math.min(1, Number(score.toFixed(3)))), reasons: uniqueStrings(reasons) };
233
+ }
234
+
235
+ function afterContenderIndex(rankedByBefore) {
236
+ const contenders = new Map();
237
+ for (const entry of rankedByBefore) {
238
+ for (const candidate of entry.ranked) {
239
+ const key = candidate.after.anchor.key;
240
+ if (!key) continue;
241
+ contenders.set(key, [...(contenders.get(key) ?? []), candidate]);
242
+ }
243
+ }
244
+ return contenders;
245
+ }
246
+
247
+ function ambiguousTargetContention(before, candidate, contendersByAfter, options) {
248
+ return (contendersByAfter.get(candidate.after.anchor.key) ?? []).filter((contender) => (
249
+ contender.before.anchor.key !== before.anchor.key
250
+ && candidate.score.confidence - contender.score.confidence < options.ambiguityMargin
251
+ ));
252
+ }
253
+
254
+ function splitLineageCandidates(before, ranked, contendersByAfter, options) {
255
+ const best = ranked[0];
256
+ if (!best) return [];
257
+ const close = ranked.filter((candidate) => best.score.confidence - candidate.score.confidence < options.ambiguityMargin);
258
+ if (close.length < 2 || close.length > 4) return [];
259
+ if (!close.every((candidate) => hasStrongLineageEvidence(candidate.score))) return [];
260
+ if (!close.every((candidate) => splitNameEvidence(before, candidate.after))) return [];
261
+ if (close.some((candidate) => ambiguousTargetContention(before, candidate, contendersByAfter, options).length > 0)) return [];
262
+ return close;
263
+ }
264
+
265
+ function hasStrongLineageEvidence(score) {
266
+ return score.reasons.includes('signature-hash-match') || score.reasons.includes('body-hash-match');
267
+ }
268
+
269
+ function splitNameEvidence(before, after) {
270
+ const beforeName = normalizedName(before.name);
271
+ const afterName = normalizedName(after.name);
272
+ return Boolean(beforeName && afterName && beforeName !== afterName && afterName.includes(beforeName));
273
+ }
274
+
275
+ function deleteRecreateCandidate(before, after, reasons) {
276
+ return before.anchor.key !== after.anchor.key
277
+ && sameSymbolSurface(before, after)
278
+ && (reasons.includes('signature-hash-match') || reasons.includes('body-hash-match'));
279
+ }
280
+
281
+ function sameSymbolSurface(before, after) {
282
+ return Boolean(
283
+ before.name
284
+ && before.name === after.name
285
+ && before.kind
286
+ && before.kind === after.kind
287
+ && before.anchor.sourcePath
288
+ && before.anchor.sourcePath === after.anchor.sourcePath
289
+ );
149
290
  }
150
291
 
151
292
  function anchorsSameLocation(before, after) {
@@ -162,6 +303,10 @@ function sourceSpanRangeSame(before, after) {
162
303
  && before.endColumn === after.endColumn;
163
304
  }
164
305
 
306
+ function normalizedName(value) {
307
+ return String(value ?? '').replace(/[^A-Za-z0-9_$]+/g, '').toLowerCase();
308
+ }
309
+
165
310
  function firstString(...values) {
166
311
  return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
167
312
  }
@@ -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
 
@@ -49,6 +54,7 @@ function createResolutionState(start) {
49
54
  current: start ? [start] : [],
50
55
  traversed: [],
51
56
  terminal: [],
57
+ sourcePaths: strings(start?.sourcePath),
52
58
  conflictKeys: [],
53
59
  evidenceIds: [],
54
60
  proofIds: [],
@@ -64,14 +70,16 @@ function createResolutionState(start) {
64
70
  }
65
71
 
66
72
  function buildResolutionRecord(state, start, maxDepth, query, options) {
73
+ const currentAnchors = uniqueAnchors(state.current).map((anchor) => anchorWithLineageLinks(anchor, state, start, query));
67
74
  const core = {
68
75
  kind: 'frontier.lang.semanticLineageResolution',
69
76
  version: 1,
70
77
  query: compactRecord({ anchorKey: start?.key, anchorId: start?.id, sourcePath: start?.sourcePath, symbolName: start?.symbolName, maxDepth }),
71
78
  startAnchor: start,
72
- currentAnchors: uniqueAnchors(state.current),
79
+ currentAnchors,
73
80
  traversedEventIds: uniqueStrings(state.traversed),
74
81
  terminalEventIds: uniqueStrings(state.terminal),
82
+ sourcePaths: lineageSourcePaths(state, start, query, currentAnchors),
75
83
  status: state.status,
76
84
  confidence: clampConfidence(state.confidence),
77
85
  conflictKeys: uniqueStrings(state.conflictKeys),
@@ -102,6 +110,8 @@ function applyLineageEvent(state, event, visitedEvents) {
102
110
  state.operationIds.push(event.crdt?.operationId);
103
111
  state.heads.push(...(event.crdt?.heads ?? []));
104
112
  state.eventKinds.push(event.eventKind);
113
+ state.reasonCodes.push(...lineageEventReasonCodes(event));
114
+ state.sourcePaths.push(event.from?.sourcePath, ...event.to.map((anchor) => anchor.sourcePath));
105
115
  if (event.confidence !== undefined) state.confidence = state.confidence === undefined ? event.confidence : Math.min(state.confidence, event.confidence);
106
116
  const matched = state.current.filter((anchor) => anchorsMatch(anchor, event.from));
107
117
  const unmatched = state.current.filter((anchor) => !anchorsMatch(anchor, event.from));
@@ -138,6 +148,7 @@ function nextLineageEvents(anchors, byFromKey, byFromId) {
138
148
 
139
149
  function classifyResolutionStatus(state) {
140
150
  if (state.current.length === 0 && state.traversed.length > 0) return 'deleted';
151
+ if (state.terminal.length > 0 && state.current.length > 0) return 'ambiguous';
141
152
  if (state.eventKinds.includes('recreated')) return 'recreated';
142
153
  if (state.current.length > 1 || state.eventKinds.includes('split') || state.eventKinds.includes('merged')) return 'ambiguous';
143
154
  if (state.traversed.length > 0) return 'resolved';
@@ -178,6 +189,39 @@ function uniqueEvents(events) {
178
189
  return true;
179
190
  });
180
191
  }
192
+ function anchorWithLineageLinks(anchor, state, start, query) {
193
+ return compactRecord({
194
+ ...anchor,
195
+ lineageEventIds: uniqueStrings(state.traversed),
196
+ terminalLineageEventIds: uniqueStrings(state.terminal),
197
+ lineageSourcePaths: lineageSourcePaths(state, start, query, [anchor]),
198
+ evidenceIds: uniqueStrings(state.evidenceIds),
199
+ proofIds: uniqueStrings(state.proofIds),
200
+ crdtOperationIds: uniqueStrings(state.operationIds),
201
+ crdtHeads: uniqueStrings(state.heads),
202
+ lineageEventKinds: uniqueStrings(state.eventKinds),
203
+ lineageReasonCodes: uniqueStrings(state.reasonCodes)
204
+ });
205
+ }
206
+ function lineageSourcePaths(state, start, query, anchors = []) {
207
+ return uniqueStrings([
208
+ query?.sourcePath,
209
+ start?.sourcePath,
210
+ ...state.sourcePaths,
211
+ ...array(anchors).map((anchor) => anchor?.sourcePath),
212
+ ...array(anchors).flatMap((anchor) => anchor?.lineageSourcePaths ?? [])
213
+ ]);
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
+ }
181
225
  function positiveInteger(value, fallback) {
182
226
  const number = Number(value);
183
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
  }
@@ -0,0 +1,221 @@
1
+ import { uniqueStrings } from '../../native-import-utils.js';
2
+
3
+ const scoreFacetWeights = Object.freeze({ ownership: 18, staleStatus: 20, testEvidence: 18, overlap: 18, size: 10, semanticSidecarQuality: 16 });
4
+
5
+ export function semanticMergeCandidateScoreFacets(input) {
6
+ const components = {
7
+ ownership: ownershipFacet(input),
8
+ staleStatus: staleStatusFacet(input),
9
+ testEvidence: testEvidenceFacet(input),
10
+ overlap: overlapFacet(input),
11
+ size: sizeFacet(input),
12
+ semanticSidecarQuality: semanticSidecarQualityFacet(input)
13
+ };
14
+ const weightedTotal = Object.values(components).reduce((sum, component) => sum + component.weightedScore, 0);
15
+ const weightTotal = Object.values(components).reduce((sum, component) => sum + component.weight, 0);
16
+ const value = roundScore(weightTotal ? weightedTotal * 100 / weightTotal : 0);
17
+ const weakFacets = Object.values(components).filter((component) => component.status === 'weak').map((component) => component.key);
18
+ const blockedFacets = Object.values(components).filter((component) => component.status === 'blocked').map((component) => component.key);
19
+ const lowestScore = Math.min(...Object.values(components).map((component) => component.score));
20
+ return {
21
+ schema: 'frontier.lang.semanticMergeCandidateScoreFacets.v1',
22
+ version: 1,
23
+ higherIsBetter: true,
24
+ value,
25
+ risk: value < 50 || blockedFacets.length ? 'high' : value < 80 || weakFacets.length ? 'medium' : 'low',
26
+ components,
27
+ summary: {
28
+ value,
29
+ risk: value < 50 || blockedFacets.length ? 'high' : value < 80 || weakFacets.length ? 'medium' : 'low',
30
+ lowestScore,
31
+ weakFacets,
32
+ blockedFacets,
33
+ availableFacets: Object.values(components).filter((component) => component.signals.available !== false).map((component) => component.key)
34
+ }
35
+ };
36
+ }
37
+
38
+ function ownershipFacet(input) {
39
+ const regions = input.changedSemanticRegions;
40
+ const keyedRegions = regions.filter((region) => region.key || region.conflictKey);
41
+ const kindedRegions = regions.filter((region) => region.regionKind);
42
+ const spannedRegions = regions.filter((region) => region.sourceSpan);
43
+ const score = regions.length
44
+ ? roundScore(keyedRegions.length * 70 / regions.length + kindedRegions.length * 15 / regions.length + spannedRegions.length * 15 / regions.length)
45
+ : input.conflictKeys.length ? 45 : 70;
46
+ return scoreFacet('ownership', score, [
47
+ ...(regions.length === 0 ? ['missing-changed-semantic-regions'] : []),
48
+ ...(regions.length && keyedRegions.length < regions.length ? ['missing-ownership-keys'] : [])
49
+ ], {
50
+ changedSemanticRegions: regions.length,
51
+ keyedRegions: keyedRegions.length,
52
+ regionKinds: uniqueStrings(regions.map((region) => region.regionKind)),
53
+ conflictKeys: input.conflictKeys,
54
+ ownershipKeys: uniqueStrings(regions.map((region) => region.key))
55
+ });
56
+ }
57
+
58
+ function staleStatusFacet(input) {
59
+ const staleEvidence = input.evidenceRecords.filter((record) => evidenceStatus(record) === 'stale' || record?.metadata?.stale === true);
60
+ const sourceHashVerified = [
61
+ input.source?.metadata?.sourceHashVerified,
62
+ input.source?.before?.metadata?.sourceHashVerified,
63
+ input.source?.after?.metadata?.sourceHashVerified,
64
+ input.candidate?.metadata?.sourceHashVerified,
65
+ input.patch?.metadata?.sourceHashVerified
66
+ ].filter((value) => value !== undefined);
67
+ const staleProofs = semanticSidecarQuality(input)?.proofSummary?.stale ?? 0;
68
+ const sourceHashStale = sourceHashVerified.includes(false);
69
+ const score = sourceHashStale ? 0 : staleEvidence.length ? 35 : staleProofs > 0 ? 55 : 100;
70
+ return scoreFacet('staleStatus', score, [
71
+ ...(sourceHashStale ? ['stale-source-hash'] : []),
72
+ ...(staleEvidence.length ? ['stale-evidence'] : []),
73
+ ...(staleProofs > 0 ? ['stale-sidecar-proof-obligations'] : [])
74
+ ], {
75
+ staleEvidenceIds: staleEvidence.map((record) => record.id).filter(Boolean),
76
+ staleProofObligations: staleProofs,
77
+ sourceHashVerified
78
+ });
79
+ }
80
+
81
+ function testEvidenceFacet(input) {
82
+ const failed = input.evidenceRecords.filter((record) => evidenceStatus(record) === 'failed');
83
+ const stale = input.evidenceRecords.filter((record) => evidenceStatus(record) === 'stale');
84
+ const pending = input.evidenceRecords.filter((record) => ['pending', 'assumed', 'unknown'].includes(evidenceStatus(record)));
85
+ const passed = input.evidenceRecords.filter((record) => ['passed', 'ok', 'success'].includes(evidenceStatus(record)));
86
+ const score = failed.length ? 0 : stale.length ? 45 : pending.length ? 65 : input.proofIds.length ? 100 : input.evidenceIds.length ? 82 : 35;
87
+ return scoreFacet('testEvidence', score, [
88
+ ...(failed.length ? ['failed-evidence'] : []),
89
+ ...(stale.length ? ['stale-evidence'] : []),
90
+ ...(pending.length ? ['pending-evidence'] : []),
91
+ ...(!input.evidenceIds.length && !input.proofIds.length ? ['missing-evidence'] : [])
92
+ ], {
93
+ evidenceIds: input.evidenceIds,
94
+ proofIds: input.proofIds,
95
+ evidenceRecords: input.evidenceRecords.length,
96
+ passedEvidenceIds: passed.map((record) => record.id).filter(Boolean),
97
+ failedEvidenceIds: failed.map((record) => record.id).filter(Boolean),
98
+ staleEvidenceIds: stale.map((record) => record.id).filter(Boolean),
99
+ pendingEvidenceIds: pending.map((record) => record.id).filter(Boolean)
100
+ });
101
+ }
102
+
103
+ function overlapFacet(input) {
104
+ const high = input.overlaps.filter((overlap) => overlap.risk === 'high').length;
105
+ const medium = input.overlaps.filter((overlap) => overlap.risk === 'medium').length;
106
+ const score = clampScore(100 - high * 45 - medium * 25 - Math.max(0, input.overlaps.length - high - medium) * 15);
107
+ return scoreFacet('overlap', score, [
108
+ ...(input.overlaps.length ? ['overlapping-semantic-regions'] : [])
109
+ ], {
110
+ overlaps: input.overlaps.length,
111
+ highRiskOverlaps: high,
112
+ mediumRiskOverlaps: medium,
113
+ conflictKeys: uniqueStrings(input.overlaps.flatMap((overlap) => overlap.conflictKeys ?? [])),
114
+ byKind: countBy(input.overlaps.map((overlap) => overlap.overlapKind))
115
+ });
116
+ }
117
+
118
+ function sizeFacet(input) {
119
+ const operationCount = array(input.candidate?.operations ?? input.patch?.operations).length;
120
+ const changedSemanticRegions = input.changedSemanticRegions.length;
121
+ const conflictKeys = input.conflictKeys.length;
122
+ const score = clampScore(100 - Math.max(0, changedSemanticRegions - 3) * 8 - Math.max(0, operationCount - 3) * 4 - Math.max(0, conflictKeys - 4) * 3);
123
+ return scoreFacet('size', score, [
124
+ ...(changedSemanticRegions > 3 ? ['large-changed-region-set'] : []),
125
+ ...(operationCount > 3 ? ['large-operation-set'] : []),
126
+ ...(conflictKeys > 4 ? ['many-conflict-keys'] : [])
127
+ ], { changedSemanticRegions, operationCount, conflictKeys });
128
+ }
129
+
130
+ function semanticSidecarQualityFacet(input) {
131
+ const quality = semanticSidecarQuality(input);
132
+ if (!quality) return scoreFacet('semanticSidecarQuality', 100, [], { available: false });
133
+ const proofSummary = quality.proofSummary ?? {};
134
+ const warningCodes = uniqueStrings([...(quality.warnings ?? []).map((warning) => warning.code), ...strings(quality.expectedMissingReasonCodes)]);
135
+ const reviewProofs = (proofSummary.open ?? 0) + (proofSummary.stale ?? 0) + (proofSummary.assumed ?? 0) + (proofSummary.externalToolRequired ?? 0) + (proofSummary.unknown ?? 0);
136
+ const score = clampScore(
137
+ (quality.imported === false || quality.selected === false ? 25 : 100)
138
+ - (quality.eligible === false ? 35 : 0)
139
+ - Math.min(35, (quality.warningCount ?? warningCodes.length ?? 0) * 7)
140
+ - Math.min(40, (proofSummary.failed ?? 0) * 20)
141
+ - Math.min(25, reviewProofs * 5)
142
+ );
143
+ return scoreFacet('semanticSidecarQuality', score, [
144
+ ...(quality.imported === false || quality.selected === false ? ['semantic-sidecar-not-imported'] : []),
145
+ ...(quality.eligible === false ? ['semantic-sidecar-not-eligible'] : []),
146
+ ...warningCodes
147
+ ], {
148
+ available: true,
149
+ expected: quality.expected,
150
+ expectedSatisfied: quality.expectedSatisfied,
151
+ selected: quality.selected,
152
+ imported: quality.imported,
153
+ eligible: quality.eligible,
154
+ importCount: quality.importCount,
155
+ symbolCount: quality.symbolCount,
156
+ ownershipRegionCount: quality.ownershipRegionCount,
157
+ patchHintCount: quality.patchHintCount,
158
+ evidenceCount: quality.evidenceCount,
159
+ warningCount: quality.warningCount ?? warningCodes.length,
160
+ warningCodes,
161
+ proofSummary
162
+ });
163
+ }
164
+
165
+ function semanticSidecarQuality(input) {
166
+ return [
167
+ input.candidate?.semanticSidecarQuality,
168
+ input.candidate?.sidecarQuality,
169
+ input.candidate?.semanticSidecar?.quality,
170
+ input.candidate?.sidecar?.quality,
171
+ input.candidate?.metadata?.semanticSidecarQuality,
172
+ input.candidate?.metadata?.sidecarQuality,
173
+ input.source?.semanticSidecarQuality,
174
+ input.source?.sidecarQuality,
175
+ input.source?.semanticSidecar?.quality,
176
+ input.source?.sidecar?.quality,
177
+ input.source?.metadata?.semanticSidecarQuality,
178
+ input.source?.metadata?.sidecarQuality,
179
+ input.patch?.semanticSidecarQuality,
180
+ input.patch?.metadata?.semanticSidecarQuality
181
+ ].find(looksLikeSemanticSidecarQuality);
182
+ }
183
+
184
+ function looksLikeSemanticSidecarQuality(value) {
185
+ return value && typeof value === 'object' && (
186
+ value.schema === 'frontier.lang.semanticSidecarQuality.v1'
187
+ || value.eligible !== undefined
188
+ || value.imported !== undefined
189
+ || value.proofSummary !== undefined
190
+ || value.warningCount !== undefined
191
+ );
192
+ }
193
+
194
+ function scoreFacet(key, score, reasonCodes, signals) {
195
+ const normalizedScore = clampScore(score);
196
+ const weight = scoreFacetWeights[key] ?? 1;
197
+ return {
198
+ key,
199
+ score: normalizedScore,
200
+ weight,
201
+ weightedScore: roundScore(normalizedScore * weight / 100),
202
+ status: facetStatus(normalizedScore),
203
+ reasonCodes: uniqueStrings(reasonCodes),
204
+ signals: compactRecord(signals)
205
+ };
206
+ }
207
+
208
+ function facetStatus(score) {
209
+ if (score <= 0) return 'blocked';
210
+ if (score < 50) return 'weak';
211
+ if (score < 80) return 'partial';
212
+ return 'strong';
213
+ }
214
+
215
+ function evidenceStatus(record) { return String(record?.status ?? record?.metadata?.status ?? '').toLowerCase(); }
216
+ function countBy(values) { const counts = {}; for (const value of values ?? []) { const key = String(value ?? 'unknown'); counts[key] = (counts[key] ?? 0) + 1; } return counts; }
217
+ function clampScore(value) { return Math.max(0, Math.min(100, roundScore(value))); }
218
+ function roundScore(value) { return Math.round((Number.isFinite(value) ? value : 0) * 100) / 100; }
219
+ function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
220
+ function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
221
+ function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }