ira-review 3.1.4 → 3.1.6

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.
package/README.github.md CHANGED
@@ -137,10 +137,6 @@ This is the feature that catches "does this actually match the ticket?" before a
137
137
 
138
138
  When a JIRA ticket has **no acceptance criteria at all**, IRA generates suggested ACs from the diff and (by default) posts them as a comment on the JIRA ticket for the Product Owner to review and refine. If your CI environment cannot or should not write back to JIRA — for example, the JIRA service account is read-only, or org policy forbids automated JIRA writes — pass `--no-post-acs-to-jira` (or set `IRA_POST_ACS_TO_JIRA=false`). The suggestions still render in the PR summary under **📝 Suggested Acceptance Criteria**, so reviewers see them either way; only the JIRA write is suppressed.
139
139
 
140
- ### "All Clear" PR Summary
141
-
142
- When IRA finishes a review with **zero issues** found and **JIRA acceptance criteria are 100% covered** (or the ticket needs no ACs), the PR summary leads with a celebratory ✅ banner: a confident, specific signal that automated review passed, alongside an explicit reminder that **human reviewer approval is still required before merge**. The banner is suppressed when an AC gap exists so the summary never reads "safe to approve" while a requirements gap is still visible — IRA augments your code review process, it doesn't replace it.
143
-
144
140
  ### Inline AI Comments
145
141
 
146
142
  Each issue is posted as an inline comment on the exact line in the PR, containing:
package/README.md CHANGED
@@ -42,7 +42,6 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
42
42
  - Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
43
43
  - JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
44
44
  - JIRA AC auto-detection — finds AC from custom field or description automatically
45
- - "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
46
45
  - Custom team review rules via `.ira-rules.json` (see below)
47
46
  - Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
48
47
  - Comment deduplication across re-runs
package/README.npm.md CHANGED
@@ -42,7 +42,6 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
42
42
  - Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
43
43
  - JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
44
44
  - JIRA AC auto-detection — finds AC from custom field or description automatically
45
- - "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
46
45
  - Custom team review rules via `.ira-rules.json` (see below)
47
46
  - Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
48
47
  - Comment deduplication across re-runs
