secufusion-mcp 1.2.3 → 1.2.4

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 (4) hide show
  1. package/AGENTS.md +11 -823
  2. package/README.md +12 -0
  3. package/index.js +6 -573
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -38,6 +38,18 @@ The SecuFusion MCP has been refactored to align with the advanced MLMCPS framewo
38
38
  2. **Subprocess PR Checks**: Tier 1 mechanical checks (`TENANT_ISOLATION`, `N_PLUS_ONE`, etc.) are now extracted into a standalone CLI script (`scripts/sfn-pr-check.js`), allowing them to be run by the AI *or* natively within your CI/CD pipelines.
39
39
  3. **Thin Index Token Discipline**: Context bloat is gone. Tools like `manage_task(read)` and `prime_session` now return lightweight "Thin Indexes"—compact Markdown summaries with absolute file paths—so the AI only reads the full JSON via `view_file` when truly necessary.
40
40
  4. **Dynamic Skill Registry**: A new `skill_recommend` tool allows the AI to dynamically discover domain-specific architectural skills without bloating the base prompt.
41
+ 5. **Poly-Repo DNA Discovery**: Through the standalone `secufusion-dna-plugin`, the AI can dynamically analyze the entire codebase of any poly-repo project at session start. It maps out microservices, API contracts, and event topologies on the fly, creating a living knowledge graph.
42
+
43
+ ---
44
+
45
+ ## 🧬 SecuFusion DNA Discovery Plugin
46
+
47
+ A new standalone Claude plugin has been introduced: **SecuFusion DNA Discovery MCP** (`secufusion-dna-plugin`). This plugin empowers agents to autonomously discover, analyze, and build a living knowledge graph of your entire software ecosystem when handed over to a new team member.
48
+
49
+ **Key Features:**
50
+ - **Dynamic Stack Analysis:** Identifies Java/Spring, Node, Docker, and other frameworks on the fly.
51
+ - **Slash Command Agents:** Use `/analyze`, `/map-architecture`, and `/blast-radius` directly in your AI chat to orchestrate complex codebase queries.
52
+ - **Automated Dependency Mapping:** Generates cross-service dependency maps and evaluates the blast radius of potential changes.
41
53
 
42
54
  ---
43
55
 
package/index.js CHANGED
@@ -2706,583 +2706,16 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2706
2706
  reason: z.string().describe("Mandatory justification for suppression")
2707
2707
  })).optional().default([]).describe("Method-scoped suppressions with mandatory reasons.")
