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 +0 -4
- package/README.md +0 -1
- package/README.npm.md +0 -1
- package/dist/cli.js +153 -113
- package/dist/index.cjs +153 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +150 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
17
|
+
import { existsSync as existsSync9 } from "fs";
|
|
18
18
|
import { readFile } from "fs/promises";
|
|
19
|
-
import { resolve as
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
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.
|
|
2672
|
-
lines.push("##
|
|
2631
|
+
if (result.comments.length > 0) {
|
|
2632
|
+
lines.push("## Findings");
|
|
2673
2633
|
lines.push("");
|
|
2674
|
-
lines.push("| File |
|
|
2675
|
-
lines.push("
|
|
2676
|
-
for (const
|
|
2677
|
-
lines.push(`| ${
|
|
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
|
-
|
|
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("###
|
|
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
|
|
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
|
|
2708
|
-
lines.push(`##
|
|
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(`##
|
|
2698
|
+
lines.push(`## Suggested Acceptance Criteria \u2014 ${ag.jiraKey} (${ag.totalCriteria} generated)`);
|
|
2718
2699
|
lines.push("");
|
|
2719
|
-
|
|
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(`###
|
|
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
|
|
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(`##
|
|
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
|
|
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(
|
|
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
|
|
3857
|
-
import { resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
4452
|
+
const rcPath = resolve4(process.cwd(), ".irarc.json");
|
|
4413
4453
|
let rcConfig = {};
|
|
4414
4454
|
if (existsSync9(rcPath)) {
|
|
4415
4455
|
try {
|