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.
Files changed (2) hide show
  1. package/index.js +182 -30
  2. 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
- }, async ({ work_item_id, skip_checks = [] }) => {
2679
- const inputChars = JSON.stringify({ work_item_id, skip_checks }).length;
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
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2808
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
2809
- suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
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
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
2816
- message: `@Query at line ${ai + 1} has no tenantId filter`,
2817
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
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
- out += `| P${i + 1} | ${p.name} | ${icon} | ${p.skipped ? "—" : p.count} |\n`;
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
- }, async ({ work_item_id, skip_checks = [] }) => {
3429
- const inputChars = JSON.stringify({ work_item_id, skip_checks }).length;
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
- p1Findings.push({
3506
- check: "TENANT_ISOLATION",
3507
- severity: "BLOCKING",
3508
- file: f.file_path,
3509
- line: idx + 1,
3510
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
3511
- suggestion: "Rename to include AndTenantId: e.g. findByIdAndTenantId(UUID id, UUID tenantId)",
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
- p1Findings.push({
3522
- check: "TENANT_ISOLATION",
3523
- severity: "BLOCKING",
3524
- file: f.file_path,
3525
- line: annotIdx + 1,
3526
- message: `@Query at line ${annotIdx + 1} does not include tenantId filter`,
3527
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query",
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
- out += `| P${passResults.indexOf(p) + 1} | ${p.name} | ${statusIcon} | ${p.skipped ? "—" : p.count} |\n`;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.60",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",