secufusion-mcp 1.0.60 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +182 -30
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -2675,8 +2675,13 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2675
2675
|
skip_checks: z.array(z.enum(["tenant_isolation", "n_plus_one", "missing_index", "kafka_sync", "early_return", "ai_review", "context_review"])).optional().default([]).describe("Checks to skip. Mechanical: tenant_isolation | n_plus_one | missing_index | kafka_sync | early_return. " +
|
|
2676
2676
|
"Review tiers: ai_review (Tier 2) | context_review (Tier 3). " +
|
|
2677
2677
|
"Use sparingly — only for acknowledged legacy files or CI environments where context is unavailable."),
|
|
2678
|
-
|
|
2679
|
-
|
|
2678
|
+
suppressions: z.array(z.object({
|
|
2679
|
+
check: z.enum(["TENANT_ISOLATION", "N_PLUS_ONE", "MISSING_INDEX", "KAFKA_SYNC", "EARLY_RETURN"]),
|
|
2680
|
+
method: z.string().describe("Method name to suppress, e.g. 'countOrphanedByDeviceUserId'"),
|
|
2681
|
+
reason: z.string().describe("Mandatory justification for suppression")
|
|
2682
|
+
})).optional().default([]).describe("Method-scoped suppressions with mandatory reasons.")
|
|
2683
|
+
}, async ({ work_item_id, skip_checks = [], suppressions = [] }) => {
|
|
2684
|
+
const inputChars = JSON.stringify({ work_item_id, skip_checks, suppressions }).length;
|
|
2680
2685
|
const wsRoot = getWorkspaceRoot();
|
|
2681
2686
|
const startTime = Date.now();
|
|
2682
2687
|
// ══════════════════════════════════════════════════════════════════════
|
|
@@ -2794,6 +2799,7 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2794
2799
|
// ══════════════════════════════════════════════════════════════════════
|
|
2795
2800
|
// ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
|
|
2796
2801
|
const p1 = [];
|
|
2802
|
+
const p1Suppressed = [];
|
|
2797
2803
|
if (!skip_checks.includes("tenant_isolation")) {
|
|
2798
2804
|
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
2799
2805
|
const queryAnnotationLines = [];
|
|
@@ -2804,23 +2810,85 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2804
2810
|
f.lines.forEach((line, idx) => {
|
|
2805
2811
|
const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
|
|
2806
2812
|
if (dm && !dm[1].toLowerCase().includes("tenant")) {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2813
|
+
let suppressionReason = "";
|
|
2814
|
+
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === dm[0] || s.method.endsWith(`.${dm[0]}`)));
|
|
2815
|
+
if (explicitSup)
|
|
2816
|
+
suppressionReason = explicitSup.reason;
|
|
2817
|
+
else {
|
|
2818
|
+
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
2819
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2820
|
+
if (match)
|
|
2821
|
+
suppressionReason = match[1];
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
if (suppressionReason) {
|
|
2825
|
+
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
|
|
2826
|
+
}
|
|
2827
|
+
else {
|
|
2828
|
+
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2829
|
+
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
2830
|
+
suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
|
|
2831
|
+
}
|
|
2810
2832
|
}
|
|
2811
2833
|
});
|
|
2812
2834
|
queryAnnotationLines.forEach(ai => {
|
|
2813
2835
|
const block = f.lines.slice(ai, ai + 5).join(" ");
|
|
2814
2836
|
if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2837
|
+
let suppressionReason = "";
|
|
2838
|
+
let queryMethodName = "";
|
|
2839
|
+
const methodDeclMatch = block.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
|
|
2840
|
+
if (methodDeclMatch)
|
|
2841
|
+
queryMethodName = methodDeclMatch[1];
|
|
2842
|
+
if (queryMethodName) {
|
|
2843
|
+
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
|
|
2844
|
+
if (explicitSup)
|
|
2845
|
+
suppressionReason = explicitSup.reason;
|
|
2846
|
+
}
|
|
2847
|
+
if (!suppressionReason) {
|
|
2848
|
+
for (let i = Math.max(0, ai - 5); i < ai; i++) {
|
|
2849
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2850
|
+
if (match)
|
|
2851
|
+
suppressionReason = match[1];
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (suppressionReason) {
|
|
2855
|
+
p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
|
|
2856
|
+
}
|
|
2857
|
+
else {
|
|
2858
|
+
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
|
|
2859
|
+
message: `@Query at line ${ai + 1} has no tenantId filter`,
|
|
2860
|
+
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
|
|
2861
|
+
}
|
|
2818
2862
|
}
|
|
2819
2863
|
});
|
|
2820
2864
|
}
|
|
2821
2865
|
}
|
|
2822
2866
|
mechanical.push(...p1);
|
|
2823
|
-
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
|
|
2867
|
+
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
|
|
2868
|
+
// Auto-log suppressions to decisions.json
|
|
2869
|
+
if (p1Suppressed.length > 0) {
|
|
2870
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
2871
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
2872
|
+
if (dir) {
|
|
2873
|
+
const decisionsPath = path.join(dir, "decisions.json");
|
|
2874
|
+
const decisions = readJson(decisionsPath) || [];
|
|
2875
|
+
let modified = false;
|
|
2876
|
+
for (const sup of p1Suppressed) {
|
|
2877
|
+
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
2878
|
+
if (!decisions.some(d => d.decision === desc)) {
|
|
2879
|
+
decisions.push({
|
|
2880
|
+
id: decisions.length + 1,
|
|
2881
|
+
decision: desc,
|
|
2882
|
+
rationale: sup.reason,
|
|
2883
|
+
logged_at: new Date().toISOString()
|
|
2884
|
+
});
|
|
2885
|
+
modified = true;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (modified)
|
|
2889
|
+
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2824
2892
|
// ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
|
|
2825
2893
|
const p2 = [];
|
|
2826
2894
|
if (!skip_checks.includes("n_plus_one")) {
|
|
@@ -3278,7 +3346,8 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
3278
3346
|
for (let i = 0; i < passResults.length; i++) {
|
|
3279
3347
|
const p = passResults[i];
|
|
3280
3348
|
const icon = p.skipped ? "⏭️ SKIP" : p.passed ? "✅ PASS" : "❌ FAIL";
|
|
3281
|
-
|
|
3349
|
+
const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
|
|
3350
|
+
out += `| P${i + 1} | ${p.name} | ${icon} | ${issueText} |\n`;
|
|
3282
3351
|
}
|
|
3283
3352
|
if (blockingMechanical.length > 0) {
|
|
3284
3353
|
out += `\n#### ❌ Blocking Mechanical Findings\n`;
|
|
@@ -3287,6 +3356,13 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
3287
3356
|
out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
|
|
3288
3357
|
}
|
|
3289
3358
|
}
|
|
3359
|
+
if (p1Suppressed.length > 0) {
|
|
3360
|
+
out += `\n#### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
|
|
3361
|
+
for (const sup of p1Suppressed) {
|
|
3362
|
+
out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
|
|
3363
|
+
out += `> Suppressed — ${sup.reason}\n`;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3290
3366
|
const warnings1 = mechanical.filter(f => f.severity === "WARNING");
|
|
3291
3367
|
if (warnings1.length > 0) {
|
|
3292
3368
|
out += `\n#### ⚠️ Warnings\n`;
|
|
@@ -3425,8 +3501,13 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3425
3501
|
work_item_id: z.string().describe("Active work item ID, e.g. 'WI-1234'."),
|
|
3426
3502
|
skip_checks: z.array(z.enum(["tenant_isolation", "n_plus_one", "missing_index", "kafka_sync", "early_return", "ai_review"])).optional().default([]).describe("Checks to skip. Options: tenant_isolation | n_plus_one | missing_index | kafka_sync | early_return | ai_review. " +
|
|
3427
3503
|
"Use sparingly — only for acknowledged legacy files that pre-date the rule."),
|
|
3428
|
-
|
|
3429
|
-
|
|
3504
|
+
suppressions: z.array(z.object({
|
|
3505
|
+
check: z.enum(["TENANT_ISOLATION", "N_PLUS_ONE", "MISSING_INDEX", "KAFKA_SYNC", "EARLY_RETURN"]),
|
|
3506
|
+
method: z.string().describe("Method name to suppress, e.g. 'countOrphanedByDeviceUserId'"),
|
|
3507
|
+
reason: z.string().describe("Mandatory justification for suppression")
|
|
3508
|
+
})).optional().default([]).describe("Method-scoped suppressions with mandatory reasons.")
|
|
3509
|
+
}, async ({ work_item_id, skip_checks = [], suppressions = [] }) => {
|
|
3510
|
+
const inputChars = JSON.stringify({ work_item_id, skip_checks, suppressions }).length;
|
|
3430
3511
|
const wsRoot = getWorkspaceRoot();
|
|
3431
3512
|
// ── Resolve task folder ────────────────────────────────────────────────
|
|
3432
3513
|
const registry = readRegistry();
|
|
@@ -3487,6 +3568,7 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3487
3568
|
// Rule: every Repository method must filter by tenantId
|
|
3488
3569
|
// ══════════════════════════════════════════════════════════════════════
|
|
3489
3570
|
const p1Findings = [];
|
|
3571
|
+
const p1Suppressed = [];
|
|
3490
3572
|
if (!skip_checks.includes("tenant_isolation")) {
|
|
3491
3573
|
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
3492
3574
|
// Find method signatures in repository interface/class
|
|
@@ -3502,14 +3584,30 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3502
3584
|
if (derivedMatch) {
|
|
3503
3585
|
const byClause = derivedMatch[1].toLowerCase();
|
|
3504
3586
|
if (!byClause.includes("tenantid") && !byClause.includes("tenant")) {
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3587
|
+
let suppressionReason = "";
|
|
3588
|
+
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === derivedMatch[0] || s.method.endsWith(`.${derivedMatch[0]}`)));
|
|
3589
|
+
if (explicitSup)
|
|
3590
|
+
suppressionReason = explicitSup.reason;
|
|
3591
|
+
else {
|
|
3592
|
+
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
3593
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
3594
|
+
if (match)
|
|
3595
|
+
suppressionReason = match[1];
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
if (suppressionReason) {
|
|
3599
|
+
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: derivedMatch[0] });
|
|
3600
|
+
}
|
|
3601
|
+
else {
|
|
3602
|
+
p1Findings.push({
|
|
3603
|
+
check: "TENANT_ISOLATION",
|
|
3604
|
+
severity: "BLOCKING",
|
|
3605
|
+
file: f.file_path,
|
|
3606
|
+
line: idx + 1,
|
|
3607
|
+
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
3608
|
+
suggestion: "Rename to include AndTenantId: e.g. findByIdAndTenantId(UUID id, UUID tenantId)",
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3513
3611
|
}
|
|
3514
3612
|
}
|
|
3515
3613
|
});
|
|
@@ -3518,20 +3616,66 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3518
3616
|
// Look at this line and next 3 lines for the query body
|
|
3519
3617
|
const queryBlock = f.lines.slice(annotIdx, annotIdx + 5).join(" ");
|
|
3520
3618
|
if (!/tenantId|tenant_id|:tenantId/i.test(queryBlock)) {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3619
|
+
let suppressionReason = "";
|
|
3620
|
+
let queryMethodName = "";
|
|
3621
|
+
const methodDeclMatch = queryBlock.match(/(?:List|Optional|Page|long|int|boolean|void|[A-Z]\w+)\s+(\w+)\s*\(/);
|
|
3622
|
+
if (methodDeclMatch)
|
|
3623
|
+
queryMethodName = methodDeclMatch[1];
|
|
3624
|
+
if (queryMethodName) {
|
|
3625
|
+
const explicitSup = suppressions.find(s => s.check === "TENANT_ISOLATION" && (s.method === queryMethodName || s.method.endsWith(`.${queryMethodName}`)));
|
|
3626
|
+
if (explicitSup)
|
|
3627
|
+
suppressionReason = explicitSup.reason;
|
|
3628
|
+
}
|
|
3629
|
+
if (!suppressionReason) {
|
|
3630
|
+
for (let i = Math.max(0, annotIdx - 5); i < annotIdx; i++) {
|
|
3631
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
3632
|
+
if (match)
|
|
3633
|
+
suppressionReason = match[1];
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
if (suppressionReason) {
|
|
3637
|
+
p1Suppressed.push({ file: f.file_path, line: annotIdx + 1, reason: suppressionReason, method: queryMethodName || "@Query method" });
|
|
3638
|
+
}
|
|
3639
|
+
else {
|
|
3640
|
+
p1Findings.push({
|
|
3641
|
+
check: "TENANT_ISOLATION",
|
|
3642
|
+
severity: "BLOCKING",
|
|
3643
|
+
file: f.file_path,
|
|
3644
|
+
line: annotIdx + 1,
|
|
3645
|
+
message: `@Query at line ${annotIdx + 1} does not include tenantId filter`,
|
|
3646
|
+
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query",
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3529
3649
|
}
|
|
3530
3650
|
});
|
|
3531
3651
|
}
|
|
3532
3652
|
}
|
|
3533
3653
|
allFindings.push(...p1Findings);
|
|
3534
|
-
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length });
|
|
3654
|
+
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length, suppressedCount: p1Suppressed.length });
|
|
3655
|
+
// Auto-log suppressions to decisions.json
|
|
3656
|
+
if (p1Suppressed.length > 0 && work_item_id) {
|
|
3657
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
3658
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
3659
|
+
if (dir) {
|
|
3660
|
+
const decisionsPath = path.join(dir, "decisions.json");
|
|
3661
|
+
const decisions = readJson(decisionsPath) || [];
|
|
3662
|
+
let modified = false;
|
|
3663
|
+
for (const sup of p1Suppressed) {
|
|
3664
|
+
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
3665
|
+
if (!decisions.some(d => d.decision === desc)) {
|
|
3666
|
+
decisions.push({
|
|
3667
|
+
id: decisions.length + 1,
|
|
3668
|
+
decision: desc,
|
|
3669
|
+
rationale: sup.reason,
|
|
3670
|
+
logged_at: new Date().toISOString()
|
|
3671
|
+
});
|
|
3672
|
+
modified = true;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
if (modified)
|
|
3676
|
+
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3535
3679
|
// ══════════════════════════════════════════════════════════════════════
|
|
3536
3680
|
// PASS 2 — N_PLUS_ONE
|
|
3537
3681
|
// Rule: no repository method calls inside for/forEach loops
|
|
@@ -3958,7 +4102,8 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3958
4102
|
out += `| Pass | Check | Status | Findings |\n|---|---|---|---|\n`;
|
|
3959
4103
|
for (const p of passResults) {
|
|
3960
4104
|
const statusIcon = p.skipped ? "⏭️ SKIPPED" : p.passed ? "✅ PASS" : "❌ FAIL";
|
|
3961
|
-
|
|
4105
|
+
const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
|
|
4106
|
+
out += `| P${passResults.indexOf(p) + 1} | ${p.name} | ${statusIcon} | ${issueText} |\n`;
|
|
3962
4107
|
}
|
|
3963
4108
|
// ── Blocking findings detail ──────────────────────────────────────────
|
|
3964
4109
|
if (blockingFindings.length > 0) {
|
|
@@ -3969,6 +4114,13 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3969
4114
|
out += `> 💡 *${f.suggestion}*\n`;
|
|
3970
4115
|
}
|
|
3971
4116
|
}
|
|
4117
|
+
if (p1Suppressed.length > 0) {
|
|
4118
|
+
out += `\n### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
|
|
4119
|
+
for (const sup of p1Suppressed) {
|
|
4120
|
+
out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
|
|
4121
|
+
out += `> Suppressed — ${sup.reason}\n`;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
3972
4124
|
// ── Warning findings detail ───────────────────────────────────────────
|
|
3973
4125
|
const warnings = allFindings.filter(f => f.severity === "WARNING");
|
|
3974
4126
|
if (warnings.length > 0) {
|