@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.
@@ -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":"AAOA,OAAO,EAeL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAKV,UAAU,EACX,MAAM,YAAY,CAAC;AAqfpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAwGpB"}
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.toLowerCase().replace(/\s+/g, "_");
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 riskIndex = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
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 affectedAreas = tier1Blocks.affectedAreas;
417
- if (affectedAreas.length === 0 && mode !== "pr_intelligence") {
418
- affectedAreas = selectTopAffectedAreas(result, { max: 2 }).map((label) => ({
419
- id: label.toLowerCase().replace(/\s+/g, "_").slice(0, 48),
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
- * Dashboard card exposure semantics display-only interpretation of totalScore.
3
- * Not persisted; not an engine output; complements merge posture (Safe / Review / Risky).
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 totalScore to a fixed exposure category (card UX only). */
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,0EAA0E;AAC1E,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,mBAAmB,GAAG,IAAI,CAM5B;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
+ {"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
- * Dashboard card exposure semantics display-only interpretation of totalScore.
3
- * Not persisted; not an engine output; complements merge posture (Safe / Review / Risky).
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 totalScore to a fixed exposure category (card UX only). */
35
- export function deriveCardExposureDisplay(score) {
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
- const category = EXPOSURE_ORDER[idx >= 0 ? idx : EXPOSURE_ORDER.length - 1];
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":"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"}
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 = [...row.paths, ...row.criticalPaths, ...row.files].filter(Boolean);
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,UAiCtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,UA2BvC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UA4BtC,CAAC"}
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":"AAcA,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
+ {"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.paths,
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: Array<{
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":"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"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scanPresentationBundle.golden.test.d.ts.map
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mergesignal/shared",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MergeSignal shared scan contract, presentation policy, and trusted validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",