@mergesignal/shared 0.2.7 → 0.2.8

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 (58) hide show
  1. package/dist/actionsStepSummary.d.ts.map +1 -1
  2. package/dist/actionsStepSummary.js +2 -29
  3. package/dist/collectNarrativeWhyBullets.d.ts +12 -0
  4. package/dist/collectNarrativeWhyBullets.d.ts.map +1 -0
  5. package/dist/collectNarrativeWhyBullets.js +104 -0
  6. package/dist/deriveScanNarrative.d.ts.map +1 -1
  7. package/dist/deriveScanNarrative.js +157 -54
  8. package/dist/deriveScanNarrative.test.js +56 -1
  9. package/dist/fixtures/repoIntelligenceFixtures.d.ts +2 -0
  10. package/dist/fixtures/repoIntelligenceFixtures.d.ts.map +1 -1
  11. package/dist/fixtures/repoIntelligenceFixtures.js +46 -0
  12. package/dist/formatInsight.d.ts +2 -2
  13. package/dist/formatInsight.d.ts.map +1 -1
  14. package/dist/formatInsight.js +2 -7
  15. package/dist/formatInsight.test.js +1 -1
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -0
  19. package/dist/narrativeParity.test.d.ts +2 -0
  20. package/dist/narrativeParity.test.d.ts.map +1 -0
  21. package/dist/narrativeParity.test.js +64 -0
  22. package/dist/narrativePresentation.d.ts +50 -0
  23. package/dist/narrativePresentation.d.ts.map +1 -0
  24. package/dist/narrativePresentation.js +180 -0
  25. package/dist/narrativePresentation.test.d.ts +2 -0
  26. package/dist/narrativePresentation.test.d.ts.map +1 -0
  27. package/dist/narrativePresentation.test.js +53 -0
  28. package/dist/normalizeGeneratedText.d.ts +8 -0
  29. package/dist/normalizeGeneratedText.d.ts.map +1 -0
  30. package/dist/normalizeGeneratedText.js +31 -0
  31. package/dist/normalizeGeneratedText.test.d.ts +2 -0
  32. package/dist/normalizeGeneratedText.test.d.ts.map +1 -0
  33. package/dist/normalizeGeneratedText.test.js +18 -0
  34. package/dist/prCheckRunPresentation.d.ts +4 -3
  35. package/dist/prCheckRunPresentation.d.ts.map +1 -1
  36. package/dist/prCheckRunPresentation.js +56 -51
  37. package/dist/prCheckRunPresentation.test.js +2 -2
  38. package/dist/presentGitHubPrComment.d.ts +9 -0
  39. package/dist/presentGitHubPrComment.d.ts.map +1 -0
  40. package/dist/presentGitHubPrComment.js +80 -0
  41. package/dist/presentScanCardSummary.d.ts.map +1 -1
  42. package/dist/presentScanCardSummary.js +69 -64
  43. package/dist/riskVocabulary.js +1 -1
  44. package/dist/scanCardSummary.d.ts +9 -1
  45. package/dist/scanCardSummary.d.ts.map +1 -1
  46. package/dist/scanCardSummary.js +1 -1
  47. package/dist/scanCardSummary.test.js +12 -8
  48. package/dist/scanDetailViewModel.d.ts +9 -1
  49. package/dist/scanDetailViewModel.d.ts.map +1 -1
  50. package/dist/scanDetailViewModel.js +164 -37
  51. package/dist/scanDetailViewModel.test.js +27 -1
  52. package/dist/scanNarrativeFacts.d.ts +21 -8
  53. package/dist/scanNarrativeFacts.d.ts.map +1 -1
  54. package/dist/scanNarrativeFacts.js +6 -0
  55. package/dist/scanSurfaceCopy.d.ts +26 -26
  56. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  57. package/dist/scanSurfaceCopy.js +26 -26
  58. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"actionsStepSummary.d.ts","sourceRoot":"","sources":["../src/actionsStepSummary.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAEV,SAAS,EACT,cAAc,EACd,UAAU,EACV,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,aAAa,CAAC;AAE9D,8EAA8E;AAC9E,eAAO,MAAM,iCAAiC,KAAK,CAAC;AACpD,gDAAgD;AAChD,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,6EAA6E;AAC7E,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,4DAA4D;AAC5D,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAyBnD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAK3E;AAOD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA6C7D;AA4ED,8GAA8G;AAC9G,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,IAAI,CAiBf;AASD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAa3E;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,cAAc,EAAE,GACrB,cAAc,EAAE,CAelB;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxE;AAaD,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxE;AAgiBD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,GAAG,MAAM,CAoJT"}
