secufusion-mcp 1.0.27 → 1.0.29

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. " +
@@ -1605,43 +1672,13 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1605
1672
  if (corpus.includes("pure ui") || corpus.includes("no backend changes") || corpus.includes("frontend only")) {
1606
1673
  backendScore = 0;
1607
1674
  }
1608
- // Determine classification
1609
- let classification;
1610
- let confidence = "HIGH";
1611
- if (backendScore > 0 && frontendScore === 0 && extensionScore === 0) {
1612
- classification = "BACKEND_ONLY";
1613
- }
1614
- else if (frontendScore > 0 && backendScore === 0 && extensionScore === 0) {
1615
- classification = "FRONTEND_ONLY";
1616
- }
1617
- else if (extensionScore > 0 && backendScore === 0 && frontendScore === 0) {
1618
- classification = "EXTENSION_ONLY";
1619
- }
1620
- else if (backendScore > 0 && (frontendScore > 0 || extensionScore > 0)) {
1621
- classification = "FULL_STACK";
1622
- }
1623
- else if (frontendScore > 0 && extensionScore > 0 && backendScore === 0) {
1624
- // Both are non-backend — treat as frontend
1625
- classification = "FRONTEND_ONLY";
1626
- }
1627
- else {
1628
- // All scores zero — ambiguous, default to backend
1629
- classification = "BACKEND_ONLY";
1630
- confidence = "LOW";
1631
- }
1632
- // Bug special rule: bugs default to backend unless explicitly UI
1633
- if (task_type === "bug" && confidence === "LOW") {
1634
- classification = "BACKEND_ONLY";
1635
- confidence = "LOW";
1636
- }
1675
+ // Determine classification will be done after Pass 4.5
1637
1676
  // ── Step 4.5: Problem statement validation ───────────────────────────────────
1638
1677
  const descLower = description.toLowerCase();
1639
1678
  let titleAccurate = true;
1640
1679
  let suggestedTitle = "";
1641
1680
  let suggestedTitleReason = "";
1642
1681
  // CHECK 1: Title accuracy
1643
- const verbs = ["delete", "create", "fetch", "update", "add", "remove", "fix", "implement"];
1644
- const nouns = ["tenant", "admin", "device", "policy", "gateway", "notification", "audit", "role", "permission", "user", "group", "mapping", "api", "endpoint", "dashboard", "db", "database"];
1645
1682
  const services = [];
1646
1683
  if (spec) {
1647
1684
  if (spec.microservices)
@@ -1654,23 +1691,45 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1654
1691
  if (services.length === 0) {
1655
1692
  services.push("sfn-iam-api", "sfn-events-api", "sfn-tenants-api", "sfn-policy-api", "sfn-gateway-api", "sfn-web-ui");
1656
1693
  }
1657
- const foundConcepts = [];
1658
- [...verbs, ...nouns, ...services].forEach(word => {
1659
- if (descLower.includes(word))
1660
- foundConcepts.push(word);
1661
- });
1662
- const titleLower = title.toLowerCase();
1663
- const matchedConcepts = foundConcepts.filter(c => titleLower.includes(c));
1664
- if (foundConcepts.length > 0 && matchedConcepts.length < (foundConcepts.length * 0.5)) {
1665
- titleAccurate = false;
1666
- const mainAction = verbs.find(v => descLower.includes(v)) || "Update";
1667
- const mainEntity = nouns.find(n => descLower.includes(n)) || "component";
1668
- const serviceName = services.find(s => descLower.includes(s)) || "service";
1669
- suggestedTitle = `${task_type.toUpperCase()}: ${mainAction} ${mainEntity} in ${serviceName}`;
1670
- suggestedTitleReason = "Title misses major concept from description";
1671
- if (title === "Tenant dashboard not loading" && descLower.includes("500") && descLower.includes("deletion")) {
1672
- suggestedTitle = "BUG: Fix false HTTP 500 response on successful tenant deletion in sfn-tenants-api";
1673
- suggestedTitleReason = "Title says dashboard loading issue but description is about API response after successful DB deletion";
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
1708
+ if (!titleAccurate) {
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)) {
1727
+ titleAccurate = true;
1728
+ // Optional advisory — not a blocking flag
1729
+ }
1730
+ else {
1731
+ suggestedTitle = "BUG: " + firstSentence.slice(0, 50) + "...";
1732
+ suggestedTitleReason = "Title symptoms do not match description core symptoms";
1674
1733
  }
1675
1734
  }
1676
1735
  // CHECK 2: Problem statement completeness
@@ -1740,30 +1799,26 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1740
1799
  scopeQuestions.push("2. Which component or service is affected?");
1741
1800
  }
