@mergesignal/shared 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/actionsStepSummary.d.ts.map +1 -1
  2. package/dist/actionsStepSummary.js +2 -29
  3. package/dist/collectNarrativeWhyBullets.d.ts +12 -0
  4. package/dist/collectNarrativeWhyBullets.d.ts.map +1 -0
  5. package/dist/collectNarrativeWhyBullets.js +104 -0
  6. package/dist/deriveScanNarrative.d.ts +10 -0
  7. package/dist/deriveScanNarrative.d.ts.map +1 -0
  8. package/dist/deriveScanNarrative.js +370 -0
  9. package/dist/deriveScanNarrative.test.d.ts +2 -0
  10. package/dist/deriveScanNarrative.test.d.ts.map +1 -0
  11. package/dist/deriveScanNarrative.test.js +164 -0
  12. package/dist/extractRepositoryContextFacts.d.ts +16 -0
  13. package/dist/extractRepositoryContextFacts.d.ts.map +1 -0
  14. package/dist/extractRepositoryContextFacts.js +71 -0
  15. package/dist/fixtures/repoIntelligenceFixtures.d.ts +10 -0
  16. package/dist/fixtures/repoIntelligenceFixtures.d.ts.map +1 -0
  17. package/dist/fixtures/repoIntelligenceFixtures.js +93 -0
  18. package/dist/formatInsight.d.ts +2 -2
  19. package/dist/formatInsight.d.ts.map +1 -1
  20. package/dist/formatInsight.js +2 -7
  21. package/dist/formatInsight.test.js +1 -1
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -0
  25. package/dist/narrativeParity.test.d.ts +2 -0
  26. package/dist/narrativeParity.test.d.ts.map +1 -0
  27. package/dist/narrativeParity.test.js +64 -0
  28. package/dist/narrativePresentation.d.ts +50 -0
  29. package/dist/narrativePresentation.d.ts.map +1 -0
  30. package/dist/narrativePresentation.js +180 -0
  31. package/dist/narrativePresentation.test.d.ts +2 -0
  32. package/dist/narrativePresentation.test.d.ts.map +1 -0
  33. package/dist/narrativePresentation.test.js +53 -0
  34. package/dist/normalizeGeneratedText.d.ts +8 -0
  35. package/dist/normalizeGeneratedText.d.ts.map +1 -0
  36. package/dist/normalizeGeneratedText.js +31 -0
  37. package/dist/normalizeGeneratedText.test.d.ts +2 -0
  38. package/dist/normalizeGeneratedText.test.d.ts.map +1 -0
  39. package/dist/normalizeGeneratedText.test.js +18 -0
  40. package/dist/prCheckRunPresentation.d.ts +4 -3
  41. package/dist/prCheckRunPresentation.d.ts.map +1 -1
  42. package/dist/prCheckRunPresentation.js +56 -51
  43. package/dist/prCheckRunPresentation.test.js +2 -2
  44. package/dist/presentGitHubPrComment.d.ts +9 -0
  45. package/dist/presentGitHubPrComment.d.ts.map +1 -0
  46. package/dist/presentGitHubPrComment.js +80 -0
  47. package/dist/presentScanCardSummary.d.ts +16 -0
  48. package/dist/presentScanCardSummary.d.ts.map +1 -0
  49. package/dist/presentScanCardSummary.js +212 -0
  50. package/dist/productMessaging.d.ts +1 -1
  51. package/dist/productMessaging.js +1 -1
  52. package/dist/repoIntelligenceSchema.d.ts +123 -0
  53. package/dist/repoIntelligenceSchema.d.ts.map +1 -0
  54. package/dist/repoIntelligenceSchema.js +174 -0
  55. package/dist/riskVocabulary.js +1 -1
  56. package/dist/scanCardSummary.d.ts +21 -3
  57. package/dist/scanCardSummary.d.ts.map +1 -1
  58. package/dist/scanCardSummary.js +17 -58
  59. package/dist/scanCardSummary.test.js +25 -8
  60. package/dist/scanDetailViewModel.d.ts +22 -0
  61. package/dist/scanDetailViewModel.d.ts.map +1 -1
  62. package/dist/scanDetailViewModel.js +247 -31
  63. package/dist/scanDetailViewModel.test.js +29 -3
  64. package/dist/scanNarrativeFacts.d.ts +92 -0
  65. package/dist/scanNarrativeFacts.d.ts.map +1 -0
  66. package/dist/scanNarrativeFacts.js +24 -0
  67. package/dist/scanSurfaceCopy.d.ts +49 -23
  68. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  69. package/dist/scanSurfaceCopy.js +49 -23
  70. package/dist/types.d.ts +1 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- import { deriveCardOperationalObservations } from "./deriveCardOperationalObservations.js";
2
- import { mergePostureFromDecision, MERGE_POSTURE_LABEL, } from "./riskVocabulary.js";
1
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
2
+ import { presentScanCardSummary } from "./presentScanCardSummary.js";
3
+ import { mergePostureFromDecision, } from "./riskVocabulary.js";
3
4
  import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
