secufusion-mcp 1.0.28 → 1.0.30

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.
@@ -12,6 +12,9 @@
12
12
  "branch_naming_convention": "Mixed, not strictly enforced. Common patterns: feature/<kebab-description>, fix/<desc> or bugfix/<ticket-or-desc>, ticket-only (TASK-####, TASK-####-<desc>, BUG-####-<desc>), release-<major>.<minor>.<patch> (dash, not slash) for release branches. Trunk branches are 'main' and 'develop' in every repo, but which one is checked out as the working default varies per repo (sfn-events-api and sfn-web-ui default to develop; snf-browser-extn defaults to main). Many ad hoc/free-text branch names also exist.",
13
13
  "commit_message_pattern": "No single enforced convention. Recurring mixture: (1) Azure Repos auto-generated 'Merged PR <number>: <title>' on PR completion (dominant pattern in all 3 sampled repos), (2) ticket-prefixed 'TASK-<number>: <desc>' or 'TASK-<number>-<desc>' (common in sfn-events-api), (3) conventional-commit style 'feat(<scope>): ...' / 'fix(<scope>): ...' (used consistently within recent 'ai-ops' feature work in sfn-web-ui, not elsewhere), (4) plain imperative descriptive messages with no prefix at all. Work items referenced via https://dev.azure.com/secufusion/SFCloud-MSSP/_workitems/edit/<id>."
14
14
  },
15
+ "coding_patterns": {
16
+ "timestamp_handling": "backend ONLY produces UTC timestamp strings. All time formatting and local timezone conversion MUST be handled by the frontend."
17
+ },
15
18
  "microservices": {
16
19
  "sfn-auth-api": {
17
20
  "artifact_id": "sfn-auth-api",
package/index.js CHANGED
@@ -1462,6 +1462,73 @@ server.tool("record_retrospective", "Records a structured retrospective after ta
1462
1462
  }
1463
1463
  return appendTelemetry({ content: [{ type: "text", text: out }] }, inputChars);
1464
1464
  });
1465
+ const STOP_WORDS = new Set([
1466
+ "the", "a", "an", "is", "are", "was", "were", "be",
1467
+ "been", "being", "have", "has", "had", "do", "does",
1468
+ "did", "will", "would", "could", "should", "may",
1469
+ "might", "shall", "can", "to", "of", "in", "on", "at",
1470
+ "by", "for", "with", "about", "against", "between",
1471
+ "into", "through", "during", "before", "after",
1472
+ "from", "up", "down", "out", "off", "over", "under",
1473
+ "and", "but", "or", "nor", "so", "yet", "both",
1474
+ "either", "neither", "not", "only", "same", "than",
1475
+ "too", "very", "just", "that", "this", "these",
1476
+ "those", "it", "its", "itself", "which", "who",
1477
+ "what", "where", "when", "why", "how", "all", "each",
1478
+ "every", "few", "more", "most", "other", "some",
1479
+ "such", "no", "own", "also", "instead", "rather",
1480
+ "shows", "show", "display", "displays", "showing",
1481
+ "shown", "currently", "now", "then", "also", "there",
1482
+ "their", "they", "them", "we", "our", "your", "my",
1483
+ "his", "her", "its", "page", "modal", "screen"
1484
+ ]);
1485
+ function extractSymptomWords(text) {
1486
+ return text
1487
+ .toLowerCase()
1488
+ .replace(/[^a-z0-9\s]/g, " ")
1489
+ .split(/\s+/)
1490
+ .filter(w => w.length > 3)
1491
+ .filter(w => !STOP_WORDS.has(w))
1492
+ .filter(w => !/^\d+$/.test(w));
1493
+ }
1494
+ function levenshteinClose(a, b, threshold = 2) {
1495
+ if (Math.abs(a.length - b.length) > threshold)
1496
+ return false;
1497
+ let matrix = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
1498
+ for (let j = 0; j <= b.length; j++)
1499
+ matrix[0][j] = j;
1500
+ for (let i = 1; i <= a.length; i++) {
1501
+ for (let j = 1; j <= b.length; j++) {
1502
+ matrix[i][j] = a[i - 1] === b[j - 1]
1503
+ ? matrix[i - 1][j - 1]
1504
+ : 1 + Math.min(matrix[i - 1][j], matrix[i][j - 1], matrix[i - 1][j - 1]);
1505
+ }
1506
+ }
1507
+ return matrix[a.length][b.length] <= threshold;
1508
+ }
1509
+ function buildScopeText(signals, domain) {
1510
+ if (signals.length === 0) {
1511
+ return `${domain} changes required`;
1512
+ }
1513
+ const serviceNames = signals.filter(s => s.includes("-api") || s.includes("-service") ||
1514
+ s.includes("sfn-"));
1515
+ const techTerms = signals.filter(s => !s.includes("-api") && !s.includes("-service") &&
1516
+ !s.includes("sfn-"));
1517
+ const parts = [];
1518
+ if (serviceNames.length > 0) {
1519
+ parts.push(`Services: ${serviceNames.join(", ")}`);
1520
+ }
1521
+ if (techTerms.length > 0 && techTerms.length <= 4) {
1522
+ parts.push(`Changes: ${techTerms.join(", ")}`);
1523
+ }
1524
+ else if (techTerms.length > 4) {
1525
+ parts.push(`Changes: ${techTerms.slice(0, 3).join(", ")}` +
1526
+ ` (+${techTerms.length - 3} more)`);
1527
+ }
1528
+ return parts.length > 0
1529
+ ? parts.join("\n ")
1530
+ : `${domain} implementation required`;
1531
+ }
1465
1532
  // ─── Tool 9: classify_task ────────────────────────────────────────────────────