1742
1801
  }
1743
- // CHECK 5: Task type correctness
1744
- let taskTypeCorrect = true;
1745
- let suggestedTaskType = "";
1746
- let taskTypeReason = "";
1747
- if (task_type === "bug" && (descLower.includes("add new feature") || descLower.includes("implement a new") || descLower.includes("create"))) {
1748
- taskTypeCorrect = false;
1749
- suggestedTaskType = "feature";
1750
- taskTypeReason = "Description implements new functionality, not fixing a defect";
1751
- }
1752
- else if (task_type === "user_story" && (descLower.includes("fix") || descLower.includes("broken") || descLower.includes("error") || descLower.includes("exception") || descLower.includes("failing"))) {
1753
- taskTypeCorrect = false;
1754
- suggestedTaskType = "bug";
1755
- taskTypeReason = "Description describes fixing a defect, not a user story";
1756
- }
1757
- else if (task_type === "refactor" && (descLower.includes("users are reporting") || descLower.includes("production issue"))) {
1758
- taskTypeCorrect = false;
1759
- suggestedTaskType = "hotfix";
1760
- taskTypeReason = "Description implies a production issue, not a refactor";
1761
- }
1762
- else if (task_type === "chore" && (descLower.includes("core business logic") || descLower.includes("endpoint"))) {
1763
- taskTypeCorrect = false;
1764
- suggestedTaskType = "feature";
1765
- taskTypeReason = "Description involves functional changes, not a chore";
1766
- }
1802
+ const DEFECT_SIGNALS = [
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"
1811
+ ];
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
+ : "";
1767
1822
  // CHECK 6: SecuFusion-specific validation
1768
1823
  const secufusionFlags = [];
1769
1824
  rejectedPatterns.forEach(r => {
@@ -1771,15 +1826,107 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1771
1826
  secufusionFlags.push(`This may re-introduce rejected pattern #${r.id}: ${r.pattern}`);
1772
1827
  }
1773
1828
  });
