@mergesignal/shared 0.2.2 → 0.2.3

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 (50) hide show
  1. package/dist/cardObservationCatalog.d.ts +55 -0
  2. package/dist/cardObservationCatalog.d.ts.map +1 -0
  3. package/dist/cardObservationCatalog.js +320 -0
  4. package/dist/cardSummaryCopy.d.ts +12 -0
  5. package/dist/cardSummaryCopy.d.ts.map +1 -0
  6. package/dist/cardSummaryCopy.js +36 -0
  7. package/dist/deriveCardDisplaySummary.d.ts +16 -0
  8. package/dist/deriveCardDisplaySummary.d.ts.map +1 -0
  9. package/dist/deriveCardDisplaySummary.js +56 -0
  10. package/dist/deriveCardDisplaySummary.test.d.ts +2 -0
  11. package/dist/deriveCardDisplaySummary.test.d.ts.map +1 -0
  12. package/dist/deriveCardDisplaySummary.test.js +89 -0
  13. package/dist/deriveCardOperationalObservations.d.ts +19 -0
  14. package/dist/deriveCardOperationalObservations.d.ts.map +1 -0
  15. package/dist/deriveCardOperationalObservations.js +180 -0
  16. package/dist/deriveCardOperationalObservations.test.d.ts +2 -0
  17. package/dist/deriveCardOperationalObservations.test.d.ts.map +1 -0
  18. package/dist/deriveCardOperationalObservations.test.js +268 -0
  19. package/dist/formatCardAreaLabels.d.ts +8 -0
  20. package/dist/formatCardAreaLabels.d.ts.map +1 -0
  21. package/dist/formatCardAreaLabels.js +65 -0
  22. package/dist/formatCardEvidenceCounts.d.ts +7 -0
  23. package/dist/formatCardEvidenceCounts.d.ts.map +1 -0
  24. package/dist/formatCardEvidenceCounts.js +19 -0
  25. package/dist/formatCardExposureDisplay.d.ts +20 -0
  26. package/dist/formatCardExposureDisplay.d.ts.map +1 -0
  27. package/dist/formatCardExposureDisplay.js +56 -0
  28. package/dist/formatCardExposureDisplay.test.d.ts +2 -0
  29. package/dist/formatCardExposureDisplay.test.d.ts.map +1 -0
  30. package/dist/formatCardExposureDisplay.test.js +36 -0
  31. package/dist/index.d.ts +9 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +9 -0
  34. package/dist/productMessaging.d.ts +85 -0
  35. package/dist/productMessaging.d.ts.map +1 -0
  36. package/dist/productMessaging.js +109 -0
  37. package/dist/productMessaging.test.d.ts +2 -0
  38. package/dist/productMessaging.test.d.ts.map +1 -0
  39. package/dist/productMessaging.test.js +97 -0
  40. package/dist/riskVocabulary.d.ts +8 -2
  41. package/dist/riskVocabulary.d.ts.map +1 -1
  42. package/dist/riskVocabulary.js +26 -9
  43. package/dist/scanCardSummary.d.ts +9 -0
  44. package/dist/scanCardSummary.d.ts.map +1 -1
  45. package/dist/scanCardSummary.js +19 -29
  46. package/dist/scanCardSummary.test.js +45 -8
  47. package/dist/truncateCardSummary.d.ts +5 -0
  48. package/dist/truncateCardSummary.d.ts.map +1 -0
  49. package/dist/truncateCardSummary.js +23 -0
  50. package/package.json +1 -1
@@ -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,19 @@ 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";
7
15
  export * from "./scanResultSchema.js";
8
16
  export * from "./scanSurfaceCopy.js";
9
17
  export * from "./trustedScanGuards.js";
10
18
  export * from "./actionsStepSummary.js";
11
19
  export * from "./prCheckRunPresentation.js";
20
+ export * from "./productMessaging.js";
12
21
  //# 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,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,18 @@ 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";
7
15
  export * from "./scanResultSchema.js";
8
16
  export * from "./scanSurfaceCopy.js";
9
17
  export * from "./trustedScanGuards.js";
10
18
  export * from "./actionsStepSummary.js";
11
19
  export * from "./prCheckRunPresentation.js";