2708
2708
  }, async ({ work_item_id, skip_checks = [], suppressions = [] }) => {
2709
- const inputChars = JSON.stringify({ work_item_id, skip_checks, suppressions }).length;
2710
- const wsRoot = getWorkspaceRoot();
2711
- const startTime = Date.now();
2712
- // ══════════════════════════════════════════════════════════════════════
2713
- // SETUP — resolve task folder and read all task brain files once
2714
- // ══════════════════════════════════════════════════════════════════════
2715
- // ── Resolve task folder ────────────────────────────────────────────────
2716
- const registry = readRegistry();
2717
- const taskEntry = registry.find(r => r["work_item_id"] === work_item_id);
2718
- let taskDir = null;
2719
- if (taskEntry?.["folder_name"]) {
2720
- taskDir = path.resolve(wsRoot, `.secufusion/tasks/${taskEntry["folder_name"]}`);
2721
- }
2722
- if (!taskDir || !fs.existsSync(taskDir)) {
2723
- const tasksBase = path.resolve(wsRoot, ".secufusion/tasks");
2724
- if (fs.existsSync(tasksBase)) {
2725
- const folders = fs.readdirSync(tasksBase);
2726
- const match = folders.find(f => f.startsWith(work_item_id));
2727
- if (match)
2728
- taskDir = path.join(tasksBase, match);
2729
- }
2730
- }
2731
- if (!taskDir || !fs.existsSync(taskDir)) {
2732
- return appendTelemetry({
2733
- isError: true,
2734
- content: [{ type: "text", text: `❌ Task folder not found for ${work_item_id}. Call manage_task(initialize) first.` }]
2735
- }, inputChars);
2736
- }
2737
- const taskSpec = (() => { try {
2738
- return JSON.parse(readFileSafe(path.join(taskDir, "spec.json")) || "{}");
2739
- }
2740
- catch {
2741
- return {};
2742
- } })();
2743
- const taskProgress = (() => { try {
2744
- return JSON.parse(readFileSafe(path.join(taskDir, "progress.json")) || "{}");
2745
- }
2746
- catch {
2747
- return {};
2748
- } })();
2749
- const decisions = (() => { try {
2750
- return JSON.parse(readFileSafe(path.join(taskDir, "decisions.json")) || "[]");
2751
- }
2752
- catch {
2753
- return [];
2754
- } })();
2755
- const filesTouched = (() => { try {
2756
- return JSON.parse(readFileSafe(path.join(taskDir, "files-touched.json")) || "[]");
2757
- }
2758
- catch {
2759
- return [];
2760
- } })();
2761
- const scenarios = (() => { try {
2762
- return JSON.parse(readFileSafe(path.join(taskDir, "scenarios.json")) || "[]");
2763
- }
2764
- catch {
2765
- return [];
2766
- } })();
2767
- if (filesTouched.length === 0) {
2768
- return appendTelemetry({
2769
- content: [{ type: "text", text: `⚠️ No files logged for ${work_item_id}. Log files with manage_task(log_file_touched) before running checks.` }]
2770
- }, inputChars);
2771
- }
2772
- // ── Load project spec (golden rules + rejected patterns) ───────────────
2773
- const PROJ_SPEC_FILE = ".secufusion-project-spec.json";
2774
- const specCandidates = [
2775
- path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), PROJ_SPEC_FILE),
2776
- path.resolve(wsRoot, PROJ_SPEC_FILE),
2777
- ];
2778
- let projectSpec = null;
2779
- for (const c of specCandidates) {
2780
- if (fs.existsSync(c)) {
2781
- try {
2782
- projectSpec = JSON.parse(readFileSafe(c) || "null");
2783
- }
2784
- catch { }
2785
- break;
2786
- }
2787
- }
2788
- const goldenRules = projectSpec?.golden_rules || {};
2789
- const codingStyle = goldenRules.coding_style || {};
2790
- const authorityRules = goldenRules.authority_rules || [];
2791
- let rejectedPatterns = [];
2792
2709
  try {
2793
- rejectedPatterns = JSON.parse(readFileSafe(resolve(".rejected-patterns.json")) || "[]");
2794
- }
2795
- catch { }
2796
- const fileRecords = filesTouched.map(ft => {
2797
- const abs = path.resolve(wsRoot, ft.file_path);
2798
- const raw = readFileSafe(abs) || "";
2799
- const ext = path.extname(ft.file_path).toLowerCase();
2800
- const base = path.basename(ft.file_path);
2801
- return {
2802
- file_path: ft.file_path,
2803
- change_summary: ft.change_summary,
2804
- content: raw,
2805
- lines: raw.split("\n"),
2806
- ext,
2807
- isJava: ext === ".java",
2808
- isTS: ext === ".ts" || ext === ".tsx",
2809
- isRepo: /Repository/i.test(base) && ext === ".java",
2810
- isKafka: raw.includes("@KafkaListener"),
2811
- isMigration: ft.file_path.includes("migration") || ft.file_path.includes("flyway") || ext === ".sql",
2812
- isController: /Controller/i.test(base) && ext === ".java",
2813
- isService: /Service/i.test(base) && ext === ".java",
2814
- isTest: /test|spec/i.test(ft.file_path),
2815
- exists: fs.existsSync(abs),
2816
- };
2817
- });
2818
- let mechanical = [];
2819
- const passResults = [];
2820
- const reviewComments = [];
2821
- let aiCost = 0;
2822
- // ══════════════════════════════════════════════════════════════════════
2823
- // TIER 1 — MECHANICAL CHECKS (P1–P5) Via Subprocess
2824
- // ══════════════════════════════════════════════════════════════════════
2825
- let tier1Passed = false;
2826
- try {
2827
- const scriptPath = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "scripts/sfn-pr-check.js");
2828
- const execResult = require('child_process').execFileSync('node', [scriptPath, '--task', work_item_id, '--workspace', wsRoot], { encoding: 'utf-8' });
2829
- const jsonStr = execResult.trim().split('\n').pop() || "{}";
2830
- const parsed = JSON.parse(jsonStr);
2831
- mechanical = parsed.mechanical || [];
2832
- tier1Passed = parsed.pass === true;
2710
+ const cp = require('child_process');
2711
+ const scriptPath = require('path').resolve(__dirname, 'scripts/sfn-pr-check.js');
2712
+ const args = JSON.stringify({ work_item_id, skip_checks, suppressions });
2713
+ const output = cp.execFileSync(process.execPath, [scriptPath, args], { encoding: 'utf-8' });
2714
+ return { content: [{ type: 'text', text: output }] };
2833
2715
  }
