@mergesignal/shared 0.6.0 → 0.8.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 (67) hide show
  1. package/dist/affectedAreaLinkage.d.ts +14 -0
  2. package/dist/affectedAreaLinkage.d.ts.map +1 -0
  3. package/dist/affectedAreaLinkage.js +100 -0
  4. package/dist/assessmentLabels.d.ts +1 -1
  5. package/dist/assessmentLabels.d.ts.map +1 -1
  6. package/dist/assessmentPresentationUtils.d.ts +6 -0
  7. package/dist/assessmentPresentationUtils.d.ts.map +1 -0
  8. package/dist/assessmentPresentationUtils.js +17 -0
  9. package/dist/assessmentProjection.d.ts +2 -2
  10. package/dist/assessmentProjection.d.ts.map +1 -1
  11. package/dist/assessmentProjection.js +42 -13
  12. package/dist/deriveScanNarrative.d.ts.map +1 -1
  13. package/dist/deriveScanNarrative.js +92 -9
  14. package/dist/deriveScanNarrative.test.js +74 -0
  15. package/dist/engineWireRegression.test.js +1 -1
  16. package/dist/fixtures/assessmentFixtures.d.ts +2 -1
  17. package/dist/fixtures/assessmentFixtures.d.ts.map +1 -1
  18. package/dist/fixtures/assessmentFixtures.js +78 -23
  19. package/dist/fixtures/assessmentScopeFixtures.d.ts +17 -0
  20. package/dist/fixtures/assessmentScopeFixtures.d.ts.map +1 -0
  21. package/dist/fixtures/assessmentScopeFixtures.js +42 -0
  22. package/dist/formatCardExposureDisplay.d.ts +5 -3
  23. package/dist/formatCardExposureDisplay.d.ts.map +1 -1
  24. package/dist/formatCardExposureDisplay.js +12 -5
  25. package/dist/formatCardExposureDisplay.test.js +9 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -1
  29. package/dist/presentation/buildGitHubCheckRunOutput.test.js +16 -41
  30. package/dist/presentation/compose/narrativeChannels.js +1 -1
  31. package/dist/presentation/dto/assessmentPresentationFields.d.ts +1 -2
  32. package/dist/presentation/dto/assessmentPresentationFields.d.ts.map +1 -1
  33. package/dist/presentation/fixtures/scanResultFixtures.d.ts.map +1 -1
  34. package/dist/presentation/fixtures/scanResultFixtures.js +19 -1
  35. package/dist/presentation/orchestration/buildScanPresentationBundle.d.ts.map +1 -1
  36. package/dist/presentation/orchestration/buildScanPresentationBundle.js +12 -5
  37. package/dist/presentation/orchestration/buildScanPresentationBundle.test.d.ts +2 -0
  38. package/dist/presentation/orchestration/buildScanPresentationBundle.test.d.ts.map +1 -0
  39. package/dist/presentation/orchestration/buildScanPresentationBundle.test.js +99 -0
  40. package/dist/presentation/orchestration/scanPresentationBundle.d.ts +1 -1
  41. package/dist/presentation/orchestration/scanPresentationBundle.d.ts.map +1 -1
  42. package/dist/presentation/presentation.test.js +1 -1
  43. package/dist/presentation/presentationPersonas.test.js +0 -5
  44. package/dist/presentation/profile/buildProfileFromAssessment.d.ts +1 -1
  45. package/dist/presentation/profile/buildProfileFromAssessment.d.ts.map +1 -1
  46. package/dist/presentation/profile/buildProfileFromAssessment.js +13 -2
  47. package/dist/presentation/surfaceParity.test.js +73 -5
  48. package/dist/repoIntelligenceSchema.d.ts +3 -3
  49. package/dist/riskSignals.d.ts +19 -0
  50. package/dist/riskSignals.d.ts.map +1 -0
  51. package/dist/riskSignals.js +32 -0
  52. package/dist/scanNarrativeFacts.d.ts +25 -7
  53. package/dist/scanNarrativeFacts.d.ts.map +1 -1
  54. package/dist/scanNarrativeFacts.js +4 -1
  55. package/dist/scanPresentationBundle.golden.test.d.ts +2 -0
  56. package/dist/scanPresentationBundle.golden.test.d.ts.map +1 -0
  57. package/dist/scanPresentationBundle.golden.test.js +78 -0
  58. package/dist/scanResultSchema.d.ts +53 -4
  59. package/dist/scanResultSchema.d.ts.map +1 -1
  60. package/dist/scanResultSchema.js +1 -1
  61. package/dist/scanResultSchema.test.js +26 -0
  62. package/dist/types.d.ts +1 -1
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +4 -2
  65. package/dist/assessmentSchema.d.ts +0 -155
  66. package/dist/assessmentSchema.d.ts.map +0 -1
  67. package/dist/assessmentSchema.js +0 -110
