@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.
- package/dist/declarations/bidirectional-target-change-evidence.d.ts +299 -0
- package/dist/declarations/bidirectional-target-change.d.ts +19 -120
- package/dist/declarations/native-project-admission.d.ts +43 -22
- package/dist/declarations/semantic-edit-bundle.d.ts +13 -1
- package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
- package/dist/declarations/semantic-edit-script.d.ts +53 -51
- package/dist/declarations/semantic-lineage.d.ts +62 -51
- package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
- package/dist/declarations/semantic-patch-bundle.d.ts +13 -0
- package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
- package/dist/declarations/semantic-sidecar.d.ts +12 -14
- package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
- package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
- package/dist/internal/index-impl/createNativeSourcePreservation.js +16 -1
- package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +151 -1
- package/dist/internal/index-impl/createSemanticImportSidecar.js +5 -0
- package/dist/internal/index-impl/createSemanticImportSidecarAdmission.js +29 -11
- package/dist/internal/index-impl/declarationRecord.js +2 -2
- package/dist/internal/index-impl/inferSemanticLineageEvents.js +8 -0
- package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
- package/dist/internal/index-impl/projectImportAdmissionMergeScore.js +26 -74
- package/dist/internal/index-impl/projectImportAdmissionProjectionCoverage.js +74 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +92 -74
- package/dist/internal/index-impl/replaySemanticEditProjection.js +114 -40
- package/dist/internal/index-impl/semanticEditBundleAdmission.js +95 -12
- package/dist/internal/index-impl/semanticEditBundleIndex.js +16 -10
- package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
- package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
- package/dist/internal/index-impl/semanticEditSourceRanges.js +283 -0
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +56 -3
- package/dist/internal/index-impl/semanticIndexFromNativeDeclarations.js +2 -2
- package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +158 -13
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +46 -2
- package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
- package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
- package/dist/internal/index-impl/semanticPatchBundleAdmission.js +122 -20
- package/dist/internal/index-impl/semanticPatchBundleLineageLinks.js +199 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +29 -3
- package/dist/internal/index-impl/semanticPatchBundleRecords.js +28 -104
- package/dist/internal/index-impl/semanticPatchBundleSourceRecords.js +127 -0
- package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
- package/dist/internal/index-impl/sourceTextForSpan.js +4 -9
- package/dist/lightweight-dependency-relations.js +113 -7
- package/dist/native-import-language-profiles.js +10 -2
- package/dist/native-import-utils.js +15 -1
- package/dist/native-region-scanner-js-helpers.js +68 -18
- package/dist/native-region-scanner-js-imports.js +7 -0
- package/dist/native-region-scanner-js.js +16 -8
- package/dist/native-region-scanner.js +2 -1
- package/dist/semantic-import-regions.js +8 -6
- package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
- package/dist/semantic-import-sidecar-entry.js +151 -7
- package/dist/semantic-import-sidecar-types.d.ts +18 -13
- package/dist/semantic-import-source-preservation-utils.js +55 -0
- package/dist/semantic-import-source-preservation.js +98 -3
- 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
|
|
12
|
-
matched.push({
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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))); }
|