2834
2716
  catch (e) {
2835
- if (e.stdout) {
2836
- const jsonStr = e.stdout.trim().split('\n').pop() || "{}";
2837
- try {
2838
- const parsed = JSON.parse(jsonStr);
2839
- mechanical = parsed.mechanical || [];
2840
- }
2841
- catch (e2) { }
2842
- }
2843
- tier1Passed = false;
2844
- }
2845
- const p1 = mechanical.filter(m => m.check === "TENANT_ISOLATION");
2846
- passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
2847
- const p2 = mechanical.filter(m => m.check === "N_PLUS_ONE");
2848
- passResults.push({ name: "N_PLUS_ONE", skipped: skip_checks.includes("n_plus_one"), passed: p2.length === 0, count: p2.length });
2849
- const p3 = mechanical.filter(m => m.check === "MISSING_INDEX");
2850
- passResults.push({ name: "MISSING_INDEX", skipped: skip_checks.includes("missing_index"), passed: p3.length === 0, count: p3.length });
2851
- const p4 = mechanical.filter(m => m.check === "KAFKA_SYNC");
2852
- passResults.push({ name: "KAFKA_SYNC", skipped: skip_checks.includes("kafka_sync"), passed: p4.length === 0, count: p4.length });
2853
- const p5 = mechanical.filter(m => m.check === "EARLY_RETURN");
2854
- passResults.push({ name: "EARLY_RETURN", skipped: skip_checks.includes("early_return"), passed: p5.length === 0, count: p5.length });
2855
- const blockingMechanical = mechanical.filter(f => f.severity === "BLOCKING");
2856
- // ══════════════════════════════════════════════════════════════════════
2857
- // TIER 2 — AI FILE REVIEWER (Pass 6)
2858
- // Reads each changed file, applies architectural heuristics.
2859
- // Only runs if Tier 1 passes.
2860
- // ══════════════════════════════════════════════════════════════════════
2861
- const AI_COST_PER_100 = 0.0008;
2862
- if (tier1Passed && !skip_checks.includes("ai_review")) {
2863
- for (const f of fileRecords) {
2864
- aiCost += (f.lines.length / 100) * AI_COST_PER_100;
2865
- // Obvious comments
2866
- const obviousComments = f.lines.filter(l => /\/\/\s*(get the|set the|loop|return|initialize|this method|this function|create a|update the)/i.test(l));
2867
- if (obviousComments.length > 0) {
2868
- reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "CODE_QUALITY",
2869
- title: `${obviousComments.length} obvious comment(s)`,
2870
- detail: "Comments explain WHY, not WHAT. Remove or replace with architectural rationale.",
2871
- action: "Replace // Get the user with // Required because tenant context is not available in JWT claims" });
2872
- }
2873
- // Work Item IDs in comments
2874
- const ticketIdComments = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /(\/\/|\/\*|\*)\s*.*(?:BUG|WI)-\d+/i.test(l));
2875
- if (ticketIdComments.length > 0) {
2876
- reviewComments.push({ tier: 2, file: f.file_path, line: ticketIdComments[0].i + 1, severity: "CRITICAL", area: "CODE_QUALITY",
2877
- title: "Ticket ID in code comment",
2878
- detail: `Line ${ticketIdComments[0].i + 1} contains a work item ID: \`${ticketIdComments[0].l.trim()}\``,
2879
- action: "Remove the ticket ID. Explain the durable WHY instead." });
2880
- }
2881
- // Hardcoded URLs/IPs
2882
- const hardcoded = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /https?:\/\/[0-9]{1,3}\.[0-9]{1,3}|localhost:[0-9]{4}|uat\.|staging\.|prod\./i.test(l) && !/\/\//.test(l));
2883
- if (hardcoded.length > 0) {
2884
- reviewComments.push({ tier: 2, file: f.file_path, line: hardcoded[0].i + 1, severity: "CRITICAL", area: "SECURITY",
2885
- title: "Hardcoded URL/IP detected",
2886
- detail: `Line ${hardcoded[0].i + 1}: \`${hardcoded[0].l.trim()}\``,
2887
- action: "Move to application.properties / environment variables immediately" });
2888
- }
2889
- // Missing @Transactional(readOnly=true) on service GET methods
2890
- if (f.isService && !f.isTest) {
2891
- const getters = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /public.*(?:List|Page|Optional|DTO|Response)\s+\w+\s*\(/.test(l));
2892
- for (const { l, i } of getters) {
2893
- const prev = f.lines.slice(Math.max(0, i - 3), i).join(" ");
2894
- if (!/readOnly.*true/.test(prev)) {
2895
- reviewComments.push({ tier: 2, file: f.file_path, line: i + 1, severity: "WARNING", area: "PERFORMANCE",
2896
- title: "Missing @Transactional(readOnly = true)",
2897
- detail: `\`${(l.match(/\w+\s*\(/) || [""])[0].trim()}\` returns data without readOnly — allows dirty reads and skips DB optimizations`,
2898
- action: "Add @Transactional(readOnly = true) above this method" });
2899
- break;
2900
- }
2901
- }
2902
- }
2903
- // Missing @PreAuthorize on controller endpoints
2904
- if (f.isController && !f.isTest) {
2905
- const endpoints = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)/.test(l));
2906
- for (const { l, i } of endpoints) {
2907
- const prev = f.lines.slice(Math.max(0, i - 5), i).join(" ");
2908
- if (!/PreAuthorize|permitAll/.test(prev)) {
2909
- reviewComments.push({ tier: 2, file: f.file_path, line: i + 1, severity: "CRITICAL", area: "SECURITY",
2910
- title: "Endpoint missing @PreAuthorize",
2911
- detail: `Endpoint at line ${i + 1} has no @PreAuthorize annotation`,
2912
- action: "Add @PreAuthorize(\"hasAuthority('domain.action')\") above the mapping" });
2913
- }
2914
- }
2915
- }
2916
- // Debug statements in production code
2917
- if (!f.isTest) {
2918
- const debug = f.lines.map((l, i) => ({ l, i })).filter(({ l }) => /console\.log|System\.out\.print|e\.printStackTrace/.test(l));
2919
- if (debug.length > 0) {
2920
- reviewComments.push({ tier: 2, file: f.file_path, line: debug[0].i + 1, severity: "WARNING", area: "CODE_QUALITY",
2921
- title: "Debug statement in production code",
2922
- detail: `\`${debug[0].l.trim()}\` at line ${debug[0].i + 1}`,
2923
- action: "Replace with SLF4J log.debug() or remove entirely" });
2924
- }
2925
- }
2926
- // Exception swallowing
2927
- if (f.isJava) {
2928
- f.lines.forEach((line, idx) => {
2929
- if (/catch\s*\(\s*Exception\s+/.test(line)) {
2930
- const block = f.lines.slice(idx, idx + 5).join(" ");
2931
- if (!/log\.|logger\.|throw/.test(block)) {
2932
- reviewComments.push({ tier: 2, file: f.file_path, line: idx + 1, severity: "WARNING", area: "ERROR_HANDLING",
2933
- title: "Catch-all exception may be swallowed",
2934
- detail: `catch(Exception) at line ${idx + 1} has no log or rethrow`,
2935
- action: "Log with context: log.error(\"[WI-xxx] failed: {}\", e.getMessage(), e) or rethrow as domain exception" });
2936
- }
2937
- }
2938
- });
2939
- }
2940
- // TypeScript any overuse
2941
- if (f.isTS && !f.isTest) {
2942
- const anyLines = f.lines.filter(l => /:\s*any\b/.test(l) && !/\/\//.test(l));
2943
- if (anyLines.length > 2) {
2944
- reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "TYPE_SAFETY",
2945
- title: `${anyLines.length} uses of \`: any\``,
2946
- detail: "Type safety defeated. `any` propagates silently through the call chain.",
2947
- action: "Replace with explicit interfaces or `unknown` with a type guard" });
2948
- }
2949
- }
2950
- // Missing Flyway for @Entity
2951
- if (f.isJava && /@Entity/.test(f.content) && !f.isTest && !fileRecords.some(r => r.isMigration)) {
2952
- reviewComments.push({ tier: 2, file: f.file_path, severity: "CRITICAL", area: "DATABASE",
2953
- title: "@Entity change with no Flyway migration in PR",
2954
- detail: "Every JPA entity schema change requires a Flyway migration",
2955
- action: "Create src/main/resources/db/migration/Vxxx__describe_change.sql and log it with manage_task(log_file_touched)" });
2956
- }
2957
- // Cross-service call timeout
2958
- if (f.isJava && /RestTemplate|WebClient|FeignClient/i.test(f.content) && !/connectTimeout|readTimeout|timeout/i.test(f.content)) {
2959
- reviewComments.push({ tier: 2, file: f.file_path, severity: "WARNING", area: "RESILIENCE",
2960
- title: "Cross-service call with no timeout",
2961
- detail: "RestTemplate/WebClient/FeignClient used without connectTimeout + readTimeout",
2962
- action: "Configure explicit timeouts. For WebClient: .responseTimeout(Duration.ofSeconds(5))" });
2963
- }
2964
- // Praise: proper SLF4J logging
2965
- if (/log\.(info|debug|warn|error)\(/.test(f.content) && !f.isTest) {
2966
- reviewComments.push({ tier: 2, file: f.file_path, severity: "PRAISE", area: "CODE_QUALITY",
2967
- title: "Proper SLF4J logging observed", detail: "" });
2968
- }
2969
- }
2970
- }
2971
- // ── Tier 2 gate ────────────────────────────────────────────────────────
2972
- const tier2Blocking = reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL");
2973
- const tier2Passed = tier1Passed && tier2Blocking.length === 0;
2974
- // ══════════════════════════════════════════════════════════════════════
2975
- // TIER 3 — CONTEXT REVIEWER
2976
- // Cross-references task brain + project-spec + file contents.
2977
- // Only runs if both Tier 1 and Tier 2 pass.
2978
- // ══════════════════════════════════════════════════════════════════════
2979
- if (tier2Passed && !skip_checks.includes("context_review")) {
2980
- // ── AC Coverage ────────────────────────────────────────────────────
2981
- const allACs = [...new Set([
2982
- ...(taskSpec.acceptance_criteria || []),
2983
- ...(taskProgress.completed_acs || []),
2984
- ...(taskProgress.pending_acs || []),
2985
- ])];
2986
- const changeCorpus = [
2987
- ...fileRecords.map(f => f.file_path + " " + f.change_summary),
2988
- ...decisions.map(d => (d.decision || "") + " " + (d.rationale || "")),
2989
- ].join(" ").toLowerCase();
2990
- for (const ac of allACs) {
2991
- if (!ac?.trim())
2992
- continue;
2993
- const kws = ac.toLowerCase().split(/\s+/).filter(w => w.length > 3 && !/^(must|shall|should|the|and|for|with|that|this|when|from|into|not|does|will|have|been|each|only|any|all)$/.test(w));
2994
- const cov = kws.length > 0 ? kws.filter(kw => changeCorpus.includes(kw)).length / kws.length : 1;
2995
- if (cov < 0.25) {
2996
- reviewComments.push({ tier: 3, severity: "CRITICAL", area: "AC_COVERAGE",
2997
- title: "AC may not be implemented",
2998
- detail: `"${ac.substring(0, 120)}${ac.length > 120 ? "..." : ""}" — no matching file changes or decisions`,
2999
- action: "Verify this AC is addressed. If so, enrich the change_summary for the relevant file." });
3000
- }
3001
- else if (cov < 0.5) {
3002
- reviewComments.push({ tier: 3, severity: "WARNING", area: "AC_COVERAGE",
3003
- title: "AC has weak coverage signal",
3004
- detail: `"${ac.substring(0, 120)}..." — only ${Math.round(cov * 100)}% keyword match in change corpus`,
3005
- action: "Confirm AC is satisfied; enrich the file change_summary to improve signal." });
3006
- }
3007
- }
3008
- // Pending ACs
3009
- if ((taskProgress.pending_acs || []).length > 0) {
3010
- reviewComments.push({ tier: 3, severity: "CRITICAL", area: "AC_COVERAGE",
3011
- title: `${taskProgress.pending_acs.length} AC(s) still pending`,
3012
- detail: taskProgress.pending_acs.map(a => `• ${a}`).join("\n"),
3013
- action: "Complete all ACs. Call manage_task(update_spec) to move them to completed_acs." });
3014
- }
3015
- // ── Decision Drift ─────────────────────────────────────────────────
3016
- const fileCorpus = fileRecords.map(f => f.file_path + " " + f.change_summary).join(" ").toLowerCase();
3017
- for (const dec of decisions) {
3018
- if (!dec.decision || /rollback|strategy|SAFE|RISKY|DANGEROUS/i.test(dec.decision))
3019
- continue;
3020
- const kws = dec.decision.toLowerCase().split(/\s+/).filter(w => w.length > 4 && !/^(using|added|decided|chose|going|will|this|that|with|from|into|the|and|for)$/.test(w));
3021
- const trace = kws.length >= 3 ? kws.filter(kw => fileCorpus.includes(kw)).length / kws.length : 1;
3022
- if (trace < 0.2) {
3023
- reviewComments.push({ tier: 3, severity: "WARNING", area: "DECISION_DRIFT",
3024
- title: `Decision #${dec.id} not traceable in files touched`,
3025
- detail: `"${dec.decision?.substring(0, 120)}"`,
3026
- action: "Verify this decision was implemented. Log the file with manage_task(log_file_touched) if missing." });
3027
- }
3028
- }
3029
- // ── Scope Creep ────────────────────────────────────────────────────
3030
- const specCorpus = [taskSpec.title || "", taskSpec.description || "", ...(taskSpec.acceptance_criteria || []), ...decisions.map(d => (d.decision || "") + " " + (d.rationale || ""))].join(" ").toLowerCase();
3031
- for (const f of fileRecords.filter(f => !f.isTest)) {
3032
- const base = path.basename(f.file_path).replace(/\.[^.]+$/, "").toLowerCase();
3033
- const words = base.replace(/([A-Z])/g, " $1").toLowerCase().trim().split(/[\s_-]+/).filter(w => w.length > 3);
3034
- if (words.length > 0 && !words.some(w => specCorpus.includes(w))) {
3035
- reviewComments.push({ tier: 3, file: f.file_path, severity: "INFO", area: "SCOPE_CREEP",
3036
- title: "File may be outside task scope",
3037
- detail: `\`${f.file_path}\` not mentioned in spec, ACs, or decisions`,
3038
- action: "If intentional, add a decision: manage_task(log_decision) explaining the touch." });
3039
- }
3040
- }
3041
- // ── Rejected Patterns ──────────────────────────────────────────────
3042
- for (const f of fileRecords.filter(f => f.exists)) {
3043
- for (const rp of rejectedPatterns) {
3044
- if (!rp.pattern)
3045
- continue;
3046
- const kws = rp.pattern.toLowerCase().split(/\s+/).filter(w => w.length > 4).slice(0, 4);
3047
- const hits = kws.filter(kw => f.content.toLowerCase().includes(kw)).length;
3048
- if (kws.length > 0 && hits / kws.length >= 0.75) {
3049
- reviewComments.push({ tier: 3, file: f.file_path, severity: "CRITICAL", area: "REJECTED_PATTERNS",
3050
- title: "Rejected pattern detected",
3051
- detail: `Pattern: "${rp.pattern}"\nReason: ${rp.reason || "See .rejected-patterns.json"} (category: ${rp.category || "unknown"})`,
3052
- action: "This pattern was explicitly rejected. Refactor before merging." });
3053
- }
3054
- }
3055
- }
3056
- // ── Test Coverage ──────────────────────────────────────────────────
3057
- const testFiles = fileRecords.filter(f => f.isTest);
3058
- const srcFiles = fileRecords.filter(f => !f.isTest && (f.isJava || f.isTS));
3059
- if (srcFiles.length > 0 && testFiles.length === 0) {
3060
- reviewComments.push({ tier: 3, severity: "CRITICAL", area: "TEST_COVERAGE",
3061
- title: "No test files in PR",
3062
- detail: `${srcFiles.length} source file(s) changed, zero test files in files-touched`,
3063
- action: "Add unit and integration tests. Log them with manage_task(log_file_touched)." });
3064
- }
3065
- else if (testFiles.length > 0) {
3066
- reviewComments.push({ tier: 3, severity: "PRAISE", area: "TEST_COVERAGE",
3067
- title: `${testFiles.length} test file(s) included`,
3068
- detail: testFiles.map(f => `• ${f.file_path}`).join("\n") });
3069
- }
3070
- if (scenarios.filter(s => s.type === "unit").length === 0 && srcFiles.length > 0) {
3071
- reviewComments.push({ tier: 3, severity: "WARNING", area: "TEST_COVERAGE",
3072
- title: "No unit test scenarios logged",
3073
- detail: "manage_task(add_scenario, type='unit') was never called",
3074
- action: "Call suggest_test_scenarios to auto-generate scenarios from ACs." });
3075
- }
3076
- // ── Golden Rules ───────────────────────────────────────────────────
3077
- // @PreAuthorize on controllers
3078
- for (const f of fileRecords.filter(f => f.isController && !f.isTest)) {
3079
- const hasAuth = f.content.includes("@PreAuthorize");
3080
- const hasEndpoints = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)/.test(f.content);
3081
- if (hasEndpoints && hasPreAuth(f.content)) {
3082
- reviewComments.push({ tier: 3, file: f.file_path, severity: "PRAISE", area: "GOLDEN_RULES",
3083
- title: "@PreAuthorize correctly applied", detail: `\`${f.file_path}\`` });
3084
- }
3085
- }
3086
- // Entity + migration praise
3087
- const entityFiles = fileRecords.filter(f => f.isJava && /@Entity/.test(f.content) && !f.isTest);
3088
- const migFiles = fileRecords.filter(f => f.isMigration);
3089
- if (entityFiles.length > 0 && migFiles.length > 0) {
3090
- reviewComments.push({ tier: 3, severity: "PRAISE", area: "GOLDEN_RULES",
3091
- title: "@Entity change has matching Flyway migration",
3092
- detail: migFiles.map(f => path.basename(f.file_path)).join(", ") });
3093
- }
3094
- // Decision quality praise
3095
- if (decisions.length >= 3) {
3096
- reviewComments.push({ tier: 3, severity: "PRAISE", area: "DECISIONS",
3097
- title: `${decisions.length} architectural decisions logged`,
3098
- detail: "Good decision hygiene — future agents will benefit from this audit trail." });
3099
- }
3100
- if (scenarios.length >= 5) {
3101
- reviewComments.push({ tier: 3, severity: "PRAISE", area: "TEST_COVERAGE",
3102
- title: `${scenarios.length} test scenarios logged`,
3103
- detail: "Comprehensive scenario coverage documented." });
3104
- }
3105
- }
3106
- // ── Helper (moved inside scope) ────────────────────────────────────────
3107
- function hasPreAuth(content) { return content.includes("@PreAuthorize"); }
3108
- // ══════════════════════════════════════════════════════════════════════
3109
- // VERDICT — unified across all three tiers
3110
- // ══════════════════════════════════════════════════════════════════════
3111
- const allCriticals = [
3112
- ...blockingMechanical,
3113
- ...reviewComments.filter(c => c.severity === "CRITICAL"),
3114
- ];
3115
- const allWarnings = [
3116
- ...mechanical.filter(f => f.severity === "WARNING"),
3117
- ...reviewComments.filter(c => c.severity === "WARNING"),
3118
- ];
3119
- const allInfos = reviewComments.filter(c => c.severity === "INFO");
3120
- const allPraises = reviewComments.filter(c => c.severity === "PRAISE");
3121
- let verdict;
3122
- let verdictReason;
3123
- if (allCriticals.length > 0) {
3124
- verdict = "❌ CHANGES_REQUESTED";
3125
- verdictReason = `${allCriticals.length} blocking finding(s) across Tiers 1–3 must be resolved before this PR can merge.`;
3126
- }
3127
- else if (allWarnings.length > 3) {
3128
- verdict = "💬 DISCUSS";
3129
- verdictReason = `All checks passed. ${allWarnings.length} warnings worth discussing before merge.`;
3130
- }
3131
- else if (allWarnings.length > 0) {
3132
- verdict = "✅ APPROVED";
3133
- verdictReason = `All checks passed. ${allWarnings.length} minor warning(s) — address at your discretion.`;
2717
+ return { content: [{ type: 'text', text: e.stdout || e.message }], isError: true };
3134
2718
  }