1466
1533
  server.tool("classify_task", "MANDATORY FIRST STEP for every task without exception. " +
1467
1534
  "Classifies a task as BACKEND_ONLY, FRONTEND_ONLY, FULL_STACK, or EXTENSION_ONLY. " +
@@ -1612,56 +1679,53 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1612
1679
  let suggestedTitle = "";
1613
1680
  let suggestedTitleReason = "";
1614
1681
  // CHECK 1: Title accuracy
1615
- const STOP_WORDS = new Set([
1616
- "the", "a", "an", "is", "are", "was", "were",
1617
- "be", "been", "being", "have", "has", "had",
1618
- "do", "does", "did", "will", "would", "could",
1619
- "should", "may", "might", "shall", "can",
1620
- "to", "of", "in", "on", "at", "by", "for",
1621
- "with", "about", "against", "between", "into",
1622
- "through", "during", "before", "after", "above",
1623
- "below", "from", "up", "down", "out", "off",
1624
- "over", "under", "again", "further", "then",
1625
- "once", "and", "but", "or", "nor", "so", "yet",
1626
- "both", "either", "neither", "not", "only",
1627
- "same", "than", "too", "very", "just", "that",
1628
- "this", "these", "those", "it", "its", "itself",
1629
- "which", "who", "whom", "what", "where", "when",
1630
- "why", "how", "all", "each", "every",
1631
- "few", "more", "most", "other", "some", "such",
1632
- "no", "own", "also", "instead", "rather",
1633
- "shows", "show", "display", "displays", "showing", "shown",
1634
- "currently", "now"
1635
- ]);
1636
- function extractSymptomWords(text) {
1637
- return text
1638
- .toLowerCase()
1639
- .replace(/[^a-z0-9\s]/g, ' ')
1640
- .split(/\s+/)
1641
- .filter(w => w.length > 3)
1642
- .filter(w => !STOP_WORDS.has(w))
1643
- .filter(w => !w.match(/^\d+$/));
1644
- }
1645
- const titleSymptoms = extractSymptomWords(title);
1646
- const descSymptoms = extractSymptomWords(description);
1647
- const firstSentence = description.split('.')[0] || "";
1648
- const coreSymptoms = extractSymptomWords(firstSentence).slice(0, 5);
1649
- const titleCoverage = coreSymptoms.filter(sym => titleSymptoms.some(ts => ts.includes(sym) || sym.includes(ts))).length / Math.max(coreSymptoms.length, 1);
1650
- titleAccurate = titleCoverage >= 0.4;
1651
- function detectDomain(text) {
1652
- const t = text.toLowerCase();
1653
- if (t.includes("modal") || t.includes("display") || t.includes("shows") || t.includes("screen"))
1654
- return "display/UI";
1655
- if (t.includes("database") || t.includes("query") || t.includes("table"))
1656
- return "database";
1657
- return "unknown";
1658
- }
1682
+ const services = [];
1683
+ if (spec) {
1684
+ if (spec.microservices)
1685
+ Object.keys(spec.microservices).forEach(k => services.push(k.toLowerCase()));
1686
+ if (spec.frontend && spec.frontend.repo)
1687
+ services.push(spec.frontend.repo.toLowerCase());
1688
+ if (spec.chrome_extension && spec.chrome_extension.repo)
1689
+ services.push(spec.chrome_extension.repo.toLowerCase());
1690
+ }
1691
+ if (services.length === 0) {
1692
+ services.push("sfn-iam-api", "sfn-events-api", "sfn-tenants-api", "sfn-policy-api", "sfn-gateway-api", "sfn-web-ui");
1693
+ }
1694
+ const firstSentence = description.split(/[.!?]/)[0] || description;
1695
+ const coreSymptoms = extractSymptomWords(firstSentence).slice(0, 6);
1696
+ const titleWords = extractSymptomWords(title);
1697
+ // Title is accurate if covers >= 35% of
1698
+ // core symptom words from first sentence
1699
+ const matchCount = coreSymptoms.filter(sym => titleWords.some(tw => tw.includes(sym) || sym.includes(tw) ||
1700
+ levenshteinClose(tw, sym))).length;
1701
+ const titleCoverage = coreSymptoms.length > 0
1702
+ ? matchCount / coreSymptoms.length
1703
+ : 1.0;
1704
+ titleAccurate = titleCoverage >= 0.35;
1705
+ // Secondary check: if title and desc are
1706
+ // about same domain → not misleading
1707
+ // even if coverage is low
1659
1708
  if (!titleAccurate) {
1660
- const titleDomain = detectDomain(title);
1661
- const descDomain = detectDomain(description);
1662
- if (titleDomain === descDomain && titleDomain !== "unknown") {
1709
+ const DISPLAY_WORDS = [
1710
+ "show", "display", "render", "modal", "page",
1711
+ "screen", "ui", "view", "format", "time", "date",
1712
+ "timezone", "utc", "local", "color", "layout",
1713
+ "button", "icon", "text", "label"
1714
+ ];
1715
+ const BACKEND_WORDS = [
1716
+ "api", "service", "endpoint", "db", "database",
1717
+ "query", "kafka", "migration", "entity", "null",
1718
+ "exception", "500", "error", "timeout", "token"
1719
+ ];
1720
+ const titleIsDisplay = DISPLAY_WORDS.some(w => title.toLowerCase().includes(w));
1721
+ const descIsDisplay = DISPLAY_WORDS.some(w => description.toLowerCase().includes(w));
1722
+ const titleIsBackend = BACKEND_WORDS.some(w => title.toLowerCase().includes(w));
1723
+ const descIsBackend = BACKEND_WORDS.some(w => description.toLowerCase().includes(w));
1724
+ // Same domain = not misleading
1725
+ if ((titleIsDisplay && descIsDisplay) ||
1726
+ (titleIsBackend && descIsBackend)) {
1663
1727
  titleAccurate = true;
1664
- // Will add to advisories later
1728
+ // Optional advisory not a blocking flag
1665
1729
  }
1666
1730
  else {
1667
1731
  suggestedTitle = "BUG: " + firstSentence.slice(0, 50) + "...";
@@ -1735,29 +1799,26 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1735
1799
  scopeQuestions.push("2. Which component or service is affected?");
1736
1800
  }
1737
1801
  }
1738
- // CHECK 5: Task type correctness
1739
- let taskTypeCorrect = true;
1740
- let suggestedTaskType = "";
1741
- let taskTypeReason = "";
1742
1802
  const DEFECT_SIGNALS = [
1743
- "instead of", "rather than", "shows",
1744
- "displays", "not showing", "not displaying",
1745
- "incorrect", "wrong", "broken", "failing",
1746
- "error", "exception", "crash", "null",
1747
- "missing", "unexpected", "should show",
1748
- "should display", "expected", "actual",
1749
- "bug", "fix", "issue", "problem",
1750
- "not working", "doesn't work", "fails"
1803
+ "instead of", "rather than", "incorrect", "wrong",
1804
+ "broken", "failing", "error", "exception", "crash",
1805
+ "null", "missing", "unexpected", "should show",
1806
+ "should display", "expected", "actual", "not working",
1807
+ "doesn't work", "fails", "not showing", "bug",
1808
+ "fix", "issue", "problem", "invalid", "400", "500",
1809
+ "404", "403", "401", "timeout", "slow", "blank",
1810
+ "empty", "duplicate", "stale", "outdated"
1751
1811
  ];
1752
- const hasDefectSignal = DEFECT_SIGNALS.some(s => (title + " " + description).toLowerCase().includes(s));
1753
- if (task_type === "bug" && !hasDefectSignal) {
1754
- taskTypeCorrect = false;
1755
- suggestedTaskType = "feature";
1756
- taskTypeReason = "No defect signals found in description";
1757
- }
1758
- else {
1759
- taskTypeCorrect = true;
1760
- }
1812
+ const hasDefectSignal = DEFECT_SIGNALS.some(s => (title + " " + description)
1813
+ .toLowerCase()
1814
+ .includes(s));
1815
+ // Only flag wrong task type when NO defect
1816
+ // signals exist at all
1817
+ let taskTypeCorrect = task_type !== "bug" || hasDefectSignal;
1818
+ let suggestedTaskType = !taskTypeCorrect ? "feature" : task_type;
1819
+ let taskTypeReason = !taskTypeCorrect
1820
+ ? "No defect signals found in title or description. Description appears to implement new functionality."
1821
+ : "";
1761
1822
  // CHECK 6: SecuFusion-specific validation
1762
1823
  const secufusionFlags = [];
1763
1824
  rejectedPatterns.forEach(r => {
@@ -1766,51 +1827,70 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1766
1827
  }
1767
1828
  });
1768
1829
  const architectureOwnershipSignals = [];
1769
- let ownershipNotes = [];
1770
- // Dynamically inject coding patterns from project spec
1830
+ const FRONTEND_OWNERSHIP_PHRASES = [
1831
+ "frontend must",
1832
+ "frontend should",
1833
+ "handled by the frontend",
1834
+ "must be handled by the frontend",
1835
+ "frontend handles",
1836
+ "frontend converts",
1837
+ "ui must",
1838
+ "client must",
1839
+ "client side",
1840
+ "never returns pre-formatted",
1841
+ "only produces",
1842
+ "backend only produces",
1843
+ "browser must",
1844
+ "all time formatting",
1845
+ "timezone conversion must"
1846
+ ];
1847
+ const BACKEND_OWNERSHIP_PHRASES = [
1848
+ "backend must",
1849
+ "backend should",
1850
+ "server must",
1851
+ "service must",
1852
+ "always scoped with",
1853
+ "must filter by",
1854
+ "never expose",
1855
+ "controller resolves",
1856
+ "service layer must",
1857
+ "repository must",
1858
+ "always include tenantid"
1859
+ ];
1771
1860
  if (spec && spec.coding_patterns) {
1772
- const FRONTEND_OWNERSHIP_PHRASES = [
1773
- "frontend must", "frontend should", "handled by the frontend",
1774
- "must be handled by the frontend", "frontend handles", "ui must",
1775
- "client must", "client side", "never returns pre-formatted",
1776
- "frontend converts", "browser must", "only produces", "backend only produces"
1777
- ];
1778
- const BACKEND_OWNERSHIP_PHRASES = [
1779
- "backend must", "backend should", "server must", "service must",
1780
- "always scoped with", "must filter by", "never expose",
1781
- "controller resolves", "service layer must"
1782
- ];
1783
- const extractKw = (text) => text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 3);
1784
1861
  for (const [ruleName, ruleText] of Object.entries(spec.coding_patterns)) {
1785
- const rText = ruleText;
1786
- const ruleTextLower = rText.toLowerCase();
1787
- const descLowerCombined = (title + " " + description).toLowerCase();
1788
- const ruleKeywords = extractKw(rText);
1789
- const taskMentionsRule = ruleKeywords.some(kw => descLowerCombined.includes(kw));
1790
- if (!taskMentionsRule)
1862
+ const ruleStr = String(ruleText).toLowerCase();
1863
+ const taskText = (title + " " + description).toLowerCase();
1864
+ const ruleWords = ruleName
1865
+ .toLowerCase()
1866
+ .replace(/_/g, " ")
1867
+ .split(" ")
1868
+ .filter(w => w.length > 3);
1869
+ const ruleApplies = ruleWords.some(w => taskText.includes(w)) || Object.keys(spec.coding_patterns).some(k => taskText.includes(k.toLowerCase().replace(/_/g, " ")));
1870
+ if (!ruleApplies)
1791
1871
  continue;
1792
- const isFrontendOwnership = FRONTEND_OWNERSHIP_PHRASES.some(phrase => ruleTextLower.includes(phrase));
1793
- const isBackendOwnership = BACKEND_OWNERSHIP_PHRASES.some(phrase => ruleTextLower.includes(phrase));
1872
+ const isFrontendOwnership = FRONTEND_OWNERSHIP_PHRASES.some(phrase => ruleStr.includes(phrase));
1873
+ const isBackendOwnership = BACKEND_OWNERSHIP_PHRASES.some(phrase => ruleStr.includes(phrase));
1794
1874
  if (isFrontendOwnership) {
1795
- frontendScore += 8;
1796
- ownershipNotes.push(`Architecture rule '${ruleName}' confirms frontend ownership: ${rText.slice(0, 80)}...`);
1875
+ frontendScore = (frontendScore || 0) + 8;
1797
1876
  architectureOwnershipSignals.push({
1798
1877
  rule: ruleName,
1799
1878
  direction: "FRONTEND",
1800
- note: rText
1879
+ note: String(ruleText).slice(0, 120)
1801
1880
  });
1802
1881
  }
1803
1882
  else if (isBackendOwnership) {
1804
- backendScore += 8;
1805
- ownershipNotes.push(`Architecture rule '${ruleName}' confirms backend ownership: ${rText.slice(0, 80)}...`);
1883
+ backendScore = (backendScore || 0) + 8;
1806
1884
  architectureOwnershipSignals.push({
1807
1885
  rule: ruleName,
1808
1886
  direction: "BACKEND",
1809
- note: rText
1887
+ note: String(ruleText).slice(0, 120)
1810
1888
  });
1811
1889
  }
1812
1890
  else {
1813
- secufusionFlags.push(`Note — Architecture Rule (${ruleName}): ${rText.slice(0, 100)}... (verify this applies to your implementation)`);
1891
+ secufusionFlags.push(`Note — Architecture Rule (${ruleName}): ` +
1892
+ `${String(ruleText).slice(0, 100)}` +
1893
+ ` (verify this applies to your implementation)`);
1814
1894
  }
1815
1895
  }
1816
1896
  }
