@mergesignal/shared 0.9.0 → 0.9.1

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.
@@ -1,4 +1,16 @@
1
- import type { ScanResult } from "../types.js";
1
+ import type { LayerScores, ScanResult } from "../types.js";
2
+ type Abi4SplitScores = {
3
+ prRiskScore: number;
4
+ repositoryHealthScore: number;
5
+ layerScores?: LayerScores;
6
+ };
7
+ /**
8
+ * Attach differentiated ABI-4 prRisk + repositoryHealth wire blocks.
9
+ * `totalScore` mirrors repository health for legacy readers; presentation must
10
+ * consume `prRisk.score` via `deriveRiskSignals()`.
11
+ */
12
+ export declare function withAbi4SplitScores<T extends ScanResult>(result: T, scores: Abi4SplitScores): T;
2
13
  /** Attach ABI-4 prRisk + repositoryHealth wire blocks for strict engine validation tests. */
3
14
  export declare function withAbi4EngineScores<T extends ScanResult>(result: T): T;
15
+ export {};
4
16
  //# sourceMappingURL=engineAbi4Fixtures.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engineAbi4Fixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/engineAbi4Fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,6FAA6F;AAC7F,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEvE"}
1
+ {"version":3,"file":"engineAbi4Fixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/engineAbi4Fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG3D,KAAK,eAAe,GAAG;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,UAAU,EACtD,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,eAAe,GACtB,CAAC,CAeH;AAED,6FAA6F;AAC7F,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEvE"}
@@ -1,4 +1,25 @@
1
1
  import { normalizeEngineOutputAbi4 } from "../scanResultSchema.js";
2
+ /**
3
+ * Attach differentiated ABI-4 prRisk + repositoryHealth wire blocks.
4
+ * `totalScore` mirrors repository health for legacy readers; presentation must
5
+ * consume `prRisk.score` via `deriveRiskSignals()`.
6
+ */
7
+ export function withAbi4SplitScores(result, scores) {
8
+ const layerScores = scores.layerScores ?? result.layerScores;
9
+ return {
10
+ ...result,
11
+ totalScore: scores.repositoryHealthScore,
12
+ layerScores,
13
+ prRisk: {
14
+ score: scores.prRiskScore,
15
+ layerScores: result.prRisk?.layerScores ?? layerScores,
16
+ },
17
+ repositoryHealth: {
18
+ totalScore: scores.repositoryHealthScore,
19
+ layerScores,
20
+ },
21
+ };
22
+ }
2
23
  /** Attach ABI-4 prRisk + repositoryHealth wire blocks for strict engine validation tests. */
3
24
  export function withAbi4EngineScores(result) {
4
25
  return normalizeEngineOutputAbi4(result);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prRiskWiringRegression.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prRiskWiringRegression.test.d.ts","sourceRoot":"","sources":["../src/prRiskWiringRegression.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveScanNarrative } from "./deriveScanNarrative.js";
