secufusion-mcp 1.0.59 → 1.1.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 +163 -29
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -511,8 +511,8 @@ function resolveTaskFolder(workItemId, tasksRoot, registryData) {
|
|
|
511
511
|
return null;
|
|
512
512
|
}
|
|
513
513
|
function generateNewTaskFolder(workItemId, title, tasksRoot) {
|
|
514
|
-
// Strip potential leading ID prefix
|
|
515
|
-
const strippedTitle = title.replace(/^[a-zA-Z]
|
|
514
|
+
// Strip potential leading ID prefix from the title (e.g. "BUG-1173: " or "1173: ")
|
|
515
|
+
const strippedTitle = title.replace(/^[a-zA-Z]*[-]?\d+[:\s]*/i, '');
|
|
516
516
|
const slug = strippedTitle.toLowerCase()
|
|
517
517
|
.replace(/[:\s]+/g, '-')
|
|
518
518
|
.replace(/[^a-z0-9-]+/g, '')
|
|
@@ -520,7 +520,31 @@ function generateNewTaskFolder(workItemId, title, tasksRoot) {
|
|
|
520
520
|
.replace(/^-|-$/g, '')
|
|
521
521
|
.slice(0, 50)
|
|
522
522
|
.replace(/-+$/, '');
|
|
523
|
-
|
|
523
|
+
// Determine standard prefix (e.g., bug-1173, story-1097)
|
|
524
|
+
let numericId = workItemId.replace(/\D/g, ''); // Extract just the digits
|
|
525
|
+
if (!numericId)
|
|
526
|
+
numericId = workItemId; // fallback
|
|
527
|
+
let taskTypeStr = "task";
|
|
528
|
+
// Try to load the classification to get the true task_type
|
|
529
|
+
const classifPath = path.join(tasksRoot, '..', 'classifications', `${workItemId}.json`);
|
|
530
|
+
const classif = readJson(classifPath);
|
|
531
|
+
if (classif && typeof classif.task_type === 'string') {
|
|
532
|
+
const rawType = classif.task_type.toLowerCase();
|
|
533
|
+
if (rawType === 'user_story')
|
|
534
|
+
taskTypeStr = 'story';
|
|
535
|
+
else
|
|
536
|
+
taskTypeStr = rawType;
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
// Fallback: guess from the workItemId string itself if passed as "BUG-1173"
|
|
540
|
+
const prefixMatch = workItemId.match(/^([a-zA-Z]+)-/);
|
|
541
|
+
if (prefixMatch) {
|
|
542
|
+
taskTypeStr = prefixMatch[1].toLowerCase();
|
|
543
|
+
if (taskTypeStr === 'wi')
|
|
544
|
+
taskTypeStr = 'task'; // Generic WI to task
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return path.join(tasksRoot, `${taskTypeStr}-${numericId}-${slug}`);
|
|
524
548
|
}
|
|
525
549
|
function readJson(filePath) {
|
|
526
550
|
const raw = readFileSafe(filePath);
|
|
@@ -2770,6 +2794,7 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2770
2794
|
// ══════════════════════════════════════════════════════════════════════
|
|
2771
2795
|
// ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
|
|
2772
2796
|
const p1 = [];
|
|
2797
|
+
const p1Suppressed = [];
|
|
2773
2798
|
if (!skip_checks.includes("tenant_isolation")) {
|
|
2774
2799
|
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
2775
2800
|
const queryAnnotationLines = [];
|
|
@@ -2780,23 +2805,69 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
2780
2805
|
f.lines.forEach((line, idx) => {
|
|
2781
2806
|
const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
|
|
2782
2807
|
if (dm && !dm[1].toLowerCase().includes("tenant")) {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2808
|
+
let suppressionReason = "";
|
|
2809
|
+
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
2810
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2811
|
+
if (match)
|
|
2812
|
+
suppressionReason = match[1];
|
|
2813
|
+
}
|
|
2814
|
+
if (suppressionReason) {
|
|
2815
|
+
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
|
|
2816
|
+
}
|
|
2817
|
+
else {
|
|
2818
|
+
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
|
|
2819
|
+
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
2820
|
+
suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
|
|
2821
|
+
}
|
|
2786
2822
|
}
|
|
2787
2823
|
});
|
|
2788
2824
|
queryAnnotationLines.forEach(ai => {
|
|
2789
2825
|
const block = f.lines.slice(ai, ai + 5).join(" ");
|
|
2790
2826
|
if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2827
|
+
let suppressionReason = "";
|
|
2828
|
+
for (let i = Math.max(0, ai - 5); i < ai; i++) {
|
|
2829
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
2830
|
+
if (match)
|
|
2831
|
+
suppressionReason = match[1];
|
|
2832
|
+
}
|
|
2833
|
+
if (suppressionReason) {
|
|
2834
|
+
p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: "@Query method" });
|
|
2835
|
+
}
|
|
2836
|
+
else {
|
|
2837
|
+
p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
|
|
2838
|
+
message: `@Query at line ${ai + 1} has no tenantId filter`,
|
|
2839
|
+
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
|
|
2840
|
+
}
|
|
2794
2841
|
}
|
|
2795
2842
|
});
|
|
2796
2843
|
}
|
|
2797
2844
|
}
|
|
2798
2845
|
mechanical.push(...p1);
|
|
2799
|
-
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
|
|
2846
|
+
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
|
|
2847
|
+
// Auto-log suppressions to decisions.json
|
|
2848
|
+
if (p1Suppressed.length > 0) {
|
|
2849
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
2850
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
2851
|
+
if (dir) {
|
|
2852
|
+
const decisionsPath = path.join(dir, "decisions.json");
|
|
2853
|
+
const decisions = readJson(decisionsPath) || [];
|
|
2854
|
+
let modified = false;
|
|
2855
|
+
for (const sup of p1Suppressed) {
|
|
2856
|
+
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
2857
|
+
if (!decisions.some(d => d.decision === desc)) {
|
|
2858
|
+
decisions.push({
|
|
2859
|
+
id: decisions.length + 1,
|
|
2860
|
+
decision: desc,
|
|
2861
|
+
rationale: sup.reason,
|
|
2862
|
+
logged_at: new Date().toISOString()
|
|
2863
|
+
});
|
|
2864
|
+
modified = true;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (modified)
|
|
2868
|
+
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2800
2871
|
// ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
|
|
2801
2872
|
const p2 = [];
|
|
2802
2873
|
if (!skip_checks.includes("n_plus_one")) {
|
|
@@ -3254,7 +3325,8 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
3254
3325
|
for (let i = 0; i < passResults.length; i++) {
|
|
3255
3326
|
const p = passResults[i];
|
|
3256
3327
|
const icon = p.skipped ? "⏭️ SKIP" : p.passed ? "✅ PASS" : "❌ FAIL";
|
|
3257
|
-
|
|
3328
|
+
const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
|
|
3329
|
+
out += `| P${i + 1} | ${p.name} | ${icon} | ${issueText} |\n`;
|
|
3258
3330
|
}
|
|
3259
3331
|
if (blockingMechanical.length > 0) {
|
|
3260
3332
|
out += `\n#### ❌ Blocking Mechanical Findings\n`;
|
|
@@ -3263,6 +3335,13 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
|
|
|
3263
3335
|
out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
|
|
3264
3336
|
}
|
|
3265
3337
|
}
|
|
3338
|
+
if (p1Suppressed.length > 0) {
|
|
3339
|
+
out += `\n#### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
|
|
3340
|
+
for (const sup of p1Suppressed) {
|
|
3341
|
+
out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
|
|
3342
|
+
out += `> Suppressed — ${sup.reason}\n`;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3266
3345
|
const warnings1 = mechanical.filter(f => f.severity === "WARNING");
|
|
3267
3346
|
if (warnings1.length > 0) {
|
|
3268
3347
|
out += `\n#### ⚠️ Warnings\n`;
|
|
@@ -3463,6 +3542,7 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3463
3542
|
// Rule: every Repository method must filter by tenantId
|
|
3464
3543
|
// ══════════════════════════════════════════════════════════════════════
|
|
3465
3544
|
const p1Findings = [];
|
|
3545
|
+
const p1Suppressed = [];
|
|
3466
3546
|
if (!skip_checks.includes("tenant_isolation")) {
|
|
3467
3547
|
for (const f of fileRecords.filter(f => f.isRepo)) {
|
|
3468
3548
|
// Find method signatures in repository interface/class
|
|
@@ -3478,14 +3558,25 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3478
3558
|
if (derivedMatch) {
|
|
3479
3559
|
const byClause = derivedMatch[1].toLowerCase();
|
|
3480
3560
|
if (!byClause.includes("tenantid") && !byClause.includes("tenant")) {
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3561
|
+
let suppressionReason = "";
|
|
3562
|
+
for (let i = Math.max(0, idx - 5); i < idx; i++) {
|
|
3563
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
3564
|
+
if (match)
|
|
3565
|
+
suppressionReason = match[1];
|
|
3566
|
+
}
|
|
3567
|
+
if (suppressionReason) {
|
|
3568
|
+
p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: derivedMatch[0] });
|
|
3569
|
+
}
|
|
3570
|
+
else {
|
|
3571
|
+
p1Findings.push({
|
|
3572
|
+
check: "TENANT_ISOLATION",
|
|
3573
|
+
severity: "BLOCKING",
|
|
3574
|
+
file: f.file_path,
|
|
3575
|
+
line: idx + 1,
|
|
3576
|
+
message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
|
|
3577
|
+
suggestion: "Rename to include AndTenantId: e.g. findByIdAndTenantId(UUID id, UUID tenantId)",
|
|
3578
|
+
});
|
|
3579
|
+
}
|
|
3489
3580
|
}
|
|
3490
3581
|
}
|
|
3491
3582
|
});
|
|
@@ -3494,20 +3585,55 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3494
3585
|
// Look at this line and next 3 lines for the query body
|
|
3495
3586
|
const queryBlock = f.lines.slice(annotIdx, annotIdx + 5).join(" ");
|
|
3496
3587
|
if (!/tenantId|tenant_id|:tenantId/i.test(queryBlock)) {
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3588
|
+
let suppressionReason = "";
|
|
3589
|
+
for (let i = Math.max(0, annotIdx - 5); i < annotIdx; i++) {
|
|
3590
|
+
const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
|
|
3591
|
+
if (match)
|
|
3592
|
+
suppressionReason = match[1];
|
|
3593
|
+
}
|
|
3594
|
+
if (suppressionReason) {
|
|
3595
|
+
p1Suppressed.push({ file: f.file_path, line: annotIdx + 1, reason: suppressionReason, method: "@Query method" });
|
|
3596
|
+
}
|
|
3597
|
+
else {
|
|
3598
|
+
p1Findings.push({
|
|
3599
|
+
check: "TENANT_ISOLATION",
|
|
3600
|
+
severity: "BLOCKING",
|
|
3601
|
+
file: f.file_path,
|
|
3602
|
+
line: annotIdx + 1,
|
|
3603
|
+
message: `@Query at line ${annotIdx + 1} does not include tenantId filter`,
|
|
3604
|
+
suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query",
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3505
3607
|
}
|
|
3506
3608
|
});
|
|
3507
3609
|
}
|
|
3508
3610
|
}
|
|
3509
3611
|
allFindings.push(...p1Findings);
|
|
3510
|
-
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length });
|
|
3612
|
+
passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length, suppressedCount: p1Suppressed.length });
|
|
3613
|
+
// Auto-log suppressions to decisions.json
|
|
3614
|
+
if (p1Suppressed.length > 0 && work_item_id) {
|
|
3615
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
3616
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
3617
|
+
if (dir) {
|
|
3618
|
+
const decisionsPath = path.join(dir, "decisions.json");
|
|
3619
|
+
const decisions = readJson(decisionsPath) || [];
|
|
3620
|
+
let modified = false;
|
|
3621
|
+
for (const sup of p1Suppressed) {
|
|
3622
|
+
const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
|
|
3623
|
+
if (!decisions.some(d => d.decision === desc)) {
|
|
3624
|
+
decisions.push({
|
|
3625
|
+
id: decisions.length + 1,
|
|
3626
|
+
decision: desc,
|
|
3627
|
+
rationale: sup.reason,
|
|
3628
|
+
logged_at: new Date().toISOString()
|
|
3629
|
+
});
|
|
3630
|
+
modified = true;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
if (modified)
|
|
3634
|
+
writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3511
3637
|
// ══════════════════════════════════════════════════════════════════════
|
|
3512
3638
|
// PASS 2 — N_PLUS_ONE
|
|
3513
3639
|
// Rule: no repository method calls inside for/forEach loops
|
|
@@ -3934,7 +4060,8 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3934
4060
|
out += `| Pass | Check | Status | Findings |\n|---|---|---|---|\n`;
|
|
3935
4061
|
for (const p of passResults) {
|
|
3936
4062
|
const statusIcon = p.skipped ? "⏭️ SKIPPED" : p.passed ? "✅ PASS" : "❌ FAIL";
|
|
3937
|
-
|
|
4063
|
+
const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
|
|
4064
|
+
out += `| P${passResults.indexOf(p) + 1} | ${p.name} | ${statusIcon} | ${issueText} |\n`;
|
|
3938
4065
|
}
|
|
3939
4066
|
// ── Blocking findings detail ──────────────────────────────────────────
|
|
3940
4067
|
if (blockingFindings.length > 0) {
|
|
@@ -3945,6 +4072,13 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
|
|
|
3945
4072
|
out += `> 💡 *${f.suggestion}*\n`;
|
|
3946
4073
|
}
|
|
3947
4074
|
}
|
|
4075
|
+
if (p1Suppressed.length > 0) {
|
|
4076
|
+
out += `\n### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
|
|
4077
|
+
for (const sup of p1Suppressed) {
|
|
4078
|
+
out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
|
|
4079
|
+
out += `> Suppressed — ${sup.reason}\n`;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
3948
4082
|
// ── Warning findings detail ───────────────────────────────────────────
|
|
3949
4083
|
const warnings = allFindings.filter(f => f.severity === "WARNING");
|
|
3950
4084
|
if (warnings.length > 0) {
|