@mergesignal/shared 0.2.7 → 0.3.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 (85) 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 +0 -3
  7. package/dist/deriveScanNarrative.d.ts.map +1 -1
  8. package/dist/deriveScanNarrative.js +177 -63
  9. package/dist/deriveScanNarrative.test.js +90 -6
  10. package/dist/engineWireRegression.test.d.ts +2 -0
  11. package/dist/engineWireRegression.test.d.ts.map +1 -0
  12. package/dist/engineWireRegression.test.js +57 -0
  13. package/dist/fixtures/legacyEngineRepoIntelligence.d.ts +28 -0
  14. package/dist/fixtures/legacyEngineRepoIntelligence.d.ts.map +1 -0
  15. package/dist/fixtures/legacyEngineRepoIntelligence.js +28 -0
  16. package/dist/fixtures/repoIntelligenceFixtures.d.ts +2 -2
  17. package/dist/fixtures/repoIntelligenceFixtures.d.ts.map +1 -1
  18. package/dist/fixtures/repoIntelligenceFixtures.js +59 -3
  19. package/dist/fixtures/repoIntelligenceTestHelpers.d.ts +5 -0
  20. package/dist/fixtures/repoIntelligenceTestHelpers.d.ts.map +1 -0
  21. package/dist/fixtures/repoIntelligenceTestHelpers.js +28 -0
  22. package/dist/formatInsight.d.ts +2 -2
  23. package/dist/formatInsight.d.ts.map +1 -1
  24. package/dist/formatInsight.js +2 -7
  25. package/dist/formatInsight.test.js +1 -1
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -0
  29. package/dist/narrativeParity.test.d.ts +2 -0
  30. package/dist/narrativeParity.test.d.ts.map +1 -0
  31. package/dist/narrativeParity.test.js +65 -0
  32. package/dist/narrativePresentation.d.ts +50 -0
  33. package/dist/narrativePresentation.d.ts.map +1 -0
  34. package/dist/narrativePresentation.js +180 -0
  35. package/dist/narrativePresentation.test.d.ts +2 -0
  36. package/dist/narrativePresentation.test.d.ts.map +1 -0
  37. package/dist/narrativePresentation.test.js +54 -0
  38. package/dist/normalizeGeneratedText.d.ts +8 -0
  39. package/dist/normalizeGeneratedText.d.ts.map +1 -0
  40. package/dist/normalizeGeneratedText.js +31 -0
  41. package/dist/normalizeGeneratedText.test.d.ts +2 -0
  42. package/dist/normalizeGeneratedText.test.d.ts.map +1 -0
  43. package/dist/normalizeGeneratedText.test.js +18 -0
  44. package/dist/prCheckRunPresentation.d.ts +4 -3
  45. package/dist/prCheckRunPresentation.d.ts.map +1 -1
  46. package/dist/prCheckRunPresentation.js +56 -51
  47. package/dist/prCheckRunPresentation.test.js +2 -2
  48. package/dist/presentGitHubPrComment.d.ts +9 -0
  49. package/dist/presentGitHubPrComment.d.ts.map +1 -0
  50. package/dist/presentGitHubPrComment.js +80 -0
  51. package/dist/presentScanCardSummary.d.ts.map +1 -1
  52. package/dist/presentScanCardSummary.js +69 -64
  53. package/dist/repoIntelligenceMappers.d.ts +20 -0
  54. package/dist/repoIntelligenceMappers.d.ts.map +1 -0
  55. package/dist/repoIntelligenceMappers.js +60 -0
  56. package/dist/repoIntelligenceSchema.contract.test.d.ts +2 -0
  57. package/dist/repoIntelligenceSchema.contract.test.d.ts.map +1 -0
  58. package/dist/repoIntelligenceSchema.contract.test.js +19 -0
  59. package/dist/repoIntelligenceSchema.d.ts +52 -83
  60. package/dist/repoIntelligenceSchema.d.ts.map +1 -1
  61. package/dist/repoIntelligenceSchema.js +99 -141
  62. package/dist/repoIntelligenceValidation.d.ts +18 -0
  63. package/dist/repoIntelligenceValidation.d.ts.map +1 -0
  64. package/dist/repoIntelligenceValidation.js +76 -0
  65. package/dist/repoIntelligenceValidation.persist.test.d.ts +2 -0
  66. package/dist/repoIntelligenceValidation.persist.test.d.ts.map +1 -0
  67. package/dist/repoIntelligenceValidation.persist.test.js +44 -0
  68. package/dist/riskVocabulary.js +1 -1
  69. package/dist/scanCardSummary.d.ts +9 -1
  70. package/dist/scanCardSummary.d.ts.map +1 -1
  71. package/dist/scanCardSummary.js +1 -1
  72. package/dist/scanCardSummary.test.js +12 -8
  73. package/dist/scanDetailViewModel.d.ts +9 -1
  74. package/dist/scanDetailViewModel.d.ts.map +1 -1
  75. package/dist/scanDetailViewModel.js +164 -37
  76. package/dist/scanDetailViewModel.test.js +28 -1
  77. package/dist/scanNarrativeFacts.d.ts +23 -8
  78. package/dist/scanNarrativeFacts.d.ts.map +1 -1
  79. package/dist/scanNarrativeFacts.js +7 -0
  80. package/dist/scanSurfaceCopy.d.ts +26 -26
  81. package/dist/scanSurfaceCopy.d.ts.map +1 -1
  82. package/dist/scanSurfaceCopy.js +26 -26
  83. package/dist/types.d.ts +15 -3
  84. package/dist/types.d.ts.map +1 -1
  85. 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,10 +1,7 @@
