@mergesignal/shared 0.2.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.
- package/dist/actionsStepSummary.d.ts +29 -0
- package/dist/actionsStepSummary.d.ts.map +1 -0
- package/dist/actionsStepSummary.js +757 -0
- package/dist/actionsStepSummary.test.d.ts +2 -0
- package/dist/actionsStepSummary.test.d.ts.map +1 -0
- package/dist/actionsStepSummary.test.js +348 -0
- package/dist/deriveScanSummaryText.d.ts +7 -0
- package/dist/deriveScanSummaryText.d.ts.map +1 -0
- package/dist/deriveScanSummaryText.js +24 -0
- package/dist/deriveScanSummaryText.test.d.ts +2 -0
- package/dist/deriveScanSummaryText.test.d.ts.map +1 -0
- package/dist/deriveScanSummaryText.test.js +77 -0
- package/dist/formatInsight.d.ts +10 -0
- package/dist/formatInsight.d.ts.map +1 -0
- package/dist/formatInsight.js +49 -0
- package/dist/formatInsight.test.d.ts +2 -0
- package/dist/formatInsight.test.d.ts.map +1 -0
- package/dist/formatInsight.test.js +102 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/prCheckRunPresentation.d.ts +67 -0
- package/dist/prCheckRunPresentation.d.ts.map +1 -0
- package/dist/prCheckRunPresentation.js +314 -0
- package/dist/prCheckRunPresentation.test.d.ts +2 -0
- package/dist/prCheckRunPresentation.test.d.ts.map +1 -0
- package/dist/prCheckRunPresentation.test.js +277 -0
- package/dist/riskVocabulary.d.ts +31 -0
- package/dist/riskVocabulary.d.ts.map +1 -0
- package/dist/riskVocabulary.js +62 -0
- package/dist/riskVocabulary.test.d.ts +2 -0
- package/dist/riskVocabulary.test.d.ts.map +1 -0
- package/dist/riskVocabulary.test.js +14 -0
- package/dist/scanResultSchema.d.ts +116 -0
- package/dist/scanResultSchema.d.ts.map +1 -0
- package/dist/scanResultSchema.js +100 -0
- package/dist/scanResultSchema.test.d.ts +2 -0
- package/dist/scanResultSchema.test.d.ts.map +1 -0
- package/dist/scanResultSchema.test.js +105 -0
- package/dist/scanSurfaceCopy.d.ts +90 -0
- package/dist/scanSurfaceCopy.d.ts.map +1 -0
- package/dist/scanSurfaceCopy.js +104 -0
- package/dist/scanSurfaceCopy.test.d.ts +2 -0
- package/dist/scanSurfaceCopy.test.d.ts.map +1 -0
- package/dist/scanSurfaceCopy.test.js +14 -0
- package/dist/selectTopAffectedAreas.d.ts +17 -0
- package/dist/selectTopAffectedAreas.d.ts.map +1 -0
- package/dist/selectTopAffectedAreas.js +80 -0
- package/dist/selectTopAffectedAreas.test.d.ts +2 -0
- package/dist/selectTopAffectedAreas.test.d.ts.map +1 -0
- package/dist/selectTopAffectedAreas.test.js +179 -0
- package/dist/trustedScanGuards.d.ts +39 -0
- package/dist/trustedScanGuards.d.ts.map +1 -0
- package/dist/trustedScanGuards.js +88 -0
- package/dist/trustedScanGuards.test.d.ts +2 -0
- package/dist/trustedScanGuards.test.d.ts.map +1 -0
- package/dist/trustedScanGuards.test.js +143 -0
- package/dist/types.d.ts +362 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/package.json +49 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionsStepSummary.test.d.ts","sourceRoot":"","sources":["../src/actionsStepSummary.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ACTIONS_SUMMARY_DEFAULT_MAX_CHARS, ACTIONS_SUMMARY_DEFAULT_MAX_LINES, actionsSummaryDefaultFoldPrefix, buildActionsStepSummaryMarkdown, humanizeEngineSurfaceText, layerDriverSummary, layerRiskBandLabel, sortPRInsightsForDisplay, sortRecommendationsForDisplay, } from "./actionsStepSummary.js";
|
|
3
|
+
import { scanSurfaceCopyFlat } from "./scanSurfaceCopy.js";
|
|
4
|
+
const copy = scanSurfaceCopyFlat();
|
|
5
|
+
function makeInsight(overrides) {
|
|
6
|
+
return {
|
|
7
|
+
type: "usage_risk",
|
|
8
|
+
priority: "high",
|
|
9
|
+
confidence: "likely",
|
|
10
|
+
scope: "changed",
|
|
11
|
+
context: "ctx",
|
|
12
|
+
remediation: "fix",
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("layerRiskBandLabel", () => {
|
|
17
|
+
it("maps numeric layer scores to Low / Moderate / High", () => {
|
|
18
|
+
expect(layerRiskBandLabel(0)).toBe("Low");
|
|
19
|
+
expect(layerRiskBandLabel(19.9)).toBe("Low");
|
|
20
|
+
expect(layerRiskBandLabel(25)).toBe("Moderate");
|
|
21
|
+
expect(layerRiskBandLabel(50)).toBe("High");
|
|
22
|
+
expect(layerRiskBandLabel(null)).toBe("—");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("humanizeEngineSurfaceText", () => {
|
|
26
|
+
it("rewrites known graph metric tokens without adding new facts", () => {
|
|
27
|
+
expect(humanizeEngineSurfaceText("graph.depth")).toBe("Indirect dependency depth");
|
|
28
|
+
expect(humanizeEngineSurfaceText("graph.transitive volume")).toBe("Transitive dependency volume");
|
|
29
|
+
expect(humanizeEngineSurfaceText("graph.fan in max")).toBe("Many import paths converge on a few widely shared packages");
|
|
30
|
+
});
|
|
31
|
+
it("rewrites ecosystem surface and duplicate-path tokens", () => {
|
|
32
|
+
expect(humanizeEngineSurfaceText("Ecosystem.package surface")).toBe("Broad declared package footprint");
|
|
33
|
+
expect(humanizeEngineSurfaceText("Graph.duplicates")).toBe("Overlapping dependency paths to the same packages");
|
|
34
|
+
});
|
|
35
|
+
it("merges dependency depth phrasing into one canonical reviewer line", () => {
|
|
36
|
+
expect(humanizeEngineSurfaceText("dependency chain depth")).toBe("Indirect dependency depth");
|
|
37
|
+
expect(humanizeEngineSurfaceText("dependency depth")).toBe("Indirect dependency depth");
|
|
38
|
+
});
|
|
39
|
+
it("leaves ordinary explain titles mostly intact", () => {
|
|
40
|
+
expect(humanizeEngineSurfaceText("Duplicate majors on runtime boundary")).toBe("Duplicate majors on runtime boundary");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("sortPRInsightsForDisplay", () => {
|
|
44
|
+
it("orders by priority, confidence, then changed scope", () => {
|
|
45
|
+
const a = makeInsight({
|
|
46
|
+
message: "a",
|
|
47
|
+
priority: "medium",
|
|
48
|
+
confidence: "confirmed",
|
|
49
|
+
scope: "all",
|
|
50
|
+
});
|
|
51
|
+
const b = makeInsight({
|
|
52
|
+
message: "b",
|
|
53
|
+
priority: "high",
|
|
54
|
+
confidence: "likely",
|
|
55
|
+
scope: "changed",
|
|
56
|
+
});
|
|
57
|
+
const c = makeInsight({
|
|
58
|
+
message: "c",
|
|
59
|
+
priority: "high",
|
|
60
|
+
confidence: "confirmed",
|
|
61
|
+
scope: "all",
|
|
62
|
+
});
|
|
63
|
+
expect(sortPRInsightsForDisplay([a, b, c]).map((i) => i.message)).toEqual([
|
|
64
|
+
"c",
|
|
65
|
+
"b",
|
|
66
|
+
"a",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("sortRecommendationsForDisplay", () => {
|
|
71
|
+
it("orders by priorityScore then impact", () => {
|
|
72
|
+
const recs = [
|
|
73
|
+
{
|
|
74
|
+
id: "1",
|
|
75
|
+
title: "Low prio",
|
|
76
|
+
rationale: "",
|
|
77
|
+
impact: "low",
|
|
78
|
+
priorityScore: 10,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "2",
|
|
82
|
+
title: "High prio",
|
|
83
|
+
rationale: "",
|
|
84
|
+
impact: "high",
|
|
85
|
+
priorityScore: 90,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
expect(sortRecommendationsForDisplay(recs).map((r) => r.id)).toEqual([
|
|
89
|
+
"2",
|
|
90
|
+
"1",
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("buildActionsStepSummaryMarkdown", () => {
|
|
95
|
+
const trustedBase = {
|
|
96
|
+
totalScore: 42,
|
|
97
|
+
layerScores: {
|
|
98
|
+
security: 10,
|
|
99
|
+
maintainability: 50,
|
|
100
|
+
ecosystem: 30,
|
|
101
|
+
upgradeImpact: 40,
|
|
102
|
+
},
|
|
103
|
+
findings: [],
|
|
104
|
+
recommendations: [],
|
|
105
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
106
|
+
methodologyVersion: "engine-test-fixture/v1",
|
|
107
|
+
decision: {
|
|
108
|
+
recommendation: "needs_review",
|
|
109
|
+
confidence: "high",
|
|
110
|
+
reasoning: ["Transitive dependency depth increased on hot paths."],
|
|
111
|
+
},
|
|
112
|
+
insights: [],
|
|
113
|
+
};
|
|
114
|
+
it("trusted: includes posture, risk index direction, and score breakdown", () => {
|
|
115
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
116
|
+
result: trustedBase,
|
|
117
|
+
profile: "trusted",
|
|
118
|
+
copy,
|
|
119
|
+
});
|
|
120
|
+
expect(md).toMatch(/# MergeSignal dependency review — Needs review/);
|
|
121
|
+
expect(md).toMatch(/Risk index 42\/100/);
|
|
122
|
+
expect(md).toContain(copy["actions.riskIndexDirectionShort"]);
|
|
123
|
+
expect(md).toContain("| Layer | Score | Layer risk |");
|
|
124
|
+
expect(md).toContain("| Security | 10 | Low |");
|
|
125
|
+
expect(md).toMatch(/<details>/);
|
|
126
|
+
expect(md).not.toContain(copy["actions.layerScoreGlossary"]);
|
|
127
|
+
});
|
|
128
|
+
it("trusted: surfaces top insights and moves remainder to details", () => {
|
|
129
|
+
const insights = [
|
|
130
|
+
makeInsight({ message: "First critical", priority: "critical" }),
|
|
131
|
+
makeInsight({ message: "Second high", priority: "high" }),
|
|
132
|
+
makeInsight({ message: "Third medium", priority: "medium" }),
|
|
133
|
+
makeInsight({ message: "Fourth overflow", priority: "medium" }),
|
|
134
|
+
];
|
|
135
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
136
|
+
result: { ...trustedBase, insights },
|
|
137
|
+
profile: "trusted",
|
|
138
|
+
copy,
|
|
139
|
+
});
|
|
140
|
+
expect(md).toContain("First critical");
|
|
141
|
+
expect(md).toContain(copy["actions.moreInsightsDetailsSummary"]);
|
|
142
|
+
expect(md).toContain("Fourth overflow");
|
|
143
|
+
});
|
|
144
|
+
it("trusted: no decision uses posture-unavailable copy", () => {
|
|
145
|
+
const { decision: _d, ...rest } = trustedBase;
|
|
146
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
147
|
+
result: rest,
|
|
148
|
+
profile: "trusted",
|
|
149
|
+
copy,
|
|
150
|
+
});
|
|
151
|
+
expect(md).toContain(copy["actions.mergePostureUnavailableShort"]);
|
|
152
|
+
});
|
|
153
|
+
it("development + stub shows demo banner and score breakdown", () => {
|
|
154
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
155
|
+
result: {
|
|
156
|
+
...trustedBase,
|
|
157
|
+
methodologyVersion: "engine-stub/v2",
|
|
158
|
+
decision: {
|
|
159
|
+
recommendation: "safe",
|
|
160
|
+
confidence: "high",
|
|
161
|
+
reasoning: [],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
profile: "development",
|
|
165
|
+
copy,
|
|
166
|
+
});
|
|
167
|
+
expect(md).toContain(copy["actions.demoSummaryBanner"]);
|
|
168
|
+
expect(md).toContain("| Layer | Score | Layer risk |");
|
|
169
|
+
expect(md).toContain(copy["actions.layerScoreGlossary"]);
|
|
170
|
+
});
|
|
171
|
+
it("default-fold budget: heavy fixture stays within caps", () => {
|
|
172
|
+
const long = "x".repeat(400);
|
|
173
|
+
const insights = Array.from({ length: 12 }, (_, i) => makeInsight({
|
|
174
|
+
message: `${long} ${i}`,
|
|
175
|
+
priority: i === 0 ? "critical" : "medium",
|
|
176
|
+
}));
|
|
177
|
+
const recs = Array.from({ length: 8 }, (_, i) => ({
|
|
178
|
+
id: `r${i}`,
|
|
179
|
+
title: `Rec ${i}`,
|
|
180
|
+
rationale: long,
|
|
181
|
+
impact: "high",
|
|
182
|
+
priorityScore: 100 - i,
|
|
183
|
+
}));
|
|
184
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
185
|
+
result: {
|
|
186
|
+
...trustedBase,
|
|
187
|
+
insights,
|
|
188
|
+
recommendations: recs,
|
|
189
|
+
explain: {
|
|
190
|
+
reasons: Array.from({ length: 10 }, (_, i) => ({
|
|
191
|
+
id: `e${i}`,
|
|
192
|
+
layer: "security",
|
|
193
|
+
title: `Explain ${i}`,
|
|
194
|
+
scoreImpact: 1,
|
|
195
|
+
})),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
profile: "trusted",
|
|
199
|
+
copy,
|
|
200
|
+
});
|
|
201
|
+
const prefix = actionsSummaryDefaultFoldPrefix(md);
|
|
202
|
+
expect(prefix.split("\n").length).toBeLessThanOrEqual(ACTIONS_SUMMARY_DEFAULT_MAX_LINES + 4);
|
|
203
|
+
expect(prefix.length).toBeLessThanOrEqual(ACTIONS_SUMMARY_DEFAULT_MAX_CHARS + 400);
|
|
204
|
+
});
|
|
205
|
+
it("includes scan-specific layer drivers from explain.reasons", () => {
|
|
206
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
207
|
+
result: {
|
|
208
|
+
...trustedBase,
|
|
209
|
+
layerScores: { ...trustedBase.layerScores, maintainability: 72 },
|
|
210
|
+
explain: {
|
|
211
|
+
reasons: [
|
|
212
|
+
{
|
|
213
|
+
id: "x",
|
|
214
|
+
layer: "maintainability",
|
|
215
|
+
title: "Duplicate majors on runtime boundary",
|
|
216
|
+
scoreImpact: 12,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "y",
|
|
220
|
+
layer: "maintainability",
|
|
221
|
+
title: "Churn in devDependencies cluster",
|
|
222
|
+
scoreImpact: 5,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
profile: "trusted",
|
|
228
|
+
copy,
|
|
229
|
+
});
|
|
230
|
+
expect(md).toContain(copy["actions.layerDriversHeading"]);
|
|
231
|
+
expect(md).toContain("Duplicate majors on runtime boundary");
|
|
232
|
+
});
|
|
233
|
+
it("layerDriverSummary returns null when no explain data for layer", () => {
|
|
234
|
+
expect(layerDriverSummary(trustedBase, "security", 120)).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
it("generic recommendation titles lead with rationale in default fold", () => {
|
|
237
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
238
|
+
result: {
|
|
239
|
+
...trustedBase,
|
|
240
|
+
insights: [],
|
|
241
|
+
recommendations: [
|
|
242
|
+
{
|
|
243
|
+
id: "r1",
|
|
244
|
+
title: "Reduce transitive dependency surface area",
|
|
245
|
+
rationale: "This PR adds lodash twice at incompatible semver ranges pulled through different path aliases, widening the install tree.",
|
|
246
|
+
impact: "high",
|
|
247
|
+
priorityScore: 90,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
profile: "trusted",
|
|
252
|
+
copy,
|
|
253
|
+
});
|
|
254
|
+
expect(md).toContain(copy["actions.recReviewLeadPrefix"]);
|
|
255
|
+
expect(md).toContain("incompatible semver");
|
|
256
|
+
expect(md).toContain("*Focus:*");
|
|
257
|
+
expect(md).toContain("Reduce transitive dependency surface area");
|
|
258
|
+
});
|
|
259
|
+
it("applies rec review prefix only to the first rationale-first recommendation", () => {
|
|
260
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
261
|
+
result: {
|
|
262
|
+
...trustedBase,
|
|
263
|
+
insights: [],
|
|
264
|
+
recommendations: [
|
|
265
|
+
{
|
|
266
|
+
id: "r1",
|
|
267
|
+
title: "Reduce transitive dependency surface area",
|
|
268
|
+
rationale: "This PR adds lodash twice at incompatible semver ranges pulled through different path aliases, widening the install tree.",
|
|
269
|
+
impact: "high",
|
|
270
|
+
priorityScore: 90,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "r2",
|
|
274
|
+
title: "Minimize dependency footprint in the service layer",
|
|
275
|
+
rationale: "Several packages are pulled in only for types while runtime bundles still resolve overlapping majors, which increases upgrade coupling across services.",
|
|
276
|
+
impact: "high",
|
|
277
|
+
priorityScore: 85,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
profile: "trusted",
|
|
282
|
+
copy,
|
|
283
|
+
});
|
|
284
|
+
const prefix = copy["actions.recReviewLeadPrefix"];
|
|
285
|
+
const first = md.indexOf(prefix);
|
|
286
|
+
const second = md.indexOf(prefix, first + 1);
|
|
287
|
+
expect(first).toBeGreaterThan(-1);
|
|
288
|
+
expect(second).toBe(-1);
|
|
289
|
+
});
|
|
290
|
+
it("humanizes graph metric tokens in explain-driven layer drivers", () => {
|
|
291
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
292
|
+
result: {
|
|
293
|
+
...trustedBase,
|
|
294
|
+
layerScores: { ...trustedBase.layerScores, maintainability: 72 },
|
|
295
|
+
explain: {
|
|
296
|
+
reasons: [
|
|
297
|
+
{
|
|
298
|
+
id: "gdepth",
|
|
299
|
+
layer: "maintainability",
|
|
300
|
+
title: "graph.depth",
|
|
301
|
+
scoreImpact: 12,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
profile: "trusted",
|
|
307
|
+
copy,
|
|
308
|
+
});
|
|
309
|
+
expect(md).toContain("Indirect dependency depth");
|
|
310
|
+
expect(md).not.toContain("graph.depth");
|
|
311
|
+
});
|
|
312
|
+
it("includes graph details when graphInsights present", () => {
|
|
313
|
+
const md = buildActionsStepSummaryMarkdown({
|
|
314
|
+
result: {
|
|
315
|
+
...trustedBase,
|
|
316
|
+
graphInsights: {
|
|
317
|
+
maxDepth: 7,
|
|
318
|
+
nodes: 900,
|
|
319
|
+
edges: 1200,
|
|
320
|
+
deepest: [
|
|
321
|
+
{
|
|
322
|
+
kind: "deep",
|
|
323
|
+
packageName: "lodash",
|
|
324
|
+
direct: false,
|
|
325
|
+
depth: 7,
|
|
326
|
+
via: ["a", "b"],
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
hotspots: [
|
|
330
|
+
{
|
|
331
|
+
kind: "hotspot",
|
|
332
|
+
packageName: "x",
|
|
333
|
+
direct: true,
|
|
334
|
+
depth: 1,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
vulnerable: [],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
profile: "trusted",
|
|
341
|
+
copy,
|
|
342
|
+
});
|
|
343
|
+
expect(md).toContain(copy["actions.dependencyGraphDetailsSummary"]);
|
|
344
|
+
expect(md).toContain("moderately deep");
|
|
345
|
+
expect(md).toContain("fairly broad");
|
|
346
|
+
expect(md).toMatch(/> \*max chain length 7 · 900 resolved packages\*/);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ScanResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One-line summary for PR scan list cards (server or client).
|
|
4
|
+
* Prefers decision reasoning, then finding severity counts.
|
|
5
|
+
*/
|
|
6
|
+
export declare function deriveScanSummaryText(result: ScanResult | null | undefined): string | null;
|
|
7
|
+
//# sourceMappingURL=deriveScanSummaryText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deriveScanSummaryText.d.ts","sourceRoot":"","sources":["../src/deriveScanSummaryText.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,GAAG,IAAI,CAqBf"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line summary for PR scan list cards (server or client).
|
|
3
|
+
* Prefers decision reasoning, then finding severity counts.
|
|
4
|
+
*/
|
|
5
|
+
export function deriveScanSummaryText(result) {
|
|
6
|
+
if (!result)
|
|
7
|
+
return null;
|
|
8
|
+
const decision = result.decision;
|
|
9
|
+
const reasoning = decision?.reasoning;
|
|
10
|
+
if (Array.isArray(reasoning) && reasoning.length > 0) {
|
|
11
|
+
const first = String(reasoning[0]).trim();
|
|
12
|
+
return first.length > 120 ? first.slice(0, 119) + "…" : first;
|
|
13
|
+
}
|
|
14
|
+
const findings = result.findings;
|
|
15
|
+
if (Array.isArray(findings) && findings.length > 0) {
|
|
16
|
+
const high = findings.filter((f) => f.severity === "high").length;
|
|
17
|
+
const medium = findings.filter((f) => f.severity === "medium").length;
|
|
18
|
+
if (high > 0)
|
|
19
|
+
return `${high} high-severity finding${high > 1 ? "s" : ""} detected`;
|
|
20
|
+
if (medium > 0)
|
|
21
|
+
return `${medium} medium-severity finding${medium > 1 ? "s" : ""} detected`;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deriveScanSummaryText.test.d.ts","sourceRoot":"","sources":["../src/deriveScanSummaryText.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
|
|
3
|
+
describe("deriveScanSummaryText", () => {
|
|
4
|
+
it("returns null for empty input", () => {
|
|
5
|
+
expect(deriveScanSummaryText(undefined)).toBeNull();
|
|
6
|
+
expect(deriveScanSummaryText(null)).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
it("uses first decision reasoning", () => {
|
|
9
|
+
const r = {
|
|
10
|
+
totalScore: 10,
|
|
11
|
+
layerScores: {
|
|
12
|
+
security: 1,
|
|
13
|
+
maintainability: 2,
|
|
14
|
+
ecosystem: 3,
|
|
15
|
+
upgradeImpact: 4,
|
|
16
|
+
},
|
|
17
|
+
findings: [],
|
|
18
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
19
|
+
decision: {
|
|
20
|
+
recommendation: "needs_review",
|
|
21
|
+
confidence: "medium",
|
|
22
|
+
reasoning: ["First reason line", "Second"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
expect(deriveScanSummaryText(r)).toBe("First reason line");
|
|
26
|
+
});
|
|
27
|
+
it("truncates long reasoning", () => {
|
|
28
|
+
const long = "a".repeat(130);
|
|
29
|
+
const r = {
|
|
30
|
+
totalScore: 10,
|
|
31
|
+
layerScores: {
|
|
32
|
+
security: 1,
|
|
33
|
+
maintainability: 2,
|
|
34
|
+
ecosystem: 3,
|
|
35
|
+
upgradeImpact: 4,
|
|
36
|
+
},
|
|
37
|
+
findings: [],
|
|
38
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
39
|
+
decision: {
|
|
40
|
+
recommendation: "safe",
|
|
41
|
+
confidence: "high",
|
|
42
|
+
reasoning: [long],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
expect(deriveScanSummaryText(r).length).toBe(120);
|
|
46
|
+
expect(deriveScanSummaryText(r).endsWith("…")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it("falls back to high-severity findings", () => {
|
|
49
|
+
const r = {
|
|
50
|
+
totalScore: 80,
|
|
51
|
+
layerScores: {
|
|
52
|
+
security: 80,
|
|
53
|
+
maintainability: 10,
|
|
54
|
+
ecosystem: 10,
|
|
55
|
+
upgradeImpact: 10,
|
|
56
|
+
},
|
|
57
|
+
findings: [
|
|
58
|
+
{
|
|
59
|
+
id: "1",
|
|
60
|
+
title: "x",
|
|
61
|
+
description: "d",
|
|
62
|
+
severity: "high",
|
|
63
|
+
packageName: "p",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "2",
|
|
67
|
+
title: "y",
|
|
68
|
+
description: "d",
|
|
69
|
+
severity: "high",
|
|
70
|
+
packageName: "q",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
74
|
+
};
|
|
75
|
+
expect(deriveScanSummaryText(r)).toBe("2 high-severity findings detected");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PRInsight, PRDecision, PRDecisionRecommendation } from "./types.js";
|
|
2
|
+
export type FormattedInsight = {
|
|
3
|
+
message: string;
|
|
4
|
+
where: string;
|
|
5
|
+
action: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function decisionTitle(recommendation: PRDecisionRecommendation): string;
|
|
8
|
+
export declare function formatInsight(insight: PRInsight): FormattedInsight;
|
|
9
|
+
export declare function renderInsightsAsMarkdown(insights: PRInsight[], decision: PRDecision): string;
|
|
10
|
+
//# sourceMappingURL=formatInsight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatInsight.d.ts","sourceRoot":"","sources":["../src/formatInsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AASF,wBAAgB,aAAa,CAC3B,cAAc,EAAE,wBAAwB,GACvC,MAAM,CAER;AAgBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAMlE;AAkBD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,EAAE,EACrB,QAAQ,EAAE,UAAU,GACnB,MAAM,CAMR"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DECISION_TITLES = {
|
|
2
|
+
safe: "No significant dependency risks detected",
|
|
3
|
+
needs_review: "Elevated dependency merge risk",
|
|
4
|
+
risky: "Merge blocked — critical dependency risk",
|
|
5
|
+
};
|
|
6
|
+
// Title rendered once per PR comment, not per insight
|
|
7
|
+
export function decisionTitle(recommendation) {
|
|
8
|
+
return DECISION_TITLES[recommendation];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes typographic punctuation to ASCII equivalents so that LLM output
|
|
12
|
+
* renders consistently across PR comments, the web UI, and CLI output.
|
|
13
|
+
* Only targets characters that cause visual/encoding inconsistency; other
|
|
14
|
+
* characters (including XSS payloads) are passed through unchanged — HTML
|
|
15
|
+
* escaping is the renderer's responsibility.
|
|
16
|
+
*/
|
|
17
|
+
function normalizeToAscii(s) {
|
|
18
|
+
return s
|
|
19
|
+
.replace(/\u2014/g, "-") // em dash —
|
|
20
|
+
.replace(/\u2013/g, "-"); // en dash –
|
|
21
|
+
}
|
|
22
|
+
// Maps one PRInsight to its display fields (no title)
|
|
23
|
+
export function formatInsight(insight) {
|
|
24
|
+
return {
|
|
25
|
+
message: normalizeToAscii(insight.message),
|
|
26
|
+
where: normalizeToAscii(insight.context),
|
|
27
|
+
action: normalizeToAscii(insight.remediation),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Renders a single formatted insight as markdown (no title, no divider)
|
|
31
|
+
function renderInsightAsMarkdown(f) {
|
|
32
|
+
return [
|
|
33
|
+
f.message,
|
|
34
|
+
"",
|
|
35
|
+
"**Where it shows up**",
|
|
36
|
+
"",
|
|
37
|
+
f.where,
|
|
38
|
+
"",
|
|
39
|
+
"**What to do**",
|
|
40
|
+
"",
|
|
41
|
+
f.action,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
// Renders a full PR comment: one bold title + all insights separated by dividers
|
|
45
|
+
export function renderInsightsAsMarkdown(insights, decision) {
|
|
46
|
+
const title = `**${decisionTitle(decision.recommendation)}**`;
|
|
47
|
+
const blocks = insights.map((insight) => renderInsightAsMarkdown(formatInsight(insight)));
|
|
48
|
+
return [title, "", ...blocks].join("\n\n---\n\n").trimEnd();
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatInsight.test.d.ts","sourceRoot":"","sources":["../src/formatInsight.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatInsight, renderInsightsAsMarkdown } from "./formatInsight.js";
|
|
3
|
+
function makeInsight(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
type: "behavioral_change",
|
|
6
|
+
priority: "high",
|
|
7
|
+
confidence: "confirmed",
|
|
8
|
+
scope: "changed",
|
|
9
|
+
message: "default message",
|
|
10
|
+
context: "default context",
|
|
11
|
+
remediation: "default remediation",
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe("formatInsight", () => {
|
|
16
|
+
describe("typographic dash normalization", () => {
|
|
17
|
+
it("replaces em dashes with ASCII hyphens in message", () => {
|
|
18
|
+
const result = formatInsight(makeInsight({ message: "A \u2014 B" }));
|
|
19
|
+
expect(result.message).toBe("A - B");
|
|
20
|
+
});
|
|
21
|
+
it("replaces en dashes with ASCII hyphens in message", () => {
|
|
22
|
+
const result = formatInsight(makeInsight({ message: "A \u2013 B" }));
|
|
23
|
+
expect(result.message).toBe("A - B");
|
|
24
|
+
});
|
|
25
|
+
it("normalizes all three fields: message, context, remediation", () => {
|
|
26
|
+
const result = formatInsight(makeInsight({
|
|
27
|
+
message: "A \u2014 B",
|
|
28
|
+
context: "C \u2013 D",
|
|
29
|
+
remediation: "E \u2014 F",
|
|
30
|
+
}));
|
|
31
|
+
expect(result.message).toBe("A - B");
|
|
32
|
+
expect(result.where).toBe("C - D");
|
|
33
|
+
expect(result.action).toBe("E - F");
|
|
34
|
+
});
|
|
35
|
+
it("handles multiple dashes in a single field", () => {
|
|
36
|
+
const result = formatInsight(makeInsight({ message: "X \u2014 Y \u2013 Z" }));
|
|
37
|
+
expect(result.message).toBe("X - Y - Z");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("XSS payload pass-through", () => {
|
|
41
|
+
it("does not alter script tag payloads — escaping is the renderer's job", () => {
|
|
42
|
+
const payload = "<script>alert(1)</script>";
|
|
43
|
+
const result = formatInsight(makeInsight({ message: payload }));
|
|
44
|
+
expect(result.message).toBe(payload);
|
|
45
|
+
});
|
|
46
|
+
it("does not alter img onerror payloads", () => {
|
|
47
|
+
const payload = "<img src=x onerror=alert(1)>";
|
|
48
|
+
const result = formatInsight(makeInsight({ context: payload }));
|
|
49
|
+
expect(result.where).toBe(payload);
|
|
50
|
+
});
|
|
51
|
+
it("does not alter javascript: URL payloads", () => {
|
|
52
|
+
const payload = "[x](javascript:alert(1))";
|
|
53
|
+
const result = formatInsight(makeInsight({ remediation: payload }));
|
|
54
|
+
expect(result.action).toBe(payload);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("edge cases", () => {
|
|
58
|
+
it("handles empty strings in all fields without throwing", () => {
|
|
59
|
+
const result = formatInsight(makeInsight({ message: "", context: "", remediation: "" }));
|
|
60
|
+
expect(result.message).toBe("");
|
|
61
|
+
expect(result.where).toBe("");
|
|
62
|
+
expect(result.action).toBe("");
|
|
63
|
+
});
|
|
64
|
+
it("maps fields correctly: context → where, remediation → action", () => {
|
|
65
|
+
const result = formatInsight(makeInsight({ message: "msg", context: "ctx", remediation: "fix" }));
|
|
66
|
+
expect(result.message).toBe("msg");
|
|
67
|
+
expect(result.where).toBe("ctx");
|
|
68
|
+
expect(result.action).toBe("fix");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("renderInsightsAsMarkdown", () => {
|
|
73
|
+
const decision = {
|
|
74
|
+
recommendation: "needs_review",
|
|
75
|
+
confidence: "high",
|
|
76
|
+
reasoning: ["reason 1"],
|
|
77
|
+
};
|
|
78
|
+
it("does not alter or escape XSS payloads — sanitization is deferred to MarkdownContent", () => {
|
|
79
|
+
const xssPayload = "<script>alert(1)</script>";
|
|
80
|
+
const insight = makeInsight({ message: xssPayload });
|
|
81
|
+
const output = renderInsightsAsMarkdown([insight], decision);
|
|
82
|
+
expect(output).toContain(xssPayload);
|
|
83
|
+
});
|
|
84
|
+
it("includes the decision title and all insight fields", () => {
|
|
85
|
+
const insight = makeInsight({
|
|
86
|
+
message: "msg text",
|
|
87
|
+
context: "ctx text",
|
|
88
|
+
remediation: "fix text",
|
|
89
|
+
});
|
|
90
|
+
const output = renderInsightsAsMarkdown([insight], decision);
|
|
91
|
+
expect(output).toContain("Elevated dependency merge risk");
|
|
92
|
+
expect(output).toContain("msg text");
|
|
93
|
+
expect(output).toContain("ctx text");
|
|
94
|
+
expect(output).toContain("fix text");
|
|
95
|
+
});
|
|
96
|
+
it("normalizes typographic dashes in rendered markdown output", () => {
|
|
97
|
+
const insight = makeInsight({ message: "A \u2014 B" });
|
|
98
|
+
const output = renderInsightsAsMarkdown([insight], decision);
|
|
99
|
+
expect(output).toContain("A - B");
|
|
100
|
+
expect(output).not.toContain("\u2014");
|
|
101
|
+
});
|
|
102
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./formatInsight.js";
|
|
3
|
+
export * from "./riskVocabulary.js";
|
|
4
|
+
export * from "./selectTopAffectedAreas.js";
|
|
5
|
+
export * from "./deriveScanSummaryText.js";
|
|
6
|
+
export * from "./scanResultSchema.js";
|
|
7
|
+
export * from "./scanSurfaceCopy.js";
|
|
8
|
+
export * from "./trustedScanGuards.js";
|
|
9
|
+
export * from "./actionsStepSummary.js";
|
|
10
|
+
export * from "./prCheckRunPresentation.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC"}
|