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

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 (27) hide show
  1. package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
  2. package/dist/declarations/semantic-edit-script.d.ts +34 -37
  3. package/dist/declarations/semantic-lineage.d.ts +63 -34
  4. package/dist/declarations/semantic-patch-bundle.d.ts +13 -0
  5. package/dist/internal/index-impl/declarationRecord.js +2 -2
  6. package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
  7. package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +56 -64
  8. package/dist/internal/index-impl/replaySemanticEditProjection.js +54 -22
  9. package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
  10. package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
  11. package/dist/internal/index-impl/semanticEditSourceRanges.js +204 -0
  12. package/dist/internal/index-impl/semanticHistoryLineageResolution.js +35 -1
  13. package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
  14. package/dist/internal/index-impl/semanticLineageInferenceMatching.js +150 -13
  15. package/dist/internal/index-impl/semanticLineageResolutionRecords.js +28 -1
  16. package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
  17. package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
  18. package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +6 -2
  19. package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
  20. package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
  21. package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
  22. package/dist/lightweight-dependency-relations.js +113 -7
  23. package/dist/native-import-utils.js +15 -1
  24. package/dist/native-region-scanner-js-helpers.js +61 -17
  25. package/dist/native-region-scanner-js.js +12 -4
  26. package/dist/semantic-import-regions.js +3 -3
  27. package/package.json +1 -1
@@ -22,7 +22,11 @@ export function resolveSemanticHistoryRecordLineage(record = {}, eventsOrMap = [
22
22
  semanticAnchorKeys: resolveIndexKeys(sourceIndex.semanticAnchorKeys, byAnchorKey, options),
23
23
  sourcePaths: uniqueStrings([
24
24
  ...array(sourceIndex.sourcePaths),
25
- ...resolutions.flatMap((resolution) => resolution.currentAnchors.map((anchor) => anchor.sourcePath))
25
+ ...resolutions.flatMap((resolution) => resolutionSourcePaths(resolution))
26
+ ]),
27
+ lineageResolutionIds: uniqueStrings([
28
+ ...array(sourceIndex.lineageResolutionIds),
29
+ ...resolutions.map((resolution) => resolution.id)
26
30
  ]),
27
31
  lineageEventIds: uniqueStrings([
28
32
  ...array(sourceIndex.lineageEventIds),
@@ -105,6 +109,13 @@ function createResolvedHistoryRecord(record, index, resolution) {
105
109
  inactiveAnchorKeys: resolution.summary.inactiveAnchorKeys,
106
110
  blockedAnchorKeys: resolution.summary.blockedAnchorKeys
107
111
  },
112
+ lineageResolutionIds: resolution.summary.lineageResolutionIds,
113
+ lineageEventIds: resolution.summary.traversedEventIds,
114
+ terminalEventIds: resolution.summary.terminalEventIds,
115
+ sourcePaths: resolution.summary.sourcePaths,
116
+ evidenceIds: resolution.summary.evidenceIds,
117
+ proofIds: resolution.summary.proofIds,
118
+ reasonCodes: resolution.summary.reasonCodes,
108
119
  autoMergeClaim: false,
109
120
  semanticEquivalenceClaim: false
110
121
  }
@@ -159,8 +170,11 @@ function summarizeSemanticHistoryLineageResolutions(resolutions, index, anchorIn
159
170
  deletedAnchorKeys: uniqueStrings(anchorInventory.deleted.map((anchor) => anchor.key)),
160
171
  unresolvedAnchorKeys: uniqueStrings(anchorInventory.unresolved.map((anchor) => anchor.key)),
161
172
  blockedAnchorKeys: uniqueStrings(anchorInventory.blocked.map((anchor) => anchor.key)),
173
+ lineageResolutionIds: uniqueStrings(resolutions.map((resolution) => resolution.id)),
162
174
  traversedEventIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.traversedEventIds)),
163
175
  terminalEventIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.terminalEventIds)),
176
+ evidenceIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.evidenceIds)),
177
+ proofIds: uniqueStrings(resolutions.flatMap((resolution) => resolution.proofIds)),
164
178
  reasonCodes: uniqueStrings(resolutions.flatMap((resolution) => resolution.reasonCodes))
165
179
  };
166
180
  }