1
- import { normalizeReachabilityKind, normalizeRuntimeSurfaceKind } from "./repoIntelligenceSchema.js";
2
1
  import { type ScanNarrativeFacts } from "./scanNarrativeFacts.js";
3
2
  import type { ScanResult } from "./types.js";
4
3
  /**
5
4
  * Consumer-agnostic scan intelligence — structured facts only (no UI copy).
6
5
  */
7
6
  export declare function deriveScanNarrative(result: ScanResult | null | undefined): ScanNarrativeFacts;
8
- /** @internal Test helper — normalize loose engine strings. */
9
- export { normalizeReachabilityKind, normalizeRuntimeSurfaceKind };
10
7
  //# sourceMappingURL=deriveScanNarrative.d.ts.map
@@ -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":"AAMA,OAAO,EAaL,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAKV,UAAU,EACX,MAAM,YAAY,CAAC;AAgapB;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,kBAAkB,CAsGpB"}
@@ -1,6 +1,6 @@
1
1
  import { extractRepositoryContextFacts } from "./extractRepositoryContextFacts.js";
2
- import { normalizeReachabilityKind, normalizeRuntimeSurfaceKind, parseRepoIntelligence, readBlastLevel, readReachabilityFromLoose, readSurfaceFromLoose, } from "./repoIntelligenceSchema.js";
3
- import { EMPTY_SCAN_NARRATIVE_FACTS, } from "./scanNarrativeFacts.js";
2
+ import { readBlastLevel, safeParseRepoIntelligence, } from "./repoIntelligenceSchema.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,124 @@ 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 collectPackageUsageForChanged(ri, changed) {
36
+ const byPackage = new Map();
37
+ const ensure = (packageName) => {
38
+ let row = byPackage.get(packageName);
39
+ if (!row) {
40
+ row = {
41
+ packageName,
42
+ paths: [],
43
+ criticalPaths: [],
44
+ files: [],
45
+ areas: [],
46
+ };
47
+ byPackage.set(packageName, row);
48
+ }
49
+ return row;
50
+ };
51
+ const mergeEntry = (entry, packageNameOverride) => {
52
+ const name = packageNameOverride ?? entry.packageName.trim();
53
+ if (!changed.all.includes(name))
54
+ return;
55
+ const row = ensure(name);
56
+ row.paths = uniqueStrings([...row.paths, ...(entry.paths ?? [])]);
57
+ row.criticalPaths = uniqueStrings([
58
+ ...row.criticalPaths,
59
+ ...(entry.criticalPaths ?? []),
60
+ ]);
61
+ row.files = uniqueStrings([...row.files, ...(entry.files ?? [])]);
62
+ row.areas = uniqueStrings([...row.areas, ...(entry.areas ?? [])]);
63
+ };
64
+ for (const name of changed.all) {
65
+ const pkgIntel = ri.packages?.[name];
66
+ if (pkgIntel?.usage)
67
+ mergeEntry(pkgIntel.usage, name);
68
+ if (pkgIntel?.areas?.length) {
69
+ const row = ensure(name);
70
+ row.areas = uniqueStrings([...row.areas, ...pkgIntel.areas]);
71
+ }
72
+ }
73
+ if (Array.isArray(ri.packageUsage)) {
74
+ for (const entry of ri.packageUsage) {
75
+ mergeEntry(entry);
76
+ }
77
+ }
78
+ return changed.all
79
+ .map((name) => byPackage.get(name))
80
+ .filter((row) => row != null);
81
+ }
82
+ function allUsagePaths(usage) {
83
+ const paths = [];
84
+ for (const row of usage) {
85
+ paths.push(...row.paths, ...row.criticalPaths, ...row.files);
86
+ }
87
+ return uniqueStrings(paths).slice(0, NARRATIVE_FACT_MAX_PATHS);
88
+ }
89
+ function collectFrameworks(ri) {
90
+ return uniqueStrings(ri.frameworks ?? []);
91
+ }
92
+ function collectCodeHotspots(ri, primary) {
93
+ const hotspots = [];
94
+ const pkgIntel = primary && ri.packages?.[primary] ? ri.packages[primary] : undefined;
95
+ const riHotspots = ri.hotspots ?? [];
96
+ for (const h of riHotspots) {
97
+ const name = (h.packageName ?? primary ?? "").trim();
98
+ if (!name)
99
+ continue;
100
+ hotspots.push({
101
+ packageName: name,
102
+ source: h.source === "graph" ? "graph" : "code",
103
+ depth: h.depth,
104
+ paths: uniqueStrings(h.paths ?? []),
105
+ });
106
+ }
107
+ return hotspots;
108
+ }
109
+ function mergeGraphHotspots(codeHotspots, graphHotspots) {
110
+ const byName = new Map();
111
+ for (const h of codeHotspots) {
112
+ byName.set(h.packageName, h);
113
+ }
114
+ for (const g of graphHotspots ?? []) {
115
+ const name = g.packageName?.trim();
116
+ if (!name || byName.has(name))
117
+ continue;
118
+ byName.set(name, {
119
+ packageName: name,
120
+ source: "graph",
121
+ depth: g.depth,
122
+ paths: [],
123
+ });
124
+ }
125
+ return [...byName.values()].slice(0, NARRATIVE_FACT_MAX_HOTSPOTS);
126
+ }
23
127
  function extractTier1FromRepoIntelligence(ri, changed) {
24
128
  const primary = changed.primary;
25
129
  const pkgIntel = primary && ri.packages?.[primary] ? ri.packages[primary] : undefined;
26
- const runtimeRaw = pkgIntel?.runtimeSurface ?? ri.runtimeSurface ?? undefined;
27
- const reachRaw = pkgIntel?.reachability ?? ri.reachability ?? undefined;
28
- const runtimeKind = readSurfaceFromLoose(runtimeRaw) ?? (primary ? "unknown" : null);
29
- 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);
130
+ const packageUsage = collectPackageUsageForChanged(ri, changed);
131
+ const frameworks = collectFrameworks(ri);
132
+ const paths = allUsagePaths(packageUsage);
133
+ const runtimeKind = pkgIntel?.runtimeSurface ?? null;
134
+ const reachKind = pkgIntel?.reachability ?? null;
39
135
  const runtimeSurface = runtimeKind != null
