@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.
- package/dist/actionsStepSummary.d.ts.map +1 -1
- package/dist/actionsStepSummary.js +2 -29
- package/dist/collectNarrativeWhyBullets.d.ts +12 -0
- package/dist/collectNarrativeWhyBullets.d.ts.map +1 -0
- package/dist/collectNarrativeWhyBullets.js +104 -0
- package/dist/deriveScanNarrative.d.ts +10 -0
- package/dist/deriveScanNarrative.d.ts.map +1 -0
- package/dist/deriveScanNarrative.js +370 -0
- package/dist/deriveScanNarrative.test.d.ts +2 -0
- package/dist/deriveScanNarrative.test.d.ts.map +1 -0
- package/dist/deriveScanNarrative.test.js +164 -0
- package/dist/extractRepositoryContextFacts.d.ts +16 -0
- package/dist/extractRepositoryContextFacts.d.ts.map +1 -0
- package/dist/extractRepositoryContextFacts.js +71 -0
- package/dist/fixtures/repoIntelligenceFixtures.d.ts +10 -0
- package/dist/fixtures/repoIntelligenceFixtures.d.ts.map +1 -0
- package/dist/fixtures/repoIntelligenceFixtures.js +93 -0
- package/dist/formatInsight.d.ts +2 -2
- package/dist/formatInsight.d.ts.map +1 -1
- package/dist/formatInsight.js +2 -7
- package/dist/formatInsight.test.js +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/narrativeParity.test.d.ts +2 -0
- package/dist/narrativeParity.test.d.ts.map +1 -0
- package/dist/narrativeParity.test.js +64 -0
- package/dist/narrativePresentation.d.ts +50 -0
- package/dist/narrativePresentation.d.ts.map +1 -0
- package/dist/narrativePresentation.js +180 -0
- package/dist/narrativePresentation.test.d.ts +2 -0
- package/dist/narrativePresentation.test.d.ts.map +1 -0
- package/dist/narrativePresentation.test.js +53 -0
- package/dist/normalizeGeneratedText.d.ts +8 -0
- package/dist/normalizeGeneratedText.d.ts.map +1 -0
- package/dist/normalizeGeneratedText.js +31 -0
- package/dist/normalizeGeneratedText.test.d.ts +2 -0
- package/dist/normalizeGeneratedText.test.d.ts.map +1 -0
- package/dist/normalizeGeneratedText.test.js +18 -0
- package/dist/prCheckRunPresentation.d.ts +4 -3
- package/dist/prCheckRunPresentation.d.ts.map +1 -1
- package/dist/prCheckRunPresentation.js +56 -51
- package/dist/prCheckRunPresentation.test.js +2 -2
- package/dist/presentGitHubPrComment.d.ts +9 -0
- package/dist/presentGitHubPrComment.d.ts.map +1 -0
- package/dist/presentGitHubPrComment.js +80 -0
- package/dist/presentScanCardSummary.d.ts +16 -0
- package/dist/presentScanCardSummary.d.ts.map +1 -0
- package/dist/presentScanCardSummary.js +212 -0
- package/dist/productMessaging.d.ts +1 -1
- package/dist/productMessaging.js +1 -1
- package/dist/repoIntelligenceSchema.d.ts +123 -0
- package/dist/repoIntelligenceSchema.d.ts.map +1 -0
- package/dist/repoIntelligenceSchema.js +174 -0
- package/dist/riskVocabulary.js +1 -1
- package/dist/scanCardSummary.d.ts +21 -3
- package/dist/scanCardSummary.d.ts.map +1 -1
- package/dist/scanCardSummary.js +17 -58
- package/dist/scanCardSummary.test.js +25 -8
- package/dist/scanDetailViewModel.d.ts +22 -0
- package/dist/scanDetailViewModel.d.ts.map +1 -1
- package/dist/scanDetailViewModel.js +247 -31
- package/dist/scanDetailViewModel.test.js +29 -3
- package/dist/scanNarrativeFacts.d.ts +92 -0
- package/dist/scanNarrativeFacts.d.ts.map +1 -0
- package/dist/scanNarrativeFacts.js +24 -0
- package/dist/scanSurfaceCopy.d.ts +49 -23
- package/dist/scanSurfaceCopy.d.ts.map +1 -1
- package/dist/scanSurfaceCopy.js +49 -23
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/scanCardSummary.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
|
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":"
|
|
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
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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 =
|
|
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 =
|
|
364
|
-
const themes =
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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:
|
|
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
|
|
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).
|
|
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);
|