@@ -1960,22 +2040,18 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1960
2040
  else if (classification === "FULL_STACK") {
1961
2041
  allowedNextAction = "CONFIRM";
1962
2042
  nextPhase = "Confirm backend scope, then plan API contract first";
1963
- const backendScopeLines = [];
1964
- const matchedServiceNames = backendFound.filter(s => s.startsWith("sfn-"));
1965
- const matchedTechConstructs = backendFound.filter(s => !s.startsWith("sfn-"));
1966
- if (matchedServiceNames.length > 0) {
1967
- backendScopeLines.push(`Services: ${[...new Set(matchedServiceNames)].join(", ")}`);
1968
- }
1969
- if (matchedTechConstructs.length > 0) {
1970
- backendScopeLines.push(`Changes: ${[...new Set(matchedTechConstructs)].join(", ")}`);
1971
- }
1972
- const backendScopeText = backendScopeLines.length > 0
1973
- ? backendScopeLines.join("\n → ")
1974
- : "Backend logic changes required";
2043
+ const uniqueBackendSignals = [
2044
+ ...new Set(backendFound.filter(Boolean))
2045
+ ];
2046
+ const uniqueFrontendSignals = [
2047
+ ...new Set(frontendFound.filter(Boolean))
2048
+ ];
2049
+ const backendScopeText = buildScopeText(uniqueBackendSignals, "backend");
2050
+ const frontendScopeText = buildScopeText(uniqueFrontendSignals, "frontend");
1975
2051
  developerMessage =
1976
2052
  `⚠️ FULL STACK TASK\n\n` +
1977
2053
  `Backend scope (yours):\n → ${backendScopeText}\n` +
1978
- `Frontend scope (not yours): ${[...new Set(frontendFound)].slice(0, 8).join(", ")}\n\n` +
2054
+ `Frontend scope (not yours):\n → ${frontendScopeText}\n\n` +
1979
2055
  `I will implement the backend only and define the API contract first.\n` +
1980
2056
  `Confirm and I will proceed to plan.`;
1981
2057
  }
@@ -2022,8 +2098,12 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
2022
2098
  }
2023
2099
  }
2024
2100
  if (architectureOwnershipSignals.length > 0) {
2025
- finalDeveloperMessage += "📐 Architecture ownership signals:\n" +
2026
- architectureOwnershipSignals.map(s => ` → [${s.direction}] Rule '${s.rule}': ${s.note.slice(0, 80)}...`).join("\n") + "\n\n";
2101
+ finalDeveloperMessage += `\n📐 Architecture ownership signals:\n`;
2102
+ for (const sig of architectureOwnershipSignals) {
2103
+ finalDeveloperMessage +=
2104
+ ` → [${sig.direction}] Rule '${sig.rule}':\n` +
2105
+ ` ${sig.note}\n`;
2106
+ }
2027
2107
  }
2028
2108
  if (learnedLogs.length > 0) {
2029
2109
  finalDeveloperMessage += `🧠 RETROSPECTIVE INTELLIGENCE APPLIED\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",