40
136
  ? {
41
137
  kind: runtimeKind,
42
138
  evidence: {
43
139
  packages: primary ? [primary] : changed.all.slice(0, 3),
44
- paths: paths.length > 0 ? paths.slice(0, 8) : undefined,
140
+ paths,
45
141
  },
46
142
  }
47
143
  : null;
@@ -49,8 +145,8 @@ function extractTier1FromRepoIntelligence(ri, changed) {
49
145
  ? {
50
146
  kind: reachKind,
51
147
  evidence: {
52
- paths: paths.length > 0 ? paths.slice(0, 8) : undefined,
53
- frameworks: ri.frameworks?.length ? ri.frameworks : undefined,
148
+ paths,
149
+ frameworks,
54
150
  },
55
151
  }
56
152
  : null;
@@ -63,44 +159,37 @@ function extractTier1FromRepoIntelligence(ri, changed) {
63
159
  ? {
64
160
  level: blastLevel,
65
161
  changedPackageCount: blastFromRi?.changedPackageCount ?? changed.all.length,
66
- factors: blastFromRi?.factors,
162
+ factors: uniqueStrings(blastFromRi?.factors ?? []),
67
163
  }
68
164
  : null;
69
165
  const areasRaw = ri.applicationAreas ?? ri.affectedAreas ?? [];
70
- const usageAreas = usageEntry?.areas ?? pkgIntel?.areas ?? [];
71
166
  const affectedAreas = [];
72
167
  for (const area of areasRaw) {
73
168
  affectedAreas.push({ id: area.id, label: area.label });
74
169
  }
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 });
170
+ for (const row of packageUsage) {
171
+ for (const label of row.areas) {
172
+ const id = label.toLowerCase().replace(/\s+/g, "_");
173
+ if (!affectedAreas.some((a) => a.id === id)) {
174
+ affectedAreas.push({ id, label });
175
+ }
79
176
  }
80
177
  }
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
- }
178
+ const codeHotspots = collectCodeHotspots(ri, primary);
94
179
  return {
180
+ packageUsage,
181
+ frameworks,
95
182
  runtimeSurface,
96
183
  reachability,
97
184
  blastRadius,
98
- affectedAreas: affectedAreas.slice(0, 6),
99
- hotspots: hotspots.slice(0, 8),
185
+ affectedAreas: affectedAreas.slice(0, NARRATIVE_FACT_MAX_AREAS),
186
+ hotspots: codeHotspots.slice(0, NARRATIVE_FACT_MAX_HOTSPOTS),
100
187
  };
