@mergesignal/shared 0.2.2 → 0.2.4

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/README.md +37 -0
  2. package/dist/cardObservationCatalog.d.ts +55 -0
  3. package/dist/cardObservationCatalog.d.ts.map +1 -0
  4. package/dist/cardObservationCatalog.js +320 -0
  5. package/dist/cardSummaryCopy.d.ts +12 -0
  6. package/dist/cardSummaryCopy.d.ts.map +1 -0
  7. package/dist/cardSummaryCopy.js +36 -0
  8. package/dist/deriveCardDisplaySummary.d.ts +16 -0
  9. package/dist/deriveCardDisplaySummary.d.ts.map +1 -0
  10. package/dist/deriveCardDisplaySummary.js +56 -0
  11. package/dist/deriveCardDisplaySummary.test.d.ts +2 -0
  12. package/dist/deriveCardDisplaySummary.test.d.ts.map +1 -0
  13. package/dist/deriveCardDisplaySummary.test.js +89 -0
  14. package/dist/deriveCardOperationalObservations.d.ts +19 -0
  15. package/dist/deriveCardOperationalObservations.d.ts.map +1 -0
  16. package/dist/deriveCardOperationalObservations.js +180 -0
  17. package/dist/deriveCardOperationalObservations.test.d.ts +2 -0
  18. package/dist/deriveCardOperationalObservations.test.d.ts.map +1 -0
  19. package/dist/deriveCardOperationalObservations.test.js +268 -0
  20. package/dist/deriveRiskScoreSummary.d.ts +33 -0
  21. package/dist/deriveRiskScoreSummary.d.ts.map +1 -0
  22. package/dist/deriveRiskScoreSummary.js +90 -0
  23. package/dist/deriveRiskScoreSummary.test.d.ts +2 -0
  24. package/dist/deriveRiskScoreSummary.test.d.ts.map +1 -0
  25. package/dist/deriveRiskScoreSummary.test.js +47 -0
  26. package/dist/deriveScanDetailRecommendations.d.ts +63 -0
  27. package/dist/deriveScanDetailRecommendations.d.ts.map +1 -0
  28. package/dist/deriveScanDetailRecommendations.js +725 -0
  29. package/dist/deriveScanDetailRecommendations.test.d.ts +2 -0
  30. package/dist/deriveScanDetailRecommendations.test.d.ts.map +1 -0
  31. package/dist/deriveScanDetailRecommendations.test.js +181 -0
  32. package/dist/formatCardAreaLabels.d.ts +8 -0
  33. package/dist/formatCardAreaLabels.d.ts.map +1 -0
  34. package/dist/formatCardAreaLabels.js +65 -0
  35. package/dist/formatCardEvidenceCounts.d.ts +7 -0
  36. package/dist/formatCardEvidenceCounts.d.ts.map +1 -0
  37. package/dist/formatCardEvidenceCounts.js +19 -0
  38. package/dist/formatCardExposureDisplay.d.ts +24 -0
  39. package/dist/formatCardExposureDisplay.d.ts.map +1 -0
  40. package/dist/formatCardExposureDisplay.js +71 -0
  41. package/dist/formatCardExposureDisplay.test.d.ts +2 -0
  42. package/dist/formatCardExposureDisplay.test.d.ts.map +1 -0
  43. package/dist/formatCardExposureDisplay.test.js +36 -0
  44. package/dist/index.d.ts +12 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +12 -0
  47. package/dist/productMessaging.d.ts +85 -0
  48. package/dist/productMessaging.d.ts.map +1 -0
  49. package/dist/productMessaging.js +109 -0
  50. package/dist/productMessaging.test.d.ts +2 -0
  51. package/dist/productMessaging.test.d.ts.map +1 -0
  52. package/dist/productMessaging.test.js +97 -0
  53. package/dist/riskVocabulary.d.ts +8 -2
  54. package/dist/riskVocabulary.d.ts.map +1 -1
  55. package/dist/riskVocabulary.js +26 -9
  56. package/dist/scanCardSummary.d.ts +9 -0
  57. package/dist/scanCardSummary.d.ts.map +1 -1
  58. package/dist/scanCardSummary.js +19 -29
  59. package/dist/scanCardSummary.test.js +45 -8
  60. package/dist/scanDetailViewModel.d.ts +108 -0
  61. package/dist/scanDetailViewModel.d.ts.map +1 -0
  62. package/dist/scanDetailViewModel.js +420 -0
  63. package/dist/scanDetailViewModel.test.d.ts +2 -0
  64. package/dist/scanDetailViewModel.test.d.ts.map +1 -0
  65. package/dist/scanDetailViewModel.test.js +503 -0
  66. package/dist/scanSurfaceCopy.d.ts +187 -0
  67. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  68. package/dist/scanSurfaceCopy.js +190 -0
  69. package/dist/truncateCardSummary.d.ts +5 -0
  70. package/dist/truncateCardSummary.d.ts.map +1 -0
  71. package/dist/truncateCardSummary.js +23 -0
  72. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=deriveScanDetailRecommendations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveScanDetailRecommendations.test.d.ts","sourceRoot":"","sources":["../src/deriveScanDetailRecommendations.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,181 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { primaryPrExample } from "./productMessaging.js";
