@mergesignal/shared 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/actionsStepSummary.d.ts.map +1 -1
  2. package/dist/actionsStepSummary.js +2 -29
  3. package/dist/collectNarrativeWhyBullets.d.ts +12 -0
  4. package/dist/collectNarrativeWhyBullets.d.ts.map +1 -0
  5. package/dist/collectNarrativeWhyBullets.js +104 -0
  6. package/dist/deriveScanNarrative.d.ts +10 -0
  7. package/dist/deriveScanNarrative.d.ts.map +1 -0
  8. package/dist/deriveScanNarrative.js +370 -0
  9. package/dist/deriveScanNarrative.test.d.ts +2 -0
  10. package/dist/deriveScanNarrative.test.d.ts.map +1 -0
  11. package/dist/deriveScanNarrative.test.js +164 -0
  12. package/dist/extractRepositoryContextFacts.d.ts +16 -0
  13. package/dist/extractRepositoryContextFacts.d.ts.map +1 -0
  14. package/dist/extractRepositoryContextFacts.js +71 -0
  15. package/dist/fixtures/repoIntelligenceFixtures.d.ts +10 -0
  16. package/dist/fixtures/repoIntelligenceFixtures.d.ts.map +1 -0
  17. package/dist/fixtures/repoIntelligenceFixtures.js +93 -0
  18. package/dist/formatInsight.d.ts +2 -2
  19. package/dist/formatInsight.d.ts.map +1 -1
  20. package/dist/formatInsight.js +2 -7
  21. package/dist/formatInsight.test.js +1 -1
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -0
  25. package/dist/narrativeParity.test.d.ts +2 -0
  26. package/dist/narrativeParity.test.d.ts.map +1 -0
  27. package/dist/narrativeParity.test.js +64 -0
  28. package/dist/narrativePresentation.d.ts +50 -0
  29. package/dist/narrativePresentation.d.ts.map +1 -0
  30. package/dist/narrativePresentation.js +180 -0
  31. package/dist/narrativePresentation.test.d.ts +2 -0
  32. package/dist/narrativePresentation.test.d.ts.map +1 -0
  33. package/dist/narrativePresentation.test.js +53 -0
  34. package/dist/normalizeGeneratedText.d.ts +8 -0
  35. package/dist/normalizeGeneratedText.d.ts.map +1 -0
  36. package/dist/normalizeGeneratedText.js +31 -0
  37. package/dist/normalizeGeneratedText.test.d.ts +2 -0
  38. package/dist/normalizeGeneratedText.test.d.ts.map +1 -0
  39. package/dist/normalizeGeneratedText.test.js +18 -0
  40. package/dist/prCheckRunPresentation.d.ts +4 -3
  41. package/dist/prCheckRunPresentation.d.ts.map +1 -1
  42. package/dist/prCheckRunPresentation.js +56 -51
  43. package/dist/prCheckRunPresentation.test.js +2 -2
  44. package/dist/presentGitHubPrComment.d.ts +9 -0
  45. package/dist/presentGitHubPrComment.d.ts.map +1 -0
  46. package/dist/presentGitHubPrComment.js +80 -0
  47. package/dist/presentScanCardSummary.d.ts +16 -0
  48. package/dist/presentScanCardSummary.d.ts.map +1 -0
  49. package/dist/presentScanCardSummary.js +212 -0
  50. package/dist/productMessaging.d.ts +1 -1
  51. package/dist/productMessaging.js +1 -1
  52. package/dist/repoIntelligenceSchema.d.ts +123 -0
  53. package/dist/repoIntelligenceSchema.d.ts.map +1 -0
  54. package/dist/repoIntelligenceSchema.js +174 -0
  55. package/dist/riskVocabulary.js +1 -1
  56. package/dist/scanCardSummary.d.ts +21 -3
  57. package/dist/scanCardSummary.d.ts.map +1 -1
  58. package/dist/scanCardSummary.js +17 -58
  59. package/dist/scanCardSummary.test.js +25 -8
  60. package/dist/scanDetailViewModel.d.ts +22 -0
  61. package/dist/scanDetailViewModel.d.ts.map +1 -1
  62. package/dist/scanDetailViewModel.js +247 -31
  63. package/dist/scanDetailViewModel.test.js +29 -3
  64. package/dist/scanNarrativeFacts.d.ts +92 -0
  65. package/dist/scanNarrativeFacts.d.ts.map +1 -0
  66. package/dist/scanNarrativeFacts.js +24 -0
  67. package/dist/scanSurfaceCopy.d.ts +49 -23
  68. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  69. package/dist/scanSurfaceCopy.js +49 -23
  70. package/dist/types.d.ts +1 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +1 -1
