@mergesignal/shared 0.2.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 (61) hide show
  1. package/dist/actionsStepSummary.d.ts +29 -0
  2. package/dist/actionsStepSummary.d.ts.map +1 -0
  3. package/dist/actionsStepSummary.js +757 -0
  4. package/dist/actionsStepSummary.test.d.ts +2 -0
  5. package/dist/actionsStepSummary.test.d.ts.map +1 -0
  6. package/dist/actionsStepSummary.test.js +348 -0
  7. package/dist/deriveScanSummaryText.d.ts +7 -0
  8. package/dist/deriveScanSummaryText.d.ts.map +1 -0
  9. package/dist/deriveScanSummaryText.js +24 -0
  10. package/dist/deriveScanSummaryText.test.d.ts +2 -0
  11. package/dist/deriveScanSummaryText.test.d.ts.map +1 -0
  12. package/dist/deriveScanSummaryText.test.js +77 -0
  13. package/dist/formatInsight.d.ts +10 -0
  14. package/dist/formatInsight.d.ts.map +1 -0
  15. package/dist/formatInsight.js +49 -0
  16. package/dist/formatInsight.test.d.ts +2 -0
  17. package/dist/formatInsight.test.d.ts.map +1 -0
  18. package/dist/formatInsight.test.js +102 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +10 -0
  22. package/dist/prCheckRunPresentation.d.ts +67 -0
  23. package/dist/prCheckRunPresentation.d.ts.map +1 -0
  24. package/dist/prCheckRunPresentation.js +314 -0
  25. package/dist/prCheckRunPresentation.test.d.ts +2 -0
  26. package/dist/prCheckRunPresentation.test.d.ts.map +1 -0
  27. package/dist/prCheckRunPresentation.test.js +277 -0
  28. package/dist/riskVocabulary.d.ts +31 -0
  29. package/dist/riskVocabulary.d.ts.map +1 -0
  30. package/dist/riskVocabulary.js +62 -0
  31. package/dist/riskVocabulary.test.d.ts +2 -0
  32. package/dist/riskVocabulary.test.d.ts.map +1 -0
  33. package/dist/riskVocabulary.test.js +14 -0
  34. package/dist/scanResultSchema.d.ts +116 -0
  35. package/dist/scanResultSchema.d.ts.map +1 -0
  36. package/dist/scanResultSchema.js +100 -0
  37. package/dist/scanResultSchema.test.d.ts +2 -0
  38. package/dist/scanResultSchema.test.d.ts.map +1 -0
  39. package/dist/scanResultSchema.test.js +105 -0
  40. package/dist/scanSurfaceCopy.d.ts +90 -0
  41. package/dist/scanSurfaceCopy.d.ts.map +1 -0
  42. package/dist/scanSurfaceCopy.js +104 -0
  43. package/dist/scanSurfaceCopy.test.d.ts +2 -0
  44. package/dist/scanSurfaceCopy.test.d.ts.map +1 -0
  45. package/dist/scanSurfaceCopy.test.js +14 -0
  46. package/dist/selectTopAffectedAreas.d.ts +17 -0
  47. package/dist/selectTopAffectedAreas.d.ts.map +1 -0
  48. package/dist/selectTopAffectedAreas.js +80 -0
  49. package/dist/selectTopAffectedAreas.test.d.ts +2 -0
  50. package/dist/selectTopAffectedAreas.test.d.ts.map +1 -0
  51. package/dist/selectTopAffectedAreas.test.js +179 -0
  52. package/dist/trustedScanGuards.d.ts +39 -0
  53. package/dist/trustedScanGuards.d.ts.map +1 -0
  54. package/dist/trustedScanGuards.js +88 -0
  55. package/dist/trustedScanGuards.test.d.ts +2 -0
  56. package/dist/trustedScanGuards.test.d.ts.map +1 -0
  57. package/dist/trustedScanGuards.test.js +143 -0
  58. package/dist/types.d.ts +362 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +12 -0
  61. package/package.json +49 -0