@@ -0,0 +1,14 @@
1
+ import type { AffectedAreaFact, ChangedPackageSemantics, NarrativePackageUsage, ScanNarrativeFacts } from "./scanNarrativeFacts.js";
2
+ import type { Finding } from "./types.js";
3
+ export declare function areaIdFromLabel(label: string): string;
4
+ export declare function enrichAffectedAreaFacts(areas: Array<{
5
+ id: string;
6
+ label: string;
7
+ }>, ctx: {
8
+ findings: Finding[];
9
+ packageUsage: NarrativePackageUsage[];
10
+ changedPackageSemantics: ChangedPackageSemantics[];
11
+ hotspots: ScanNarrativeFacts["hotspots"];
12
+ applicationAreaIds: ReadonlySet<string>;
13
+ }): AffectedAreaFact[];
14
+ //# sourceMappingURL=affectedAreaLinkage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affectedAreaLinkage.d.ts","sourceRoot":"","sources":["../src/affectedAreaLinkage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EAErB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ1C,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAuGD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAC3C,GAAG,EAAE;IACH,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,EAAE,qBAAqB,EAAE,CAAC;IACtC,uBAAuB,EAAE,uBAAuB,EAAE,CAAC;IACnD,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACzC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACzC,GACA,gBAAgB,EAAE,CAwBpB"}
@@ -0,0 +1,100 @@
1
+ const EVIDENCE_STRENGTH_RANK = {
2
+ high: 3,
3
+ medium: 2,
4
+ low: 1,
5
+ };
6
+ export function areaIdFromLabel(label) {
7
+ return label.toLowerCase().replace(/\s+/g, "_").slice(0, 48);
8
+ }
9
+ function packagesForArea(area, packageUsage, applicationAreaIds) {
10
+ const linked = new Set();
11
+ for (const row of packageUsage) {
12
+ for (const usageArea of row.areas) {
13
+ if (areaIdFromLabel(usageArea) === area.id ||
14
+ usageArea.toLowerCase() === area.label.toLowerCase()) {
15
+ linked.add(row.packageName);
16
+ }
17
+ }
18
+ }
19
+ if (applicationAreaIds.has(area.id)) {
20
+ for (const row of packageUsage) {
21
+ linked.add(row.packageName);
22
+ }
23
+ }
24
+ return [...linked].sort((a, b) => a.localeCompare(b));
25
+ }
26
+ function findingIdsForPackages(findings, packages) {
27
+ if (packages.length === 0)
28
+ return [];
29
+ const pkgSet = new Set(packages);
30
+ return findings.filter((f) => pkgSet.has(f.packageName)).map((f) => f.id);
31
+ }
32
+ function pathsForPackages(packageUsage, packages) {
33
+ const pkgSet = new Set(packages);
34
+ const paths = [];
35
+ for (const row of packageUsage) {
36
+ if (!pkgSet.has(row.packageName))
37
+ continue;
38
+ paths.push(...row.paths, ...row.criticalPaths, ...row.files);
39
+ }
40
+ const seen = new Set();
41
+ return paths.filter((p) => {
42
+ if (!p || seen.has(p))
43
+ return false;
44
+ seen.add(p);
45
+ return true;
46
+ });
47
+ }
48
+ function evidenceStrengthForPackages(semantics, packages) {
49
+ const pkgSet = new Set(packages);
50
+ let best = null;
51
+ let bestRank = 0;
52
+ for (const row of semantics) {
53
+ if (!pkgSet.has(row.packageName) || !row.evidenceStrength)
54
+ continue;
55
+ const rank = EVIDENCE_STRENGTH_RANK[row.evidenceStrength];
56
+ if (rank > bestRank) {
57
+ bestRank = rank;
58
+ best = row.evidenceStrength;
59
+ }
60
+ }
61
+ return best;
62
+ }
63
+ function hotspotPackagesForPackages(hotspots, packages) {
64
+ const pkgSet = new Set(packages);
65
+ const linked = new Set();
66
+ for (const hotspot of hotspots) {
67
+ if (pkgSet.has(hotspot.packageName)) {
68
+ linked.add(hotspot.packageName);
69
+ }
70
+ }
71
+ return [...linked].sort((a, b) => a.localeCompare(b));
72
+ }
73
+ function verificationFocusForPackages(semantics, packages) {
74
+ const pkgSet = new Set(packages);
75
+ const focus = new Set();
76
+ for (const row of semantics) {
77
+ if (!pkgSet.has(row.packageName))
78
+ continue;
79
+ for (const item of row.verificationFocus) {
80
+ if (item.trim())
81
+ focus.add(item.trim());
82
+ }
83
+ }
84
+ return [...focus].sort((a, b) => a.localeCompare(b));
85
+ }
86
+ export function enrichAffectedAreaFacts(areas, ctx) {
87
+ return areas.map((area) => {
88
+ const packages = packagesForArea(area, ctx.packageUsage, ctx.applicationAreaIds);
89
+ return {
90
+ id: area.id,
91
+ label: area.label,
92
+ packages,
93
+ findingIds: findingIdsForPackages(ctx.findings, packages),
94
+ paths: pathsForPackages(ctx.packageUsage, packages),
95
+ evidenceStrength: evidenceStrengthForPackages(ctx.changedPackageSemantics, packages),
96
+ hotspotPackages: hotspotPackagesForPackages(ctx.hotspots, packages),
97
+ verificationFocus: verificationFocusForPackages(ctx.changedPackageSemantics, packages),
98
+ };
99
+ });
100
+ }
@@ -1,4 +1,4 @@
1
- import type { MergeConcernKind } from "./assessmentSchema.js";
1
+ import type { MergeConcernKind } from "@mergesignal/contracts";
2
2
  /** Display labels for factor tokens (mirrors engine factor-registry labels). */
