@mergesignal/shared 0.4.3 → 0.6.0

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 (181) hide show
  1. package/dist/actionsStepSummary.d.ts +1 -22
  2. package/dist/actionsStepSummary.d.ts.map +1 -1
  3. package/dist/actionsStepSummary.js +19 -716
  4. package/dist/actionsStepSummary.test.js +34 -329
  5. package/dist/assessmentLabels.d.ts +6 -0
  6. package/dist/assessmentLabels.d.ts.map +1 -0
  7. package/dist/assessmentLabels.js +33 -0
  8. package/dist/assessmentProjection.d.ts +12 -0
  9. package/dist/assessmentProjection.d.ts.map +1 -0
  10. package/dist/assessmentProjection.js +100 -0
  11. package/dist/assessmentSchema.d.ts +155 -0
  12. package/dist/assessmentSchema.d.ts.map +1 -0
  13. package/dist/assessmentSchema.js +110 -0
  14. package/dist/cardObservationCatalog.d.ts +2 -0
  15. package/dist/cardObservationCatalog.d.ts.map +1 -1
  16. package/dist/cardObservationCatalog.js +47 -1
  17. package/dist/engineWireRegression.test.js +22 -2
  18. package/dist/fixtures/assessmentFixtures.d.ts +13 -0
  19. package/dist/fixtures/assessmentFixtures.d.ts.map +1 -0
  20. package/dist/fixtures/assessmentFixtures.js +164 -0
  21. package/dist/formatCardExposureDisplay.d.ts +0 -4
  22. package/dist/formatCardExposureDisplay.d.ts.map +1 -1
  23. package/dist/formatCardExposureDisplay.js +0 -15
  24. package/dist/formatInsight.d.ts +3 -2
  25. package/dist/formatInsight.d.ts.map +1 -1
  26. package/dist/formatInsight.js +26 -1
  27. package/dist/index.d.ts +7 -11
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +7 -11
  30. package/dist/narrativeParity.test.js +4 -34
  31. package/dist/presentGitHubPrComment.d.ts +1 -3
  32. package/dist/presentGitHubPrComment.d.ts.map +1 -1
  33. package/dist/presentGitHubPrComment.js +0 -17
  34. package/dist/presentation/buildGitHubCheckRunOutput.d.ts +20 -0
  35. package/dist/presentation/buildGitHubCheckRunOutput.d.ts.map +1 -0
  36. package/dist/presentation/buildGitHubCheckRunOutput.js +42 -0
  37. package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts +2 -0
  38. package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts.map +1 -0
  39. package/dist/presentation/buildGitHubCheckRunOutput.test.js +188 -0
  40. package/dist/presentation/compose/narrativeChannels.d.ts.map +1 -1
  41. package/dist/presentation/compose/narrativeChannels.js +13 -43
  42. package/dist/presentation/compose/narrativeCompose.d.ts.map +1 -1
  43. package/dist/presentation/compose/narrativeCompose.js +2 -5
  44. package/dist/presentation/dto/assessmentPresentationFields.d.ts +13 -0
  45. package/dist/presentation/dto/assessmentPresentationFields.d.ts.map +1 -0
  46. package/dist/presentation/dto/assessmentPresentationFields.js +1 -0
  47. package/dist/presentation/dto/dashboardCardPresentation.d.ts +2 -1
  48. package/dist/presentation/dto/dashboardCardPresentation.d.ts.map +1 -1
  49. package/dist/presentation/dto/githubAndCliPresentation.d.ts +4 -3
  50. package/dist/presentation/dto/githubAndCliPresentation.d.ts.map +1 -1
  51. package/dist/presentation/dto/scanDetailsPresentation.d.ts +2 -1
  52. package/dist/presentation/dto/scanDetailsPresentation.d.ts.map +1 -1
  53. package/dist/presentation/fixtures/dashboardCardGolden.test.js +21 -14
  54. package/dist/presentation/fixtures/presentationPersonaFixtures.d.ts.map +1 -1
  55. package/dist/presentation/fixtures/presentationPersonaFixtures.js +31 -24
  56. package/dist/presentation/fixtures/scanResultFixtures.d.ts.map +1 -1
  57. package/dist/presentation/fixtures/scanResultFixtures.js +14 -3
  58. package/dist/presentation/index.d.ts +2 -2
  59. package/dist/presentation/index.d.ts.map +1 -1
  60. package/dist/presentation/index.js +2 -2
  61. package/dist/presentation/orchestration/buildScanCardPresentation.d.ts.map +1 -1
  62. package/dist/presentation/orchestration/buildScanCardPresentation.js +8 -3
  63. package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts +1 -1
  64. package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts.map +1 -1
  65. package/dist/presentation/orchestration/buildScanPresentationBundle.js +12 -10
  66. package/dist/presentation/orchestration/scanPresentationBundle.d.ts +8 -3
  67. package/dist/presentation/orchestration/scanPresentationBundle.d.ts.map +1 -1
  68. package/dist/presentation/presentationPersonas.test.js +6 -6
  69. package/dist/presentation/presenters/presentCliScanSummary.d.ts.map +1 -1
  70. package/dist/presentation/presenters/presentCliScanSummary.js +2 -0
  71. package/dist/presentation/presenters/presentDashboardCard.d.ts +1 -0
  72. package/dist/presentation/presenters/presentDashboardCard.d.ts.map +1 -1
  73. package/dist/presentation/presenters/presentDashboardCard.js +18 -0
  74. package/dist/presentation/presenters/presentGitHubCheckRun.d.ts.map +1 -1
  75. package/dist/presentation/presenters/presentGitHubCheckRun.js +2 -0
  76. package/dist/presentation/presenters/presentGitHubPrComment.d.ts.map +1 -1
  77. package/dist/presentation/presenters/presentGitHubPrComment.js +15 -21
  78. package/dist/presentation/presenters/presentScanDetails.d.ts.map +1 -1
  79. package/dist/presentation/presenters/presentScanDetails.js +52 -129
  80. package/dist/presentation/profile/buildProfileFromAssessment.d.ts +5 -0
  81. package/dist/presentation/profile/buildProfileFromAssessment.d.ts.map +1 -0
  82. package/dist/presentation/profile/buildProfileFromAssessment.js +58 -0
  83. package/dist/presentation/projectAssessmentFields.d.ts +4 -0
  84. package/dist/presentation/projectAssessmentFields.d.ts.map +1 -0
  85. package/dist/presentation/projectAssessmentFields.js +13 -0
  86. package/dist/presentation/surfaceParity.test.d.ts +2 -0
  87. package/dist/presentation/surfaceParity.test.d.ts.map +1 -0
  88. package/dist/presentation/surfaceParity.test.js +74 -0
  89. package/dist/productMessaging.d.ts +4 -4
  90. package/dist/productMessaging.d.ts.map +1 -1
  91. package/dist/productMessaging.js +4 -4
  92. package/dist/productMessaging.test.js +2 -2
  93. package/dist/repoIntelligenceSchema.d.ts +2 -2
  94. package/dist/resolvePipelineStatus.d.ts +15 -0
  95. package/dist/resolvePipelineStatus.d.ts.map +1 -0
  96. package/dist/resolvePipelineStatus.js +37 -0
  97. package/dist/scanCardPresentationState.d.ts +10 -0
  98. package/dist/scanCardPresentationState.d.ts.map +1 -0
  99. package/dist/scanCardPresentationState.js +4 -0
  100. package/dist/{scanDetailViewModel.d.ts → scanDetailViewModelTypes.d.ts} +60 -32
  101. package/dist/scanDetailViewModelTypes.d.ts.map +1 -0
  102. package/dist/scanDetailViewModelTypes.js +4 -0
  103. package/dist/scanResultSchema.d.ts +75 -2
  104. package/dist/scanResultSchema.d.ts.map +1 -1
  105. package/dist/scanResultSchema.js +4 -2
  106. package/dist/scanResultSchema.test.js +16 -0
  107. package/dist/scanSurfaceCopy.d.ts +2 -0
  108. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  109. package/dist/scanSurfaceCopy.js +2 -0
  110. package/dist/trustedScanGuards.test.js +7 -0
  111. package/dist/types.d.ts +3 -0
  112. package/dist/types.d.ts.map +1 -1
  113. package/package.json +1 -1
  114. package/dist/collectNarrativeWhyBullets.d.ts +0 -12
  115. package/dist/collectNarrativeWhyBullets.d.ts.map +0 -1
  116. package/dist/collectNarrativeWhyBullets.js +0 -104
  117. package/dist/deriveCardDisplaySummary.d.ts +0 -16
  118. package/dist/deriveCardDisplaySummary.d.ts.map +0 -1
  119. package/dist/deriveCardDisplaySummary.js +0 -56
  120. package/dist/deriveCardDisplaySummary.test.d.ts +0 -2
  121. package/dist/deriveCardDisplaySummary.test.d.ts.map +0 -1
  122. package/dist/deriveCardDisplaySummary.test.js +0 -89
  123. package/dist/deriveCardOperationalObservations.d.ts +0 -19
  124. package/dist/deriveCardOperationalObservations.d.ts.map +0 -1
  125. package/dist/deriveCardOperationalObservations.js +0 -180
  126. package/dist/deriveCardOperationalObservations.test.d.ts +0 -2
  127. package/dist/deriveCardOperationalObservations.test.d.ts.map +0 -1
  128. package/dist/deriveCardOperationalObservations.test.js +0 -268
  129. package/dist/deriveRiskScoreSummary.d.ts +0 -33
  130. package/dist/deriveRiskScoreSummary.d.ts.map +0 -1
  131. package/dist/deriveRiskScoreSummary.js +0 -90
  132. package/dist/deriveRiskScoreSummary.test.d.ts +0 -2
  133. package/dist/deriveRiskScoreSummary.test.d.ts.map +0 -1
  134. package/dist/deriveRiskScoreSummary.test.js +0 -47
  135. package/dist/deriveScanDetailRecommendations.d.ts +0 -63
  136. package/dist/deriveScanDetailRecommendations.d.ts.map +0 -1
  137. package/dist/deriveScanDetailRecommendations.js +0 -725
  138. package/dist/deriveScanDetailRecommendations.test.d.ts +0 -2
  139. package/dist/deriveScanDetailRecommendations.test.d.ts.map +0 -1
  140. package/dist/deriveScanDetailRecommendations.test.js +0 -181
  141. package/dist/deriveScanSummaryText.d.ts +0 -7
  142. package/dist/deriveScanSummaryText.d.ts.map +0 -1
  143. package/dist/deriveScanSummaryText.js +0 -24
  144. package/dist/deriveScanSummaryText.test.d.ts +0 -2
  145. package/dist/deriveScanSummaryText.test.d.ts.map +0 -1
  146. package/dist/deriveScanSummaryText.test.js +0 -77
  147. package/dist/formatCardEvidenceCounts.d.ts +0 -7
  148. package/dist/formatCardEvidenceCounts.d.ts.map +0 -1
  149. package/dist/formatCardEvidenceCounts.js +0 -19
  150. package/dist/prCheckRunPresentation.d.ts +0 -68
  151. package/dist/prCheckRunPresentation.d.ts.map +0 -1
  152. package/dist/prCheckRunPresentation.js +0 -319
  153. package/dist/prCheckRunPresentation.test.d.ts +0 -2
  154. package/dist/prCheckRunPresentation.test.d.ts.map +0 -1
  155. package/dist/prCheckRunPresentation.test.js +0 -278
  156. package/dist/presentation/intent/derivePresentationInterpretation.d.ts +0 -4
  157. package/dist/presentation/intent/derivePresentationInterpretation.d.ts.map +0 -1
  158. package/dist/presentation/intent/derivePresentationInterpretation.js +0 -202
  159. package/dist/presentation/intent/formatPresentationCopy.d.ts +0 -8
  160. package/dist/presentation/intent/formatPresentationCopy.d.ts.map +0 -1
  161. package/dist/presentation/intent/formatPresentationCopy.js +0 -140
  162. package/dist/presentation/intent/presentationIntent.test.d.ts +0 -2
  163. package/dist/presentation/intent/presentationIntent.test.d.ts.map +0 -1
  164. package/dist/presentation/intent/presentationIntent.test.js +0 -29
  165. package/dist/presentation/intent/verificationFocusLabels.d.ts +0 -2
  166. package/dist/presentation/intent/verificationFocusLabels.d.ts.map +0 -1
  167. package/dist/presentation/intent/verificationFocusLabels.js +0 -45
  168. package/dist/presentation/profile/derivePresentationProfile.d.ts +0 -12
  169. package/dist/presentation/profile/derivePresentationProfile.d.ts.map +0 -1
  170. package/dist/presentation/profile/derivePresentationProfile.js +0 -85
  171. package/dist/scanCardSummary.d.ts +0 -58
  172. package/dist/scanCardSummary.d.ts.map +0 -1
  173. package/dist/scanCardSummary.js +0 -146
  174. package/dist/scanCardSummary.test.d.ts +0 -2
  175. package/dist/scanCardSummary.test.d.ts.map +0 -1
  176. package/dist/scanCardSummary.test.js +0 -234
  177. package/dist/scanDetailViewModel.d.ts.map +0 -1
  178. package/dist/scanDetailViewModel.js +0 -636
  179. package/dist/scanDetailViewModel.test.d.ts +0 -2
  180. package/dist/scanDetailViewModel.test.d.ts.map +0 -1
  181. package/dist/scanDetailViewModel.test.js +0 -530