package/dist/cli.js CHANGED
@@ -14,10 +14,9 @@ import {
14
14
 
15
15
  // src/cli.ts
16
16
  import { Command } from "commander";
17
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
17
+ import { existsSync as existsSync9 } from "fs";
18
18
  import { readFile } from "fs/promises";
19
- import { resolve as resolve3, join as join7, dirname } from "path";
20
- import { fileURLToPath } from "url";
19
+ import { resolve as resolve4, join as join7 } from "path";
21
20
  import { execSync as execSync5 } from "child_process";
22
21
 
23
22
  // src/core/sonarClient.ts
@@ -914,7 +913,7 @@ var AmpCliProvider = class {
914
913
  return parseAIResponse(cleaned);
915
914
  }
916
915
  rawReview(prompt) {
917
- return new Promise((resolve4, reject) => {
916
+ return new Promise((resolve5, reject) => {
918
917
  const env2 = { ...process.env };
919
918
  const home = homedir();
920
919
  const isWin = process.platform === "win32";
@@ -1005,13 +1004,13 @@ var AmpCliProvider = class {
1005
1004
  }
1006
1005
  }
1007
1006
  if (result) {
1008
- resolve4(result);
1007
+ resolve5(result);
1009
1008
  } else if (errorOutput) {
1010
1009
  reject(new Error(`AMP CLI failed: ${errorOutput.trim()}`));
1011
1010
  } else if (code !== 0) {
1012
1011
  reject(new Error(`AMP CLI exited with code ${code}`));
1013
1012
  } else {
1014
- resolve4("");
1013
+ resolve5("");
1015
1014
  }
1016
1015
  });
1017
1016
  });
@@ -1028,11 +1027,9 @@ var CopilotCliProvider = class {
1028
1027
  return parseAIResponse(cleaned);
1029
1028
  }
1030
1029
  rawReview(prompt) {
1031
- return new Promise((resolve4, reject) => {
1030
+ return new Promise((resolve5, reject) => {
1032
1031
  const env2 = { ...process.env };
1033
1032
  const args = [
1034
- "-p",
1035
- prompt,
1036
1033
  "-s",
1037
1034
  // silent — only the response, no stats lines
1038
1035
  "--allow-all-tools",
@@ -1043,10 +1040,12 @@ var CopilotCliProvider = class {
1043
1040
  ];
1044
1041
  const useShell = process.platform === "win32";
1045
1042
  const child = spawn("copilot", args, {
1046
- stdio: ["ignore", "pipe", "pipe"],
1043
+ stdio: ["pipe", "pipe", "pipe"],
1047
1044
  env: env2,
1048
1045
  shell: useShell
1049
1046
  });
1047
+ child.stdin.write(prompt);
1048
+ child.stdin.end();
1050
1049
  let stdout = "";
1051
1050
  let stderr = "";
1052
1051
  child.stdout.on("data", (chunk) => {
@@ -1061,7 +1060,7 @@ var CopilotCliProvider = class {
1061
1060
  });
1062
1061
  child.on("close", (code) => {
1063
1062
  if (code === 0) {
1064
- resolve4(stdout.trim());
1063
+ resolve5(stdout.trim());
1065
1064
  return;
1066
1065
  }
1067
1066
  const detail = stderr.trim() || stdout.trim() || `exited with code ${code}`;
@@ -2612,73 +2611,36 @@ function validateCoverage(value) {
2612
2611
  }
2613
2612
 
2614
2613
  // src/core/summaryBuilder.ts
2615
- function buildSummary2(result) {
2614
+ function buildSummary2(result, meta = {}) {
2616
2615
  const lines = [];
2617
- lines.push("# \u{1F50D} IRA Review Summary");
2616
+ const dot = computeStatusDot(result);
2617
+ const riskLevel = result.risk?.level ?? "LOW";
2618
+ const riskScore = result.risk?.score ?? 0;
2619
+ const riskMax = result.risk?.maxScore ?? 100;
2620
+ lines.push(`### ${dot} IRA Review \xB7 ${riskLevel} risk (${riskScore}/${riskMax})`);
2618
2621
  lines.push("");
2619
- if (result.risk) {
2620
- const emoji = result.risk.level === "CRITICAL" ? "\u{1F534}" : result.risk.level === "HIGH" ? "\u{1F7E0}" : result.risk.level === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
2621
- lines.push(
2622
- `## ${emoji} Risk: ${result.risk.level} (${result.risk.score}/${result.risk.maxScore})`
2623
- );
2624
- lines.push("");
2625
- if (result.reviewMode === "sonar") {
2626
- lines.push("| Factor | Score | Detail |");
2627
- lines.push("|---|---|---|");
2628
- for (const f of result.risk.factors) {
2629
- lines.push(`| ${f.name} | ${f.score}/${f.maxScore} | ${f.detail} |`);
2630
- }
2631
- lines.push("");
2632
- }
2633
- }
2634
- const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
2635
- if (result.comments.length === 0 && !acGapExists) {
2636
- const fw = result.framework ?? "your stack";
2637
- const acLine = result.requirementCompletion ? `Acceptance criteria for **${result.requirementCompletion.jiraKey}**: **${result.requirementCompletion.completionPercentage}% covered** (${result.requirementCompletion.metCriteria}/${result.requirementCompletion.totalCriteria}).` : result.acceptanceValidation ? `Acceptance criteria for **${result.acceptanceValidation.jiraKey}**: ${result.acceptanceValidation.overallPass ? "**all met** \u2705" : "**partially met** \u2014 see the JIRA section above"}.` : result.acGeneration && result.acGeneration.criteria.length > 0 ? result.acGeneration.postedToJira ? `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** and posted them as a comment on the JIRA ticket for the Product Owner / requirement author to review and refine.` : `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** (see the Suggested Acceptance Criteria section below).` : null;
2638
- lines.push("## \u2705 All Clear \u2014 No Issues Found");
2639
- lines.push("");
2640
- lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
2641
- lines.push(`>`);
2642
- lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
2643
- if (acLine) {
2644
- lines.push(`>`);
2645
- lines.push(`> ${acLine}`);
2646
- }
2647
- if (result.risk) {
2648
- lines.push(`>`);
2649
- lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
2650
- }
2651
- lines.push(`>`);
2652
- lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
2653
- lines.push(`>`);
2654
- lines.push(`> \u{1F465} **Human reviewer approval is still required before merge.** IRA augments your code review process \u2014 it doesn't replace it. Please ensure your team's review and approval requirements have been met before merging.`);
2655
- lines.push("");
2656
- }
2657
- lines.push("## Overview");
2658
- lines.push("");
2659
- lines.push(`| Metric | Value |`);
2660
- lines.push(`|---|---|`);
2661
- lines.push(`| Review mode | ${result.reviewMode === "standalone" ? "AI-only" : "Sonar + AI"} |`);
2662
- lines.push(`| Total issues | ${result.totalIssues} |`);
2663
- lines.push(`| Reviewed (AI) | ${result.reviewedIssues} |`);
2664
- lines.push(
2665
- `| Framework | ${result.framework ?? "not detected"} |`
2666
- );
2622
+ const metrics = [];
2623
+ if (typeof result.filesReviewed === "number") {
2624
+ metrics.push(`${result.filesReviewed} file${result.filesReviewed === 1 ? "" : "s"} reviewed`);
2625
+ }
2626
+ metrics.push(`${result.comments.length} finding${result.comments.length === 1 ? "" : "s"}`);
2627
+ const acMetric = formatAcMetric(result);
2628
+ if (acMetric) metrics.push(acMetric);
2629
+ lines.push(metrics.join(" \xB7 "));
2667
2630
  lines.push("");
2668
- if (result.complexity && result.complexity.hotspots.length > 0) {
2669
- lines.push("## \u{1F9E0} Complexity Hotspots");
2631
+ if (result.comments.length > 0) {
2632
+ lines.push("## Findings");
2670
2633
  lines.push("");
2671
- lines.push("| File | Complexity | Cognitive |");
2672
- lines.push("|---|---|---|");
2673
- for (const h of result.complexity.hotspots.slice(0, 5)) {
2674
- lines.push(`| ${h.filePath} | ${h.complexity} | ${h.cognitiveComplexity} |`);
2634
+ lines.push("| File | Line | Rule | Severity |");
2635
+ lines.push("|---|---|---|---|");
2636
+ for (const c of result.comments) {
2637
+ lines.push(`| ${c.filePath} | ${c.line} | \`${c.rule}\` | ${c.severity} |`);
2675
2638
  }
2676
2639
  lines.push("");
2677
2640
  }
2678
2641
  if (result.requirementCompletion) {
2679
2642
  const rc = result.requirementCompletion;
2680
- const pctIcon = rc.completionPercentage === 100 ? "\u2705" : rc.completionPercentage >= 50 ? "\u{1F7E1}" : "\u{1F534}";
2681
- lines.push(`## ${pctIcon} Requirements: ${rc.jiraKey} - ${rc.completionPercentage}% Complete (${rc.metCriteria}/${rc.totalCriteria})`);
2643
+ lines.push(`## Acceptance Criteria \u2014 ${rc.jiraKey} (${rc.completionPercentage}% covered, ${rc.metCriteria}/${rc.totalCriteria})`);
2682
2644
  lines.push("");
2683
2645
  for (const r of rc.requirements) {
2684
2646
  const icon = r.coverage === "full" ? "\u2705" : r.coverage === "partial" ? "\u{1F7E1}" : "\u274C";
@@ -2689,42 +2651,65 @@ function buildSummary2(result) {
2689
2651
  }
2690
2652
  if (rc.edgeCases.length > 0) {
2691
2653
  lines.push("");
2692
- lines.push("### \u26A0\uFE0F Edge Cases Not Covered");
2654
+ lines.push("### Edge cases not covered");
2693
2655
  for (const e of rc.edgeCases) {
2694
2656
  lines.push(`- ${e}`);
2695
2657
  }
2696
2658
  }
2697
2659
  if (rc.parseWarning) {
2698
2660
  lines.push("");
2699
- lines.push(`> \u26A0\uFE0F **Warning:** ${rc.parseWarning}`);
2661
+ lines.push(`> \u26A0\uFE0F ${rc.parseWarning}`);
2700
2662
  }
2701
2663
  lines.push("");
2702
2664
  } else if (result.acceptanceValidation) {
2703
2665
  const av = result.acceptanceValidation;
2704
- const icon = av.overallPass ? "\u2705" : "\u274C";
2705
- lines.push(`## ${icon} JIRA: ${av.jiraKey} - ${av.summary}`);
2666
+ const status = av.overallPass ? "all met" : "gaps found";
2667
+ lines.push(`## Acceptance Criteria \u2014 ${av.jiraKey} (${status})`);
2668
+ lines.push("");
2669
+ lines.push(`_${av.summary}_`);
2706
2670
  lines.push("");
2707
2671
  for (const c of av.criteria) {
2708
2672
  lines.push(`- ${c.met ? "\u2705" : "\u274C"} ${c.description}`);
2709
2673
  }
2710
2674
  lines.push("");
2711
2675
  }
2676
+ if (result.risk && result.reviewMode === "sonar" && result.risk.factors.length > 0) {
2677
+ lines.push("## Risk Factors");
2678
+ lines.push("");
2679
+ lines.push("| Factor | Score | Detail |");
2680
+ lines.push("|---|---|---|");
2681
+ for (const f of result.risk.factors) {
2682
+ lines.push(`| ${f.name} | ${f.score}/${f.maxScore} | ${f.detail} |`);
2683
+ }
2684
+ lines.push("");
2685
+ }
2686
+ if (result.complexity && result.complexity.hotspots.length > 0) {
2687
+ lines.push("## Complexity Hotspots");
2688
+ lines.push("");
2689
+ lines.push("| File | Complexity | Cognitive |");
2690
+ lines.push("|---|---|---|");
2691
+ for (const h of result.complexity.hotspots.slice(0, 5)) {
2692
+ lines.push(`| ${h.filePath} | ${h.complexity} | ${h.cognitiveComplexity} |`);
2693
+ }
2694
+ lines.push("");
2695
+ }
2712
2696
  if (result.acGeneration && result.acGeneration.criteria.length > 0) {
2713
2697
  const ag = result.acGeneration;
2714
- lines.push(`## \u{1F4DD} Suggested Acceptance Criteria (${ag.totalCriteria} generated)`);
2698
+ lines.push(`## Suggested Acceptance Criteria \u2014 ${ag.jiraKey} (${ag.totalCriteria} generated)`);
2715
2699
  lines.push("");
2716
- lines.push(`> No acceptance criteria found in ${ag.jiraKey}. IRA inferred the following from: ${ag.sources.join(", ")}.`);
2700
+ const postedNote = ag.postedToJira ? `Posted to JIRA as a comment for the Product Owner to review.` : `Not posted to JIRA (suggestions stay in this summary only).`;
2701
+ lines.push(`> No acceptance criteria found on **${ag.jiraKey}**. IRA inferred the following from: ${ag.sources.join(", ")}. ${postedNote}`);
2717
2702
  lines.push("");
2718
2703
  for (const ac of ag.criteria) {
2719
- lines.push(`**${ac.id}:**`);
2704
+ lines.push(`**${ac.id}**`);
2720
2705
  lines.push(`- **Given** ${ac.given}`);
2721
2706
  lines.push(`- **When** ${ac.when}`);
2722
2707
  lines.push(`- **Then** ${ac.then}`);
2723
2708
  lines.push("");
2724
2709
  }
2725
2710
  if (ag.reviewHints && ag.reviewHints.length > 0) {
2726
- lines.push(`### \u2753 Questions for PO`);
2727
- lines.push(`> IRA could not determine the following from the code. Answering these will strengthen the ACs above:`);
2711
+ lines.push(`### Questions for PO`);
2712
+ lines.push(`> IRA could not determine the following from the code. Answering these will strengthen the ACs above.`);
2728
2713
  lines.push("");
2729
2714
  for (const hint of ag.reviewHints) {
2730
2715
  lines.push(`- ${hint}`);
@@ -2732,7 +2717,7 @@ function buildSummary2(result) {
2732
2717
  lines.push("");
2733
2718
  }
2734
2719
  if (ag.parseWarning) {
2735
- lines.push(`> \u26A0\uFE0F **Warning:** ${ag.parseWarning}`);
2720
+ lines.push(`> \u26A0\uFE0F ${ag.parseWarning}`);
2736
2721
  lines.push("");
2737
2722
  }
2738
2723
  }
@@ -2743,7 +2728,7 @@ function buildSummary2(result) {
2743
2728
  const headerParts = [`${testableCount} test${testableCount !== 1 ? "s" : ""}`];
2744
2729
  if (tg.edgeCases > 0) headerParts.push(`${tg.edgeCases} advanced cases`);
2745
2730
  if (notTestableCount > 0) headerParts.push(`${notTestableCount} not-testable`);
2746
- lines.push(`## \u{1F9EA} Generated Test Cases (${headerParts.join(", ")})`);
2731
+ lines.push(`## Generated Test Cases (${headerParts.join(", ")})`);
2747
2732
  lines.push("");
2748
2733
  const byCriterion = /* @__PURE__ */ new Map();
2749
2734
  for (const tc of tg.testCases) {
@@ -2772,25 +2757,62 @@ function buildSummary2(result) {
2772
2757
  }
2773
2758
  }
2774
2759
  if (result.testGeneration?.parseWarning) {
2775
- lines.push(`> \u26A0\uFE0F **Warning:** ${result.testGeneration.parseWarning}`);
2776
- lines.push("");
2777
- }
2778
- if (result.comments.length > 0) {
2779
- lines.push("## Issues Reviewed");
2780
- lines.push("");
2781
- lines.push("| File | Line | Rule | Severity |");
2782
- lines.push("|---|---|---|---|");
2783
- for (const c of result.comments) {
2784
- lines.push(
2785
- `| ${c.filePath} | ${c.line} | \`${c.rule}\` | ${c.severity} |`
2786
- );
2787
- }
2760
+ lines.push(`> \u26A0\uFE0F ${result.testGeneration.parseWarning}`);
2788
2761
  lines.push("");
2789
2762
  }
2790
2763
  lines.push("---");
2791
- lines.push("*Generated by [ira-review](https://www.npmjs.com/package/ira-review)*");
2764
+ lines.push(`_${formatFooter(meta)}_`);
2765
+ lines.push("");
2766
+ lines.push("_Human reviewer approval is still required before merge. IRA augments code review; it does not replace it._");
2792
2767
  return lines.join("\n");
2793
2768
  }
2769
+ function computeStatusDot(result) {
2770
+ const riskRank = riskRankFor(result.risk?.level ?? "LOW");
2771
+ const acRank = acGapRank(result);
2772
+ const blockerRank = result.comments.some((c) => c.severity === "BLOCKER") ? 3 : 0;
2773
+ const worst = Math.max(riskRank, acRank, blockerRank);
2774
+ return ["\u{1F7E2}", "\u{1F7E1}", "\u{1F7E0}", "\u{1F534}"][worst] ?? "\u{1F7E2}";
2775
+ }
2776
+ function riskRankFor(level) {
2777
+ switch (level) {
2778
+ case "CRITICAL":
2779
+ return 3;
2780
+ case "HIGH":
2781
+ return 2;
2782
+ case "MEDIUM":
2783
+ return 1;
2784
+ default:
2785
+ return 0;
2786
+ }
2787
+ }
2788
+ function acGapRank(result) {
2789
+ if (result.requirementCompletion && result.requirementCompletion.completionPercentage < 100) return 1;
2790
+ if (result.acceptanceValidation && !result.acceptanceValidation.overallPass) return 1;
2791
+ return 0;
2792
+ }
2793
+ function formatAcMetric(result) {
2794
+ if (result.requirementCompletion) {
2795
+ const rc = result.requirementCompletion;
2796
+ return `${rc.jiraKey}: ${rc.metCriteria}/${rc.totalCriteria} ACs met`;
2797
+ }
2798
+ if (result.acceptanceValidation) {
2799
+ const av = result.acceptanceValidation;
2800
+ return `${av.jiraKey}: ${av.overallPass ? "ACs met" : "AC gaps"}`;
2801
+ }
2802
+ if (result.acGeneration && result.acGeneration.criteria.length > 0) {
2803
+ const ag = result.acGeneration;
2804
+ return `${ag.jiraKey}: ${ag.totalCriteria} AC${ag.totalCriteria === 1 ? "" : "s"} suggested`;
2805
+ }
2806
+ return null;
2807
+ }
2808
+ function formatFooter(meta) {
2809
+ const parts = [];
2810
+ parts.push(`ira-review${meta.version ? ` ${meta.version}` : ""}`);
2811
+ if (meta.aiProvider || meta.aiModel) {
2812
+ parts.push([meta.aiProvider, meta.aiModel].filter(Boolean).join("/"));
2813
+ }
2814
+ return parts.join(" \xB7 ");
2815
+ }
2794
2816
 
2795
2817
  // src/integrations/notifier.ts
2796
2818
  var Notifier = class {
@@ -2910,6 +2932,31 @@ function resolveGitRoot() {
2910
2932
  }
2911
2933
  }
2912
2934
 
2935
+ // src/utils/packageInfo.ts
2936
+ import { readFileSync as readFileSync6 } from "fs";
2937
+ import { dirname, resolve as resolve2 } from "path";
2938
+ import { fileURLToPath } from "url";
2939
+ function readPackageVersion() {
2940
+ try {
2941
+ const here = dirname(fileURLToPath(import.meta.url));
2942
+ const candidates = [
2943
+ resolve2(here, "..", "package.json"),
2944
+ // dist/utils/* → ../package.json
2945
+ resolve2(here, "..", "..", "package.json")
2946
+ // src/utils/* → ../../package.json
2947
+ ];
2948
+ for (const pkgPath of candidates) {
2949
+ try {
2950
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
2951
+ if (pkg.name === "ira-review" && pkg.version) return pkg.version;
2952
+ } catch {
2953
+ }
2954
+ }
2955
+ } catch {
2956
+ }
2957
+ return "unknown";
2958
+ }
2959
+
2913
2960
  // src/core/acGenerator.ts
2914
2961
  function escapeSentinels5(text) {
2915
2962
  return text.replace(/<\/(ticket_context|diff_context|commit_context|epic_context)>/gi, "<\\/$1>");
@@ -3514,6 +3561,7 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3514
3561
  (c) => !existing.has(deduplicateKey(c.filePath, c.line, c.rule))
3515
3562
  );
3516
3563
  }
3564
+ const filesReviewed = reviewMode === "sonar" ? grouped.length : diffByFile.size;
3517
3565
  const result = {
3518
3566
  pullRequestId,
3519
3567
  framework,
@@ -3528,9 +3576,14 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3528
3576
  testGeneration,
3529
3577
  requirementCompletion,
3530
3578
  acGeneration,
3531
- warnings
3579
+ warnings,
3580
+ filesReviewed
3532
3581
  };
3533
- const summary = buildSummary2(result);
3582
+ const summary = buildSummary2(result, {
3583
+ version: readPackageVersion(),
3584
+ aiProvider: this.config.ai.provider,
3585
+ aiModel: this.config.ai.model
3586
+ });
3534
3587
  if (this.config.dryRun) {
3535
3588
  console.log(summary);
3536
3589
  for (const comment of comments) {
@@ -3850,19 +3903,19 @@ function optionalEnv(key) {
3850
3903
  }
3851
3904
 
3852
3905
  // src/utils/configFile.ts
3853
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
3854
- import { resolve as resolve2 } from "path";
3906
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
3907
+ import { resolve as resolve3 } from "path";
3855
3908
  var CONFIG_FILENAMES = [".irarc.json", "ira.config.json"];
3856
3909
  function loadConfigFile(explicitPath, cwd = process.cwd()) {
3857
3910
  if (explicitPath) {
3858
- const filePath = resolve2(cwd, explicitPath);
3911
+ const filePath = resolve3(cwd, explicitPath);
3859
3912
  if (!existsSync8(filePath)) {
3860
3913
  throw new Error(`Config file not found: ${filePath}`);
3861
3914
  }
3862
3915
  return parseConfigFile(filePath);
3863
3916
  }
3864
3917
  for (const filename of CONFIG_FILENAMES) {
3865
- const filePath = resolve2(cwd, filename);
3918
+ const filePath = resolve3(cwd, filename);
3866
3919
  if (existsSync8(filePath)) {
3867
3920
  return parseConfigFile(filePath);
3868
3921
  }
@@ -3871,7 +3924,7 @@ function loadConfigFile(explicitPath, cwd = process.cwd()) {
3871
3924
  }
3872
3925
  function parseConfigFile(filePath) {
3873
3926
  try {
3874
- const raw = readFileSync6(filePath, "utf-8");
3927
+ const raw = readFileSync7(filePath, "utf-8");
3875
3928
  const parsed = JSON.parse(raw);
3876
3929
  return mapConfigToFlat(parsed);
3877
3930
  } catch (error) {
@@ -4059,7 +4112,7 @@ function isCiEnvironment() {
4059
4112
  }
4060
4113
  function isFirstRun() {
4061
4114
  if (isCiEnvironment()) return false;
4062
- return !existsSync9(resolve3(process.cwd(), ".irarc.json")) && !existsSync9(resolve3(process.cwd(), "ira.config.json"));
4115
+ return !existsSync9(resolve4(process.cwd(), ".irarc.json")) && !existsSync9(resolve4(process.cwd(), "ira.config.json"));
4063
4116
  }
4064
4117
  function logEnvironmentInfo(config) {
4065
4118
  const proxy = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy;
@@ -4085,16 +4138,6 @@ function logEnvironmentInfo(config) {
4085
4138
  step("\u{1F510}", `SSL_CERT_FILE: ${sslCert}`);
4086
4139
  }
4087
4140
  }
4088
- function readPackageVersion() {
4089
- try {
4090
- const here = dirname(fileURLToPath(import.meta.url));
4091
- const pkgPath = resolve3(here, "..", "package.json");
4092
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4093
- if (pkg.version) return pkg.version;
4094
- } catch {
4095
- }
4096
- return "unknown";
4097
- }
4098
4141
  var program = new Command();
4099
4142
  program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version(readPackageVersion());
4100
4143
  program.command("review").description("Run AI-powered review on a pull request").option("--sonar-url <url>", "SonarQube/SonarCloud base URL (or IRA_SONAR_URL)").option("--sonar-token <token>", "SonarQube API token (or IRA_SONAR_TOKEN)").option("--project-key <key>", "Sonar project key (or IRA_PROJECT_KEY)").option("--pr <id>", "Pull request ID (or IRA_PR)").option("--scm-provider <provider>", "SCM provider: bitbucket or github (or IRA_SCM_PROVIDER)").option("--bitbucket-token <token>", "Bitbucket API token (or IRA_BITBUCKET_TOKEN)").option("--repo <repo>", "Bitbucket workspace/repo-slug (Cloud) or PROJECT/repo-slug (Server) (or IRA_REPO)").option("--bitbucket-url <url>", "Bitbucket base URL (or IRA_BITBUCKET_URL)").option("--bitbucket-type <type>", "Bitbucket type: cloud or server (auto-detects from URL if omitted; or IRA_BITBUCKET_TYPE)").option("--github-token <token>", "GitHub API token (or IRA_GITHUB_TOKEN)").option("--github-repo <repo>", "GitHub owner/repo (or IRA_GITHUB_REPO)").option("--github-url <url>", "GitHub Enterprise URL (or IRA_GITHUB_URL)").option("--ai-provider <provider>", "AI provider").option("--ai-model <model>", "AI model to use").option("--dry-run", "Print comments to stdout instead of posting to SCM").option("--min-severity <level>", "Minimum severity to review (BLOCKER|CRITICAL|MAJOR|MINOR|INFO)").option("--jira-url <url>", "JIRA base URL (or IRA_JIRA_URL)").option("--jira-email <email>", "JIRA email (or IRA_JIRA_EMAIL)").option("--jira-token <token>", "JIRA API token (or IRA_JIRA_TOKEN)").option("--jira-type <type>", "JIRA type: cloud or server (auto-detects from URL if omitted)").option("--jira-ticket <key>", "JIRA ticket key (e.g. PROJ-123)").option("--jira-ac-field <field>", "Custom field ID for acceptance criteria").option("--jira-ac-source <source>", "Where to look for AC: customField, description, or both (default: customField)").option("--no-post-acs-to-jira", "Don't post AI-generated acceptance criteria back to the JIRA ticket when none exist; keep them in the PR summary only (or IRA_POST_ACS_TO_JIRA=false)").option("--slack-webhook <url>", "Slack webhook URL for notifications").option("--teams-webhook <url>", "Teams webhook URL for notifications").option("--notify-min-risk <level>", "Only notify when risk is at or above this level: low, medium, high, critical").option("--notify-on-ac-fail", "Send notification when JIRA acceptance criteria validation fails").option("--ai-base-url <url>", "AI provider base URL (Azure endpoint, Ollama URL)").option("--ai-api-key <key>", "AI API key (or IRA_AI_API_KEY / OPENAI_API_KEY)").option("--ai-api-version <version>", "Azure OpenAI API version").option("--ai-deployment <name>", "Azure OpenAI deployment name").option("--ai-model-critical <model>", "Stronger AI model for BLOCKER/CRITICAL issues").option("--generate-tests", "Generate test cases from JIRA acceptance criteria").option("--test-framework <framework>", "Test framework: jest, vitest, mocha, playwright, cypress, gherkin, pytest, junit (default: jest)").option("--config <path>", "Path to config file (default: auto-detect .irarc.json / ira.config.json)").option("--no-config-file", "Disable auto-loading config file from repo").option("--rules-url <url>", "Fetch .ira-rules.json from URL (raw HTTP) \u2014 useful when CI has no full checkout (or IRA_RULES_URL)").option("--comment-style <style>", "Comment formatter style: compact (default) or detailed (or IRA_COMMENT_STYLE)").action(async (opts) => {
@@ -4406,7 +4449,7 @@ program.command("init").description("Interactive setup: detect config and write
4406
4449
  const ai = detectAiProvider();
4407
4450
  step("\u2713", `SCM: ${scm ?? "not detected"} | AI: ${ai ? ai.provider : "not detected"}`);
4408
4451
  step("\u23F3", "Writing project config\u2026");
4409
- const rcPath = resolve3(process.cwd(), ".irarc.json");
4452
+ const rcPath = resolve4(process.cwd(), ".irarc.json");
4410
4453
  let rcConfig = {};
4411
4454
  if (existsSync9(rcPath)) {
4412
4455
  try {