@@ -225,8 +239,17 @@ function anchorEntry(anchor, resolution) {
225
239
  sourcePath: anchor.sourcePath,
226
240
  symbolId: anchor.symbolId,
227
241
  symbolName: anchor.symbolName,
242
+ sourcePaths: resolutionSourcePaths(resolution, anchor),
243
+ lineageEventIds: uniqueStrings(resolution.traversedEventIds),
244
+ terminalEventIds: uniqueStrings(resolution.terminalEventIds),
245
+ evidenceIds: uniqueStrings(resolution.evidenceIds),
246
+ proofIds: uniqueStrings(resolution.proofIds),
247
+ crdtOperationIds: uniqueStrings(resolution.crdtOperationIds),
248
+ crdtHeads: uniqueStrings(resolution.crdtHeads),
249
+ lineageEventKinds: uniqueStrings(resolution.lineageEventKinds),
228
250
  status: resolution.status,
229
251
  resolutionId: resolution.id,
252
+ confidence: resolution.confidence,
230
253
  reasonCodes: uniqueStrings(resolution.reasonCodes)
231
254
  });
232
255
  }
@@ -250,6 +273,17 @@ function uniqueAnchorEntries(entries) {
250
273
  });
251
274
  }
252
275
 
276
+ function resolutionSourcePaths(resolution, anchor) {
277
+ return uniqueStrings([
278
+ anchor?.sourcePath,
279
+ ...array(anchor?.lineageSourcePaths),
280
+ resolution.query?.sourcePath,
281
+ resolution.startAnchor?.sourcePath,
282
+ ...array(resolution.sourcePaths),
283
+ ...resolution.currentAnchors.flatMap((entry) => [entry.sourcePath, ...array(entry.lineageSourcePaths)])
284
+ ]);
285
+ }
286
+
253
287
  function array(value) { return value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]; }
254
288
  function uniqueStrings(values) { return uniqueRawStrings(array(values).map((value) => String(value ?? '')).filter(Boolean)); }
255
289
  function firstString(...values) { return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean); }
@@ -1,4 +1,4 @@
1
- import{idFragment}from'../../native-import-utils.js';import{semanticOwnershipRegionForDeclaration}from'../../semantic-import-regions.js';import{createSemanticIndexRecord,hashSemanticValue}from'@shapeshift-labs/frontier-lang-kernel';
1
+ import{idFragment,caseSensitiveIdFragment}from'../../native-import-utils.js';import{semanticOwnershipRegionForDeclaration}from'../../semantic-import-regions.js';import{createSemanticIndexRecord,hashSemanticValue}from'@shapeshift-labs/frontier-lang-kernel';
2
2
  import{relationPredicateForDeclaration}from'./relationPredicateForDeclaration.js';