@@ -1,530 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { primaryPrExample } from "./productMessaging.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";
5
- import { analysisPreparationWithValidRepoIntel } from "./fixtures/repoIntelligenceTestHelpers.js";
6
- const baseResult = {
7
- totalScore: 10,
8
- layerScores: {
9
- security: 1,
10
- maintainability: 2,
11
- ecosystem: 3,
12
- upgradeImpact: 4,
13
- },
14
- findings: [],
15
- generatedAt: "2026-01-01T00:00:00.000Z",
16
- };
17
- function minimalResult(overrides = {}) {
18
- return {
19
- totalScore: 42,
20
- layerScores: {
21
- security: 10,
22
- maintainability: 20,
23
- ecosystem: 30,
24
- upgradeImpact: 15,
25
- },
26
- findings: [],
27
- generatedAt: new Date().toISOString(),
28
- decision: {
29
- recommendation: "needs_review",
30
- confidence: "medium",
31
- reasoning: ["Lockfile adds transitive depth on shared utilities."],
32
- },
33
- ...overrides,
34
- };
35
- }
36
- describe("reach vocabulary on fixtures", () => {
37
- it("quiet safe scan — Safe · Focused reach", () => {
38
- const vm = deriveScanDetailViewModel({
39
- ...baseResult,
40
- decision: {
41
- recommendation: "safe",
42
- confidence: "high",
43
- reasoning: ["No merge blockers"],
44
- },
45
- }, { scanId: "s1", status: "done" });
46
- expect(vm?.verdict.posture).toBe("safe");
47
- expect(vm?.verdict.scopeChip).toBe("Focused reach");
48
- expect(vm?.verdict.verdictLine).toContain("No merge blockers");
49
- expect(vm?.verdict.verdictLine).toContain("narrow footprint");
50
- });
51
- it("safe + wide reach — score 85", () => {
52
- const vm = deriveScanDetailViewModel({
53
- ...baseResult,
54
- totalScore: 85,
55
- decision: {
56
- recommendation: "safe",
57
- confidence: "high",
58
- reasoning: [],
59
- },
60
- changedPackages: Array.from({ length: 14 }, (_, i) => `pkg-${i}`),
61
- }, { scanId: "s2", status: "done" });
62
- expect(vm?.verdict.scopeChip).toBe("Wide reach");
63
- expect(vm?.verdict.verdictLine).toBe("No merge blockers - wide upgrade footprint");
64
- });
65
- it("needs review + limited reach — minimalResult score 42", () => {
66
- const vm = deriveScanDetailViewModel(minimalResult(), {
67
- scanId: "s3",
68
- status: "done",
69
- });
70
- expect(vm?.verdict.posture).toBe("needs_review");
71
- expect(vm?.verdict.scopeChip).toBe("Limited reach");
72
- expect(vm?.verdict.verdictLine).toContain("Review before merge");
73
- });
74
- it("risky + focused reach — low score, risky posture", () => {
75
- const vm = deriveScanDetailViewModel({
76
- ...baseResult,
77
- totalScore: 18,
78
- decision: {
79
- recommendation: "risky",
80
- confidence: "high",
81
- reasoning: ["Critical issue in direct dependency"],
82
- },
83
- findings: [
84
- {
85
- id: "f1",
86
- title: "Critical CVE",
87
- description: "Direct dependency vulnerability",
88
- severity: "critical",
89
- packageName: "minimist",
90
- },
91
- ],
92
- }, { scanId: "s4", status: "done" });
93
- expect(vm?.verdict.scopeChip).toBe("Focused reach");
94
- expect(vm?.verdict.posture).toBe("risky");
95
- expect(vm?.verdict.verdictLine).toContain("Resolve blockers");
96
- });
97
- it("risky + moderate reach — fat scan score 72", () => {
98
- const vm = deriveScanDetailViewModel({
99
- ...baseResult,
100
- totalScore: 72,
101
- decision: {
102
- recommendation: "risky",
103
- confidence: "high",
104
- reasoning: [],
105
- },
106
- graphInsights: {
107
- maxDepth: 9,
108
- nodes: 1500,
109
- edges: 4000,
110
- vulnerable: [
111
- {
112
- kind: "vulnerable",
113
- packageName: "lodash",
114
- direct: false,
115
- depth: 3,
116
- },
117
- ],
118
- },
119
- }, { scanId: "s5", status: "done" });
120
- expect(vm?.verdict.scopeChip).toBe("Moderate reach");
121
- expect(vm?.verdict.verdictLine).toContain("moderate dependency footprint");
122
- });
123
- });
124
- describe("deriveVerdictLine", () => {
125
- it("maps posture and reach bands", () => {
126
- expect(deriveVerdictLine("safe", 10)).toContain("narrow footprint");
127
- expect(deriveVerdictLine("safe", 85)).toContain("wide upgrade footprint");
128
- expect(deriveVerdictLine("needs_review", 42)).toContain("Review before merge");
129
- expect(deriveVerdictLine("risky", 18)).toContain("focused footprint");
130
- });
131
- });
132
- describe("Tier 1 success criterion", () => {
133
- it("surfaces package usage paths in narrative context and evidence when corpus available", () => {
134
- const vm = deriveScanDetailViewModel({
135
- ...baseResult,
136
- changedPackages: ["fastify"],
137
- analysisPreparation: analysisPreparationWithValidRepoIntel(),
138
- repoIntelligence: fixtureRepoIntelligenceFastify,
139
- insights: [
140
- {
141
- type: "usage_risk",
142
- priority: "high",
143
- confidence: "confirmed",
144
- scope: "changed",
145
- message: "Fastify middleware ordering may shift",
146
- context: "",
147
- remediation: "Run auth integration tests",
148
- },
149
- ],
150
- }, { scanId: "corpus", status: "done" });
151
- expect(vm?.narrativeContext.usageHighlights.length).toBeGreaterThan(0);
152
- expect(vm?.narrativeContext.usageContextLine).toMatch(/Used in/);
153
- expect(vm?.narrativeContext.blastRadiusFactors.length).toBeGreaterThan(0);
154
- const hotspotArea = vm?.evidence?.attentionAreas.find((a) => a.problemLabel.includes("hotspot"));
155
- expect(hotspotArea?.packages[0]?.name).toBe("fastify");
156
- expect(tier1PassesRecallTest(vm.operationalImpact)).toBe(true);
157
- });
158
- it("insight-rich scan passes recall test", () => {
159
- const vm = deriveScanDetailViewModel({
160
- ...minimalResult(),
161
- insights: [
162
- {
163
- type: "behavioral_change",
164
- priority: "high",
165
- confidence: "confirmed",
166
- scope: "changed",
167
- message: primaryPrExample.message,
168
- context: primaryPrExample.where[0],
169
- remediation: "Confirm auth guards still run before protected handlers",
170
- affectedFiles: [...primaryPrExample.where],
171
- },
172
- ],
173
- }, { scanId: "rich", status: "done" });
174
- expect(vm?.operationalImpact.status).toBe("rich");
175
- expect(vm?.operationalImpact.items[0]?.message).toContain("middleware");
176
- expect(tier1PassesRecallTest(vm.operationalImpact)).toBe(true);
177
- });
178
- it("graph-only lightweight scan uses F3 fallback", () => {
179
- const impact = deriveOperationalImpact({
180
- ...minimalResult(),
181
- insights: [],
182
- reportPresentation: { mode: "lightweight_pr_graph_baseline" },
183
- });
184
- expect(impact.status).toBe("fallback");
185
- expect(impact.fallbackMessage).toContain("limited code-change signal");
186
- expect(tier1PassesRecallTest(impact)).toBe(true);
187
- });
188
- it("quiet safe uses Q fallback", () => {
189
- const impact = deriveOperationalImpact({
190
- ...baseResult,
191
- decision: {
192
- recommendation: "safe",
193
- confidence: "high",
194
- reasoning: [],
195
- },
196
- });
197
- expect(impact.fallbackMessage).toContain("No application-level impact");
198
- });
199
- });
200
- describe("Act 2 guardrails", () => {
201
- it("sanitizes forbidden content", () => {
202
- expect(sanitizeAct2Theme("lodash 4.17.20 CVE-2021-23337")).toBeNull();
203
- expect(sanitizeAct2Theme("@scope/pkg@1.2.3")).toBeNull();
204
- expect(sanitizeAct2Theme("Known vulnerabilities in the transitive tree")).toBe("Known vulnerabilities in the transitive tree");
205
- });
206
- it("caps themes at ACT2_MAX_THEMES", () => {
207
- const fat = {
208
- ...baseResult,
209
- totalScore: 72,
210
- recommendations: [
211
- {
212
- id: "r1",
213
- title: "Reduce duplicate dependency versions",
214
- rationale: "semver overlap",
215
- impact: "high",
216
- },
217
- {
218
- id: "r2",
219
- title: "Review transitive dependency surface",
220
- rationale: "graph.transitive volume",
221
- impact: "high",
222
- },
223
- ],
224
- explain: {
225
- reasons: [
226
- {
227
- id: "g1",
228
- layer: "security",
229
- title: "graph.vulnerable packages",
230
- scoreImpact: 20,
231
- },
232
- {
233
- id: "g2",
234
- layer: "maintainability",
235
- title: "Stale releases on multiple direct dependencies",
236
- scoreImpact: 15,
237
- },
238
- {
239
- id: "g3",
240
- layer: "ecosystem",
241
- title: "graph.transitive volume cluster",
242
- scoreImpact: 12,
243
- },
244
- ],
245
- },
246
- graphInsights: {
247
- maxDepth: 9,
248
- nodes: 1500,
249
- edges: 4000,
250
- vulnerable: [
251
- {
252
- kind: "vulnerable",
253
- packageName: "lodash",
254
- direct: false,
255
- depth: 3,
256
- },
257
- ],
258
- hotspots: Array.from({ length: 5 }, (_, i) => ({
259
- kind: "hotspot",
260
- packageName: `pkg-${i}`,
261
- direct: true,
262
- depth: 1,
263
- })),
264
- },
265
- decision: {
266
- recommendation: "risky",
267
- confidence: "high",
268
- reasoning: ["Multiple structural concerns"],
269
- },
270
- };
271
- const themes = deriveAct2Themes(fat);
272
- expect(themes.length).toBeLessThanOrEqual(ACT2_MAX_THEMES);
273
- for (const theme of themes) {
274
- expect(theme.length).toBeLessThanOrEqual(80);
275
- expect(sanitizeAct2Theme(theme)).toBe(theme);
276
- }
277
- });
278
- it("Act 2 themes never contain package names from evidence", () => {
279
- const vm = deriveScanDetailViewModel({
280
- ...baseResult,
281
- totalScore: 72,
282
- graphInsights: {
283
- maxDepth: 9,
284
- nodes: 1500,
285
- edges: 4000,
286
- vulnerable: [
287
- {
288
- kind: "vulnerable",
289
- packageName: "lodash",
290
- direct: false,
291
- depth: 3,
292
- },
293
- ],
294
- },
295
- decision: {
296
- recommendation: "risky",
297
- confidence: "high",
298
- reasoning: [],
299
- },
300
- }, { scanId: "act2", status: "done" });
301
- for (const theme of vm?.because?.themes ?? []) {
302
- expect(theme.toLowerCase()).not.toContain("lodash");
303
- }
304
- expect(vm?.evidence?.attentionAreas[0]?.packages[0]?.name).toBe("lodash");
305
- });
306
- });
307
- describe("scenario matrix", () => {
308
- function assertScenario(name, vm, checks) {
309
- expect(vm, name).not.toBeNull();
310
- if (!vm)
311
- return;
312
- if (checks.tier0MaxWords != null) {
313
- const words = vm.verdict.verdictLine.split(/\s+/).length;
314
- expect(words, `${name} tier0`).toBeLessThanOrEqual(checks.tier0MaxWords);
315
- }
316
- if (checks.tier1Meaningful) {
317
- expect(tier1PassesRecallTest(vm.operationalImpact), `${name} tier1`).toBe(true);
318
- }
319
- if (checks.act2MaxBullets != null && vm.because) {
320
- expect(vm.because.themes.length, `${name} act2`).toBeLessThanOrEqual(checks.act2MaxBullets);
321
- }
322
- if (checks.act3Navigable && vm.evidence) {
323
- expect(vm.evidence.findings.length + vm.evidence.attentionAreas.length, `${name} act3`).toBeGreaterThan(0);
324
- }
325
- if (checks.recommendationsActionable) {
326
- expect(vm.recommendedActions.items.length, `${name} recs`).toBeGreaterThan(0);
327
- expect(vm.recommendedActions.items[0]?.detail.why.length, `${name} rec detail`).toBeGreaterThan(10);
328
- }
329
- }
330
- it("quiet safe scan", () => {
331
- assertScenario("quiet safe", deriveScanDetailViewModel({
332
- ...baseResult,
333
- decision: {
334
- recommendation: "safe",
335
- confidence: "high",
336
- reasoning: [],
337
- },
338
- }, { scanId: "q", status: "done" }), { tier0MaxWords: 12, tier1Meaningful: true, act2MaxBullets: 0 });
339
- });
340
- it("safe + wide reach", () => {
341
- assertScenario("safe wide", deriveScanDetailViewModel({
342
- ...baseResult,
343
- totalScore: 88,
344
- decision: {
345
- recommendation: "safe",
346
- confidence: "high",
347
- reasoning: [],
348
- },
349
- graphInsights: {
350
- maxDepth: 8,
351
- nodes: 900,
352
- edges: 1200,
353
- deepest: [],
354
- },
355
- }, { scanId: "sw", status: "done" }), { tier0MaxWords: 12, tier1Meaningful: true });
356
- });
357
- it("risky vulnerability-heavy", () => {
358
- assertScenario("risky vuln", deriveScanDetailViewModel({
359
- ...baseResult,
360
- totalScore: 72,
361
- findings: [
362
- {
363
- id: "f1",
364
- title: "CVE in lodash",
365
- description: "Remote code execution",
366
- severity: "critical",
367
- packageName: "lodash",
368
- },
369
- ],
370
- graphInsights: {
371
- maxDepth: 5,
372
- nodes: 400,
373
- edges: 800,
374
- vulnerable: [
375
- {
376
- kind: "vulnerable",
377
- packageName: "lodash",
378
- direct: false,
379
- depth: 2,
380
- },
381
- ],
382
- },
383
- recommendations: [
384
- {
385
- id: "r1",
386
- title: "Upgrade lodash to patched version",
387
- rationale: "Resolves critical CVE",
388
- impact: "high",
389
- },
390
- ],
391
- decision: {
392
- recommendation: "risky",
393
- confidence: "high",
394
- reasoning: [],
395
- },
396
- }, { scanId: "rv", status: "done" }), {
397
- tier0MaxWords: 12,
398
- tier1Meaningful: true,
399
- act2MaxBullets: 4,
400
- act3Navigable: true,
401
- recommendationsActionable: true,
402
- });
403
- });
404
- it("graph-only scan", () => {
405
- assertScenario("graph only", deriveScanDetailViewModel({
406
- ...minimalResult(),
407
- insights: [],
408
- reportPresentation: { mode: "lightweight_pr_graph_baseline" },
409
- }, { scanId: "go", status: "done" }), { tier0MaxWords: 12, tier1Meaningful: true, act2MaxBullets: 4 });
410
- });
411
- it("insight-rich scan", () => {
412
- assertScenario("insight rich", deriveScanDetailViewModel({
413
- ...minimalResult({ totalScore: 55 }),
414
- insights: [
415
- {
416
- type: "behavioral_change",
417
- priority: "high",
418
- confidence: "confirmed",
419
- scope: "changed",
420
- message: primaryPrExample.message,
421
- context: "apps/api/src/middleware/auth.ts",
422
- remediation: "Run auth integration tests",
423
- },
424
- ],
425
- recommendations: [
426
- {
427
- id: "r1",
428
- title: "Review auth middleware ordering",
429
- rationale: "Middleware order affects auth guards",
430
- impact: "high",
431
- },
432
- ],
433
- }, { scanId: "ir", status: "done" }), {
434
- tier0MaxWords: 12,
435
- tier1Meaningful: true,
436
- act2MaxBullets: 4,
437
- recommendationsActionable: true,
438
- });
439
- });
440
- it("large enterprise scan", () => {
441
- const fat = {
442
- ...minimalResult({ totalScore: 72 }),
443
- graphInsights: {
444
- maxDepth: 14,
445
- nodes: 2400,
446
- edges: 9000,
447
- deepest: Array.from({ length: 20 }, (_, i) => ({
448
- kind: "deep",
449
- packageName: `pkg-${i}`,
450
- depth: 10 + i,
451
- direct: false,
452
- via: ["a", "b"],
453
- })),
454
- hotspots: Array.from({ length: 12 }, (_, i) => ({
455
- kind: "hotspot",
456
- packageName: `hot-${i}`,
457
- depth: 3,
458
- direct: false,
459
- })),
460
- vulnerable: Array.from({ length: 8 }, (_, i) => ({
461
- kind: "vulnerable",
462
- packageName: `vuln-${i}`,
463
- depth: 2,
464
- direct: false,
465
- })),
466
- },
467
- insights: Array.from({ length: 30 }, (_, i) => ({
468
- type: "behavioral_change",
469
- priority: "high",
470
- confidence: "confirmed",
471
- scope: "changed",
472
- message: `Insight message ${i} about runtime behavior`,
473
- context: `src/module-${i}/index.ts`,
474
- remediation: `Verify step ${i}`,
475
- })),
476
- recommendations: Array.from({ length: 25 }, (_, i) => ({
477
- id: `rec-${i}`,
478
- title: `Recommendation title ${i}`,
479
- rationale: `Rationale ${i}`,
480
- impact: "high",
481
- })),
482
- findings: Array.from({ length: 40 }, (_, i) => ({
483
- id: `find-${i}`,
484
- title: `Finding ${i}`,
485
- description: `Description ${i}`,
486
- severity: "high",
487
- packageName: `pkg-${i}`,
488
- })),
489
- decision: {
490
- recommendation: "risky",
491
- confidence: "high",
492
- reasoning: [],
493
- },
494
- };
495
- const vm = deriveScanDetailViewModel(fat, {
496
- scanId: "fat",
497
- status: "done",
498
- });
499
- expect(vm?.operationalImpact.items.length).toBeGreaterThan(TIER1_MAX_VISIBLE_IMPACTS);
500
- expect(vm?.evidence?.findings.length).toBe(40);
501
- expect(vm?.recommendedActions.items.length).toBeLessThanOrEqual(3);
502
- expect(vm?.recommendedActions.items[0]?.detail.whyNow.length).toBeGreaterThan(0);
503
- expect(vm?.evidence?.findingsOverflowCount).toBe(30);
504
- });
505
- it("includes signalSummary on done scans", () => {
506
- const vm = deriveScanDetailViewModel({ ...minimalResult(), totalScore: 71 }, { scanId: "risk", status: "done" });
507
- expect(vm?.signalSummary?.overallBand).toBe("moderate");
508
- expect(vm?.signalSummary?.score).toBe(71);
509
- expect(vm?.signalSummary?.overallLabel).toBe("Moderate");
510
- });
511
- it("derives follow-up bridge note from recommended action count", () => {
512
- expect(deriveFollowUpBridgeNote(0)).toBeNull();
513
- expect(deriveFollowUpBridgeNote(1)).toBe("1 follow-up improvement was identified");
514
- expect(deriveFollowUpBridgeNote(3)).toBe("3 follow-up improvements were identified");
515
- const vm = deriveScanDetailViewModel(minimalResult(), {
516
- scanId: "bridge",
517
- status: "done",
518
- });
519
- expect(vm?.followUpBridgeNote).toMatch(/follow-up improvement/);
520
- });
521
- });
522
- describe("repo context", () => {
523
- it("defaults to hidden", () => {
524
- const vm = deriveScanDetailViewModel(minimalResult(), {
525
- scanId: "x",
526
- status: "done",
527
- });
528
- expect(vm?.repoContext).toEqual({ status: "hidden" });
529
- });
530
- });