101
188
  }
102
189
  function hasMeaningfulTier1(tier1) {
103
- return (tier1.runtimeSurface != null ||
190
+ return (tier1.packageUsage.length > 0 ||
191
+ tier1.frameworks.length > 0 ||
192
+ tier1.runtimeSurface != null ||
104
193
  tier1.reachability != null ||
105
194
  tier1.blastRadius != null ||
106
195
  tier1.affectedAreas.length > 0 ||
@@ -176,6 +265,42 @@ function resolveAvailabilityMode(tier1, tier2, codeIntelligenceAvailable) {
176
265
  return "insights";
177
266
  return "graph_fallback";
178
267
  }
268
+ function emptyTier1Blocks() {
269
+ return {
270
+ packageUsage: [],
271
+ frameworks: [],
272
+ runtimeSurface: null,
273
+ reachability: null,
274
+ blastRadius: null,
275
+ affectedAreas: [],
276
+ hotspots: [],
277
+ };
278
+ }
279
+ function blastRadiusFromChangedCount(count) {
280
+ return {
281
+ level: blastLevelFromChangedCount(count),
282
+ changedPackageCount: count,
283
+ factors: [],
284
+ };
285
+ }
286
+ function resolveTrustedRepoIntelligence(result) {
287
+ const raw = result.repoIntelligence;
288
+ const validationStatus = result.analysisPreparation?.repoIntelligenceValidation?.status;
289
+ if (raw == null) {
290
+ return { parsed: null, parseStatus: "absent" };
291
+ }
292
+ if (validationStatus === "invalid") {
293
+ return { parsed: null, parseStatus: "untrusted" };
294
+ }
295
+ if (validationStatus !== "valid") {
296
+ return { parsed: null, parseStatus: "absent" };
297
+ }
298
+ const parsed = safeParseRepoIntelligence(raw);
299
+ if (parsed.ok) {
300
+ return { parsed: parsed.value, parseStatus: "ok" };
301
+ }
302
+ return { parsed: null, parseStatus: "invalid" };
303
+ }
179
304
  /**
180
305
  * Consumer-agnostic scan intelligence — structured facts only (no UI copy).
181
306
  */
@@ -189,30 +314,21 @@ export function deriveScanNarrative(result) {
189
314
  ? result.totalScore
190
315
  : null;
191
316
  const codeIntelligenceAvailable = result.analysisPreparation?.codeIntelligenceAvailable !== false;
192
- const parsedRi = parseRepoIntelligence(result.repoIntelligence);
193
- const corpusGate = codeIntelligenceAvailable && parsedRi != null;
194
- let tier1Blocks = {
195
- runtimeSurface: null,
196
- reachability: null,
197
- blastRadius: null,
198
- affectedAreas: [],
199
- hotspots: [],
200
- };
317
+ const { parsed: parsedRi, parseStatus: repoIntelligenceParse } = resolveTrustedRepoIntelligence(result);
318
+ const corpusGate = codeIntelligenceAvailable &&
319
+ repoIntelligenceParse === "ok" &&
320
+ parsedRi != null;
321
+ let tier1Blocks = emptyTier1Blocks();
201
322
  if (corpusGate && parsedRi) {
202
323
  tier1Blocks = extractTier1FromRepoIntelligence(parsedRi, changedPackages);
203
324
  }
204
325
  else if (changedPackages.all.length > 0) {
205
326
  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: [],
327
+ ...emptyTier1Blocks(),
328
+ blastRadius: blastRadiusFromChangedCount(changedPackages.all.length),
214
329
  };
215
330
  }
331
+ tier1Blocks.hotspots = mergeGraphHotspots(tier1Blocks.hotspots, result.graphInsights?.hotspots);
216
332
  const tier1 = corpusGate && hasMeaningfulTier1(tier1Blocks);
217
333
  const reviewerGuidance = collectReviewerGuidance(result);
218
334
  const tier2 = reviewerGuidance.some((g) => g.scope === "changed" || g.kind === "insight");
@@ -228,10 +344,7 @@ export function deriveScanNarrative(result) {
228
344
  : null,
229
345
  };
230
346
  if (!tier1 && mode === "graph_fallback" && changedPackages.all.length > 0) {
231
- tier1Blocks.blastRadius = {
232
- level: blastLevelFromChangedCount(changedPackages.all.length),
233
- changedPackageCount: changedPackages.all.length,
234
- };
347
+ tier1Blocks.blastRadius = blastRadiusFromChangedCount(changedPackages.all.length);
235
348
  }
236
349
  let affectedAreas = tier1Blocks.affectedAreas;
237
350
  if (affectedAreas.length === 0) {
@@ -249,8 +362,11 @@ export function deriveScanNarrative(result) {
249
362
  tier2: reviewerGuidance.length > 0,
250
363
  tier3,
251
364
  },
365
+ repoIntelligenceParse,
252
366
  },
253
367
  changedPackages,
368
+ packageUsage: tier1Blocks.packageUsage,
369
+ frameworks: tier1Blocks.frameworks,
254
370
  runtimeSurface: tier1Blocks.runtimeSurface,
255
371
  reachability: tier1Blocks.reachability,
256
372
  blastRadius: tier1Blocks.blastRadius,
@@ -263,5 +379,3 @@ export function deriveScanNarrative(result) {
263
379
  riskIndex,
264
380
  };
265
381
  }
266
- /** @internal Test helper — normalize loose engine strings. */
267
- export { normalizeReachabilityKind, normalizeRuntimeSurfaceKind };
@@ -1,6 +1,8 @@
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 { legacyEngineRepoIntelligenceJwt } from "./fixtures/legacyEngineRepoIntelligence.js";
4
+ import { fixtureRepoIntelligenceFastify, fixtureRepoIntelligenceMultiPackage, fixtureRepoIntelligenceTypescript, } from "./fixtures/repoIntelligenceFixtures.js";
5
+ import { analysisPreparationWithInvalidRepoIntel, analysisPreparationWithValidRepoIntel, } from "./fixtures/repoIntelligenceTestHelpers.js";
4
6
  const baseResult = {
5
7
  totalScore: 10,
6
8
  layerScores: {
@@ -18,15 +20,42 @@ describe("deriveScanNarrative", () => {
18
20
  expect(facts.availability.mode).toBe("denormalized");
19
21
  expect(facts.changedPackages.all).toEqual([]);
20
22
  });
23
+ it("ignores repoIntelligence when validation status is not valid", () => {
24
+ const result = {
25
+ ...baseResult,
26
+ changedPackages: ["fastify"],
27
+ analysisPreparation: {
28
+ codeIntelligenceAvailable: true,
29
+ warnings: [],
30
+ },
31
+ repoIntelligence: fixtureRepoIntelligenceFastify,
32
+ };
33
+ const facts = deriveScanNarrative(result);
34
+ expect(facts.availability.repoIntelligenceParse).toBe("absent");
35
+ expect(facts.availability.tiersPresent.tier1).toBe(false);
36
+ });
37
+ it("treats worker-invalid repoIntelligence as untrusted (raw retained, not consumed)", () => {
38
+ const result = {
39
+ ...baseResult,
40
+ changedPackages: ["jsonwebtoken"],
41
+ analysisPreparation: analysisPreparationWithInvalidRepoIntel(),
42
+ repoIntelligence: legacyEngineRepoIntelligenceJwt,
43
+ };
44
+ const facts = deriveScanNarrative(result);
45
+ expect(facts.availability.repoIntelligenceParse).toBe("untrusted");
46
+ expect(facts.availability.tiersPresent.tier1).toBe(false);
47
+ expect(facts.packageUsage).toHaveLength(0);
48
+ });
21
49
  it("populates tier1 from repoIntelligence when corpus available", () => {
22
50
  const result = {
23
51
  ...baseResult,
24
52
  changedPackages: ["fastify"],
25
- analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
53
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
26
54
  repoIntelligence: fixtureRepoIntelligenceFastify,
27
55
  };
28
56
  const facts = deriveScanNarrative(result);
29
57
  expect(facts.availability.mode).toBe("pr_intelligence");
58
+ expect(facts.availability.repoIntelligenceParse).toBe("ok");
30
59
  expect(facts.availability.codeIntelligenceAvailable).toBe(true);
31
60
  expect(facts.changedPackages.primary).toBe("fastify");
32
61
  expect(facts.runtimeSurface?.kind).toBe("runtime");
@@ -87,7 +116,7 @@ describe("deriveScanNarrative", () => {
87
116
  const result = {
88
117
  ...baseResult,
89
118
  changedPackages: ["typescript"],
90
- analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
119
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
91
120
  repoIntelligence: fixtureRepoIntelligenceTypescript,
92
121
  };
93
122
  const facts = deriveScanNarrative(result);
@@ -96,14 +125,69 @@ describe("deriveScanNarrative", () => {
96
125
  expect(facts.reachability?.kind).toBe("build_only");
97
126
  expect(facts.blastRadius?.level).toBe("narrow");
98
127
  });
99
- it("treats empty packages-only repoIntelligence as absent", () => {
128
+ it("extracts packageUsage for all changed packages with blast factors and frameworks", () => {
129
+ const result = {
130
+ ...baseResult,
131
+ changedPackages: ["lodash", "axios"],
132
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
133
+ repoIntelligence: fixtureRepoIntelligenceMultiPackage,
134
+ };
135
+ const facts = deriveScanNarrative(result);
136
+ expect(facts.packageUsage).toHaveLength(2);
137
+ expect(facts.packageUsage.map((u) => u.packageName)).toEqual([
138
+ "lodash",
139
+ "axios",
140
+ ]);
141
+ expect(facts.packageUsage[0]?.paths).toContain("apps/billing/export.ts");
142
+ expect(facts.blastRadius?.factors).toEqual([
143
+ "multiple_runtime_consumers",
144
+ "shared_middleware",
145
+ ]);
146
+ expect(facts.frameworks).toEqual(["express", "react"]);
147
+ expect(facts.reachability?.evidence.frameworks).toEqual([
148
+ "express",
149
+ "react",
150
+ ]);
151
+ });
152
+ it("merges graph hotspots without duplicating code hotspots", () => {
153
+ const result = {
154
+ ...baseResult,
155
+ changedPackages: ["fastify"],
156
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
157
+ repoIntelligence: fixtureRepoIntelligenceFastify,
158
+ graphInsights: {
159
+ maxDepth: 3,
160
+ nodes: 10,
161
+ edges: 12,
162
+ hotspots: [
163
+ {
164
+ kind: "hotspot",
165
+ packageName: "fastify",
166
+ direct: true,
167
+ depth: 2,
168
+ },
169
+ {
170
+ kind: "hotspot",
171
+ packageName: "transitive-only-pkg",
172
+ direct: false,
173
+ depth: 4,
174
+ },
175
+ ],
176
+ },
177
+ };
178
+ const facts = deriveScanNarrative(result);
179
+ expect(facts.hotspots.map((h) => h.packageName)).toContain("fastify");
180
+ expect(facts.hotspots.map((h) => h.packageName)).toContain("transitive-only-pkg");
181
+ expect(facts.hotspots.filter((h) => h.packageName === "fastify")).toHaveLength(1);
182
+ });
183
+ it("does not tier1 when repoIntelligence block is omitted", () => {
100
184
  const result = {
101
185
  ...baseResult,
102
186
  changedPackages: ["react"],
103
- analysisPreparation: { codeIntelligenceAvailable: true, warnings: [] },
104
- repoIntelligence: fixtureRepoIntelligenceEmpty,
187
+ analysisPreparation: analysisPreparationWithValidRepoIntel(),
105
188
  };
106
189
  const facts = deriveScanNarrative(result);
190
+ expect(facts.availability.repoIntelligenceParse).toBe("absent");
107
191
  expect(facts.availability.tiersPresent.tier1).toBe(false);
108
192
  });
109
193
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=engineWireRegression.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineWireRegression.test.d.ts","sourceRoot":"","sources":["../src/engineWireRegression.test.ts"],"names":[],"mappings":""}