@mergesignal/shared 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/assessmentLabels.d.ts +1 -1
  2. package/dist/assessmentLabels.d.ts.map +1 -1
  3. package/dist/assessmentPresentationUtils.d.ts +6 -0
  4. package/dist/assessmentPresentationUtils.d.ts.map +1 -0
  5. package/dist/assessmentPresentationUtils.js +17 -0
  6. package/dist/assessmentProjection.d.ts +2 -2
  7. package/dist/assessmentProjection.d.ts.map +1 -1
  8. package/dist/assessmentProjection.js +42 -13
  9. package/dist/deriveScanNarrative.d.ts.map +1 -1
  10. package/dist/deriveScanNarrative.js +20 -1
  11. package/dist/deriveScanNarrative.test.js +13 -0
  12. package/dist/engineWireRegression.test.js +1 -1
  13. package/dist/fixtures/assessmentFixtures.d.ts +2 -1
  14. package/dist/fixtures/assessmentFixtures.d.ts.map +1 -1
  15. package/dist/fixtures/assessmentFixtures.js +78 -23
  16. package/dist/fixtures/assessmentScopeFixtures.d.ts +17 -0
  17. package/dist/fixtures/assessmentScopeFixtures.d.ts.map +1 -0
  18. package/dist/fixtures/assessmentScopeFixtures.js +42 -0
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/presentation/buildGitHubCheckRunOutput.d.ts +20 -0
  23. package/dist/presentation/buildGitHubCheckRunOutput.d.ts.map +1 -0
  24. package/dist/presentation/buildGitHubCheckRunOutput.js +42 -0
  25. package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts +2 -0
  26. package/dist/presentation/buildGitHubCheckRunOutput.test.d.ts.map +1 -0
  27. package/dist/presentation/buildGitHubCheckRunOutput.test.js +163 -0
  28. package/dist/presentation/compose/narrativeChannels.js +1 -1
  29. package/dist/presentation/dto/assessmentPresentationFields.d.ts +1 -2
  30. package/dist/presentation/dto/assessmentPresentationFields.d.ts.map +1 -1
  31. package/dist/presentation/index.d.ts +2 -1
  32. package/dist/presentation/index.d.ts.map +1 -1
  33. package/dist/presentation/index.js +2 -1
  34. package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts.map +1 -1
  35. package/dist/presentation/orchestration/buildScanPresentationBundle.js +12 -5
  36. package/dist/presentation/orchestration/buildScanPresentationBundle.test.d.ts +2 -0
  37. package/dist/presentation/orchestration/buildScanPresentationBundle.test.d.ts.map +1 -0
  38. package/dist/presentation/orchestration/buildScanPresentationBundle.test.js +99 -0
  39. package/dist/presentation/orchestration/scanPresentationBundle.d.ts +1 -1
  40. package/dist/presentation/orchestration/scanPresentationBundle.d.ts.map +1 -1
  41. package/dist/presentation/presentation.test.js +1 -1
  42. package/dist/presentation/presentationPersonas.test.js +0 -5
  43. package/dist/presentation/presenters/presentDashboardCard.d.ts +1 -0
  44. package/dist/presentation/presenters/presentDashboardCard.d.ts.map +1 -1
  45. package/dist/presentation/presenters/presentDashboardCard.js +16 -0
  46. package/dist/presentation/profile/buildProfileFromAssessment.d.ts +1 -1
  47. package/dist/presentation/profile/buildProfileFromAssessment.d.ts.map +1 -1
  48. package/dist/presentation/profile/buildProfileFromAssessment.js +13 -2
  49. package/dist/presentation/surfaceParity.test.js +73 -5
  50. package/dist/repoIntelligenceSchema.d.ts +3 -3
  51. package/dist/scanCardPresentationState.d.ts +8 -2
  52. package/dist/scanCardPresentationState.d.ts.map +1 -1
  53. package/dist/scanResultSchema.d.ts +53 -4
  54. package/dist/scanResultSchema.d.ts.map +1 -1
  55. package/dist/scanResultSchema.js +1 -1
  56. package/dist/scanResultSchema.test.js +26 -0
  57. package/dist/scanSurfaceCopy.d.ts +2 -0
  58. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  59. package/dist/scanSurfaceCopy.js +2 -0
  60. package/dist/types.d.ts +1 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +4 -2
  63. package/dist/assessmentSchema.d.ts +0 -155
  64. package/dist/assessmentSchema.d.ts.map +0 -1
  65. package/dist/assessmentSchema.js +0 -110
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { assessmentFastifyRuntime, assessmentTypescriptPatch, } from "../fixtures/assessmentFixtures.js";
3
+ import { withAssessmentScope, minimalReviewFocalPoint, emptyReachScope, emptyVerificationScope, reachScopeFor, } from "../fixtures/assessmentScopeFixtures.js";
4
+ import { buildScanPresentationBundle, presentDashboardCard } from "./index.js";
5
+ import { buildGitHubCheckRunOutput } from "./buildGitHubCheckRunOutput.js";
6
+ const safeAssessment = assessmentTypescriptPatch;
7
+ const baselineAssessment = withAssessmentScope({
8
+ posture: "safe",
9
+ confidence: "medium",
10
+ primaryConcern: "graph_baseline_only",
11
+ concerns: [
12
+ {
13
+ kind: "graph_baseline_only",
14
+ rank: 1,
15
+ evidenceRefs: ["fixture:baseline"],
16
+ },
17
+ ],
18
+ factors: ["graph_baseline_only"],
19
+ changeClasses: ["tooling_maintenance"],
20
+ presentation: {
21
+ narrativeIntensity: "minimal",
22
+ reachVisibility: "hidden",
23
+ verificationIntensity: "none",
24
+ insightEmissionFloor: "none",
25
+ reportMode: "lightweight_pr_graph_baseline",
26
+ },
27
+ }, {
28
+ reviewFocalPoint: minimalReviewFocalPoint(["@types/minimist"]),
29
+ reachScope: emptyReachScope(),
30
+ verificationScope: emptyVerificationScope(),
31
+ });
32
+ const needsReviewAssessment = assessmentFastifyRuntime;
33
+ const riskyAssessment = withAssessmentScope({
34
+ posture: "risky",
35
+ confidence: "high",
36
+ primaryConcern: "breaking_or_major",
37
+ concerns: [
38
+ {
39
+ kind: "breaking_or_major",
40
+ rank: 1,
41
+ packages: ["react"],
42
+ evidenceRefs: ["fixture:breaking"],
43
+ },
44
+ ],
45
+ factors: ["breaking_or_major"],
46
+ changeClasses: ["breaking_change"],
47
+ presentation: {
48
+ narrativeIntensity: "elevated",
49
+ reachVisibility: "prominent",
50
+ verificationIntensity: "required",
51
+ insightEmissionFloor: "full",
52
+ reportMode: "high_signal_pr",
53
+ },
54
+ }, {
55
+ reviewFocalPoint: minimalReviewFocalPoint(["react"]),
56
+ reachScope: reachScopeFor(["react"], "high"),
57
+ verificationScope: emptyVerificationScope(),
58
+ });
59
+ function assertNoLegacyCheckRunMarkers(summary) {
60
+ expect(summary).not.toContain("Risk index");
61
+ expect(summary).not.toContain("Layer scores");
62
+ expect(summary).not.toContain("<details>");
63
+ expect(summary).not.toMatch(/\d+\/100/);
64
+ }
65
+ function minimalScanResult(overrides = {}) {
66
+ return {
67
+ totalScore: 72,
68
+ layerScores: {
69
+ security: 10,
70
+ maintainability: 20,
71
+ ecosystem: 30,
72
+ upgradeImpact: 15,
73
+ },
74
+ findings: [],
75
+ methodologyVersion: "mergesignal-engine/test",
76
+ generatedAt: new Date().toISOString(),
77
+ assessment: safeAssessment,
78
+ decision: {
79
+ recommendation: "safe",
80
+ confidence: "medium",
81
+ reasoning: [],
82
+ },
83
+ ...overrides,
84
+ };
85
+ }
86
+ describe("buildGitHubCheckRunOutput", () => {
87
+ it("maps safe posture to success conclusion with scan link", () => {
88
+ const out = buildGitHubCheckRunOutput(minimalScanResult(), {
89
+ scanId: "scan-safe",
90
+ webAppOrigin: "https://app.test",
91
+ });
92
+ expect(out.conclusion).toBe("success");
93
+ expect(out.summary).toContain("/scan/scan-safe");
94
+ expect(out.summary).toContain("Full scan:");
95
+ assertNoLegacyCheckRunMarkers(out.summary);
96
+ });
97
+ it("maps needs_review posture to neutral conclusion", () => {
98
+ const out = buildGitHubCheckRunOutput(minimalScanResult({
99
+ assessment: needsReviewAssessment,
100
+ decision: {
101
+ recommendation: "needs_review",
102
+ confidence: "medium",
103
+ reasoning: ["Runtime usage confirmed"],
104
+ },
105
+ changedPackages: ["fastify"],
106
+ }), { scanId: "scan-review", webAppOrigin: "https://app.test" });
107
+ expect(out.conclusion).toBe("neutral");
108
+ expect(out.summary).toContain("/scan/scan-review");
109
+ assertNoLegacyCheckRunMarkers(out.summary);
110
+ });
111
+ it("maps risky posture to failure conclusion", () => {
112
+ const out = buildGitHubCheckRunOutput(minimalScanResult({
113
+ assessment: riskyAssessment,
114
+ decision: {
115
+ recommendation: "risky",
116
+ confidence: "high",
117
+ reasoning: ["Breaking change"],
118
+ },
119
+ }), { scanId: "scan-risky", webAppOrigin: "https://app.test" });
120
+ expect(out.conclusion).toBe("failure");
121
+ assertNoLegacyCheckRunMarkers(out.summary);
122
+ });
123
+ it("produces success conclusion for lightweight baseline presentation", () => {
124
+ const out = buildGitHubCheckRunOutput(minimalScanResult({
125
+ assessment: baselineAssessment,
126
+ reportPresentation: { mode: "lightweight_pr_graph_baseline" },
127
+ changedPackages: ["@types/minimist"],
128
+ decision: {
129
+ recommendation: "safe",
130
+ confidence: "medium",
131
+ reasoning: [],
132
+ },
133
+ }), { scanId: "scan-lw", webAppOrigin: "https://app.test" });
134
+ expect(out.conclusion).toBe("success");
135
+ expect(out.summary).toContain("/scan/scan-lw");
136
+ assertNoLegacyCheckRunMarkers(out.summary);
137
+ });
138
+ it("throws when assessment is missing", () => {
139
+ expect(() => buildGitHubCheckRunOutput(minimalScanResult({ assessment: undefined }), "scan-no-assessment")).toThrow(/assessment/i);
140
+ });
141
+ it("title matches dashboard card headline from the same bundle", () => {
142
+ const result = minimalScanResult({
143
+ assessment: needsReviewAssessment,
144
+ decision: {
145
+ recommendation: "needs_review",
146
+ confidence: "medium",
147
+ reasoning: ["Runtime usage confirmed"],
148
+ },
149
+ changedPackages: ["fastify"],
150
+ });
151
+ const out = buildGitHubCheckRunOutput(result, {
152
+ scanId: "scan-parity",
153
+ webAppOrigin: "https://app.test",
154
+ });
155
+ const bundle = buildScanPresentationBundle({
156
+ result,
157
+ pipelineStatus: "done",
158
+ });
159
+ expect(bundle).not.toBeNull();
160
+ const card = presentDashboardCard(bundle);
161
+ expect(out.title).toBe(card.headline);
162
+ });
163
+ });
@@ -19,7 +19,7 @@ export function buildNarrativeChannels(bundle) {
19
19
  const { assessment, presentation, profile, result } = bundle;
20
20
  const reachLabel = reachVisibilityLabel(presentation.reachVisibility);
21
21
  return {
22
- headline: formatAssessmentHeadline(assessment, result, profile.status),
22
+ headline: formatAssessmentHeadline(assessment, profile.status),
23
23
  limitedContextMessage: profile.degradedMessage ?? undefined,
24
24
  reachLabel,
25
25
  insights: projectInsightLines(assessment, result),
@@ -1,5 +1,4 @@
1
- import type { MergeConcernKind } from "../../assessmentSchema.js";
2
- import type { NarrativeIntensity, ReachVisibility } from "../../assessmentSchema.js";
1
+ import type { MergeConcernKind, NarrativeIntensity, ReachVisibility } from "@mergesignal/contracts";
3
2
  /** Assessment-aligned fields projected identically across surfaces. */
4
3
  export type AssessmentPresentationFields = {
5
4
  posture: "safe" | "needs_review" | "risky";
@@ -1 +1 @@
1
- {"version":3,"file":"assessmentPresentationFields.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/assessmentPresentationFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EAEhB,MAAM,2BAA2B,CAAC;AAEnC,uEAAuE;AACvE,MAAM,MAAM,4BAA4B,GAAG;IACzC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,OAAO,CAAC;IAC3C,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC,CAAC"}
1
+ {"version":3,"file":"assessmentPresentationFields.d.ts","sourceRoot":"","sources":["../../../src/presentation/dto/assessmentPresentationFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EAEhB,MAAM,wBAAwB,CAAC;AAEhC,uEAAuE;AACvE,MAAM,MAAM,4BAA4B,GAAG;IACzC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,OAAO,CAAC;IAC3C,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC,CAAC"}
@@ -14,13 +14,14 @@ export type { DashboardCardLayout } from "./dto/dashboardCardPresentation.js";
14
14
  export type { DashboardCardPresentation as ScanCardPresentation } from "./dto/dashboardCardPresentation.js";
15
15
  export type { ScanDetailsPresentation } from "./dto/scanDetailsPresentation.js";
16
16
  export type { GitHubCheckRunPresentation, GitHubPrCommentPresentation, CliScanPresentation, } from "./dto/githubAndCliPresentation.js";
17
- export { presentDashboardCard, presentPipelineDashboardCard, } from "./presenters/presentDashboardCard.js";
17
+ export { presentDashboardCard, presentPipelineDashboardCard, presentSurfacesIncompleteDashboardCard, } from "./presenters/presentDashboardCard.js";
18
18
  export { presentScanDetails } from "./presenters/presentScanDetails.js";
19
19
  export type { PresentScanDetailsContext } from "./presenters/presentScanDetails.js";
20
20
  export { presentGitHubCheckRun } from "./presenters/presentGitHubCheckRun.js";
21
21
  export { presentGitHubPrComment } from "./presenters/presentGitHubPrComment.js";
22
22
  export { presentCliScanSummary } from "./presenters/presentCliScanSummary.js";
23
23
  export { renderGitHubCheckRunMarkdown } from "./render/renderGitHubCheckRunMarkdown.js";
24
+ export { buildGitHubCheckRunOutput, resolveWebAppOrigin, type GitHubCheckRunConclusion, type GitHubCheckRunOutput, type BuildGitHubCheckRunOutputOptions, } from "./buildGitHubCheckRunOutput.js";
24
25
  export { renderGitHubPrCommentMarkdown } from "./render/renderGitHubPrCommentMarkdown.js";
25
26
  export { renderCliScanSummaryText } from "./render/renderCliScanSummaryText.js";
26
27
  export { buildNarrativeChannels, projectCompactKeyPoints, composeHeadline, composeSubheadline, composeKeyPoints, composeVerificationActions, composeAffectedAreaLabels, composeEvidenceRows, composeSupportingContext, evidenceContextFromProfile, } from "./compose/narrativeCompose.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/presentation/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,YAAY,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAC;AACvG,OAAO,EAAE,yBAAyB,EAAE,MAAM,8CAA8C,CAAC;AACzF,YAAY,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AACnG,OAAO,EAAE,4BAA4B,EAAE,MAAM,iDAAiD,CAAC;AAC/F,YAAY,EAAE,iCAAiC,EAAE,MAAM,iDAAiD,CAAC;AAEzG,YAAY,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,YAAY,EACV,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACpF,YAAY,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,gDAAgD;AAChD,YAAY,EAAE,yBAAyB,IAAI,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC5G,YAAY,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAChF,YAAY,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACxF,OAAO,EAAE,6BAA6B,EAAE,MAAM,2CAA2C,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/presentation/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,YAAY,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAC;AACvG,OAAO,EAAE,yBAAyB,EAAE,MAAM,8CAA8C,CAAC;AACzF,YAAY,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AACnG,OAAO,EAAE,4BAA4B,EAAE,MAAM,iDAAiD,CAAC;AAC/F,YAAY,EAAE,iCAAiC,EAAE,MAAM,iDAAiD,CAAC;AAEzG,YAAY,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,YAAY,EACV,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACpF,YAAY,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,gDAAgD;AAChD,YAAY,EAAE,yBAAyB,IAAI,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC5G,YAAY,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAChF,YAAY,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,sCAAsC,GACvC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACxF,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,gCAAgC,GACtC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,6BAA6B,EAAE,MAAM,2CAA2C,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,+BAA+B,CAAC"}
@@ -1,12 +1,13 @@
1
1
  export { buildScanPresentationBundle } from "./orchestration/buildScanPresentationBundle.js";
2
2
  export { buildScanCardPresentation } from "./orchestration/buildScanCardPresentation.js";
3
3
  export { buildScanDetailsPresentation } from "./orchestration/buildScanDetailsPresentation.js";
4
- export { presentDashboardCard, presentPipelineDashboardCard, } from "./presenters/presentDashboardCard.js";
4
+ export { presentDashboardCard, presentPipelineDashboardCard, presentSurfacesIncompleteDashboardCard, } from "./presenters/presentDashboardCard.js";
5
5
  export { presentScanDetails } from "./presenters/presentScanDetails.js";
6
6
  export { presentGitHubCheckRun } from "./presenters/presentGitHubCheckRun.js";
7
7
  export { presentGitHubPrComment } from "./presenters/presentGitHubPrComment.js";
8
8
  export { presentCliScanSummary } from "./presenters/presentCliScanSummary.js";
9
9
  export { renderGitHubCheckRunMarkdown } from "./render/renderGitHubCheckRunMarkdown.js";
10
+ export { buildGitHubCheckRunOutput, resolveWebAppOrigin, } from "./buildGitHubCheckRunOutput.js";
10
11
  export { renderGitHubPrCommentMarkdown } from "./render/renderGitHubPrCommentMarkdown.js";
11
12
  export { renderCliScanSummaryText } from "./render/renderCliScanSummaryText.js";
12
13
  export { buildNarrativeChannels, projectCompactKeyPoints, composeHeadline, composeSubheadline, composeKeyPoints, composeVerificationActions, composeAffectedAreaLabels, composeEvidenceRows, composeSupportingContext, evidenceContextFromProfile, } from "./compose/narrativeCompose.js";
@@ -1 +1 @@
1
- {"version":3,"file":"buildScanPresentationBundle.d.ts","sourceRoot":"","sources":["../../../src/presentation/orchestration/buildScanPresentationBundle.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,MAAM,MAAM,gCAAgC,GAAG;IAC7C,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gCAAgC,GACtC,sBAAsB,GAAG,IAAI,CAsB/B"}
1
+ {"version":3,"file":"buildScanPresentationBundle.d.ts","sourceRoot":"","sources":["../../../src/presentation/orchestration/buildScanPresentationBundle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,MAAM,MAAM,gCAAgC,GAAG;IAC7C,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gCAAgC,GACtC,sBAAsB,GAAG,IAAI,CA4B/B"}
@@ -1,4 +1,5 @@
1
- import { parseAssessmentOrThrow, toPublicPresentation, } from "../../assessmentSchema.js";
1
+ import { parseAssessmentOrThrow } from "@mergesignal/contracts";
2
+ import { toPublicPresentation } from "../../assessmentPresentationUtils.js";
2
3
  import { deriveScanNarrative } from "../../deriveScanNarrative.js";
3
4
  import { buildProfileFromAssessment } from "../profile/buildProfileFromAssessment.js";
4
5
  /**
@@ -11,12 +12,18 @@ export function buildScanPresentationBundle(input) {
11
12
  const assessment = input.result.assessment;
12
13
  if (!assessment)
13
14
  return null;
14
- parseAssessmentOrThrow(assessment);
15
- const presentation = toPublicPresentation(assessment.presentation);
16
- const profile = buildProfileFromAssessment(assessment, presentation, input.result);
15
+ let normalized;
16
+ try {
17
+ normalized = parseAssessmentOrThrow(assessment);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ const presentation = toPublicPresentation(normalized.presentation);
23
+ const profile = buildProfileFromAssessment(normalized, presentation, input.result);
17
24
  const facts = deriveScanNarrative(input.result);
18
25
  return {
19
- assessment,
26
+ assessment: normalized,
20
27
  presentation,
21
28
  profile,
22
29
  facts,
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=buildScanPresentationBundle.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildScanPresentationBundle.test.d.ts","sourceRoot":"","sources":["../../../src/presentation/orchestration/buildScanPresentationBundle.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,99 @@
1
+ import { parseAssessmentOrThrow } from "@mergesignal/contracts";
2
+ import { describe, expect, it } from "vitest";
3
+ import { assessmentTypescriptPatch } from "../../fixtures/assessmentFixtures.js";
4
+ import { buildScanCardPresentation } from "./buildScanCardPresentation.js";
5
+ import { buildScanPresentationBundle } from "./buildScanPresentationBundle.js";
6
+ const abi1Assessment = {
7
+ posture: "safe",
8
+ confidence: "high",
9
+ primaryConcern: null,
10
+ concerns: [],
11
+ factors: ["tooling_maintenance"],
12
+ changeClasses: ["tooling_maintenance"],
13
+ presentation: {
14
+ narrativeIntensity: "minimal",
15
+ reachVisibility: "hidden",
16
+ verificationIntensity: "advisory",
17
+ insightEmissionFloor: "none",
18
+ reportMode: "high_signal_pr",
19
+ },
20
+ };
21
+ function minimalScanResult(assessment) {
22
+ return {
23
+ totalScore: 42,
24
+ layerScores: {
25
+ security: 10,
26
+ maintainability: 20,
27
+ ecosystem: 30,
28
+ upgradeImpact: 40,
29
+ },
30
+ findings: [],
31
+ generatedAt: "2026-01-01T00:00:00.000Z",
32
+ decision: {
33
+ recommendation: "safe",
34
+ confidence: "high",
35
+ reasoning: [],
36
+ },
37
+ assessment,
38
+ };
39
+ }
40
+ describe("buildScanPresentationBundle (presentation boundary)", () => {
41
+ it("ABI-2 assessment produces a bundle", () => {
42
+ const result = minimalScanResult(assessmentTypescriptPatch);
43
+ const bundle = buildScanPresentationBundle({
44
+ result,
45
+ pipelineStatus: "done",
46
+ });
47
+ expect(bundle).not.toBeNull();
48
+ expect(bundle.assessment.reviewFocalPoint.anchors).toContain("typescript");
49
+ });
50
+ it("ABI-2 assessment renders a dashboard card headline", () => {
51
+ const result = minimalScanResult(assessmentTypescriptPatch);
52
+ const card = buildScanCardPresentation({
53
+ pipelineStatus: "done",
54
+ result,
55
+ });
56
+ expect(card.headline.length).toBeGreaterThan(0);
57
+ });
58
+ it("ABI-1 assessment returns null without throwing", () => {
59
+ const result = minimalScanResult(abi1Assessment);
60
+ expect(() => buildScanPresentationBundle({
61
+ result,
62
+ pipelineStatus: "done",
63
+ })).not.toThrow();
64
+ expect(buildScanPresentationBundle({
65
+ result,
66
+ pipelineStatus: "done",
67
+ })).toBeNull();
68
+ });
69
+ it("ABI-1 assessment does not mutate stored assessment JSON", () => {
70
+ const result = minimalScanResult(structuredClone(abi1Assessment));
71
+ const before = JSON.stringify(result.assessment);
72
+ buildScanPresentationBundle({ result, pipelineStatus: "done" });
73
+ expect(JSON.stringify(result.assessment)).toBe(before);
74
+ expect(result.assessment).not.toHaveProperty("reviewFocalPoint");
75
+ });
76
+ it("ABI-1 direct parseAssessmentOrThrow still throws (contracts strict)", () => {
77
+ expect(() => parseAssessmentOrThrow(abi1Assessment)).toThrow();
78
+ });
79
+ it("mixed PR index: ABI-2 ok and ABI-1 degraded without throw", () => {
80
+ const abi2 = minimalScanResult(assessmentTypescriptPatch);
81
+ const abi1 = minimalScanResult(abi1Assessment);
82
+ for (const result of [abi2, abi1]) {
83
+ expect(() => buildScanCardPresentation({
84
+ pipelineStatus: "done",
85
+ result,
86
+ })).not.toThrow();
87
+ }
88
+ const abi2Card = buildScanCardPresentation({
89
+ pipelineStatus: "done",
90
+ result: abi2,
91
+ });
92
+ const abi1Card = buildScanCardPresentation({
93
+ pipelineStatus: "done",
94
+ result: abi1,
95
+ });
96
+ expect(abi2Card.headline.length).toBeGreaterThan(0);
97
+ expect(abi1Card.pipeline?.status).toBe("failed");
98
+ });
99
+ });
@@ -1,4 +1,4 @@
1
- import type { Assessment, AssessmentPresentationPublic } from "../../assessmentSchema.js";
1
+ import type { Assessment, AssessmentPresentationPublic } from "@mergesignal/contracts";
2
2
  import type { ScanNarrativeFacts } from "../../scanNarrativeFacts.js";
3
3
  import type { ScanResult } from "../../types.js";
4
4
  import type { PresentationProfile } from "../profile/presentationProfile.js";
@@ -1 +1 @@
1
- {"version":3,"file":"scanPresentationBundle.d.ts","sourceRoot":"","sources":["../../../src/presentation/orchestration/scanPresentationBundle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,4BAA4B,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;;OAGG;IACH,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC"}
1
+ {"version":3,"file":"scanPresentationBundle.d.ts","sourceRoot":"","sources":["../../../src/presentation/orchestration/scanPresentationBundle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,4BAA4B,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;;OAGG;IACH,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC"}
@@ -66,7 +66,7 @@ describe("presentDashboardCard personas", () => {
66
66
  expect(card.verdict?.posture).toBe("safe");
67
67
  expect(card.headline).toMatch(/patch upgrade/i);
68
68
  expect(card.headline).not.toMatch(/needs review/i);
69
- expect(card.verification.length).toBeGreaterThan(0);
69
+ expect(card.verification.length).toBe(0);
70
70
  });
71
71
  });
72
72
  describe("presentation parity across surfaces", () => {
@@ -16,8 +16,6 @@ const personas = [
16
16
  density: "minimal",
17
17
  headlineMatch: /patch upgrade/i,
18
18
  headlineMustNot: /needs review/i,
19
- verificationIncludes: ["CI", "typecheck"],
20
- verificationMustNot: /auth flow/i,
21
19
  },
22
20
  {
23
21
  name: "eslint",
@@ -27,7 +25,6 @@ const personas = [
27
25
  density: "minimal",
28
26
  headlineMatch: /patch upgrade/i,
29
27
  headlineMustNot: /needs review/i,
30
- verificationIncludes: ["ci", "lint"],
31
28
  },
32
29
  {
33
30
  name: "prettier",
@@ -37,7 +34,6 @@ const personas = [
37
34
  density: "minimal",
38
35
  headlineMatch: /patch upgrade/i,
39
36
  headlineMustNot: /needs review/i,
40
- verificationIncludes: ["format"],
41
37
  },
42
38
  {
43
39
  name: "vitest",
@@ -47,7 +43,6 @@ const personas = [
47
43
  density: "minimal",
48
44
  headlineMatch: /patch upgrade|dependency upgrade/i,
49
45
  headlineMustNot: /needs review/i,
50
- verificationIncludes: ["test"],
51
46
  },
52
47
  {
53
48
  name: "fastify runtime",
@@ -1,5 +1,6 @@
1
1
  import type { DashboardCardPresentation } from "../dto/dashboardCardPresentation.js";
2
2
  import type { ScanPresentationBundle } from "../orchestration/scanPresentationBundle.js";
3
3
  export declare function presentDashboardCard(bundle: ScanPresentationBundle): DashboardCardPresentation;
4
+ export declare function presentSurfacesIncompleteDashboardCard(): DashboardCardPresentation;
4
5
  export declare function presentPipelineDashboardCard(pipelineStatus: "queued" | "running" | "failed"): DashboardCardPresentation;
5
6
  //# sourceMappingURL=presentDashboardCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"presentDashboardCard.d.ts","sourceRoot":"","sources":["../../../src/presentation/presenters/presentDashboardCard.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAoEzF,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,sBAAsB,GAC7B,yBAAyB,CA8C3B;AAED,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAC9C,yBAAyB,CAqB3B"}
1
+ {"version":3,"file":"presentDashboardCard.d.ts","sourceRoot":"","sources":["../../../src/presentation/presenters/presentDashboardCard.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAoEzF,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,sBAAsB,GAC7B,yBAAyB,CA8C3B;AAED,wBAAgB,sCAAsC,IAAI,yBAAyB,CAelF;AAED,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAC9C,yBAAyB,CAqB3B"}
@@ -96,6 +96,22 @@ export function presentDashboardCard(bundle) {
96
96
  };
97
97
  return normalizeDashboardCard(card);
98
98
  }
99
+ export function presentSurfacesIncompleteDashboardCard() {
100
+ const headline = scanSurfaceCopy.pipeline.surfacesNotSynchronized;
101
+ return normalizeDashboardCard({
102
+ pipeline: {
103
+ status: "failed",
104
+ headline,
105
+ subheadline: scanSurfaceCopy.pipeline.surfacesNotSynchronizedDetail,
106
+ },
107
+ headline,
108
+ insights: [],
109
+ verification: [],
110
+ layout: "standard",
111
+ detailActionLabel: scanSurfaceCopy.presentation.actionLabelReview,
112
+ sortKey: { postureRank: -1, riskIndex: -1 },
113
+ });
114
+ }
99
115
  export function presentPipelineDashboardCard(pipelineStatus) {
100
116
  const isRunning = pipelineStatus === "queued" || pipelineStatus === "running";
101
117
  const headline = isRunning
@@ -1,4 +1,4 @@
1
- import type { Assessment, AssessmentPresentationPublic } from "../../assessmentSchema.js";
1
+ import type { Assessment, AssessmentPresentationPublic } from "@mergesignal/contracts";
2
2
  import type { ScanResult } from "../../types.js";
3
3
  import type { PresentationProfile } from "./presentationProfile.js";
4
4
  export declare function buildProfileFromAssessment(assessment: Assessment, presentation: AssessmentPresentationPublic, result: ScanResult): PresentationProfile;
@@ -1 +1 @@
1
- {"version":3,"file":"buildProfileFromAssessment.d.ts","sourceRoot":"","sources":["../../../src/presentation/profile/buildProfileFromAssessment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC7B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAwCpE,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,4BAA4B,EAC1C,MAAM,EAAE,UAAU,GACjB,mBAAmB,CAuBrB"}
1
+ {"version":3,"file":"buildProfileFromAssessment.d.ts","sourceRoot":"","sources":["../../../src/presentation/profile/buildProfileFromAssessment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC7B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAiDpE,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,4BAA4B,EAC1C,MAAM,EAAE,UAAU,GACjB,mBAAmB,CAuBrB"}
@@ -1,10 +1,21 @@
1
- import { narrativeIntensityToDensity } from "../../assessmentSchema.js";
1
+ import { narrativeIntensityToDensity } from "../../assessmentPresentationUtils.js";
2
2
  import { safeParseRepoIntelligence } from "../../repoIntelligenceSchema.js";
3
3
  import { scanSurfaceCopy } from "../../scanSurfaceCopy.js";
4
4
  import { collectVerificationFocus } from "../../assessmentProjection.js";
5
+ function focalAnchorPackage(assessment) {
6
+ const anchors = assessment.reviewFocalPoint.anchors;
7
+ if (!anchors.length)
8
+ return null;
9
+ const token = anchors[0];
10
+ if (token === "dependency_graph")
11
+ return null;
12
+ if (token.includes("+"))
13
+ return token.split("+")[0];
14
+ return token;
15
+ }
5
16
  function buildInterpretation(assessment, presentation, result) {
6
17
  const verificationLabels = collectVerificationFocus(result);
7
- const anchorPackage = result.changedPackages?.[0] ?? null;
18
+ const anchorPackage = focalAnchorPackage(assessment);
8
19
  const suppressRuntimeNarrative = presentation.reachVisibility === "hidden";
9
20
  let intent = "unknown_upgrade";
10
21
  if (assessment.primaryConcern === "insufficient_evidence") {
@@ -8,14 +8,34 @@ import { renderGitHubCheckRunMarkdown } from "./render/renderGitHubCheckRunMarkd
8
8
  import { renderGitHubPrCommentMarkdown } from "./render/renderGitHubPrCommentMarkdown.js";
9
9
  import { scanResultBullmq, scanResultEslint, scanResultFastifyRuntime, scanResultMixedTypescriptFastify, scanResultPrettier, scanResultTypescriptPatch, scanResultVitest, } from "./fixtures/presentationPersonaFixtures.js";
10
10
  import { projectAssessmentFields } from "./projectAssessmentFields.js";
11
+ import { buildNarrativeChannels } from "./compose/narrativeChannels.js";
11
12
  const SCAN_ID = "22222222-2222-4222-8222-222222222222";
12
13
  const ORIGIN = "https://app.example.com";
14
+ const validationPersonasCore = [
15
+ {
16
+ id: "validation-pr-26",
17
+ name: "PR #26 mixed",
18
+ result: scanResultMixedTypescriptFastify,
19
+ },
20
+ {
21
+ id: "validation-pr-27",
22
+ name: "PR #27 fastify",
23
+ result: scanResultFastifyRuntime,
24
+ },
25
+ {
26
+ id: "validation-pr-28",
27
+ name: "PR #28 typescript",
28
+ result: scanResultTypescriptPatch,
29
+ },
30
+ {
31
+ id: "validation-pr-29",
32
+ name: "PR #29 prettier",
33
+ result: scanResultPrettier,
34
+ },
35
+ { id: "validation-pr-30", name: "PR #30 bullmq", result: scanResultBullmq },
36
+ ];
13
37
  const validationPersonas = [
14
- { name: "PR #26 mixed", result: scanResultMixedTypescriptFastify },
15
- { name: "PR #27 fastify", result: scanResultFastifyRuntime },
16
- { name: "PR #28 typescript", result: scanResultTypescriptPatch },
17
- { name: "PR #29 prettier", result: scanResultPrettier },
18
- { name: "PR #30 bullmq", result: scanResultBullmq },
38
+ ...validationPersonasCore,
19
39
  { name: "eslint", result: scanResultEslint },
20
40
  { name: "vitest", result: scanResultVitest },
21
41
  ];
@@ -71,4 +91,52 @@ describe("surfaceParity guardrail", () => {
71
91
  expect(s.details.hero.headline).toBe(s.card.headline);
72
92
  });
73
93
  }
94
+ it("validation personas #26–#30: pairwise distinct reviewFocalPoint and headlines", () => {
95
+ const focalPoints = [];
96
+ const headlines = [];
97
+ for (const persona of validationPersonasCore) {
98
+ const s = surfacesFor(persona.result);
99
+ focalPoints.push(persona.result.assessment.reviewFocalPoint);
100
+ headlines.push(s.card.headline);
101
+ expect(s.check.title).toBe(s.card.headline);
102
+ }
103
+ for (let i = 0; i < headlines.length; i++) {
104
+ for (let j = i + 1; j < headlines.length; j++) {
105
+ expect(headlines[i]).not.toBe(headlines[j]);
106
+ }
107
+ }
108
+ for (let i = 0; i < focalPoints.length; i++) {
109
+ for (let j = i + 1; j < focalPoints.length; j++) {
110
+ expect(focalPoints[i]).not.toEqual(focalPoints[j]);
111
+ }
112
+ }
113
+ });
114
+ it("PR #26: headline uses focal fastify, not changedPackages[0] typescript", () => {
115
+ const result = scanResultMixedTypescriptFastify;
116
+ expect(result.changedPackages?.[0]).toBe("typescript");
117
+ const s = surfacesFor(result);
118
+ expect(s.card.headline.toLowerCase()).toContain("fastify");
119
+ expect(s.card.headline.toLowerCase()).not.toContain("typescript");
120
+ });
121
+ it("validation personas #28–#30: reach and verification channels differ by scope", () => {
122
+ const channelsById = new Map(validationPersonasCore.map((p) => {
123
+ const bundle = buildScanPresentationBundle({
124
+ result: p.result,
125
+ pipelineStatus: "done",
126
+ });
127
+ return [p.id, buildNarrativeChannels(bundle)];
128
+ }));
129
+ const ch28 = channelsById.get("validation-pr-28");
130
+ const ch29 = channelsById.get("validation-pr-29");
131
+ const ch27 = channelsById.get("validation-pr-27");
132
+ const ch30 = channelsById.get("validation-pr-30");
133
+ const ch26 = channelsById.get("validation-pr-26");
134
+ expect(ch28.reachLabel).toBeUndefined();
135
+ expect(ch29.reachLabel).toBeUndefined();
136
+ expect(ch27.verification.length).toBeGreaterThan(0);
137
+ expect(ch30.verification.length).toBeGreaterThan(0);
138
+ expect(ch27.verification).not.toEqual(ch30.verification);
139
+ expect(ch26.verification).not.toEqual(ch27.verification);
140
+ expect(ch26.scopeAreas.length).toBeGreaterThan(0);
141
+ });
74
142
  });
@@ -35,16 +35,17 @@ export declare const repoIntelligenceWireSchema: z.ZodObject<{
35
35
  }, z.core.$strip>;
36
36
  areas: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
37
  dependencyClass: z.ZodOptional<z.ZodEnum<{
38
- format: "format";
39
38
  unknown: "unknown";
40
39
  runtime: "runtime";
41
40
  build: "build";
42
41
  test: "test";
43
42
  tooling: "tooling";
44
43
  lint: "lint";
44
+ format: "format";
45
45
  ci: "ci";
46
46
  }>>;
47
47
  packageRole: z.ZodOptional<z.ZodEnum<{
48
+ queue: "queue";
48
49
  unknown: "unknown";
49
50
  typechecker: "typechecker";
50
51
  compiler: "compiler";
@@ -54,7 +55,6 @@ export declare const repoIntelligenceWireSchema: z.ZodObject<{
54
55
  test_runner: "test_runner";
55
56
  http_framework: "http_framework";
56
57
  auth: "auth";
57
- queue: "queue";
58
58
  orm: "orm";
59
59
  }>>;
60
60
  lockfileKind: z.ZodOptional<z.ZodEnum<{
@@ -79,8 +79,8 @@ export declare const repoIntelligenceWireSchema: z.ZodObject<{
79
79
  }>>;
80
80
  evidenceStrength: z.ZodOptional<z.ZodEnum<{
81
81
  low: "low";
82
- medium: "medium";
83
82
  high: "high";
83
+ medium: "medium";
84
84
  }>>;
85
85
  confidenceReason: z.ZodOptional<z.ZodString>;
86
86
  verificationFocus: z.ZodOptional<z.ZodArray<z.ZodString>>;