3
3
  export function semanticIndexFromNativeDeclarations(declarations, input, options) {
4
4
  const documentId = `doc_${idFragment(input.sourcePath ?? input.language)}_${idFragment(input.sourceHash)}`;
@@ -9,7 +9,7 @@ export function semanticIndexFromNativeDeclarations(declarations, input, options
9
9
  const facts = [];
10
10
  const mappings = [];
11
11
  for (const declaration of declarations) {
12
- const symbolId = declaration.symbolId ?? `symbol:${input.language}:${declaration.role === 'import' ? 'import:' : ''}${idFragment(declaration.name)}`;
12
+ const symbolId = declaration.symbolId ?? `symbol:${input.language}:${declaration.role === 'import' ? 'import:' : ''}${caseSensitiveIdFragment(declaration.name)}`;
13
13
  const occurrenceId = `occ_${idFragment(declaration.nativeNode.id)}_${declaration.role ?? 'definition'}`;
14
14
  const ownershipRegion = semanticOwnershipRegionForDeclaration(input, {
15
15
  ...declaration,
@@ -8,8 +8,12 @@ export function matchExactAnchors(beforeSymbols, afterSymbols) {
8
8
  const matchedAfterKeys = new Set();
9
9
  for (const before of beforeSymbols) {
10
10
  const after = afterByKey.get(before.anchor.key);
11
- if (after && anchorsSameLocation(before.anchor, after.anchor)) {
12
- matched.push({ before: symbolSummary(before), after: symbolSummary(after) });
11
+ if (after) {
12
+ matched.push({
13
+ before: symbolSummary(before),
14
+ after: symbolSummary(after),
15
+ sourceSpanMoved: !anchorsSameLocation(before.anchor, after.anchor)
16
+ });
13
17
  matchedAfterKeys.add(after.anchor.key);
14
18
  } else {
15
19
  unmatchedBefore.push(before);
@@ -27,21 +31,33 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
27
31
  const events = [];
28
32
  const ambiguous = [];
29
33
  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);
34
+ const rankedByBefore = beforeSymbols.map((before) => ({
35
+ before,
36
+ ranked: rankLineageCandidates(before, afterSymbols, options)
37
+ }));
38
+ const contendersByAfter = afterContenderIndex(rankedByBefore);
39
+ for (const entry of rankedByBefore) {
40
+ const before = entry.before;
41
+ const ranked = entry.ranked.filter((candidate) => !claimedAfter.has(candidate.after.anchor.key));
36
42
  const best = ranked[0];
37
43
  const runnerUp = ranked[1];
38
44
  if (!best) {
39
45
  unmatchedBefore.push(before);
40
46
  continue;
41
47
  }
48
+ const splitTargets = splitLineageCandidates(before, ranked, contendersByAfter, options);
49
+ if (splitTargets.length > 1) {
50
+ for (const candidate of splitTargets) claimedAfter.add(candidate.after.anchor.key);
51
+ events.push(inferredSplitEvent(before, splitTargets, input));
52
+ continue;
53
+ }
54
+ const targetContention = ambiguousTargetContention(before, best, contendersByAfter, options);
55
+ if (targetContention.length > 0) {
56
+ ambiguous.push(ambiguousMatch(before, ranked, ['ambiguous-lineage-candidates', 'ambiguous-target-lineage-candidates']));
57
+ continue;
58
+ }
42
59
  if (runnerUp && best.score.confidence - runnerUp.score.confidence < options.ambiguityMargin) {
43
60
  ambiguous.push(ambiguousMatch(before, ranked));
44
- unmatchedBefore.push(before);
45
61
  continue;
46
62
  }
47
63
  claimedAfter.add(best.after.anchor.key);
@@ -55,6 +71,14 @@ export function matchLineageCandidates(beforeSymbols, afterSymbols, input, optio
55
71
  };
56
72
  }
57
73
 
74
+ function rankLineageCandidates(before, afterSymbols, options) {
75
+ return afterSymbols
76
+ .filter((after) => before.anchor.key !== after.anchor.key)
77
+ .map((after) => ({ before, after, score: scoreLineagePair(before, after) }))
78
+ .filter((candidate) => candidate.score.confidence >= options.minConfidence)
79
+ .sort(compareCandidateScores);
80
+ }
81
+
58
82
  export function symbolSummary(symbol) {
59
83
  return {
60
84
  key: symbol.anchor.key,
@@ -71,7 +95,7 @@ export function symbolSummary(symbol) {
71
95
  };
72
96
  }
73
97
 
74
- function ambiguousMatch(before, ranked) {
98
+ function ambiguousMatch(before, ranked, reasonCodes = ['ambiguous-lineage-candidates']) {
75
99
  return {
76
100
  before: symbolSummary(before),
77
101
  candidates: ranked.slice(0, 4).map((candidate) => ({
@@ -79,7 +103,7 @@ function ambiguousMatch(before, ranked) {
79
103
  confidence: candidate.score.confidence,
80
104
  reasons: candidate.score.reasons
81
105
  })),
82
- reasonCodes: ['ambiguous-lineage-candidates']
106
+ reasonCodes: uniqueStrings(reasonCodes)
83
107
  };
84
108
  }
85
109
 
@@ -94,7 +118,8 @@ function inferredEvent(before, after, score, input) {
94
118
  && before.anchor.symbolName !== after.anchor.symbolName;
95
119
  const pathChanged = before.anchor.sourcePath !== after.anchor.sourcePath;
96
120
  const spanMoved = JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(after.anchor.sourceSpan ?? null);
97
- const eventKind = nameChanged ? 'renamed' : 'moved';
121
+ const recreated = !nameChanged && score.reasons.includes('delete-recreate-candidate');
122
+ const eventKind = nameChanged ? 'renamed' : recreated ? 'recreated' : 'moved';
98
123
  return createSemanticLineageEvent({
99
124
  id: `lineage_inferred_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(after.anchor.key)}`,
100
125
  createdAt: input.generatedAt,
@@ -121,6 +146,53 @@ function inferredEvent(before, after, score, input) {
121
146
  reasonCodes: score.reasons,
122
147
  moved: pathChanged || spanMoved,
123
148
  renamed: nameChanged,
149
+ recreated,
150
+ anchorKeyChanged: before.anchor.key !== after.anchor.key,
151
+ autoMergeClaim: false,
152
+ semanticEquivalenceClaim: false
153
+ }
154
+ });
155
+ }
156
+
157
+ function inferredSplitEvent(before, candidates, input) {
158
+ const targets = candidates.map((candidate) => candidate.after);
159
+ const reasons = uniqueStrings([
160
+ ...candidates.flatMap((candidate) => candidate.score.reasons),
161
+ 'split-lineage-candidate'
162
+ ]);
163
+ const confidence = Math.min(...candidates.map((candidate) => candidate.score.confidence));
164
+ const pathMatch = targets.every((target) => before.anchor.sourcePath === target.anchor.sourcePath);
165
+ const spanMoved = targets.some((target) => (
166
+ before.anchor.sourcePath !== target.anchor.sourcePath
167
+ || JSON.stringify(before.anchor.sourceSpan ?? null) !== JSON.stringify(target.anchor.sourceSpan ?? null)
168
+ ));
169
+ return createSemanticLineageEvent({
170
+ id: `lineage_split_${idFragment(firstString(input.id, before.anchor.key))}_${idFragment(targets.map((target) => target.anchor.key).join('_'))}`,
171
+ createdAt: input.generatedAt,
172
+ eventKind: 'split',
173
+ from: before.anchor,
174
+ to: targets.map((target) => target.anchor),
175
+ confidence,
176
+ actor: input.actor,
177
+ actorId: input.actorId,
178
+ actorRole: input.actorRole ?? 'semantic-lineage-inference',
179
+ operationId: input.operationId,
180
+ deps: input.deps,
181
+ heads: input.heads,
182
+ stateVector: input.stateVector,
183
+ evidenceIds: [input.evidenceId ?? `evidence_${idFragment(input.id ?? before.anchor.key)}_lineage_inference`],
184
+ signatureHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('signature-hash-match')),
185
+ bodyHashMatch: candidates.every((candidate) => candidate.score.reasons.includes('body-hash-match')),
186
+ pathMatch,
187
+ sourceSpanMoved: spanMoved,
188
+ conflictKeys: uniqueStrings([before.anchor.key, ...targets.map((target) => target.anchor.key)]),
189
+ metadata: {
190
+ inferred: true,
191
+ algorithm: 'frontier.semantic-lineage-inference.v1',
192
+ reasonCodes: reasons,
193
+ split: true,
194
+ targetCount: targets.length,
195
+ candidateConfidences: candidates.map((candidate) => candidate.score.confidence),
124
196
  autoMergeClaim: false,
125
197
  semanticEquivalenceClaim: false
126
198
  }
@@ -134,6 +206,7 @@ function scoreLineagePair(before, after) {
134
206
  score += value;
135
207
  reasons.push(reason);
136
208
  };
209
+ const note = (reason) => reasons.push(reason);
137
210
  if (before.anchor.key && before.anchor.key === after.anchor.key) add(0.4, 'anchor-key-match');
138
211
  if (before.name && before.name === after.name) add(0.28, 'symbol-name-match');
139
212
  if (before.kind && before.kind === after.kind) add(0.12, 'symbol-kind-match');
@@ -145,7 +218,67 @@ function scoreLineagePair(before, after) {
145
218
  if (before.ownershipRegionKind && before.ownershipRegionKind === after.ownershipRegionKind) add(0.04, 'ownership-kind-match');
146
219
  if (before.nativeAstNodeId && before.nativeAstNodeId === after.nativeAstNodeId) add(0.06, 'native-node-id-match');
147
220
  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 };
221
+ if (before.anchor.key && after.anchor.key && before.anchor.key !== after.anchor.key) note('anchor-key-changed');
222
+ if (sameSymbolSurface(before, after)) note('same-symbol-surface');
223
+ if (deleteRecreateCandidate(before, after, reasons)) note('delete-recreate-candidate');
224
+ return { confidence: Math.max(0, Math.min(1, Number(score.toFixed(3)))), reasons: uniqueStrings(reasons) };
225
+ }
226
+
227
+ function afterContenderIndex(rankedByBefore) {
228
+ const contenders = new Map();
229
+ for (const entry of rankedByBefore) {
230
+ for (const candidate of entry.ranked) {
231
+ const key = candidate.after.anchor.key;
232
+ if (!key) continue;
233
+ contenders.set(key, [...(contenders.get(key) ?? []), candidate]);
234
+ }
235
+ }
236
+ return contenders;
237
+ }
238
+
239
+ function ambiguousTargetContention(before, candidate, contendersByAfter, options) {
240
+ return (contendersByAfter.get(candidate.after.anchor.key) ?? []).filter((contender) => (
241
+ contender.before.anchor.key !== before.anchor.key
242
+ && candidate.score.confidence - contender.score.confidence < options.ambiguityMargin
243
+ ));
244
+ }
245
+
246
+ function splitLineageCandidates(before, ranked, contendersByAfter, options) {
247
+ const best = ranked[0];
248
+ if (!best) return [];
249
+ const close = ranked.filter((candidate) => best.score.confidence - candidate.score.confidence < options.ambiguityMargin);
250
+ if (close.length < 2 || close.length > 4) return [];
251
+ if (!close.every((candidate) => hasStrongLineageEvidence(candidate.score))) return [];
252
+ if (!close.every((candidate) => splitNameEvidence(before, candidate.after))) return [];
253
+ if (close.some((candidate) => ambiguousTargetContention(before, candidate, contendersByAfter, options).length > 0)) return [];
254
+ return close;
255
+ }
256
+
257
+ function hasStrongLineageEvidence(score) {
258
+ return score.reasons.includes('signature-hash-match') || score.reasons.includes('body-hash-match');
259
+ }
260
+
261
+ function splitNameEvidence(before, after) {
262
+ const beforeName = normalizedName(before.name);
263
+ const afterName = normalizedName(after.name);
264
+ return Boolean(beforeName && afterName && beforeName !== afterName && afterName.includes(beforeName));
265
+ }
266
+
267
+ function deleteRecreateCandidate(before, after, reasons) {
268
+ return before.anchor.key !== after.anchor.key
269
+ && sameSymbolSurface(before, after)
270
+ && (reasons.includes('signature-hash-match') || reasons.includes('body-hash-match'));
271
+ }
272
+
273
+ function sameSymbolSurface(before, after) {
274
+ return Boolean(
275
+ before.name
276
+ && before.name === after.name
277
+ && before.kind
278
+ && before.kind === after.kind
279
+ && before.anchor.sourcePath
280
+ && before.anchor.sourcePath === after.anchor.sourcePath
281
+ );
149
282
  }
150
283
 
151
284
  function anchorsSameLocation(before, after) {
@@ -162,6 +295,10 @@ function sourceSpanRangeSame(before, after) {
162
295
  && before.endColumn === after.endColumn;
163
296
  }
164
297
 
298
+ function normalizedName(value) {
299
+ return String(value ?? '').replace(/[^A-Za-z0-9_$]+/g, '').toLowerCase();
300
+ }
301
+
165
302
  function firstString(...values) {
166
303
  return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
167
304
  }
@@ -49,6 +49,7 @@ function createResolutionState(start) {
49
49
  current: start ? [start] : [],
50
50
  traversed: [],
51
51
  terminal: [],
52
+ sourcePaths: strings(start?.sourcePath),
52
53
  conflictKeys: [],
53
54
  evidenceIds: [],
54
55
  proofIds: [],
@@ -64,14 +65,16 @@ function createResolutionState(start) {
64
65
  }
65
66
 
66
67
  function buildResolutionRecord(state, start, maxDepth, query, options) {
68
+ const currentAnchors = uniqueAnchors(state.current).map((anchor) => anchorWithLineageLinks(anchor, state, start, query));
67
69
  const core = {
68
70
  kind: 'frontier.lang.semanticLineageResolution',
69
71
  version: 1,
70
72
  query: compactRecord({ anchorKey: start?.key, anchorId: start?.id, sourcePath: start?.sourcePath, symbolName: start?.symbolName, maxDepth }),
71
73
  startAnchor: start,
72
- currentAnchors: uniqueAnchors(state.current),
74
+ currentAnchors,
73
75
  traversedEventIds: uniqueStrings(state.traversed),
74
76
  terminalEventIds: uniqueStrings(state.terminal),
77
+ sourcePaths: lineageSourcePaths(state, start, query, currentAnchors),
75
78
  status: state.status,
76
79
  confidence: clampConfidence(state.confidence),
77
80
  conflictKeys: uniqueStrings(state.conflictKeys),
@@ -102,6 +105,7 @@ function applyLineageEvent(state, event, visitedEvents) {
102
105
  state.operationIds.push(event.crdt?.operationId);
103
106
  state.heads.push(...(event.crdt?.heads ?? []));
104
107
  state.eventKinds.push(event.eventKind);
108
+ state.sourcePaths.push(event.from?.sourcePath, ...event.to.map((anchor) => anchor.sourcePath));
105
109
  if (event.confidence !== undefined) state.confidence = state.confidence === undefined ? event.confidence : Math.min(state.confidence, event.confidence);
106
110
  const matched = state.current.filter((anchor) => anchorsMatch(anchor, event.from));
107
111
  const unmatched = state.current.filter((anchor) => !anchorsMatch(anchor, event.from));
@@ -178,6 +182,29 @@ function uniqueEvents(events) {
178
182
  return true;
179
183
  });
180
184
  }
185
+ function anchorWithLineageLinks(anchor, state, start, query) {
186
+ return compactRecord({
187
+ ...anchor,
188
+ lineageEventIds: uniqueStrings(state.traversed),
189
+ terminalLineageEventIds: uniqueStrings(state.terminal),
190
+ lineageSourcePaths: lineageSourcePaths(state, start, query, [anchor]),
191
+ evidenceIds: uniqueStrings(state.evidenceIds),
192
+ proofIds: uniqueStrings(state.proofIds),
193
+ crdtOperationIds: uniqueStrings(state.operationIds),
194
+ crdtHeads: uniqueStrings(state.heads),
195
+ lineageEventKinds: uniqueStrings(state.eventKinds),
196
+ lineageReasonCodes: uniqueStrings(state.reasonCodes)
197
+ });
198
+ }
199
+ function lineageSourcePaths(state, start, query, anchors = []) {
200
+ return uniqueStrings([
201
+ query?.sourcePath,
202
+ start?.sourcePath,
203
+ ...state.sourcePaths,
204
+ ...array(anchors).map((anchor) => anchor?.sourcePath),
205
+ ...array(anchors).flatMap((anchor) => anchor?.lineageSourcePaths ?? [])
206
+ ]);
207
+ }
181
208
  function positiveInteger(value, fallback) {
182
209
  const number = Number(value);
183
210
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
@@ -3,32 +3,42 @@ import { normalizeSemanticMergeReadiness, uniqueStrings } from '../../native-imp
3
3
  export function createSemanticPatchBundleAdmission(input = {}, context = {}) {
4
4
  const transformAdmission = semanticTransformAdmission(context);
5
5
  const semanticEditAdmission = context.semanticEditAdmission ?? { status: 'none', action: 'none', readiness: 'needs-review', reasonCodes: [] };
6
- const fallbackReadiness = fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, context.readiness);
7
- const readiness = normalizeSemanticMergeReadiness(input.readiness ?? fallbackReadiness) ?? input.readiness ?? fallbackReadiness;
8
- const status = input.status ?? admissionStatusForReadiness(readiness, transformAdmission, semanticEditAdmission);
9
- const autoApplyCandidate = input.autoApplyCandidate ?? (status === 'admitted' && [
10
- transformAdmission.action,
11
- semanticEditAdmission.action
12
- ].includes('admit'));
6
+ const evidenceAdmission = autoMergeEvidenceAdmission(context, { transformAdmission, semanticEditAdmission });
7
+ const fallbackReadiness = fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, evidenceAdmission, context.readiness);
8
+ const inputReadiness = normalizeSemanticMergeReadiness(input.readiness ?? fallbackReadiness) ?? input.readiness ?? fallbackReadiness;
9
+ const readiness = hasPositiveApplyAction(transformAdmission, semanticEditAdmission) && evidenceAdmission.action !== 'admit'
10
+ ? evidenceAdmission.readiness
11
+ : inputReadiness;
12
+ const computedStatus = admissionStatusForReadiness(readiness, transformAdmission, semanticEditAdmission, evidenceAdmission);
13
+ const status = input.status === 'admitted' && computedStatus !== 'admitted' ? computedStatus : input.status ?? computedStatus;
14
+ const computedAutoApplyCandidate = status === 'admitted' &&
15
+ hasPositiveApplyAction(transformAdmission, semanticEditAdmission) &&
16
+ evidenceAdmission.action === 'admit';
17
+ const autoApplyCandidate = input.autoApplyCandidate === true ? computedAutoApplyCandidate : input.autoApplyCandidate ?? computedAutoApplyCandidate;
18
+ const admittedWithoutPositiveProof = status === 'admitted' &&
19
+ hasPositiveApplyAction(transformAdmission, semanticEditAdmission) &&
20
+ evidenceAdmission.action !== 'admit';
13
21
  return compactRecord({
14
22
  status,
15
23
  readiness,
16
- reviewRequired: input.reviewRequired ?? status !== 'admitted',
24
+ reviewRequired: input.reviewRequired ?? (status !== 'admitted' || admittedWithoutPositiveProof),
17
25
  autoMergeClaim: false,
18
26
  autoApplyCandidate,
19
27
  transformAdmission,
20
28
  semanticEditAdmission,
29
+ evidenceAdmission,
21
30
  reasonCodes: uniqueStrings([
22
31
  ...strings(input.reasonCodes),
23
32
  ...strings(context.source?.reasons),
24
33
  ...strings(context.mergeCandidate?.reasons),
25
34
  ...transformAdmission.reasonCodes,
26
- ...strings(semanticEditAdmission.reasonCodes)
27
- ]),
35
+ ...strings(semanticEditAdmission.reasonCodes),
36
+ ...strings(evidenceAdmission.reasonCodes)
37
+ ].filter(Boolean)),
28
38
  conflictKeys: uniqueStrings([...strings(input.conflictKeys), ...context.conflictKeys]),
29
39
  admittedAt: input.admittedAt,
30
40
  reviewerId: input.reviewerId,
31
- evidenceIds: uniqueStrings([...strings(input.evidenceIds), ...strings(transformAdmission.evidenceIds)]),
41
+ evidenceIds: uniqueStrings([...strings(input.evidenceIds), ...strings(transformAdmission.evidenceIds), ...strings(evidenceAdmission.evidenceIds)]),
32
42
  metadata: input.metadata
33
43
  });
34
44
  }
@@ -74,7 +84,7 @@ function transformReasonCodes(input) {
74
84
  !input.complete ? 'transform-evidence-incomplete' : undefined,
75
85
  input.ready ? 'transform-auto-apply-candidate' : undefined,
76
86
  ...input.readinesses.map((readiness) => `transform-readiness:${readiness}`)
77
- ]);
87
+ ].filter(Boolean));
78
88
  }
79
89
 
80
90
  function transformReadiness(value) {
@@ -87,21 +97,90 @@ function transformReadiness(value) {
87
97
  return undefined;
88
98
  }
89
99
 
90
- function fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, fallback) {
91
- if ([transformAdmission.readiness, semanticEditAdmission.readiness].includes('blocked')) return 'blocked';
92
- if (hasAdmissibleReadyAction(transformAdmission, semanticEditAdmission)) return 'ready';
100
+ function autoMergeEvidenceAdmission(context, admissions) {
101
+ const evidence = uniqueEvidenceRecords([
102
+ ...array(context.evidenceRecords),
103
+ ...array(context.evidence),
104
+ ...array(context.source?.evidence),
105
+ ...array(context.source?.patch?.evidence),
106
+ ...array(context.source?.semanticPatch?.evidence),
107
+ ...array(context.mergeCandidate?.evidence)
108
+ ]);
109
+ const positiveApply = hasPositiveApplyAttempt(admissions.transformAdmission, admissions.semanticEditAdmission);
110
+ if (!positiveApply) return { status: 'none', action: 'none', readiness: 'needs-review', reasonCodes: [], evidenceIds: evidenceIds(evidence) };
111
+ const summary = summarizeAutoMergeEvidence(evidence);
112
+ const blocked = summary.failed > 0 || summary.conflict > 0;
113
+ const ready = !blocked && summary.stale === 0 && summary.passed > 0;
114
+ const status = blocked ? 'blocked' : summary.stale > 0 ? 'stale' : ready ? 'ready' : 'needs-review';
115
+ return compactRecord({
116
+ status,
117
+ action: blocked ? 'block' : status === 'stale' ? 'rerun-semantic-import' : ready ? 'admit' : 'review',
118
+ readiness: blocked ? 'blocked' : ready ? 'ready' : 'needs-review',
119
+ reasonCodes: autoMergeEvidenceReasonCodes(summary, status),
120
+ evidenceIds: summary.evidenceIds,
121
+ passed: summary.passed,
122
+ failed: summary.failed,
123
+ conflict: summary.conflict,
124
+ stale: summary.stale
125
+ });
126
+ }
127
+
128
+ function summarizeAutoMergeEvidence(evidence) {
129
+ const testLike = evidence.filter(isAutoMergeTestEvidence);
130
+ const failed = testLike.filter((record) => evidenceStatus(record, ['failed', 'failure', 'error', 'blocked', 'rejected']));
131
+ const passed = testLike.filter((record) => evidenceStatus(record, ['passed', 'ok', 'success', 'succeeded', 'accepted', 'verified']));
132
+ const conflict = evidence.filter((record) => evidenceStatus(record, ['conflict', 'conflicted']) || record?.metadata?.conflict === true || strings(record?.reasonCodes ?? record?.reasons).some((reason) => reason.toLowerCase().includes('conflict')));
133
+ const stale = evidence.filter((record) => evidenceStatus(record, ['stale']) || record?.metadata?.stale === true || strings(record?.reasonCodes ?? record?.reasons).some((reason) => reason.toLowerCase().includes('stale')));
134
+ return {
135
+ evidenceIds: evidenceIds(evidence),
136
+ passed: passed.length,
137
+ failed: failed.length,
138
+ conflict: conflict.length,
139
+ stale: stale.length
140
+ };
141
+ }
142
+
143
+ function autoMergeEvidenceReasonCodes(summary, status) {
144
+ return uniqueStrings([
145
+ summary.passed ? 'auto-merge-tests-passed' : undefined,
146
+ summary.passed === 0 ? 'auto-merge-tests-passed-evidence-missing' : undefined,
147
+ summary.failed ? 'auto-merge-tests-failed' : undefined,
148
+ summary.conflict ? 'auto-merge-conflict-evidence' : undefined,
149
+ summary.stale ? 'auto-merge-stale-evidence' : undefined,
150
+ status === 'ready' ? 'auto-merge-positive-proof' : undefined
151
+ ].filter(Boolean));
152
+ }
153
+
154
+ function fallbackAdmissionReadiness(transformAdmission, semanticEditAdmission, evidenceAdmission, fallback) {
155
+ if ([transformAdmission.readiness, semanticEditAdmission.readiness, evidenceAdmission.readiness].includes('blocked')) return 'blocked';
156
+ if (hasSkipReadyAction(semanticEditAdmission)) return 'ready';
157
+ if (hasPositiveApplyAction(transformAdmission, semanticEditAdmission)) return evidenceAdmission.action === 'admit' ? 'ready' : evidenceAdmission.readiness;
93
158
  return fallback;
94
159
  }
95
160
 
96
- function admissionStatusForReadiness(readiness, transformAdmission, semanticEditAdmission) {
161
+ function admissionStatusForReadiness(readiness, transformAdmission, semanticEditAdmission, evidenceAdmission) {
97
162
  if (readiness === 'blocked') return 'blocked';
98
- if (hasAdmissibleReadyAction(transformAdmission, semanticEditAdmission) && readiness === 'ready') return 'admitted';
163
+ if (hasAdmissibleReadyAction(transformAdmission, semanticEditAdmission, evidenceAdmission) && readiness === 'ready') return 'admitted';
99
164
  return readiness === 'needs-review' ? 'needs-review' : 'proposed';
100
165
  }
101
166
 
102
- function hasAdmissibleReadyAction(transformAdmission, semanticEditAdmission) {
103
- return [transformAdmission.action, semanticEditAdmission.action].includes('admit') ||
104
- (semanticEditAdmission.action === 'skip' && semanticEditAdmission.readiness === 'ready' && semanticEditAdmission.reviewRequired === false);
167
+ function hasAdmissibleReadyAction(transformAdmission, semanticEditAdmission, evidenceAdmission) {
168
+ return hasSkipReadyAction(semanticEditAdmission) ||
169
+ (hasPositiveApplyAction(transformAdmission, semanticEditAdmission) && evidenceAdmission.action === 'admit');
170
+ }
171
+
172
+ function hasPositiveApplyAction(transformAdmission, semanticEditAdmission) {
173
+ return [transformAdmission.action, semanticEditAdmission.action].includes('admit');
174
+ }
175
+
176
+ function hasPositiveApplyAttempt(transformAdmission, semanticEditAdmission) {
177
+ return hasPositiveApplyAction(transformAdmission, semanticEditAdmission) ||
178
+ Number(semanticEditAdmission.summary?.acceptedClean ?? 0) > 0 ||
179
+ strings(transformAdmission.reasonCodes).includes('transform-readiness:auto-merge-candidate');
180
+ }
181
+
182
+ function hasSkipReadyAction(semanticEditAdmission) {
183
+ return semanticEditAdmission.action === 'skip' && semanticEditAdmission.readiness === 'ready' && semanticEditAdmission.reviewRequired === false;
105
184
  }
106
185
 
107
186
  function hasCrossLanguageTransform(index) {
@@ -109,6 +188,29 @@ function hasCrossLanguageTransform(index) {
109
188
  return strings(index.transformTargetLanguages).some((target) => !source.has(target));
110
189
  }
111
190
 
191
+ function isAutoMergeTestEvidence(record) {
192
+ const kind = String(record?.kind ?? record?.type ?? '').toLowerCase();
193
+ return ['test', 'tests', 'proof', 'gate', 'verification', 'check'].includes(kind);
194
+ }
195
+
196
+ function evidenceStatus(record, statuses) {
197
+ const status = String(record?.status ?? record?.outcome ?? '').toLowerCase();
198
+ return statuses.includes(status);
199
+ }
200
+
201
+ function uniqueEvidenceRecords(records) {
202
+ const seen = new Set();
203
+ const result = [];
204
+ for (const record of records.filter(Boolean)) {
205
+ const key = record.id ?? JSON.stringify(record);
206
+ if (seen.has(key)) continue;
207
+ seen.add(key);
208
+ result.push(record);
209
+ }
210
+ return result;
211
+ }
212
+
213
+ function evidenceIds(evidence) { return uniqueStrings(evidence.map((record) => record.id)); }
112
214
  function array(value) { if (value === undefined || value === null) return []; return Array.isArray(value) ? value : [value]; }
113
215
  function strings(value) { return array(value).map((entry) => String(entry ?? '')).filter(Boolean); }
114
216
  function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }