@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.
- package/dist/actionsStepSummary.d.ts +1 -22
- package/dist/actionsStepSummary.d.ts.map +1 -1
- package/dist/actionsStepSummary.js +19 -716
- package/dist/actionsStepSummary.test.js +34 -329
- package/dist/assessmentLabels.d.ts +6 -0
- package/dist/assessmentLabels.d.ts.map +1 -0
- package/dist/assessmentLabels.js +33 -0
- package/dist/assessmentProjection.d.ts +12 -0
- package/dist/assessmentProjection.d.ts.map +1 -0
- package/dist/assessmentProjection.js +100 -0
- package/dist/assessmentSchema.d.ts +155 -0
- package/dist/assessmentSchema.d.ts.map +1 -0
- package/dist/assessmentSchema.js +110 -0
- package/dist/cardObservationCatalog.d.ts +2 -0
- package/dist/cardObservationCatalog.d.ts.map +1 -1
- package/dist/cardObservationCatalog.js +47 -1
- package/dist/engineWireRegression.test.js +22 -2
- package/dist/fixtures/assessmentFixtures.d.ts +13 -0
- package/dist/fixtures/assessmentFixtures.d.ts.map +1 -0
- package/dist/fixtures/assessmentFixtures.js +164 -0
- package/dist/formatCardExposureDisplay.d.ts +0 -4
- package/dist/formatCardExposureDisplay.d.ts.map +1 -1
- package/dist/formatCardExposureDisplay.js +0 -15
- package/dist/formatInsight.d.ts +3 -2
- package/dist/formatInsight.d.ts.map +1 -1
- package/dist/formatInsight.js +26 -1
- package/dist/index.d.ts +7 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -11
- package/dist/narrativeParity.test.js +4 -34
- package/dist/presentGitHubPrComment.d.ts +1 -3
- package/dist/presentGitHubPrComment.d.ts.map +1 -1
- package/dist/presentGitHubPrComment.js +0 -17
- package/dist/presentation/buildGitHubCheckRunOutput.d.ts +20 -0
- package/dist/presentation/buildGitHubCheckRunOutput.d.ts.map +1 -0
- package/dist/presentation/buildGitHubCheckRunOutput.js +42 -0
- package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts +2 -0
- package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts.map +1 -0
- package/dist/presentation/buildGitHubCheckRunOutput.test.js +188 -0
- package/dist/presentation/compose/narrativeChannels.d.ts.map +1 -1
- package/dist/presentation/compose/narrativeChannels.js +13 -43
- package/dist/presentation/compose/narrativeCompose.d.ts.map +1 -1
- package/dist/presentation/compose/narrativeCompose.js +2 -5
- package/dist/presentation/dto/assessmentPresentationFields.d.ts +13 -0
- package/dist/presentation/dto/assessmentPresentationFields.d.ts.map +1 -0
- package/dist/presentation/dto/assessmentPresentationFields.js +1 -0
- package/dist/presentation/dto/dashboardCardPresentation.d.ts +2 -1
- package/dist/presentation/dto/dashboardCardPresentation.d.ts.map +1 -1
- package/dist/presentation/dto/githubAndCliPresentation.d.ts +4 -3
- package/dist/presentation/dto/githubAndCliPresentation.d.ts.map +1 -1
- package/dist/presentation/dto/scanDetailsPresentation.d.ts +2 -1
- package/dist/presentation/dto/scanDetailsPresentation.d.ts.map +1 -1
- package/dist/presentation/fixtures/dashboardCardGolden.test.js +21 -14
- package/dist/presentation/fixtures/presentationPersonaFixtures.d.ts.map +1 -1
- package/dist/presentation/fixtures/presentationPersonaFixtures.js +31 -24
- package/dist/presentation/fixtures/scanResultFixtures.d.ts.map +1 -1
- package/dist/presentation/fixtures/scanResultFixtures.js +14 -3
- package/dist/presentation/index.d.ts +2 -2
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +2 -2
- package/dist/presentation/orchestration/buildScanCardPresentation.d.ts.map +1 -1
- package/dist/presentation/orchestration/buildScanCardPresentation.js +8 -3
- package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts +1 -1
- package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts.map +1 -1
- package/dist/presentation/orchestration/buildScanPresentationBundle.js +12 -10
- package/dist/presentation/orchestration/scanPresentationBundle.d.ts +8 -3
- package/dist/presentation/orchestration/scanPresentationBundle.d.ts.map +1 -1
- package/dist/presentation/presentationPersonas.test.js +6 -6
- package/dist/presentation/presenters/presentCliScanSummary.d.ts.map +1 -1
- package/dist/presentation/presenters/presentCliScanSummary.js +2 -0
- package/dist/presentation/presenters/presentDashboardCard.d.ts +1 -0
- package/dist/presentation/presenters/presentDashboardCard.d.ts.map +1 -1
- package/dist/presentation/presenters/presentDashboardCard.js +18 -0
- package/dist/presentation/presenters/presentGitHubCheckRun.d.ts.map +1 -1
- package/dist/presentation/presenters/presentGitHubCheckRun.js +2 -0
- package/dist/presentation/presenters/presentGitHubPrComment.d.ts.map +1 -1
- package/dist/presentation/presenters/presentGitHubPrComment.js +15 -21
- package/dist/presentation/presenters/presentScanDetails.d.ts.map +1 -1
- package/dist/presentation/presenters/presentScanDetails.js +52 -129
- package/dist/presentation/profile/buildProfileFromAssessment.d.ts +5 -0
- package/dist/presentation/profile/buildProfileFromAssessment.d.ts.map +1 -0
- package/dist/presentation/profile/buildProfileFromAssessment.js +58 -0
- package/dist/presentation/projectAssessmentFields.d.ts +4 -0
- package/dist/presentation/projectAssessmentFields.d.ts.map +1 -0
- package/dist/presentation/projectAssessmentFields.js +13 -0
- package/dist/presentation/surfaceParity.test.d.ts +2 -0
- package/dist/presentation/surfaceParity.test.d.ts.map +1 -0
- package/dist/presentation/surfaceParity.test.js +74 -0
- package/dist/productMessaging.d.ts +4 -4
- package/dist/productMessaging.d.ts.map +1 -1
- package/dist/productMessaging.js +4 -4
- package/dist/productMessaging.test.js +2 -2
- package/dist/repoIntelligenceSchema.d.ts +2 -2
- package/dist/resolvePipelineStatus.d.ts +15 -0
- package/dist/resolvePipelineStatus.d.ts.map +1 -0
- package/dist/resolvePipelineStatus.js +37 -0
- package/dist/scanCardPresentationState.d.ts +10 -0
- package/dist/scanCardPresentationState.d.ts.map +1 -0
- package/dist/scanCardPresentationState.js +4 -0
- package/dist/{scanDetailViewModel.d.ts → scanDetailViewModelTypes.d.ts} +60 -32
- package/dist/scanDetailViewModelTypes.d.ts.map +1 -0
- package/dist/scanDetailViewModelTypes.js +4 -0
- package/dist/scanResultSchema.d.ts +75 -2
- package/dist/scanResultSchema.d.ts.map +1 -1
- package/dist/scanResultSchema.js +4 -2
- package/dist/scanResultSchema.test.js +16 -0
- package/dist/scanSurfaceCopy.d.ts +2 -0
- package/dist/scanSurfaceCopy.d.ts.map +1 -1
- package/dist/scanSurfaceCopy.js +2 -0
- package/dist/trustedScanGuards.test.js +7 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/collectNarrativeWhyBullets.d.ts +0 -12
- package/dist/collectNarrativeWhyBullets.d.ts.map +0 -1
- package/dist/collectNarrativeWhyBullets.js +0 -104
- package/dist/deriveCardDisplaySummary.d.ts +0 -16
- package/dist/deriveCardDisplaySummary.d.ts.map +0 -1
- package/dist/deriveCardDisplaySummary.js +0 -56
- package/dist/deriveCardDisplaySummary.test.d.ts +0 -2
- package/dist/deriveCardDisplaySummary.test.d.ts.map +0 -1
- package/dist/deriveCardDisplaySummary.test.js +0 -89
- package/dist/deriveCardOperationalObservations.d.ts +0 -19
- package/dist/deriveCardOperationalObservations.d.ts.map +0 -1
- package/dist/deriveCardOperationalObservations.js +0 -180
- package/dist/deriveCardOperationalObservations.test.d.ts +0 -2
- package/dist/deriveCardOperationalObservations.test.d.ts.map +0 -1
- package/dist/deriveCardOperationalObservations.test.js +0 -268
- package/dist/deriveRiskScoreSummary.d.ts +0 -33
- package/dist/deriveRiskScoreSummary.d.ts.map +0 -1
- package/dist/deriveRiskScoreSummary.js +0 -90
- package/dist/deriveRiskScoreSummary.test.d.ts +0 -2
- package/dist/deriveRiskScoreSummary.test.d.ts.map +0 -1
- package/dist/deriveRiskScoreSummary.test.js +0 -47
- package/dist/deriveScanDetailRecommendations.d.ts +0 -63
- package/dist/deriveScanDetailRecommendations.d.ts.map +0 -1
- package/dist/deriveScanDetailRecommendations.js +0 -725
- package/dist/deriveScanDetailRecommendations.test.d.ts +0 -2
- package/dist/deriveScanDetailRecommendations.test.d.ts.map +0 -1
- package/dist/deriveScanDetailRecommendations.test.js +0 -181
- package/dist/deriveScanSummaryText.d.ts +0 -7
- package/dist/deriveScanSummaryText.d.ts.map +0 -1
- package/dist/deriveScanSummaryText.js +0 -24
- package/dist/deriveScanSummaryText.test.d.ts +0 -2
- package/dist/deriveScanSummaryText.test.d.ts.map +0 -1
- package/dist/deriveScanSummaryText.test.js +0 -77
- package/dist/formatCardEvidenceCounts.d.ts +0 -7
- package/dist/formatCardEvidenceCounts.d.ts.map +0 -1
- package/dist/formatCardEvidenceCounts.js +0 -19
- package/dist/prCheckRunPresentation.d.ts +0 -68
- package/dist/prCheckRunPresentation.d.ts.map +0 -1
- package/dist/prCheckRunPresentation.js +0 -319
- package/dist/prCheckRunPresentation.test.d.ts +0 -2
- package/dist/prCheckRunPresentation.test.d.ts.map +0 -1
- package/dist/prCheckRunPresentation.test.js +0 -278
- package/dist/presentation/intent/derivePresentationInterpretation.d.ts +0 -4
- package/dist/presentation/intent/derivePresentationInterpretation.d.ts.map +0 -1
- package/dist/presentation/intent/derivePresentationInterpretation.js +0 -202
- package/dist/presentation/intent/formatPresentationCopy.d.ts +0 -8
- package/dist/presentation/intent/formatPresentationCopy.d.ts.map +0 -1
- package/dist/presentation/intent/formatPresentationCopy.js +0 -140
- package/dist/presentation/intent/presentationIntent.test.d.ts +0 -2
- package/dist/presentation/intent/presentationIntent.test.d.ts.map +0 -1
- package/dist/presentation/intent/presentationIntent.test.js +0 -29
- package/dist/presentation/intent/verificationFocusLabels.d.ts +0 -2
- package/dist/presentation/intent/verificationFocusLabels.d.ts.map +0 -1
- package/dist/presentation/intent/verificationFocusLabels.js +0 -45
- package/dist/presentation/profile/derivePresentationProfile.d.ts +0 -12
- package/dist/presentation/profile/derivePresentationProfile.d.ts.map +0 -1
- package/dist/presentation/profile/derivePresentationProfile.js +0 -85
- package/dist/scanCardSummary.d.ts +0 -58
- package/dist/scanCardSummary.d.ts.map +0 -1
- package/dist/scanCardSummary.js +0 -146
- package/dist/scanCardSummary.test.d.ts +0 -2
- package/dist/scanCardSummary.test.d.ts.map +0 -1
- package/dist/scanCardSummary.test.js +0 -234
- package/dist/scanDetailViewModel.d.ts.map +0 -1
- package/dist/scanDetailViewModel.js +0 -636
- package/dist/scanDetailViewModel.test.d.ts +0 -2
- package/dist/scanDetailViewModel.test.d.ts.map +0 -1
- package/dist/scanDetailViewModel.test.js +0 -530
|
@@ -1,730 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GitHub Actions `GITHUB_STEP_SUMMARY` markdown
|
|
3
|
-
*
|
|
4
|
-
* Inventory (legacy `render-mergesignal-step-summary.mjs` → sections):
|
|
5
|
-
* - Demo `#` + banner → `buildDemoHeader`
|
|
6
|
-
* - Trusted `# MergeSignal — … Risk index` → `buildTrustedTitleLines`
|
|
7
|
-
* - `**High/Medium/Low risk**` → folded into title + “why” bullets (posture-first)
|
|
8
|
-
* - Methodology line → `buildMethodologyLine`
|
|
9
|
-
* - `## Recommended actions` + time heuristics → default “what to do” + `<details> More actions`
|
|
10
|
-
* - `<details> Risk breakdown` + layer table + graph one-liner → score + graph `<details>` blocks
|
|
2
|
+
* GitHub Actions `GITHUB_STEP_SUMMARY` markdown — projection of presentation bundle.
|
|
11
3
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
4
|
+
import { buildScanPresentationBundle } from "./presentation/orchestration/buildScanPresentationBundle.js";
|
|
5
|
+
import { presentCliScanSummary } from "./presentation/presenters/presentCliScanSummary.js";
|
|
6
|
+
import { renderCliScanSummaryText } from "./presentation/render/renderCliScanSummaryText.js";
|
|
15
7
|
import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
|
|
16
|
-
import { isStubMethodologyVersion } from "./trustedScanGuards.js";
|
|
17
|
-
/** Default fold: max lines excluding trailing blank (tune with plan §3.0). */
|
|
18
|
-
export const ACTIONS_SUMMARY_DEFAULT_MAX_LINES = 18;
|
|
19
|
-
/** Default fold: max characters (plan §3.0). */
|
|
20
|
-
export const ACTIONS_SUMMARY_DEFAULT_MAX_CHARS = 1200;
|
|
21
|
-
/** Full markdown soft cap (character count, includes `<details>` bodies). */
|
|
22
|
-
export const ACTIONS_SUMMARY_SOFT_MAX_CHARS = 6200;
|
|
23
|
-
/** Truncated recommendation `rationale` in default fold. */
|
|
24
|
-
export const ACTIONS_REC_RATIONALE_MAX_CHARS = 130;
|
|
25
|
-
const LAYER_ORDER = [
|
|
26
|
-
["security", "Security"],
|
|
27
|
-
["maintainability", "Maintainability"],
|
|
28
|
-
["ecosystem", "Ecosystem"],
|
|
29
|
-
["upgradeImpact", "Upgrade Impact"],
|
|
30
|
-
];
|
|
31
|
-
/** Titles that read like generic hygiene advice — prefer rationale-first layout. */
|
|
32
|
-
const GENERIC_RECOMMENDATION_TITLE = /\b(reduce|flatten|minimize|minimise|avoid|improve|consider|optimize|optimise|review|upgrade|decrease|increase|check|verify|ensure|audit)\b.*\b(depend|transitiv|surface|depth|chain|area|footprint|overlap|duplicat|lockfile|semver|vulnerabil|supply)/i;
|
|
33
8
|
function copyLine(flat, key, fallback) {
|
|
34
9
|
const v = flat[key];
|
|
35
10
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
36
11
|
}
|
|
37
|
-
export function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (score < 40)
|
|
43
|
-
return "Moderate";
|
|
44
|
-
return "High";
|
|
45
|
-
}
|
|
46
|
-
function shouldShowLayerDrivers(score) {
|
|
47
|
-
const band = layerRiskBandLabel(score);
|
|
48
|
-
return band === "High" || band === "Moderate";
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Presentation-only: turn engine metric tokens in explain titles / contribution ids
|
|
52
|
-
* into reviewer-oriented phrasing. Does not add facts — only rewrites known patterns.
|
|
53
|
-
*/
|
|
54
|
-
export function humanizeEngineSurfaceText(raw) {
|
|
55
|
-
const input = raw.trim();
|
|
56
|
-
if (!input)
|
|
57
|
-
return input;
|
|
58
|
-
let t = input.replace(/_/g, " ");
|
|
59
|
-
t = t.replace(/^(security|maintainability|ecosystem|upgradeimpact|upgrade\s+impact)[.:]\s*/i, "");
|
|
60
|
-
const subs = [
|
|
61
|
-
[
|
|
62
|
-
/ecosystem\.package\s*surface|ecosystem\s+package\s*surface|package\s+surface\b/gi,
|
|
63
|
-
"broad declared package footprint",
|
|
64
|
-
],
|
|
65
|
-
[
|
|
66
|
-
/graph\.duplicates?\b|graph\s+duplicates?\b/gi,
|
|
67
|
-
"overlapping dependency paths to the same packages",
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
/graph\.fan\s*in\s*max|graph\s+fan\s+in\s+max/gi,
|
|
71
|
-
"many import paths converge on a few widely shared packages",
|
|
72
|
-
],
|
|
73
|
-
[
|
|
74
|
-
/graph\.fan\s*in|graph\s+fan\s+in/gi,
|
|
75
|
-
"shared packages see many inbound dependency paths",
|
|
76
|
-
],
|
|
77
|
-
[
|
|
78
|
-
/graph\.transitive(?:\s+volume)?|graph\s+transitive(?:\s+volume)?/gi,
|
|
79
|
-
"transitive dependency volume",
|
|
80
|
-
],
|
|
81
|
-
[/graph\.depth|graph\s+depth/gi, "indirect dependency depth"],
|
|
82
|
-
[/graph\.hotspot|graph\s+hotspot/gi, "dependency hotspots"],
|
|
83
|
-
[/graph\.hidden|graph\s+hidden/gi, "hidden transitive paths"],
|
|
84
|
-
[
|
|
85
|
-
/graph\.vulnerable|graph\s+vulnerable/gi,
|
|
86
|
-
"known vulnerable packages on the graph",
|
|
87
|
-
],
|
|
88
|
-
[/graph\.fan|graph\s+fan/gi, "fan-in"],
|
|
89
|
-
];
|
|
90
|
-
for (const [re, rep] of subs)
|
|
91
|
-
t = t.replace(re, rep);
|
|
92
|
-
t = t
|
|
93
|
-
.replace(/\bdependency\s+chain\s+depth\b/gi, "indirect dependency depth")
|
|
94
|
-
.replace(/^dependency\s+depth$/gi, "indirect dependency depth");
|
|
95
|
-
t = t.replace(/\s+/g, " ").trim();
|
|
96
|
-
if (!t)
|
|
97
|
-
return input;
|
|
98
|
-
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
99
|
-
}
|
|
100
|
-
function explainTitlesForLayer(result, layer, limit) {
|
|
101
|
-
const reasons = result.explain?.reasons;
|
|
102
|
-
if (!Array.isArray(reasons))
|
|
103
|
-
return [];
|
|
104
|
-
return [...reasons]
|
|
105
|
-
.filter((r) => Boolean(r) && r.layer === layer)
|
|
106
|
-
.sort((a, b) => Math.abs(Number(b.scoreImpact) || 0) -
|
|
107
|
-
Math.abs(Number(a.scoreImpact) || 0))
|
|
108
|
-
.slice(0, limit)
|
|
109
|
-
.map((r) => humanizeEngineSurfaceText(String(r.title ?? r.id ?? "").trim()))
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
}
|
|
112
|
-
function humanizeContributionId(id) {
|
|
113
|
-
return humanizeEngineSurfaceText(id.replace(/[-_]+/g, " ").trim());
|
|
114
|
-
}
|
|
115
|
-
function contributionHintsForLayer(result, layer, limit) {
|
|
116
|
-
const list = result.contributions;
|
|
117
|
-
if (!Array.isArray(list))
|
|
118
|
-
return [];
|
|
119
|
-
return [...list]
|
|
120
|
-
.filter((c) => Boolean(c) && c.layer === layer)
|
|
121
|
-
.sort((a, b) => Math.abs(Number(b.scoreImpact) || 0) -
|
|
122
|
-
Math.abs(Number(a.scoreImpact) || 0))
|
|
123
|
-
.slice(0, limit)
|
|
124
|
-
.map((c) => humanizeContributionId(String(c.id ?? "")).trim())
|
|
125
|
-
.filter(Boolean);
|
|
126
|
-
}
|
|
127
|
-
function driverPhraseDedupeKey(phrase) {
|
|
128
|
-
const n = phrase.toLowerCase().replace(/\s+/g, " ").trim();
|
|
129
|
-
if (n.includes("indirect dependency depth") ||
|
|
130
|
-
n.includes("dependency chain depth") ||
|
|
131
|
-
n === "dependency depth") {
|
|
132
|
-
return "__depth__";
|
|
133
|
-
}
|
|
134
|
-
if (n.includes("converge on") ||
|
|
135
|
-
n.includes("inbound dependency paths") ||
|
|
136
|
-
n.includes("widely shared packages")) {
|
|
137
|
-
return "__shared_paths__";
|
|
138
|
-
}
|
|
139
|
-
if (n.includes("transitive dependency volume"))
|
|
140
|
-
return "__transitive_vol__";
|
|
141
|
-
return n;
|
|
142
|
-
}
|
|
143
|
-
function dedupeDriverDisplayPhrases(phrases) {
|
|
144
|
-
const seen = new Set();
|
|
145
|
-
const out = [];
|
|
146
|
-
for (const p of phrases) {
|
|
147
|
-
const key = driverPhraseDedupeKey(p);
|
|
148
|
-
if (seen.has(key))
|
|
149
|
-
continue;
|
|
150
|
-
seen.add(key);
|
|
151
|
-
out.push(p);
|
|
152
|
-
}
|
|
153
|
-
return out;
|
|
154
|
-
}
|
|
155
|
-
/** Plain-language drivers for one layer from engine `explain` / `contributions` only (no invented causes). */
|
|
156
|
-
export function layerDriverSummary(result, layer, maxChars) {
|
|
157
|
-
const fromExplain = explainTitlesForLayer(result, layer, 5);
|
|
158
|
-
const fromContrib = contributionHintsForLayer(result, layer, 3);
|
|
159
|
-
const parts = [];
|
|
160
|
-
for (const t of fromExplain) {
|
|
161
|
-
if (!parts.includes(t))
|
|
162
|
-
parts.push(t);
|
|
163
|
-
}
|
|
164
|
-
for (const c of fromContrib) {
|
|
165
|
-
const exists = parts.some((p) => p.toLowerCase().includes(c.toLowerCase().slice(0, 12)));
|
|
166
|
-
if (!exists)
|
|
167
|
-
parts.push(c);
|
|
168
|
-
if (parts.length >= 5)
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
if (parts.length === 0)
|
|
172
|
-
return null;
|
|
173
|
-
const deduped = dedupeDriverDisplayPhrases(parts);
|
|
174
|
-
return truncateWithEllipsis(deduped.join(" · "), maxChars);
|
|
175
|
-
}
|
|
176
|
-
function impactOrder(impact) {
|
|
177
|
-
const i = (impact ?? "low").toLowerCase();
|
|
178
|
-
if (i === "high")
|
|
179
|
-
return 3;
|
|
180
|
-
if (i === "medium")
|
|
181
|
-
return 2;
|
|
182
|
-
return 1;
|
|
183
|
-
}
|
|
184
|
-
export function sortPRInsightsForDisplay(insights) {
|
|
185
|
-
const prio = (p) => p === "critical" ? 3 : p === "high" ? 2 : 1;
|
|
186
|
-
const conf = (c) => c === "confirmed" ? 3 : c === "likely" ? 2 : 1;
|
|
187
|
-
const scope = (s) => (s === "changed" ? 2 : 1);
|
|
188
|
-
return [...insights].sort((a, b) => {
|
|
189
|
-
const d = prio(b.priority) - prio(a.priority);
|
|
190
|
-
if (d !== 0)
|
|
191
|
-
return d;
|
|
192
|
-
const d2 = conf(b.confidence) - conf(a.confidence);
|
|
193
|
-
if (d2 !== 0)
|
|
194
|
-
return d2;
|
|
195
|
-
return scope(b.scope) - scope(a.scope);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
export function sortRecommendationsForDisplay(recs) {
|
|
199
|
-
return [...recs].sort((a, b) => {
|
|
200
|
-
const pa = a.priorityScore ?? NaN;
|
|
201
|
-
const pb = b.priorityScore ?? NaN;
|
|
202
|
-
const fa = Number.isFinite(pa);
|
|
203
|
-
const fb = Number.isFinite(pb);
|
|
204
|
-
if (fa && fb && pb !== pa)
|
|
205
|
-
return pb - pa;
|
|
206
|
-
if (fa !== fb)
|
|
207
|
-
return fa ? -1 : 1;
|
|
208
|
-
const ia = impactOrder(a.impact);
|
|
209
|
-
const ib = impactOrder(b.impact);
|
|
210
|
-
if (ib !== ia)
|
|
211
|
-
return ib - ia;
|
|
212
|
-
const ra = a.rank ?? 9999;
|
|
213
|
-
const rb = b.rank ?? 9999;
|
|
214
|
-
return ra - rb;
|
|
12
|
+
export function buildActionsStepSummaryMarkdown(opts) {
|
|
13
|
+
const { result, profile, copy: flat } = opts;
|
|
14
|
+
const bundle = buildScanPresentationBundle({
|
|
15
|
+
result,
|
|
16
|
+
pipelineStatus: "done",
|
|
215
17
|
});
|
|
216
|
-
}
|
|
217
|
-
export function truncateWithEllipsis(s, maxChars) {
|
|
218
|
-
const t = s.trim();
|
|
219
|
-
if (t.length <= maxChars)
|
|
220
|
-
return t;
|
|
221
|
-
if (maxChars <= 1)
|
|
222
|
-
return "…";
|
|
223
|
-
return `${t.slice(0, maxChars - 1)}…`;
|
|
224
|
-
}
|
|
225
|
-
function scoreBandLabelFromTotal(score) {
|
|
226
|
-
if (score === null)
|
|
227
|
-
return "Unknown risk";
|
|
228
|
-
if (score >= 70)
|
|
229
|
-
return "High risk";
|
|
230
|
-
if (score >= 40)
|
|
231
|
-
return "Medium risk";
|
|
232
|
-
return "Low risk";
|
|
233
|
-
}
|
|
234
|
-
function roundScore(n) {
|
|
235
|
-
return Math.round(n);
|
|
236
|
-
}
|
|
237
|
-
export function actionsSummaryDefaultFoldPrefix(markdown) {
|
|
238
|
-
const idx = markdown.indexOf("<details>");
|
|
239
|
-
return (idx === -1 ? markdown : markdown.slice(0, idx)).trimEnd();
|
|
240
|
-
}
|
|
241
|
-
function countLines(s) {
|
|
242
|
-
if (!s)
|
|
243
|
-
return 0;
|
|
244
|
-
return s.split("\n").length;
|
|
245
|
-
}
|
|
246
|
-
function enforceDefaultFoldBudget(text) {
|
|
247
|
-
let out = text.trimEnd();
|
|
248
|
-
for (let i = 0; i < 40; i++) {
|
|
249
|
-
const lines = countLines(out);
|
|
250
|
-
const chars = out.length;
|
|
251
|
-
if (lines <= ACTIONS_SUMMARY_DEFAULT_MAX_LINES &&
|
|
252
|
-
chars <= ACTIONS_SUMMARY_DEFAULT_MAX_CHARS) {
|
|
253
|
-
return out;
|
|
254
|
-
}
|
|
255
|
-
// Drop from end: remove last non-empty line until within budget (last resort trimmer).
|
|
256
|
-
const parts = out.split("\n");
|
|
257
|
-
if (parts.length <= 3)
|
|
258
|
-
break;
|
|
259
|
-
parts.pop();
|
|
260
|
-
out = parts.join("\n").trimEnd();
|
|
261
|
-
}
|
|
262
|
-
return out.slice(0, ACTIONS_SUMMARY_DEFAULT_MAX_CHARS).trimEnd();
|
|
263
|
-
}
|
|
264
|
-
function buildDemoHeader(flat, lines) {
|
|
265
|
-
const demoTitle = copyLine(flat, "actions.demoSummaryTitle", scanSurfaceCopy.actions.demoSummaryTitle);
|
|
266
|
-
const demoBanner = copyLine(flat, "actions.demoSummaryBanner", scanSurfaceCopy.actions.demoSummaryBanner);
|
|
267
|
-
lines.push(`# ${demoTitle}`);
|
|
268
|
-
lines.push("");
|
|
269
|
-
lines.push(`> **${demoBanner}**`);
|
|
270
|
-
lines.push("");
|
|
271
|
-
}
|
|
272
|
-
function buildMethodologyLine(flat, methodology, trustedLayout, isStub, lines) {
|
|
273
|
-
if (!methodology)
|
|
274
|
-
return;
|
|
275
|
-
const methLabel = trustedLayout && !isStub
|
|
276
|
-
? copyLine(flat, "actions.trustedSummaryMethodologyLine", scanSurfaceCopy.actions.trustedSummaryMethodologyLine)
|
|
277
|
-
: "Methodology";
|
|
278
|
-
lines.push(`**${methLabel}:** ${methodology}`);
|
|
279
|
-
lines.push("");
|
|
280
|
-
}
|
|
281
|
-
function qualitativeGraphSentences(g) {
|
|
282
|
-
const out = [];
|
|
283
|
-
const depth = typeof g.maxDepth === "number" && Number.isFinite(g.maxDepth)
|
|
284
|
-
? g.maxDepth
|
|
285
|
-
: null;
|
|
286
|
-
const nodes = typeof g.nodes === "number" && Number.isFinite(g.nodes) ? g.nodes : null;
|
|
287
|
-
const hotspotCount = Array.isArray(g.hotspots) ? g.hotspots.length : 0;
|
|
288
|
-
if (depth !== null && depth >= 8) {
|
|
289
|
-
out.push("The deepest resolved chain is long, so indirect impact from upgrades and security patches is harder to reason about.");
|
|
290
|
-
}
|
|
291
|
-
else if (depth !== null && depth >= 5) {
|
|
292
|
-
out.push("Dependency chains run moderately deep—expect meaningful transitive distance from your direct dependencies.");
|
|
293
|
-
}
|
|
294
|
-
if (out.length < 2 && nodes !== null && nodes >= 1200) {
|
|
295
|
-
out.push("This is a large resolved install: more packages usually widens the review surface for churn and advisories.");
|
|
296
|
-
}
|
|
297
|
-
else if (out.length < 2 && nodes !== null && nodes >= 600) {
|
|
298
|
-
out.push("The install graph is fairly broad—watch where new indirect dependencies cluster.");
|
|
299
|
-
}
|
|
300
|
-
if (out.length < 2 && hotspotCount >= 4) {
|
|
301
|
-
out.push("Several packages sit on many import paths—upgrades touching them deserve a quick spot-check.");
|
|
302
|
-
}
|
|
303
|
-
return out.slice(0, 2);
|
|
304
|
-
}
|
|
305
|
-
function formatGraphNarrativeLines(gi) {
|
|
306
|
-
const out = [];
|
|
307
|
-
if (!gi)
|
|
308
|
-
return out;
|
|
309
|
-
const deepest = Array.isArray(gi.deepest) ? gi.deepest : [];
|
|
310
|
-
for (const d of deepest.slice(0, 2)) {
|
|
311
|
-
const name = String(d?.packageName ?? "package");
|
|
312
|
-
const depth = typeof d?.depth === "number" && Number.isFinite(d.depth) ? d.depth : null;
|
|
313
|
-
const direct = Boolean(d?.direct);
|
|
314
|
-
const via = Array.isArray(d?.via) ? d.via.join(" → ") : "";
|
|
315
|
-
const depthPhrase = depth === null ? "deep in the graph" : `${depth} levels deep`;
|
|
316
|
-
out.push(`- **${name}** is reached ${direct ? "directly" : "transitively"} (${depthPhrase})${via ? ` via ${via}` : ""}.`);
|
|
317
|
-
}
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
function graphDetailsHasContent(gi) {
|
|
321
|
-
if (!gi || typeof gi !== "object")
|
|
322
|
-
return false;
|
|
323
|
-
const g = gi;
|
|
324
|
-
const nodes = g.nodes;
|
|
325
|
-
const depth = g.maxDepth;
|
|
326
|
-
const edges = g.edges;
|
|
327
|
-
const deepest = g.deepest;
|
|
328
|
-
const hotspots = g.hotspots;
|
|
329
|
-
const vulnerable = g.vulnerable;
|
|
330
|
-
return ((typeof nodes === "number" && Number.isFinite(nodes)) ||
|
|
331
|
-
(typeof depth === "number" && Number.isFinite(depth)) ||
|
|
332
|
-
(typeof edges === "number" && Number.isFinite(edges)) ||
|
|
333
|
-
(Array.isArray(deepest) && deepest.length > 0) ||
|
|
334
|
-
(Array.isArray(hotspots) && hotspots.length > 0) ||
|
|
335
|
-
(Array.isArray(vulnerable) && vulnerable.length > 0));
|
|
336
|
-
}
|
|
337
|
-
/** Raw counts: terse when qualitative or deepest-path narrative already carries meaning. */
|
|
338
|
-
function buildGraphRawMetricsLine(g, terse) {
|
|
339
|
-
const nodes = g.nodes;
|
|
340
|
-
const depth = g.maxDepth;
|
|
341
|
-
const edges = g.edges;
|
|
342
|
-
const hotspotCount = Array.isArray(g.hotspots) ? g.hotspots.length : 0;
|
|
343
|
-
const vulnCount = Array.isArray(g.vulnerable) ? g.vulnerable.length : 0;
|
|
344
|
-
if (terse) {
|
|
345
|
-
const bits = [];
|
|
346
|
-
if (typeof depth === "number" && Number.isFinite(depth)) {
|
|
347
|
-
bits.push(`max chain length ${String(depth)}`);
|
|
348
|
-
}
|
|
349
|
-
if (typeof nodes === "number" && Number.isFinite(nodes)) {
|
|
350
|
-
bits.push(`${String(nodes)} resolved packages`);
|
|
351
|
-
}
|
|
352
|
-
if (vulnCount > 0) {
|
|
353
|
-
bits.push(`${String(vulnCount)} with listed advisories`);
|
|
354
|
-
}
|
|
355
|
-
if (bits.length > 0)
|
|
356
|
-
return `> *${bits.join(" · ")}*`;
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
const nStr = typeof nodes === "number" && Number.isFinite(nodes) ? String(nodes) : "—";
|
|
360
|
-
const dStr = typeof depth === "number" && Number.isFinite(depth) ? String(depth) : "—";
|
|
361
|
-
const eStr = typeof edges === "number" && Number.isFinite(edges) ? String(edges) : "—";
|
|
362
|
-
return `> *${nStr} packages · max depth ${dStr} · ${eStr} edges${hotspotCount > 0 ? ` · ${hotspotCount} hotspot${hotspotCount > 1 ? "s" : ""}` : ""}${vulnCount > 0 ? ` · ${vulnCount} flagged vulnerable` : ""}*`;
|
|
363
|
-
}
|
|
364
|
-
function buildScoreBreakdownDetails(result, flat, opts) {
|
|
365
|
-
const layers = result.layerScores ?? {};
|
|
366
18
|
const lines = [];
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
lines.push(copyLine(flat, "actions.layerScoreGlossary", scanSurfaceCopy.actions.layerScoreGlossary));
|
|
372
|
-
lines.push("");
|
|
373
|
-
}
|
|
374
|
-
lines.push("| Layer | Score | Layer risk |");
|
|
375
|
-
lines.push("|-------|-------|------------|");
|
|
376
|
-
for (const [key, label] of LAYER_ORDER) {
|
|
377
|
-
const v = layers[key];
|
|
378
|
-
const n = typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
379
|
-
const display = n === null ? "—" : String(roundScore(n));
|
|
380
|
-
const band = layerRiskBandLabel(n);
|
|
381
|
-
lines.push(`| ${label} | ${display} | ${band} |`);
|
|
382
|
-
}
|
|
383
|
-
lines.push("");
|
|
384
|
-
const driverLines = [];
|
|
385
|
-
for (const [key, label] of LAYER_ORDER) {
|
|
386
|
-
const v = layers[key];
|
|
387
|
-
const n = typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
388
|
-
if (!shouldShowLayerDrivers(n))
|
|
389
|
-
continue;
|
|
390
|
-
const drivers = layerDriverSummary(result, key, 200);
|
|
391
|
-
if (drivers)
|
|
392
|
-
driverLines.push(`- **${label}:** ${drivers}`);
|
|
19
|
+
if (profile !== "trusted") {
|
|
20
|
+
const title = copyLine(flat, "actions.demoSummaryTitle", scanSurfaceCopy.actions.demoSummaryTitle);
|
|
21
|
+
const banner = copyLine(flat, "actions.demoSummaryBanner", scanSurfaceCopy.actions.demoSummaryBanner);
|
|
22
|
+
lines.push(`# ${title}`, "", banner, "");
|
|
393
23
|
}
|
|
394
|
-
if (
|
|
395
|
-
lines.push(
|
|
396
|
-
lines.
|
|
397
|
-
lines.push("");
|
|
24
|
+
if (!bundle) {
|
|
25
|
+
lines.push("## Analysis could not be completed", "");
|
|
26
|
+
return lines.join("\n");
|
|
398
27
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
function buildGraphDetails(result, flat) {
|
|
403
|
-
const gi = result.graphInsights;
|
|
404
|
-
if (!graphDetailsHasContent(gi))
|
|
405
|
-
return null;
|
|
406
|
-
const g = (gi ?? {});
|
|
407
|
-
const lines = [];
|
|
408
|
-
lines.push("<details>");
|
|
409
|
-
lines.push(`<summary>${copyLine(flat, "actions.dependencyGraphDetailsSummary", scanSurfaceCopy.actions.dependencyGraphDetailsSummary)}</summary>`);
|
|
410
|
-
lines.push("");
|
|
411
|
-
const qualitative = qualitativeGraphSentences(g);
|
|
412
|
-
if (qualitative.length > 0) {
|
|
413
|
-
lines.push(...qualitative);
|
|
414
|
-
lines.push("");
|
|
415
|
-
}
|
|
416
|
-
const narrative = formatGraphNarrativeLines(g);
|
|
417
|
-
if (narrative.length > 0) {
|
|
418
|
-
lines.push(...narrative);
|
|
419
|
-
lines.push("");
|
|
420
|
-
}
|
|
421
|
-
const note = copyLine(flat, "actions.supportingGraphContextNote", scanSurfaceCopy.actions.supportingGraphContextNote);
|
|
422
|
-
lines.push(`*${note}*`);
|
|
423
|
-
lines.push("");
|
|
424
|
-
const hasStory = qualitative.length > 0 || narrative.length > 0;
|
|
425
|
-
let rawLine = buildGraphRawMetricsLine(g, hasStory);
|
|
426
|
-
if (!rawLine)
|
|
427
|
-
rawLine = buildGraphRawMetricsLine(g, false);
|
|
428
|
-
if (rawLine)
|
|
429
|
-
lines.push(rawLine);
|
|
430
|
-
const vulnCount = Array.isArray(g.vulnerable) ? g.vulnerable.length : 0;
|
|
431
|
-
if (vulnCount > 0) {
|
|
432
|
-
lines.push("");
|
|
433
|
-
lines.push(copyLine(flat, "actions.vulnerableReviewerHint", scanSurfaceCopy.actions.vulnerableReviewerHint));
|
|
434
|
-
}
|
|
435
|
-
lines.push("");
|
|
436
|
-
lines.push("</details>");
|
|
437
|
-
return lines.join("\n");
|
|
438
|
-
}
|
|
439
|
-
function renderInsightDetailsBlock(insight) {
|
|
440
|
-
const f = formatInsight(insight);
|
|
441
|
-
const bits = [
|
|
442
|
-
`**${insight.priority}** · ${insight.type.replace(/_/g, " ")}`,
|
|
443
|
-
"",
|
|
444
|
-
f.message,
|
|
445
|
-
];
|
|
446
|
-
if (f.where) {
|
|
447
|
-
bits.push("", "**Where it shows up**", "", f.where);
|
|
448
|
-
}
|
|
449
|
-
if (f.action) {
|
|
450
|
-
bits.push("", "**What to do**", "", f.action);
|
|
451
|
-
}
|
|
452
|
-
return bits.join("\n");
|
|
453
|
-
}
|
|
454
|
-
function buildMoreInsightsDetails(insights, flat) {
|
|
455
|
-
if (insights.length === 0)
|
|
456
|
-
return null;
|
|
457
|
-
const lines = [];
|
|
458
|
-
lines.push("<details>");
|
|
459
|
-
lines.push(`<summary>${copyLine(flat, "actions.moreInsightsDetailsSummary", scanSurfaceCopy.actions.moreInsightsDetailsSummary)} (${insights.length})</summary>`);
|
|
460
|
-
lines.push("");
|
|
461
|
-
const blocks = insights.map((ins) => renderInsightDetailsBlock(ins));
|
|
462
|
-
lines.push(blocks.join("\n\n---\n\n"));
|
|
463
|
-
lines.push("");
|
|
464
|
-
lines.push("</details>");
|
|
465
|
-
return lines.join("\n");
|
|
466
|
-
}
|
|
467
|
-
function buildMoreActionsDetails(recs, flat) {
|
|
468
|
-
if (recs.length === 0)
|
|
469
|
-
return null;
|
|
470
|
-
const lines = [];
|
|
471
|
-
lines.push("<details>");
|
|
472
|
-
lines.push(`<summary>${copyLine(flat, "actions.moreActionsDetailsSummary", scanSurfaceCopy.actions.moreActionsDetailsSummary)} (${recs.length})</summary>`);
|
|
473
|
-
lines.push("");
|
|
474
|
-
recs.forEach((rec, i) => {
|
|
475
|
-
const bodyLines = formatRecommendationPrimaryLines(rec, ACTIONS_REC_RATIONALE_MAX_CHARS);
|
|
476
|
-
const pkgs = Array.isArray(rec.packages) && rec.packages.length > 0
|
|
477
|
-
? `\`${rec.packages.slice(0, 8).join("`, `")}\`${rec.packages.length > 8 ? ` +${rec.packages.length - 8} more` : ""}`
|
|
478
|
-
: "";
|
|
479
|
-
bodyLines.forEach((line, j) => {
|
|
480
|
-
if (j === 0) {
|
|
481
|
-
lines.push(`${i + 1}. ${line}`);
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
lines.push(line);
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
if (pkgs) {
|
|
488
|
-
lines.push("");
|
|
489
|
-
lines.push(`Packages: ${pkgs}`);
|
|
490
|
-
}
|
|
491
|
-
lines.push("");
|
|
28
|
+
const cli = presentCliScanSummary(bundle, {
|
|
29
|
+
repoLabel: copyLine(flat, "actions.repoLabel", "repository"),
|
|
492
30
|
});
|
|
493
|
-
lines.push(
|
|
31
|
+
lines.push(renderCliScanSummaryText(cli));
|
|
494
32
|
return lines.join("\n");
|
|
495
33
|
}
|
|
496
|
-
function buildTrustedTitleLines(result, flat) {
|
|
497
|
-
const total = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
|
|
498
|
-
? result.totalScore
|
|
499
|
-
: null;
|
|
500
|
-
const direction = copyLine(flat, "actions.riskIndexDirectionShort", scanSurfaceCopy.actions.riskIndexDirectionShort);
|
|
501
|
-
const decision = result.decision;
|
|
502
|
-
const postureToken = decision?.recommendation;
|
|
503
|
-
const postureLabel = postureToken
|
|
504
|
-
? mergePostureLabel(postureToken)
|
|
505
|
-
: copyLine(flat, "actions.mergePostureUnavailableShort", scanSurfaceCopy.actions.mergePostureUnavailableShort);
|
|
506
|
-
const indexPart = total === null
|
|
507
|
-
? "Risk index unavailable"
|
|
508
|
-
: `Risk index ${roundScore(total)}/100`;
|
|
509
|
-
const titleLines = [];
|
|
510
|
-
const productPrefix = copyLine(flat, "actions.trustedStepSummaryTitlePrefix", scanSurfaceCopy.actions.trustedStepSummaryTitlePrefix);
|
|
511
|
-
titleLines.push(`# ${productPrefix} — ${postureLabel} · ${indexPart} (${direction})`);
|
|
512
|
-
if (!postureToken && total !== null) {
|
|
513
|
-
titleLines.push("");
|
|
514
|
-
titleLines.push(`*${copyLine(flat, "actions.mergePostureUnavailableDetail", scanSurfaceCopy.actions.mergePostureUnavailableDetail)}*`);
|
|
515
|
-
}
|
|
516
|
-
return titleLines;
|
|
517
|
-
}
|
|
518
|
-
function collectWhyBullets(result, max) {
|
|
519
|
-
return dedupeDriverDisplayPhrases(collectWhyBulletsFromResult(result, max));
|
|
520
|
-
}
|
|
521
|
-
function buildWhatToDoItems(result) {
|
|
522
|
-
const sortedI = sortPRInsightsForDisplay(Array.isArray(result.insights) ? result.insights : []);
|
|
523
|
-
const sortedR = sortRecommendationsForDisplay(Array.isArray(result.recommendations) ? result.recommendations : []);
|
|
524
|
-
const defaultItems = [];
|
|
525
|
-
for (const ins of sortedI) {
|
|
526
|
-
if (defaultItems.length >= 3)
|
|
527
|
-
break;
|
|
528
|
-
defaultItems.push({ kind: "insight", insight: ins });
|
|
529
|
-
}
|
|
530
|
-
for (const rec of sortedR) {
|
|
531
|
-
if (defaultItems.length >= 3)
|
|
532
|
-
break;
|
|
533
|
-
defaultItems.push({ kind: "rec", rec });
|
|
534
|
-
}
|
|
535
|
-
const insightUsed = defaultItems.filter((x) => x.kind === "insight").length;
|
|
536
|
-
const recUsed = defaultItems.filter((x) => x.kind === "rec").length;
|
|
537
|
-
const overflowInsights = sortedI.slice(insightUsed);
|
|
538
|
-
const overflowRecs = sortedR.slice(recUsed);
|
|
539
|
-
return { defaultItems, overflowInsights, overflowRecs };
|
|
540
|
-
}
|
|
541
|
-
function recommendationLeadsWithRationale(rec) {
|
|
542
|
-
const title = String(rec.title ?? "Review dependencies").trim() || "Review";
|
|
543
|
-
const rat = (rec.rationale ?? "").trim();
|
|
544
|
-
if (!rat)
|
|
545
|
-
return false;
|
|
546
|
-
return (GENERIC_RECOMMENDATION_TITLE.test(title) ||
|
|
547
|
-
(rat.length > title.length * 1.15 && title.length < 52));
|
|
548
|
-
}
|
|
549
|
-
function formatRecommendationPrimaryLines(rec, rationaleMax, opts) {
|
|
550
|
-
const title = String(rec.title ?? "Review dependencies").trim() || "Review";
|
|
551
|
-
const rat = (rec.rationale ?? "").trim();
|
|
552
|
-
if (!rat)
|
|
553
|
-
return [`**${title}**`];
|
|
554
|
-
const leadWithRationale = GENERIC_RECOMMENDATION_TITLE.test(title) ||
|
|
555
|
-
(rat.length > title.length * 1.15 && title.length < 52);
|
|
556
|
-
if (leadWithRationale) {
|
|
557
|
-
const ratBudget = Math.min(220, rationaleMax + 85);
|
|
558
|
-
let ratTrunc = truncateWithEllipsis(rat, ratBudget);
|
|
559
|
-
if (opts?.copy && opts?.recReviewPrefix) {
|
|
560
|
-
const prefix = copyLine(opts.copy, "actions.recReviewLeadPrefix", scanSurfaceCopy.actions.recReviewLeadPrefix).trim();
|
|
561
|
-
ratTrunc = truncateWithEllipsis(rat, Math.max(48, ratBudget - prefix.length - 2));
|
|
562
|
-
ratTrunc = `${prefix} ${ratTrunc}`.trim();
|
|
563
|
-
}
|
|
564
|
-
return [ratTrunc, ` → *Focus:* **${title}**`];
|
|
565
|
-
}
|
|
566
|
-
return [`**${title}** — ${truncateWithEllipsis(rat, rationaleMax)}`];
|
|
567
|
-
}
|
|
568
|
-
function formatDefaultWhatLine(item, index, opts) {
|
|
569
|
-
const linesOut = [];
|
|
570
|
-
if (item.kind === "insight") {
|
|
571
|
-
const f = formatInsight(item.insight);
|
|
572
|
-
const msg = truncateWithEllipsis(f.message, 220);
|
|
573
|
-
linesOut.push(`${index}. ${msg}`);
|
|
574
|
-
if (opts.firstInsightSubLine) {
|
|
575
|
-
const sub = f.where.trim() || f.action.trim();
|
|
576
|
-
if (sub) {
|
|
577
|
-
linesOut.push(` - ${truncateWithEllipsis(sub, 140)}`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return linesOut;
|
|
581
|
-
}
|
|
582
|
-
const rec = item.rec;
|
|
583
|
-
const parts = formatRecommendationPrimaryLines(rec, opts.rationaleMax, {
|
|
584
|
-
copy: opts.copy,
|
|
585
|
-
recReviewPrefix: opts.applyRecReviewPrefix,
|
|
586
|
-
});
|
|
587
|
-
parts.forEach((line, j) => {
|
|
588
|
-
linesOut.push(j === 0 ? `${index}. ${line}` : line);
|
|
589
|
-
});
|
|
590
|
-
return linesOut;
|
|
591
|
-
}
|
|
592
|
-
function appendDevRiskLine(result, recsLen, lines) {
|
|
593
|
-
const total = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
|
|
594
|
-
? result.totalScore
|
|
595
|
-
: null;
|
|
596
|
-
const label = scoreBandLabelFromTotal(total);
|
|
597
|
-
const emoji = total === null ? "⚪" : total >= 70 ? "🔴" : total >= 40 ? "⚠️" : "✅";
|
|
598
|
-
const tail = recsLen === 0 ? " — No critical issues" : "";
|
|
599
|
-
lines.push(`${emoji} **${label}**${tail}`);
|
|
600
|
-
lines.push("");
|
|
601
|
-
}
|
|
602
|
-
export function buildActionsStepSummaryMarkdown(opts) {
|
|
603
|
-
const { result, profile, copy: flat } = opts;
|
|
604
|
-
const methodology = result.methodologyVersion
|
|
605
|
-
? String(result.methodologyVersion)
|
|
606
|
-
: null;
|
|
607
|
-
const isStub = isStubMethodologyVersion(methodology ?? undefined);
|
|
608
|
-
const trustedLayout = profile === "trusted" && !isStub;
|
|
609
|
-
const showDemoHeader = profile !== "trusted";
|
|
610
|
-
const lines = [];
|
|
611
|
-
if (showDemoHeader) {
|
|
612
|
-
buildDemoHeader(flat, lines);
|
|
613
|
-
}
|
|
614
|
-
if (!trustedLayout) {
|
|
615
|
-
const recs = Array.isArray(result.recommendations)
|
|
616
|
-
? result.recommendations
|
|
617
|
-
: [];
|
|
618
|
-
appendDevRiskLine(result, recs.length, lines);
|
|
619
|
-
}
|
|
620
|
-
const recsAll = Array.isArray(result.recommendations)
|
|
621
|
-
? result.recommendations
|
|
622
|
-
: [];
|
|
623
|
-
if (trustedLayout) {
|
|
624
|
-
const titleBlock = [...buildTrustedTitleLines(result, flat), ""];
|
|
625
|
-
const why = collectWhyBullets(result, 2);
|
|
626
|
-
const whyLines = [];
|
|
627
|
-
for (const w of why) {
|
|
628
|
-
whyLines.push(`- ${w}`);
|
|
629
|
-
}
|
|
630
|
-
if (whyLines.length > 0)
|
|
631
|
-
whyLines.push("");
|
|
632
|
-
const { defaultItems, overflowInsights, overflowRecs } = buildWhatToDoItems(result);
|
|
633
|
-
const whatLines = [];
|
|
634
|
-
if (defaultItems.length > 0) {
|
|
635
|
-
let n = 1;
|
|
636
|
-
let usedRecReviewPrefix = false;
|
|
637
|
-
for (let i = 0; i < defaultItems.length; i++) {
|
|
638
|
-
const item = defaultItems[i];
|
|
639
|
-
const firstSub = i === 0 && item.kind === "insight";
|
|
640
|
-
const applyRecReviewPrefix = item.kind === "rec" &&
|
|
641
|
-
!usedRecReviewPrefix &&
|
|
642
|
-
recommendationLeadsWithRationale(item.rec);
|
|
643
|
-
const chunk = formatDefaultWhatLine(item, n, {
|
|
644
|
-
firstInsightSubLine: firstSub,
|
|
645
|
-
rationaleMax: ACTIONS_REC_RATIONALE_MAX_CHARS,
|
|
646
|
-
copy: flat,
|
|
647
|
-
applyRecReviewPrefix,
|
|
648
|
-
});
|
|
649
|
-
if (applyRecReviewPrefix)
|
|
650
|
-
usedRecReviewPrefix = true;
|
|
651
|
-
whatLines.push(...chunk);
|
|
652
|
-
n += 1;
|
|
653
|
-
}
|
|
654
|
-
whatLines.push("");
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
whatLines.push("*No prioritized actions in the default summary — see score breakdown below.*");
|
|
658
|
-
whatLines.push("");
|
|
659
|
-
}
|
|
660
|
-
const methLines = [];
|
|
661
|
-
if (methodology) {
|
|
662
|
-
const methLabel = copyLine(flat, "actions.trustedSummaryMethodologyLine", scanSurfaceCopy.actions.trustedSummaryMethodologyLine);
|
|
663
|
-
methLines.push(`**${methLabel}:** ${methodology}`);
|
|
664
|
-
methLines.push("");
|
|
665
|
-
}
|
|
666
|
-
const headMd = [...titleBlock, ...whyLines, ...whatLines]
|
|
667
|
-
.join("\n")
|
|
668
|
-
.trimEnd();
|
|
669
|
-
const trimmedHead = enforceDefaultFoldBudget(headMd);
|
|
670
|
-
lines.length = 0;
|
|
671
|
-
lines.push(...trimmedHead.split("\n"));
|
|
672
|
-
if (methLines.length > 0) {
|
|
673
|
-
lines.push(...methLines);
|
|
674
|
-
}
|
|
675
|
-
lines.push(buildScoreBreakdownDetails(result, flat, {
|
|
676
|
-
includeScoreGlossary: false,
|
|
677
|
-
}));
|
|
678
|
-
const graphBlock = buildGraphDetails(result, flat);
|
|
679
|
-
if (graphBlock) {
|
|
680
|
-
lines.push("");
|
|
681
|
-
lines.push(graphBlock);
|
|
682
|
-
}
|
|
683
|
-
const moreAct = buildMoreActionsDetails(overflowRecs, flat);
|
|
684
|
-
if (moreAct) {
|
|
685
|
-
lines.push("");
|
|
686
|
-
lines.push(moreAct);
|
|
687
|
-
}
|
|
688
|
-
const moreIns = buildMoreInsightsDetails(overflowInsights, flat);
|
|
689
|
-
if (moreIns) {
|
|
690
|
-
lines.push("");
|
|
691
|
-
lines.push(moreIns);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
// Development / demo body (parity with legacy: methodology + recs + single details bundle)
|
|
696
|
-
buildMethodologyLine(flat, methodology, false, isStub, lines);
|
|
697
|
-
if (recsAll.length > 0) {
|
|
698
|
-
lines.push(`## Recommended actions (${Math.min(3, recsAll.length)})`);
|
|
699
|
-
const top = sortRecommendationsForDisplay(recsAll).slice(0, 3);
|
|
700
|
-
top.forEach((rec, i) => {
|
|
701
|
-
const title = String(rec.title ?? "Review dependencies");
|
|
702
|
-
const rat = rec.rationale
|
|
703
|
-
? truncateWithEllipsis(rec.rationale, ACTIONS_REC_RATIONALE_MAX_CHARS)
|
|
704
|
-
: "";
|
|
705
|
-
const pkgs = Array.isArray(rec.packages) && rec.packages.length > 0
|
|
706
|
-
? ` — \`${rec.packages.slice(0, 3).join("`, `")}\`${rec.packages.length > 3 ? `, +${rec.packages.length - 3} more` : ""}`
|
|
707
|
-
: "";
|
|
708
|
-
lines.push(`${i + 1}. **${title}**${rat ? ` — ${rat}` : ""}${pkgs}`);
|
|
709
|
-
});
|
|
710
|
-
lines.push("");
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
lines.push(`## ${copyLine(flat, "actions.devNoImmediateActions", scanSurfaceCopy.actions.devNoImmediateActions)}`);
|
|
714
|
-
lines.push("");
|
|
715
|
-
}
|
|
716
|
-
lines.push(buildScoreBreakdownDetails(result, flat, {
|
|
717
|
-
includeScoreGlossary: true,
|
|
718
|
-
}));
|
|
719
|
-
const graphBlock = buildGraphDetails(result, flat);
|
|
720
|
-
if (graphBlock) {
|
|
721
|
-
lines.push("");
|
|
722
|
-
lines.push(graphBlock);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
let md = `${lines.join("\n").trimEnd()}\n`;
|
|
726
|
-
if (md.length > ACTIONS_SUMMARY_SOFT_MAX_CHARS) {
|
|
727
|
-
md = `${md.slice(0, ACTIONS_SUMMARY_SOFT_MAX_CHARS).trimEnd()}\n\n*(Summary truncated for size.)*\n`;
|
|
728
|
-
}
|
|
729
|
-
return md;
|
|
730
|
-
}
|