@@ -0,0 +1,31 @@
1
+ import type { PRDecisionRecommendation } from "./types.js";
2
+ export type MergePosture = PRDecisionRecommendation;
3
+ /** Canonical display strings for merge posture values. */
4
+ export declare const MERGE_POSTURE_LABEL: Record<MergePosture, string>;
5
+ /**
6
+ * Sorting weights: higher number = listed first (riskiest first).
7
+ * Use as: arr.sort((a, b) => MERGE_POSTURE_SORT_ORDER[b] - MERGE_POSTURE_SORT_ORDER[a])
8
+ */
9
+ export declare const MERGE_POSTURE_SORT_ORDER: Record<MergePosture, number>;
10
+ /** Normalize a raw `decision` string to a typed MergePosture, or null. */
11
+ export declare function mergePostureFromDecision(decision: string | null | undefined): MergePosture | null;
12
+ /** Returns the display label for a decision, or a fallback string. */
13
+ export declare function mergePostureLabel(decision: string | null | undefined, fallback?: string): string;
14
+ /**
15
+ * Accessible composite label for a merge posture badge.
16
+ * e.g. "Risky, risk score 72" or "Safe"
17
+ */
18
+ export declare function ariaLabelForPosture(decision: string | null | undefined, score: number | null | undefined): string;
19
+ export type SignalSeverity = "low" | "medium" | "high";
20
+ /**
21
+ * Format a signal severity for prose use — always returns a qualified string
22
+ * so it cannot be confused with merge posture at a glance.
23
+ * e.g. "Severity: High"
24
+ */
25
+ export declare function formatSignalSeverity(severity: string): string;
26
+ /**
27
+ * Format a count + severity for compact prose.
28
+ * e.g. "3 high-severity findings"
29
+ */
30
+ export declare function formatSeverityCount(count: number, severity: string): string;
31
+ //# sourceMappingURL=riskVocabulary.d.ts.map
@@ -0,0 +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;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"}
@@ -0,0 +1,62 @@
1
+ /** Canonical display strings for merge posture values. */
2
+ export const MERGE_POSTURE_LABEL = {
3
+ safe: "Safe",
4
+ needs_review: "Needs review",
5
+ risky: "Risky",
6
+ };
7
+ /**
8
+ * Sorting weights: higher number = listed first (riskiest first).
9
+ * Use as: arr.sort((a, b) => MERGE_POSTURE_SORT_ORDER[b] - MERGE_POSTURE_SORT_ORDER[a])
10
+ */
11
+ export const MERGE_POSTURE_SORT_ORDER = {
12
+ risky: 2,
13
+ needs_review: 1,
14
+ safe: 0,
15
+ };
16
+ /** Normalize a raw `decision` string to a typed MergePosture, or null. */
17
+ export function mergePostureFromDecision(decision) {
18
+ if (decision === "safe" ||
19
+ decision === "needs_review" ||
20
+ decision === "risky") {
21
+ return decision;
22
+ }
23
+ return null;
24
+ }
25
+ /** Returns the display label for a decision, or a fallback string. */
26
+ export function mergePostureLabel(decision, fallback = "—") {
27
+ const posture = mergePostureFromDecision(decision);
28
+ return posture ? MERGE_POSTURE_LABEL[posture] : fallback;
29
+ }
30
+ /**
31
+ * Accessible composite label for a merge posture badge.
32
+ * e.g. "Risky, risk score 72" or "Safe"
33
+ */
34
+ export function ariaLabelForPosture(decision, score) {
35
+ const label = mergePostureLabel(decision);
36
+ if (score != null)
37
+ return `${label}, risk score ${Math.round(score)}`;
38
+ return label;
39
+ }
40
+ const SIGNAL_SEVERITY_LABEL = {
41
+ low: "Low",
42
+ medium: "Medium",
43
+ high: "High",
44
+ };
45
+ /**
46
+ * Format a signal severity for prose use — always returns a qualified string
47
+ * so it cannot be confused with merge posture at a glance.
48
+ * e.g. "Severity: High"
49
+ */
50
+ export function formatSignalSeverity(severity) {
51
+ const s = severity.toLowerCase();
52
+ const label = SIGNAL_SEVERITY_LABEL[s] ?? severity;
53
+ return `Severity: ${label}`;
54
+ }
55
+ /**
56
+ * Format a count + severity for compact prose.
57
+ * e.g. "3 high-severity findings"
58
+ */
59
+ export function formatSeverityCount(count, severity) {
60
+ const s = severity.toLowerCase();
61
+ return `${count} ${s}-severity finding${count !== 1 ? "s" : ""}`;
62
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=riskVocabulary.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"riskVocabulary.test.d.ts","sourceRoot":"","sources":["../src/riskVocabulary.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mergePostureFromDecision } from "./riskVocabulary.js";
3
+ describe("mergePostureFromDecision", () => {
4
+ it("accepts only canonical lowercase merge posture tokens", () => {
5
+ expect(mergePostureFromDecision("safe")).toBe("safe");
6
+ expect(mergePostureFromDecision("needs_review")).toBe("needs_review");
7
+ expect(mergePostureFromDecision("risky")).toBe("risky");
8
+ });
9
+ it("rejects alternate engine spellings (no silent normalization)", () => {
10
+ expect(mergePostureFromDecision("SAFE")).toBeNull();
11
+ expect(mergePostureFromDecision("RISKY")).toBeNull();
12
+ expect(mergePostureFromDecision("BLOCK")).toBeNull();
13
+ });
14
+ });
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import type { EngineEmittedScanResult, ScanResult } from "./types.js";
3
+ /** Bump when persisted `result` JSON validation rules change materially (relaxed / legacy-tolerant). */
4
+ export declare const SCAN_RESULT_ABI: "1";
5
+ /** Bump when strict fresh-engine-output validation rules change materially. */
6
+ export declare const ENGINE_OUTPUT_SCAN_ABI: "2";
7
+ /**
8
+ * Minimum structural invariants for **persisted** `scans.result` JSON and legacy reads.
9
+ * `methodologyVersion` stays optional so historical rows without it remain valid.
10
+ * Unknown top-level keys are preserved (forward-compatible with newer engines).
11
+ */
12
+ export declare const scanResultSchema: z.ZodObject<{
13
+ totalScore: z.ZodNumber;
14
+ layerScores: z.ZodObject<{
15
+ security: z.ZodNumber;
16
+ maintainability: z.ZodNumber;
17
+ ecosystem: z.ZodNumber;
18
+ upgradeImpact: z.ZodNumber;
19
+ }, z.core.$strip>;
20
+ findings: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodUnknown>, z.ZodNull]>>, z.ZodTransform<unknown[], unknown[] | null | undefined>>;
21
+ methodologyVersion: z.ZodOptional<z.ZodString>;
22
+ confidence: z.ZodOptional<z.ZodEnum<{
23
+ low: "low";
24
+ medium: "medium";
25
+ high: "high";
26
+ }>>;
27
+ signals: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
28
+ contributions: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
29
+ recommendations: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
30
+ dataset: z.ZodOptional<z.ZodUnknown>;
31
+ explain: z.ZodOptional<z.ZodUnknown>;
32
+ graphInsights: z.ZodOptional<z.ZodUnknown>;
33
+ generatedAt: z.ZodString;
34
+ insights: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
35
+ decision: z.ZodOptional<z.ZodObject<{
36
+ recommendation: z.ZodEnum<{
37
+ safe: "safe";
38
+ needs_review: "needs_review";
39
+ risky: "risky";
40
+ }>;
41
+ confidence: z.ZodOptional<z.ZodEnum<{
42
+ low: "low";
43
+ medium: "medium";
44
+ high: "high";
45
+ }>>;
46
+ reasoning: z.ZodOptional<z.ZodArray<z.ZodString>>;
47
+ }, z.core.$loose>>;
48
+ codeAnalysisMetrics: z.ZodOptional<z.ZodUnknown>;
49
+ }, z.core.$loose>;
50
+ /**
51
+ * Stricter schema for **fresh** `analyze()` output only. Do not use when hydrating
52
+ * historical `scans.result` blobs from the database.
53
+ */
54
+ export declare const engineOutputScanResultSchema: z.ZodObject<{
55
+ totalScore: z.ZodNumber;
56
+ layerScores: z.ZodObject<{
57
+ security: z.ZodNumber;
58
+ maintainability: z.ZodNumber;
59
+ ecosystem: z.ZodNumber;
60
+ upgradeImpact: z.ZodNumber;
61
+ }, z.core.$strip>;
62
+ findings: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodUnknown>, z.ZodNull]>>, z.ZodTransform<unknown[], unknown[] | null | undefined>>;
63
+ confidence: z.ZodOptional<z.ZodEnum<{
64
+ low: "low";
65
+ medium: "medium";
66
+ high: "high";
67
+ }>>;
68
+ signals: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
69
+ contributions: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
70
+ recommendations: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
71
+ dataset: z.ZodOptional<z.ZodUnknown>;
72
+ explain: z.ZodOptional<z.ZodUnknown>;
73
+ graphInsights: z.ZodOptional<z.ZodUnknown>;
74
+ insights: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
75
+ decision: z.ZodOptional<z.ZodObject<{
76
+ recommendation: z.ZodEnum<{
77
+ safe: "safe";
78
+ needs_review: "needs_review";
79
+ risky: "risky";
80
+ }>;
81
+ confidence: z.ZodOptional<z.ZodEnum<{
82
+ low: "low";
83
+ medium: "medium";
84
+ high: "high";
85
+ }>>;
86
+ reasoning: z.ZodOptional<z.ZodArray<z.ZodString>>;
87
+ }, z.core.$loose>>;
88
+ codeAnalysisMetrics: z.ZodOptional<z.ZodUnknown>;
89
+ methodologyVersion: z.ZodString;
90
+ generatedAt: z.ZodString;
91
+ }, z.core.$loose>;
92
+ export type EngineOutputScanResultParseFailure = {
93
+ ok: false;
94
+ message: string;
95
+ issues: string[];
96
+ };
97
+ export type EngineOutputScanResultParseSuccess = {
98
+ ok: true;
99
+ result: EngineEmittedScanResult;
100
+ };
101
+ export declare function safeParseEngineOutputScanResult(data: unknown): EngineOutputScanResultParseSuccess | EngineOutputScanResultParseFailure;
102
+ /** Validates fresh engine output; throws on failure. Not for legacy persisted JSON. */
103
+ export declare function parseEngineOutputScanResultOrThrow(data: unknown): EngineEmittedScanResult;
104
+ export type ScanResultParseFailure = {
105
+ ok: false;
106
+ message: string;
107
+ issues: string[];
108
+ };
109
+ export type ScanResultParseSuccess = {
110
+ ok: true;
111
+ result: ScanResult;
112
+ };
113
+ export declare function safeParseScanResult(data: unknown): ScanResultParseSuccess | ScanResultParseFailure;
114
+ /** Validates engine output at the worker boundary; throws on failure. */
115
+ export declare function parseScanResultOrThrow(data: unknown): ScanResult;
116
+ //# sourceMappingURL=scanResultSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanResultSchema.d.ts","sourceRoot":"","sources":["../src/scanResultSchema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtE,wGAAwG;AACxG,eAAO,MAAM,eAAe,EAAG,GAAY,CAAC;AAE5C,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAG,GAAY,CAAC;AASnD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4Bb,CAAC;AAUjB;;;GAGG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAGvC,CAAC;AAEH,MAAM,MAAM,kCAAkC,GAAG;IAC/C,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,OAAO,GACZ,kCAAkC,GAAG,kCAAkC,CAazE;AAED,uFAAuF;AACvF,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,OAAO,GACZ,uBAAuB,CAMzB;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,GACZ,sBAAsB,GAAG,sBAAsB,CAajD;AAED,yEAAyE;AACzE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAMhE"}
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ /** Bump when persisted `result` JSON validation rules change materially (relaxed / legacy-tolerant). */
3
+ export const SCAN_RESULT_ABI = "1";
4
+ /** Bump when strict fresh-engine-output validation rules change materially. */
5
+ export const ENGINE_OUTPUT_SCAN_ABI = "2";
6
+ const layerScoresSchema = z.object({
7
+ security: z.number(),
8
+ maintainability: z.number(),
9
+ ecosystem: z.number(),
10
+ upgradeImpact: z.number(),
11
+ });
12
+ /**
13
+ * Minimum structural invariants for **persisted** `scans.result` JSON and legacy reads.
14
+ * `methodologyVersion` stays optional so historical rows without it remain valid.
15
+ * Unknown top-level keys are preserved (forward-compatible with newer engines).
16
+ */
17
+ export const scanResultSchema = z
18
+ .object({
19
+ totalScore: z.number().min(0).max(100),
20
+ layerScores: layerScoresSchema,
21
+ findings: z
22
+ .union([z.array(z.unknown()), z.null()])
23
+ .optional()
24
+ .transform((v) => (Array.isArray(v) ? v : [])),
25
+ methodologyVersion: z.string().optional(),
26
+ confidence: z.enum(["low", "medium", "high"]).optional(),
27
+ signals: z.array(z.unknown()).optional(),
28
+ contributions: z.array(z.unknown()).optional(),
29
+ recommendations: z.array(z.unknown()).optional(),
30
+ dataset: z.unknown().optional(),
31
+ explain: z.unknown().optional(),
32
+ graphInsights: z.unknown().optional(),
33
+ generatedAt: z.string().min(1),
34
+ insights: z.array(z.unknown()).optional(),
35
+ decision: z
36
+ .object({
37
+ recommendation: z.enum(["safe", "needs_review", "risky"]),
38
+ confidence: z.enum(["low", "medium", "high"]).optional(),
39
+ reasoning: z.array(z.string()).optional(),
40
+ })
41
+ .passthrough()
42
+ .optional(),
43
+ codeAnalysisMetrics: z.unknown().optional(),
44
+ })
45
+ .passthrough();
46
+ const engineOutputGeneratedAtSchema = z
47
+ .string()
48
+ .trim()
49
+ .min(1)
50
+ .refine((s) => !Number.isNaN(Date.parse(s)), {
51
+ error: "generatedAt must be a parseable ISO-8601 timestamp",
52
+ });
53
+ /**
54
+ * Stricter schema for **fresh** `analyze()` output only. Do not use when hydrating
55
+ * historical `scans.result` blobs from the database.
56
+ */
57
+ export const engineOutputScanResultSchema = scanResultSchema.extend({
58
+ methodologyVersion: z.string().trim().min(1),
59
+ generatedAt: engineOutputGeneratedAtSchema,
60
+ });
61
+ export function safeParseEngineOutputScanResult(data) {
62
+ const parsed = engineOutputScanResultSchema.safeParse(data);
63
+ if (!parsed.success) {
64
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
65
+ return {
66
+ ok: false,
67
+ message: issues.join("; "),
68
+ issues,
69
+ };
70
+ }
71
+ return { ok: true, result: parsed.data };
72
+ }
73
+ /** Validates fresh engine output; throws on failure. Not for legacy persisted JSON. */
74
+ export function parseEngineOutputScanResultOrThrow(data) {
75
+ const r = safeParseEngineOutputScanResult(data);
76
+ if (!r.ok) {
77
+ throw new Error(`validation: ${r.message}`);
78
+ }
79
+ return r.result;
80
+ }
81
+ export function safeParseScanResult(data) {
82
+ const parsed = scanResultSchema.safeParse(data);
83
+ if (!parsed.success) {
84
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
85
+ return {
86
+ ok: false,
87
+ message: issues.join("; "),
88
+ issues,
89
+ };
90
+ }
91
+ return { ok: true, result: parsed.data };
92
+ }
93
+ /** Validates engine output at the worker boundary; throws on failure. */
94
+ export function parseScanResultOrThrow(data) {
95
+ const r = safeParseScanResult(data);
96
+ if (!r.ok) {
97
+ throw new Error(`validation: ${r.message}`);
98
+ }
99
+ return r.result;
100
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scanResultSchema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanResultSchema.test.d.ts","sourceRoot":"","sources":["../src/scanResultSchema.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { safeParseScanResult, parseScanResultOrThrow, scanResultSchema, safeParseEngineOutputScanResult, parseEngineOutputScanResultOrThrow, } from "./scanResultSchema.js";
3
+ const minimalValid = {
4
+ totalScore: 42,
5
+ layerScores: {
6
+ security: 10,
7
+ maintainability: 20,
8
+ ecosystem: 30,
9
+ upgradeImpact: 40,
10
+ },
11
+ findings: [],
12
+ generatedAt: "2026-01-01T00:00:00.000Z",
13
+ };
14
+ describe("scanResultSchema", () => {
15
+ it("accepts a minimal valid payload", () => {
16
+ const r = safeParseScanResult(minimalValid);
17
+ expect(r.ok).toBe(true);
18
+ if (r.ok)
19
+ expect(r.result.totalScore).toBe(42);
20
+ });
21
+ it("preserves unknown top-level keys (passthrough)", () => {
22
+ const r = safeParseScanResult({
23
+ ...minimalValid,
24
+ futureEngineField: { x: 1 },
25
+ });
26
+ expect(r.ok).toBe(true);
27
+ if (r.ok)
28
+ expect(r.result.futureEngineField).toEqual({
29
+ x: 1,
30
+ });
31
+ });
32
+ it("rejects missing layerScores", () => {
33
+ const r = safeParseScanResult({
34
+ totalScore: 1,
35
+ findings: [],
36
+ generatedAt: "2026-01-01T00:00:00.000Z",
37
+ });
38
+ expect(r.ok).toBe(false);
39
+ });
40
+ it("rejects out-of-range totalScore", () => {
41
+ const r = safeParseScanResult({
42
+ ...minimalValid,
43
+ totalScore: 101,
44
+ });
45
+ expect(r.ok).toBe(false);
46
+ });
47
+ it("rejects invalid decision.recommendation", () => {
48
+ const r = safeParseScanResult({
49
+ ...minimalValid,
50
+ decision: { recommendation: "maybe", reasoning: [] },
51
+ });
52
+ expect(r.ok).toBe(false);
53
+ });
54
+ it("rejects non-canonical merge posture tokens (e.g. proprietary uppercase)", () => {
55
+ const r = safeParseScanResult({
56
+ ...minimalValid,
57
+ decision: { recommendation: "SAFE", reasoning: [] },
58
+ });
59
+ expect(r.ok).toBe(false);
60
+ });
61
+ it("rejects empty generatedAt", () => {
62
+ const r = safeParseScanResult({
63
+ ...minimalValid,
64
+ generatedAt: "",
65
+ });
66
+ expect(r.ok).toBe(false);
67
+ });
68
+ it("parseScanResultOrThrow returns on success", () => {
69
+ expect(parseScanResultOrThrow(minimalValid).totalScore).toBe(42);
70
+ });
71
+ it("parseScanResultOrThrow throws with validation prefix", () => {
72
+ expect(() => parseScanResultOrThrow({})).toThrow(/^validation:/);
73
+ });
74
+ it("scanResultSchema default empty findings when omitted", () => {
75
+ const { findings: _f, ...rest } = minimalValid;
76
+ const parsed = scanResultSchema.parse(rest);
77
+ expect(parsed.findings).toEqual([]);
78
+ });
79
+ });
80
+ describe("engineOutputScanResultSchema (strict, fresh engine only)", () => {
81
+ const withMethodology = {
82
+ ...minimalValid,
83
+ methodologyVersion: "engine-test-fixture/v1",
84
+ };
85
+ it("rejects payload that relaxed parser accepts when methodology is missing", () => {
86
+ const r = safeParseEngineOutputScanResult(minimalValid);
87
+ expect(r.ok).toBe(false);
88
+ });
89
+ it("accepts when methodology and parseable generatedAt are present", () => {
90
+ const r = safeParseEngineOutputScanResult(withMethodology);
91
+ expect(r.ok).toBe(true);
92
+ if (r.ok)
93
+ expect(r.result.methodologyVersion).toBe("engine-test-fixture/v1");
94
+ });
95
+ it("parseEngineOutputScanResultOrThrow throws with validation prefix", () => {
96
+ expect(() => parseEngineOutputScanResultOrThrow(minimalValid)).toThrow(/^validation:/);
97
+ });
98
+ it("rejects unparseable generatedAt", () => {
99
+ const r = safeParseEngineOutputScanResult({
100
+ ...withMethodology,
101
+ generatedAt: "not-a-date",
102
+ });
103
+ expect(r.ok).toBe(false);
104
+ });
105
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Single source for short scan pipeline / Actions copy (not i18n — keep flat and small).
3
+ * Risk / merge posture labels stay in riskVocabulary / MERGE_POSTURE_LABEL.
4
+ */
5
+ export declare const scanSurfaceCopy: {
6
+ readonly pipeline: {
7
+ readonly scanRunning: "Scan in progress";
8
+ readonly scanIncomplete: "Waiting for scan results";
9
+ readonly scanUnavailable: "Scan data unavailable";
10
+ readonly analysisIncomplete: "Analysis could not be completed";
11
+ readonly outputNotVerified: "These results could not be verified";
12
+ };
13
+ readonly actions: {
14
+ readonly failureTitle: "MergeSignal";
15
+ readonly failureBody: "Analysis could not be completed. Check the workflow logs for details.";
16
+ readonly demoSummaryTitle: "MergeSignal (demo output)";
17
+ readonly demoSummaryBanner: "Sample analysis only — not production MergeSignal results. Do not use for merge decisions.";
18
+ readonly trustedSummaryMethodologyLine: "Methodology";
19
+ /** Dogfood workflow: secret missing on same-repo runs (annotation + logs). */
20
+ readonly trustedWorkflowSecretMissing: "Trusted MergeSignal analysis is unavailable in this repository. Complete engine access setup, then re-run the workflow.";
21
+ /** Composite: trusted profile without engine token input. */
22
+ readonly trustedCompositeTokenMissing: "Trusted analysis is unavailable: engine access was not provided to this workflow run.";
23
+ readonly trustedEngineRepoLayoutInvalid: "MergeSignal could not prepare the analysis engine (unsupported project layout).";
24
+ readonly trustedEngineBuildOutputMissing: "MergeSignal could not prepare the analysis engine (expected build output was not found).";
25
+ /** Stub methodology or trusted summary preflight. */
26
+ readonly trustedSummaryStubBlocked: "Trusted analysis is unavailable: output did not meet verification requirements.";
27
+ readonly trustedMethodologyMissing: "Trusted analysis is unavailable: verified analysis metadata was missing.";
28
+ readonly trustedMethodologyPolicyMismatch: "Trusted analysis is unavailable: methodology output did not match the configured policy.";
29
+ readonly trustedAuditEnvInvalid: "Trusted analysis verification could not run in this environment.";
30
+ /** Post-render audit: summary text failed trusted verification. */
31
+ readonly trustedSummaryVerificationFailed: "MergeSignal could not verify this summary for trusted analysis.";
32
+ readonly prAnalysisUnavailableFork: "MergeSignal analysis is not available for this pull request (fork PRs use a restricted CI context).";
33
+ readonly prAnalysisUnavailableDependabot: "MergeSignal analysis is not available for automated dependency update PRs in this CI context.";
34
+ /** push/workflow_dispatch on a clone with no engine token configured (forks/mirrors). */
35
+ readonly pushTrustedScanSkippedNoEngineToken: "Trusted MergeSignal analysis was skipped because no engine access token is configured for this repository.";
36
+ /** Step summary when trusted scan cannot start (canonical repo, token missing). */
37
+ readonly trustedWorkflowCredentialHintBody: "The engine checkout token was not available to this workflow run. Add it under this repository's Actions secrets, or grant this repository access to an organization secret. Secrets that exist only on a GitHub Environment are not available here unless this job declares that environment.";
38
+ /** Trusted step summary H1 product line (before posture · risk index). */
39
+ readonly trustedStepSummaryTitlePrefix: "MergeSignal dependency review";
40
+ /** GitHub Actions step summary: risk index direction (shared with web/CLI where imported). */
41
+ readonly riskIndexDirectionShort: "0 = lowest merge risk, 100 = highest merge risk";
42
+ /** When `decision` is missing on scan JSON (legacy output). */
43
+ readonly mergePostureUnavailableShort: "Posture unavailable";
44
+ readonly mergePostureUnavailableDetail: "This scan did not include a merge posture verdict; use the risk index as a coarse signal only.";
45
+ /** `<summary>` lines for collapsible sections (keep short for GitHub UI). */
46
+ readonly scoreBreakdownDetailsSummary: "Layer scores — this scan";
47
+ readonly dependencyGraphDetailsSummary: "Graph context";
48
+ readonly moreActionsDetailsSummary: "More guidance";
49
+ readonly moreInsightsDetailsSummary: "More insights";
50
+ /** Inside dependency graph `<details>`. */
51
+ readonly supportingGraphContextNote: "Supporting context — not a merge verdict by itself.";
52
+ readonly layerScoreGlossary: "*Higher column scores = more risk in that dimension (0 best → 100 worst).*";
53
+ /** After layer table: scan-specific drivers from explain/contributions. */
54
+ readonly layerDriversHeading: "Signals behind elevated scores";
55
+ /** When surfacing recommendation rationale first (trusted default fold). */
56
+ readonly recReviewLeadPrefix: "**For this PR:**";
57
+ readonly vulnerableReviewerHint: "If vulnerable packages are listed, confirm they apply to your usage before treating counts as merge blockers.";
58
+ /** Development profile: no recommendations or actionable insights. */
59
+ readonly devNoImmediateActions: "No immediate actions required";
60
+ };
61
+ /** GitHub App Check Run (PR) — calm, concise; not Actions step summary. */
62
+ readonly checkRun: {
63
+ readonly titleBase: "MergeSignal scan - PR dependency change";
64
+ readonly titleBaselineSuffix: "baseline scan only";
65
+ readonly baselineOutcomePrimary: "No actionable dependency concerns showed up for this PR in this scan.";
66
+ readonly baselineOutcomeScope: "This run emphasizes repository-wide context; open the scan for PR-specific detail when available.";
67
+ readonly baselineBoundaryNote: "PR-targeted dependency signals are not included in baseline-only runs.";
68
+ readonly footerLinkLabel: "View full scan";
69
+ readonly layerScoresDetailsSummary: "Layer scores";
70
+ readonly layerNoDrivers: "No notable drivers surfaced for this dimension.";
71
+ readonly repoContextLabel: "Risk index";
72
+ readonly mergePostureUnavailable: "Posture unavailable";
73
+ };
74
+ readonly product: {
75
+ /** Single line for UI/CLI/Actions parity (risk index semantics). */
76
+ readonly riskIndexDirectionShort: "0 is best · 100 is worst";
77
+ };
78
+ readonly cli: {
79
+ readonly stderrAnalysisIncomplete: "Analysis could not be completed.";
80
+ readonly stderrOutputNotVerified: "These results could not be verified.";
81
+ };
82
+ /** @mergesignal/engine loader (stderr / thrown when impl missing). */
83
+ readonly engineLoader: {
84
+ readonly implRequiredTrustedScan: "Trusted analysis requires a configured analysis engine. Use demo output only when you explicitly intend to run without a real engine.";
85
+ readonly implRequiredProduction: "A configured analysis engine is required in this environment. Use demo output only when you explicitly intend to run without a real engine.";
86
+ };
87
+ };
88
+ /** Flatten for `scripts/ci/*.mjs` consumers (generated JSON). */
89
+ export declare function scanSurfaceCopyFlat(): Record<string, string>;
90
+ //# sourceMappingURL=scanSurfaceCopy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanSurfaceCopy.d.ts","sourceRoot":"","sources":["../src/scanSurfaceCopy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;QAgBxB,8EAA8E;;QAG9E,6DAA6D;;;;QAO7D,qDAAqD;;;;;QASrD,mEAAmE;;;;QAOnE,yFAAyF;;QAGzF,mFAAmF;;QAGnF,0EAA0E;;QAE1E,8FAA8F;;QAE9F,+DAA+D;;;QAI/D,6EAA6E;;;;;QAK7E,2CAA2C;;;QAK3C,2EAA2E;;QAE3E,4EAA4E;;;QAI5E,sEAAsE;;;IAGxE,2EAA2E;;;;;;;;;;;;;;QAiBzE,oEAAoE;;;;;;;IAOtE,sEAAsE;;;;;CAO9D,CAAC;AAEX,iEAAiE;AACjE,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAa5D"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Single source for short scan pipeline / Actions copy (not i18n — keep flat and small).
3
+ * Risk / merge posture labels stay in riskVocabulary / MERGE_POSTURE_LABEL.
4
+ */
5
+ export const scanSurfaceCopy = {
6
+ pipeline: {
7
+ scanRunning: "Scan in progress",
8
+ scanIncomplete: "Waiting for scan results",
9
+ scanUnavailable: "Scan data unavailable",
10
+ analysisIncomplete: "Analysis could not be completed",
11
+ outputNotVerified: "These results could not be verified",
12
+ },
13
+ actions: {
14
+ failureTitle: "MergeSignal",
15
+ failureBody: "Analysis could not be completed. Check the workflow logs for details.",
16
+ demoSummaryTitle: "MergeSignal (demo output)",
17
+ demoSummaryBanner: "Sample analysis only — not production MergeSignal results. Do not use for merge decisions.",
18
+ trustedSummaryMethodologyLine: "Methodology",
19
+ /** Dogfood workflow: secret missing on same-repo runs (annotation + logs). */
20
+ trustedWorkflowSecretMissing: "Trusted MergeSignal analysis is unavailable in this repository. Complete engine access setup, then re-run the workflow.",
21
+ /** Composite: trusted profile without engine token input. */
22
+ trustedCompositeTokenMissing: "Trusted analysis is unavailable: engine access was not provided to this workflow run.",
23
+ trustedEngineRepoLayoutInvalid: "MergeSignal could not prepare the analysis engine (unsupported project layout).",
24
+ trustedEngineBuildOutputMissing: "MergeSignal could not prepare the analysis engine (expected build output was not found).",
25
+ /** Stub methodology or trusted summary preflight. */
26
+ trustedSummaryStubBlocked: "Trusted analysis is unavailable: output did not meet verification requirements.",
27
+ trustedMethodologyMissing: "Trusted analysis is unavailable: verified analysis metadata was missing.",
28
+ trustedMethodologyPolicyMismatch: "Trusted analysis is unavailable: methodology output did not match the configured policy.",
29
+ trustedAuditEnvInvalid: "Trusted analysis verification could not run in this environment.",
30
+ /** Post-render audit: summary text failed trusted verification. */
31
+ trustedSummaryVerificationFailed: "MergeSignal could not verify this summary for trusted analysis.",
32
+ prAnalysisUnavailableFork: "MergeSignal analysis is not available for this pull request (fork PRs use a restricted CI context).",
33
+ prAnalysisUnavailableDependabot: "MergeSignal analysis is not available for automated dependency update PRs in this CI context.",
34
+ /** push/workflow_dispatch on a clone with no engine token configured (forks/mirrors). */
35
+ pushTrustedScanSkippedNoEngineToken: "Trusted MergeSignal analysis was skipped because no engine access token is configured for this repository.",
36
+ /** Step summary when trusted scan cannot start (canonical repo, token missing). */
37
+ trustedWorkflowCredentialHintBody: "The engine checkout token was not available to this workflow run. Add it under this repository's Actions secrets, or grant this repository access to an organization secret. Secrets that exist only on a GitHub Environment are not available here unless this job declares that environment.",
38
+ /** Trusted step summary H1 product line (before posture · risk index). */
39
+ trustedStepSummaryTitlePrefix: "MergeSignal dependency review",
40
+ /** GitHub Actions step summary: risk index direction (shared with web/CLI where imported). */
41
+ riskIndexDirectionShort: "0 = lowest merge risk, 100 = highest merge risk",
42
+ /** When `decision` is missing on scan JSON (legacy output). */
43
+ mergePostureUnavailableShort: "Posture unavailable",
44
+ mergePostureUnavailableDetail: "This scan did not include a merge posture verdict; use the risk index as a coarse signal only.",
45
+ /** `<summary>` lines for collapsible sections (keep short for GitHub UI). */
46
+ scoreBreakdownDetailsSummary: "Layer scores — this scan",
47
+ dependencyGraphDetailsSummary: "Graph context",
48
+ moreActionsDetailsSummary: "More guidance",
49
+ moreInsightsDetailsSummary: "More insights",
50
+ /** Inside dependency graph `<details>`. */
51
+ supportingGraphContextNote: "Supporting context — not a merge verdict by itself.",
52
+ layerScoreGlossary: "*Higher column scores = more risk in that dimension (0 best → 100 worst).*",
53
+ /** After layer table: scan-specific drivers from explain/contributions. */
54
+ layerDriversHeading: "Signals behind elevated scores",
55
+ /** When surfacing recommendation rationale first (trusted default fold). */
56
+ recReviewLeadPrefix: "**For this PR:**",
57
+ vulnerableReviewerHint: "If vulnerable packages are listed, confirm they apply to your usage before treating counts as merge blockers.",
58
+ /** Development profile: no recommendations or actionable insights. */
59
+ devNoImmediateActions: "No immediate actions required",
60
+ },
61
+ /** GitHub App Check Run (PR) — calm, concise; not Actions step summary. */
62
+ checkRun: {
63
+ titleBase: "MergeSignal scan - PR dependency change",
64
+ titleBaselineSuffix: "baseline scan only",
65
+ baselineOutcomePrimary: "No actionable dependency concerns showed up for this PR in this scan.",
66
+ baselineOutcomeScope: "This run emphasizes repository-wide context; open the scan for PR-specific detail when available.",
67
+ baselineBoundaryNote: "PR-targeted dependency signals are not included in baseline-only runs.",
68
+ footerLinkLabel: "View full scan",
69
+ layerScoresDetailsSummary: "Layer scores",
70
+ layerNoDrivers: "No notable drivers surfaced for this dimension.",
71
+ repoContextLabel: "Risk index",
72
+ mergePostureUnavailable: "Posture unavailable",
73
+ },
74
+ product: {
75
+ /** Single line for UI/CLI/Actions parity (risk index semantics). */
76
+ riskIndexDirectionShort: "0 is best · 100 is worst",
77
+ },
78
+ cli: {
79
+ stderrAnalysisIncomplete: "Analysis could not be completed.",
80
+ stderrOutputNotVerified: "These results could not be verified.",
81
+ },
82
+ /** @mergesignal/engine loader (stderr / thrown when impl missing). */
83
+ engineLoader: {
84
+ implRequiredTrustedScan: "Trusted analysis requires a configured analysis engine. Use demo output only when you explicitly intend to run without a real engine.",
85
+ implRequiredProduction: "A configured analysis engine is required in this environment. Use demo output only when you explicitly intend to run without a real engine.",
86
+ },
87
+ };
88
+ /** Flatten for `scripts/ci/*.mjs` consumers (generated JSON). */
89
+ export function scanSurfaceCopyFlat() {
90
+ const out = {};
91
+ const walk = (prefix, obj) => {
92
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
93
+ for (const [k, v] of Object.entries(obj)) {
94
+ const key = prefix ? `${prefix}.${k}` : k;
95
+ if (typeof v === "string")
96
+ out[key] = v;
97
+ else
98
+ walk(key, v);
99
+ }
100
+ }
101
+ };
102
+ walk("", scanSurfaceCopy);
103
+ return out;
104
+ }