@@ -0,0 +1,180 @@
1
+ import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
2
+ export function selectPrimaryChangedPackage(facts) {
3
+ return facts.changedPackages.primary;
4
+ }
5
+ export function formatChangedPackagesShort(facts, maxNames = 2) {
6
+ const { primary, others } = facts.changedPackages;
7
+ if (!primary)
8
+ return null;
9
+ if (others.length === 0)
10
+ return primary;
11
+ if (maxNames >= 2 && others.length === 1) {
12
+ return `${primary}, ${others[0]}`;
13
+ }
14
+ return `${primary} +${others.length}`;
15
+ }
16
+ export function formatChangedPackagesDetail(facts) {
17
+ return facts.changedPackages.all;
18
+ }
19
+ export function summarizePackageUsage(facts, options = {}) {
20
+ const maxPaths = options.maxPaths ?? 3;
21
+ const maxPackages = options.maxPackages ?? 5;
22
+ const rows = facts.packageUsage.slice(0, maxPackages);
23
+ if (rows.length === 0)
24
+ return null;
25
+ const pathSamples = [];
26
+ let pathCount = 0;
27
+ const areaLabels = [];
28
+ const packageNames = [];
29
+ for (const row of rows) {
30
+ packageNames.push(row.packageName);
31
+ const rowPaths = [...row.paths, ...row.criticalPaths, ...row.files].filter(Boolean);
32
+ pathCount += rowPaths.length;
33
+ for (const p of rowPaths) {
34
+ if (pathSamples.length >= maxPaths)
35
+ break;
36
+ if (!pathSamples.includes(p))
37
+ pathSamples.push(p);
38
+ }
39
+ for (const a of row.areas) {
40
+ if (!areaLabels.includes(a))
41
+ areaLabels.push(a);
42
+ }
43
+ }
44
+ if (pathCount === 0 && areaLabels.length === 0)
45
+ return null;
46
+ return { pathCount, pathSamples, areaLabels, packageNames };
47
+ }
48
+ function labelRuntimeSurface(facts) {
49
+ const rs = facts.runtimeSurface;
50
+ if (!rs)
51
+ return null;
52
+ return scanSurfaceCopy.narrativeCard.runtimeSurface[rs.kind];
53
+ }
54
+ function labelReachabilityKind(facts) {
55
+ const r = facts.reachability;
56
+ if (!r)
57
+ return null;
58
+ return scanSurfaceCopy.narrativeCard.reachability[r.kind];
59
+ }
60
+ function labelBlastRadiusLevel(facts) {
61
+ const br = facts.blastRadius;
62
+ if (!br)
63
+ return null;
64
+ return scanSurfaceCopy.narrativeCard.blastRadius[br.level];
65
+ }
66
+ export function summarizeReachability(facts, maxPaths = 1) {
67
+ const kindLabel = labelReachabilityKind(facts);
68
+ const paths = facts.reachability?.evidence.paths ?? [];
69
+ const frameworks = facts.reachability?.evidence.frameworks ?? facts.frameworks;
70
+ return {
71
+ kindLabel,
72
+ pathSamples: paths.slice(0, maxPaths),
73
+ frameworks: frameworks.slice(0, 3),
74
+ };
75
+ }
76
+ export function summarizeBlastRadius(facts, maxFactors = 3) {
77
+ const br = facts.blastRadius;
78
+ return {
79
+ levelLabel: labelBlastRadiusLevel(facts),
80
+ factors: (br?.factors ?? []).slice(0, maxFactors),
81
+ changedPackageCount: br?.changedPackageCount ?? null,
82
+ };
83
+ }
84
+ export function summarizeHotspots(facts, max = 5) {
85
+ return facts.hotspots.slice(0, max).map((h) => ({
86
+ packageName: h.packageName,
87
+ source: h.source,
88
+ pathSample: h.paths[0] ?? null,
89
+ }));
90
+ }
91
+ export function selectReviewerGuidance(facts, options = {}) {
92
+ const max = options.max ?? 6;
93
+ let list = facts.reviewerGuidance;
94
+ if (options.scope === "changed") {
95
+ list = list.filter((g) => g.scope === "changed");
96
+ }
97
+ else if (options.scope === "all") {
98
+ list = list.filter((g) => g.scope === "all");
99
+ }
100
+ return list.slice(0, max);
101
+ }
102
+ export function composeVerificationPrompt(facts) {
103
+ const changed = selectReviewerGuidance(facts, { scope: "changed", max: 1 });
104
+ const remediation = changed[0]?.remediation?.trim();
105
+ if (remediation)
106
+ return remediation;
107
+ const usage = summarizePackageUsage(facts, { maxPaths: 1 });
108
+ if (usage?.pathSamples[0]) {
109
+ return `Confirm behavior where this dependency is used (${usage.pathSamples[0]}).`;
110
+ }
111
+ const any = selectReviewerGuidance(facts, { max: 1 });
112
+ return any[0]?.remediation?.trim() ?? null;
113
+ }
114
+ export function formatUsageSummaryLine(facts, maxPaths = 1) {
115
+ const usage = summarizePackageUsage(facts, { maxPaths });
116
+ if (!usage)
117
+ return null;
118
+ if (usage.pathSamples.length > 0) {
119
+ const sample = usage.pathSamples[0];
120
+ if (usage.pathCount <= 1) {
121
+ return `Used in ${sample}`;
122
+ }
123
+ return `Used in ${usage.pathCount} paths (${sample})`;
124
+ }
125
+ if (usage.areaLabels.length > 0) {
126
+ return `Used in ${usage.areaLabels.slice(0, 2).join(", ")}`;
127
+ }
128
+ return null;
129
+ }
130
+ export function formatBlastRadiusDetailLine(facts, maxFactors = 1) {
131
+ const summary = summarizeBlastRadius(facts, maxFactors);
132
+ if (summary.factors.length === 0)
133
+ return null;
134
+ const factor = summary.factors[0].replace(/_/g, " ");
135
+ if (summary.levelLabel) {
136
+ return `${summary.levelLabel}: ${factor}`;
137
+ }
138
+ return factor;
139
+ }
140
+ export function formatFrameworksSummary(facts, max = 2) {
141
+ const frameworks = facts.frameworks.slice(0, max);
142
+ if (frameworks.length === 0)
143
+ return null;
144
+ if (frameworks.length === 1)
145
+ return frameworks[0];
146
+ return frameworks.join(", ");
147
+ }
148
+ export function composeContextLineFromFacts(facts, options = {}) {
149
+ const parts = [];
150
+ const runtime = labelRuntimeSurface(facts);
151
+ const reach = summarizeReachability(facts, options.includePathSample ? 1 : 0);
152
+ const blast = labelBlastRadiusLevel(facts);
153
+ if (runtime)
154
+ parts.push(runtime);
155
+ if (reach.kindLabel) {
156
+ if (options.includePathSample && reach.pathSamples[0]) {
157
+ parts.push(`${reach.kindLabel} (${reach.pathSamples[0]})`);
158
+ }
159
+ else {
160
+ parts.push(reach.kindLabel);
161
+ }
162
+ }
163
+ if (blast)
164
+ parts.push(blast);
165
+ const maxAreas = options.maxAreas ?? 2;
166
+ const areaLabels = [];
167
+ for (const area of facts.affectedAreas) {
168
+ if (areaLabels.length >= maxAreas)
169
+ break;
170
+ if (!areaLabels.includes(area.label))
171
+ areaLabels.push(area.label);
172
+ }
173
+ if (areaLabels.length > 0) {
174
+ parts.push(areaLabels.join(scanSurfaceCopy.narrativeCard.areasSeparator));
175
+ }
176
+ if (parts.length === 0)
177
+ return null;
178
+ return parts.join(scanSurfaceCopy.narrativeCard.contextSeparator);
179
+ }
180
+ export { labelRuntimeSurface, labelReachabilityKind, labelBlastRadiusLevel };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=narrativePresentation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narrativePresentation.test.d.ts","sourceRoot":"","sources":["../src/narrativePresentation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
3
+ import { fixtureRepoIntelligenceFastify } from "./fixtures/repoIntelligenceFixtures.js";
4
+ import { composeVerificationPrompt, formatChangedPackagesShort, formatUsageSummaryLine, summarizePackageUsage, } from "./narrativePresentation.js";
5
+ const baseResult = {
6
+ totalScore: 10,
7
+ layerScores: {
8
+ security: 1,
9
+ maintainability: 2,
10
+ ecosystem: 3,
11
+ upgradeImpact: 4,
12
+ },
13
+ findings: [],
14
+ generatedAt: "2026-01-01T00:00:00.000Z",
15
+ };
16
+ describe("narrativePresentation", () => {
17
+ it("formats changed packages with second name when only one other", () => {
18
+ const facts = deriveScanNarrative({
19
+ ...baseResult,
20
+ changedPackages: ["lodash", "axios"],
21
+ });
22
+ expect(formatChangedPackagesShort(facts, 2)).toBe("lodash, axios");
23
+ });
24
+ it("summarizes package usage paths from facts", () => {
25
+ const facts = deriveScanNarrative({
26
+ ...baseResult,
27
+ changedPackages: ["fastify"],
28
+ analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
29
+ repoIntelligence: fixtureRepoIntelligenceFastify,
30
+ });
31
+ const usage = summarizePackageUsage(facts);
32
+ expect(usage?.pathCount).toBeGreaterThan(0);
33
+ expect(formatUsageSummaryLine(facts)).toMatch(/Used in/);
34
+ });
35
+ it("composes verification from remediation when present", () => {
36
+ const facts = deriveScanNarrative({
37
+ ...baseResult,
38
+ changedPackages: ["pkg"],
39
+ insights: [
40
+ {
41
+ type: "usage_risk",
42
+ priority: "high",
43
+ confidence: "confirmed",
44
+ scope: "changed",
45
+ message: "Risk in billing",
46
+ context: "billing",
47
+ remediation: "Run export job smoke test",
48
+ },
49
+ ],
50
+ });
51
+ expect(composeVerificationPrompt(facts)).toBe("Run export job smoke test");
52
+ });
53
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Enforce ASCII-safe generated user-facing text (hyphens, separators, ellipsis).
3
+ */
4
+ export declare function normalizeGeneratedText(text: string): string;
5
+ export declare function normalizeGeneratedTextNullable(text: string | null | undefined): string | null;
6
+ /** Normalize every string field on a shallow object (presenter DTOs). */
7
+ export declare function normalizeGeneratedStrings<T extends Record<string, unknown>>(obj: T): T;
8
+ //# sourceMappingURL=normalizeGeneratedText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeGeneratedText.d.ts","sourceRoot":"","sources":["../src/normalizeGeneratedText.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,MAAM,GAAG,IAAI,CAIf;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,GAAG,EAAE,CAAC,GACL,CAAC,CAYH"}
@@ -0,0 +1,31 @@
1
+ const TYPOGRAPHIC_DASH = /[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g;
2
+ /**
3
+ * Enforce ASCII-safe generated user-facing text (hyphens, separators, ellipsis).
4
+ */
5
+ export function normalizeGeneratedText(text) {
6
+ return text
7
+ .replace(TYPOGRAPHIC_DASH, "-")
8
+ .replace(/\u00B7/g, " | ")
9
+ .replace(/\u2026/g, "...")
10
+ .replace(/&ndash;/gi, "-")
11
+ .replace(/&mdash;/gi, "-");
12
+ }
13
+ export function normalizeGeneratedTextNullable(text) {
14
+ if (text == null)
15
+ return null;
16
+ const normalized = normalizeGeneratedText(text);
17
+ return normalized.length > 0 ? normalized : null;
18
+ }
19
+ /** Normalize every string field on a shallow object (presenter DTOs). */
20
+ export function normalizeGeneratedStrings(obj) {
21
+ const out = { ...obj };
22
+ for (const [key, value] of Object.entries(out)) {
23
+ if (typeof value === "string") {
24
+ out[key] = normalizeGeneratedText(value);
25
+ }
26
+ else if (Array.isArray(value)) {
27
+ out[key] = value.map((item) => typeof item === "string" ? normalizeGeneratedText(item) : item);
28
+ }
29
+ }
30
+ return out;
31
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=normalizeGeneratedText.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeGeneratedText.test.d.ts","sourceRoot":"","sources":["../src/normalizeGeneratedText.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeGeneratedText } from "./normalizeGeneratedText.js";
3
+ import { scanSurfaceCopyFlat } from "./scanSurfaceCopy.js";
4
+ describe("normalizeGeneratedText", () => {
5
+ it("replaces typographic dashes with ASCII hyphen-minus", () => {
6
+ expect(normalizeGeneratedText("foo – bar — baz")).toBe("foo - bar - baz");
7
+ expect(normalizeGeneratedText("a&mdash;b&ndash;c")).toBe("a-b-c");
8
+ });
9
+ it("leaves ASCII text unchanged", () => {
10
+ expect(normalizeGeneratedText("Safe - Needs review")).toBe("Safe - Needs review");
11
+ });
12
+ it("scanSurfaceCopy flat strings are ASCII-safe", () => {
13
+ const asciiOnly = /^[\x00-\x7F]*$/;
14
+ for (const value of Object.values(scanSurfaceCopyFlat())) {
15
+ expect(asciiOnly.test(value), value).toBe(true);
16
+ }
17
+ });
18
+ });
@@ -1,3 +1,4 @@
1
+ import type { ScanNarrativeFacts } from "./scanNarrativeFacts.js";
1
2
  import type { ScanResult } from "./types.js";
2
3
  export declare const CHECK_RUN_MAX_WHY_BULLETS = 3;
3
4
  export declare const CHECK_RUN_MAX_ACTION_BULLETS = 3;
@@ -57,9 +58,9 @@ export type CheckRunSection = {
57
58
  export declare function formatScanDashboardUrl(webAppOrigin: string, scanId: string): string;
58
59
  export declare function buildPrCheckRunTitle(opts?: PrCheckRunTitleOptions): string;
59
60
  /** 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[];
61
+ export declare function hasStrongRepoGraphDrivers(result: ScanResult, facts?: ScanNarrativeFacts): boolean;
62
+ export declare function deriveCheckRunPolicy(result: ScanResult, ctx: Pick<PrCheckRunSummaryContext, "baseline">, facts?: ScanNarrativeFacts): CheckRunPolicy;
63
+ export declare function buildPrCheckRunSections(policy: CheckRunPolicy, result: ScanResult, ctx: PrCheckRunSummaryContext, facts?: ScanNarrativeFacts): CheckRunSection[];
63
64
  export declare function renderCheckRunMarkdown(sections: CheckRunSection[]): string;
64
65
  export declare function buildPrCheckRunSummaryMarkdown(ctx: PrCheckRunSummaryContext): string;
65
66
  /** Whether baseline scan has no actionable PR bullets (for tests / callers). */
@@ -1 +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,CA4E1E;AAED,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,wBAAwB,GAC5B,MAAM,CAIR;AAED,gFAAgF;AAChF,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAExE"}
1
+ {"version":3,"file":"prCheckRunPresentation.d.ts","sourceRoot":"","sources":["../src/prCheckRunPresentation.ts"],"names":[],"mappings":"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"}
@@ -2,9 +2,12 @@
2
2
  * GitHub Check Run markdown — policy → sections → dumb renderer.
3
3
  * Thin projection of ScanResult for PR decision support (not a presentation framework).
4
4
  */
5
- import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
5
+ import { collectWhyBullets } from "./collectNarrativeWhyBullets.js";
6
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
6
7
  import { formatInsight } from "./formatInsight.js";
7
- import { humanizeEngineSurfaceText, layerDriverSummary, sortPRInsightsForDisplay, sortRecommendationsForDisplay, truncateWithEllipsis, } from "./actionsStepSummary.js";
8
+ import { normalizeGeneratedText } from "./normalizeGeneratedText.js";
9
+ import { selectReviewerGuidance } from "./narrativePresentation.js";
10
+ import { layerDriverSummary, sortPRInsightsForDisplay, sortRecommendationsForDisplay, truncateWithEllipsis, } from "./actionsStepSummary.js";
8
11
  import { mergePostureLabel } from "./riskVocabulary.js";
9
12
  import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
10
13
  // --- Caps (change only with tests) ---
@@ -40,43 +43,17 @@ export function formatScanDashboardUrl(webAppOrigin, scanId) {
40
43
  export function buildPrCheckRunTitle(opts = {}) {
41
44
  const base = scanSurfaceCopy.checkRun.titleBase;
42
45
  if (opts.baselineOnly) {
43
- return `${base} ${scanSurfaceCopy.checkRun.titleBaselineSuffix}`;
46
+ return `${base} - ${scanSurfaceCopy.checkRun.titleBaselineSuffix}`;
44
47
  }
45
48
  return base;
46
49
  }
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
50
  /** 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);
51
+ export function hasStrongRepoGraphDrivers(result, facts) {
52
+ if (facts?.availability.mode === "pr_intelligence" ||
53
+ facts?.availability.tiersPresent.tier1) {
54
+ return false;
55
+ }
56
+ const phrases = collectWhyBullets(facts ?? deriveScanNarrative(result), result, CHECK_RUN_MAX_REPO_DRIVER_PHRASES);
80
57
  return phrases.some((p) => p.trim().length >= 12);
81
58
  }
82
59
  function hasActionableBullets(result) {
@@ -92,7 +69,7 @@ function insightToBullet(insight) {
92
69
  const msg = truncateWithEllipsis(f.message, CHECK_RUN_ACTION_BULLET_MAX_CHARS);
93
70
  const action = f.action.trim();
94
71
  if (action && action.length < 120) {
95
- return `${msg} ${truncateWithEllipsis(action, 100)}`;
72
+ return normalizeGeneratedText(`${msg} - ${truncateWithEllipsis(action, 100)}`);
96
73
  }
97
74
  return msg;
98
75
  }
@@ -100,7 +77,7 @@ function recommendationToBullet(rec) {
100
77
  const title = String(rec.title ?? "").trim();
101
78
  const rat = String(rec.rationale ?? "").trim();
102
79
  const pkgs = Array.isArray(rec.packages) && rec.packages.length > 0
103
- ? ` (${rec.packages.slice(0, 3).join(", ")}${rec.packages.length > 3 ? ", " : ""})`
80
+ ? ` (${rec.packages.slice(0, 3).join(", ")}${rec.packages.length > 3 ? ", ..." : ""})`
104
81
  : "";
105
82
  if (rat && (!title || rat.length > title.length)) {
106
83
  return truncateWithEllipsis(rat, CHECK_RUN_ACTION_BULLET_MAX_CHARS) + pkgs;
@@ -113,6 +90,31 @@ function findingToBullet(f) {
113
90
  const pkg = f.packageName ? ` (\`${f.packageName}\`)` : "";
114
91
  return (truncateWithEllipsis(title || f.description, CHECK_RUN_ACTION_BULLET_MAX_CHARS) + pkg);
115
92
  }
93
+ function guidanceToActionBullet(g) {
94
+ const msg = truncateWithEllipsis(g.message.trim(), CHECK_RUN_ACTION_BULLET_MAX_CHARS);
95
+ const action = g.remediation?.trim();
96
+ if (action && action.length < 120) {
97
+ return normalizeGeneratedText(`${msg} - ${truncateWithEllipsis(action, 100)}`);
98
+ }
99
+ return normalizeGeneratedText(msg);
100
+ }
101
+ function buildActionBulletsFromFacts(facts, result, max) {
102
+ const prIntel = facts.availability.mode === "pr_intelligence" ||
103
+ facts.availability.tiersPresent.tier1;
104
+ if (!prIntel) {
105
+ return buildActionBullets(result, max);
106
+ }
107
+ const out = [];
108
+ for (const g of selectReviewerGuidance(facts, { scope: "changed", max })) {
109
+ out.push(guidanceToActionBullet(g));
110
+ if (out.length >= max)
111
+ return out.slice(0, max);
112
+ }
113
+ if (out.length >= max)
114
+ return out.slice(0, max);
115
+ const legacy = buildActionBullets(result, max - out.length);
116
+ return [...out, ...legacy].slice(0, max);
117
+ }
116
118
  function buildActionBullets(result, max) {
117
119
  const out = [];
118
120
  const sortedI = sortPRInsightsForDisplay(Array.isArray(result.insights) ? result.insights : []);
@@ -156,12 +158,13 @@ function layerScoreRows(result) {
156
158
  }
157
159
  return rows;
158
160
  }
159
- export function deriveCheckRunPolicy(result, ctx) {
161
+ export function deriveCheckRunPolicy(result, ctx, facts) {
162
+ const resolvedFacts = facts ?? deriveScanNarrative(result);
160
163
  const baseline = ctx.baseline;
161
- const actionBullets = buildActionBullets(result, CHECK_RUN_MAX_ACTION_BULLETS);
164
+ const actionBullets = buildActionBulletsFromFacts(resolvedFacts, result, CHECK_RUN_MAX_ACTION_BULLETS);
162
165
  const layerRows = layerScoreRows(result);
163
166
  const showRepoGraphContext = !baseline &&
164
- hasStrongRepoGraphDrivers(result) &&
167
+ hasStrongRepoGraphDrivers(result, resolvedFacts) &&
165
168
  typeof result.totalScore === "number" &&
166
169
  Number.isFinite(result.totalScore);
167
170
  const showLayerScores = !baseline && layerRows.length > 0;
@@ -189,14 +192,15 @@ function buildLeadSection(result, policy) {
189
192
  : null;
190
193
  return { kind: "lead", posture, riskIndexLine };
191
194
  }
192
- export function buildPrCheckRunSections(policy, result, ctx) {
195
+ export function buildPrCheckRunSections(policy, result, ctx, facts) {
196
+ const resolvedFacts = facts ?? deriveScanNarrative(result);
193
197
  const sections = {};
194
198
  sections.lead = buildLeadSection(result, policy);
195
- const why = collectWhyBullets(result, policy.maxWhyBullets);
199
+ const why = collectWhyBullets(resolvedFacts, result, policy.maxWhyBullets);
196
200
  if (why.length > 0) {
197
201
  sections.why = { kind: "why", bullets: why };
198
202
  }
199
- const actions = buildActionBullets(result, policy.maxActionBullets);
203
+ const actions = buildActionBulletsFromFacts(resolvedFacts, result, policy.maxActionBullets);
200
204
  if (actions.length > 0) {
201
205
  sections.actions = { kind: "actions", bullets: actions };
202
206
  }
@@ -212,7 +216,7 @@ export function buildPrCheckRunSections(policy, result, ctx) {
212
216
  sections.repoContext = {
213
217
  kind: "repoContext",
214
218
  score,
215
- driverPhrases: collectWhyBullets(result, policy.maxRepoDriverPhrases),
219
+ driverPhrases: collectWhyBullets(resolvedFacts, result, policy.maxRepoDriverPhrases),
216
220
  };
217
221
  }
218
222
  if (policy.showLayerScores) {
@@ -270,8 +274,8 @@ export function renderCheckRunMarkdown(sections) {
270
274
  break;
271
275
  }
272
276
  case "repoContext": {
273
- const drivers = section.driverPhrases.join(" · ");
274
- parts.push(`${scanSurfaceCopy.checkRun.repoContextLabel} ${section.score}/100 ${drivers}`);
277
+ const drivers = section.driverPhrases.join(" | ");
278
+ parts.push(normalizeGeneratedText(`${scanSurfaceCopy.checkRun.repoContextLabel} ${section.score}/100 - ${drivers}`));
275
279
  parts.push("");
276
280
  break;
277
281
  }
@@ -280,7 +284,7 @@ export function renderCheckRunMarkdown(sections) {
280
284
  parts.push(`<summary>${scanSurfaceCopy.checkRun.layerScoresDetailsSummary}</summary>`);
281
285
  parts.push("");
282
286
  for (const row of section.rows) {
283
- parts.push(`- **${row.label}** ${row.score}/100 ${row.driver}`);
287
+ parts.push(normalizeGeneratedText(`- **${row.label}** ${row.score}/100 - ${row.driver}`));
284
288
  }
285
289
  parts.push("");
286
290
  parts.push("</details>");
@@ -297,15 +301,16 @@ export function renderCheckRunMarkdown(sections) {
297
301
  }
298
302
  }
299
303
  }
300
- let md = parts.join("\n").trimEnd();
304
+ let md = normalizeGeneratedText(parts.join("\n").trimEnd());
301
305
  if (md.length > CHECK_RUN_SOFT_MAX_CHARS) {
302
- md = md.slice(0, CHECK_RUN_SOFT_MAX_CHARS - 1) + "";
306
+ md = md.slice(0, CHECK_RUN_SOFT_MAX_CHARS - 1) + "...";
303
307
  }
304
308
  return md;
305
309
  }
306
310
  export function buildPrCheckRunSummaryMarkdown(ctx) {
307
- const policy = deriveCheckRunPolicy(ctx.result, { baseline: ctx.baseline });
308
- const sections = buildPrCheckRunSections(policy, ctx.result, ctx);
311
+ const facts = deriveScanNarrative(ctx.result);
312
+ const policy = deriveCheckRunPolicy(ctx.result, { baseline: ctx.baseline }, facts);
313
+ const sections = buildPrCheckRunSections(policy, ctx.result, ctx, facts);
309
314
  return renderCheckRunMarkdown(sections);
310
315
  }
311
316
  /** Whether baseline scan has no actionable PR bullets (for tests / callers). */
@@ -38,7 +38,7 @@ describe("buildPrCheckRunTitle", () => {
38
38
  expect(buildPrCheckRunTitle()).not.toContain("—");
39
39
  });
40
40
  it("appends baseline suffix when requested", () => {
41
- expect(buildPrCheckRunTitle({ baselineOnly: true })).toBe("PR dependency change baseline scan only");
41
+ expect(buildPrCheckRunTitle({ baselineOnly: true })).toBe("PR dependency change - baseline scan only");
42
42
  });
43
43
  });
44
44
  describe("formatScanDashboardUrl", () => {
@@ -271,7 +271,7 @@ describe("buildPrCheckRunSummaryMarkdown", () => {
271
271
  baseline: false,
272
272
  });
273
273
  if (md.includes("Layer scores")) {
274
- expect(md).toMatch(/Maintainability.*\/100 —/);
274
+ expect(md).toMatch(/Maintainability.*\/100 -/);
275
275
  expect(md).not.toMatch(/\| Security \| 0 \|/);
276
276
  }
277
277
  });
@@ -0,0 +1,9 @@
1
+ import type { ScanNarrativeFacts } from "./scanNarrativeFacts.js";
2
+ import type { PRDecision, PRInsight, ScanResult } from "./types.js";
3
+ /**
4
+ * PR comment markdown from narrative facts (compressed dashboard story).
5
+ */
6
+ export declare function presentGitHubPrComment(facts: ScanNarrativeFacts, result: ScanResult): string;
7
+ /** Backward-compatible entry: derives facts then presents. */
8
+ export declare function renderInsightsAsMarkdown(insights: PRInsight[], decision: PRDecision): string;
9
+ //# sourceMappingURL=presentGitHubPrComment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentGitHubPrComment.d.ts","sourceRoot":"","sources":["../src/presentGitHubPrComment.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAoBpE;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,UAAU,GACjB,MAAM,CA6CR;AAED,8DAA8D;AAC9D,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,EAAE,EACrB,QAAQ,EAAE,UAAU,GACnB,MAAM,CAgBR"}
@@ -0,0 +1,80 @@
1
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
2
+ import { composeVerificationPrompt, formatChangedPackagesShort, formatUsageSummaryLine, selectReviewerGuidance, } from "./narrativePresentation.js";
3
+ import { normalizeGeneratedText } from "./normalizeGeneratedText.js";
4
+ import { MERGE_POSTURE_LABEL, mergePostureFromDecision, } from "./riskVocabulary.js";
5
+ import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
6
+ function renderGuidanceBlock(message, where, action) {
7
+ return [
8
+ normalizeGeneratedText(message),
9
+ "",
10
+ "**Where it shows up**",
11
+ "",
12
+ normalizeGeneratedText(where),
13
+ "",
14
+ "**What to do**",
15
+ "",
16
+ normalizeGeneratedText(action),
17
+ ].join("\n");
18
+ }
19
+ /**
20
+ * PR comment markdown from narrative facts (compressed dashboard story).
21
+ */
22
+ export function presentGitHubPrComment(facts, result) {
23
+ const posture = facts.mergePosture ??
24
+ mergePostureFromDecision(result.decision?.recommendation);
25
+ const title = posture
26
+ ? `**${MERGE_POSTURE_LABEL[posture]}**`
27
+ : `**${scanSurfaceCopy.checkRun.mergePostureUnavailable}**`;
28
+ const introLines = [];
29
+ const changed = formatChangedPackagesShort(facts, 3);
30
+ if (changed)
31
+ introLines.push(`Changed: ${changed}`);
32
+ const usage = formatUsageSummaryLine(facts, 1);
33
+ if (usage)
34
+ introLines.push(usage);
35
+ const verify = composeVerificationPrompt(facts);
36
+ if (verify)
37
+ introLines.push(`Verify: ${verify}`);
38
+ const guidance = selectReviewerGuidance(facts, { scope: "changed", max: 3 });
39
+ if (guidance.length === 0) {
40
+ const fallback = selectReviewerGuidance(facts, { max: 3 });
41
+ guidance.push(...fallback.slice(0, 3 - guidance.length));
42
+ }
43
+ const blocks = guidance.map((g) => {
44
+ const where = g.context?.trim() ||
45
+ facts.packageUsage
46
+ .flatMap((u) => u.paths.slice(0, 1))
47
+ .filter(Boolean)[0] ||
48
+ "See scan detail for affected paths.";
49
+ const action = g.remediation?.trim() ||
50
+ composeVerificationPrompt(facts) ||
51
+ "Review before merge.";
52
+ return renderGuidanceBlock(g.message, where, action);
53
+ });
54
+ const parts = [title];
55
+ if (introLines.length > 0) {
56
+ parts.push("", introLines.join("\n"));
57
+ }
58
+ if (blocks.length > 0) {
59
+ parts.push("", ...blocks);
60
+ }
61
+ return parts.join("\n\n---\n\n").trimEnd();
62
+ }
63
+ /** Backward-compatible entry: derives facts then presents. */
64
+ export function renderInsightsAsMarkdown(insights, decision) {
65
+ const result = {
66
+ totalScore: 0,
67
+ layerScores: {
68
+ security: 0,
69
+ maintainability: 0,
70
+ ecosystem: 0,
71
+ upgradeImpact: 0,
72
+ },
73
+ findings: [],
74
+ generatedAt: new Date().toISOString(),
75
+ insights,
76
+ decision,
77
+ };
78
+ const facts = deriveScanNarrative(result);
79
+ return presentGitHubPrComment(facts, result);
80
+ }