1774
- // Dynamically inject coding patterns from project spec
1829
+ const architectureOwnershipSignals = [];
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
+ ];
1775
1860
  if (spec && spec.coding_patterns) {
1776
- Object.entries(spec.coding_patterns).forEach(([key, rule]) => {
1777
- const keyTokens = key.toLowerCase().split('_');
1778
- // if any token from the key is in the corpus, add the rule
1779
- if (keyTokens.some(token => corpus.includes(token))) {
1780
- secufusionFlags.push(`Architecture Rule (${key}): ${rule}`);
1861
+ for (const [ruleName, ruleText] of Object.entries(spec.coding_patterns)) {
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)
1871
+ continue;
1872
+ const isFrontendOwnership = FRONTEND_OWNERSHIP_PHRASES.some(phrase => ruleStr.includes(phrase));
1873
+ const isBackendOwnership = BACKEND_OWNERSHIP_PHRASES.some(phrase => ruleStr.includes(phrase));
1874
+ if (isFrontendOwnership) {
1875
+ frontendScore = (frontendScore || 0) + 8;
1876
+ architectureOwnershipSignals.push({
1877
+ rule: ruleName,
1878
+ direction: "FRONTEND",
1879
+ note: String(ruleText).slice(0, 120)
1880
+ });
1781
1881
  }
1782
- });
1882
+ else if (isBackendOwnership) {
1883
+ backendScore = (backendScore || 0) + 8;
1884
+ architectureOwnershipSignals.push({
1885
+ rule: ruleName,
1886
+ direction: "BACKEND",
1887
+ note: String(ruleText).slice(0, 120)
1888
+ });
1889
+ }
1890
+ else {
1891
+ secufusionFlags.push(`Note — Architecture Rule (${ruleName}): ` +
1892
+ `${String(ruleText).slice(0, 100)}` +
1893
+ ` (verify this applies to your implementation)`);
1894
+ }
1895
+ }
1896
+ }
1897
+ // Pass 5 — Confidence gate & Classification assignment
1898
+ let classification;
1899
+ let confidence = "HIGH";
1900
+ if (backendScore > 0 && frontendScore === 0 && extensionScore === 0) {
1901
+ classification = "BACKEND_ONLY";
1902
+ }
1903
+ else if (frontendScore > 0 && backendScore === 0 && extensionScore === 0) {
1904
+ classification = "FRONTEND_ONLY";
1905
+ }
1906
+ else if (extensionScore > 0 && backendScore === 0 && frontendScore === 0) {
1907
+ classification = "EXTENSION_ONLY";
1908
+ }
1909
+ else if (backendScore > 0 && (frontendScore > 0 || extensionScore > 0)) {
1910
+ if (frontendScore > backendScore * 2.5) {
1911
+ classification = "FRONTEND_ONLY";
1912
+ }
1913
+ else if (backendScore > frontendScore * 2.5) {
1914
+ classification = "BACKEND_ONLY";
1915
+ }
1916
+ else {
1917
+ classification = "FULL_STACK";
1918
+ }
1919
+ }
1920
+ else if (frontendScore > 0 && extensionScore > 0 && backendScore === 0) {
1921
+ classification = "FRONTEND_ONLY";
1922
+ }
1923
+ else {
1924
+ classification = "BACKEND_ONLY";
1925
+ confidence = "LOW";
1926
+ }
1927
+ if (task_type === "bug" && confidence === "LOW") {
1928
+ classification = "BACKEND_ONLY";
1929
+ confidence = "LOW";
1783
1930
  }
1784
1931
  let validationVerdict = "CLEAN";
1785
1932
  if (!titleAccurate || !taskTypeCorrect || (task_type === "bug" && rootCauseAssumed)) {
@@ -1893,10 +2040,18 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1893
2040
  else if (classification === "FULL_STACK") {
1894
2041
  allowedNextAction = "CONFIRM";
1895
2042
  nextPhase = "Confirm backend scope, then plan API contract first";
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");
1896
2051
  developerMessage =
1897
2052
  `⚠️ FULL STACK TASK\n\n` +
1898
- `Backend scope (yours): ${backendFound.slice(0, 8).join(", ")}\n` +
1899
- `Frontend scope (not yours): ${frontendFound.slice(0, 8).join(", ")}\n\n` +
2053
+ `Backend scope (yours):\n → ${backendScopeText}\n` +
2054
+ `Frontend scope (not yours):\n → ${frontendScopeText}\n\n` +
1900
2055
  `I will implement the backend only and define the API contract first.\n` +
1901
2056
  `Confirm and I will proceed to plan.`;
1902
2057
  }
@@ -1942,6 +2097,14 @@ server.tool("classify_task", "MANDATORY FIRST STEP for every task without except
1942
2097
  finalDeveloperMessage += "\n────────────────────────────────────────\n";
1943
2098
  }
1944
2099
  }
2100
+ if (architectureOwnershipSignals.length > 0) {
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
+ }
2107
+ }
1945
2108
  if (learnedLogs.length > 0) {
1946
2109
  finalDeveloperMessage += `🧠 RETROSPECTIVE INTELLIGENCE APPLIED\n`;
1947
2110
  finalDeveloperMessage += learnedLogs.join("\n") + "\n\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",