3
+ import { resolveRepositoryHealthScore } from "./prRiskWire.js";
4
+ import { buildScanPresentationBundle } from "./presentation/orchestration/buildScanPresentationBundle.js";
5
+ import { presentDashboardCard } from "./presentation/presenters/presentDashboardCard.js";
6
+ import { presentScanDetails } from "./presentation/presenters/presentScanDetails.js";
7
+ import { presentCliScanSummary } from "./presentation/presenters/presentCliScanSummary.js";
8
+ import { presentGitHubCheckRun } from "./presentation/presenters/presentGitHubCheckRun.js";
9
+ import { presentGitHubPrComment } from "./presentation/presenters/presentGitHubPrComment.js";
10
+ import { scanResultFastifyRuntime, scanResultTypescriptPatch, } from "./presentation/fixtures/scanResultFixtures.js";
11
+ import { scanResultPrettier } from "./presentation/fixtures/presentationPersonaFixtures.js";
12
+ const SCAN_ID = "22222222-2222-4222-8222-222222222222";
13
+ const ORIGIN = "https://app.example.com";
14
+ const validationPrShapes = [
15
+ {
16
+ id: "PR #27 fastify",
17
+ result: scanResultFastifyRuntime,
18
+ expectedPrRisk: 55,
19
+ expectedRepositoryHealth: 63,
20
+ },
21
+ {
22
+ id: "PR #28 typescript",
23
+ result: scanResultTypescriptPatch,
24
+ expectedPrRisk: 30,
25
+ expectedRepositoryHealth: 63,
26
+ },
27
+ {
28
+ id: "PR #29 prettier",
29
+ result: scanResultPrettier,
30
+ expectedPrRisk: 30,
31
+ expectedRepositoryHealth: 61,
32
+ },
33
+ ];
34
+ function surfacesFor(result) {
35
+ const bundle = buildScanPresentationBundle({
36
+ result,
37
+ pipelineStatus: "done",
38
+ });
39
+ return {
40
+ bundle,
41
+ card: presentDashboardCard(bundle),
42
+ details: presentScanDetails(bundle, { scanId: SCAN_ID }),
43
+ cli: presentCliScanSummary(bundle, { repoLabel: "mergesignal/app" }),
44
+ check: presentGitHubCheckRun(bundle, {
45
+ scanId: SCAN_ID,
46
+ webAppOrigin: ORIGIN,
47
+ }),
48
+ comment: presentGitHubPrComment(bundle),
49
+ };
50
+ }
51
+ describe("PR Risk wiring regression (Investigation 1)", () => {
52
+ it.each(validationPrShapes)("$id: riskIndex follows prRisk.score, not repository health", ({ result, expectedPrRisk, expectedRepositoryHealth }) => {
53
+ expect(result.prRisk?.score).toBe(expectedPrRisk);
54
+ expect(result.repositoryHealth?.totalScore).toBe(expectedRepositoryHealth);
55
+ expect(result.totalScore).toBe(expectedRepositoryHealth);
56
+ expect(resolveRepositoryHealthScore(result)).toBe(expectedRepositoryHealth);
57
+ const facts = deriveScanNarrative(result);
58
+ expect(facts.riskSignals?.riskIndex).toBe(expectedPrRisk);
59
+ expect(facts.riskIndex).toBe(expectedPrRisk);
60
+ const { card, details, cli, check, comment } = surfacesFor(result);
61
+ expect(card.sortKey.riskIndex).toBe(expectedPrRisk);
62
+ expect(details.hero.prRiskScore).toBe(expectedPrRisk);
63
+ expect(details.signalSummary?.prRiskScore).toBe(expectedPrRisk);
64
+ expect(cli.metrics?.prRiskScore).toBe(expectedPrRisk);
65
+ expect(cli.metrics?.riskIndex).toBe(expectedPrRisk);
66
+ const prRiskSection = check.sections.find((s) => s.title === "PR Risk");
67
+ expect(prRiskSection?.bullets[0]).toMatch(new RegExp(`^${expectedPrRisk} \\(`));
68
+ expect(comment.introLines.some((l) => l.startsWith("PR Risk:"))).toBe(true);
69
+ expect(comment.introLines.find((l) => l.startsWith("PR Risk:"))).toContain(String(expectedPrRisk));
70
+ });
71
+ it("same repository health with different PR evidence yields different riskIndex", () => {
72
+ const fastify = surfacesFor(scanResultFastifyRuntime);
73
+ const typescript = surfacesFor(scanResultTypescriptPatch);
74
+ expect(resolveRepositoryHealthScore(scanResultFastifyRuntime)).toBe(resolveRepositoryHealthScore(scanResultTypescriptPatch));
75
+ expect(fastify.card.sortKey.riskIndex).not.toBe(typescript.card.sortKey.riskIndex);
76
+ expect(fastify.card.sortKey.riskIndex).toBe(55);
77
+ expect(typescript.card.sortKey.riskIndex).toBe(30);
78
+ });
79
+ it("historical fallback: missing prRisk uses totalScore for riskIndex", () => {
80
+ const historical = {
81
+ totalScore: 48,
82
+ layerScores: {
83
+ security: 10,
84
+ maintainability: 12,
85
+ ecosystem: 14,
86
+ upgradeImpact: 12,
87
+ },
88
+ findings: [],
89
+ generatedAt: "2026-01-01T00:00:00.000Z",
90
+ assessment: scanResultTypescriptPatch.assessment,
91
+ decision: scanResultTypescriptPatch.decision,
92
+ };
93
+ const facts = deriveScanNarrative(historical);
94
+ expect(facts.riskSignals?.riskIndex).toBe(48);
95
+ expect(facts.riskIndex).toBe(48);
96
+ const bundle = buildScanPresentationBundle({
97
+ result: historical,
98
+ pipelineStatus: "done",
99
+ });
100
+ expect(presentDashboardCard(bundle).sortKey.riskIndex).toBe(48);
101
+ });
102
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"presentationPersonaFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/presentationPersonaFixtures.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAkBjD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,yBAAyB,CAAC;AA+KjC,eAAO,MAAM,gBAAgB,EAAE,UAG7B,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,UAG/B,CAAC;AAEH,eAAO,MAAM,gBAAgB,EAAE,UAG7B,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,UAI/B,CAAC;AAEH,eAAO,MAAM,gBAAgB,EAAE,UAa7B,CAAC;AAEH,eAAO,MAAM,qBAAqB,EAAE,UAMnC,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,UAO9C,CAAC;AAEF,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,GAC1B,CAAC"}
1
+ {"version":3,"file":"presentationPersonaFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/presentationPersonaFixtures.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,yBAAyB,CAAC;AA+KjC,eAAO,MAAM,gBAAgB,EAAE,UAG7B,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,UAShC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAG7B,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,UAI/B,CAAC;AAEH,eAAO,MAAM,gBAAgB,EAAE,UAa7B,CAAC;AAEH,eAAO,MAAM,qBAAqB,EAAE,UAMnC,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,UAO9C,CAAC;AAEF,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,yBAAyB,GAC1B,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { assessmentBullmq, assessmentEslint, assessmentMixedTypescriptFastify, assessmentNextAuth, assessmentPrettier, assessmentUnknownSafe, assessmentVitest, } from "../../fixtures/assessmentFixtures.js";
2
2
  import { fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceTypescript, } from "../../fixtures/repoIntelligenceFixtures.js";
3
+ import { withAbi4SplitScores } from "../../fixtures/engineAbi4Fixtures.js";
3
4
  import { analysisPreparationWithValidRepoIntel } from "../../fixtures/repoIntelligenceTestHelpers.js";
4
5
  import { scanResultFastifyRuntime, scanResultLimitedContext, scanResultTypescriptPatch, } from "./scanResultFixtures.js";
5
6
  const basePrep = analysisPreparationWithValidRepoIntel();
@@ -165,9 +166,12 @@ export const scanResultEslint = scanBase(assessmentEslint, {
165
166
  changedPackages: ["eslint"],
166
167
  repoIntelligence: fixtureRepoIntelligenceEslint,
167
168
  });
168
- export const scanResultPrettier = scanBase(assessmentPrettier, {
169
+ export const scanResultPrettier = withAbi4SplitScores(scanBase(assessmentPrettier, {
169
170
  changedPackages: ["prettier"],
170
171
  repoIntelligence: fixtureRepoIntelligencePrettier,
172
+ }), {
173
+ prRiskScore: 30,
174
+ repositoryHealthScore: 61,
171
175
  });
172
176
  export const scanResultVitest = scanBase(assessmentVitest, {
173
177
  changedPackages: ["vitest"],
@@ -1 +1 @@
1
- {"version":3,"file":"scanResultFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/scanResultFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAYjD,eAAO,MAAM,wBAAwB,EAAE,UA0CtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,UA2BvC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAqCtC,CAAC"}
1
+ {"version":3,"file":"scanResultFixtures.d.ts","sourceRoot":"","sources":["../../../src/presentation/fixtures/scanResultFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAoBjD,eAAO,MAAM,wBAAwB,EAAE,UA4CtC,CAAC;AASF,eAAO,MAAM,yBAAyB,EAAE,UA6BvC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAqCtC,CAAC"}
@@ -1,14 +1,16 @@
1
+ import { withAbi4SplitScores } from "../../fixtures/engineAbi4Fixtures.js";
1
2
  import { assessmentFastifyRuntime, assessmentLimitedContext, assessmentTypescriptPatch, } from "../../fixtures/assessmentFixtures.js";
2
3
  import { fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceTypescript, } from "../../fixtures/repoIntelligenceFixtures.js";
3
4
  import { analysisPreparationWithValidRepoIntel } from "../../fixtures/repoIntelligenceTestHelpers.js";
4
- export const scanResultFastifyRuntime = {
5
- totalScore: 55,
6
- layerScores: {
7
- security: 10,
8
- maintainability: 20,
9
- ecosystem: 30,
10
- upgradeImpact: 15,
11
- },
5
+ const fastifyLayerScores = {
6
+ security: 10,
7
+ maintainability: 20,
8
+ ecosystem: 30,
9
+ upgradeImpact: 15,
10
+ };
11
+ export const scanResultFastifyRuntime = withAbi4SplitScores({
12
+ totalScore: 63,
13
+ layerScores: fastifyLayerScores,
12
14
  findings: [
13
15
  {
14
16
  id: "finding-fastify-runtime",
@@ -43,15 +45,20 @@ export const scanResultFastifyRuntime = {
43
45
  remediation: "Run auth smoke tests",
44
46
  },
45
47
  ],
48
+ }, {
49
+ prRiskScore: 55,
50
+ repositoryHealthScore: 63,
51
+ layerScores: fastifyLayerScores,
52
+ });
53
+ const typescriptLayerScores = {
54
+ security: 5,
55
+ maintainability: 8,
56
+ ecosystem: 10,
57
+ upgradeImpact: 5,
46
58
  };
47
- export const scanResultTypescriptPatch = {
48
- totalScore: 18,
49
- layerScores: {
50
- security: 5,
51
- maintainability: 8,
52
- ecosystem: 10,
53
- upgradeImpact: 5,
54
- },
59
+ export const scanResultTypescriptPatch = withAbi4SplitScores({
60
+ totalScore: 63,
61
+ layerScores: typescriptLayerScores,
55
62
  findings: [],
56
63
  generatedAt: "2026-01-01T00:00:00.000Z",
57
64
  changedPackages: ["typescript"],
@@ -71,7 +78,11 @@ export const scanResultTypescriptPatch = {
71
78
  edges: 340,
72
79
  hotspots: [],
73
80
  },
74
- };
81
+ }, {
82
+ prRiskScore: 30,
83
+ repositoryHealthScore: 63,
84
+ layerScores: typescriptLayerScores,
85
+ });
75
86
  export const scanResultLimitedContext = {
76
87
  totalScore: 40,
77
88
  layerScores: {
@@ -8,7 +8,7 @@ export function presentCliScanSummary(bundle, ctx = {}) {
8
8
  const layerLine = layerScores
9
9
  ? `security=${layerScores.security} maintainability=${layerScores.maintainability} ecosystem=${layerScores.ecosystem} upgradeImpact=${layerScores.upgradeImpact}`
10
10
  : "";
11
- const prRiskScore = facts.riskIndex ?? 0;
11
+ const prRiskScore = facts.riskSignals?.riskIndex ?? facts.riskIndex ?? 0;
12
12
  return {
13
13
  ...projectAssessmentFields(bundle),
14
14
  header: {
@@ -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,sCAAsC,IAAI,yBAAyB,CAelF;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,CA+C3B;AAED,wBAAgB,sCAAsC,IAAI,yBAAyB,CAelF;AAED,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAC9C,yBAAyB,CAqB3B"}
@@ -59,8 +59,9 @@ export function presentDashboardCard(bundle) {
59
59
  const { facts, profile } = bundle;
60
60
  const channels = buildNarrativeChannels(bundle);
61
61
  const posture = profile.status;
62
- const riskIndex = facts.riskIndex != null && Number.isFinite(facts.riskIndex)
63
- ? facts.riskIndex
62
+ const riskIndex = facts.riskSignals?.riskIndex != null &&
63
+ Number.isFinite(facts.riskSignals.riskIndex)
64
+ ? facts.riskSignals.riskIndex
64
65
  : -1;
65
66
  const insights = channels.insights.slice(0, INSIGHTS_CAP);
66
67
  const scopeAreas = channels.scopeAreas.length > 0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=riskSignals.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"riskSignals.test.d.ts","sourceRoot":"","sources":["../src/riskSignals.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveRiskSignals } from "./riskSignals.js";
3
+ import { resolveRepositoryHealthScore } from "./prRiskWire.js";
4
+ function baseResult(overrides = {}) {
5
+ return {
6
+ totalScore: 63,
7
+ layerScores: {
8
+ security: 10,
9
+ maintainability: 20,
10
+ ecosystem: 30,
11
+ upgradeImpact: 15,
12
+ },
13
+ findings: [],
14
+ generatedAt: "2026-01-01T00:00:00.000Z",
15
+ ...overrides,
16
+ };
17
+ }
18
+ describe("deriveRiskSignals", () => {
19
+ it("prefers prRisk.score over totalScore and repositoryHealth", () => {
20
+ const result = baseResult({
21
+ prRisk: { score: 55 },
22
+ repositoryHealth: { totalScore: 63 },
23
+ totalScore: 63,
24
+ });
25
+ expect(deriveRiskSignals(result)?.riskIndex).toBe(55);
26
+ expect(resolveRepositoryHealthScore(result)).toBe(63);
27
+ });
28
+ it("falls back to totalScore when prRisk is missing (historical scans)", () => {
29
+ const result = baseResult({ totalScore: 48 });
30
+ expect(deriveRiskSignals(result)?.riskIndex).toBe(48);
31
+ });
32
+ });
@@ -58,8 +58,8 @@ describe("scanPresentationBundle golden — proof model", () => {
58
58
  const { facts } = bundle;
59
59
  expect(facts.availability.corpusGateReason).toBe("ok");
60
60
  expect(facts.packageSemantics?.dependencyClass).toBe("tooling");
61
- expect(facts.riskSignals?.riskIndex).toBe(18);
62
- expect(facts.riskSignals?.band).toBe(scoreToBand(18));
61
+ expect(facts.riskSignals?.riskIndex).toBe(30);
62
+ expect(facts.riskSignals?.band).toBe(scoreToBand(30));
63
63
  });
64
64
  it("affected areas always carry linkage arrays (structure)", () => {
65
65
  for (const { result } of validationPersonas) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mergesignal/shared",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "MergeSignal shared scan contract, presentation policy, and trusted validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",