@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.
Files changed (61) hide show
  1. package/dist/actionsStepSummary.d.ts +29 -0
  2. package/dist/actionsStepSummary.d.ts.map +1 -0
  3. package/dist/actionsStepSummary.js +757 -0
  4. package/dist/actionsStepSummary.test.d.ts +2 -0
  5. package/dist/actionsStepSummary.test.d.ts.map +1 -0
  6. package/dist/actionsStepSummary.test.js +348 -0
  7. package/dist/deriveScanSummaryText.d.ts +7 -0
  8. package/dist/deriveScanSummaryText.d.ts.map +1 -0
  9. package/dist/deriveScanSummaryText.js +24 -0
  10. package/dist/deriveScanSummaryText.test.d.ts +2 -0
  11. package/dist/deriveScanSummaryText.test.d.ts.map +1 -0
  12. package/dist/deriveScanSummaryText.test.js +77 -0
  13. package/dist/formatInsight.d.ts +10 -0
  14. package/dist/formatInsight.d.ts.map +1 -0
  15. package/dist/formatInsight.js +49 -0
  16. package/dist/formatInsight.test.d.ts +2 -0
  17. package/dist/formatInsight.test.d.ts.map +1 -0
  18. package/dist/formatInsight.test.js +102 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +10 -0
  22. package/dist/prCheckRunPresentation.d.ts +67 -0
  23. package/dist/prCheckRunPresentation.d.ts.map +1 -0
  24. package/dist/prCheckRunPresentation.js +314 -0
  25. package/dist/prCheckRunPresentation.test.d.ts +2 -0
  26. package/dist/prCheckRunPresentation.test.d.ts.map +1 -0
  27. package/dist/prCheckRunPresentation.test.js +277 -0
  28. package/dist/riskVocabulary.d.ts +31 -0
  29. package/dist/riskVocabulary.d.ts.map +1 -0
  30. package/dist/riskVocabulary.js +62 -0
  31. package/dist/riskVocabulary.test.d.ts +2 -0
  32. package/dist/riskVocabulary.test.d.ts.map +1 -0
  33. package/dist/riskVocabulary.test.js +14 -0
  34. package/dist/scanResultSchema.d.ts +116 -0
  35. package/dist/scanResultSchema.d.ts.map +1 -0
  36. package/dist/scanResultSchema.js +100 -0
  37. package/dist/scanResultSchema.test.d.ts +2 -0
  38. package/dist/scanResultSchema.test.d.ts.map +1 -0
  39. package/dist/scanResultSchema.test.js +105 -0
  40. package/dist/scanSurfaceCopy.d.ts +90 -0
  41. package/dist/scanSurfaceCopy.d.ts.map +1 -0
  42. package/dist/scanSurfaceCopy.js +104 -0
  43. package/dist/scanSurfaceCopy.test.d.ts +2 -0
  44. package/dist/scanSurfaceCopy.test.d.ts.map +1 -0
  45. package/dist/scanSurfaceCopy.test.js +14 -0
  46. package/dist/selectTopAffectedAreas.d.ts +17 -0
  47. package/dist/selectTopAffectedAreas.d.ts.map +1 -0
  48. package/dist/selectTopAffectedAreas.js +80 -0
  49. package/dist/selectTopAffectedAreas.test.d.ts +2 -0
  50. package/dist/selectTopAffectedAreas.test.d.ts.map +1 -0
  51. package/dist/selectTopAffectedAreas.test.js +179 -0
  52. package/dist/trustedScanGuards.d.ts +39 -0
  53. package/dist/trustedScanGuards.d.ts.map +1 -0
  54. package/dist/trustedScanGuards.js +88 -0
  55. package/dist/trustedScanGuards.test.d.ts +2 -0
  56. package/dist/trustedScanGuards.test.d.ts.map +1 -0
  57. package/dist/trustedScanGuards.test.js +143 -0
  58. package/dist/types.d.ts +362 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +12 -0
  61. package/package.json +49 -0
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
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";
@@ -0,0 +1,67 @@
1
+ import type { ScanResult } from "./types.js";
2
+ export declare const CHECK_RUN_MAX_WHY_BULLETS = 3;
3
+ export declare const CHECK_RUN_MAX_ACTION_BULLETS = 3;
4
+ export declare const CHECK_RUN_MAX_REPO_DRIVER_PHRASES = 2;
5
+ export declare const CHECK_RUN_ACTION_BULLET_MAX_CHARS = 220;
6
+ export declare const CHECK_RUN_SOFT_MAX_CHARS = 3800;
7
+ export type PrCheckRunTitleOptions = {
8
+ baselineOnly?: boolean;
9
+ };
10
+ export type PrCheckRunSummaryContext = {
11
+ result: ScanResult;
12
+ scanId: string;
13
+ webAppOrigin: string;
14
+ baseline: boolean;
15
+ };
16
+ /** Policy flags — one place per product rule. */
17
+ export type CheckRunPolicy = {
18
+ baseline: boolean;
19
+ showBaselineOutcome: boolean;
20
+ showRepoGraphContext: boolean;
21
+ showLayerScores: boolean;
22
+ useDetailsForLayers: boolean;
23
+ maxWhyBullets: number;
24
+ maxActionBullets: number;
25
+ maxRepoDriverPhrases: number;
26
+ };
27
+ export type CheckRunSection = {
28
+ kind: "lead";
29
+ posture: string;
30
+ riskIndexLine: string | null;
31
+ } | {
32
+ kind: "why";
33
+ bullets: string[];
34
+ } | {
35
+ kind: "actions";
36
+ bullets: string[];
37
+ } | {
38
+ kind: "baselineOutcome";
39
+ primary: string;
40
+ scope?: string;
41
+ } | {
42
+ kind: "repoContext";
43
+ score: number;
44
+ driverPhrases: string[];
45
+ } | {
46
+ kind: "layerScores";
47
+ rows: Array<{
48
+ label: string;
49
+ score: string;
50
+ driver: string;
51
+ }>;
52
+ } | {
53
+ kind: "footer";
54
+ url: string;
55
+ label: string;
56
+ };
57
+ export declare function formatScanDashboardUrl(webAppOrigin: string, scanId: string): string;
58
+ export declare function buildPrCheckRunTitle(opts?: PrCheckRunTitleOptions): string;
59
+ /** Strong drivers for optional repo-context line (full mode only). */
60
+ export declare function hasStrongRepoGraphDrivers(result: ScanResult): boolean;
61
+ export declare function deriveCheckRunPolicy(result: ScanResult, ctx: Pick<PrCheckRunSummaryContext, "baseline">): CheckRunPolicy;
62
+ export declare function buildPrCheckRunSections(policy: CheckRunPolicy, result: ScanResult, ctx: PrCheckRunSummaryContext): CheckRunSection[];
63
+ export declare function renderCheckRunMarkdown(sections: CheckRunSection[]): string;
64
+ export declare function buildPrCheckRunSummaryMarkdown(ctx: PrCheckRunSummaryContext): string;
65
+ /** Whether baseline scan has no actionable PR bullets (for tests / callers). */
66
+ export declare function checkRunHasActionableContent(result: ScanResult): boolean;
67
+ //# sourceMappingURL=prCheckRunPresentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prCheckRunPresentation.d.ts","sourceRoot":"","sources":["../src/prCheckRunPresentation.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAC3C,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C,eAAO,MAAM,iCAAiC,IAAI,CAAC;AACnD,eAAO,MAAM,iCAAiC,MAAM,CAAC;AACrD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAqB7C,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,iDAAiD;AACjD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAUnD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED,wBAAgB,oBAAoB,CAClC,IAAI,GAAE,sBAA2B,GAChC,MAAM,CAMR;AA4BD,sEAAsE;AACtE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAGrE;AAkGD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,IAAI,CAAC,wBAAwB,EAAE,UAAU,CAAC,GAC9C,cAAc,CA0BhB;AAqBD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,wBAAwB,GAC5B,eAAe,EAAE,CAmDnB;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CA0E1E;AAED,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,wBAAwB,GAC5B,MAAM,CAIR;AAED,gFAAgF;AAChF,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAExE"}
@@ -0,0 +1,314 @@
1
+ /**
2
+ * GitHub Check Run markdown — policy → sections → dumb renderer.
3
+ * Thin projection of ScanResult for PR decision support (not a presentation framework).
4
+ */
5
+ import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
6
+ import { formatInsight } from "./formatInsight.js";
7
+ import { humanizeEngineSurfaceText, layerDriverSummary, sortPRInsightsForDisplay, sortRecommendationsForDisplay, truncateWithEllipsis, } from "./actionsStepSummary.js";
8
+ import { mergePostureLabel } from "./riskVocabulary.js";
9
+ import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
10
+ // --- Caps (change only with tests) ---
11
+ export const CHECK_RUN_MAX_WHY_BULLETS = 3;
12
+ export const CHECK_RUN_MAX_ACTION_BULLETS = 3;
13
+ export const CHECK_RUN_MAX_REPO_DRIVER_PHRASES = 2;
14
+ export const CHECK_RUN_ACTION_BULLET_MAX_CHARS = 220;
15
+ export const CHECK_RUN_SOFT_MAX_CHARS = 3800;
16
+ const LAYER_LABELS = [
17
+ ["security", "Security"],
18
+ ["maintainability", "Maintainability"],
19
+ ["ecosystem", "Ecosystem"],
20
+ ["upgradeImpact", "Upgrade Impact"],
21
+ ];
22
+ const DEFAULT_SECTION_ORDER = [
23
+ "lead",
24
+ "why",
25
+ "actions",
26
+ "baselineOutcome",
27
+ "repoContext",
28
+ "layerScores",
29
+ "footer",
30
+ ];
31
+ function roundScore(n) {
32
+ return Math.round(n);
33
+ }
34
+ function normalizeOrigin(origin) {
35
+ return origin.replace(/\/+$/, "");
36
+ }
37
+ export function formatScanDashboardUrl(webAppOrigin, scanId) {
38
+ return `${normalizeOrigin(webAppOrigin)}/scan/${scanId}`;
39
+ }
40
+ export function buildPrCheckRunTitle(opts = {}) {
41
+ const base = scanSurfaceCopy.checkRun.titleBase;
42
+ if (opts.baselineOnly) {
43
+ return `${base} - ${scanSurfaceCopy.checkRun.titleBaselineSuffix}`;
44
+ }
45
+ return base;
46
+ }
47
+ function collectWhyBullets(result, max) {
48
+ const out = [];
49
+ const reasoning = result.decision?.reasoning;
50
+ if (Array.isArray(reasoning)) {
51
+ for (const r of reasoning) {
52
+ const s = humanizeEngineSurfaceText(String(r).trim());
53
+ if (s && !out.includes(s))
54
+ out.push(s);
55
+ if (out.length >= max)
56
+ return out;
57
+ }
58
+ }
59
+ const reasons = result.explain?.reasons;
60
+ if (Array.isArray(reasons) && out.length < max) {
61
+ for (const r of reasons) {
62
+ const title = String(r?.title ?? r?.id ?? "").trim();
63
+ const readable = humanizeEngineSurfaceText(title);
64
+ if (readable && !out.includes(readable))
65
+ out.push(readable);
66
+ if (out.length >= max)
67
+ break;
68
+ }
69
+ }
70
+ if (out.length === 0) {
71
+ const one = deriveScanSummaryText(result);
72
+ if (one)
73
+ out.push(one);
74
+ }
75
+ return out.slice(0, max);
76
+ }
77
+ /** Strong drivers for optional repo-context line (full mode only). */
78
+ export function hasStrongRepoGraphDrivers(result) {
79
+ const phrases = collectWhyBullets(result, CHECK_RUN_MAX_REPO_DRIVER_PHRASES);
80
+ return phrases.some((p) => p.trim().length >= 12);
81
+ }
82
+ function hasActionableBullets(result) {
83
+ const insights = Array.isArray(result.insights) ? result.insights : [];
84
+ const recs = Array.isArray(result.recommendations)
85
+ ? result.recommendations
86
+ : [];
87
+ const findings = Array.isArray(result.findings) ? result.findings : [];
88
+ return insights.length > 0 || recs.length > 0 || findings.length > 0;
89
+ }
90
+ function insightToBullet(insight) {
91
+ const f = formatInsight(insight);
92
+ const msg = truncateWithEllipsis(f.message, CHECK_RUN_ACTION_BULLET_MAX_CHARS);
93
+ const action = f.action.trim();
94
+ if (action && action.length < 120) {
95
+ return `${msg} — ${truncateWithEllipsis(action, 100)}`;
96
+ }
97
+ return msg;
98
+ }
99
+ function recommendationToBullet(rec) {
100
+ const title = String(rec.title ?? "").trim();
101
+ const rat = String(rec.rationale ?? "").trim();
102
+ const pkgs = Array.isArray(rec.packages) && rec.packages.length > 0
103
+ ? ` (${rec.packages.slice(0, 3).join(", ")}${rec.packages.length > 3 ? ", …" : ""})`
104
+ : "";
105
+ if (rat && (!title || rat.length > title.length)) {
106
+ return truncateWithEllipsis(rat, CHECK_RUN_ACTION_BULLET_MAX_CHARS) + pkgs;
107
+ }
108
+ const line = title || "Review dependency guidance";
109
+ return truncateWithEllipsis(line, CHECK_RUN_ACTION_BULLET_MAX_CHARS) + pkgs;
110
+ }
111
+ function findingToBullet(f) {
112
+ const title = String(f.title ?? "").trim();
113
+ const pkg = f.packageName ? ` (\`${f.packageName}\`)` : "";
114
+ return (truncateWithEllipsis(title || f.description, CHECK_RUN_ACTION_BULLET_MAX_CHARS) + pkg);
115
+ }
116
+ function buildActionBullets(result, max) {
117
+ const out = [];
118
+ const sortedI = sortPRInsightsForDisplay(Array.isArray(result.insights) ? result.insights : []);
119
+ const sortedR = sortRecommendationsForDisplay(Array.isArray(result.recommendations) ? result.recommendations : []);
120
+ const findings = Array.isArray(result.findings) ? result.findings : [];
121
+ for (const ins of sortedI) {
122
+ if (out.length >= max)
123
+ break;
124
+ out.push(insightToBullet(ins));
125
+ }
126
+ for (const rec of sortedR) {
127
+ if (out.length >= max)
128
+ break;
129
+ out.push(recommendationToBullet(rec));
130
+ }
131
+ for (const f of findings) {
132
+ if (out.length >= max)
133
+ break;
134
+ if (f.severity === "high" || f.severity === "critical") {
135
+ out.push(findingToBullet(f));
136
+ }
137
+ }
138
+ return out.slice(0, max);
139
+ }
140
+ function layerScoreRows(result) {
141
+ const layers = result.layerScores ?? {};
142
+ const rows = [];
143
+ for (const [key, label] of LAYER_LABELS) {
144
+ const v = layers[key];
145
+ const n = typeof v === "number" && Number.isFinite(v) ? v : null;
146
+ if (n === null)
147
+ continue;
148
+ const drivers = layerDriverSummary(result, key, 120);
149
+ if (!drivers)
150
+ continue;
151
+ rows.push({
152
+ label,
153
+ score: String(roundScore(n)),
154
+ driver: drivers,
155
+ });
156
+ }
157
+ return rows;
158
+ }
159
+ export function deriveCheckRunPolicy(result, ctx) {
160
+ const baseline = ctx.baseline;
161
+ const actionBullets = buildActionBullets(result, CHECK_RUN_MAX_ACTION_BULLETS);
162
+ const layerRows = layerScoreRows(result);
163
+ const showRepoGraphContext = !baseline &&
164
+ hasStrongRepoGraphDrivers(result) &&
165
+ typeof result.totalScore === "number" &&
166
+ Number.isFinite(result.totalScore);
167
+ const showLayerScores = !baseline && layerRows.length > 0;
168
+ return {
169
+ baseline,
170
+ showBaselineOutcome: baseline && actionBullets.length === 0,
171
+ showRepoGraphContext,
172
+ showLayerScores,
173
+ useDetailsForLayers: showLayerScores && layerRows.length >= 2,
174
+ maxWhyBullets: CHECK_RUN_MAX_WHY_BULLETS,
175
+ maxActionBullets: CHECK_RUN_MAX_ACTION_BULLETS,
176
+ maxRepoDriverPhrases: CHECK_RUN_MAX_REPO_DRIVER_PHRASES,
177
+ };
178
+ }
179
+ function buildLeadSection(result, policy) {
180
+ const posture = result.decision?.recommendation
181
+ ? mergePostureLabel(result.decision.recommendation)
182
+ : scanSurfaceCopy.checkRun.mergePostureUnavailable;
183
+ const total = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
184
+ ? result.totalScore
185
+ : null;
186
+ const showRiskIndexInLead = !policy.baseline && !policy.showRepoGraphContext && total !== null;
187
+ const riskIndexLine = showRiskIndexInLead
188
+ ? `${scanSurfaceCopy.checkRun.repoContextLabel} ${roundScore(total)}/100 (${scanSurfaceCopy.product.riskIndexDirectionShort})`
189
+ : null;
190
+ return { kind: "lead", posture, riskIndexLine };
191
+ }
192
+ export function buildPrCheckRunSections(policy, result, ctx) {
193
+ const sections = {};
194
+ sections.lead = buildLeadSection(result, policy);
195
+ const why = collectWhyBullets(result, policy.maxWhyBullets);
196
+ if (why.length > 0) {
197
+ sections.why = { kind: "why", bullets: why };
198
+ }
199
+ const actions = buildActionBullets(result, policy.maxActionBullets);
200
+ if (actions.length > 0) {
201
+ sections.actions = { kind: "actions", bullets: actions };
202
+ }
203
+ if (policy.showBaselineOutcome) {
204
+ sections.baselineOutcome = {
205
+ kind: "baselineOutcome",
206
+ primary: scanSurfaceCopy.checkRun.baselineOutcomePrimary,
207
+ scope: scanSurfaceCopy.checkRun.baselineOutcomeScope,
208
+ };
209
+ }
210
+ if (policy.showRepoGraphContext) {
211
+ const score = roundScore(result.totalScore);
212
+ sections.repoContext = {
213
+ kind: "repoContext",
214
+ score,
215
+ driverPhrases: collectWhyBullets(result, policy.maxRepoDriverPhrases),
216
+ };
217
+ }
218
+ if (policy.showLayerScores) {
219
+ const rows = layerScoreRows(result);
220
+ if (rows.length > 0) {
221
+ sections.layerScores = { kind: "layerScores", rows };
222
+ }
223
+ }
224
+ sections.footer = {
225
+ kind: "footer",
226
+ url: formatScanDashboardUrl(ctx.webAppOrigin, ctx.scanId),
227
+ label: scanSurfaceCopy.checkRun.footerLinkLabel,
228
+ };
229
+ const out = [];
230
+ for (const key of DEFAULT_SECTION_ORDER) {
231
+ const s = sections[key];
232
+ if (s)
233
+ out.push(s);
234
+ }
235
+ return out;
236
+ }
237
+ export function renderCheckRunMarkdown(sections) {
238
+ const parts = [];
239
+ for (const section of sections) {
240
+ switch (section.kind) {
241
+ case "lead": {
242
+ parts.push(`**${section.posture}**`);
243
+ if (section.riskIndexLine) {
244
+ parts.push(section.riskIndexLine);
245
+ }
246
+ parts.push("");
247
+ break;
248
+ }
249
+ case "why": {
250
+ for (const b of section.bullets) {
251
+ parts.push(`- ${b}`);
252
+ }
253
+ parts.push("");
254
+ break;
255
+ }
256
+ case "actions": {
257
+ for (const b of section.bullets) {
258
+ parts.push(`- ${b}`);
259
+ }
260
+ parts.push("");
261
+ break;
262
+ }
263
+ case "baselineOutcome": {
264
+ parts.push(section.primary);
265
+ if (section.scope) {
266
+ parts.push("");
267
+ parts.push(section.scope);
268
+ }
269
+ parts.push("");
270
+ break;
271
+ }
272
+ case "repoContext": {
273
+ const drivers = section.driverPhrases.join(" · ");
274
+ parts.push(`${scanSurfaceCopy.checkRun.repoContextLabel} ${section.score}/100 — ${drivers}`);
275
+ parts.push("");
276
+ break;
277
+ }
278
+ case "layerScores": {
279
+ parts.push("<details>");
280
+ parts.push(`<summary>${scanSurfaceCopy.checkRun.layerScoresDetailsSummary}</summary>`);
281
+ parts.push("");
282
+ for (const row of section.rows) {
283
+ parts.push(`- **${row.label}** ${row.score}/100 — ${row.driver}`);
284
+ }
285
+ parts.push("");
286
+ parts.push("</details>");
287
+ parts.push("");
288
+ break;
289
+ }
290
+ case "footer": {
291
+ parts.push(`[${section.label}](${section.url})`);
292
+ break;
293
+ }
294
+ default: {
295
+ const _exhaustive = section;
296
+ void _exhaustive;
297
+ }
298
+ }
299
+ }
300
+ let md = parts.join("\n").trimEnd();
301
+ if (md.length > CHECK_RUN_SOFT_MAX_CHARS) {
302
+ md = md.slice(0, CHECK_RUN_SOFT_MAX_CHARS - 1) + "…";
303
+ }
304
+ return md;
305
+ }
306
+ export function buildPrCheckRunSummaryMarkdown(ctx) {
307
+ const policy = deriveCheckRunPolicy(ctx.result, { baseline: ctx.baseline });
308
+ const sections = buildPrCheckRunSections(policy, ctx.result, ctx);
309
+ return renderCheckRunMarkdown(sections);
310
+ }
311
+ /** Whether baseline scan has no actionable PR bullets (for tests / callers). */
312
+ export function checkRunHasActionableContent(result) {
313
+ return hasActionableBullets(result);
314
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prCheckRunPresentation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prCheckRunPresentation.test.d.ts","sourceRoot":"","sources":["../src/prCheckRunPresentation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,277 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CHECK_RUN_MAX_ACTION_BULLETS, CHECK_RUN_SOFT_MAX_CHARS, buildPrCheckRunSummaryMarkdown, buildPrCheckRunTitle, deriveCheckRunPolicy, formatScanDashboardUrl, hasStrongRepoGraphDrivers, } from "./prCheckRunPresentation.js";
3
+ const ORIGIN = "https://mergesignal-web.fly.dev";
4
+ const SCAN_ID = "11111111-1111-4111-8111-111111111111";
5
+ function minimalResult(overrides = {}) {
6
+ return {
7
+ totalScore: 42,
8
+ layerScores: {
9
+ security: 10,
10
+ maintainability: 20,
11
+ ecosystem: 30,
12
+ upgradeImpact: 15,
13
+ },
14
+ findings: [],
15
+ generatedAt: new Date().toISOString(),
16
+ decision: {
17
+ recommendation: "needs_review",
18
+ confidence: "medium",
19
+ reasoning: ["Lockfile adds transitive depth on shared utilities."],
20
+ },
21
+ explain: {
22
+ reasons: [
23
+ {
24
+ id: "graph.depth",
25
+ layer: "ecosystem",
26
+ title: "graph.depth",
27
+ scoreImpact: 12,
28
+ },
29
+ ],
30
+ },
31
+ ...overrides,
32
+ };
33
+ }
34
+ describe("buildPrCheckRunTitle", () => {
35
+ it("uses hyphenated product title", () => {
36
+ expect(buildPrCheckRunTitle()).toBe("MergeSignal scan - PR dependency change");
37
+ expect(buildPrCheckRunTitle()).not.toContain("—");
38
+ });
39
+ it("appends baseline suffix when requested", () => {
40
+ expect(buildPrCheckRunTitle({ baselineOnly: true })).toBe("MergeSignal scan - PR dependency change - baseline scan only");
41
+ });
42
+ });
43
+ describe("formatScanDashboardUrl", () => {
44
+ it("strips trailing slash from origin", () => {
45
+ expect(formatScanDashboardUrl(`${ORIGIN}/`, SCAN_ID)).toBe(`${ORIGIN}/scan/${SCAN_ID}`);
46
+ });
47
+ });
48
+ describe("deriveCheckRunPolicy", () => {
49
+ it("never shows repo graph context in baseline", () => {
50
+ const rich = minimalResult({
51
+ totalScore: 68,
52
+ explain: {
53
+ reasons: [
54
+ {
55
+ id: "graph.transitive",
56
+ layer: "ecosystem",
57
+ title: "Large transitive dependency volume",
58
+ scoreImpact: 20,
59
+ },
60
+ ],
61
+ },
62
+ });
63
+ const policy = deriveCheckRunPolicy(rich, { baseline: true });
64
+ expect(policy.showRepoGraphContext).toBe(false);
65
+ expect(policy.baseline).toBe(true);
66
+ });
67
+ it("shows repo graph context in full only with strong drivers", () => {
68
+ const weak = minimalResult({
69
+ explain: { reasons: [] },
70
+ decision: {
71
+ recommendation: "safe",
72
+ confidence: "high",
73
+ reasoning: [],
74
+ },
75
+ });
76
+ expect(hasStrongRepoGraphDrivers(weak)).toBe(false);
77
+ expect(deriveCheckRunPolicy(weak, { baseline: false }).showRepoGraphContext).toBe(false);
78
+ const strong = minimalResult({
79
+ explain: {
80
+ reasons: [
81
+ {
82
+ id: "graph.depth",
83
+ layer: "ecosystem",
84
+ title: "Indirect dependency depth exceeds typical baseline",
85
+ scoreImpact: 15,
86
+ },
87
+ ],
88
+ },
89
+ });
90
+ expect(hasStrongRepoGraphDrivers(strong)).toBe(true);
91
+ expect(deriveCheckRunPolicy(strong, { baseline: false }).showRepoGraphContext).toBe(true);
92
+ });
93
+ });
94
+ describe("buildPrCheckRunSummaryMarkdown", () => {
95
+ it("always includes dashboard footer link", () => {
96
+ const md = buildPrCheckRunSummaryMarkdown({
97
+ result: minimalResult(),
98
+ scanId: SCAN_ID,
99
+ webAppOrigin: ORIGIN,
100
+ baseline: false,
101
+ });
102
+ expect(md).toContain(`[View full scan](${ORIGIN}/scan/${SCAN_ID})`);
103
+ });
104
+ it("baseline body omits repo graph score lines", () => {
105
+ const md = buildPrCheckRunSummaryMarkdown({
106
+ result: minimalResult({ totalScore: 68 }),
107
+ scanId: SCAN_ID,
108
+ webAppOrigin: ORIGIN,
109
+ baseline: true,
110
+ });
111
+ expect(md).not.toMatch(/68\/100/);
112
+ expect(md).not.toMatch(/moderate risk/i);
113
+ expect(md).not.toMatch(/Repository graph/i);
114
+ });
115
+ it("full mode omits unexplained score when drivers are weak", () => {
116
+ const md = buildPrCheckRunSummaryMarkdown({
117
+ result: minimalResult({
118
+ totalScore: 68,
119
+ explain: { reasons: [] },
120
+ decision: { recommendation: "safe", confidence: "high", reasoning: [] },
121
+ }),
122
+ scanId: SCAN_ID,
123
+ webAppOrigin: ORIGIN,
124
+ baseline: false,
125
+ });
126
+ expect(md).not.toMatch(/68\/100 —/);
127
+ });
128
+ it("shows baseline outcome when baseline and no actionable bullets", () => {
129
+ const md = buildPrCheckRunSummaryMarkdown({
130
+ result: minimalResult({
131
+ insights: [],
132
+ recommendations: [],
133
+ findings: [],
134
+ decision: {
135
+ recommendation: "safe",
136
+ confidence: "high",
137
+ reasoning: [],
138
+ },
139
+ explain: { reasons: [] },
140
+ }),
141
+ scanId: SCAN_ID,
142
+ webAppOrigin: ORIGIN,
143
+ baseline: true,
144
+ });
145
+ expect(md).toContain("No actionable dependency concerns showed up for this PR");
146
+ expect(md).not.toMatch(/high-confidence/i);
147
+ expect(md).not.toMatch(/did not emit/i);
148
+ });
149
+ it("does not include standalone changed packages list", () => {
150
+ const md = buildPrCheckRunSummaryMarkdown({
151
+ result: minimalResult({
152
+ insights: [
153
+ {
154
+ type: "usage_risk",
155
+ priority: "high",
156
+ confidence: "likely",
157
+ scope: "changed",
158
+ message: "Runtime path may load updated semver helpers.",
159
+ context: "src/version.ts",
160
+ remediation: "Run integration tests for version parsing.",
161
+ },
162
+ ],
163
+ }),
164
+ scanId: SCAN_ID,
165
+ webAppOrigin: ORIGIN,
166
+ baseline: false,
167
+ });
168
+ expect(md).not.toMatch(/^Changed packages:/m);
169
+ expect(md).not.toMatch(/^@types\/semver/m);
170
+ });
171
+ it("caps action bullets and stays under soft char limit for fat ScanResult", () => {
172
+ const fat = {
173
+ ...minimalResult(),
174
+ totalScore: 72,
175
+ graphInsights: {
176
+ maxDepth: 14,
177
+ nodes: 2400,
178
+ edges: 9000,
179
+ deepest: Array.from({ length: 20 }, (_, i) => ({
180
+ kind: "deep",
181
+ packageName: `pkg-${i}`,
182
+ depth: 10 + i,
183
+ direct: false,
184
+ via: ["a", "b", "c"],
185
+ })),
186
+ hotspots: Array.from({ length: 12 }, (_, i) => ({
187
+ kind: "hotspot",
188
+ packageName: `hot-${i}`,
189
+ depth: 3,
190
+ direct: false,
191
+ })),
192
+ vulnerable: Array.from({ length: 8 }, (_, i) => ({
193
+ kind: "vulnerable",
194
+ packageName: `vuln-${i}`,
195
+ depth: 2,
196
+ direct: false,
197
+ })),
198
+ },
199
+ insights: Array.from({ length: 30 }, (_, i) => ({
200
+ type: "behavioral_change",
201
+ priority: "high",
202
+ confidence: "confirmed",
203
+ scope: "changed",
204
+ message: `Insight message ${i} with extra detail about runtime behavior and dependency coupling.`,
205
+ context: `src/module-${i}/index.ts`,
206
+ remediation: `Remediation step ${i} including verify, test, and document.`,
207
+ })),
208
+ recommendations: Array.from({ length: 25 }, (_, i) => ({
209
+ id: `rec-${i}`,
210
+ title: `Recommendation title ${i}`,
211
+ rationale: `Long rationale ${i} explaining ecosystem overlap and transitive churn in multiple sentences.`,
212
+ impact: "high",
213
+ packages: [`pkg-a-${i}`, `pkg-b-${i}`],
214
+ })),
215
+ explain: {
216
+ reasons: Array.from({ length: 40 }, (_, i) => ({
217
+ id: `graph.metric.${i}`,
218
+ layer: (["security", "maintainability", "ecosystem", "upgradeImpact"][i % 4] ?? "ecosystem"),
219
+ title: `graph.transitive volume cluster ${i}`,
220
+ scoreImpact: 5 + i,
221
+ })),
222
+ },
223
+ };
224
+ const md = buildPrCheckRunSummaryMarkdown({
225
+ result: fat,
226
+ scanId: SCAN_ID,
227
+ webAppOrigin: ORIGIN,
228
+ baseline: false,
229
+ });
230
+ expect(md).toContain("Insight message 0");
231
+ expect(md).not.toContain("Insight message 3");
232
+ const insightBullets = md
233
+ .split("\n")
234
+ .filter((l) => l.includes("Insight message"));
235
+ expect(insightBullets.length).toBeLessThanOrEqual(CHECK_RUN_MAX_ACTION_BULLETS);
236
+ expect(md.length).toBeLessThanOrEqual(CHECK_RUN_SOFT_MAX_CHARS);
237
+ expect(md).not.toContain("pkg-0 is reached transitively");
238
+ expect(md).not.toContain("topology");
239
+ expect(md).not.toMatch(/moderate risk/i);
240
+ });
241
+ it("pairs layer scores with drivers in details when shown", () => {
242
+ const md = buildPrCheckRunSummaryMarkdown({
243
+ result: minimalResult({
244
+ layerScores: {
245
+ security: 0,
246
+ maintainability: 45,
247
+ ecosystem: 50,
248
+ upgradeImpact: 10,
249
+ },
250
+ explain: {
251
+ reasons: [
252
+ {
253
+ id: "maint.band",
254
+ layer: "maintainability",
255
+ title: "Stale releases on multiple direct dependencies",
256
+ scoreImpact: 18,
257
+ },
258
+ ],
259
+ },
260
+ contributions: [
261
+ {
262
+ id: "maint-stale",
263
+ layer: "maintainability",
264
+ scoreImpact: 10,
265
+ },
266
+ ],
267
+ }),
268
+ scanId: SCAN_ID,
269
+ webAppOrigin: ORIGIN,
270
+ baseline: false,
271
+ });
272
+ if (md.includes("Layer scores")) {
273
+ expect(md).toMatch(/Maintainability.*\/100 —/);
274
+ expect(md).not.toMatch(/\| Security \| 0 \|/);
275
+ }
276
+ });
277
+ });