1
+ {"version":3,"file":"actionsStepSummary.d.ts","sourceRoot":"","sources":["../src/actionsStepSummary.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,SAAS,EACT,cAAc,EACd,UAAU,EACV,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,aAAa,CAAC;AAE9D,8EAA8E;AAC9E,eAAO,MAAM,iCAAiC,KAAK,CAAC;AACpD,gDAAgD;AAChD,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,6EAA6E;AAC7E,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,4DAA4D;AAC5D,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAyBnD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAK3E;AAOD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA6C7D;AA4ED,8GAA8G;AAC9G,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,IAAI,CAiBf;AASD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAa3E;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,cAAc,EAAE,GACrB,cAAc,EAAE,CAelB;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxE;AAaD,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxE;AAygBD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,GAAG,MAAM,CAoJT"}
@@ -9,7 +9,7 @@
9
9
  * - `## Recommended actions` + time heuristics → default “what to do” + `<details> More actions`
10
10
  * - `<details> Risk breakdown` + layer table + graph one-liner → score + graph `<details>` blocks
11
11
  */
12
- import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
12
+ import { collectWhyBulletsFromResult } from "./collectNarrativeWhyBullets.js";
13
13
  import { formatInsight } from "./formatInsight.js";
14
14
  import { mergePostureLabel } from "./riskVocabulary.js";
15
15
  import { scanSurfaceCopy } from "./scanSurfaceCopy.js";
@@ -516,34 +516,7 @@ function buildTrustedTitleLines(result, flat) {
516
516
  return titleLines;
517
517
  }
518
518
  function collectWhyBullets(result, max) {
519
- const out = [];
520
- const reasoning = result.decision?.reasoning;
521
- if (Array.isArray(reasoning)) {
522
- for (const r of reasoning) {
523
- const s = humanizeEngineSurfaceText(String(r).trim());
524
- if (s && !out.includes(s))
525
- out.push(s);
526
- if (out.length >= max)
527
- return dedupeDriverDisplayPhrases(out).slice(0, max);
528
- }
529
- }
530
- const reasons = result.explain?.reasons;
531
- if (Array.isArray(reasons) && out.length < max) {
532
- for (const r of reasons) {
533
- const title = String(r?.title ?? r?.id ?? "").trim();
534
- const readable = humanizeEngineSurfaceText(title);
535
- if (readable && !out.includes(readable))
536
- out.push(readable);
537
- if (out.length >= max)
538
- break;
539
- }
540
- }
541
- if (out.length === 0) {
542
- const one = deriveScanSummaryText(result);
543
- if (one)
544
- out.push(one);
545
- }
546
- return dedupeDriverDisplayPhrases(out).slice(0, max);
519
+ return dedupeDriverDisplayPhrases(collectWhyBulletsFromResult(result, max));
547
520
  }
548
521
  function buildWhatToDoItems(result) {
549
522
  const sortedI = sortPRInsightsForDisplay(Array.isArray(result.insights) ? result.insights : []);
@@ -0,0 +1,12 @@
1
+ import type { ScanNarrativeFacts } from "./scanNarrativeFacts.js";
2
+ import type { ScanResult } from "./types.js";
3
+ /**
4
+ * Why bullets from narrative facts (tier-1/tier-2). Preferred for GitHub + Actions.
5
+ */
6
+ export declare function collectWhyBulletsFromFacts(facts: ScanNarrativeFacts, max: number): string[];
7
+ /**
8
+ * Legacy why bullets from raw ScanResult (fallback when facts are thin).
9
+ */
10
+ export declare function collectWhyBulletsFromResult(result: ScanResult, max: number): string[];
11
+ export declare function collectWhyBullets(facts: ScanNarrativeFacts, result: ScanResult, max: number): string[];
12
+ //# sourceMappingURL=collectNarrativeWhyBullets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collectNarrativeWhyBullets.d.ts","sourceRoot":"","sources":["../src/collectNarrativeWhyBullets.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAsCV;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAwBV;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAiBV"}
@@ -0,0 +1,104 @@
1
+ import { deriveScanSummaryText } from "./deriveScanSummaryText.js";
2
+ import { humanizeEngineSurfaceText, truncateWithEllipsis, } from "./actionsStepSummary.js";
3
+ import { composeContextLineFromFacts, formatBlastRadiusDetailLine, formatUsageSummaryLine, labelReachabilityKind, selectReviewerGuidance, } from "./narrativePresentation.js";
4
+ import { normalizeGeneratedText } from "./normalizeGeneratedText.js";
5
+ function pushUnique(out, line, max) {
6
+ const t = normalizeGeneratedText(line.trim());
7
+ if (!t || out.includes(t))
8
+ return;
9
+ out.push(t);
10
+ if (out.length > max)
11
+ out.length = max;
12
+ }
13
+ /**
14
+ * Why bullets from narrative facts (tier-1/tier-2). Preferred for GitHub + Actions.
15
+ */
16
+ export function collectWhyBulletsFromFacts(facts, max) {
17
+ const prIntel = facts.availability.mode === "pr_intelligence" ||
18
+ facts.availability.tiersPresent.tier1;
19
+ if (!prIntel)
20
+ return [];
21
+ const out = [];
22
+ if (prIntel) {
23
+ const context = composeContextLineFromFacts(facts, {
24
+ includePathSample: true,
25
+ maxAreas: 2,
26
+ });
27
+ if (context)
28
+ pushUnique(out, context, max);
29
+ const usage = formatUsageSummaryLine(facts, 1);
30
+ if (usage)
31
+ pushUnique(out, usage, max);
32
+ const blast = formatBlastRadiusDetailLine(facts, 2);
33
+ if (blast)
34
+ pushUnique(out, blast, max);
35
+ for (const area of facts.affectedAreas) {
36
+ if (out.length >= max)
37
+ break;
38
+ pushUnique(out, area.label, max);
39
+ }
40
+ const reach = labelReachabilityKind(facts);
41
+ if (reach)
42
+ pushUnique(out, reach, max);
43
+ }
44
+ if (out.length < max) {
45
+ for (const g of selectReviewerGuidance(facts, { max: max - out.length })) {
46
+ if (out.length >= max)
47
+ break;
48
+ pushUnique(out, truncateWithEllipsis(g.message.trim(), 200), max);
49
+ }
50
+ }
51
+ return out.slice(0, max);
52
+ }
53
+ /**
54
+ * Legacy why bullets from raw ScanResult (fallback when facts are thin).
55
+ */
56
+ export function collectWhyBulletsFromResult(result, max) {
57
+ const out = [];
58
+ const reasoning = result.decision?.reasoning;
59
+ if (Array.isArray(reasoning)) {
60
+ for (const r of reasoning) {
61
+ const s = humanizeEngineSurfaceText(String(r).trim());
62
+ if (s)
63
+ pushUnique(out, s, max);
64
+ if (out.length >= max)
65
+ return out.slice(0, max);
66
+ }
67
+ }
68
+ const reasons = result.explain?.reasons;
69
+ if (Array.isArray(reasons) && out.length < max) {
70
+ for (const r of reasons) {
71
+ const title = String(r?.title ?? r?.id ?? "").trim();
72
+ const readable = humanizeEngineSurfaceText(title);
73
+ if (readable)
74
+ pushUnique(out, readable, max);
75
+ if (out.length >= max)
76
+ break;
77
+ }
78
+ }
79
+ if (out.length === 0) {
80
+ const one = deriveScanSummaryText(result);
81
+ if (one)
82
+ pushUnique(out, one, max);
83
+ }
84
+ return out.slice(0, max);
85
+ }
86
+ export function collectWhyBullets(facts, result, max) {
87
+ const fromFacts = collectWhyBulletsFromFacts(facts, max);
88
+ if (fromFacts.length >= max)
89
+ return fromFacts;
90
+ if (facts.availability.mode === "pr_intelligence" ||
91
+ facts.availability.tiersPresent.tier1) {
92
+ return fromFacts;
93
+ }
94
+ const legacy = collectWhyBulletsFromResult(result, max);
95
+ if (fromFacts.length === 0)
96
+ return legacy;
97
+ const merged = [...fromFacts];
98
+ for (const line of legacy) {
99
+ pushUnique(merged, line, max);
100
+ if (merged.length >= max)
101
+ break;
102
+ }
103
+ return merged.slice(0, max);
104
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAM5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAQL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAIV,UAAU,EACX,MAAM,YAAY,CAAC;AAqPpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CA8GpB;AAED,8DAA8D;AAC9D,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,CAAC"}
1
+ {"version":3,"file":"deriveScanNarrative.d.ts","sourceRoot":"","sources":["../src/deriveScanNarrative.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAM5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAYL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAKV,UAAU,EACX,MAAM,YAAY,CAAC;AAkZpB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAiGpB;AAED,8DAA8D;AAC9D,OAAO,EAAE,yBAAyB,EAAE,2BAA2B,EAAE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { extractRepositoryContextFacts } from "./extractRepositoryContextFacts.js";
2
2
  import { normalizeReachabilityKind, normalizeRuntimeSurfaceKind, parseRepoIntelligence, readBlastLevel, readReachabilityFromLoose, readSurfaceFromLoose, } from "./repoIntelligenceSchema.js";
3
- import { EMPTY_SCAN_NARRATIVE_FACTS, } from "./scanNarrativeFacts.js";
3
+ import { EMPTY_SCAN_NARRATIVE_FACTS, NARRATIVE_FACT_MAX_AREAS, NARRATIVE_FACT_MAX_HOTSPOTS, NARRATIVE_FACT_MAX_PATHS, } from "./scanNarrativeFacts.js";
4
4
  import { mergePostureFromDecision } from "./riskVocabulary.js";
5
5
  import { selectTopAffectedAreas } from "./selectTopAffectedAreas.js";
6
6
  function parseChangedPackages(changed) {
@@ -20,28 +20,132 @@ function blastLevelFromChangedCount(count) {
20
20
  return "moderate";
21
21
  return "narrow";
22
22
  }
23
+ function uniqueStrings(values) {
24
+ const seen = new Set();
25
+ const out = [];
26
+ for (const v of values) {
27
+ const t = v.trim();
28
+ if (!t || seen.has(t))
29
+ continue;
30
+ seen.add(t);
31
+ out.push(t);
32
+ }
33
+ return out;
34
+ }
35
+ function usageEntryPackageName(entry) {
36
+ const name = (entry.packageName ?? entry.package ?? "").trim();
37
+ return name || null;
38
+ }
39
+ function collectPackageUsageForChanged(ri, changed) {
40
+ const byPackage = new Map();
41
+ const ensure = (packageName) => {
42
+ let row = byPackage.get(packageName);
43
+ if (!row) {
44
+ row = {
45
+ packageName,
46
+ paths: [],
47
+ criticalPaths: [],
48
+ files: [],
49
+ areas: [],
50
+ };
51
+ byPackage.set(packageName, row);
52
+ }
53
+ return row;
54
+ };
55
+ const mergeEntry = (entry, packageNameOverride) => {
56
+ const name = packageNameOverride ?? usageEntryPackageName(entry);
57
+ if (!name || !changed.all.includes(name))
58
+ return;
59
+ const row = ensure(name);
60
+ row.paths = uniqueStrings([...row.paths, ...(entry.paths ?? [])]);
61
+ row.criticalPaths = uniqueStrings([
62
+ ...row.criticalPaths,
63
+ ...(entry.criticalPaths ?? []),
64
+ ]);
65
+ row.files = uniqueStrings([...row.files, ...(entry.files ?? [])]);
66
+ row.areas = uniqueStrings([...row.areas, ...(entry.areas ?? [])]);
67
+ };
68
+ for (const name of changed.all) {
69
+ const pkgIntel = ri.packages?.[name];
70
+ if (pkgIntel?.usage)
71
+ mergeEntry(pkgIntel.usage, name);
72
+ if (pkgIntel?.packageUsage)
73
+ mergeEntry(pkgIntel.packageUsage, name);
74
+ if (pkgIntel?.areas?.length) {
75
+ const row = ensure(name);
76
+ row.areas = uniqueStrings([...row.areas, ...pkgIntel.areas]);
77
+ }
78
+ }
79
+ if (Array.isArray(ri.packageUsage)) {
80
+ for (const entry of ri.packageUsage) {
81
+ mergeEntry(entry);
82
+ }
83
+ }
84
+ return changed.all
85
+ .map((name) => byPackage.get(name))
86
+ .filter((row) => row != null);
87
+ }
88
+ function allUsagePaths(usage) {
89
+ const paths = [];
90
+ for (const row of usage) {
91
+ paths.push(...row.paths, ...row.criticalPaths, ...row.files);
92
+ }
93
+ return uniqueStrings(paths).slice(0, NARRATIVE_FACT_MAX_PATHS);
94
+ }
95
+ function collectFrameworks(ri) {
96
+ return uniqueStrings(ri.frameworks ?? []);
97
+ }
98
+ function collectCodeHotspots(ri, primary) {
99
+ const hotspots = [];
100
+ const pkgIntel = primary && ri.packages?.[primary] ? ri.packages[primary] : undefined;
101
+ const riHotspots = ri.hotspots ?? pkgIntel?.hotspots ?? [];
102
+ for (const h of riHotspots) {
103
+ const name = (h.packageName ?? primary ?? "").trim();
104
+ if (!name)
105
+ continue;
106
+ hotspots.push({
107
+ packageName: name,
108
+ source: h.source === "graph" ? "graph" : "code",
109
+ depth: h.depth,
110
+ paths: uniqueStrings(h.paths ?? []),
111
+ });
112
+ }
113
+ return hotspots;
114
+ }
115
+ function mergeGraphHotspots(codeHotspots, graphHotspots) {
116
+ const byName = new Map();
117
+ for (const h of codeHotspots) {
118
+ byName.set(h.packageName, h);
119
+ }
120
+ for (const g of graphHotspots ?? []) {
121
+ const name = g.packageName?.trim();
122
+ if (!name || byName.has(name))
123
+ continue;
124
+ byName.set(name, {
125
+ packageName: name,
126
+ source: "graph",
127
+ depth: g.depth,
128
+ paths: [],
129
+ });
130
+ }
131
+ return [...byName.values()].slice(0, NARRATIVE_FACT_MAX_HOTSPOTS);
132
+ }
23
133
  function extractTier1FromRepoIntelligence(ri, changed) {
24
134
  const primary = changed.primary;
25
135
  const pkgIntel = primary && ri.packages?.[primary] ? ri.packages[primary] : undefined;
136
+ const packageUsage = collectPackageUsageForChanged(ri, changed);
137
+ const frameworks = collectFrameworks(ri);
138
+ const paths = allUsagePaths(packageUsage);
26
139
  const runtimeRaw = pkgIntel?.runtimeSurface ?? ri.runtimeSurface ?? undefined;
27
140
  const reachRaw = pkgIntel?.reachability ?? ri.reachability ?? undefined;
28
141
  const runtimeKind = readSurfaceFromLoose(runtimeRaw) ?? (primary ? "unknown" : null);
29
142
  const reachKind = readReachabilityFromLoose(reachRaw);
30
- const usageEntry = pkgIntel?.usage ??
31
- pkgIntel?.packageUsage ??
32
- ri.packageUsage?.find((u) => (u.packageName ?? u.package) === primary ||
33
- changed.all.includes(u.packageName ?? u.package ?? ""));
34
- const paths = [
35
- ...(usageEntry?.paths ?? []),
36
- ...(usageEntry?.criticalPaths ?? []),
37
- ...(usageEntry?.files ?? []),
38
- ].filter(Boolean);
39
143
  const runtimeSurface = runtimeKind != null
40
144
  ? {
41
145
  kind: runtimeKind,
42
146
  evidence: {
43
147
  packages: primary ? [primary] : changed.all.slice(0, 3),
44
- paths: paths.length > 0 ? paths.slice(0, 8) : undefined,
148
+ paths,
45
149
  },
46
150
  }
47
151
  : null;
@@ -49,8 +153,8 @@ function extractTier1FromRepoIntelligence(ri, changed) {
49
153
  ? {
50
154
  kind: reachKind,
51
155
  evidence: {
52
- paths: paths.length > 0 ? paths.slice(0, 8) : undefined,
53
- frameworks: ri.frameworks?.length ? ri.frameworks : undefined,
156
+ paths,
157
+ frameworks,
54
158
  },
55
159
  }
56
160
  : null;
@@ -63,44 +167,37 @@ function extractTier1FromRepoIntelligence(ri, changed) {
63
167
  ? {
64
168
  level: blastLevel,
65
169
  changedPackageCount: blastFromRi?.changedPackageCount ?? changed.all.length,
66
- factors: blastFromRi?.factors,
170
+ factors: uniqueStrings(blastFromRi?.factors ?? []),
67
171
  }
68
172
  : null;
69
173
  const areasRaw = ri.applicationAreas ?? ri.affectedAreas ?? [];
70
- const usageAreas = usageEntry?.areas ?? pkgIntel?.areas ?? [];
71
174
  const affectedAreas = [];
72
175
  for (const area of areasRaw) {
73
176
  affectedAreas.push({ id: area.id, label: area.label });
74
177
  }
75
- for (const label of usageAreas) {
76
- const id = label.toLowerCase().replace(/\s+/g, "_");
77
- if (!affectedAreas.some((a) => a.id === id)) {
78
- affectedAreas.push({ id, label });
178
+ for (const row of packageUsage) {
179
+ for (const label of row.areas) {
180
+ const id = label.toLowerCase().replace(/\s+/g, "_");
181
+ if (!affectedAreas.some((a) => a.id === id)) {
182
+ affectedAreas.push({ id, label });
183
+ }
79
184
  }
80
185
  }
81
- const hotspots = [];
82
- const riHotspots = ri.hotspots ?? pkgIntel?.hotspots ?? [];
83
- for (const h of riHotspots) {
84
- const name = h.packageName ?? primary;
85
- if (!name)
86
- continue;
87
- hotspots.push({
88
- packageName: name,
89
- source: h.source === "graph" ? "graph" : "code",
90
- depth: h.depth,
91
- paths: h.paths,
92
- });
93
- }
186
+ const codeHotspots = collectCodeHotspots(ri, primary);
94
187
  return {
188
+ packageUsage,
189
+ frameworks,
95
190
  runtimeSurface,
96
191
  reachability,
97
192
  blastRadius,
98
- affectedAreas: affectedAreas.slice(0, 6),
99
- hotspots: hotspots.slice(0, 8),
193
+ affectedAreas: affectedAreas.slice(0, NARRATIVE_FACT_MAX_AREAS),
194
+ hotspots: codeHotspots.slice(0, NARRATIVE_FACT_MAX_HOTSPOTS),
100
195
  };
101
196
  }
102
197
  function hasMeaningfulTier1(tier1) {
103
- return (tier1.runtimeSurface != null ||
198
+ return (tier1.packageUsage.length > 0 ||
199
+ tier1.frameworks.length > 0 ||
200
+ tier1.runtimeSurface != null ||
104
201
  tier1.reachability != null ||
105
202
  tier1.blastRadius != null ||
106
203
  tier1.affectedAreas.length > 0 ||
@@ -176,6 +273,24 @@ function resolveAvailabilityMode(tier1, tier2, codeIntelligenceAvailable) {
176
273
  return "insights";
177
274
  return "graph_fallback";
178
275
  }
276
+ function emptyTier1Blocks() {
277
+ return {
278
+ packageUsage: [],
279
+ frameworks: [],
280
+ runtimeSurface: null,
281
+ reachability: null,
282
+ blastRadius: null,
283
+ affectedAreas: [],
284
+ hotspots: [],
285
+ };
286
+ }
287
+ function blastRadiusFromChangedCount(count) {
288
+ return {
289
+ level: blastLevelFromChangedCount(count),
290
+ changedPackageCount: count,
291
+ factors: [],
292
+ };
293
+ }
179
294
  /**
180
295
  * Consumer-agnostic scan intelligence — structured facts only (no UI copy).
181
296
  */
@@ -191,28 +306,17 @@ export function deriveScanNarrative(result) {
191
306
  const codeIntelligenceAvailable = result.analysisPreparation?.codeIntelligenceAvailable !== false;
192
307
  const parsedRi = parseRepoIntelligence(result.repoIntelligence);
193
308
  const corpusGate = codeIntelligenceAvailable && parsedRi != null;
194
- let tier1Blocks = {
195
- runtimeSurface: null,
196
- reachability: null,
197
- blastRadius: null,
198
- affectedAreas: [],
199
- hotspots: [],
200
- };
309
+ let tier1Blocks = emptyTier1Blocks();
201
310
  if (corpusGate && parsedRi) {
202
311
  tier1Blocks = extractTier1FromRepoIntelligence(parsedRi, changedPackages);
203
312
  }
204
313
  else if (changedPackages.all.length > 0) {
205
314
  tier1Blocks = {
206
- runtimeSurface: null,
207
- reachability: null,
208
- blastRadius: {
209
- level: blastLevelFromChangedCount(changedPackages.all.length),
210
- changedPackageCount: changedPackages.all.length,
211
- },
212
- affectedAreas: [],
213
- hotspots: [],
315
+ ...emptyTier1Blocks(),
316
+ blastRadius: blastRadiusFromChangedCount(changedPackages.all.length),
214
317
  };
215
318
  }
319
+ tier1Blocks.hotspots = mergeGraphHotspots(tier1Blocks.hotspots, result.graphInsights?.hotspots);
216
320
  const tier1 = corpusGate && hasMeaningfulTier1(tier1Blocks);
217
321
  const reviewerGuidance = collectReviewerGuidance(result);
218
322
  const tier2 = reviewerGuidance.some((g) => g.scope === "changed" || g.kind === "insight");
@@ -228,10 +332,7 @@ export function deriveScanNarrative(result) {
228
332
  : null,
229
333
  };
230
334
  if (!tier1 && mode === "graph_fallback" && changedPackages.all.length > 0) {
231
- tier1Blocks.blastRadius = {
232
- level: blastLevelFromChangedCount(changedPackages.all.length),
233
- changedPackageCount: changedPackages.all.length,
234
- };
335
+ tier1Blocks.blastRadius = blastRadiusFromChangedCount(changedPackages.all.length);
235
336
  }
236
337
  let affectedAreas = tier1Blocks.affectedAreas;
237
338
  if (affectedAreas.length === 0) {
@@ -251,6 +352,8 @@ export function deriveScanNarrative(result) {
251
352
  },
252
353
  },
253
354
  changedPackages,
355
+ packageUsage: tier1Blocks.packageUsage,
356
+ frameworks: tier1Blocks.frameworks,
254
357
  runtimeSurface: tier1Blocks.runtimeSurface,
255
358
  reachability: tier1Blocks.reachability,
256
359
  blastRadius: tier1Blocks.blastRadius,
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { deriveScanNarrative } from "./deriveScanNarrative.js";
3
- import { fixtureRepoIntelligenceEmpty, fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceTypescript, } from "./fixtures/repoIntelligenceFixtures.js";
3
+ import { fixtureRepoIntelligenceEmpty, fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceMultiPackage, fixtureRepoIntelligenceTypescript, } from "./fixtures/repoIntelligenceFixtures.js";
4
4
  const baseResult = {
5
5
  totalScore: 10,
6
6
  layerScores: {
@@ -96,6 +96,61 @@ describe("deriveScanNarrative", () => {
96
96
  expect(facts.reachability?.kind).toBe("build_only");
97
97
  expect(facts.blastRadius?.level).toBe("narrow");
98
98
  });
99
+ it("extracts packageUsage for all changed packages with blast factors and frameworks", () => {
100
+ const result = {
101
+ ...baseResult,
102
+ changedPackages: ["lodash", "axios"],
103
+ analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
104
+ repoIntelligence: fixtureRepoIntelligenceMultiPackage,
105
+ };
106
+ const facts = deriveScanNarrative(result);
107
+ expect(facts.packageUsage).toHaveLength(2);
108
+ expect(facts.packageUsage.map((u) => u.packageName)).toEqual([
109
+ "lodash",
110
+ "axios",
111
+ ]);
112
+ expect(facts.packageUsage[0]?.paths).toContain("apps/billing/export.ts");
113
+ expect(facts.blastRadius?.factors).toEqual([
114
+ "multiple_runtime_consumers",
115
+ "shared_middleware",
116
+ ]);
117
+ expect(facts.frameworks).toEqual(["express", "react"]);
118
+ expect(facts.reachability?.evidence.frameworks).toEqual([
119
+ "express",
120
+ "react",
121
+ ]);
122
+ });
123
+ it("merges graph hotspots without duplicating code hotspots", () => {
124
+ const result = {
125
+ ...baseResult,
126
+ changedPackages: ["fastify"],
127
+ analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
128
+ repoIntelligence: fixtureRepoIntelligenceFastify,
129
+ graphInsights: {
130
+ maxDepth: 3,
131
+ nodes: 10,
132
+ edges: 12,
133
+ hotspots: [
134
+ {
135
+ kind: "hotspot",
136
+ packageName: "fastify",
137
+ direct: true,
138
+ depth: 2,
139
+ },
140
+ {
141
+ kind: "hotspot",
142
+ packageName: "transitive-only-pkg",
143
+ direct: false,
144
+ depth: 4,
145
+ },
146
+ ],
147
+ },
148
+ };
149
+ const facts = deriveScanNarrative(result);
150
+ expect(facts.hotspots.map((h) => h.packageName)).toContain("fastify");
151
+ expect(facts.hotspots.map((h) => h.packageName)).toContain("transitive-only-pkg");
152
+ expect(facts.hotspots.filter((h) => h.packageName === "fastify")).toHaveLength(1);
153
+ });
99
154
  it("treats empty packages-only repoIntelligence as absent", () => {
100
155
  const result = {
101
156
  ...baseResult,
@@ -5,4 +5,6 @@ export declare const fixtureRepoIntelligenceFastify: RepoIntelligence;
5
5
  export declare const fixtureRepoIntelligenceTypescript: RepoIntelligence;
6
6
  /** Empty corpus stub (engine-test-fixture default). */
7
7
  export declare const fixtureRepoIntelligenceEmpty: RepoIntelligence;
8
+ /** Multi-package PR with distinct usage per package. */
9
+ export declare const fixtureRepoIntelligenceMultiPackage: RepoIntelligence;
8
10
  //# sourceMappingURL=repoIntelligenceFixtures.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repoIntelligenceFixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/repoIntelligenceFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,iEAAiE;AACjE,eAAO,MAAM,8BAA8B,EAAE,gBA8B5C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,iCAAiC,EAAE,gBAS/C,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,4BAA4B,EAAE,gBAE1C,CAAC"}
1
+ {"version":3,"file":"repoIntelligenceFixtures.d.ts","sourceRoot":"","sources":["../../src/fixtures/repoIntelligenceFixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,iEAAiE;AACjE,eAAO,MAAM,8BAA8B,EAAE,gBA8B5C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,iCAAiC,EAAE,gBAS/C,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,4BAA4B,EAAE,gBAE1C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,mCAAmC,EAAE,gBA4CjD,CAAC"}
@@ -45,3 +45,49 @@ export const fixtureRepoIntelligenceTypescript = {
45
45
  export const fixtureRepoIntelligenceEmpty = {
46
46
  packages: {},
47
47
  };
48
+ /** Multi-package PR with distinct usage per package. */
49
+ export const fixtureRepoIntelligenceMultiPackage = {
50
+ packages: {
51
+ lodash: {
52
+ runtimeSurface: "runtime",
53
+ reachability: "on_runtime_paths",
54
+ usage: {
55
+ packageName: "lodash",
56
+ paths: ["apps/billing/export.ts"],
57
+ areas: ["Billing"],
58
+ },
59
+ },
60
+ axios: {
61
+ runtimeSurface: "runtime",
62
+ reachability: "on_runtime_paths",
63
+ usage: {
64
+ packageName: "axios",
65
+ paths: ["apps/api/client.ts"],
66
+ areas: ["API"],
67
+ },
68
+ },
69
+ },
70
+ packageUsage: [
71
+ {
72
+ packageName: "lodash",
73
+ paths: ["apps/billing/export.ts"],
74
+ },
75
+ {
76
+ packageName: "axios",
77
+ paths: ["apps/api/client.ts"],
78
+ },
79
+ ],
80
+ blastRadius: {
81
+ level: "moderate",
82
+ factors: ["multiple_runtime_consumers", "shared_middleware"],
83
+ changedPackageCount: 2,
84
+ },
85
+ frameworks: ["express", "react"],
86
+ hotspots: [
87
+ {
88
+ packageName: "lodash",
89
+ source: "code",
90
+ paths: ["apps/billing/export.ts"],
91
+ },
92
+ ],
93
+ };
@@ -1,4 +1,4 @@
1
- import type { PRInsight, PRDecision, PRDecisionRecommendation } from "./types.js";
1
+ import type { PRInsight, PRDecisionRecommendation } from "./types.js";
2
2
  export type FormattedInsight = {
3
3
  message: string;
4
4
  where: string;
@@ -6,5 +6,5 @@ export type FormattedInsight = {
6
6
  };
7
7
  export declare function decisionTitle(recommendation: PRDecisionRecommendation): string;
8
8
  export declare function formatInsight(insight: PRInsight): FormattedInsight;
9
- export declare function renderInsightsAsMarkdown(insights: PRInsight[], decision: PRDecision): string;
9
+ export { renderInsightsAsMarkdown } from "./presentGitHubPrComment.js";
10
10
  //# sourceMappingURL=formatInsight.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatInsight.d.ts","sourceRoot":"","sources":["../src/formatInsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AASF,wBAAgB,aAAa,CAC3B,cAAc,EAAE,wBAAwB,GACvC,MAAM,CAER;AAgBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAMlE;AAkBD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,EAAE,EACrB,QAAQ,EAAE,UAAU,GACnB,MAAM,CAMR"}
1
+ {"version":3,"file":"formatInsight.d.ts","sourceRoot":"","sources":["../src/formatInsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AASF,wBAAgB,aAAa,CAC3B,cAAc,EAAE,wBAAwB,GACvC,MAAM,CAER;AAgBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAMlE;AAiBD,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,7 +1,7 @@
1
1
  const DECISION_TITLES = {
2
2
  safe: "No significant dependency risks detected",
3
3
  needs_review: "Elevated dependency merge risk",
4
- risky: "Merge blocked critical dependency risk",
4
+ risky: "Merge blocked - critical dependency risk",
5
5
  };
6
6
  // Title rendered once per PR comment, not per insight
7
7
  export function decisionTitle(recommendation) {
@@ -41,9 +41,4 @@ function renderInsightAsMarkdown(f) {
41
41
  f.action,
42
42
  ].join("\n");
43
43
  }
44
- // Renders a full PR comment: one bold title + all insights separated by dividers
45
- export function renderInsightsAsMarkdown(insights, decision) {
46
- const title = `**${decisionTitle(decision.recommendation)}**`;
47
- const blocks = insights.map((insight) => renderInsightAsMarkdown(formatInsight(insight)));
48
- return [title, "", ...blocks].join("\n\n---\n\n").trimEnd();
49
- }
44
+ export { renderInsightsAsMarkdown } from "./presentGitHubPrComment.js";
@@ -88,7 +88,7 @@ describe("renderInsightsAsMarkdown", () => {
88
88
  remediation: "fix text",
89
89
  });
90
90
  const output = renderInsightsAsMarkdown([insight], decision);
91
- expect(output).toContain("Elevated dependency merge risk");
91
+ expect(output).toContain("Needs review");
92
92
  expect(output).toContain("msg text");
93
93
  expect(output).toContain("ctx text");
94
94
  expect(output).toContain("fix text");