3
3
  export declare const ASSESSMENT_FACTOR_LABELS: Record<string, string>;
4
4
  export declare const MERGE_CONCERN_LABELS: Record<MergeConcernKind, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"assessmentLabels.d.ts","sourceRoot":"","sources":["../src/assessmentLabels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmB3D,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAcjE,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D"}
1
+ {"version":3,"file":"assessmentLabels.d.ts","sourceRoot":"","sources":["../src/assessmentLabels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmB3D,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAcjE,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -0,0 +1,6 @@
1
+ import type { AssessmentPresentationPublic, AssessmentPresentationWire, NarrativeIntensity } from "@mergesignal/contracts";
2
+ /** Public presentation subset — excludes engine policy fields. */
3
+ export declare function toPublicPresentation(wire: AssessmentPresentationWire): AssessmentPresentationPublic;
4
+ /** Map engine narrative intensity to presentation density (formatting only). */
5
+ export declare function narrativeIntensityToDensity(intensity: NarrativeIntensity): "minimal" | "standard" | "rich";
6
+ //# sourceMappingURL=assessmentPresentationUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assessmentPresentationUtils.d.ts","sourceRoot":"","sources":["../src/assessmentPresentationUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,0BAA0B,EAC1B,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAEhC,kEAAkE;AAClE,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,0BAA0B,GAC/B,4BAA4B,CAO9B;AAED,gFAAgF;AAChF,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,GAC5B,SAAS,GAAG,UAAU,GAAG,MAAM,CAIjC"}
@@ -0,0 +1,17 @@
1
+ /** Public presentation subset — excludes engine policy fields. */
2
+ export function toPublicPresentation(wire) {
3
+ return {
4
+ narrativeIntensity: wire.narrativeIntensity,
5
+ reachVisibility: wire.reachVisibility,
6
+ verificationIntensity: wire.verificationIntensity,
7
+ reportMode: wire.reportMode,
8
+ };
9
+ }
10
+ /** Map engine narrative intensity to presentation density (formatting only). */
11
+ export function narrativeIntensityToDensity(intensity) {
12
+ if (intensity === "minimal")
13
+ return "minimal";
14
+ if (intensity === "elevated")
15
+ return "rich";
16
+ return "standard";
17
+ }
@@ -1,10 +1,10 @@
1
1
  import type { ScanResult } from "./types.js";
2
2
  import { type RepoIntelligence } from "./repoIntelligenceSchema.js";
3
- import type { Assessment, AssessmentPresentationPublic, ReachVisibility } from "./assessmentSchema.js";
3
+ import type { Assessment, AssessmentPresentationPublic, ReachVisibility } from "@mergesignal/contracts";
4
4
  import type { PresentationStatus } from "./presentation/dto/types.js";
5
5
  export declare function collectVerificationFocus(result: ScanResult): string[];
6
6
  export declare function reachVisibilityLabel(visibility: ReachVisibility): string | undefined;
7
- export declare function formatAssessmentHeadline(assessment: Assessment, result: ScanResult, status: PresentationStatus): string;
7
+ export declare function formatAssessmentHeadline(assessment: Assessment, status: PresentationStatus): string;
8
8
  export declare function projectReasoningLines(result: ScanResult): string[];
9
9
  export declare function projectInsightLines(assessment: Assessment, result: ScanResult): string[];
10
10
  export declare function projectVerificationActions(assessment: Assessment, presentation: AssessmentPresentationPublic, result: ScanResult, max: number): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"assessmentProjection.d.ts","sourceRoot":"","sources":["../src/assessmentProjection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC5B,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAO/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAgBrE;AAED,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,eAAe,GAC1B,MAAM,GAAG,SAAS,CAWpB;AAED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,kBAAkB,GACzB,MAAM,CAqDR;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAIlE;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,UAAU,GACjB,MAAM,EAAE,CAaV;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,4BAA4B,EAC1C,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAIV;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,GAAG,MAAM,EAAE,CAI3E"}
1
+ {"version":3,"file":"assessmentProjection.d.ts","sourceRoot":"","sources":["../src/assessmentProjection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EACV,UAAU,EACV,4BAA4B,EAC5B,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAgCtE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAkBrE;AAED,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,eAAe,GAC1B,MAAM,GAAG,SAAS,CAWpB;AAED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,kBAAkB,GACzB,MAAM,CAiDR;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAIlE;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,UAAU,GACjB,MAAM,EAAE,CAaV;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,4BAA4B,EAC1C,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAIV;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,GAAG,MAAM,EAAE,CAI3E"}
@@ -2,14 +2,47 @@ import { safeParseRepoIntelligence, } from "./repoIntelligenceSchema.js";
2
2
  import { labelAssessmentFactor, MERGE_CONCERN_LABELS, } from "./assessmentLabels.js";
3
3
  import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
4
4
  import { applyPostureVocabularyGuardHeadline } from "./presentation/intent/applyPostureVocabularyGuard.js";
5
+ function focalDisplayPackage(assessment) {
6
+ const anchors = assessment.reviewFocalPoint.anchors;
7
+ if (anchors.length === 0)
8
+ return "dependency";
9
+ const token = anchors[0];
10
+ if (token === "dependency_graph")
11
+ return "dependencies";
12
+ if (token.includes("+"))
13
+ return token.split("+")[0];
14
+ return token;
15
+ }
16
+ function focalHeadlinePackages(assessment) {
17
+ const focal = assessment.reviewFocalPoint;
18
+ if (focal.episodeShape === "multi_anchor") {
19
+ return focal.anchors.join(", ");
20
+ }
21
+ return focalDisplayPackage(assessment);
22
+ }
23
+ /** Copy-formatting only — fills {surface} in runtime headline template. */
24
+ function surfaceHintFromAssessment(assessment) {
25
+ const factors = assessment.factors;
26
+ const anchor = focalDisplayPackage(assessment);
27
+ if (anchor === "bullmq" || factors.includes("queue_infrastructure")) {
28
+ return "workers and queues";
29
+ }
30
+ if (anchor === "next-auth" || factors.includes("auth_infrastructure")) {
31
+ return "authentication flows";
32
+ }
33
+ return "application code";
34
+ }
5
35
  export function collectVerificationFocus(result) {
36
+ const scope = result.assessment?.verificationScope;
37
+ if (scope?.focus?.length)
38
+ return [...scope.focus];
39
+ const packages = scope?.packages ?? [];
6
40
  const parsed = safeParseRepoIntelligence(result.repoIntelligence);
7
41
  if (!parsed.ok)
8
42
  return [];
9
- const changed = new Set(result.changedPackages ?? []);
10
43
  const labels = [];
11
44
  const seen = new Set();
12
- for (const pkg of changed) {
45
+ for (const pkg of packages) {
13
46
  const row = parsed.value.packages[pkg];
14
47
  for (const focus of row?.verificationFocus ?? []) {
15
48
  const t = focus.trim();
@@ -33,13 +66,13 @@ export function reachVisibilityLabel(visibility) {
33
66
  return undefined;
34
67
  }
35
68
  }
36
- export function formatAssessmentHeadline(assessment, result, status) {
69
+ export function formatAssessmentHeadline(assessment, status) {
37
70
  const copy = scanSurfaceCopy.presentation;
38
- const runtimePkg = assessment.concerns.find((c) => c.kind === "confirmed_runtime_usage")?.packages?.[0];
39
- const primaryPkg = runtimePkg ??
40
- result.changedPackages?.[0] ??
41
- assessment.concerns.find((c) => c.packages?.length)?.packages?.[0] ??
42
- "dependency";
71
+ const primaryPkg = focalHeadlinePackages(assessment);
72
+ if (assessment.reviewFocalPoint.episodeShape === "structural") {
73
+ const headline = copy.defaultUpgradeHeadline.replace("{package}", "dependencies");
74
+ return applyPostureVocabularyGuardHeadline(headline, status, null);
75
+ }
43
76
  if (assessment.primaryConcern === "insufficient_evidence") {
44
77
  return applyPostureVocabularyGuardHeadline(copy.limitedContextHeadline, status, primaryPkg);
45
78
  }
@@ -50,11 +83,7 @@ export function formatAssessmentHeadline(assessment, result, status) {
50
83
  return applyPostureVocabularyGuardHeadline(headline, status, primaryPkg);
51
84
  }
52
85
  if (assessment.primaryConcern === "confirmed_runtime_usage") {
53
- const surface = assessment.factors.includes("queue_infrastructure")
54
- ? "workers and queues"
55
- : assessment.factors.includes("auth_infrastructure")
56
- ? "authentication flows"
57
- : "application code";
86
+ const surface = surfaceHintFromAssessment(assessment);
58
87
  const headline = copy.runtimeUpgradeHeadline
59
88
  .replace("{package}", primaryPkg)
60
89
  .replace("{surface}", surface);
@@ -1 +1 @@
1
- {"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"AAOA,OAAO,EAeL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAKV,UAAU,EACX,MAAM,YAAY,CAAC;AA+dpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAwGpB"}
1
+ {"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"AAYA,OAAO,EAgBL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAMV,UAAU,EACX,MAAM,YAAY,CAAC;AAoiBpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAsIpB"}
@@ -1,6 +1,8 @@
1
1
  import { extractRepositoryContextFacts } from "./extractRepositoryContextFacts.js";
2
+ import { areaIdFromLabel, enrichAffectedAreaFacts, } from "./affectedAreaLinkage.js";
2
3
  import { readBlastLevel, safeParseRepoIntelligence, } from "./repoIntelligenceSchema.js";
3
4
  import { isRuntimeNarrativeSafe } from "./repoIntelligenceSemantics.js";
5
+ import { deriveRiskSignals } from "./riskSignals.js";
4
6
  import { EMPTY_SCAN_NARRATIVE_FACTS, NARRATIVE_FACT_MAX_AREAS, NARRATIVE_FACT_MAX_HOTSPOTS, NARRATIVE_FACT_MAX_PATHS, } from "./scanNarrativeFacts.js";
5
7
  import { mergePostureFromDecision } from "./riskVocabulary.js";
6
8
  import { selectTopAffectedAreas } from "./selectTopAffectedAreas.js";
@@ -14,6 +16,25 @@ function parseChangedPackages(changed) {
14
16
  all,
15
17
  };
16
18
  }
19
+ function changedPackagesFromAssessment(result) {
20
+ const assessment = result.assessment;
21
+ const focal = assessment?.reviewFocalPoint;
22
+ // Follow-up: remove parseChangedPackages fallback after historical scan re-scan window.
23
+ if (focal?.anchors?.length && focal.anchors[0] !== "dependency_graph") {
24
+ const token = focal.anchors[0];
25
+ const primary = token.includes("+") ? token.split("+")[0] : token;
26
+ const supporting = focal.supportingPackages ?? [];
27
+ const all = [
28
+ ...new Set([primary, ...supporting, ...(result.changedPackages ?? [])]),
29
+ ].sort((a, b) => a.localeCompare(b));
30
+ return {
31
+ primary,
32
+ others: all.filter((p) => p !== primary),
33
+ all,
34
+ };
35
+ }
36
+ return parseChangedPackages(result.changedPackages);
37
+ }
17
38
  function blastLevelFromChangedCount(count) {
18
39
  if (count >= 8)
19
40
  return "wide";
@@ -205,13 +226,15 @@ function extractTier1FromRepoIntelligence(ri, changed) {
205
226
  }
206
227
  : null;
207
228
  const areasRaw = ri.applicationAreas ?? ri.affectedAreas ?? [];
229
+ const applicationAreaIds = new Set();
208
230
  const affectedAreas = [];
209
231
  for (const area of areasRaw) {
232
+ applicationAreaIds.add(area.id);
210
233
  affectedAreas.push({ id: area.id, label: area.label });
211
234
  }
212
235
  for (const row of packageUsage) {
213
236
  for (const label of row.areas) {
214
- const id = label.toLowerCase().replace(/\s+/g, "_");
237
+ const id = areaIdFromLabel(label);
215
238
  if (!affectedAreas.some((a) => a.id === id)) {
216
239
  affectedAreas.push({ id, label });
217
240
  }
@@ -228,6 +251,7 @@ function extractTier1FromRepoIntelligence(ri, changed) {
228
251
  blastRadius,
229
252
  affectedAreas: affectedAreas.slice(0, NARRATIVE_FACT_MAX_AREAS),
230
253
  hotspots: codeHotspots.slice(0, NARRATIVE_FACT_MAX_HOTSPOTS),
254
+ applicationAreaIds,
231
255
  };
232
256
  }
233
257
  function hasMeaningfulTier1(tier1) {
@@ -322,6 +346,7 @@ function emptyTier1Blocks() {
322
346
  blastRadius: null,
323
347
  affectedAreas: [],
324
348
  hotspots: [],
349
+ applicationAreaIds: new Set(),
325
350
  };
326
351
  }
327
352
  function blastRadiusFromChangedCount(count) {
@@ -349,6 +374,38 @@ function resolveTrustedRepoIntelligence(result) {
349
374
  }
350
375
  return { parsed: null, parseStatus: "invalid" };
351
376
  }
377
+ function preparationWarningsFromResult(result) {
378
+ const warnings = result.analysisPreparation?.warnings;
379
+ if (!Array.isArray(warnings))
380
+ return [];
381
+ return warnings.map((w) => ({
382
+ code: w.code,
383
+ message: w.message,
384
+ ...(w.details != null ? { details: w.details } : {}),
385
+ }));
386
+ }
387
+ function resolveCorpusGateReason(input) {
388
+ if (input.corpusGate)
389
+ return "ok";
390
+ if (!input.codeIntelligenceAvailable)
391
+ return "no_code_intelligence";
392
+ if (input.repoIntelligenceParse === "invalid" ||
393
+ input.repoIntelligenceParse === "untrusted") {
394
+ return "repo_intelligence_invalid";
395
+ }
396
+ return "repo_intelligence_absent";
397
+ }
398
+ function assessmentConfidenceFromResult(result) {
399
+ const c = result.assessment?.confidence;
400
+ if (c === "low" || c === "medium" || c === "high")
401
+ return c;
402
+ return null;
403
+ }
404
+ function limitedContextDiagnostic(input) {
405
+ return (input.corpusGateReason !== "ok" ||
406
+ input.preparationWarnings.length > 0 ||
407
+ input.assessmentConfidence === "low");
408
+ }
352
409
  /**
353
410
  * Consumer-agnostic scan intelligence — structured facts only (no UI copy).
354
411
  */
@@ -356,25 +413,31 @@ export function deriveScanNarrative(result) {
356
413
  if (!result) {
357
414
  return { ...EMPTY_SCAN_NARRATIVE_FACTS };
358
415
  }
359
- const changedPackages = parseChangedPackages(result.changedPackages);
416
+ const changedPackages = changedPackagesFromAssessment(result);
360
417
  const mergePosture = mergePostureFromDecision(result.decision?.recommendation) ?? null;
361
- const riskIndex = typeof result.totalScore === "number" && Number.isFinite(result.totalScore)
362
- ? result.totalScore
363
- : null;
418
+ const preparationWarnings = preparationWarningsFromResult(result);
364
419
  const codeIntelligenceAvailable = result.analysisPreparation?.codeIntelligenceAvailable !== false;
365
420
  const { parsed: parsedRi, parseStatus: repoIntelligenceParse } = resolveTrustedRepoIntelligence(result);
366
421
  const corpusGate = codeIntelligenceAvailable &&
367
422
  repoIntelligenceParse === "ok" &&
368
423
  parsedRi != null;
424
+ const corpusGateReason = resolveCorpusGateReason({
425
+ codeIntelligenceAvailable,
426
+ repoIntelligenceParse,
427
+ corpusGate,
428
+ });
369
429
  let tier1Blocks = emptyTier1Blocks();
430
+ let applicationAreaIds = tier1Blocks.applicationAreaIds;
370
431
  if (corpusGate && parsedRi) {
371
432
  tier1Blocks = extractTier1FromRepoIntelligence(parsedRi, changedPackages);
433
+ applicationAreaIds = tier1Blocks.applicationAreaIds;
372
434
  }
373
435
  else if (changedPackages.all.length > 0) {
374
436
  tier1Blocks = {
375
437
  ...emptyTier1Blocks(),
376
438
  blastRadius: blastRadiusFromChangedCount(changedPackages.all.length),
377
439
  };
440
+ applicationAreaIds = tier1Blocks.applicationAreaIds;
378
441
  }
379
442
  tier1Blocks.hotspots = mergeGraphHotspots(tier1Blocks.hotspots, result.graphInsights?.hotspots);
380
443
  const tier1 = corpusGate && hasMeaningfulTier1(tier1Blocks);
@@ -384,23 +447,40 @@ export function deriveScanNarrative(result) {
384
447
  const tier3 = repositoryContext.length > 0;
385
448
  const mode = resolveAvailabilityMode(tier1, tier2 || reviewerGuidance.length > 0, codeIntelligenceAvailable);
386
449
  const decisionConf = result.decision?.confidence;
450
+ const assessmentConfidence = assessmentConfidenceFromResult(result);
387
451
  const confidence = {
388
452
  decision: decisionConf === "low" ||
389
453
  decisionConf === "medium" ||
390
454
  decisionConf === "high"
391
455
  ? decisionConf
392
456
  : null,
457
+ assessment: assessmentConfidence,
458
+ limitedContext: limitedContextDiagnostic({
459
+ corpusGateReason,
460
+ preparationWarnings,
461
+ assessmentConfidence,
462
+ }),
393
463
  };
394
464
  if (!tier1 && mode === "graph_fallback" && changedPackages.all.length > 0) {
395
465
  tier1Blocks.blastRadius = blastRadiusFromChangedCount(changedPackages.all.length);
396
466
  }
397
- let affectedAreas = tier1Blocks.affectedAreas;
398
- if (affectedAreas.length === 0 && mode !== "pr_intelligence") {
399
- affectedAreas = selectTopAffectedAreas(result, { max: 2 }).map((label) => ({
400
- id: label.toLowerCase().replace(/\s+/g, "_").slice(0, 48),
467
+ let affectedAreaSeeds = tier1Blocks.affectedAreas;
468
+ if (affectedAreaSeeds.length === 0 && mode !== "pr_intelligence") {
469
+ affectedAreaSeeds = selectTopAffectedAreas(result, { max: 2 }).map((label) => ({
470
+ id: areaIdFromLabel(label),
401
471
  label,
402
472
  }));
403
473
  }
474
+ const findings = Array.isArray(result.findings) ? result.findings : [];
475
+ const affectedAreas = enrichAffectedAreaFacts(affectedAreaSeeds, {
476
+ findings,
477
+ packageUsage: tier1Blocks.packageUsage,
478
+ changedPackageSemantics: tier1Blocks.changedPackageSemantics,
479
+ hotspots: tier1Blocks.hotspots,
480
+ applicationAreaIds,
481
+ });
482
+ const riskSignals = deriveRiskSignals(result);
483
+ const riskIndex = riskSignals?.riskIndex ?? null;
404
484
  return {
405
485
  availability: {
406
486
  mode,
@@ -411,6 +491,8 @@ export function deriveScanNarrative(result) {
411
491
  tier3,
412
492
  },
413
493
  repoIntelligenceParse,
494
+ preparationWarnings,
495
+ corpusGateReason,
414
496
  },
415
497
  changedPackages,
416
498
  packageUsage: tier1Blocks.packageUsage,
@@ -427,5 +509,6 @@ export function deriveScanNarrative(result) {
427
509
  repositoryContext,
428
510
  mergePosture,
429
511
  riskIndex,
512
+ riskSignals,
430
513
  };
431
514
  }
@@ -3,6 +3,7 @@ import { deriveScanNarrative } from "./deriveScanNarrative.js";
3
3
  import { legacyEngineRepoIntelligenceJwt } from "./fixtures/legacyEngineRepoIntelligence.js";
4
4
  import { fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceMultiPackage, fixtureRepoIntelligenceTypescript, } from "./fixtures/repoIntelligenceFixtures.js";
5
5
  import { analysisPreparationWithInvalidRepoIntel, analysisPreparationWithValidRepoIntel, } from "./fixtures/repoIntelligenceTestHelpers.js";
6
+ import { assessmentFastifyRuntime } from "./fixtures/assessmentFixtures.js";
6
7
  const baseResult = {
7
8
  totalScore: 10,
8
9
  layerScores: {
@@ -64,6 +65,18 @@ describe("deriveScanNarrative", () => {
64
65
  expect(facts.affectedAreas.map((a) => a.id)).toContain("api");
65
66
  expect(facts.hotspots[0]?.source).toBe("code");
66
67
  });
68
+ it("changedPackages.primary follows reviewFocalPoint anchor, not changedPackages order", () => {
69
+ const result = {
70
+ ...baseResult,
71
+ changedPackages: ["typescript", "fastify"],
72
+ assessment: assessmentFastifyRuntime,
73
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
74
+ repoIntelligence: fixtureRepoIntelligenceFastify,
75
+ };
76
+ const facts = deriveScanNarrative(result);
77
+ expect(facts.changedPackages.primary).toBe("fastify");
78
+ expect(result.changedPackages?.[0]).toBe("typescript");
79
+ });
67
80
  it("prefers changed-scope insights for reviewer guidance ordering", () => {
68
81
  const result = {
69
82
  ...baseResult,
@@ -197,4 +210,65 @@ describe("deriveScanNarrative", () => {
197
210
  expect(facts.availability.repoIntelligenceParse).toBe("absent");
198
211
  expect(facts.availability.tiersPresent.tier1).toBe(false);
199
212
  });
213
+ it("projects preparation warnings and corpus gate reason", () => {
214
+ const result = {
215
+ ...baseResult,
216
+ changedPackages: ["lodash"],
217
+ analysisPreparation: {
218
+ codeIntelligenceAvailable: false,
219
+ warnings: [
220
+ {
221
+ code: "base_lockfile_missing",
222
+ message: "Base lockfile not available",
223
+ },
224
+ ],
225
+ },
226
+ };
227
+ const facts = deriveScanNarrative(result);
228
+ expect(facts.availability.preparationWarnings).toHaveLength(1);
229
+ expect(facts.availability.preparationWarnings[0]?.code).toBe("base_lockfile_missing");
230
+ expect(facts.availability.corpusGateReason).toBe("no_code_intelligence");
231
+ expect(facts.confidence.limitedContext).toBe(true);
232
+ });
233
+ it("links affected areas to packages, findings, paths, and verification", () => {
234
+ const result = {
235
+ ...baseResult,
236
+ changedPackages: ["fastify"],
237
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
238
+ repoIntelligence: fixtureRepoIntelligenceFastify,
239
+ findings: [
240
+ {
241
+ id: "f1",
242
+ title: "Auth middleware",
243
+ description: "d",
244
+ severity: "high",
245
+ packageName: "fastify",
246
+ },
247
+ ],
248
+ };
249
+ const facts = deriveScanNarrative(result);
250
+ const apiArea = facts.affectedAreas.find((a) => a.id === "api");
251
+ expect(apiArea).toBeDefined();
252
+ expect(apiArea?.packages).toContain("fastify");
253
+ expect(apiArea?.findingIds).toContain("f1");
254
+ expect(apiArea?.paths.length).toBeGreaterThan(0);
255
+ expect(apiArea?.evidenceStrength).toBe("high");
256
+ expect(apiArea?.hotspotPackages).toContain("fastify");
257
+ expect(apiArea?.verificationFocus).toContain("routes");
258
+ });
259
+ it("derives riskSignals with exposure layers", () => {
260
+ const facts = deriveScanNarrative(baseResult);
261
+ expect(facts.riskSignals?.riskIndex).toBe(10);
262
+ expect(facts.riskSignals?.exposure).toBe("minimal");
263
+ expect(facts.riskSignals?.layers).toHaveLength(4);
264
+ expect(facts.riskIndex).toBe(10);
265
+ });
266
+ it("projects assessment confidence verbatim", () => {
267
+ const result = {
268
+ ...baseResult,
269
+ assessment: assessmentFastifyRuntime,
270
+ };
271
+ const facts = deriveScanNarrative(result);
272
+ expect(facts.confidence.assessment).toBe("medium");
273
+ });
200
274
  });
@@ -70,8 +70,8 @@ describe("engine wire regression (canonical fixtures)", () => {
70
70
  const facts = deriveScanNarrative(result);
71
71
  expect(facts.availability.mode).toBe("pr_intelligence");
72
72
  expect(facts.packageUsage.map((u) => u.packageName)).toEqual([
73
- "lodash",
74
73
  "axios",
74
+ "lodash",
75
75
  ]);
76
76
  });
77
77
  });
@@ -1,4 +1,4 @@
1
- import type { Assessment } from "../assessmentSchema.js";
1
+ import type { Assessment } from "@mergesignal/contracts";
2
2
  export declare const assessmentTypescriptPatch: Assessment;
3
3
  export declare const assessmentPrettier: Assessment;
4
4
  export declare const assessmentEslint: Assessment;
@@ -6,6 +6,7 @@ export declare const assessmentVitest: Assessment;
6
6
  export declare const assessmentFastifyRuntime: Assessment;
7
7
  export declare const assessmentBullmq: Assessment;
8
8
  export declare const assessmentNextAuth: Assessment;
9
+ /** PR #26 — focal fastify despite typescript in changedPackages order. */
9
10
  export declare const assessmentMixedTypescriptFastify: Assessment;
10
11
  export declare const assessmentLimitedContext: Assessment;
11
12
  export declare const assessmentStub: Assessment;
@@ -1 +1 @@
1
- {"version":3,"file":"assessmentFixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/assessmentFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAkBzD,eAAO,MAAM,yBAAyB,EAAE,UAQvC,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,UAGhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAE9B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAQ9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAetC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAmB9B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,UAehC,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,UAe9C,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAoBtC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,UAoB5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,UAcnC,CAAC"}
1
+ {"version":3,"file":"assessmentFixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/assessmentFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA0BzD,eAAO,MAAM,yBAAyB,EAAE,UAevC,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,UAUhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAS9B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAe9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UAyBtC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UA6B9B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,UAyBhC,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,EAAE,UAgC9C,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,UA2BtC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,UA6B5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,UAqBnC,CAAC"}