3
+ import { deriveScanDetailRecommendations, RECOMMENDATION_MAX_ITEMS, } from "./deriveScanDetailRecommendations.js";
4
+ import { deriveScanDetailViewModel } from "./scanDetailViewModel.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
+ function minimalResult(overrides = {}) {
17
+ return {
18
+ totalScore: 42,
19
+ layerScores: {
20
+ security: 10,
21
+ maintainability: 20,
22
+ ecosystem: 30,
23
+ upgradeImpact: 15,
24
+ },
25
+ findings: [],
26
+ generatedAt: new Date().toISOString(),
27
+ decision: {
28
+ recommendation: "needs_review",
29
+ confidence: "medium",
30
+ reasoning: ["Lockfile adds transitive depth on shared utilities."],
31
+ },
32
+ ...overrides,
33
+ };
34
+ }
35
+ describe("deriveScanDetailRecommendations", () => {
36
+ it("always returns 1–3 enriched items with detail fields", () => {
37
+ const archetypes = [
38
+ {
39
+ ...baseResult,
40
+ decision: { recommendation: "safe", confidence: "high", reasoning: [] },
41
+ },
42
+ minimalResult(),
43
+ {
44
+ ...minimalResult(),
45
+ decision: {
46
+ recommendation: "risky",
47
+ confidence: "high",
48
+ reasoning: [],
49
+ },
50
+ findings: [
51
+ {
52
+ id: "f1",
53
+ severity: "critical",
54
+ title: "Critical vulnerability",
55
+ description: "Transitive CVE",
56
+ packageName: "lodash",
57
+ recommendation: "Upgrade lodash",
58
+ },
59
+ ],
60
+ },
61
+ ];
62
+ for (const result of archetypes) {
63
+ const center = deriveScanDetailRecommendations(result);
64
+ expect(center.items.length).toBeGreaterThanOrEqual(1);
65
+ expect(center.items.length).toBeLessThanOrEqual(RECOMMENDATION_MAX_ITEMS);
66
+ expect(center.defaultSelectedId).toBe(center.items[0]?.id);
67
+ for (const item of center.items) {
68
+ expect(item.detail.why.length).toBeGreaterThan(0);
69
+ expect(item.detail.whyNow.length).toBeGreaterThan(0);
70
+ expect(item.detail.expectedBenefit.length).toBeGreaterThan(0);
71
+ expect(item.detail.why).not.toMatch(/CVE-\d|@\d|\d+\.\d+\.\d+/);
72
+ }
73
+ }
74
+ });
75
+ it("quiet safe scan uses posture playbook with scan context", () => {
76
+ const center = deriveScanDetailRecommendations({
77
+ ...baseResult,
78
+ decision: { recommendation: "safe", confidence: "high", reasoning: [] },
79
+ });
80
+ expect(center.posture).toBe("safe");
81
+ expect(center.scanContext).toBeTruthy();
82
+ expect(center.items[0]?.priority).toBe("low");
83
+ expect(center.items[0]?.title).toBe("Merge normally");
84
+ });
85
+ it("duplicate recommendation includes whyNow and signals with packages", () => {
86
+ const center = deriveScanDetailRecommendations({
87
+ ...minimalResult(),
88
+ recommendations: [
89
+ {
90
+ id: "r1",
91
+ title: "Reduce duplicate dependency versions",
92
+ rationale: "semver overlap",
93
+ impact: "high",
94
+ packages: ["semver", "string-width", "@opentelemetry/api-logs"],
95
+ },
96
+ ],
97
+ });
98
+ const dup = center.items.find((i) => i.title.toLowerCase().includes("duplicate"));
99
+ expect(dup?.detail.whyNow.toLowerCase()).toContain("semver");
100
+ expect(dup?.detail.signals.length).toBeGreaterThan(0);
101
+ expect(dup?.detail.affectedPackages?.names).toContain("semver");
102
+ });
103
+ it("insight-rich scan has high priority and scan-specific whyNow", () => {
104
+ const center = deriveScanDetailRecommendations({
105
+ ...minimalResult(),
106
+ insights: [
107
+ {
108
+ type: "behavioral_change",
109
+ priority: "high",
110
+ confidence: "confirmed",
111
+ scope: "changed",
112
+ message: primaryPrExample.message,
113
+ context: primaryPrExample.where[0],
114
+ remediation: "Confirm auth guards still run before protected handlers",
115
+ affectedFiles: [...primaryPrExample.where],
116
+ },
117
+ ],
118
+ });
119
+ expect(center.items[0]?.source).toBe("insight");
120
+ expect(center.items[0]?.priority).toBe("high");
121
+ expect(center.items[0]?.detail.whyNow).toContain("middleware");
122
+ });
123
+ it("risky scan with critical findings leads with resolve guidance", () => {
124
+ const center = deriveScanDetailRecommendations({
125
+ ...minimalResult(),
126
+ decision: { recommendation: "risky", confidence: "high", reasoning: [] },
127
+ findings: [
128
+ {
129
+ id: "f1",
130
+ severity: "critical",
131
+ title: "Critical vulnerability",
132
+ description: "Transitive CVE",
133
+ packageName: "lodash",
134
+ recommendation: "Upgrade lodash",
135
+ },
136
+ ],
137
+ });
138
+ expect(center.items[0]?.title).toContain("Resolve critical findings");
139
+ expect(center.items[0]?.priority).toBe("high");
140
+ expect(center.items[0]?.proofRefs?.findingIds).toContain("f1");
141
+ });
142
+ });
143
+ describe("recommendedActions on scan detail view model", () => {
144
+ it("includes recommendedActions slice on done scans", () => {
145
+ const vm = deriveScanDetailViewModel(minimalResult(), {
146
+ scanId: "s1",
147
+ status: "done",
148
+ });
149
+ expect(vm?.recommendedActions.items.length).toBeGreaterThanOrEqual(1);
150
+ });
151
+ it("strips duplicate finding recommendations covered by recommended actions", () => {
152
+ const vm = deriveScanDetailViewModel({
153
+ ...minimalResult(),
154
+ decision: {
155
+ recommendation: "risky",
156
+ confidence: "high",
157
+ reasoning: [],
158
+ },
159
+ findings: [
160
+ {
161
+ id: "f1",
162
+ severity: "critical",
163
+ title: "Critical vulnerability",
164
+ description: "Transitive CVE",
165
+ packageName: "lodash",
166
+ recommendation: "Upgrade lodash",
167
+ },
168
+ ],
169
+ }, { scanId: "dedupe", status: "done" });
170
+ const finding = vm?.evidence?.findings.find((f) => f.id === "f1");
171
+ expect(finding?.recommendation).toBeUndefined();
172
+ expect(finding?.coveredByRecommendationRank).toBeGreaterThan(0);
173
+ });
174
+ it("does not expose Act 4 next steps", () => {
175
+ const vm = deriveScanDetailViewModel(minimalResult(), {
176
+ scanId: "s1",
177
+ status: "done",
178
+ });
179
+ expect(vm).not.toHaveProperty("next");
180
+ });
181
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Filter and rank area labels for dashboard cards (max 2).
3
+ * Longer specific labels win; generic taxonomy is dropped.
4
+ */
5
+ export declare function formatCardAreaLabels(areas: string[] | null | undefined, max?: number): string[];
6
+ /** Join formatted areas for evidence row display. */
7
+ export declare function joinCardAreaLabels(areas: string[]): string | null;
8
+ //# sourceMappingURL=formatCardAreaLabels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCardAreaLabels.d.ts","sourceRoot":"","sources":["../src/formatCardAreaLabels.ts"],"names":[],"mappings":"AAmCA;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EAClC,GAAG,SAAI,GACN,MAAM,EAAE,CAmBV;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAIjE"}
@@ -0,0 +1,65 @@
1
+ import { GENERIC_CARD_AREA_PHRASES } from "./cardSummaryCopy.js";
2
+ const PATH_LIKE = /[/\\]|^packages\/|\.tsx?$|\.jsx?$|^src\//i;
3
+ const KEBAB_SNAKE = /^[a-z0-9]+[-_][a-z0-9_-]+$/;
4
+ function normalizeKey(label) {
5
+ return label.trim().toLowerCase();
6
+ }
7
+ function isTechnicalLabel(label) {
8
+ const t = label.trim();
9
+ if (!t)
10
+ return true;
11
+ if (PATH_LIKE.test(t))
12
+ return true;
13
+ if (KEBAB_SNAKE.test(t) && !/\s/.test(t))
14
+ return true;
15
+ return false;
16
+ }
17
+ function isGenericArea(label) {
18
+ const key = normalizeKey(label);
19
+ return GENERIC_CARD_AREA_PHRASES.some((g) => key === g || key.startsWith(`${g} `) || key.endsWith(` ${g}`));
20
+ }
21
+ function humanizeLabel(label) {
22
+ const clean = label.replace(/^(Finding:\s*|Area:\s*)/i, "").trim();
23
+ if (!clean)
24
+ return "";
25
+ if (/\s/.test(clean) || /^[A-Z]/.test(clean))
26
+ return clean;
27
+ return clean
28
+ .split(/[-_]/)
29
+ .filter(Boolean)
30
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
31
+ .join(" ");
32
+ }
33
+ /**
34
+ * Filter and rank area labels for dashboard cards (max 2).
35
+ * Longer specific labels win; generic taxonomy is dropped.
36
+ */
37
+ export function formatCardAreaLabels(areas, max = 2) {
38
+ if (!Array.isArray(areas) || areas.length === 0)
39
+ return [];
40
+ const seen = new Set();
41
+ const candidates = [];
42
+ for (const raw of areas) {
43
+ if (isTechnicalLabel(raw))
44
+ continue;
45
+ const label = humanizeLabel(raw);
46
+ if (!label || label.length < 4)
47
+ continue;
48
+ if (isGenericArea(label))
49
+ continue;
50
+ const key = normalizeKey(label);
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ candidates.push(label);
55
+ }
56
+ candidates.sort((a, b) => b.length - a.length);
57
+ return candidates.slice(0, max);
58
+ }
59
+ /** Join formatted areas for evidence row display. */
60
+ export function joinCardAreaLabels(areas) {
61
+ const formatted = formatCardAreaLabels(areas);
62
+ if (formatted.length === 0)
63
+ return null;
64
+ return formatted.join(" · ");
65
+ }
@@ -0,0 +1,7 @@
1
+ import type { FindingCountSummary } from "./scanCardSummary.js";
2
+ import type { MergePosture } from "./riskVocabulary.js";
3
+ /**
4
+ * One soft evidence phrase for dashboard cards (not scanner-style metrics).
5
+ */
6
+ export declare function formatCardEvidenceCounts(counts: FindingCountSummary | null | undefined, posture: MergePosture | null): string | null;
7
+ //# sourceMappingURL=formatCardEvidenceCounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCardEvidenceCounts.d.ts","sourceRoot":"","sources":["../src/formatCardEvidenceCounts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,EAC9C,OAAO,EAAE,YAAY,GAAG,IAAI,GAC3B,MAAM,GAAG,IAAI,CAaf"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * One soft evidence phrase for dashboard cards (not scanner-style metrics).
3
+ */
4
+ export function formatCardEvidenceCounts(counts, posture) {
5
+ if (!counts || !posture || posture === "safe")
6
+ return null;
7
+ const { critical, high } = counts;
8
+ if (critical > 0 && posture === "risky") {
9
+ if (critical === 1)
10
+ return "1 critical finding";
11
+ return "Critical findings present";
12
+ }
13
+ if (high > 0 && (posture === "risky" || posture === "needs_review")) {
14
+ if (high === 1)
15
+ return "1 high-severity finding";
16
+ return `${high} high-severity findings`;
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Dashboard card exposure semantics — display-only interpretation of totalScore.
3
+ * Not persisted; not an engine output; complements merge posture (Safe / Review / Risky).
4
+ */
5
+ export type CardExposureCategory = "minimal" | "limited" | "moderate" | "elevated" | "broad";
6
+ export type CardExposureDisplay = {
7
+ category: CardExposureCategory;
8
+ /** User-facing label, e.g. "Moderate exposure". */
9
+ label: string;
10
+ value: number;
11
+ };
12
+ /** User-facing exposure labels for docs and UI copy (low → high). */
13
+ export declare const CARD_EXPOSURE_CATEGORY_LABELS: readonly string[];
14
+ /** Map numeric totalScore to a fixed exposure category (card UX only). */
15
+ export declare function deriveCardExposureDisplay(score: number | null | undefined): CardExposureDisplay | null;
16
+ /** Compact card line: exposure category label only. */
17
+ export declare function formatCardExposureLine(score: number | null | undefined): string | null;
18
+ /** Fragment for composite aria labels on PR cards. */
19
+ export declare function exposureAriaFragment(score: number | null | undefined): string | null;
20
+ /** Detail-page reach chip labels (not "exposure" jargon). */
21
+ export declare const DETAIL_REACH_CHIP_LABEL: Record<CardExposureCategory, string>;
22
+ /** Reach chip for scan detail page — maps score band to reviewer-facing scope label. */
23
+ export declare function deriveDetailReachChip(score: number | null | undefined): string | null;
24
+ //# sourceMappingURL=formatCardExposureDisplay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCardExposureDisplay.d.ts","sourceRoot":"","sources":["../src/formatCardExposureDisplay.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,SAAS,GACT,UAAU,GACV,UAAU,GACV,OAAO,CAAC;AAEZ,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAUF,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAM1D,CAAC;AAkBF,0EAA0E;AAC1E,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,mBAAmB,GAAG,IAAI,CAM5B;AAED,uDAAuD;AACvD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAIf;AAED,sDAAsD;AACtD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAIf;AAED,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAMxE,CAAC;AAEF,wFAAwF;AACxF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAIf"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Dashboard card exposure semantics — display-only interpretation of totalScore.
3
+ * Not persisted; not an engine output; complements merge posture (Safe / Review / Risky).
4
+ */
5
+ const EXPOSURE_LABEL = {
6
+ minimal: "Minimal exposure",
7
+ limited: "Limited exposure",
8
+ moderate: "Moderate exposure",
9
+ elevated: "Elevated exposure",
10
+ broad: "Broad exposure",
11
+ };
12
+ /** User-facing exposure labels for docs and UI copy (low → high). */
13
+ export const CARD_EXPOSURE_CATEGORY_LABELS = [
14
+ EXPOSURE_LABEL.minimal,
15
+ EXPOSURE_LABEL.limited,
16
+ EXPOSURE_LABEL.moderate,
17
+ EXPOSURE_LABEL.elevated,
18
+ EXPOSURE_LABEL.broad,
19
+ ];
20
+ /** Inclusive upper bounds (0–100). Five stable buckets only. */
21
+ const EXPOSURE_UPPER_BOUND = [24, 44, 74, 89, 100];
22
+ const EXPOSURE_ORDER = [
23
+ "minimal",
24
+ "limited",
25
+ "moderate",
26
+ "elevated",
27
+ "broad",
28
+ ];
29
+ function clampScore(score) {
30
+ if (!Number.isFinite(score))
31
+ return 0;
32
+ return Math.min(100, Math.max(0, Math.round(score)));
33
+ }
34
+ /** Map numeric totalScore to a fixed exposure category (card UX only). */
35
+ export function deriveCardExposureDisplay(score) {
36
+ if (score == null || !Number.isFinite(score))
37
+ return null;
38
+ const value = clampScore(score);
39
+ const idx = EXPOSURE_UPPER_BOUND.findIndex((bound) => value <= bound);
40
+ const category = EXPOSURE_ORDER[idx >= 0 ? idx : EXPOSURE_ORDER.length - 1];
41
+ return { category, label: EXPOSURE_LABEL[category], value };
42
+ }
43
+ /** Compact card line: exposure category label only. */
44
+ export function formatCardExposureLine(score) {
45
+ const display = deriveCardExposureDisplay(score);
46
+ if (!display)
47
+ return null;
48
+ return display.label;
49
+ }
50
+ /** Fragment for composite aria labels on PR cards. */
51
+ export function exposureAriaFragment(score) {
52
+ const display = deriveCardExposureDisplay(score);
53
+ if (!display)
54
+ return null;
55
+ return display.label;
56
+ }
57
+ /** Detail-page reach chip labels (not "exposure" jargon). */
58
+ export const DETAIL_REACH_CHIP_LABEL = {
59
+ minimal: "Focused reach",
60
+ limited: "Limited reach",
61
+ moderate: "Moderate reach",
62
+ elevated: "Wide reach",
63
+ broad: "Wide reach",
64
+ };
65
+ /** Reach chip for scan detail page — maps score band to reviewer-facing scope label. */
66
+ export function deriveDetailReachChip(score) {
67
+ const display = deriveCardExposureDisplay(score);
68
+ if (!display)
69
+ return null;
70
+ return DETAIL_REACH_CHIP_LABEL[display.category];
71
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=formatCardExposureDisplay.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCardExposureDisplay.test.d.ts","sourceRoot":"","sources":["../src/formatCardExposureDisplay.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveCardExposureDisplay, exposureAriaFragment, formatCardExposureLine, } from "./formatCardExposureDisplay.js";
3
+ describe("deriveCardExposureDisplay", () => {
4
+ it("maps score to five fixed exposure buckets", () => {
5
+ expect(deriveCardExposureDisplay(0)?.category).toBe("minimal");
6
+ expect(deriveCardExposureDisplay(24)?.category).toBe("minimal");
7
+ expect(deriveCardExposureDisplay(25)?.category).toBe("limited");
8
+ expect(deriveCardExposureDisplay(44)?.category).toBe("limited");
9
+ expect(deriveCardExposureDisplay(45)?.category).toBe("moderate");
10
+ expect(deriveCardExposureDisplay(71)?.category).toBe("moderate");
11
+ expect(deriveCardExposureDisplay(74)?.category).toBe("moderate");
12
+ expect(deriveCardExposureDisplay(75)?.category).toBe("elevated");
13
+ expect(deriveCardExposureDisplay(89)?.category).toBe("elevated");
14
+ expect(deriveCardExposureDisplay(90)?.category).toBe("broad");
15
+ expect(deriveCardExposureDisplay(100)?.category).toBe("broad");
16
+ });
17
+ it("clamps and rounds non-finite input away", () => {
18
+ expect(deriveCardExposureDisplay(null)).toBeNull();
19
+ expect(deriveCardExposureDisplay(NaN)).toBeNull();
20
+ expect(deriveCardExposureDisplay(71.6)?.value).toBe(72);
21
+ expect(deriveCardExposureDisplay(150)?.value).toBe(100);
22
+ });
23
+ });
24
+ describe("formatCardExposureLine", () => {
25
+ it("returns exposure label only", () => {
26
+ expect(formatCardExposureLine(71)).toBe("Moderate exposure");
27
+ expect(formatCardExposureLine(12)).toBe("Minimal exposure");
28
+ expect(formatCardExposureLine(80)).toBe("Elevated exposure");
29
+ });
30
+ });
31
+ describe("exposureAriaFragment", () => {
32
+ it("returns exposure label only", () => {
33
+ expect(exposureAriaFragment(45)).toBe("Moderate exposure");
34
+ expect(exposureAriaFragment(80)).toBe("Elevated exposure");
35
+ });
36
+ });
package/dist/index.d.ts CHANGED
@@ -3,10 +3,22 @@ export * from "./formatInsight.js";
3
3
  export * from "./riskVocabulary.js";
4
4
  export * from "./selectTopAffectedAreas.js";
5
5
  export * from "./deriveScanSummaryText.js";
6
+ export * from "./cardSummaryCopy.js";
7
+ export * from "./truncateCardSummary.js";
8
+ export * from "./formatCardAreaLabels.js";
9
+ export * from "./formatCardEvidenceCounts.js";
10
+ export * from "./formatCardExposureDisplay.js";
11
+ export * from "./cardObservationCatalog.js";
12
+ export * from "./deriveCardOperationalObservations.js";
13
+ export * from "./deriveCardDisplaySummary.js";
6
14
  export * from "./scanCardSummary.js";
15
+ export * from "./deriveScanDetailRecommendations.js";
16
+ export * from "./deriveRiskScoreSummary.js";
17
+ export * from "./scanDetailViewModel.js";
7
18
  export * from "./scanResultSchema.js";
8
19
  export * from "./scanSurfaceCopy.js";
9
20
  export * from "./trustedScanGuards.js";
10
21
  export * from "./actionsStepSummary.js";
11
22
  export * from "./prCheckRunPresentation.js";
23
+ export * from "./productMessaging.js";
12
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sCAAsC,CAAC;AACrD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -3,9 +3,21 @@ export * from "./formatInsight.js";
3
3
  export * from "./riskVocabulary.js";
4
4
  export * from "./selectTopAffectedAreas.js";
5
5
  export * from "./deriveScanSummaryText.js";
6
+ export * from "./cardSummaryCopy.js";
7
+ export * from "./truncateCardSummary.js";
8
+ export * from "./formatCardAreaLabels.js";
9
+ export * from "./formatCardEvidenceCounts.js";
10
+ export * from "./formatCardExposureDisplay.js";
11
+ export * from "./cardObservationCatalog.js";
12
+ export * from "./deriveCardOperationalObservations.js";
13
+ export * from "./deriveCardDisplaySummary.js";
6
14
  export * from "./scanCardSummary.js";
15
+ export * from "./deriveScanDetailRecommendations.js";
16
+ export * from "./deriveRiskScoreSummary.js";
17
+ export * from "./scanDetailViewModel.js";
7
18
  export * from "./scanResultSchema.js";
8
19
  export * from "./scanSurfaceCopy.js";
9
20
  export * from "./trustedScanGuards.js";
10
21
  export * from "./actionsStepSummary.js";
11
22
  export * from "./prCheckRunPresentation.js";
23
+ export * from "./productMessaging.js";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Canonical marketing / category copy. Operational scan strings stay in scanSurfaceCopy.
3
+ */
4
+ export declare const productMessaging: {
5
+ readonly formalDefinition: "MergeSignal analyzes dependency upgrades, identifies affected application flows, and provides reviewer guidance before merge.";
6
+ readonly hero: {
7
+ readonly kicker: "Merge-decision intelligence for dependency upgrades";
8
+ readonly h1: "See what a dependency upgrade can break.";
9
+ readonly lead: "Dependency upgrade analysis that shows where runtime behavior may affect critical application flows.";
10
+ };
11
+ readonly seo: {
12
+ readonly title: "MergeSignal - know what an upgrade may break before merge";
13
+ readonly description: "Know what a dependency upgrade may affect in your app before merge. Affected flows, reviewer checks, and merge guidance for this PR.";
14
+ };
15
+ readonly finalCta: {
16
+ readonly headline: "Try it on your next upgrade PR";
17
+ readonly lead: "Run a local scan or add GitHub Actions to your workflow.";
18
+ readonly button: "Get started for free";
19
+ };
20
+ readonly homepageSections: {
21
+ readonly pain: {
22
+ readonly title: "Why dependency upgrades are hard to review";
23
+ readonly subhead: "Upgrading is easy. Knowing what it affects is not.";
24
+ readonly rows: readonly [{
25
+ readonly title: "Changelogs don't show real impact";
26
+ readonly body: "Release notes describe package changes, not how they affect your code paths and critical application flows.";
27
+ }, {
28
+ readonly title: "Too much noise, not enough clarity";
29
+ readonly body: "Security alerts rarely show what actually matters for this specific upgrade PR.";
30
+ }, {
31
+ readonly title: "Runtime impact is hard to reason about";
32
+ readonly body: "Understanding how a dependency change affects critical application flows requires deeper application context.";
33
+ }];
34
+ };
35
+ readonly focus: {
36
+ readonly title: "What MergeSignal focuses on";
37
+ readonly subhead: "Clearer upgrade decisions with fewer merge surprises";
38
+ readonly rows: readonly [{
39
+ readonly title: "What this upgrade affects";
40
+ readonly body: "Potential failure modes based on how your code uses the updated dependencies.";
41
+ }, {
42
+ readonly title: "Where it shows up";
43
+ readonly body: "The application flows, routes, and entrypoints affected by this upgrade.";
44
+ }, {
45
+ readonly title: "What to verify before merge";
46
+ readonly body: "Concrete reviewer checks for this specific upgrade.";
47
+ }];
48
+ };
49
+ readonly value: {
50
+ readonly title: "What changes when you use MergeSignal";
51
+ readonly subhead: "More focused reviews and targeted validation.";
52
+ readonly rows: readonly [{
53
+ readonly title: "Reviews move faster";
54
+ readonly body: "Start with affected flows instead of reconstructing impact from changelogs.";
55
+ }, {
56
+ readonly title: "Testing stays targeted";
57
+ readonly body: "Focus validation on the flows this upgrade affects.";
58
+ }, {
59
+ readonly title: "Fewer merge surprises";
60
+ readonly body: "Catch upgrade impact while the PR is still small.";
61
+ }, {
62
+ readonly title: "High-signal only";
63
+ readonly body: "MergeSignal only interrupts you when an upgrade deserves attention.";
64
+ }];
65
+ };
66
+ };
67
+ /** Repository dashboard PR cards — reviewer-oriented semantics (not engine/scoring docs). */
68
+ readonly dashboardPrCard: {
69
+ readonly intro: "The repository dashboard is a runtime-aware review surface for open upgrade pull requests: triage by merge recommendation, then read operational context on each card.";
70
+ readonly posture: `Posture indicates the merge recommendation (${string}, ${string}, ${string}).`;
71
+ readonly exposure: "Exposure indicates how broadly the upgrade touches runtime-relevant application areas.";
72
+ readonly exposureCategoriesLead: "On each scanned card, exposure reads as one of:";
73
+ readonly exposureCategories: readonly string[];
74
+ readonly cardBody: "Below the verdict, scanned cards surface up to three compact operational observations (transitive volume, blast radius, duplicate versions, and related topology signals) when the engine finds mappable structure.";
75
+ };
76
+ };
77
+ /** Illustrative scan-card example for homepage proof band and getting-started. */
78
+ export declare const primaryPrExample: {
79
+ readonly package: "express";
80
+ readonly message: "This upgrade affects middleware ordering used in auth routes.";
81
+ readonly where: readonly ["apps/api/src/middleware/auth.ts - auth guard depends on middleware order", "apps/api/src/routes/account.ts - protected handlers assume validation ran first"];
82
+ };
83
+ /** All homepage section row bodies for word-count validation. */
84
+ export declare function homepageSectionRowBodies(): string[];
85
+ //# sourceMappingURL=productMessaging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"productMessaging.d.ts","sourceRoot":"","sources":["../src/productMessaging.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmF3B,6FAA6F;;;;;;;;;CAYrF,CAAC;AAEX,kFAAkF;AAClF,eAAO,MAAM,gBAAgB;;;;CAOnB,CAAC;AAEX,iEAAiE;AACjE,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAOnD"}