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 +0 -4
- package/README.md +0 -1
- package/README.npm.md +0 -1
- package/dist/cli.js +156 -113
- package/dist/index.cjs +156 -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 +153 -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,11 +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
|
-
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: ["
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
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.
|
|
2669
|
-
lines.push("##
|
|
2631
|
+
if (result.comments.length > 0) {
|
|
2632
|
+
lines.push("## Findings");
|
|
2670
2633
|
lines.push("");
|
|
2671
|
-
lines.push("| File |
|
|
2672
|
-
lines.push("
|
|
2673
|
-
for (const
|
|
2674
|
-
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} |`);
|
|
2675
2638
|
}
|
|
2676
2639
|
lines.push("");
|
|
2677
2640
|
}
|
|
2678
2641
|
if (result.requirementCompletion) {
|
|
2679
2642
|
const rc = result.requirementCompletion;
|
|
2680
|
-
|
|
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("###
|
|
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
|
|
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
|
|
2705
|
-
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}_`);
|
|
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(`##
|
|
2698
|
+
lines.push(`## Suggested Acceptance Criteria \u2014 ${ag.jiraKey} (${ag.totalCriteria} generated)`);
|
|
2715
2699
|
lines.push("");
|
|
2716
|
-
|
|
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(`###
|
|
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
|
|
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(`##
|
|
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
|
|
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(
|
|
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
|
|
3854
|
-
import { resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
4452
|
+
const rcPath = resolve4(process.cwd(), ".irarc.json");
|
|
4410
4453
|
let rcConfig = {};
|
|
4411
4454
|
if (existsSync9(rcPath)) {
|
|
4412
4455
|
try {
|