@mergesignal/shared 0.7.0 → 0.8.1
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/affectedAreaLinkage.d.ts +14 -0
- package/dist/affectedAreaLinkage.d.ts.map +1 -0
- package/dist/affectedAreaLinkage.js +100 -0
- package/dist/deriveScanNarrative.d.ts.map +1 -1
- package/dist/deriveScanNarrative.js +72 -8
- package/dist/deriveScanNarrative.test.js +61 -0
- package/dist/formatCardExposureDisplay.d.ts +5 -3
- package/dist/formatCardExposureDisplay.d.ts.map +1 -1
- package/dist/formatCardExposureDisplay.js +12 -5
- package/dist/formatCardExposureDisplay.test.js +9 -1
- package/dist/narrativePresentation.d.ts +3 -1
- package/dist/narrativePresentation.d.ts.map +1 -1
- package/dist/narrativePresentation.js +13 -1
- package/dist/presentation/fixtures/scanResultFixtures.d.ts.map +1 -1
- package/dist/presentation/fixtures/scanResultFixtures.js +19 -1
- package/dist/presentation/presentation.test.js +28 -0
- package/dist/presentation/presenters/presentScanDetails.d.ts.map +1 -1
- package/dist/presentation/presenters/presentScanDetails.js +2 -1
- package/dist/riskSignals.d.ts +19 -0
- package/dist/riskSignals.d.ts.map +1 -0
- package/dist/riskSignals.js +32 -0
- package/dist/scanNarrativeFacts.d.ts +25 -7
- package/dist/scanNarrativeFacts.d.ts.map +1 -1
- package/dist/scanNarrativeFacts.js +4 -1
- package/dist/scanPresentationBundle.golden.test.d.ts +2 -0
- package/dist/scanPresentationBundle.golden.test.d.ts.map +1 -0
- package/dist/scanPresentationBundle.golden.test.js +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AffectedAreaFact, ChangedPackageSemantics, NarrativePackageUsage, ScanNarrativeFacts } from "./scanNarrativeFacts.js";
|
|
2
|
+
import type { Finding } from "./types.js";
|
|
3
|
+
export declare function areaIdFromLabel(label: string): string;
|
|
4
|
+
export declare function enrichAffectedAreaFacts(areas: Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}>, ctx: {
|
|
8
|
+
findings: Finding[];
|
|
9
|
+
packageUsage: NarrativePackageUsage[];
|
|
10
|
+
changedPackageSemantics: ChangedPackageSemantics[];
|
|
11
|
+
hotspots: ScanNarrativeFacts["hotspots"];
|
|
12
|
+
applicationAreaIds: ReadonlySet<string>;
|
|
13
|
+
}): AffectedAreaFact[];
|
|
14
|
+
//# sourceMappingURL=affectedAreaLinkage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affectedAreaLinkage.d.ts","sourceRoot":"","sources":["../src/affectedAreaLinkage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EAErB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ1C,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAuGD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3C,GAAG,EAAE;IACH,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,EAAE,qBAAqB,EAAE,CAAC;IACtC,uBAAuB,EAAE,uBAAuB,EAAE,CAAC;IACnD,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACzC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACzC,GACA,gBAAgB,EAAE,CAwBpB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const EVIDENCE_STRENGTH_RANK = {
|
|
2
|
+
high: 3,
|
|
3
|
+
medium: 2,
|
|
4
|
+
low: 1,
|
|
5
|
+
};
|
|
6
|
+
export function areaIdFromLabel(label) {
|
|
7
|
+
return label.toLowerCase().replace(/\s+/g, "_").slice(0, 48);
|
|
8
|
+
}
|
|
9
|
+
function packagesForArea(area, packageUsage, applicationAreaIds) {
|
|
10
|
+
const linked = new Set();
|
|
11
|
+
for (const row of packageUsage) {
|
|
12
|
+
for (const usageArea of row.areas) {
|
|
13
|
+
if (areaIdFromLabel(usageArea) === area.id ||
|
|
14
|
+
usageArea.toLowerCase() === area.label.toLowerCase()) {
|
|
15
|
+
linked.add(row.packageName);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (applicationAreaIds.has(area.id)) {
|
|
20
|
+
for (const row of packageUsage) {
|
|
21
|
+
linked.add(row.packageName);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...linked].sort((a, b) => a.localeCompare(b));
|
|
25
|
+
}
|
|
26
|
+
function findingIdsForPackages(findings, packages) {
|
|
27
|
+
if (packages.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
const pkgSet = new Set(packages);
|
|
30
|
+
return findings.filter((f) => pkgSet.has(f.packageName)).map((f) => f.id);
|
|
31
|
+
}
|
|
32
|
+
function pathsForPackages(packageUsage, packages) {
|
|
33
|
+
const pkgSet = new Set(packages);
|
|
34
|
+
const paths = [];
|
|
35
|
+
for (const row of packageUsage) {
|
|
36
|
+
if (!pkgSet.has(row.packageName))
|
|
37
|
+
continue;
|
|
38
|
+
paths.push(...row.paths, ...row.criticalPaths, ...row.files);
|
|
39
|
+
}
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
return paths.filter((p) => {
|
|
42
|
+
if (!p || seen.has(p))
|
|
43
|
+
return false;
|
|
44
|
+
seen.add(p);
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function evidenceStrengthForPackages(semantics, packages) {
|
|
49
|
+
const pkgSet = new Set(packages);
|
|
50
|
+
let best = null;
|
|
51
|
+
let bestRank = 0;
|
|
52
|
+
for (const row of semantics) {
|
|
53
|
+
if (!pkgSet.has(row.packageName) || !row.evidenceStrength)
|
|
54
|
+
continue;
|
|
55
|
+
const rank = EVIDENCE_STRENGTH_RANK[row.evidenceStrength];
|
|
56
|
+
if (rank > bestRank) {
|
|
57
|
+
bestRank = rank;
|
|
58
|
+
best = row.evidenceStrength;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return best;
|
|
62
|
+
}
|
|
63
|
+
function hotspotPackagesForPackages(hotspots, packages) {
|
|
64
|
+
const pkgSet = new Set(packages);
|
|
65
|
+
const linked = new Set();
|
|
66
|
+
for (const hotspot of hotspots) {
|
|
67
|
+
if (pkgSet.has(hotspot.packageName)) {
|
|
68
|
+
linked.add(hotspot.packageName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [...linked].sort((a, b) => a.localeCompare(b));
|
|
72
|
+
}
|
|
73
|
+
function verificationFocusForPackages(semantics, packages) {
|
|
74
|
+
const pkgSet = new Set(packages);
|
|
75
|
+
const focus = new Set();
|
|
76
|
+
for (const row of semantics) {
|
|
77
|
+
if (!pkgSet.has(row.packageName))
|
|
78
|
+
continue;
|
|
79
|
+
for (const item of row.verificationFocus) {
|
|
80
|
+
if (item.trim())
|
|
81
|
+
focus.add(item.trim());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...focus].sort((a, b) => a.localeCompare(b));
|
|
85
|
+
}
|
|
86
|
+
export function enrichAffectedAreaFacts(areas, ctx) {
|
|
87
|
+
return areas.map((area) => {
|
|
88
|
+
const packages = packagesForArea(area, ctx.packageUsage, ctx.applicationAreaIds);
|
|
89
|
+
return {
|
|
90
|
+
id: area.id,
|
|
91
|
+
label: area.label,
|
|
92
|
+
packages,
|
|
93
|
+
findingIds: findingIdsForPackages(ctx.findings, packages),
|
|
94
|
+
paths: pathsForPackages(ctx.packageUsage, packages),
|
|
95
|
+
evidenceStrength: evidenceStrengthForPackages(ctx.changedPackageSemantics, packages),
|
|
96
|
+
hotspotPackages: hotspotPackagesForPackages(ctx.hotspots, packages),
|
|
97
|
+
verificationFocus: verificationFocusForPackages(ctx.changedPackageSemantics, packages),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"AAYA,OAAO,EAgBL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAMV,UAAU,EACX,MAAM,YAAY,CAAC;AAoiBpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAsIpB"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { extractRepositoryContextFacts } from "./extractRepositoryContextFacts.js";
|
|
2
|
+
import { areaIdFromLabel, enrichAffectedAreaFacts, } from "./affectedAreaLinkage.js";
|
|
2
3
|
import { readBlastLevel, safeParseRepoIntelligence, } from "./repoIntelligenceSchema.js";
|
|
3
4
|
import { isRuntimeNarrativeSafe } from "./repoIntelligenceSemantics.js";
|
|
5
|
+
import { deriveRiskSignals } from "./riskSignals.js";
|
|
4
6
|
import { EMPTY_SCAN_NARRATIVE_FACTS, NARRATIVE_FACT_MAX_AREAS, NARRATIVE_FACT_MAX_HOTSPOTS, NARRATIVE_FACT_MAX_PATHS, } from "./scanNarrativeFacts.js";
|
|
5
7
|
import { mergePostureFromDecision } from "./riskVocabulary.js";
|
|
6
8
|
import { selectTopAffectedAreas } from "./selectTopAffectedAreas.js";
|
|
@@ -224,13 +226,15 @@ function extractTier1FromRepoIntelligence(ri, changed) {
|
|
|
224
226
|
}
|
|
225
227
|
: null;
|
|
226
228
|
const areasRaw = ri.applicationAreas ?? ri.affectedAreas ?? [];
|
|
229
|
+
const applicationAreaIds = new Set();
|
|
227
230
|
const affectedAreas = [];
|
|
228
231
|
for (const area of areasRaw) {
|
|
232
|
+
applicationAreaIds.add(area.id);
|
|
229
233
|
affectedAreas.push({ id: area.id, label: area.label });
|
|
230
234
|
}
|
|
231
235
|
for (const row of packageUsage) {
|
|
232
236
|
for (const label of row.areas) {
|
|
233
|
-
const id = label
|
|
237
|
+
const id = areaIdFromLabel(label);
|
|
234
238
|
if (!affectedAreas.some((a) => a.id === id)) {
|
|
235
239
|
affectedAreas.push({ id, label });
|
|
236
240
|
}
|
|
@@ -247,6 +251,7 @@ function extractTier1FromRepoIntelligence(ri, changed) {
|
|
|
247
251
|
blastRadius,
|
|
248
252
|
affectedAreas: affectedAreas.slice(0, NARRATIVE_FACT_MAX_AREAS),
|
|
249
253
|
hotspots: codeHotspots.slice(0, NARRATIVE_FACT_MAX_HOTSPOTS),
|
|
254
|
+
applicationAreaIds,
|
|
250
255
|
};
|
|
251
256
|
}
|
|
252
257
|
function hasMeaningfulTier1(tier1) {
|
|
@@ -341,6 +346,7 @@ function emptyTier1Blocks() {
|
|
|
341
346
|
blastRadius: null,
|
|
342
347
|
affectedAreas: [],
|
|
343
348
|
hotspots: [],
|
|
349
|
+
applicationAreaIds: new Set(),
|
|
344
350
|
};
|
|
345
351
|
}
|
|
346
352
|
function blastRadiusFromChangedCount(count) {
|
|
@@ -368,6 +374,38 @@ function resolveTrustedRepoIntelligence(result) {
|
|
|
368
374
|
}
|
|
369
375
|
return { parsed: null, parseStatus: "invalid" };
|
|
370
376
|
}
|
|
377
|
+
function preparationWarningsFromResult(result) {
|
|
378
|
+
const warnings = result.analysisPreparation?.warnings;
|
|
379
|
+
if (!Array.isArray(warnings))
|
|
380
|
+
return [];
|
|
381
|
+
return warnings.map((w) => ({
|
|
382
|
+
code: w.code,
|
|
383
|
+
message: w.message,
|
|
384
|
+
...(w.details != null ? { details: w.details } : {}),
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
function resolveCorpusGateReason(input) {
|
|
388
|
+
if (input.corpusGate)
|
|
389
|
+
return "ok";
|
|
390
|
+
if (!input.codeIntelligenceAvailable)
|
|
391
|
+
return "no_code_intelligence";
|
|
392
|
+
if (input.repoIntelligenceParse === "invalid" ||
|
|
393
|
+
input.repoIntelligenceParse === "untrusted") {
|
|
394
|
+
return "repo_intelligence_invalid";
|
|
395
|
+
}
|
|
396
|
+
return "repo_intelligence_absent";
|
|
397
|
+
}
|
|
398
|
+
function assessmentConfidenceFromResult(result) {
|
|
399
|
+
const c = result.assessment?.confidence;
|
|
400
|
+
if (c === "low" || c === "medium" || c === "high")
|
|
401
|
+
return c;
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
function limitedContextDiagnostic(input) {
|
|
405
|
+
return (input.corpusGateReason !== "ok" ||
|
|
406
|
+
input.preparationWarnings.length > 0 ||
|
|
407
|
+
input.assessmentConfidence === "low");
|
|
408
|
+
}
|
|
371
409
|
/**
|
|
372
410
|
* Consumer-agnostic scan intelligence — structured facts only (no UI copy).
|
|
373
411
|
*/
|
|
@@ -377,23 +415,29 @@ export function deriveScanNarrative(result) {
|
|
|
377
415
|
}
|
|
378
416
|
const changedPackages = changedPackagesFromAssessment(result);
|
|
379
417
|
const mergePosture = mergePostureFromDecision(result.decision?.recommendation) ?? null;
|
|
380
|
-
const
|
|
381
|
-
? result.totalScore
|
|
382
|
-
: null;
|
|
418
|
+
const preparationWarnings = preparationWarningsFromResult(result);
|
|
383
419
|
const codeIntelligenceAvailable = result.analysisPreparation?.codeIntelligenceAvailable !== false;
|
|
384
420
|
const { parsed: parsedRi, parseStatus: repoIntelligenceParse } = resolveTrustedRepoIntelligence(result);
|
|
385
421
|
const corpusGate = codeIntelligenceAvailable &&
|
|
386
422
|
repoIntelligenceParse === "ok" &&
|
|
387
423
|
parsedRi != null;
|
|
424
|
+
const corpusGateReason = resolveCorpusGateReason({
|
|
425
|
+
codeIntelligenceAvailable,
|
|
426
|
+
repoIntelligenceParse,
|
|
427
|
+
corpusGate,
|
|
428
|
+
});
|
|
388
429
|
let tier1Blocks = emptyTier1Blocks();
|
|
430
|
+
let applicationAreaIds = tier1Blocks.applicationAreaIds;
|
|
389
431
|
if (corpusGate && parsedRi) {
|
|
390
432
|
tier1Blocks = extractTier1FromRepoIntelligence(parsedRi, changedPackages);
|
|
433
|
+
applicationAreaIds = tier1Blocks.applicationAreaIds;
|
|
391
434
|
}
|
|
392
435
|
else if (changedPackages.all.length > 0) {
|
|
393
436
|
tier1Blocks = {
|
|
394
437
|
...emptyTier1Blocks(),
|
|
395
438
|
blastRadius: blastRadiusFromChangedCount(changedPackages.all.length),
|
|
396
439
|
};
|
|
440
|
+
applicationAreaIds = tier1Blocks.applicationAreaIds;
|
|
397
441
|
}
|
|
398
442
|
tier1Blocks.hotspots = mergeGraphHotspots(tier1Blocks.hotspots, result.graphInsights?.hotspots);
|
|
399
443
|
const tier1 = corpusGate && hasMeaningfulTier1(tier1Blocks);
|
|
@@ -403,23 +447,40 @@ export function deriveScanNarrative(result) {
|
|
|
403
447
|
const tier3 = repositoryContext.length > 0;
|
|
404
448
|
const mode = resolveAvailabilityMode(tier1, tier2 || reviewerGuidance.length > 0, codeIntelligenceAvailable);
|
|
405
449
|
const decisionConf = result.decision?.confidence;
|
|
450
|
+
const assessmentConfidence = assessmentConfidenceFromResult(result);
|
|
406
451
|
const confidence = {
|
|
407
452
|
decision: decisionConf === "low" ||
|
|
408
453
|
decisionConf === "medium" ||
|
|
409
454
|
decisionConf === "high"
|
|
410
455
|
? decisionConf
|
|
411
456
|
: null,
|
|
457
|
+
assessment: assessmentConfidence,
|
|
458
|
+
limitedContext: limitedContextDiagnostic({
|
|
459
|
+
corpusGateReason,
|
|
460
|
+
preparationWarnings,
|
|
461
|
+
assessmentConfidence,
|
|
462
|
+
}),
|
|
412
463
|
};
|
|
413
464
|
if (!tier1 && mode === "graph_fallback" && changedPackages.all.length > 0) {
|
|
414
465
|
tier1Blocks.blastRadius = blastRadiusFromChangedCount(changedPackages.all.length);
|
|
415
466
|
}
|
|
416
|
-
let
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
id: label
|
|
467
|
+
let affectedAreaSeeds = tier1Blocks.affectedAreas;
|
|
468
|
+
if (affectedAreaSeeds.length === 0 && mode !== "pr_intelligence") {
|
|
469
|
+
affectedAreaSeeds = selectTopAffectedAreas(result, { max: 2 }).map((label) => ({
|
|
470
|
+
id: areaIdFromLabel(label),
|
|
420
471
|
label,
|
|
421
472
|
}));
|
|
422
473
|
}
|
|
474
|
+
const findings = Array.isArray(result.findings) ? result.findings : [];
|
|
475
|
+
const affectedAreas = enrichAffectedAreaFacts(affectedAreaSeeds, {
|
|
476
|
+
findings,
|
|
477
|
+
packageUsage: tier1Blocks.packageUsage,
|
|
478
|
+
changedPackageSemantics: tier1Blocks.changedPackageSemantics,
|
|
479
|
+
hotspots: tier1Blocks.hotspots,
|
|
480
|
+
applicationAreaIds,
|
|
481
|
+
});
|
|
482
|
+
const riskSignals = deriveRiskSignals(result);
|
|
483
|
+
const riskIndex = riskSignals?.riskIndex ?? null;
|
|
423
484
|
return {
|
|
424
485
|
availability: {
|
|
425
486
|
mode,
|
|
@@ -430,6 +491,8 @@ export function deriveScanNarrative(result) {
|
|
|
430
491
|
tier3,
|
|
431
492
|
},
|
|
432
493
|
repoIntelligenceParse,
|
|
494
|
+
preparationWarnings,
|
|
495
|
+
corpusGateReason,
|
|
433
496
|
},
|
|
434
497
|
changedPackages,
|
|
435
498
|
packageUsage: tier1Blocks.packageUsage,
|
|
@@ -446,5 +509,6 @@ export function deriveScanNarrative(result) {
|
|
|
446
509
|
repositoryContext,
|
|
447
510
|
mergePosture,
|
|
448
511
|
riskIndex,
|
|
512
|
+
riskSignals,
|
|
449
513
|
};
|
|
450
514
|
}
|
|
@@ -210,4 +210,65 @@ describe("deriveScanNarrative", () => {
|
|
|
210
210
|
expect(facts.availability.repoIntelligenceParse).toBe("absent");
|
|
211
211
|
expect(facts.availability.tiersPresent.tier1).toBe(false);
|
|
212
212
|
});
|
|
213
|
+
it("projects preparation warnings and corpus gate reason", () => {
|
|
214
|
+
const result = {
|
|
215
|
+
...baseResult,
|
|
216
|
+
changedPackages: ["lodash"],
|
|
217
|
+
analysisPreparation: {
|
|
218
|
+
codeIntelligenceAvailable: false,
|
|
219
|
+
warnings: [
|
|
220
|
+
{
|
|
221
|
+
code: "base_lockfile_missing",
|
|
222
|
+
message: "Base lockfile not available",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const facts = deriveScanNarrative(result);
|
|
228
|
+
expect(facts.availability.preparationWarnings).toHaveLength(1);
|
|
229
|
+
expect(facts.availability.preparationWarnings[0]?.code).toBe("base_lockfile_missing");
|
|
230
|
+
expect(facts.availability.corpusGateReason).toBe("no_code_intelligence");
|
|
231
|
+
expect(facts.confidence.limitedContext).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
it("links affected areas to packages, findings, paths, and verification", () => {
|
|
234
|
+
const result = {
|
|
235
|
+
...baseResult,
|
|
236
|
+
changedPackages: ["fastify"],
|
|
237
|
+
analysisPreparation: analysisPreparationWithValidRepoIntel(),
|
|
238
|
+
repoIntelligence: fixtureRepoIntelligenceFastify,
|
|
239
|
+
findings: [
|
|
240
|
+
{
|
|
241
|
+
id: "f1",
|
|
242
|
+
title: "Auth middleware",
|
|
243
|
+
description: "d",
|
|
244
|
+
severity: "high",
|
|
245
|
+
packageName: "fastify",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
const facts = deriveScanNarrative(result);
|
|
250
|
+
const apiArea = facts.affectedAreas.find((a) => a.id === "api");
|
|
251
|
+
expect(apiArea).toBeDefined();
|
|
252
|
+
expect(apiArea?.packages).toContain("fastify");
|
|
253
|
+
expect(apiArea?.findingIds).toContain("f1");
|
|
254
|
+
expect(apiArea?.paths.length).toBeGreaterThan(0);
|
|
255
|
+
expect(apiArea?.evidenceStrength).toBe("high");
|
|
256
|
+
expect(apiArea?.hotspotPackages).toContain("fastify");
|
|
257
|
+
expect(apiArea?.verificationFocus).toContain("routes");
|
|
258
|
+
});
|
|
259
|
+
it("derives riskSignals with exposure layers", () => {
|
|
260
|
+
const facts = deriveScanNarrative(baseResult);
|
|
261
|
+
expect(facts.riskSignals?.riskIndex).toBe(10);
|
|
262
|
+
expect(facts.riskSignals?.exposure).toBe("minimal");
|
|
263
|
+
expect(facts.riskSignals?.layers).toHaveLength(4);
|
|
264
|
+
expect(facts.riskIndex).toBe(10);
|
|
265
|
+
});
|
|
266
|
+
it("projects assessment confidence verbatim", () => {
|
|
267
|
+
const result = {
|
|
268
|
+
...baseResult,
|
|
269
|
+
assessment: assessmentFastifyRuntime,
|
|
270
|
+
};
|
|
271
|
+
const facts = deriveScanNarrative(result);
|
|
272
|
+
expect(facts.confidence.assessment).toBe("medium");
|
|
273
|
+
});
|
|
213
274
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Canonical exposure bucket thresholds for numeric risk scores (0–100).
|
|
3
|
+
* Consumed by `deriveRiskSignals` for bundle facts; label formatters remain for aria copy.
|
|
4
4
|
*/
|
|
5
5
|
export type CardExposureCategory = "minimal" | "limited" | "moderate" | "elevated" | "broad";
|
|
6
6
|
export type CardExposureDisplay = {
|
|
@@ -11,7 +11,9 @@ export type CardExposureDisplay = {
|
|
|
11
11
|
};
|
|
12
12
|
/** User-facing exposure labels for docs and UI copy (low → high). */
|
|
13
13
|
export declare const CARD_EXPOSURE_CATEGORY_LABELS: readonly string[];
|
|
14
|
-
/** Map numeric
|
|
14
|
+
/** Map numeric score to a fixed exposure category (canonical thresholds). */
|
|
15
|
+
export declare function deriveCardExposureCategory(score: number | null | undefined): CardExposureCategory | null;
|
|
16
|
+
/** Map numeric totalScore to exposure category + display label. */
|
|
15
17
|
export declare function deriveCardExposureDisplay(score: number | null | undefined): CardExposureDisplay | null;
|
|
16
18
|
/** Compact card line: exposure category label only. */
|
|
17
19
|
export declare function formatCardExposureLine(score: number | null | undefined): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatCardExposureDisplay.d.ts","sourceRoot":"","sources":["../src/formatCardExposureDisplay.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,CAAC;AAEZ,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAUF,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAM1D,CAAC;AAkBF,
|
|
1
|
+
{"version":3,"file":"formatCardExposureDisplay.d.ts","sourceRoot":"","sources":["../src/formatCardExposureDisplay.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,CAAC;AAEZ,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAUF,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAM1D,CAAC;AAkBF,6EAA6E;AAC7E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,oBAAoB,GAAG,IAAI,CAK7B;AAED,mEAAmE;AACnE,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,mBAAmB,GAAG,IAAI,CAK5B;AAED,uDAAuD;AACvD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAIf;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Canonical exposure bucket thresholds for numeric risk scores (0–100).
|
|
3
|
+
* Consumed by `deriveRiskSignals` for bundle facts; label formatters remain for aria copy.
|
|
4
4
|
*/
|
|
5
5
|
const EXPOSURE_LABEL = {
|
|
6
6
|
minimal: "Minimal exposure",
|
|
@@ -31,13 +31,20 @@ function clampScore(score) {
|
|
|
31
31
|
return 0;
|
|
32
32
|
return Math.min(100, Math.max(0, Math.round(score)));
|
|
33
33
|
}
|
|
34
|
-
/** Map numeric
|
|
35
|
-
export function
|
|
34
|
+
/** Map numeric score to a fixed exposure category (canonical thresholds). */
|
|
35
|
+
export function deriveCardExposureCategory(score) {
|
|
36
36
|
if (score == null || !Number.isFinite(score))
|
|
37
37
|
return null;
|
|
38
38
|
const value = clampScore(score);
|
|
39
39
|
const idx = EXPOSURE_UPPER_BOUND.findIndex((bound) => value <= bound);
|
|
40
|
-
|
|
40
|
+
return EXPOSURE_ORDER[idx >= 0 ? idx : EXPOSURE_ORDER.length - 1];
|
|
41
|
+
}
|
|
42
|
+
/** Map numeric totalScore to exposure category + display label. */
|
|
43
|
+
export function deriveCardExposureDisplay(score) {
|
|
44
|
+
const category = deriveCardExposureCategory(score);
|
|
45
|
+
if (category == null)
|
|
46
|
+
return null;
|
|
47
|
+
const value = clampScore(score);
|
|
41
48
|
return { category, label: EXPOSURE_LABEL[category], value };
|
|
42
49
|
}
|
|
43
50
|
/** Compact card line: exposure category label only. */
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { deriveCardExposureDisplay, exposureAriaFragment, formatCardExposureLine, } from "./formatCardExposureDisplay.js";
|
|
2
|
+
import { deriveCardExposureCategory, deriveCardExposureDisplay, exposureAriaFragment, formatCardExposureLine, } from "./formatCardExposureDisplay.js";
|
|
3
|
+
describe("deriveCardExposureCategory", () => {
|
|
4
|
+
it("maps score ranges to exposure categories", () => {
|
|
5
|
+
expect(deriveCardExposureCategory(0)).toBe("minimal");
|
|
6
|
+
expect(deriveCardExposureCategory(45)).toBe("moderate");
|
|
7
|
+
expect(deriveCardExposureCategory(90)).toBe("broad");
|
|
8
|
+
expect(deriveCardExposureCategory(null)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
3
11
|
describe("deriveCardExposureDisplay", () => {
|
|
4
12
|
it("maps score to five fixed exposure buckets", () => {
|
|
5
13
|
expect(deriveCardExposureDisplay(0)?.category).toBe("minimal");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { ScanNarrativeFacts } from "./scanNarrativeFacts.js";
|
|
1
|
+
import type { NarrativePackageUsage, ScanNarrativeFacts } from "./scanNarrativeFacts.js";
|
|
2
|
+
/** Deduplicated union of wire usage path buckets — matches reachability/linkage semantics. */
|
|
3
|
+
export declare function normalizedPackageUsagePaths(row: NarrativePackageUsage): string[];
|
|
2
4
|
export type SummarizePackageUsageOptions = {
|
|
3
5
|
maxPaths?: number;
|
|
4
6
|
maxPackages?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrativePresentation.d.ts","sourceRoot":"","sources":["../src/narrativePresentation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"narrativePresentation.d.ts","sourceRoot":"","sources":["../src/narrativePresentation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
|
|
2
|
+
/** Deduplicated union of wire usage path buckets — matches reachability/linkage semantics. */
|
|
3
|
+
export function normalizedPackageUsagePaths(row) {
|
|
4
|
+
const paths = [];
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
for (const p of [...row.paths, ...row.criticalPaths, ...row.files]) {
|
|
7
|
+
if (!p || seen.has(p))
|
|
8
|
+
continue;
|
|
9
|
+
seen.add(p);
|
|
10
|
+
paths.push(p);
|
|
11
|
+
}
|
|
12
|
+
return paths;
|
|
13
|
+
}
|
|
2
14
|
export function selectPrimaryChangedPackage(facts) {
|
|
3
15
|
return facts.changedPackages.primary;
|
|
4
16
|
}
|
|
@@ -28,7 +40,7 @@ export function summarizePackageUsage(facts, options = {}) {
|
|
|
28
40
|
const packageNames = [];
|
|
29
41
|
for (const row of rows) {
|
|
30
42
|
packageNames.push(row.packageName);
|
|
31
|
-
const rowPaths =
|
|
43
|
+
const rowPaths = normalizedPackageUsagePaths(row);
|
|
32
44
|
pathCount += rowPaths.length;
|
|
33
45
|
for (const p of rowPaths) {
|
|
34
46
|
if (pathSamples.length >= maxPaths)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanResultFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/scanResultFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAYjD,eAAO,MAAM,wBAAwB,EAAE,
|
|
1
|
+
{"version":3,"file":"scanResultFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/scanResultFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAYjD,eAAO,MAAM,wBAAwB,EAAE,UA0CtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,UA2BvC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAqCtC,CAAC"}
|
|
@@ -9,7 +9,16 @@ export const scanResultFastifyRuntime = {
|
|
|
9
9
|
ecosystem: 30,
|
|
10
10
|
upgradeImpact: 15,
|
|
11
11
|
},
|
|
12
|
-
findings: [
|
|
12
|
+
findings: [
|
|
13
|
+
{
|
|
14
|
+
id: "finding-fastify-runtime",
|
|
15
|
+
title: "Runtime middleware coupling",
|
|
16
|
+
description: "Fastify middleware ordering may affect auth paths.",
|
|
17
|
+
severity: "medium",
|
|
18
|
+
packageName: "fastify",
|
|
19
|
+
recommendation: "Run auth smoke tests",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
13
22
|
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
14
23
|
changedPackages: ["fastify"],
|
|
15
24
|
analysisPreparation: analysisPreparationWithValidRepoIntel(),
|
|
@@ -74,6 +83,15 @@ export const scanResultLimitedContext = {
|
|
|
74
83
|
findings: [],
|
|
75
84
|
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
76
85
|
changedPackages: ["lodash"],
|
|
86
|
+
analysisPreparation: {
|
|
87
|
+
codeIntelligenceAvailable: false,
|
|
88
|
+
warnings: [
|
|
89
|
+
{
|
|
90
|
+
code: "base_lockfile_missing",
|
|
91
|
+
message: "Base lockfile not available for diff",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
77
95
|
repoIntelligence: { invalid: true },
|
|
78
96
|
assessment: assessmentLimitedContext,
|
|
79
97
|
graphInsights: {
|
|
@@ -6,6 +6,7 @@ import { presentGitHubCheckRun } from "./presenters/presentGitHubCheckRun.js";
|
|
|
6
6
|
import { presentGitHubPrComment } from "./presenters/presentGitHubPrComment.js";
|
|
7
7
|
import { presentCliScanSummary } from "./presenters/presentCliScanSummary.js";
|
|
8
8
|
import { scanResultFastifyRuntime, scanResultLimitedContext, scanResultTypescriptPatch, } from "./fixtures/scanResultFixtures.js";
|
|
9
|
+
import { scanResultEslint } from "./fixtures/presentationPersonaFixtures.js";
|
|
9
10
|
const SCAN_ID = "22222222-2222-4222-8222-222222222222";
|
|
10
11
|
const ORIGIN = "https://app.example.com";
|
|
11
12
|
describe("buildScanPresentationBundle", () => {
|
|
@@ -69,6 +70,33 @@ describe("presentDashboardCard personas", () => {
|
|
|
69
70
|
expect(card.verification.length).toBe(0);
|
|
70
71
|
});
|
|
71
72
|
});
|
|
73
|
+
describe("presentScanDetails usage paths", () => {
|
|
74
|
+
it("maps wire usage.files into detailPresentation.usage.items[].paths", () => {
|
|
75
|
+
const bundle = buildScanPresentationBundle({
|
|
76
|
+
result: scanResultEslint,
|
|
77
|
+
pipelineStatus: "done",
|
|
78
|
+
});
|
|
79
|
+
const details = presentScanDetails(bundle, { scanId: SCAN_ID });
|
|
80
|
+
const eslintUsage = details.usage?.items.find((item) => item.packageName === "eslint");
|
|
81
|
+
expect(eslintUsage).toBeDefined();
|
|
82
|
+
expect(eslintUsage.paths).toEqual([".eslintrc.cjs"]);
|
|
83
|
+
expect(bundle.facts.packageUsage[0]?.paths).toEqual([]);
|
|
84
|
+
expect(bundle.facts.packageUsage[0]?.files).toEqual([".eslintrc.cjs"]);
|
|
85
|
+
});
|
|
86
|
+
it("deduplicates paths across paths, criticalPaths, and files buckets", () => {
|
|
87
|
+
const bundle = buildScanPresentationBundle({
|
|
88
|
+
result: scanResultFastifyRuntime,
|
|
89
|
+
pipelineStatus: "done",
|
|
90
|
+
});
|
|
91
|
+
const details = presentScanDetails(bundle, { scanId: SCAN_ID });
|
|
92
|
+
const fastifyUsage = details.usage?.items.find((item) => item.packageName === "fastify");
|
|
93
|
+
expect(fastifyUsage).toBeDefined();
|
|
94
|
+
expect(fastifyUsage.paths.length).toBeGreaterThan(0);
|
|
95
|
+
expect(fastifyUsage.paths).toContain("apps/api/src/server.ts");
|
|
96
|
+
expect(fastifyUsage.paths).toContain("apps/api/src/middleware/auth.ts");
|
|
97
|
+
expect(new Set(fastifyUsage.paths).size).toBe(fastifyUsage.paths.length);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
72
100
|
describe("presentation parity across surfaces", () => {
|
|
73
101
|
it("same bundle drives card, details, GitHub, CLI with identical profile", () => {
|
|
74
102
|
const bundle = buildScanPresentationBundle({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presentScanDetails.d.ts","sourceRoot":"","sources":["../../../src/presentation/presenters/presentScanDetails.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"presentScanDetails.d.ts","sourceRoot":"","sources":["../../../src/presentation/presenters/presentScanDetails.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAGzF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAmDF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,sBAAsB,EAC9B,GAAG,EAAE,yBAAyB,GAC7B,uBAAuB,CAwEzB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MERGE_CONCERN_LABELS } from "../../assessmentLabels.js";
|
|
2
|
+
import { normalizedPackageUsagePaths } from "../../narrativePresentation.js";
|
|
2
3
|
import { MERGE_POSTURE_LABEL } from "../../riskVocabulary.js";
|
|
3
4
|
import { buildNarrativeChannels, composeSubheadline, composeSupportingContext, evidenceContextFromProfile, projectCompactKeyPoints, } from "../compose/narrativeCompose.js";
|
|
4
5
|
import { normalizeGeneratedText, normalizeGeneratedTextNullable, } from "../../normalizeGeneratedText.js";
|
|
@@ -15,7 +16,7 @@ function mapUsage(bundle) {
|
|
|
15
16
|
: undefined,
|
|
16
17
|
items: facts.packageUsage.map((row) => ({
|
|
17
18
|
packageName: row.packageName,
|
|
18
|
-
paths: row
|
|
19
|
+
paths: normalizedPackageUsagePaths(row),
|
|
19
20
|
areas: row.areas,
|
|
20
21
|
criticalPaths: row.criticalPaths,
|
|
21
22
|
})),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CardExposureCategory } from "./formatCardExposureDisplay.js";
|
|
2
|
+
import type { ScanResult, ScoreLayer } from "./types.js";
|
|
3
|
+
export type RiskSignalLayer = {
|
|
4
|
+
layer: ScoreLayer;
|
|
5
|
+
score: number;
|
|
6
|
+
exposure: CardExposureCategory;
|
|
7
|
+
};
|
|
8
|
+
/** Canonical exposure classification for totalScore and layerScores. */
|
|
9
|
+
export type RiskSignals = {
|
|
10
|
+
riskIndex: number | null;
|
|
11
|
+
exposure: CardExposureCategory | null;
|
|
12
|
+
layers: RiskSignalLayer[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Single owner for numeric risk index + exposure bucket classification.
|
|
16
|
+
* Surfaces must consume `facts.riskSignals` in future phases — do not re-derive bands.
|
|
17
|
+
*/
|
|
18
|
+
export declare function deriveRiskSignals(result: ScanResult): RiskSignals | null;
|
|
19
|
+
//# sourceMappingURL=riskSignals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"riskSignals.d.ts","sourceRoot":"","sources":["../src/riskSignals.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAe,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,oBAAoB,CAAC;CAChC,CAAC;AAEF,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACtC,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B,CAAC;AASF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,CAwBxE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { deriveCardExposureCategory, } from "./formatCardExposureDisplay.js";
|
|
2
|
+
const LAYER_ORDER = [
|
|
3
|
+
"security",
|
|
4
|
+
"maintainability",
|
|
5
|
+
"ecosystem",
|
|
6
|
+
"upgradeImpact",
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Single owner for numeric risk index + exposure bucket classification.
|
|
10
|
+
* Surfaces must consume `facts.riskSignals` in future phases — do not re-derive bands.
|
|
11
|
+
*/
|
|
12
|
+
export function deriveRiskSignals(result) {
|
|
13
|
+
const riskIndex = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
|
|
14
|
+
? result.totalScore
|
|
15
|
+
: null;
|
|
16
|
+
const layerScores = result.layerScores;
|
|
17
|
+
if (riskIndex == null && layerScores == null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const exposure = deriveCardExposureCategory(riskIndex);
|
|
21
|
+
const layers = layerScores
|
|
22
|
+
? LAYER_ORDER.map((layer) => {
|
|
23
|
+
const score = layerScores[layer];
|
|
24
|
+
return {
|
|
25
|
+
layer,
|
|
26
|
+
score,
|
|
27
|
+
exposure: deriveCardExposureCategory(score) ?? "minimal",
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
: [];
|
|
31
|
+
return { riskIndex, exposure, layers };
|
|
32
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ObservationSignalFamily } from "./cardObservationCatalog.js";
|
|
2
2
|
import type { MergePosture } from "./riskVocabulary.js";
|
|
3
|
+
import type { RiskSignals } from "./riskSignals.js";
|
|
4
|
+
import type { AnalysisContextWarning } from "./types.js";
|
|
3
5
|
export type NarrativeAvailabilityMode = "pr_intelligence" | "insights" | "graph_fallback" | "denormalized";
|
|
4
6
|
export type RuntimeSurfaceKind = "runtime" | "build" | "test" | "unknown";
|
|
5
7
|
export type ReachabilityKind = "on_runtime_paths" | "build_only" | "test_only" | "unreachable" | "unknown";
|
|
@@ -20,6 +22,23 @@ export type PackageRoleKind = "typechecker" | "compiler" | "bundler" | "linter"
|
|
|
20
22
|
export type PackageRuntimeImpact = "none" | "possible" | "confirmed" | "unknown";
|
|
21
23
|
export type PackageExpectedImpact = "runtime" | "build_time" | "typecheck" | "test_time" | "development_only" | "unknown";
|
|
22
24
|
export type PackageEvidenceStrength = "high" | "medium" | "low";
|
|
25
|
+
export type CorpusGateReason = "ok" | "no_code_intelligence" | "repo_intelligence_invalid" | "repo_intelligence_absent";
|
|
26
|
+
/** Enriched affected area with proof-chain linkage (Phase 3). */
|
|
27
|
+
export type AffectedAreaFact = {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
packages: string[];
|
|
31
|
+
findingIds: string[];
|
|
32
|
+
paths: string[];
|
|
33
|
+
evidenceStrength: PackageEvidenceStrength | null;
|
|
34
|
+
hotspotPackages: string[];
|
|
35
|
+
verificationFocus: string[];
|
|
36
|
+
};
|
|
37
|
+
export type ScanNarrativeConfidenceFacts = {
|
|
38
|
+
decision: "low" | "medium" | "high" | null;
|
|
39
|
+
assessment: "low" | "medium" | "high" | null;
|
|
40
|
+
limitedContext: boolean;
|
|
41
|
+
};
|
|
23
42
|
/** Engine semantic projection for one changed package (wire copy only). */
|
|
24
43
|
export type ChangedPackageSemantics = {
|
|
25
44
|
packageName: string;
|
|
@@ -52,6 +71,8 @@ export type ScanNarrativeFacts = {
|
|
|
52
71
|
tier3: boolean;
|
|
53
72
|
};
|
|
54
73
|
repoIntelligenceParse: RepoIntelligenceParseStatus;
|
|
74
|
+
preparationWarnings: AnalysisContextWarning[];
|
|
75
|
+
corpusGateReason: CorpusGateReason;
|
|
55
76
|
};
|
|
56
77
|
changedPackages: {
|
|
57
78
|
primary: string | null;
|
|
@@ -83,10 +104,7 @@ export type ScanNarrativeFacts = {
|
|
|
83
104
|
changedPackageCount?: number;
|
|
84
105
|
factors: string[];
|
|
85
106
|
} | null;
|
|
86
|
-
affectedAreas:
|
|
87
|
-
id: string;
|
|
88
|
-
label: string;
|
|
89
|
-
}>;
|
|
107
|
+
affectedAreas: AffectedAreaFact[];
|
|
90
108
|
hotspots: Array<{
|
|
91
109
|
packageName: string;
|
|
92
110
|
source: "code" | "graph";
|
|
@@ -103,9 +121,7 @@ export type ScanNarrativeFacts = {
|
|
|
103
121
|
remediation?: string;
|
|
104
122
|
affectedFiles?: string[];
|
|
105
123
|
}>;
|
|
106
|
-
confidence:
|
|
107
|
-
decision: "low" | "medium" | "high" | null;
|
|
108
|
-
};
|
|
124
|
+
confidence: ScanNarrativeConfidenceFacts;
|
|
109
125
|
repositoryContext: Array<{
|
|
110
126
|
family: ObservationSignalFamily;
|
|
111
127
|
source: RepositoryContextSource;
|
|
@@ -115,7 +131,9 @@ export type ScanNarrativeFacts = {
|
|
|
115
131
|
};
|
|
116
132
|
}>;
|
|
117
133
|
mergePosture: MergePosture | null;
|
|
134
|
+
/** @deprecated Use `riskSignals.riskIndex` — kept for frozen surface presenters. */
|
|
118
135
|
riskIndex: number | null;
|
|
136
|
+
riskSignals: RiskSignals | null;
|
|
119
137
|
};
|
|
120
138
|
export declare const EMPTY_SCAN_NARRATIVE_FACTS: ScanNarrativeFacts;
|
|
121
139
|
/** Derivation caps — presenters apply tighter channel limits. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanNarrativeFacts.d.ts","sourceRoot":"","sources":["../src/scanNarrativeFacts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"scanNarrativeFacts.d.ts","sourceRoot":"","sources":["../src/scanNarrativeFacts.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,8 @@ export const EMPTY_SCAN_NARRATIVE_FACTS = {
|
|
|
4
4
|
codeIntelligenceAvailable: false,
|
|
5
5
|
tiersPresent: { tier1: false, tier2: false, tier3: false },
|
|
6
6
|
repoIntelligenceParse: "absent",
|
|
7
|
+
preparationWarnings: [],
|
|
8
|
+
corpusGateReason: "repo_intelligence_absent",
|
|
7
9
|
},
|
|
8
10
|
changedPackages: { primary: null, others: [], all: [] },
|
|
9
11
|
packageUsage: [],
|
|
@@ -16,10 +18,11 @@ export const EMPTY_SCAN_NARRATIVE_FACTS = {
|
|
|
16
18
|
affectedAreas: [],
|
|
17
19
|
hotspots: [],
|
|
18
20
|
reviewerGuidance: [],
|
|
19
|
-
confidence: { decision: null },
|
|
21
|
+
confidence: { decision: null, assessment: null, limitedContext: false },
|
|
20
22
|
repositoryContext: [],
|
|
21
23
|
mergePosture: null,
|
|
22
24
|
riskIndex: null,
|
|
25
|
+
riskSignals: null,
|
|
23
26
|
};
|
|
24
27
|
/** Derivation caps — presenters apply tighter channel limits. */
|
|
25
28
|
export const NARRATIVE_FACT_MAX_PATHS = 20;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanPresentationBundle.golden.test.d.ts","sourceRoot":"","sources":["../src/scanPresentationBundle.golden.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { deriveCardExposureCategory } from "./formatCardExposureDisplay.js";
|
|
3
|
+
import { buildScanPresentationBundle } from "./presentation/orchestration/buildScanPresentationBundle.js";
|
|
4
|
+
import { scanResultBullmq, scanResultEslint, scanResultFastifyRuntime, scanResultLimitedContext, scanResultMixedTypescriptFastify, scanResultPrettier, scanResultTypescriptPatch, scanResultVitest, } from "./presentation/fixtures/presentationPersonaFixtures.js";
|
|
5
|
+
const validationPersonas = [
|
|
6
|
+
{ id: "fastify-runtime", result: scanResultFastifyRuntime },
|
|
7
|
+
{ id: "typescript-patch", result: scanResultTypescriptPatch },
|
|
8
|
+
{ id: "mixed-ts-fastify", result: scanResultMixedTypescriptFastify },
|
|
9
|
+
{ id: "prettier", result: scanResultPrettier },
|
|
10
|
+
{ id: "bullmq", result: scanResultBullmq },
|
|
11
|
+
{ id: "eslint", result: scanResultEslint },
|
|
12
|
+
{ id: "vitest", result: scanResultVitest },
|
|
13
|
+
{ id: "limited-context", result: scanResultLimitedContext },
|
|
14
|
+
];
|
|
15
|
+
function bundleFor(result) {
|
|
16
|
+
return buildScanPresentationBundle({
|
|
17
|
+
result,
|
|
18
|
+
pipelineStatus: "done",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
describe("scanPresentationBundle golden — proof model", () => {
|
|
22
|
+
it.each(validationPersonas)("builds bundle for $id", ({ result }) => {
|
|
23
|
+
const bundle = bundleFor(result);
|
|
24
|
+
expect(bundle).not.toBeNull();
|
|
25
|
+
expect(bundle.facts.availability.corpusGateReason).toBeDefined();
|
|
26
|
+
expect(bundle.facts.confidence).toMatchObject({
|
|
27
|
+
limitedContext: expect.any(Boolean),
|
|
28
|
+
});
|
|
29
|
+
expect(bundle.facts.riskSignals).not.toBeNull();
|
|
30
|
+
expect(bundle.facts.riskSignals.layers).toHaveLength(4);
|
|
31
|
+
});
|
|
32
|
+
it("fastify-runtime — linkage and diagnostics", () => {
|
|
33
|
+
const bundle = bundleFor(scanResultFastifyRuntime);
|
|
34
|
+
const { facts } = bundle;
|
|
35
|
+
expect(facts.availability.corpusGateReason).toBe("ok");
|
|
36
|
+
expect(facts.confidence.limitedContext).toBe(false);
|
|
37
|
+
expect(facts.confidence.assessment).toBe("medium");
|
|
38
|
+
const apiArea = facts.affectedAreas.find((a) => a.id === "api");
|
|
39
|
+
expect(apiArea?.packages).toContain("fastify");
|
|
40
|
+
expect(apiArea?.findingIds).toContain("finding-fastify-runtime");
|
|
41
|
+
expect(apiArea?.paths.length).toBeGreaterThan(0);
|
|
42
|
+
expect(apiArea?.hotspotPackages).toContain("fastify");
|
|
43
|
+
expect(apiArea?.verificationFocus.length).toBeGreaterThan(0);
|
|
44
|
+
expect(facts.riskSignals?.riskIndex).toBe(55);
|
|
45
|
+
expect(facts.riskSignals?.exposure).toBe(deriveCardExposureCategory(55));
|
|
46
|
+
expect(facts.riskIndex).toBe(facts.riskSignals?.riskIndex);
|
|
47
|
+
});
|
|
48
|
+
it("limited-context — diagnostics without synthetic confidence", () => {
|
|
49
|
+
const bundle = bundleFor(scanResultLimitedContext);
|
|
50
|
+
const { facts } = bundle;
|
|
51
|
+
expect(facts.availability.corpusGateReason).toBe("no_code_intelligence");
|
|
52
|
+
expect(facts.availability.preparationWarnings.some((w) => w.code === "base_lockfile_missing")).toBe(true);
|
|
53
|
+
expect(facts.confidence.assessment).toBe("low");
|
|
54
|
+
expect(facts.confidence.limitedContext).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it("typescript-patch — tooling semantics with risk signals", () => {
|
|
57
|
+
const bundle = bundleFor(scanResultTypescriptPatch);
|
|
58
|
+
const { facts } = bundle;
|
|
59
|
+
expect(facts.availability.corpusGateReason).toBe("ok");
|
|
60
|
+
expect(facts.packageSemantics?.dependencyClass).toBe("tooling");
|
|
61
|
+
expect(facts.riskSignals?.riskIndex).toBe(18);
|
|
62
|
+
expect(facts.riskSignals?.exposure).toBe(deriveCardExposureCategory(18));
|
|
63
|
+
});
|
|
64
|
+
it("affected areas always carry linkage arrays (structure)", () => {
|
|
65
|
+
for (const { result } of validationPersonas) {
|
|
66
|
+
const bundle = bundleFor(result);
|
|
67
|
+
if (!bundle)
|
|
68
|
+
continue;
|
|
69
|
+
for (const area of bundle.facts.affectedAreas) {
|
|
70
|
+
expect(Array.isArray(area.packages)).toBe(true);
|
|
71
|
+
expect(Array.isArray(area.findingIds)).toBe(true);
|
|
72
|
+
expect(Array.isArray(area.paths)).toBe(true);
|
|
73
|
+
expect(Array.isArray(area.hotspotPackages)).toBe(true);
|
|
74
|
+
expect(Array.isArray(area.verificationFocus)).toBe(true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|