3135
- else {
3136
- verdict = "✅ APPROVED";
3137
- verdictReason = `Full review passed — mechanical checks clean, AI review clean, context review clean.`;
3138
- }
3139
- // ══════════════════════════════════════════════════════════════════════
3140
- // OUTPUT
3141
- // ══════════════════════════════════════════════════════════════════════
3142
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3143
- const skippedList = skip_checks.length > 0 ? ` (skipped: ${skip_checks.join(", ")})` : "";
3144
- let out = `## run_pre_pr_checks_with_reviewer_agent — ${work_item_id}\n\n`;
3145
- out += `📁 **${fileRecords.length} file(s)** reviewed in **${elapsed}s**${skippedList}\n\n`;
3146
- // Task context strip
3147
- out += `> 📋 **${taskSpec.title || "(untitled)"}** | `;
3148
- out += `ACs: ${(taskSpec.acceptance_criteria || []).length} total, ${(taskProgress.completed_acs || []).length} done, ${(taskProgress.pending_acs || []).length} pending | `;
3149
- out += `Decisions: ${decisions.length} | Scenarios: ${scenarios.length}\n\n`;
3150
- // ── Tier 1: Mechanical table ───────────────────────────────────────────
3151
- out += `### 🔩 Tier 1 — Mechanical Checks\n`;
3152
- out += `| Pass | Check | Status | Issues |\n|---|---|---|---|\n`;
3153
- for (let i = 0; i < passResults.length; i++) {
3154
- const p = passResults[i];
3155
- const icon = p.skipped ? "⏭️ SKIP" : p.passed ? "✅ PASS" : "❌ FAIL";
3156
- const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
3157
- out += `| P${i + 1} | ${p.name} | ${icon} | ${issueText} |\n`;
3158
- }
3159
- if (blockingMechanical.length > 0) {
3160
- out += `\n#### ❌ Blocking Mechanical Findings\n`;
3161
- for (const f of blockingMechanical) {
3162
- out += `\n**[${f.check}]** \`${f.file}\`${f.line ? ` line ${f.line}` : ""}\n`;
3163
- out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
3164
- }
3165
- }
3166
- if (p1Suppressed.length > 0) {
3167
- out += `\n#### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
3168
- for (const sup of p1Suppressed) {
3169
- out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
3170
- out += `> Suppressed — ${sup.reason}\n`;
3171
- }
3172
- }
3173
- const warnings1 = mechanical.filter(f => f.severity === "WARNING");
3174
- if (warnings1.length > 0) {
3175
- out += `\n#### ⚠️ Warnings\n`;
3176
- for (const f of warnings1) {
3177
- out += `\n**[${f.check}]** \`${f.file}\`${f.line ? ` line ${f.line}` : ""}\n`;
3178
- out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
3179
- }
3180
- }
3181
- // ── Tier 2: AI Reviewer ────────────────────────────────────────────────
3182
- out += `\n### 🤖 Tier 2 — AI File Reviewer\n`;
3183
- if (!tier1Passed) {
3184
- out += `> ⏸️ Skipped — resolve Tier 1 blocking failures first.\n`;
3185
- }
3186
- else if (skip_checks.includes("ai_review")) {
3187
- out += `> ⏭️ Skipped by skip_checks.\n`;
3188
- }
3189
- else {
3190
- const t2c = reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL");
3191
- const t2w = reviewComments.filter(c => c.tier === 2 && c.severity === "WARNING");
3192
- const t2p = reviewComments.filter(c => c.tier === 2 && c.severity === "PRAISE");
3193
- if (t2c.length > 0) {
3194
- out += `\n#### 🔴 Critical (${t2c.length})\n`;
3195
- for (const c of t2c) {
3196
- out += `\n**\`${c.file}\`**${c.line ? ` line ${c.line}` : ""}\n> [${c.area}] **${c.title}** — ${c.detail}\n> 🔧 *${c.action}*\n`;
3197
- }
3198
- }
3199
- if (t2w.length > 0) {
3200
- out += `\n#### 🟡 Warnings (${t2w.length})\n`;
3201
- for (const c of t2w) {
3202
- out += `\n**\`${c.file}\`**${c.line ? ` line ${c.line}` : ""}\n> [${c.area}] ${c.title} — ${c.detail}\n> 💡 *${c.action}*\n`;
3203
- }
3204
- }
3205
- if (t2p.length > 0) {
3206
- out += `\n#### ✅ Praise\n`;
3207
- for (const c of t2p) {
3208
- out += `- \`${c.file}\`: ${c.title}\n`;
3209
- }
3210
- }
3211
- if (t2c.length === 0 && t2w.length === 0 && t2p.length === 0) {
3212
- out += `> ✅ No file-level issues found.\n`;
3213
- }
3214
- out += `> 💰 *AI review cost: ~$${aiCost.toFixed(4)}*\n`;
3215
- }
3216
- // ── Tier 3: Context Reviewer ───────────────────────────────────────────
3217
- out += `\n### 🧠 Tier 3 — Context Reviewer\n`;
3218
- if (!tier2Passed) {
3219
- out += `> ⏸️ Skipped — resolve Tier 1 and Tier 2 issues first.\n`;
3220
- }
3221
- else if (skip_checks.includes("context_review")) {
3222
- out += `> ⏭️ Skipped by skip_checks.\n`;
3223
- }
3224
- else {
3225
- const t3c = reviewComments.filter(c => c.tier === 3 && c.severity === "CRITICAL");
3226
- const t3w = reviewComments.filter(c => c.tier === 3 && c.severity === "WARNING");
3227
- const t3i = reviewComments.filter(c => c.tier === 3 && c.severity === "INFO");
3228
- const t3p = reviewComments.filter(c => c.tier === 3 && c.severity === "PRAISE");
3229
- if (t3c.length > 0) {
3230
- out += `\n#### 🔴 Critical (${t3c.length})\n`;
3231
- for (const c of t3c) {
3232
- out += `\n**[${c.area}] ${c.title}**${c.file ? ` — \`${c.file}\`` : ""}\n`;
3233
- out += `> ${c.detail.replace(/\n/g, "\n> ")}\n`;
3234
- if (c.action)
3235
- out += `> 🔧 *${c.action}*\n`;
3236
- }
3237
- }
3238
- if (t3w.length > 0) {
3239
- out += `\n#### 🟡 Warnings (${t3w.length})\n`;
3240
- for (const c of t3w) {
3241
- out += `\n**[${c.area}] ${c.title}**${c.file ? ` — \`${c.file}\`` : ""}\n`;
3242
- out += `> ${c.detail.replace(/\n/g, "\n> ")}\n`;
3243
- if (c.action)
3244
- out += `> 💡 *${c.action}*\n`;
3245
- }
3246
- }
3247
- if (t3i.length > 0) {
3248
- out += `\n#### 🟢 Info (${t3i.length})\n`;
3249
- for (const c of t3i) {
3250
- out += `- **[${c.area}]** \`${c.file || ""}\` — ${c.detail}\n`;
3251
- if (c.action)
3252
- out += ` > 💡 *${c.action}*\n`;
3253
- }
3254
- }
3255
- if (t3p.length > 0) {
3256
- out += `\n#### ✅ Praise\n`;
3257
- for (const c of t3p)
3258
- out += `- **${c.title}** — ${c.detail.replace(/\n/g, " ")}\n`;
3259
- }
3260
- if (t3c.length === 0 && t3w.length === 0 && t3i.length === 0 && t3p.length === 0) {
3261
- out += `> ✅ Task brain cross-reference clean — all ACs traceable, no rejected patterns, no scope creep.\n`;
3262
- }
3263
- }
3264
- // ── Summary table ──────────────────────────────────────────────────────
3265
- out += `\n---\n`;
3266
- out += `| Tier | Scope | 🔴 Critical | 🟡 Warning | 🟢 Info | ✅ Praise |\n|---|---|---|---|---|---|\n`;
3267
- out += `| Tier 1 | Mechanical | ${blockingMechanical.length} | ${warnings1.length} | — | — |\n`;
3268
- out += `| Tier 2 | AI File Review | ${reviewComments.filter(c => c.tier === 2 && c.severity === "CRITICAL").length} | ${reviewComments.filter(c => c.tier === 2 && c.severity === "WARNING").length} | — | ${reviewComments.filter(c => c.tier === 2 && c.severity === "PRAISE").length} |\n`;
3269
- out += `| Tier 3 | Context Review | ${reviewComments.filter(c => c.tier === 3 && c.severity === "CRITICAL").length} | ${reviewComments.filter(c => c.tier === 3 && c.severity === "WARNING").length} | ${allInfos.length} | ${reviewComments.filter(c => c.tier === 3 && c.severity === "PRAISE").length} |\n`;
3270
- out += `| **Total** | | **${allCriticals.length}** | **${allWarnings.length}** | **${allInfos.length}** | **${allPraises.length}** |\n\n`;
3271
- // ── Verdict ────────────────────────────────────────────────────────────
3272
- out += `## ${verdict}\n\n${verdictReason}\n`;
3273
- if (verdict === "✅ APPROVED") {
3274
- out += `\n**Next steps:**\n`;
3275
- out += `1. \`manage_task(action: "complete", work_item_id: "${work_item_id}")\`\n`;
3276
- out += `2. Generate PR and ADO documents manually using AGENTS.md rules\n`;
3277
- out += `3. Raise the PR\n`;
3278
- }
3279
- else if (verdict === "❌ CHANGES_REQUESTED") {
3280
- out += `\n**Resolve all 🔴 Critical findings above, then re-run \`run_pre_pr_checks_with_reviewer_agent\`.**\n`;
3281
- }
3282
- else {
3283
- out += `\n**Discuss 🟡 warnings with your team, then proceed with \`manage_task(complete)\`.**\n`;
3284
- }
3285
- return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
3286
2719
  });
3287
2720
  // Full-spectrum PR review system.
3288
2721
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",