20
+ 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"}
@@ -0,0 +1,109 @@
1
+ import { CARD_EXPOSURE_CATEGORY_LABELS } from "./formatCardExposureDisplay.js";
2
+ import { MERGE_POSTURE_LABEL } from "./riskVocabulary.js";
3
+ /**
4
+ * Canonical marketing / category copy. Operational scan strings stay in scanSurfaceCopy.
5
+ */
6
+ export const productMessaging = {
7
+ formalDefinition: "MergeSignal analyzes dependency upgrades, identifies affected application flows, and provides reviewer guidance before merge.",
8
+ hero: {
9
+ kicker: "Merge-decision intelligence for dependency upgrades",
10
+ h1: "See what a dependency upgrade can break.",
11
+ lead: "Dependency upgrade analysis that shows where runtime behavior may affect critical application flows.",
12
+ },
13
+ seo: {
14
+ title: "MergeSignal - know what an upgrade may break before merge",
15
+ description: "Know what a dependency upgrade may affect in your app before merge. Affected flows, reviewer checks, and merge guidance for this PR.",
16
+ },
17
+ finalCta: {
18
+ headline: "Try it on your next upgrade PR",
19
+ lead: "Run a local scan or add GitHub Actions to your workflow.",
20
+ button: "Get started for free",
21
+ },
22
+ homepageSections: {
23
+ pain: {
24
+ title: "Why dependency upgrades are hard to review",
25
+ subhead: "Upgrading is easy. Knowing what it affects is not.",
26
+ rows: [
27
+ {
28
+ title: "Changelogs don't show real impact",
29
+ body: "Release notes describe package changes, not how they affect your code paths and critical application flows.",
30
+ },
31
+ {
32
+ title: "Too much noise, not enough clarity",
33
+ body: "Security alerts rarely show what actually matters for this specific upgrade PR.",
34
+ },
35
+ {
36
+ title: "Runtime impact is hard to reason about",
37
+ body: "Understanding how a dependency change affects critical application flows requires deeper application context.",
38
+ },
39
+ ],
40
+ },
41
+ focus: {
42
+ title: "What MergeSignal focuses on",
43
+ subhead: "Clearer upgrade decisions with fewer merge surprises",
44
+ rows: [
45
+ {
46
+ title: "What this upgrade affects",
47
+ body: "Potential failure modes based on how your code uses the updated dependencies.",
48
+ },
49
+ {
50
+ title: "Where it shows up",
51
+ body: "The application flows, routes, and entrypoints affected by this upgrade.",
52
+ },
53
+ {
54
+ title: "What to verify before merge",
55
+ body: "Concrete reviewer checks for this specific upgrade.",
56
+ },
57
+ ],
58
+ },
59
+ value: {
60
+ title: "What changes when you use MergeSignal",
61
+ subhead: "More focused reviews and targeted validation.",
62
+ rows: [
63
+ {
64
+ title: "Reviews move faster",
65
+ body: "Start with affected flows instead of reconstructing impact from changelogs.",
66
+ },
67
+ {
68
+ title: "Testing stays targeted",
69
+ body: "Focus validation on the flows this upgrade affects.",
70
+ },
71
+ {
72
+ title: "Fewer merge surprises",
73
+ body: "Catch upgrade impact while the PR is still small.",
74
+ },
75
+ {
76
+ title: "High-signal only",
77
+ body: "MergeSignal only interrupts you when an upgrade deserves attention.",
78
+ },
79
+ ],
80
+ },
81
+ },
82
+ /** Repository dashboard PR cards — reviewer-oriented semantics (not engine/scoring docs). */
83
+ dashboardPrCard: {
84
+ 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.",
85
+ posture: `Posture indicates the merge recommendation (${MERGE_POSTURE_LABEL.safe}, ${MERGE_POSTURE_LABEL.needs_review}, ${MERGE_POSTURE_LABEL.risky}).`,
86
+ exposure: "Exposure indicates how broadly the upgrade touches runtime-relevant application areas.",
87
+ exposureCategoriesLead: "On each scanned card, exposure reads as one of:",
88
+ exposureCategories: CARD_EXPOSURE_CATEGORY_LABELS,
89
+ 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.",
90
+ },
91
+ };
92
+ /** Illustrative scan-card example for homepage proof band and getting-started. */
93
+ export const primaryPrExample = {
94
+ package: "express",
95
+ message: "This upgrade affects middleware ordering used in auth routes.",
96
+ where: [
97
+ "apps/api/src/middleware/auth.ts - auth guard depends on middleware order",
98
+ "apps/api/src/routes/account.ts - protected handlers assume validation ran first",
99
+ ],
100
+ };
101
+ /** All homepage section row bodies for word-count validation. */
102
+ export function homepageSectionRowBodies() {
103
+ const sections = productMessaging.homepageSections;
104
+ return [
105
+ ...sections.pain.rows,
106
+ ...sections.value.rows,
107
+ ...sections.focus.rows,
108
+ ].map((row) => row.body);
109
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=productMessaging.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"productMessaging.test.d.ts","sourceRoot":"","sources":["../src/productMessaging.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { homepageSectionRowBodies, primaryPrExample, productMessaging, } from "./productMessaging.js";
3
+ const LOCKED_H1 = "See what a dependency upgrade can break.";
4
+ const FORBIDDEN_MARKETING_TERMS = [
5
+ "runtime-aware",
6
+ "AI-powered",
7
+ "framework-aware",
8
+ "observability",
9
+ "exhaustive indexing",
10
+ "repo analytics",
11
+ "risk platform",
12
+ "risk intelligence platform",
13
+ "security analysis platform",
14
+ "scanner platform",
15
+ ];
16
+ describe("productMessaging", () => {
17
+ it("locks hero H1 exactly including trailing period", () => {
18
+ expect(productMessaging.hero.h1).toBe(LOCKED_H1);
19
+ expect(productMessaging.hero.h1.endsWith(".")).toBe(true);
20
+ });
21
+ it("keeps lead separate from H1", () => {
22
+ expect(productMessaging.hero.lead).not.toBe(productMessaging.hero.h1);
23
+ });
24
+ it("uses express middleware example for marketing card", () => {
25
+ expect(primaryPrExample.package).toBe("express");
26
+ expect(primaryPrExample.message).toContain("middleware ordering");
27
+ expect(primaryPrExample.where).toHaveLength(2);
28
+ });
29
+ it("uses engineering-native example domains", () => {
30
+ const text = [primaryPrExample.message, ...primaryPrExample.where].join(" ");
31
+ expect(text.toLowerCase()).toMatch(/auth|middleware|routes/);
32
+ });
33
+ it("keeps section row bodies under 20 words", () => {
34
+ for (const body of homepageSectionRowBodies()) {
35
+ const wordCount = body.trim().split(/\s+/).length;
36
+ expect(wordCount).toBeLessThanOrEqual(20);
37
+ }
38
+ });
39
+ it("avoids em dashes in marketing copy", () => {
40
+ const strings = collectMarketingStrings();
41
+ for (const s of strings) {
42
+ expect(s).not.toMatch(/[\u2013\u2014]/);
43
+ }
44
+ });
45
+ it("avoids forbidden marketing terms in hero and seo", () => {
46
+ const heroSeo = [
47
+ productMessaging.hero.kicker,
48
+ productMessaging.hero.h1,
49
+ productMessaging.hero.lead,
50
+ productMessaging.seo.title,
51
+ productMessaging.seo.description,
52
+ productMessaging.formalDefinition,
53
+ ].join(" ");
54
+ for (const term of FORBIDDEN_MARKETING_TERMS) {
55
+ expect(heroSeo.toLowerCase()).not.toContain(term.toLowerCase());
56
+ }
57
+ });
58
+ it("does not expose comparison hooks in hero exports", () => {
59
+ expect(productMessaging.hero.kicker).not.toContain("Dependabot");
60
+ expect(productMessaging.hero.h1).not.toContain("Dependabot");
61
+ expect(productMessaging.hero.lead).not.toContain("Dependabot");
62
+ });
63
+ it("describes dashboard posture and exposure without scoring jargon", () => {
64
+ const d = productMessaging.dashboardPrCard;
65
+ const text = [
66
+ d.intro,
67
+ d.posture,
68
+ d.exposure,
69
+ d.exposureCategoriesLead,
70
+ ...d.exposureCategories,
71
+ d.cardBody,
72
+ ].join(" ");
73
+ expect(text).toContain("merge recommendation");
74
+ expect(text).toContain("runtime-relevant");
75
+ expect(text).not.toMatch(/risk index|total score|0-100|formula|weight/i);
76
+ expect(d.exposureCategories).toHaveLength(5);
77
+ expect(d.exposureCategories).toContain("Elevated exposure");
78
+ });
79
+ });
80
+ function collectMarketingStrings() {
81
+ const out = [
82
+ productMessaging.formalDefinition,
83
+ productMessaging.hero.kicker,
84
+ productMessaging.hero.h1,
85
+ productMessaging.hero.lead,
86
+ productMessaging.seo.title,
87
+ productMessaging.seo.description,
88
+ primaryPrExample.message,
89
+ ...primaryPrExample.where,
90
+ ];
91
+ for (const body of homepageSectionRowBodies()) {
92
+ out.push(body);
93
+ }
94
+ const d = productMessaging.dashboardPrCard;
95
+ out.push(d.intro, d.posture, d.exposure, d.exposureCategoriesLead, d.cardBody, ...d.exposureCategories);
96
+ return out;
97
+ }
@@ -2,6 +2,12 @@ import type { PRDecisionRecommendation } from "./types.js";
2
2
  export type MergePosture = PRDecisionRecommendation;
3
3
  /** Canonical display strings for merge posture values. */
4
4
  export declare const MERGE_POSTURE_LABEL: Record<MergePosture, string>;
5
+ /**
6
+ * Dashboard card badge labels. Defaults to full canonical names for triage clarity.
7
+ * Wire values remain `safe | needs_review | risky`.
8
+ */
9
+ export declare const CARD_POSTURE_DISPLAY_LABEL: Record<MergePosture, string>;
10
+ export declare function cardPostureDisplayLabel(posture: MergePosture): string;
5
11
  /**
6
12
  * Sorting weights: higher number = listed first (riskiest first).
7
13
  * Use as: arr.sort((a, b) => MERGE_POSTURE_SORT_ORDER[b] - MERGE_POSTURE_SORT_ORDER[a])
@@ -16,8 +22,8 @@ export declare function mergePostureLabel(decision: string | null | undefined, f
16
22
  * e.g. "Risky, risk score 72" or "Safe"
17
23
  */
18
24
  export declare function ariaLabelForPosture(decision: string | null | undefined, score: number | null | undefined): string;
19
- /** Accessible label for unified PR card risk block. */
20
- export declare function ariaLabelForCardSummary(headline: string, riskIndex: number | null | undefined, summaryLine: string | null | undefined): string;
25
+ /** Accessible label for unified PR card findings block. */
26
+ export declare function ariaLabelForCardSummary(postureLabel: string, riskIndex: number | null | undefined, observations: string[] | null | undefined, supportingLine?: string | null): string;
21
27
  export type SignalSeverity = "low" | "medium" | "high";
22
28
  /**
23
29
  * Format a signal severity for prose use — always returns a qualified string
@@ -1 +1 @@
1
- {"version":3,"file":"riskVocabulary.d.ts","sourceRoot":"","sources":["../src/riskVocabulary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAkB3D,MAAM,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAEpD,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAI5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAIjE,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,YAAY,GAAG,IAAI,CASrB;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,QAAQ,SAAM,GACb,MAAM,CAGR;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,CAIR;AAED,uDAAuD;AACvD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,CAKR;AAMD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAQvD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG3E"}
1
+ {"version":3,"file":"riskVocabulary.d.ts","sourceRoot":"","sources":["../src/riskVocabulary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAkB3D,MAAM,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAEpD,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAI5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAEnE,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAErE;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAIjE,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,YAAY,GAAG,IAAI,CASrB;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,QAAQ,SAAM,GACb,MAAM,CAGR;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,CAKR;AAED,2DAA2D;AAC3D,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EACzC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,CASR;AAMD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAQvD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG3E"}
@@ -1,9 +1,20 @@
1
+ import { exposureAriaFragment } from "./formatCardExposureDisplay.js";
1
2
  /** Canonical display strings for merge posture values. */
2
3
  export const MERGE_POSTURE_LABEL = {
3
4
  safe: "Safe",
4
5
  needs_review: "Needs review",
5
6
  risky: "Risky",
6
7
  };
8
+ /**
9
+ * Dashboard card badge labels. Defaults to full canonical names for triage clarity.
10
+ * Wire values remain `safe | needs_review | risky`.
11
+ */
12
+ export const CARD_POSTURE_DISPLAY_LABEL = {
13
+ ...MERGE_POSTURE_LABEL,
14
+ };
15
+ export function cardPostureDisplayLabel(posture) {
16
+ return CARD_POSTURE_DISPLAY_LABEL[posture];
17
+ }
7
18
  /**
8
19
  * Sorting weights: higher number = listed first (riskiest first).
9
20
  * Use as: arr.sort((a, b) => MERGE_POSTURE_SORT_ORDER[b] - MERGE_POSTURE_SORT_ORDER[a])
@@ -33,17 +44,23 @@ export function mergePostureLabel(decision, fallback = "—") {
33
44
  */
34
45
  export function ariaLabelForPosture(decision, score) {
35
46
  const label = mergePostureLabel(decision);
36
- if (score != null)
37
- return `${label}, risk score ${Math.round(score)}`;
47
+ const exposure = exposureAriaFragment(score);
48
+ if (exposure)
49
+ return `${label}, ${exposure}`;
38
50
  return label;
39
51
  }
40
- /** Accessible label for unified PR card risk block. */
41
- export function ariaLabelForCardSummary(headline, riskIndex, summaryLine) {
42
- const parts = [headline];
43
- if (riskIndex != null)
44
- parts.push(`risk index ${Math.round(riskIndex)}`);
45
- if (summaryLine?.trim())
46
- parts.push(summaryLine.trim());
52
+ /** Accessible label for unified PR card findings block. */
53
+ export function ariaLabelForCardSummary(postureLabel, riskIndex, observations, supportingLine) {
54
+ const parts = [postureLabel];
55
+ const exposure = exposureAriaFragment(riskIndex);
56
+ if (exposure)
57
+ parts.push(exposure);
58
+ for (const obs of observations ?? []) {
59
+ if (obs?.trim())
60
+ parts.push(obs.trim());
61
+ }
62
+ if (supportingLine?.trim())
63
+ parts.push(supportingLine.trim());
47
64
  return parts.join(". ");
48
65
  }
49
66
  const SIGNAL_SEVERITY_LABEL = {
@@ -1,3 +1,4 @@
1
+ import type { CatalogPhrase } from "./cardObservationCatalog.js";
1
2
  import { type MergePosture } from "./riskVocabulary.js";
2
3
  import type { Finding, ScanResult } from "./types.js";
3
4
  export type ScanPipelineStatus = "queued" | "running" | "done" | "failed";
@@ -14,9 +15,15 @@ export type ScanCardSummary = {
14
15
  riskIndex: number | null;
15
16
  riskIndexBand: RiskIndexBand | null;
16
17
  headline: string;
18
+ /** @deprecated Use operationalObservations */
17
19
  summaryLine: string | null;
18
20
  findingCounts: FindingCountSummary | null;
21
+ /** @deprecated Use operationalObservations */
19
22
  topAffectedAreas: string[];
23
+ /** Catalog-mapped topology observations (0–3). */
24
+ operationalObservations: CatalogPhrase[];
25
+ /** Optional quiet subline when exactly one observation is shown. */
26
+ supportingLine: string | null;
20
27
  };
21
28
  export declare const SCAN_CARD_SCANNING_SUMMARY = "Waiting for results\u2026";
22
29
  export type PipelineCompletionEvidence = {
@@ -42,6 +49,8 @@ export declare function aggregateFindingCounts(findings: Finding[] | null | unde
42
49
  /**
43
50
  * Server-side card summary for PR dashboard list views.
44
51
  * Pipeline lifecycle copy is included when `pipelineStatus !== "done"`.
52
+ *
53
+ * Quiet safe cards with no mappable signals show posture + exposure only.
45
54
  */
46
55
  export declare function deriveScanCardSummary(result: ScanResult | null | undefined, pipelineStatus: ScanPipelineStatus): ScanCardSummary;
47
56
  /** Subline appended when scan results are stale (head SHA mismatch). */
@@ -1 +1 @@
1
- {"version":3,"file":"scanCardSummary.d.ts","sourceRoot":"","sources":["../src/scanCardSummary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scanCardSummary.d.ts","sourceRoot":"","sources":["../src/scanCardSummary.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,7 @@
1
- import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
1
+ import { deriveCardOperationalObservations } from "./deriveCardOperationalObservations.js";
2
2
  import { mergePostureFromDecision, MERGE_POSTURE_LABEL, } from "./riskVocabulary.js";
3
3
  import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
4
4
  import { selectTopAffectedAreas } from "./selectTopAffectedAreas.js";
5
- const SAFE_SUMMARY = "No merge blockers detected";
6
5
  const STALE_SUBLINE = "Based on earlier commit";
7
6
  export const SCAN_CARD_SCANNING_SUMMARY = "Waiting for results…";
8
7
  function normalizePipelineStatus(status) {
@@ -94,18 +93,11 @@ export function aggregateFindingCounts(findings) {
94
93
  }
95
94
  return counts;
96
95
  }
97
- function hasActionableFindings(counts) {
98
- return counts.critical + counts.high + counts.medium + counts.low > 0;
99
- }
100
- function deriveSafeSummaryLine(posture, counts, fallback) {
101
- if (posture === "safe" && !hasActionableFindings(counts)) {
102
- return SAFE_SUMMARY;
103
- }
104
- return fallback;
105
- }
106
96
  /**
107
97
  * Server-side card summary for PR dashboard list views.
108
98
  * Pipeline lifecycle copy is included when `pipelineStatus !== "done"`.
99
+ *
100
+ * Quiet safe cards with no mappable signals show posture + exposure only.
109
101
  */
110
102
  export function deriveScanCardSummary(result, pipelineStatus) {
111
103
  if (pipelineStatus === "queued" || pipelineStatus === "running") {
@@ -117,6 +109,8 @@ export function deriveScanCardSummary(result, pipelineStatus) {
117
109
  summaryLine: SCAN_CARD_SCANNING_SUMMARY,
118
110
  findingCounts: null,
119
111
  topAffectedAreas: [],
112
+ operationalObservations: [],
113
+ supportingLine: null,
120
114
  };
121
115
  }
122
116
  if (pipelineStatus === "failed") {
@@ -128,6 +122,8 @@ export function deriveScanCardSummary(result, pipelineStatus) {
128
122
  summaryLine: null,
129
123
  findingCounts: null,
130
124
  topAffectedAreas: [],
125
+ operationalObservations: [],
126
+ supportingLine: null,
131
127
  };
132
128
  }
133
129
  if (!result) {
@@ -139,27 +135,33 @@ export function deriveScanCardSummary(result, pipelineStatus) {
139
135
  summaryLine: null,
140
136
  findingCounts: null,
141
137
  topAffectedAreas: [],
138
+ operationalObservations: [],
139
+ supportingLine: null,
142
140
  };
143
141
  }
144
142
  const posture = mergePostureFromDecision(result.decision?.recommendation) ?? null;
145
143
  const riskIndex = typeof result.totalScore === "number" ? result.totalScore : null;
146
144
  const riskIndexBand = deriveRiskIndexBand(riskIndex);
147
145
  const findingCounts = aggregateFindingCounts(result.findings);
148
- const summaryFromText = deriveScanSummaryText(result);
149
146
  const headline = posture
150
147
  ? MERGE_POSTURE_LABEL[posture]
151
148
  : scanSurfaceCopy.checkRun.mergePostureUnavailable;
152
- const summaryLine = posture
153
- ? deriveSafeSummaryLine(posture, findingCounts, summaryFromText)
154
- : summaryFromText;
149
+ const topAffectedAreas = selectTopAffectedAreas(result, { max: 2 });
150
+ const { operationalObservations, supportingLine } = deriveCardOperationalObservations(result, {
151
+ mergePosture: posture,
152
+ hasFullResult: true,
153
+ max: 3,
154
+ });
155
155
  return {
156
156
  mergePosture: posture,
157
157
  riskIndex,
158
158
  riskIndexBand,
159
159
  headline,
160
- summaryLine,
160
+ summaryLine: null,
161
161
  findingCounts,
162
- topAffectedAreas: selectTopAffectedAreas(result, { max: 2 }),
162
+ topAffectedAreas,
163
+ operationalObservations,
164
+ supportingLine,
163
165
  };
164
166
  }
165
167
  /** Subline appended when scan results are stale (head SHA mismatch). */
@@ -187,18 +189,6 @@ export function deriveScanCardSummaryFromDenormalized(decision, totalScore, summ
187
189
  : undefined,
188
190
  };
189
191
  const summary = deriveScanCardSummary(minimalResult, "done");
190
- const sanitized = isPipelinePlaceholderCopy(summaryText)
191
- ? null
192
- : (summaryText?.trim() ?? null);
193
- if (sanitized && !summary.summaryLine) {
194
- return { ...summary, summaryLine: sanitized };
195
- }
196
- if (sanitized &&
197
- posture &&
198
- summary.summaryLine === SAFE_SUMMARY &&
199
- sanitized !== SAFE_SUMMARY) {
200
- return { ...summary, summaryLine: sanitized };
201
- }
202
192
  return summary;
203
193
  }
204
194
  /**