@4da/mcp-server 5.0.4 → 6.0.1

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 (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/README.md +70 -39
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -58
  6. package/dist/db.js +226 -299
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +12 -1
  51. package/dist/live/osv-scanner.js +50 -15
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -136
  158. package/package.json +35 -35
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -6,195 +6,16 @@
6
6
  * breaking changes, tool discoveries, etc.) with priority levels.
7
7
  * Cross-references against the user's ACE-detected tech stack.
8
8
  */
9
+ import { isMaintenanceNotice } from "../live/maintenance.js";
10
+ import { presentedSeverity, scopeAdjustmentReason } from "../live/severity-scope.js";
9
11
  import { getLiveIntelligence } from "../live-singleton.js";
10
- // ============================================================================
11
- // Pattern Definitions
12
- // ============================================================================
13
- const SIGNAL_PATTERNS = {
14
- security_alert: {
15
- keywords: [
16
- "cve", "vulnerability", "exploit", "breach", "security flaw",
17
- "zero-day", "zero day", "0-day", "patch", "ransomware",
18
- "malware", "rce", "injection attack", "xss", "csrf",
19
- "privilege escalation", "backdoor", "supply chain attack",
20
- ],
21
- boostWords: ["critical", "urgent", "severe", "actively exploited", "emergency"],
22
- weight: 1.0,
23
- },
24
- breaking_change: {
25
- keywords: [
26
- "breaking change", "deprecated", "end of life", "eol",
27
- "migration guide", "major release", "drops support",
28
- "removed in", "no longer supported", "sunset",
29
- "backwards incompatible", "api change",
30
- ],
31
- boostWords: ["v2", "v3", "v4", "v5", "major version", "upgrade required"],
32
- weight: 0.9,
33
- },
34
- tool_discovery: {
35
- keywords: [
36
- "new release", "just released", "announcing", "launch",
37
- "alternative to", "built with", "replacement for",
38
- "open source", "open-source", "introducing",
39
- "we built", "i built", "show hn",
40
- ],
41
- boostWords: ["faster", "better", "simpler", "lightweight", "blazing"],
42
- weight: 0.7,
43
- },
44
- tech_trend: {
45
- keywords: [
46
- "adoption", "growing", "trending", "benchmark",
47
- "comparison", "state of", "survey", "report",
48
- "market share", "ecosystem", "roadmap",
49
- ],
50
- boostWords: ["2025", "2026", "accelerating", "mainstream", "industry"],
51
- weight: 0.6,
52
- },
53
- learning: {
54
- keywords: [
55
- "tutorial", "how to", "guide", "deep dive",
56
- "explained", "best practices", "patterns",
57
- "architecture", "lessons learned", "walkthrough",
58
- "step by step", "from scratch",
59
- ],
60
- boostWords: ["advanced", "production", "real-world", "comprehensive"],
61
- weight: 0.5,
62
- },
63
- competitive_intel: {
64
- keywords: [
65
- "acquired", "funding", "raised", "ipo",
66
- "valuation", "market share", "competitor",
67
- "pivots", "pivot", "layoffs", "shutdown",
68
- "acqui-hire", "series a", "series b",
69
- ],
70
- boostWords: ["million", "billion", "disrupts", "overtakes"],
71
- weight: 0.6,
72
- },
73
- };
74
- const BASE_WEIGHTS = {
75
- security_alert: 2,
76
- breaking_change: 2,
77
- tool_discovery: 1,
78
- tech_trend: 1,
79
- learning: 1,
80
- competitive_intel: 1,
81
- };
82
- // Word-boundary matching prevents "rce" matching inside "source", "xss" inside "success", etc.
83
- function hasWordBoundary(text, term) {
84
- let searchFrom = 0;
85
- while (true) {
86
- const pos = text.indexOf(term, searchFrom);
87
- if (pos === -1)
88
- return false;
89
- const beforeOk = pos === 0 || !/[a-zA-Z0-9]/.test(text[pos - 1]);
90
- const afterIdx = pos + term.length;
91
- const afterOk = afterIdx >= text.length || !/[a-zA-Z0-9]/.test(text[afterIdx]);
92
- if (beforeOk && afterOk)
93
- return true;
94
- searchFrom = pos + 1;
95
- }
96
- }
97
- const PRIORITY_LABELS = {
98
- security_alert: "Security Alert",
99
- breaking_change: "Breaking Change",
100
- tool_discovery: "Tool Discovery",
101
- tech_trend: "Tech Trend",
102
- learning: "Learning",
103
- competitive_intel: "Competitive Intel",
104
- };
105
- const KEYWORD_MAP = new Map();
106
- // Initialize keyword map
107
- for (const [type, pattern] of Object.entries(SIGNAL_PATTERNS)) {
108
- for (const kw of pattern.keywords) {
109
- if (!KEYWORD_MAP.has(kw)) {
110
- KEYWORD_MAP.set(kw, []);
111
- }
112
- KEYWORD_MAP.get(kw).push({ type, weight: pattern.weight, isBoost: false });
113
- }
114
- for (const bw of pattern.boostWords) {
115
- if (!KEYWORD_MAP.has(bw)) {
116
- KEYWORD_MAP.set(bw, []);
117
- }
118
- KEYWORD_MAP.get(bw).push({ type, weight: 0.2, isBoost: true });
119
- }
120
- }
121
- function classify(title, content, relevanceScore, detectedTech) {
122
- // Pre-compute lowercased text once
123
- const textLower = `${title} ${content}`.toLowerCase();
124
- const titleLower = title.toLowerCase();
125
- // Track scores per signal type
126
- const typeScores = {};
127
- // Single pass through keyword map (word-boundary matched to prevent false positives)
128
- for (const [keyword, entries] of KEYWORD_MAP.entries()) {
129
- if (hasWordBoundary(textLower, keyword)) {
130
- for (const entry of entries) {
131
- if (!typeScores[entry.type]) {
132
- typeScores[entry.type] = { score: 0, matched: [] };
133
- }
134
- typeScores[entry.type].score += entry.weight;
135
- typeScores[entry.type].matched.push(keyword);
136
- // Boost if keyword is in title (only for non-boost words)
137
- if (!entry.isBoost && hasWordBoundary(titleLower, keyword)) {
138
- typeScores[entry.type].score += entry.weight * 0.5;
139
- }
140
- }
141
- }
142
- }
143
- // Find best type
144
- let bestType = null;
145
- let bestConfidence = 0;
146
- let bestTriggers = [];
147
- for (const [type, data] of Object.entries(typeScores)) {
148
- const confidence = Math.min(data.score / 3.0, 1.0);
149
- if (confidence > bestConfidence) {
150
- bestType = type;
151
- bestConfidence = confidence;
152
- bestTriggers = data.matched;
153
- }
154
- }
155
- if (!bestType)
156
- return null;
157
- // Require at least 2 keyword matches — single keyword produces too many false positives
158
- if (bestTriggers.length < 2)
159
- return null;
160
- // Compute priority
161
- let priorityScore = BASE_WEIGHTS[bestType];
162
- const techMatch = detectedTech.find((t) => hasWordBoundary(textLower, t.toLowerCase()));
163
- // Security alerts MUST mention something in the user's stack to be relevant.
164
- // A CVE for "Prometheus" or "PraisonAI" is noise if the user doesn't use them.
165
- // Only keep security alerts that explicitly name a technology the user has.
166
- if (bestType === "security_alert" && !techMatch) {
167
- return null;
168
- }
169
- if (techMatch)
170
- priorityScore += 1;
171
- if (relevanceScore > 0.7)
172
- priorityScore += 1;
173
- priorityScore = Math.min(priorityScore, 4);
174
- const priority = priorityScore >= 4 ? "critical" : priorityScore === 3 ? "high" : priorityScore === 2 ? "medium" : "low";
175
- // Generate action
176
- const shortTitle = title.length > 60 ? title.substring(0, 60) + "..." : title;
177
- let action;
178
- if (bestType === "security_alert" && techMatch) {
179
- action = `Review ${shortTitle} - affects your ${techMatch} stack`;
180
- }
181
- else if (bestType === "breaking_change" && techMatch) {
182
- action = `Check migration path - ${techMatch} breaking change`;
183
- }
184
- else if (bestType === "tool_discovery" && techMatch) {
185
- action = `Evaluate for your ${techMatch} workflow: ${shortTitle}`;
186
- }
187
- else {
188
- action = `${PRIORITY_LABELS[bestType] || bestType}: ${shortTitle}`;
189
- }
190
- return {
191
- signalType: bestType,
192
- priority,
193
- confidence: Math.round(bestConfidence * 100) / 100,
194
- action,
195
- triggers: bestTriggers,
196
- };
197
- }
12
+ import { driftAction } from "./install-drift-notes.js";
13
+ import { normalizeStoredPriority } from "./signal-classifier.js";
14
+ // The priority vocabulary lives in signal-classifier.ts; re-exported for existing importers.
15
+ export { normalizeStoredPriority } from "./signal-classifier.js";
16
+ /** Feed sources whose rows are structured facts (an advisory record, a registry release), not prose. */
17
+ const STRUCTURED_SOURCES = new Set(["cve", "osv", "crates_io", "npm_registry", "pypi", "go_modules", "github"]);
18
+ const ADVISORY_SOURCES = new Set(["cve", "osv"]);
198
19
  // ============================================================================