4
- import { selectTopAffectedAreas } from "./selectTopAffectedAreas.js";
5
5
  const STALE_SUBLINE = "Based on earlier commit";
6
- export const SCAN_CARD_SCANNING_SUMMARY = "Waiting for results";
6
+ export const SCAN_CARD_SCANNING_SUMMARY = "Waiting for results...";
7
7
  function normalizePipelineStatus(status) {
8
8
  const st = String(status ?? "")
9
9
  .trim()
@@ -59,7 +59,8 @@ export function isPipelineCardSummary(summary) {
59
59
  summary.riskIndex === null &&
60
60
  (summary.headline === scanSurfaceCopy.pipeline.scanRunning ||
61
61
  summary.headline === scanSurfaceCopy.pipeline.analysisIncomplete ||
62
- summary.headline === scanSurfaceCopy.pipeline.scanUnavailable));
62
+ summary.headline === scanSurfaceCopy.pipeline.scanUnavailable) &&
63
+ summary.narrativeMode === "denormalized");
63
64
  }
64
65
  /** Single source for risk index color bands (aligns with decision score bands). */
65
66
  export function deriveRiskIndexBand(score) {
@@ -101,68 +102,26 @@ export function aggregateFindingCounts(findings) {
101
102
  */
102
103
  export function deriveScanCardSummary(result, pipelineStatus) {
103
104
  if (pipelineStatus === "queued" || pipelineStatus === "running") {
104
- return {
105
- mergePosture: null,
106
- riskIndex: null,
107
- riskIndexBand: null,
108
- headline: scanSurfaceCopy.pipeline.scanRunning,
109
- summaryLine: SCAN_CARD_SCANNING_SUMMARY,
110
- findingCounts: null,
111
- topAffectedAreas: [],
112
- operationalObservations: [],
113
- supportingLine: null,
114
- };
105
+ return presentScanCardSummary(deriveScanNarrative(null), {
106
+ pipelineStatus,
107
+ });
115
108
  }
116
109
  if (pipelineStatus === "failed") {
117
- return {
118
- mergePosture: null,
119
- riskIndex: null,
120
- riskIndexBand: null,
121
- headline: scanSurfaceCopy.pipeline.analysisIncomplete,
122
- summaryLine: null,
123
- findingCounts: null,
124
- topAffectedAreas: [],
125
- operationalObservations: [],
126
- supportingLine: null,
127
- };
110
+ return presentScanCardSummary(deriveScanNarrative(null), {
111
+ pipelineStatus: "failed",
112
+ });
128
113
  }
129
114
  if (!result) {
130
115
  return {
131
- mergePosture: null,
132
- riskIndex: null,
133
- riskIndexBand: null,
116
+ ...presentScanCardSummary(deriveScanNarrative(null), { pipelineStatus }),
134
117
  headline: scanSurfaceCopy.pipeline.scanUnavailable,
135
- summaryLine: null,
136
- findingCounts: null,
137
- topAffectedAreas: [],
138
- operationalObservations: [],
139
- supportingLine: null,
140
118
  };
141
119
  }
142
- const posture = mergePostureFromDecision(result.decision?.recommendation) ?? null;
143
- const riskIndex = typeof result.totalScore === "number" ? result.totalScore : null;
144
- const riskIndexBand = deriveRiskIndexBand(riskIndex);
145
- const findingCounts = aggregateFindingCounts(result.findings);
146
- const headline = posture
147
- ? MERGE_POSTURE_LABEL[posture]
148
- : scanSurfaceCopy.checkRun.mergePostureUnavailable;
149
- const topAffectedAreas = selectTopAffectedAreas(result, { max: 2 });
150
- const { operationalObservations, supportingLine } = deriveCardOperationalObservations(result, {
151
- mergePosture: posture,
152
- hasFullResult: true,
153
- max: 3,
120
+ const facts = deriveScanNarrative(result);
121
+ return presentScanCardSummary(facts, {
122
+ pipelineStatus,
123
+ findings: result.findings,
154
124
  });
155
- return {
156
- mergePosture: posture,
157
- riskIndex,
158
- riskIndexBand,
159
- headline,
160
- summaryLine: null,
161
- findingCounts,
162
- topAffectedAreas,
163
- operationalObservations,
164
- supportingLine,
165
- };
166
125
  }
167
126
  /** Subline appended when scan results are stale (head SHA mismatch). */
168
127
  export function staleScanSubline() {
@@ -1,6 +1,21 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { isCatalogPhrase } from "./cardObservationCatalog.js";
3
3
  import { aggregateFindingCounts, deriveRiskIndexBand, deriveScanCardSummary, deriveScanCardSummaryFromDenormalized, isPipelineCardSummary, isPipelinePlaceholderCopy, resolvePipelineStatus, resolvePrScanCardSummary, staleScanSubline, } from "./scanCardSummary.js";
4
+ const pipelinePresentationDefaults = {
5
+ narrativeMode: "denormalized",
6
+ codeIntelligenceAvailable: false,
7
+ changedPackagesDisplay: null,
8
+ runtimeSurfaceLabel: null,
9
+ reachabilityLabel: null,
10
+ blastRadiusLabel: null,
11
+ affectedAreas: [],
12
+ primaryInsight: null,
13
+ structuralOnlyDisclaimer: null,
14
+ usageSummary: null,
15
+ verificationLine: null,
16
+ blastRadiusDetail: null,
17
+ frameworksSummary: null,
18
+ };
4
19
  const baseResult = {
5
20
  totalScore: 10,
6
21
  layerScores: {
@@ -53,7 +68,7 @@ describe("deriveScanCardSummary", () => {
53
68
  it("returns scanning copy for queued/running", () => {
54
69
  const queued = deriveScanCardSummary(null, "queued");
55
70
  expect(queued.headline).toBe("Scan in progress");
56
- expect(queued.summaryLine).toBe("Waiting for results");
71
+ expect(queued.summaryLine).toBe("Waiting for results...");
57
72
  expect(queued.mergePosture).toBeNull();
58
73
  const running = deriveScanCardSummary(null, "running");
59
74
  expect(running.headline).toBe("Scan in progress");
@@ -173,11 +188,12 @@ describe("isPipelineCardSummary", () => {
173
188
  riskIndex: null,
174
189
  riskIndexBand: null,
175
190
  headline: "Scan in progress",
176
- summaryLine: "Waiting for results",
191
+ summaryLine: "Waiting for results...",
177
192
  findingCounts: null,
178
193
  topAffectedAreas: [],
179
194
  operationalObservations: [],
180
195
  supportingLine: null,
196
+ ...pipelinePresentationDefaults,
181
197
  })).toBe(true);
182
198
  });
183
199
  it("detects hybrid completed posture with pipeline summary line", () => {
@@ -186,17 +202,18 @@ describe("isPipelineCardSummary", () => {
186
202
  riskIndex: 72,
187
203
  riskIndexBand: "high",
188
204
  headline: "Risky",
189
- summaryLine: "Waiting for results",
205
+ summaryLine: "Waiting for results...",
190
206
  findingCounts: null,
191
207
  topAffectedAreas: [],
192
208
  operationalObservations: [],
193
209
  supportingLine: null,
210
+ ...pipelinePresentationDefaults,
194
211
  })).toBe(true);
195
212
  });
196
213
  });
197
214
  describe("isPipelinePlaceholderCopy", () => {
198
215
  it("identifies waiting copy", () => {
199
- expect(isPipelinePlaceholderCopy("Waiting for results")).toBe(true);
216
+ expect(isPipelinePlaceholderCopy("Waiting for results...")).toBe(true);
200
217
  expect(isPipelinePlaceholderCopy("Auth change")).toBe(false);
201
218
  });
202
219
  });
@@ -210,9 +227,9 @@ describe("deriveScanCardSummaryFromDenormalized", () => {
210
227
  expect(summary.operationalObservations).toEqual([]);
211
228
  });
212
229
  it("does not inject pipeline placeholder summaryText", () => {
213
- const summary = deriveScanCardSummaryFromDenormalized("risky", 72, "Waiting for results", "done");
230
+ const summary = deriveScanCardSummaryFromDenormalized("risky", 72, "Waiting for results...", "done");
214
231
  expect(summary.mergePosture).toBe("risky");
215
- expect(summary.summaryLine).not.toBe("Waiting for results");
232
+ expect(summary.summaryLine).not.toBe("Waiting for results...");
216
233
  });
217
234
  it("does not inject raw generic summaryText on quiet safe cards", () => {
218
235
  const summary = deriveScanCardSummaryFromDenormalized("safe", 12, "No high-confidence merge risks from this PR dependency change", "done");
@@ -226,11 +243,11 @@ describe("resolvePrScanCardSummary", () => {
226
243
  pipelineStatus: "running",
227
244
  decision: "risky",
228
245
  totalScore: 80,
229
- summaryText: "Waiting for results",
246
+ summaryText: "Waiting for results...",
230
247
  result: null,
231
248
  });
232
249
  expect(summary.mergePosture).toBe("risky");
233
250
  expect(summary.headline).toBe("Risky");
234
- expect(summary.summaryLine).not.toBe("Waiting for results");
251
+ expect(summary.summaryLine).not.toBe("Waiting for results...");
235
252
  });
236
253
  });
@@ -1,3 +1,4 @@
1
+ import type { ScanNarrativeFacts } from "./scanNarrativeFacts.js";
1
2
  import { type ScanDetailRecommendationCenter } from "./deriveScanDetailRecommendations.js";
2
3
  import { type ScanDetailSignalSummary } from "./deriveRiskScoreSummary.js";
3
4
  import { type MergePosture } from "./riskVocabulary.js";
@@ -57,6 +58,22 @@ export type ScanDetailTopology = {
57
58
  via: string[];
58
59
  }>;
59
60
  };
61
+ export type ScanDetailNarrativeContext = {
62
+ mode: import("./scanNarrativeFacts.js").NarrativeAvailabilityMode;
63
+ codeIntelligenceAvailable: boolean;
64
+ changedPackagesDisplay: string | null;
65
+ runtimeSurfaceLabel: string | null;
66
+ reachabilityLabel: string | null;
67
+ blastRadiusLabel: string | null;
68
+ affectedAreas: string[];
69
+ structuralOnlyDisclaimer: string | null;
70
+ usageHighlights: string[];
71
+ frameworks: string[];
72
+ blastRadiusFactors: string[];
73
+ hotspotNames: string[];
74
+ usageContextLine: string | null;
75
+ upgradeContextLine: string | null;
76
+ };
60
77
  export type ScanDetailViewModel = {
61
78
  verdict: ScanDetailVerdict;
62
79
  signalSummary: ScanDetailSignalSummary | null;
@@ -79,12 +96,14 @@ export type ScanDetailViewModel = {
79
96
  status: "loaded";
80
97
  comparisonLine: string;
81
98
  };
99
+ narrativeContext: ScanDetailNarrativeContext;
82
100
  metadata: {
83
101
  scanId: string;
84
102
  generatedAt?: string;
85
103
  methodologyVersion?: string | null;
86
104
  changedPackagesSummary?: string;
87
105
  codeAnalysisTimedOut?: boolean;
106
+ codeIntelligenceAvailable?: boolean;
88
107
  };
89
108
  };
90
109
  export type DeriveScanDetailOptions = {
@@ -102,6 +121,9 @@ export declare function sanitizeAct2Theme(raw: string): string | null;
102
121
  export declare function deriveVerdictLine(posture: MergePosture | null, totalScore: number | null | undefined): string;
103
122
  export declare function deriveAct2Themes(result: ScanResult): string[];
104
123
  export declare function deriveOperationalImpact(result: ScanResult, verifyLabels?: Set<string>): ScanDetailOperationalImpact;
124
+ export declare function presentScanDetailNarrativeContext(facts: ScanNarrativeFacts): ScanDetailNarrativeContext;
125
+ /** Scan detail presentation from shared narrative facts. */
126
+ export declare function presentScanDetailViewModel(facts: ScanNarrativeFacts, result: ScanResult, options: DeriveScanDetailOptions): ScanDetailViewModel;
105
127
  export declare function deriveScanDetailViewModel(result: ScanResult | null | undefined, options: DeriveScanDetailOptions): ScanDetailViewModel | null;
106
128
  /** Tier 1 recall test helper — true when message describes application-level impact. */
107
129
  export declare function tier1PassesRecallTest(operationalImpact: ScanDetailOperationalImpact): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"scanDetailViewModel.d.ts","sourceRoot":"","sources":["../src/scanDetailViewModel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scanDetailViewModel.d.ts","sourceRoot":"","sources":["../src/scanDetailViewModel.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,9 @@
1
1
  import { humanizeEngineSurfaceText, layerRiskBandLabel, sortPRInsightsForDisplay, truncateWithEllipsis, } from "./actionsStepSummary.js";
2
2
  import { mapFindingToCatalogPhrase, mapGraphInsightsToCatalogPhrases, phraseForFamily, } from "./cardObservationCatalog.js";
3
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
4
+ import { formatCardAreaLabels } from "./formatCardAreaLabels.js";
5
+ import { composeContextLineFromFacts, formatChangedPackagesShort, formatUsageSummaryLine, labelBlastRadiusLevel, labelReachabilityKind, labelRuntimeSurface, summarizeBlastRadius, } from "./narrativePresentation.js";
6
+ import { normalizeGeneratedText, normalizeGeneratedTextNullable, } from "./normalizeGeneratedText.js";
3
7
  import { deriveCardOperationalObservations } from "./deriveCardOperationalObservations.js";
4
8
  import { deriveScanDetailRecommendations, recommendationTitlesForDedupe, } from "./deriveScanDetailRecommendations.js";
5
9
  import { deriveSignalSummary, } from "./deriveRiskScoreSummary.js";
@@ -206,6 +210,72 @@ function graphInsightToPackage(item, severity, evidence) {
206
210
  evidence,
207
211
  };
208
212
  }
213
+ function usageWhereFromFacts(facts) {
214
+ const primary = facts.changedPackages.primary;
215
+ const row = facts.packageUsage.find((u) => u.packageName === primary) ??
216
+ facts.packageUsage[0];
217
+ if (!row)
218
+ return undefined;
219
+ const paths = [...row.paths, ...row.criticalPaths, ...row.files].filter(Boolean);
220
+ if (paths.length === 0 && row.areas.length > 0) {
221
+ return row.areas.slice(0, 3).join(", ");
222
+ }
223
+ if (paths.length === 0)
224
+ return undefined;
225
+ const sample = paths.slice(0, 3).join(", ");
226
+ if (paths.length > 3) {
227
+ return `${sample} (+${paths.length - 3} more)`;
228
+ }
229
+ return sample;
230
+ }
231
+ function buildAttentionAreasFromFacts(facts) {
232
+ const areas = [];
233
+ if (facts.hotspots.length > 0) {
234
+ const packages = facts.hotspots.map((h) => ({
235
+ name: h.packageName,
236
+ direct: true,
237
+ evidence: h.paths[0],
238
+ }));
239
+ areas.push({
240
+ problemLabel: "Dependency hotspots",
241
+ problemDescription: "Packages where paths converge or corpus analysis flagged critical usage.",
242
+ packages: packages.slice(0, ACT3_EVIDENCE_COLLAPSE_THRESHOLD),
243
+ overflowCount: Math.max(0, packages.length - ACT3_EVIDENCE_COLLAPSE_THRESHOLD),
244
+ });
245
+ }
246
+ const blast = facts.blastRadius;
247
+ if (blast &&
248
+ (blast.level === "wide" || (blast.changedPackageCount ?? 0) >= 8)) {
249
+ const names = facts.changedPackages.all;
250
+ if (names.length > 0) {
251
+ const packages = names
252
+ .slice(0, ACT3_EVIDENCE_COLLAPSE_THRESHOLD)
253
+ .map((name) => ({ name, direct: true }));
254
+ areas.push({
255
+ problemLabel: phraseForFamily("blast_radius"),
256
+ problemDescription: blast.factors.length > 0
257
+ ? `Wide blast radius: ${blast.factors.slice(0, 3).join(", ").replace(/_/g, " ")}`
258
+ : "Many packages changed in this upgrade.",
259
+ packages,
260
+ overflowCount: Math.max(0, names.length - ACT3_EVIDENCE_COLLAPSE_THRESHOLD),
261
+ });
262
+ }
263
+ }
264
+ return areas;
265
+ }
266
+ function mergeAttentionAreas(fromFacts, fromGraph) {
267
+ if (fromFacts.length === 0)
268
+ return fromGraph;
269
+ const factPackageNames = new Set(fromFacts.flatMap((a) => a.packages.map((p) => p.name)));
270
+ const filteredGraph = fromGraph.filter((area) => {
271
+ if (area.problemLabel !== phraseForFamily("transitive_hotspots")) {
272
+ return true;
273
+ }
274
+ const onlyDupes = area.packages.every((p) => factPackageNames.has(p.name));
275
+ return !onlyDupes;
276
+ });
277
+ return [...fromFacts, ...filteredGraph];
278
+ }
209
279
  function buildAttentionAreas(result) {
210
280
  const areas = [];
211
281
  const gi = result.graphInsights;
@@ -310,7 +380,7 @@ function buildTopology(result) {
310
380
  }))
311
381
  : [];
312
382
  return {
313
- summaryLine: `${nodes} packages · ${edges} edges · max depth ${maxDepth}`,
383
+ summaryLine: normalizeGeneratedText(`${nodes} packages | ${edges} edges | max depth ${maxDepth}`),
314
384
  deepest,
315
385
  };
316
386
  }
@@ -344,44 +414,173 @@ function deriveConfidenceCaveat(result) {
344
414
  }
345
415
  return undefined;
346
416
  }
347
- function changedPackagesSummary(changed) {
348
- if (!changed?.length)
349
- return undefined;
350
- if (changed.length === 1)
351
- return changed[0];
352
- return `${changed[0]} +${changed.length - 1} changed`;
417
+ export function presentScanDetailNarrativeContext(facts) {
418
+ const affectedAreas = [];
419
+ for (const area of facts.affectedAreas) {
420
+ const formatted = formatCardAreaLabels([area.label], 1);
421
+ if (formatted[0])
422
+ affectedAreas.push(formatted[0]);
423
+ if (affectedAreas.length >= 4)
424
+ break;
425
+ }
426
+ const usageHighlights = [];
427
+ for (const row of facts.packageUsage.slice(0, 4)) {
428
+ const paths = [...row.paths, ...row.criticalPaths, ...row.files];
429
+ if (paths[0])
430
+ usageHighlights.push(paths[0]);
431
+ }
432
+ const blast = summarizeBlastRadius(facts, 5);
433
+ return {
434
+ mode: facts.availability.mode,
435
+ codeIntelligenceAvailable: facts.availability.codeIntelligenceAvailable,
436
+ changedPackagesDisplay: formatChangedPackagesShort(facts, 4),
437
+ runtimeSurfaceLabel: labelRuntimeSurface(facts),
438
+ reachabilityLabel: labelReachabilityKind(facts),
439
+ blastRadiusLabel: labelBlastRadiusLevel(facts),
440
+ affectedAreas,
441
+ structuralOnlyDisclaimer: facts.availability.mode === "graph_fallback" &&
442
+ !facts.availability.tiersPresent.tier1
443
+ ? scanSurfaceCopy.narrativeCard.structuralOnlyDisclaimer
444
+ : null,
445
+ usageHighlights: usageHighlights.slice(0, 5),
446
+ frameworks: facts.frameworks.slice(0, 5),
447
+ blastRadiusFactors: blast.factors.map((f) => f.replace(/_/g, " ")),
448
+ hotspotNames: facts.hotspots.map((h) => h.packageName).slice(0, 8),
449
+ usageContextLine: normalizeGeneratedTextNullable(formatUsageSummaryLine(facts, 2)),
450
+ upgradeContextLine: normalizeGeneratedTextNullable(composeContextLineFromFacts(facts, {
451
+ includePathSample: true,
452
+ maxAreas: 4,
453
+ })),
454
+ };
353
455
  }
354
- export function deriveScanDetailViewModel(result, options) {
355
- if (!result || options.status !== "done")
456
+ function narrativeContextFromFacts(facts) {
457
+ return presentScanDetailNarrativeContext(facts);
458
+ }
459
+ function guidanceToImpactItem(g, verifyLabels, facts) {
460
+ const verify = g.remediation &&
461
+ !verifyLabels.has(g.remediation.toLowerCase().replace(/\s+/g, " ").trim())
462
+ ? g.remediation
463
+ : undefined;
464
+ let where = g.context?.trim() || undefined;
465
+ if (!where &&
466
+ facts &&
467
+ (facts.availability.mode === "pr_intelligence" ||
468
+ facts.availability.tiersPresent.tier1)) {
469
+ where = usageWhereFromFacts(facts);
470
+ }
471
+ return {
472
+ message: normalizeGeneratedText(g.message),
473
+ where: where ? normalizeGeneratedText(where) : undefined,
474
+ verify: verify ? normalizeGeneratedText(verify) : undefined,
475
+ affectedFiles: g.affectedFiles,
476
+ };
477
+ }
478
+ function deriveOperationalImpactFromFacts(facts, result, verifyLabels) {
479
+ const changedGuidance = facts.reviewerGuidance.filter((g) => g.scope === "changed" || g.kind === "insight");
480
+ if (facts.availability.mode === "pr_intelligence" ||
481
+ (facts.availability.tiersPresent.tier1 && changedGuidance.length > 0)) {
482
+ const items = changedGuidance
483
+ .slice(0, 6)
484
+ .map((g) => guidanceToImpactItem(g, verifyLabels, facts));
485
+ if (items.length > 0) {
486
+ return { status: "rich", items };
487
+ }
488
+ }
489
+ if (changedGuidance.length > 0) {
490
+ const insightCount = Array.isArray(result.insights)
491
+ ? result.insights.length
492
+ : 0;
493
+ if (insightCount > TIER1_MAX_VISIBLE_IMPACTS) {
494
+ return deriveOperationalImpact(result, verifyLabels);
495
+ }
496
+ return {
497
+ status: "rich",
498
+ items: changedGuidance
499
+ .slice(0, 6)
500
+ .map((g) => guidanceToImpactItem(g, verifyLabels, facts)),
501
+ };
502
+ }
503
+ return deriveOperationalImpact(result, verifyLabels);
504
+ }
505
+ function themeCandidateFromLabel(label) {
506
+ const trimmed = label.trim();
507
+ if (!trimmed)
356
508
  return null;
509
+ return sanitizeAct2Theme(trimmed) ?? trimmed.slice(0, ACT2_MAX_THEME_CHARS);
510
+ }
511
+ function deriveBecauseThemesFromFacts(facts) {
512
+ const themes = [];
513
+ const seen = new Set();
514
+ const prIntelligence = facts.availability.mode === "pr_intelligence" ||
515
+ facts.availability.tiersPresent.tier1;
516
+ const pushTheme = (raw) => {
517
+ if (!raw)
518
+ return;
519
+ const normalized = normalizeGeneratedText(raw);
520
+ const key = normalized.toLowerCase();
521
+ if (seen.has(key))
522
+ return;
523
+ seen.add(key);
524
+ themes.push(normalized);
525
+ };
526
+ if (prIntelligence) {
527
+ for (const area of facts.affectedAreas) {
528
+ pushTheme(themeCandidateFromLabel(area.label));
529
+ if (themes.length >= ACT2_MAX_THEMES)
530
+ return themes;
531
+ }
532
+ for (const factor of facts.blastRadius?.factors ?? []) {
533
+ pushTheme(themeCandidateFromLabel(factor.replace(/_/g, " ")));
534
+ if (themes.length >= ACT2_MAX_THEMES)
535
+ return themes;
536
+ }
537
+ const reachLabel = labelReachabilityKind(facts);
538
+ if (reachLabel)
539
+ pushTheme(reachLabel);
540
+ }
541
+ for (const g of facts.reviewerGuidance.slice(0, ACT2_MAX_THEMES + 2)) {
542
+ pushTheme(sanitizeAct2Theme(g.message));
543
+ if (themes.length >= ACT2_MAX_THEMES)
544
+ return themes;
545
+ }
546
+ if (themes.length < ACT2_MAX_THEMES && !prIntelligence) {
547
+ for (const ctx of facts.repositoryContext) {
548
+ pushTheme(sanitizeAct2Theme(phraseForFamily(ctx.family)));
549
+ if (themes.length >= ACT2_MAX_THEMES)
550
+ break;
551
+ }
552
+ }
553
+ return themes.slice(0, ACT2_MAX_THEMES);
554
+ }
555
+ function deriveVerdictLineFromFacts(facts, result) {
556
+ const topGuidance = facts.reviewerGuidance.find((g) => (g.scope === "changed" || g.kind === "insight") && g.message.trim());
557
+ if (topGuidance) {
558
+ return truncateWithEllipsis(topGuidance.message.trim(), VERDICT_LINE_MAX_CHARS);
559
+ }
560
+ const posture = mergePostureFromDecision(result.decision?.recommendation);
561
+ return truncateWithEllipsis(deriveVerdictLine(posture, result.totalScore), VERDICT_LINE_MAX_CHARS);
562
+ }
563
+ /** Scan detail presentation from shared narrative facts. */
564
+ export function presentScanDetailViewModel(facts, result, options) {
357
565
  const posture = mergePostureFromDecision(result.decision?.recommendation);
358
566
  const scopeChip = deriveDetailReachChip(result.totalScore);
359
- const verdictLine = truncateWithEllipsis(deriveVerdictLine(posture, result.totalScore), VERDICT_LINE_MAX_CHARS);
567
+ const verdictLine = deriveVerdictLineFromFacts(facts, result);
568
+ const narrativeContext = narrativeContextFromFacts(facts);
360
569
  const recommendedActions = deriveScanDetailRecommendations(result);
361
570
  const signalSummary = deriveSignalSummary(result);
362
571
  const verifyLabels = recommendationTitlesForDedupe(recommendedActions);
363
- const operationalImpact = deriveOperationalImpact(result, verifyLabels);
364
- const themes = deriveAct2Themes(result);
572
+ const operationalImpact = deriveOperationalImpactFromFacts(facts, result, verifyLabels);
573
+ const themes = deriveBecauseThemesFromFacts(facts);
365
574
  const confidenceCaveat = deriveConfidenceCaveat(result);
366
- const because = themes.length > 0 || confidenceCaveat
367
- ? {
368
- themes,
369
- confidenceCaveat,
370
- }
371
- : null;
372
- const attentionAreas = buildAttentionAreas(result);
575
+ const because = themes.length > 0 || confidenceCaveat ? { themes, confidenceCaveat } : null;
576
+ const factsAttention = facts.availability.mode === "pr_intelligence" ||
577
+ facts.availability.tiersPresent.tier1
578
+ ? buildAttentionAreasFromFacts(facts)
579
+ : [];
580
+ const attentionAreas = mergeAttentionAreas(factsAttention, buildAttentionAreas(result));
373
581
  const allFindings = annotateFindingsWithCoverage(buildFindingRows(result), recommendedActions);
374
- const findings = allFindings;
375
582
  const topology = buildTopology(result);
376
- const hasEvidence = attentionAreas.length > 0 || allFindings.length > 0 || topology !== null;
377
- const evidence = hasEvidence
378
- ? {
379
- attentionAreas,
380
- findings,
381
- findingsOverflowCount: Math.max(0, allFindings.length - ACT3_EVIDENCE_COLLAPSE_THRESHOLD),
382
- topology,
383
- }
384
- : null;
583
+ const hasEvidence = attentionAreas.length > 0 || allFindings.length > 0 || topology != null;
385
584
  return {
386
585
  verdict: {
387
586
  posture,
@@ -394,17 +593,34 @@ export function deriveScanDetailViewModel(result, options) {
394
593
  recommendedActions,
395
594
  operationalImpact,
396
595
  because,
397
- evidence,
596
+ evidence: hasEvidence
597
+ ? {
598
+ attentionAreas,
599
+ findings: allFindings,
600
+ findingsOverflowCount: Math.max(0, allFindings.length - ACT3_EVIDENCE_COLLAPSE_THRESHOLD),
601
+ topology,
602
+ }
603
+ : null,
398
604
  repoContext: options.repoContext ?? { status: "hidden" },
605
+ narrativeContext,
399
606
  metadata: {
400
607
  scanId: options.scanId,
401
608
  generatedAt: result.generatedAt,
402
609
  methodologyVersion: options.methodologyVersion,
403
- changedPackagesSummary: changedPackagesSummary(result.changedPackages),
610
+ changedPackagesSummary: narrativeContext.changedPackagesDisplay ??
611
+ facts.changedPackages.primary ??
612
+ undefined,
404
613
  codeAnalysisTimedOut: result.codeAnalysisMetrics?.timedOut === true,
614
+ codeIntelligenceAvailable: facts.availability.codeIntelligenceAvailable,
405
615
  },
406
616
  };
407
617
  }
618
+ export function deriveScanDetailViewModel(result, options) {
619
+ if (!result || options.status !== "done")
620
+ return null;
621
+ const facts = deriveScanNarrative(result);
622
+ return presentScanDetailViewModel(facts, result, options);
623
+ }
408
624
  /** Tier 1 recall test helper — true when message describes application-level impact. */
409
625
  export function tier1PassesRecallTest(operationalImpact) {
410
626
  if (operationalImpact.status === "rich") {
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { primaryPrExample } from "./productMessaging.js";
3
- import { ACT2_MAX_THEMES, deriveAct2Themes, deriveOperationalImpact, deriveFollowUpBridgeNote, deriveScanDetailViewModel, deriveVerdictLine, sanitizeAct2Theme, tier1PassesRecallTest, } from "./scanDetailViewModel.js";
3
+ import { ACT2_MAX_THEMES, TIER1_MAX_VISIBLE_IMPACTS, deriveAct2Themes, deriveOperationalImpact, deriveFollowUpBridgeNote, deriveScanDetailViewModel, deriveVerdictLine, sanitizeAct2Theme, tier1PassesRecallTest, } from "./scanDetailViewModel.js";
4
+ import { fixtureRepoIntelligenceFastify } from "./fixtures/repoIntelligenceFixtures.js";
4
5
  const baseResult = {
5
6
  totalScore: 10,
6
7
  layerScores: {
@@ -58,7 +59,7 @@ describe("reach vocabulary on fixtures", () => {
58
59
  changedPackages: Array.from({ length: 14 }, (_, i) => `pkg-${i}`),
59
60
  }, { scanId: "s2", status: "done" });
60
61
  expect(vm?.verdict.scopeChip).toBe("Wide reach");
61
- expect(vm?.verdict.verdictLine).toBe("No merge blockers · wide upgrade footprint");
62
+ expect(vm?.verdict.verdictLine).toBe("No merge blockers - wide upgrade footprint");
62
63
  });
63
64
  it("needs review + limited reach — minimalResult score 42", () => {
64
65
  const vm = deriveScanDetailViewModel(minimalResult(), {
@@ -128,6 +129,31 @@ describe("deriveVerdictLine", () => {
128
129
  });
129
130
  });
130
131
  describe("Tier 1 success criterion", () => {
132
+ it("surfaces package usage paths in narrative context and evidence when corpus available", () => {
133
+ const vm = deriveScanDetailViewModel({
134
+ ...baseResult,
135
+ changedPackages: ["fastify"],
136
+ analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
137
+ repoIntelligence: fixtureRepoIntelligenceFastify,
138
+ insights: [
139
+ {
140
+ type: "usage_risk",
141
+ priority: "high",
142
+ confidence: "confirmed",
143
+ scope: "changed",
144
+ message: "Fastify middleware ordering may shift",
145
+ context: "",
146
+ remediation: "Run auth integration tests",
147
+ },
148
+ ],
149
+ }, { scanId: "corpus", status: "done" });
150
+ expect(vm?.narrativeContext.usageHighlights.length).toBeGreaterThan(0);
151
+ expect(vm?.narrativeContext.usageContextLine).toMatch(/Used in/);
152
+ expect(vm?.narrativeContext.blastRadiusFactors.length).toBeGreaterThan(0);
153
+ const hotspotArea = vm?.evidence?.attentionAreas.find((a) => a.problemLabel.includes("hotspot"));
154
+ expect(hotspotArea?.packages[0]?.name).toBe("fastify");
155
+ expect(tier1PassesRecallTest(vm.operationalImpact)).toBe(true);
156
+ });
131
157
  it("insight-rich scan passes recall test", () => {
132
158
  const vm = deriveScanDetailViewModel({
133
159
  ...minimalResult(),
@@ -469,7 +495,7 @@ describe("scenario matrix", () => {
469
495
  scanId: "fat",
470
496
  status: "done",
471
497
  });
472
- expect(vm?.operationalImpact.items.length).toBe(30);
498
+ expect(vm?.operationalImpact.items.length).toBeGreaterThan(TIER1_MAX_VISIBLE_IMPACTS);
473
499
  expect(vm?.evidence?.findings.length).toBe(40);
474
500
  expect(vm?.recommendedActions.items.length).toBeLessThanOrEqual(3);
475
501
  expect(vm?.recommendedActions.items[0]?.detail.whyNow.length).toBeGreaterThan(0);