ira-review 3.1.5 → 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,12 +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
- "",
1036
- // empty -p triggers stdin read on copilot CLI 1.x
1037
1033
  "-s",
1038
1034
  // silent — only the response, no stats lines
1039
1035
  "--allow-all-tools",
@@ -1064,7 +1060,7 @@ var CopilotCliProvider = class {
1064
1060
  });
1065
1061
  child.on("close", (code) => {
1066
1062
  if (code === 0) {
1067
- resolve4(stdout.trim());
1063
+ resolve5(stdout.trim());
1068
1064
  return;
1069
1065
  }
1070
1066
  const detail = stderr.trim() || stdout.trim() || `exited with code ${code}`;
@@ -2615,73 +2611,36 @@ function validateCoverage(value) {
2615
2611
  }
2616
2612
 
2617
2613
  // src/core/summaryBuilder.ts
2618
- function buildSummary2(result) {
2614
+ function buildSummary2(result, meta = {}) {
2619
2615
  const lines = [];
2620
- lines.push("# \u{1F50D} IRA Review Summary");
2621
- lines.push("");
2622
- if (result.risk) {
2623
- const emoji = result.risk.level === "CRITICAL" ? "\u{1F534}" : result.risk.level === "HIGH" ? "\u{1F7E0}" : result.risk.level === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
2624
- lines.push(
2625
- `## ${emoji} Risk: ${result.risk.level} (${result.risk.score}/${result.risk.maxScore})`
2626
- );
2627
- lines.push("");
2628
- if (result.reviewMode === "sonar") {
2629
- lines.push("| Factor | Score | Detail |");
2630
- lines.push("|---|---|---|");
2631
- for (const f of result.risk.factors) {
2632
- lines.push(`| ${f.name} | ${f.score}/${f.maxScore} | ${f.detail} |`);
2633
- }
2634
- lines.push("");
2635
- }
2636
- }
2637
- const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
2638
- if (result.comments.length === 0 && !acGapExists) {
2639
- const fw = result.framework ?? "your stack";
2640
- 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;
2641
- lines.push("## \u2705 All Clear \u2014 No Issues Found");
2642
- lines.push("");
2643
- lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
2644
- lines.push(`>`);
2645
- lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
2646
- if (acLine) {
2647
- lines.push(`>`);
2648
- lines.push(`> ${acLine}`);
2649
- }
2650
- if (result.risk) {
2651
- lines.push(`>`);
2652
- lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
2653
- }
2654
- lines.push(`>`);
2655
- lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
2656
- lines.push(`>`);
2657
- 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.`);
2658
- lines.push("");
2659
- }
2660
- lines.push("## Overview");
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})`);
2661
2621
  lines.push("");
2662
- lines.push(`| Metric | Value |`);
2663
- lines.push(`|---|---|`);
2664
- lines.push(`| Review mode | ${result.reviewMode === "standalone" ? "AI-only" : "Sonar + AI"} |`);
2665
- lines.push(`| Total issues | ${result.totalIssues} |`);
2666
- lines.push(`| Reviewed (AI) | ${result.reviewedIssues} |`);
2667
- lines.push(
2668
- `| Framework | ${result.framework ?? "not detected"} |`
2669
- );
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 "));
2670
2630
  lines.push("");
2671
- if (result.complexity && result.complexity.hotspots.length > 0) {
2672
- lines.push("## \u{1F9E0} Complexity Hotspots");
2631
+ if (result.comments.length > 0) {
2632
+ lines.push("## Findings");
2673
2633
  lines.push("");
2674
- lines.push("| File | Complexity | Cognitive |");
2675
- lines.push("|---|---|---|");
2676
- for (const h of result.complexity.hotspots.slice(0, 5)) {
2677
- 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} |`);
2678
2638
  }
2679
2639
  lines.push("");
2680
2640
  }
2681
2641
  if (result.requirementCompletion) {
2682
2642
  const rc = result.requirementCompletion;
2683
- const pctIcon = rc.completionPercentage === 100 ? "\u2705" : rc.completionPercentage >= 50 ? "\u{1F7E1}" : "\u{1F534}";
2684
- 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})`);
2685
2644
  lines.push("");
2686
2645
  for (const r of rc.requirements) {
2687
2646
  const icon = r.coverage === "full" ? "\u2705" : r.coverage === "partial" ? "\u{1F7E1}" : "\u274C";
@@ -2692,42 +2651,65 @@ function buildSummary2(result) {
2692
2651
  }
2693
2652
  if (rc.edgeCases.length > 0) {
2694
2653
  lines.push("");
2695
- lines.push("### \u26A0\uFE0F Edge Cases Not Covered");
2654
+ lines.push("### Edge cases not covered");
2696
2655
  for (const e of rc.edgeCases) {
2697
2656
  lines.push(`- ${e}`);
2698
2657
  }
2699
2658
  }
2700
2659
  if (rc.parseWarning) {
2701
2660
  lines.push("");
2702
- lines.push(`> \u26A0\uFE0F **Warning:** ${rc.parseWarning}`);
2661
+ lines.push(`> \u26A0\uFE0F ${rc.parseWarning}`);
2703
2662
  }
2704
2663
  lines.push("");
2705
2664
  } else if (result.acceptanceValidation) {
2706
2665
  const av = result.acceptanceValidation;
2707
- const icon = av.overallPass ? "\u2705" : "\u274C";
2708
- 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}_`);
2709
2670
  lines.push("");
2710
2671
  for (const c of av.criteria) {
2711
2672
  lines.push(`- ${c.met ? "\u2705" : "\u274C"} ${c.description}`);
2712
2673
  }
2713
2674
  lines.push("");
2714
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
+ }
2715
2696
  if (result.acGeneration && result.acGeneration.criteria.length > 0) {
2716
2697
  const ag = result.acGeneration;
2717
- lines.push(`## \u{1F4DD} Suggested Acceptance Criteria (${ag.totalCriteria} generated)`);
2698
+ lines.push(`## Suggested Acceptance Criteria \u2014 ${ag.jiraKey} (${ag.totalCriteria} generated)`);
2718
2699
  lines.push("");
2719
- 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}`);
2720
2702
  lines.push("");
2721
2703
  for (const ac of ag.criteria) {
2722
- lines.push(`**${ac.id}:**`);
2704
+ lines.push(`**${ac.id}**`);
2723
2705
  lines.push(`- **Given** ${ac.given}`);
2724
2706
  lines.push(`- **When** ${ac.when}`);
2725
2707
  lines.push(`- **Then** ${ac.then}`);
2726
2708
  lines.push("");
2727
2709
  }
2728
2710
  if (ag.reviewHints && ag.reviewHints.length > 0) {
2729
- lines.push(`### \u2753 Questions for PO`);
2730
- 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.`);
2731
2713
  lines.push("");
2732
2714
  for (const hint of ag.reviewHints) {
2733
2715
  lines.push(`- ${hint}`);
@@ -2735,7 +2717,7 @@ function buildSummary2(result) {
2735
2717
  lines.push("");
2736
2718
  }
2737
2719
  if (ag.parseWarning) {
2738
- lines.push(`> \u26A0\uFE0F **Warning:** ${ag.parseWarning}`);
2720
+ lines.push(`> \u26A0\uFE0F ${ag.parseWarning}`);
2739
2721
  lines.push("");
2740
2722
  }
2741
2723
  }
@@ -2746,7 +2728,7 @@ function buildSummary2(result) {
2746
2728
  const headerParts = [`${testableCount} test${testableCount !== 1 ? "s" : ""}`];
2747
2729
  if (tg.edgeCases > 0) headerParts.push(`${tg.edgeCases} advanced cases`);
2748
2730
  if (notTestableCount > 0) headerParts.push(`${notTestableCount} not-testable`);
2749
- lines.push(`## \u{1F9EA} Generated Test Cases (${headerParts.join(", ")})`);
2731
+ lines.push(`## Generated Test Cases (${headerParts.join(", ")})`);
2750
2732
  lines.push("");
2751
2733
  const byCriterion = /* @__PURE__ */ new Map();
2752
2734
  for (const tc of tg.testCases) {
@@ -2775,25 +2757,62 @@ function buildSummary2(result) {
2775
2757
  }
2776
2758
  }
2777
2759
  if (result.testGeneration?.parseWarning) {
2778
- lines.push(`> \u26A0\uFE0F **Warning:** ${result.testGeneration.parseWarning}`);
2779
- lines.push("");
2780
- }
2781
- if (result.comments.length > 0) {
2782
- lines.push("## Issues Reviewed");
2783
- lines.push("");
2784
- lines.push("| File | Line | Rule | Severity |");
2785
- lines.push("|---|---|---|---|");
2786
- for (const c of result.comments) {
2787
- lines.push(
2788
- `| ${c.filePath} | ${c.line} | \`${c.rule}\` | ${c.severity} |`
2789
- );
2790
- }
2760
+ lines.push(`> \u26A0\uFE0F ${result.testGeneration.parseWarning}`);
2791
2761
  lines.push("");
2792
2762
  }
2793
2763
  lines.push("---");
2794
- 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._");
2795
2767
  return lines.join("\n");
2796
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
+ }
2797
2816
 
2798
2817
  // src/integrations/notifier.ts
2799
2818
  var Notifier = class {
@@ -2913,6 +2932,31 @@ function resolveGitRoot() {
2913
2932
  }
2914
2933
  }
2915
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
+
2916
2960
  // src/core/acGenerator.ts
2917
2961
  function escapeSentinels5(text) {
2918
2962
  return text.replace(/<\/(ticket_context|diff_context|commit_context|epic_context)>/gi, "<\\/$1>");
@@ -3517,6 +3561,7 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3517
3561
  (c) => !existing.has(deduplicateKey(c.filePath, c.line, c.rule))
3518
3562
  );
3519
3563
  }
3564
+ const filesReviewed = reviewMode === "sonar" ? grouped.length : diffByFile.size;
3520
3565
  const result = {
3521
3566
  pullRequestId,
3522
3567
  framework,
@@ -3531,9 +3576,14 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3531
3576
  testGeneration,
3532
3577
  requirementCompletion,
3533
3578
  acGeneration,
3534
- warnings
3579
+ warnings,
3580
+ filesReviewed
3535
3581
  };
3536
- 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
+ });
3537
3587
  if (this.config.dryRun) {
3538
3588
  console.log(summary);
3539
3589
  for (const comment of comments) {
@@ -3853,19 +3903,19 @@ function optionalEnv(key) {
3853
3903
  }
3854
3904
 
3855
3905
  // src/utils/configFile.ts
3856
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
3857
- import { resolve as resolve2 } from "path";
3906
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
3907
+ import { resolve as resolve3 } from "path";
3858
3908
  var CONFIG_FILENAMES = [".irarc.json", "ira.config.json"];
3859
3909
  function loadConfigFile(explicitPath, cwd = process.cwd()) {
3860
3910
  if (explicitPath) {
3861
- const filePath = resolve2(cwd, explicitPath);
3911
+ const filePath = resolve3(cwd, explicitPath);
3862
3912
  if (!existsSync8(filePath)) {
3863
3913
  throw new Error(`Config file not found: ${filePath}`);
3864
3914
  }
3865
3915
  return parseConfigFile(filePath);
3866
3916
  }
3867
3917
  for (const filename of CONFIG_FILENAMES) {
3868
- const filePath = resolve2(cwd, filename);
3918
+ const filePath = resolve3(cwd, filename);
3869
3919
  if (existsSync8(filePath)) {
3870
3920
  return parseConfigFile(filePath);
3871
3921
  }
@@ -3874,7 +3924,7 @@ function loadConfigFile(explicitPath, cwd = process.cwd()) {
3874
3924
  }
3875
3925
  function parseConfigFile(filePath) {
3876
3926
  try {
3877
- const raw = readFileSync6(filePath, "utf-8");
3927
+ const raw = readFileSync7(filePath, "utf-8");
3878
3928
  const parsed = JSON.parse(raw);
3879
3929
  return mapConfigToFlat(parsed);
3880
3930
  } catch (error) {
@@ -4062,7 +4112,7 @@ function isCiEnvironment() {
4062
4112
  }
4063
4113
  function isFirstRun() {
4064
4114
  if (isCiEnvironment()) return false;
4065
- 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"));
4066
4116
  }
4067
4117
  function logEnvironmentInfo(config) {
4068
4118
  const proxy = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy;
@@ -4088,16 +4138,6 @@ function logEnvironmentInfo(config) {
4088
4138
  step("\u{1F510}", `SSL_CERT_FILE: ${sslCert}`);
4089
4139
  }
4090
4140
  }
4091
- function readPackageVersion() {
4092
- try {
4093
- const here = dirname(fileURLToPath(import.meta.url));
4094
- const pkgPath = resolve3(here, "..", "package.json");
4095
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4096
- if (pkg.version) return pkg.version;
4097
- } catch {
4098
- }
4099
- return "unknown";
4100
- }
4101
4141
  var program = new Command();
4102
4142
  program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version(readPackageVersion());
4103
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) => {
@@ -4409,7 +4449,7 @@ program.command("init").description("Interactive setup: detect config and write
4409
4449
  const ai = detectAiProvider();
4410
4450
  step("\u2713", `SCM: ${scm ?? "not detected"} | AI: ${ai ? ai.provider : "not detected"}`);
4411
4451
  step("\u23F3", "Writing project config\u2026");
4412
- const rcPath = resolve3(process.cwd(), ".irarc.json");
4452
+ const rcPath = resolve4(process.cwd(), ".irarc.json");
4413
4453
  let rcConfig = {};
4414
4454
  if (existsSync9(rcPath)) {
4415
4455
  try {