199
20
  // Tool Definition
200
21
  // ============================================================================
@@ -231,68 +52,172 @@ Use this to get prioritized, actionable intelligence from 4DA's feed.`,
231
52
  },
232
53
  since_hours: {
233
54
  type: "number",
234
- description: "Only include items from the last N hours. Default: 48",
55
+ description: "Only include items from the last N hours. Default: 48, max: 720 (30 days)",
235
56
  default: 48,
236
57
  },
237
58
  },
238
59
  },
239
60
  };
61
+ /** Widest window a caller may ask for: 30 days. */
62
+ const MAX_SINCE_HOURS = 720;
63
+ /** Windows past this reach rows the current scoring pipeline may not have re-scored. */
64
+ const DEEP_WINDOW_HOURS = 168;
65
+ /**
66
+ * Group scan entries that are the same vulnerability under different ids.
67
+ *
68
+ * OSV returns a GHSA record and a RUSTSEC (or CVE) record for one bug, each
69
+ * naming the other in `aliases`; injected one-per-record they read as two
70
+ * problems and doubled the advisory count in every briefing. Union-find over
71
+ * `[vulnId, ...aliases]` connects the records transitively (A knows B, B knows
72
+ * C), scoped to the affected (ecosystem, package, version) so one advisory
73
+ * against two packages — or two pinned versions of one package — stays two
74
+ * things to fix. The representative is the GHSA record when the cluster has
75
+ * one (the richest and most stable id), else the first seen; `ids` lists
76
+ * every identifier in first-seen order.
77
+ */
78
+ export function clusterVulnerabilities(vulns) {
79
+ const parent = new Map();
80
+ const find = (key) => {
81
+ let root = key;
82
+ while (parent.get(root) !== root)
83
+ root = parent.get(root);
84
+ // Path compression keeps repeated lookups flat.
85
+ let cursor = key;
86
+ while (parent.get(cursor) !== root) {
87
+ const next = parent.get(cursor);
88
+ parent.set(cursor, root);
89
+ cursor = next;
90
+ }
91
+ return root;
92
+ };
93
+ const union = (a, b) => {
94
+ const ra = find(a);
95
+ const rb = find(b);
96
+ if (ra !== rb)
97
+ parent.set(ra, rb);
98
+ };
99
+ const scopeOf = (v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`;
100
+ const idsOf = (v) => [v.vulnId, ...v.aliases].filter(Boolean);
101
+ for (const v of vulns) {
102
+ const scope = scopeOf(v);
103
+ const keys = idsOf(v).map((id) => `${scope}\0${id}`);
104
+ for (const key of keys)
105
+ if (!parent.has(key))
106
+ parent.set(key, key);
107
+ for (let i = 1; i < keys.length; i++)
108
+ union(keys[0], keys[i]);
109
+ }
110
+ const groups = new Map();
111
+ for (const v of vulns) {
112
+ const root = find(`${scopeOf(v)}\0${v.vulnId}`);
113
+ const members = groups.get(root);
114
+ if (members)
115
+ members.push(v);
116
+ else
117
+ groups.set(root, [v]);
118
+ }
119
+ return [...groups.values()].map((members) => {
120
+ const representative = members.find((m) => m.vulnId.startsWith("GHSA-")) ?? members[0];
121
+ const ids = [...new Set(members.flatMap(idsOf))];
122
+ return { representative, ids };
123
+ });
124
+ }
125
+ /** Latest relevance-judge verdict per item (`llm_judgments`), when the desktop app has judged it. */
126
+ function judgeVerdicts(db, ids) {
127
+ const out = new Map();
128
+ if (ids.length === 0)
129
+ return out;
130
+ try {
131
+ const raw = db.getRawDb();
132
+ for (let i = 0; i < ids.length; i += 500) {
133
+ const chunk = ids.slice(i, i + 500);
134
+ const rows = raw
135
+ .prepare(`SELECT source_item_id AS id, relevance_score AS v FROM llm_judgments
136
+ WHERE id IN (SELECT MAX(id) FROM llm_judgments WHERE source_item_id IN (${chunk.map(() => "?").join(",")}) GROUP BY source_item_id)`)
137
+ .all(...chunk);
138
+ for (const row of rows)
139
+ out.set(row.id, row.v);
140
+ }
141
+ }
142
+ catch {
143
+ // No llm_judgments table (standalone or an older app): no verdicts.
144
+ }
145
+ return out;
146
+ }
147
+ /** What an agent can do with a signal of this type and source. */
148
+ function actionFor(type, source) {
149
+ if (ADVISORY_SOURCES.has(source))
150
+ return "Advisory record: run vulnerability_scan to see whether your installed versions are affected.";
151
+ if (source === "crates_io" || source === "npm_registry" || source === "pypi" || source === "go_modules") {
152
+ return "New release of a dependency: call upgrade_impact for what changed before bumping.";
153
+ }
154
+ if (type === "security_alert")
155
+ return "Security news (not an advisory against your versions): read for context.";
156
+ if (type === "breaking_change")
157
+ return "Possible breaking change: check whether it touches a dependency you use.";
158
+ return "Read for context.";
159
+ }
240
160
  // ============================================================================
241
161
  // Execution
242
162
  // ============================================================================
243
- export function executeGetActionableSignals(db, params) {
163
+ export function executeGetActionableSignals(db, params, liveIntel = getLiveIntelligence()) {
244
164
  const limit = Math.max(1, Math.min(100, params.limit ?? 20));
245
- const sinceHours = Math.max(1, Math.min(168, params.since_hours ?? 48));
246
- // Get items from DB (reuse existing method with low min score to get more items)
247
- // Try progressively wider time windows if no items found
248
- let items = db.getRelevantContent(0.1, undefined, 200, sinceHours);
165
+ const sinceHours = Math.max(1, Math.min(MAX_SINCE_HOURS, params.since_hours ?? 48));
166
+ // Get items from DB (low min score to get more items). A signal_type filter
167
+ // is pushed into the read as well: the general read is capped at the top
168
+ // 200 ranked items across EVERY type, and on a real corpus that cap starved
169
+ // a type-specific pass — live 2026-09-07, the two in-window stored security
170
+ // alerts ranked 299th and 582nd, so a security-only read returned nothing.
171
+ // Stored matches are fetched by type and merged ahead of the general read,
172
+ // which still carries the unstamped rows for keyword classification.
173
+ const fetchItems = (minScore, hours, requireCurrentVersion) => {
174
+ const general = db.getRelevantContent(minScore, undefined, 200, hours, requireCurrentVersion);
175
+ if (!params.signal_type)
176
+ return general;
177
+ const stored = db.getRelevantContent(minScore, undefined, 200, hours, requireCurrentVersion, params.signal_type);
178
+ const seen = new Set(stored.map((item) => item.id));
179
+ return [...stored, ...general.filter((item) => !seen.has(item.id))];
180
+ };
181
+ // Try progressively wider time windows if no items found. A window wider
182
+ // than seven days reaches the stale-epoch tail, so it carries the same
183
+ // current-pipeline-version guard as the deep fallback below.
184
+ let items = fetchItems(0.1, sinceHours, sinceHours > DEEP_WINDOW_HOURS);
249
185
  if (items.length === 0 && sinceHours < 168) {
250
- items = db.getRelevantContent(0.1, undefined, 200, 168); // Try 7 days
186
+ items = fetchItems(0.1, 168, false); // Try 7 days
251
187
  }
252
188
  if (items.length === 0) {
253
189
  // Deep 30-day/any-score fallback reaches the stale-epoch tail after a
254
190
  // pipeline-version bump. This path trusts stored signal_type/signal_priority
255
191
  // verbatim (confidence 0.90 below), so it MUST only see current-version
256
192
  // rows — stale persisted signals are claims the live brain no longer makes.
257
- items = db.getRelevantContent(0.0, undefined, 200, 720, true);
193
+ items = fetchItems(0.0, 720, true);
258
194
  }
259
- // Get user's detected tech for cross-referencing
260
- const context = db.getUserContext(true, false);
261
- const detectedTech = (context.ace?.detected_tech || []).map((t) => t.name);
262
195
  const signals = [];
196
+ const verdicts = judgeVerdicts(db, items.map((item) => item.id));
263
197
  for (const item of items) {
264
- // Prefer pipeline-computed signals from DB over keyword re-classification
265
- if (item.signal_type && item.signal_priority) {
266
- const storedType = item.signal_type;
267
- const storedPriority = item.signal_priority;
268
- // Apply filters
269
- if (params.priority_filter && storedPriority !== params.priority_filter)
270
- continue;
271
- if (params.signal_type && storedType !== params.signal_type)
272
- continue;
273
- signals.push({
274
- id: item.id,
275
- title: item.title,
276
- url: item.url,
277
- source_type: item.source_type,
278
- relevance_score: item.relevance_score,
279
- signal_type: storedType,
280
- signal_priority: storedPriority,
281
- action: `${storedType}: ${item.title.substring(0, 60)}`,
282
- triggers: [],
283
- confidence: 0.90,
284
- discovered_ago: item.discovered_ago,
285
- });
198
+ // Only the pipeline's own classifications, and only when they can be
199
+ // stood behind: a prose item (news, social, papers) appears when the
200
+ // relevance judge accepted it (latest verdict >= 0.5); a structured row
201
+ // (advisory record, registry release) may appear unjudged. Measured
202
+ // 2026-10-01: of 154 items stamped security_alert in 14 days the judge
203
+ // accepted 10, rejected 62 and never saw 82; 3 came from an advisory source.
204
+ if (!item.signal_type || !item.signal_priority)
286
205
  continue;
287
- }
288
- // Fallback: keyword classification for items without pipeline signals
289
- const result = classify(item.title, item.content || "", item.relevance_score, detectedTech);
290
- if (!result)
206
+ const verdict = verdicts.get(item.id);
207
+ const structured = STRUCTURED_SOURCES.has(item.source_type);
208
+ if (verdict !== undefined ? verdict < 0.5 : !structured)
291
209
  continue;
210
+ const storedType = item.signal_type;
211
+ let priority = normalizeStoredPriority(item.signal_priority);
212
+ // A headline about security is not an advisory against your versions:
213
+ // never above medium unless it comes from an advisory database.
214
+ if (storedType === "security_alert" && !ADVISORY_SOURCES.has(item.source_type) && (priority === "critical" || priority === "high")) {
215
+ priority = "medium";
216
+ }
292
217
  // Apply filters
293
- if (params.priority_filter && result.priority !== params.priority_filter)
218
+ if (params.priority_filter && priority !== params.priority_filter)
294
219
  continue;
295
- if (params.signal_type && result.signalType !== params.signal_type)
220
+ if (params.signal_type && storedType !== params.signal_type)
296
221
  continue;
297
222
  signals.push({
298
223
  id: item.id,
@@ -300,39 +225,77 @@ export function executeGetActionableSignals(db, params) {
300
225
  url: item.url,
301
226
  source_type: item.source_type,
302
227
  relevance_score: item.relevance_score,
303
- signal_type: result.signalType,
304
- signal_priority: result.priority,
305
- action: result.action,
306
- triggers: result.triggers,
307
- confidence: result.confidence,
228
+ signal_type: storedType,
229
+ signal_priority: priority,
230
+ action: actionFor(storedType, item.source_type),
231
+ triggers: [],
232
+ // The judge's verdict when there is one; a structured row carries none.
233
+ confidence: verdict ?? (structured ? 0.5 : 0),
308
234
  discovered_ago: item.discovered_ago,
309
235
  });
310
236
  }
311
- // Inject live vulnerability data as security_alert signals
312
- const liveIntel = getLiveIntelligence();
313
- if (liveIntel) {
314
- const vulnResult = liveIntel.getVulnerabilities();
315
- if (vulnResult && vulnResult.vulnerabilities.length > 0) {
316
- for (const vuln of vulnResult.vulnerabilities) {
317
- // Apply filters
318
- const vulnPriority = vuln.severity === "critical" ? "critical" :
319
- vuln.severity === "high" ? "high" : "medium";
320
- if (params.priority_filter && vulnPriority !== params.priority_filter)
321
- continue;
322
- if (params.signal_type && params.signal_type !== "security_alert")
237
+ // Inject live vulnerability data as security_alert signals — one per
238
+ // vulnerability (alias clusters), not one per OSV record.
239
+ const vulnResult = liveIntel?.getVulnerabilities() ?? null;
240
+ if (vulnResult && vulnResult.vulnerabilities.length > 0) {
241
+ if (!params.signal_type || params.signal_type === "security_alert") {
242
+ for (const { representative: vuln, ids } of clusterVulnerabilities(vulnResult.vulnerabilities)) {
243
+ // The grade every surface shares (AD-046; twin of the app's
244
+ // `osv::identity::scope_adjusted_urgency`), not the raw advisory tier:
245
+ // a transitive of unknown scope read "CRITICAL: Sandbox Breakout" here
246
+ // while the app graded it High (sandbox@3.1.2, 2026-09-10).
247
+ const presented = presentedSeverity(vuln);
248
+ // Low stays low (a "LOW: ..." finding was listed at medium); an
249
+ // advisory with no severity at all is medium, not hidden.
250
+ let priority = presented === "critical" ? "critical" :
251
+ presented === "high" ? "high" :
252
+ presented === "low" ? "low" : "medium";
253
+ let relevance = presented === "critical" ? 1.0 : presented === "high" ? 0.9 : presented === "low" ? 0.5 : 0.7;
254
+ // An installed copy the lockfile does not pin is fixed by a reinstall,
255
+ // not an upgrade: the lockfile is already where it should be.
256
+ let action = vuln.installDriftOf
257
+ ? driftAction(vuln)
258
+ : vuln.fixedVersion
259
+ ? `Upgrade ${vuln.package} to ${vuln.fixedVersion}`
260
+ : `Review ${vuln.package} — no fix version published`;
261
+ const adjusted = scopeAdjustmentReason(vuln);
262
+ if (adjusted) {
263
+ action += ` (the advisory rates it ${vuln.severity}; graded ${presented} as a ${adjusted})`;
264
+ }
265
+ let label = presented.toUpperCase();
266
+ // "X is unmaintained" is information to plan around, not a fix to
267
+ // apply today; an advisory against a crate this host never builds is
268
+ // not this host's exposure. Both stay visible at LOW so they cannot
269
+ // outrank, or be mistaken for, a live CVE — and the briefing's
270
+ // delegation verdict does not read them at all.
271
+ const maintenance = isMaintenanceNotice(vuln);
272
+ if (maintenance) {
273
+ priority = "low";
274
+ relevance = 0.3;
275
+ action = "Maintenance notice — no fix to apply";
276
+ label = "MAINTENANCE";
277
+ }
278
+ if (vuln.platformActive === false) {
279
+ priority = "low";
280
+ relevance = 0.3;
281
+ action = `Not built on this host — ${action}`;
282
+ // The title must agree with the priority: "MEDIUM: ... (glib@0.18.5)" at
283
+ // priority low read as a medium issue on a Windows host that never builds glib.
284
+ if (!maintenance)
285
+ label = `NOT BUILT HERE (${presented})`;
286
+ }
287
+ if (params.priority_filter && priority !== params.priority_filter)
323
288
  continue;
324
289
  signals.push({
325
290
  id: -1,
326
- title: `${vuln.severity.toUpperCase()}: ${vuln.summary} (${vuln.package}@${vuln.currentVersion})`,
291
+ title: `${label}: ${vuln.summary} (${vuln.package}@${vuln.currentVersion})`,
327
292
  url: vuln.references[0] || null,
328
293
  source_type: "osv_live",
329
- relevance_score: vuln.severity === "critical" ? 1.0 : vuln.severity === "high" ? 0.9 : 0.7,
294
+ relevance_score: relevance,
330
295
  signal_type: "security_alert",
331
- signal_priority: vulnPriority,
332
- action: vuln.fixedVersion
333
- ? `Upgrade ${vuln.package} to ${vuln.fixedVersion}`
334
- : `Review ${vuln.package} — no fix version published`,
335
- triggers: [vuln.vulnId, ...vuln.aliases].filter(Boolean),
296
+ signal_priority: priority,
297
+ action,
298
+ triggers: ids,
336
299
  confidence: 1.0,
337
300
  discovered_ago: vuln.published ? `published ${vuln.published}` : "recently",
338
301
  });
@@ -37,4 +37,6 @@ export declare function executeGetContext(db: FourDADatabase, params: GetContext
37
37
  ecosystem: string;
38
38
  isDev: boolean;
39
39
  }>;
40
+ resolved_dependencies_total?: number;
41
+ resolved_dependencies_note?: string;
40
42
  };
@@ -45,16 +45,24 @@ export function executeGetContext(db, params) {
45
45
  if (liveIntel) {
46
46
  const deps = liveIntel.getResolvedDeps();
47
47
  if (deps.length > 0) {
48
+ // Bounded: the full direct-dependency list ran this answer to ~30 KB
49
+ // on a 150-dependency project. vulnerability_scan / dependency_health
50
+ // are the tools for the whole list.
48
51
  return {
49
52
  ...context,
50
- resolved_dependencies: deps.map((d) => ({
53
+ resolved_dependencies: deps.slice(0, MAX_LISTED_DEPENDENCIES).map((d) => ({
51
54
  name: d.name,
52
55
  version: d.version,
53
56
  ecosystem: d.ecosystem,
54
57
  isDev: d.isDev,
55
58
  })),
59
+ resolved_dependencies_total: deps.length,
60
+ ...(deps.length > MAX_LISTED_DEPENDENCIES
61
+ ? { resolved_dependencies_note: `showing ${MAX_LISTED_DEPENDENCIES} of ${deps.length}; dependency_health lists them all` }
62
+ : {}),
56
63
  };
57
64
  }
58
65
  }
59
66
  return context;
60
67
  }
68
+ const MAX_LISTED_DEPENDENCIES = 40;
@@ -40,4 +40,6 @@ export declare const getRelevantContentTool: {
40
40
  /**
41
41
  * Execute the get_relevant_content tool
42
42
  */
43
- export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): RelevantItem[];
43
+ export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): Array<RelevantItem & {
44
+ judge_relevance: number | null;
45
+ }>;
@@ -30,8 +30,13 @@ Use this to find content relevant to the user's current context.`,
30
30
  },
31
31
  source_type: {
32
32
  type: "string",
33
- description: 'Filter by source type: "hackernews", "arxiv", or "reddit". Leave empty for all sources.',
34
- enum: ["hackernews", "arxiv", "reddit"],
33
+ description: "Only items from this source. Leave empty for all sources.",
34
+ // Every source the app fetches; the enum used to list three of them.
35
+ enum: [
36
+ "hackernews", "reddit", "lobsters", "devto", "arxiv", "huggingface", "papers_with_code",
37
+ "github", "crates_io", "npm_registry", "pypi", "go_modules", "cve", "osv", "stackoverflow",
38
+ "rss", "youtube", "mastodon", "lemmy", "bluesky", "twitter", "producthunt",
39
+ ],
35
40
  },
36
41
  limit: {
37
42
  type: "number",
@@ -64,5 +69,31 @@ export function executeGetRelevantContent(db, params) {
64
69
  // numbers the live scoring brain doesn't stand behind.
65
70
  items = db.getRelevantContent(0.0, params.source_type, limit, 720, true);
66
71
  }
67
- return items;
72
+ // Items the relevance judge rejected are not "relevant content", whatever
73
+ // their score: measured 2026-10-01, 151 of 950 feed-relevant items carried
74
+ // a latest judge verdict below 0.5. Unjudged items stay (scored, not yet
75
+ // judged) and carry `judge_relevance: null`.
76
+ const verdicts = latestJudgeVerdicts(db, items.map((item) => item.id));
77
+ return items
78
+ .filter((item) => (verdicts.get(item.id) ?? 1) >= 0.5)
79
+ .map((item) => ({ ...item, judge_relevance: verdicts.get(item.id) ?? null }));
80
+ }
81
+ /** Latest relevance-judge verdict per item, when the desktop app has judged it. */
82
+ function latestJudgeVerdicts(db, ids) {
83
+ const out = new Map();
84
+ if (ids.length === 0)
85
+ return out;
86
+ try {
87
+ const rows = db
88
+ .getRawDb()
89
+ .prepare(`SELECT source_item_id AS id, relevance_score AS v FROM llm_judgments
90
+ WHERE id IN (SELECT MAX(id) FROM llm_judgments WHERE source_item_id IN (${ids.map(() => "?").join(",")}) GROUP BY source_item_id)`)
91
+ .all(...ids);
92
+ for (const row of rows)
93
+ out.set(row.id, Math.round(row.v * 100) / 100);
94
+ }
95
+ catch {
96
+ // No llm_judgments table: nothing judged.
97
+ }
98
+ return out;
68
99
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Tool exports for 4DA MCP Server — 14 tools across 5 categories
2
+ * Tool exports for 4DA MCP Server — 16 tools across 5 categories
3
3
  *
4
- * Security (3) — vulnerability scanning, dependency health, upgrade planning
4
+ * Security (5) — vulnerability scanning, dependency health, upgrade planning, upgrade impact, dependency check
5
5
  * Intelligence (6) — briefing, ecosystem news, context, content feed, signals, knowledge gaps, feedback
6
6
  * Decisions (2) — decision memory, alignment checking
7
7
  * Agent (1) — cross-session persistent memory
@@ -10,6 +10,8 @@
10
10
  export { vulnerabilityScanTool, executeVulnerabilityScan, } from "./vulnerability-scan.js";
11
11
  export { dependencyHealthTool, executeDependencyHealth, } from "./dependency-health.js";
12
12
  export { upgradePlannerTool, executeUpgradePlanner, } from "./upgrade-planner.js";
13
+ export { dependencyCheckTool, executeDependencyCheck, } from "./dependency-check.js";
14
+ export { upgradeImpactTool, executeUpgradeImpact, } from "./upgrade-impact.js";
13
15
  export { whatShouldIKnowTool, executeWhatShouldIKnow, } from "./what-should-i-know.js";
14
16
  export { ecosystemPulseTool, executeEcosystemPulse, } from "./ecosystem-pulse.js";
15
17
  export { getContextTool, executeGetContext } from "./get-context.js";
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
- * Tool exports for 4DA MCP Server — 14 tools across 5 categories
3
+ * Tool exports for 4DA MCP Server — 16 tools across 5 categories
4
4
  *
5
- * Security (3) — vulnerability scanning, dependency health, upgrade planning
5
+ * Security (5) — vulnerability scanning, dependency health, upgrade planning, upgrade impact, dependency check
6
6
  * Intelligence (6) — briefing, ecosystem news, context, content feed, signals, knowledge gaps, feedback
7
7
  * Decisions (2) — decision memory, alignment checking
8
8
  * Agent (1) — cross-session persistent memory
@@ -12,6 +12,8 @@
12
12
  export { vulnerabilityScanTool, executeVulnerabilityScan, } from "./vulnerability-scan.js";
13
13
  export { dependencyHealthTool, executeDependencyHealth, } from "./dependency-health.js";
14
14
  export { upgradePlannerTool, executeUpgradePlanner, } from "./upgrade-planner.js";
15
+ export { dependencyCheckTool, executeDependencyCheck, } from "./dependency-check.js";
16
+ export { upgradeImpactTool, executeUpgradeImpact, } from "./upgrade-impact.js";
15
17
  // Intelligence
16
18
  export { whatShouldIKnowTool, executeWhatShouldIKnow, } from "./what-should-i-know.js";
17
19
  export { ecosystemPulseTool, executeEcosystemPulse, } from "./ecosystem-pulse.js";