@4da/mcp-server 5.0.2 → 5.0.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 (96) hide show
  1. package/README.md +3 -3
  2. package/dist/auth-context.d.ts +0 -1
  3. package/dist/auth-context.js +0 -1
  4. package/dist/auth.d.ts +0 -1
  5. package/dist/auth.js +0 -1
  6. package/dist/db.d.ts +17 -2
  7. package/dist/db.js +104 -67
  8. package/dist/doctor.d.ts +0 -1
  9. package/dist/doctor.js +17 -15
  10. package/dist/embeddings.d.ts +0 -1
  11. package/dist/embeddings.js +0 -1
  12. package/dist/http-transport.d.ts +0 -1
  13. package/dist/http-transport.js +0 -1
  14. package/dist/index.d.ts +0 -1
  15. package/dist/index.js +17 -5
  16. package/dist/live/cache.d.ts +0 -1
  17. package/dist/live/cache.js +0 -1
  18. package/dist/live/crates-registry.d.ts +0 -1
  19. package/dist/live/crates-registry.js +5 -8
  20. package/dist/live/cvss.d.ts +0 -1
  21. package/dist/live/cvss.js +0 -1
  22. package/dist/live/go-registry.d.ts +0 -1
  23. package/dist/live/go-registry.js +4 -9
  24. package/dist/live/hn-fetcher.d.ts +0 -1
  25. package/dist/live/hn-fetcher.js +23 -11
  26. package/dist/live/http-utils.d.ts +0 -1
  27. package/dist/live/http-utils.js +0 -1
  28. package/dist/live/index.d.ts +0 -1
  29. package/dist/live/index.js +0 -1
  30. package/dist/live/npm-registry.d.ts +0 -1
  31. package/dist/live/npm-registry.js +4 -8
  32. package/dist/live/osv-scanner.d.ts +20 -2
  33. package/dist/live/osv-scanner.js +82 -9
  34. package/dist/live/platform.d.ts +0 -1
  35. package/dist/live/platform.js +0 -1
  36. package/dist/live/pypi-registry.d.ts +0 -1
  37. package/dist/live/pypi-registry.js +7 -10
  38. package/dist/live/rate-limiter.d.ts +0 -1
  39. package/dist/live/rate-limiter.js +0 -1
  40. package/dist/live/semver-utils.d.ts +11 -1
  41. package/dist/live/semver-utils.js +14 -1
  42. package/dist/live/types.d.ts +0 -1
  43. package/dist/live/types.js +0 -1
  44. package/dist/live/version-resolver.d.ts +0 -1
  45. package/dist/live/version-resolver.js +0 -1
  46. package/dist/live-singleton.d.ts +0 -1
  47. package/dist/live-singleton.js +0 -1
  48. package/dist/project-scanner.d.ts +0 -1
  49. package/dist/project-scanner.js +0 -1
  50. package/dist/schema-registry.d.ts +17 -8
  51. package/dist/schema-registry.js +54 -9
  52. package/dist/schemas/developer-dna.json +1 -1
  53. package/dist/schemas/get-context.json +2 -2
  54. package/dist/schemas/get-relevant-content.json +1 -1
  55. package/dist/schemas/record-feedback.json +1 -1
  56. package/dist/setup.d.ts +0 -1
  57. package/dist/setup.js +0 -1
  58. package/dist/tool-dispatch.d.ts +0 -1
  59. package/dist/tool-dispatch.js +21 -2
  60. package/dist/tools/agent-memory.d.ts +0 -1
  61. package/dist/tools/agent-memory.js +0 -1
  62. package/dist/tools/decision-enforcement.d.ts +0 -1
  63. package/dist/tools/decision-enforcement.js +0 -1
  64. package/dist/tools/decision-memory.d.ts +0 -1
  65. package/dist/tools/decision-memory.js +0 -1
  66. package/dist/tools/decision-recall.d.ts +0 -1
  67. package/dist/tools/decision-recall.js +0 -1
  68. package/dist/tools/dependency-health.d.ts +0 -1
  69. package/dist/tools/dependency-health.js +31 -5
  70. package/dist/tools/developer-dna.d.ts +3 -3
  71. package/dist/tools/developer-dna.js +63 -47
  72. package/dist/tools/ecosystem-pulse.d.ts +7 -2
  73. package/dist/tools/ecosystem-pulse.js +39 -3
  74. package/dist/tools/get-actionable-signals.d.ts +0 -1
  75. package/dist/tools/get-actionable-signals.js +0 -1
  76. package/dist/tools/get-context.d.ts +0 -1
  77. package/dist/tools/get-context.js +2 -3
  78. package/dist/tools/get-relevant-content.d.ts +0 -1
  79. package/dist/tools/get-relevant-content.js +1 -3
  80. package/dist/tools/index.d.ts +0 -1
  81. package/dist/tools/index.js +0 -1
  82. package/dist/tools/knowledge-gaps.d.ts +0 -1
  83. package/dist/tools/knowledge-gaps.js +79 -17
  84. package/dist/tools/recall.d.ts +0 -1
  85. package/dist/tools/recall.js +0 -1
  86. package/dist/tools/record-feedback.d.ts +1 -2
  87. package/dist/tools/record-feedback.js +7 -8
  88. package/dist/tools/upgrade-planner.d.ts +0 -1
  89. package/dist/tools/upgrade-planner.js +15 -3
  90. package/dist/tools/vulnerability-scan.d.ts +0 -1
  91. package/dist/tools/vulnerability-scan.js +0 -1
  92. package/dist/tools/what-should-i-know.d.ts +0 -1
  93. package/dist/tools/what-should-i-know.js +6 -3
  94. package/dist/types.d.ts +2 -3
  95. package/dist/types.js +0 -1
  96. package/package.json +37 -11
@@ -210,4 +210,3 @@ export function executeCheckDecisionAlignment(db, params) {
210
210
  // Provider configured -> hybrid retrieval (returns a Promise; dispatch awaits).
211
211
  return hybridDecisionRecall(db, query, candidates, 50, config).then(({ ranked, semanticById, recall_mode }) => assembleAlignment(params, ranked, semanticById, recall_mode));
212
212
  }
213
- //# sourceMappingURL=decision-enforcement.js.map
@@ -127,4 +127,3 @@ export declare const decisionMemoryTool: {
127
127
  };
128
128
  };
129
129
  export declare function executeDecisionMemory(db: FourDADatabase, params: DecisionMemoryParams): object | Promise<object>;
130
- //# sourceMappingURL=decision-memory.d.ts.map
@@ -293,4 +293,3 @@ export function executeDecisionMemory(db, params) {
293
293
  return { error: `Unknown action: ${params.action}` };
294
294
  }
295
295
  }
296
- //# sourceMappingURL=decision-memory.js.map
@@ -56,4 +56,3 @@ export declare function decisionEmbedText(row: DecisionLike): string;
56
56
  * be embedded.
57
57
  */
58
58
  export declare function hybridDecisionRecall<T extends DecisionLike>(db: FourDADatabase, query: string, rows: T[], limit: number, config: EmbeddingConfig): Promise<DecisionRecallResult<T>>;
59
- //# sourceMappingURL=decision-recall.d.ts.map
@@ -84,4 +84,3 @@ export async function hybridDecisionRecall(db, query, rows, limit, config) {
84
84
  .map((x) => x.row);
85
85
  return { ranked, semanticById: sem.semanticById, recall_mode: "hybrid" };
86
86
  }
87
- //# sourceMappingURL=decision-recall.js.map
@@ -42,4 +42,3 @@ export declare const dependencyHealthTool: {
42
42
  };
43
43
  };
44
44
  export declare function executeDependencyHealth(_db: FourDADatabase, params: DependencyHealthParams, liveIntel: LiveIntelligence | null): Promise<DependencyHealthResult>;
45
- //# sourceMappingURL=dependency-health.d.ts.map
@@ -112,11 +112,38 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
112
112
  });
113
113
  const limit = params.limit ?? 50;
114
114
  const limited = sorted.slice(0, limit);
115
- // Health score: 100 = perfect, 0 = everything is on fire
115
+ // Health score: 100 = perfect. Proportional and severity-weighted — the old
116
+ // absolute-count formula (15 points per vulnerable package, 2 per outdated,
117
+ // regardless of severity, degree, or project size) pinned any real-sized
118
+ // project at 0/100: a patch-behind dep cost the same as a critical CVE and
119
+ // the number informed nothing.
116
120
  const total = registryData.length || 1;
117
- const vulnPenalty = Math.min(vulnerable * 15, 50);
118
- const deprecatedPenalty = Math.min(deprecated * 10, 30);
119
- const outdatedPenalty = Math.min(outdated * 2, 40);
121
+ // Vulnerabilities: severity-weighted; transitive findings at one-third
122
+ // weight (their fix arrives via a parent bump — the direct surface is what
123
+ // the user controls directly).
124
+ const sevWeight = { critical: 20, high: 10, medium: 4, low: 1 };
125
+ let vulnPenaltyRaw = 0;
126
+ if (vulnResult) {
127
+ for (const v of vulnResult.vulnerabilities) {
128
+ const w = sevWeight[v.severity] ?? 2; // "unknown" (mostly unmaintained-notices) counts low
129
+ vulnPenaltyRaw += v.isDirect ? w : w / 3;
130
+ }
131
+ }
132
+ const vulnPenalty = Math.min(Math.round(vulnPenaltyRaw), 55);
133
+ // Staleness: degree-weighted and proportional to the tracked set — a major
134
+ // behind is a real liability, a patch behind is routine drift.
135
+ let stalenessWeight = 0;
136
+ for (const dep of registryData) {
137
+ const label = dep.versionsBehind?.label;
138
+ if (label === "major")
139
+ stalenessWeight += 1;
140
+ else if (label === "minor")
141
+ stalenessWeight += 0.35;
142
+ else if (label === "patch")
143
+ stalenessWeight += 0.1;
144
+ }
145
+ const outdatedPenalty = Math.min(30, Math.round((stalenessWeight / total) * 60));
146
+ const deprecatedPenalty = Math.min(deprecated * 8, 15);
120
147
  const healthScore = Math.max(0, 100 - vulnPenalty - deprecatedPenalty - outdatedPenalty);
121
148
  const vulnSummary = vulnResult ? {
122
149
  critical: vulnResult.bySeverity.critical,
@@ -151,4 +178,3 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
151
178
  cached: false,
152
179
  };
153
180
  }
154
- //# sourceMappingURL=dependency-health.js.map
@@ -2,7 +2,7 @@
2
2
  * developer_dna tool
3
3
  *
4
4
  * Export a comprehensive Developer DNA profile built from project scans,
5
- * engagement data, and learned preferences.
5
+ * explicit interests, and local interaction history.
6
6
  */
7
7
  import type { FourDADatabase } from "../db.js";
8
8
  export interface DeveloperDnaParams {
@@ -46,7 +46,8 @@ interface EngagedTopic {
46
46
  interface BlindSpotEntry {
47
47
  dependency: string;
48
48
  severity: string;
49
- days_stale: number;
49
+ /** Days since last engagement with content about this dep; null = never engaged. */
50
+ days_stale: number | null;
50
51
  }
51
52
  interface SourceEngagement {
52
53
  source_type: string;
@@ -76,4 +77,3 @@ interface DeveloperDna {
76
77
  }
77
78
  export declare function executeDeveloperDna(db: FourDADatabase, params: DeveloperDnaParams): DeveloperDna;
78
79
  export {};
79
- //# sourceMappingURL=developer-dna.d.ts.map
@@ -3,14 +3,14 @@
3
3
  * developer_dna tool
4
4
  *
5
5
  * Export a comprehensive Developer DNA profile built from project scans,
6
- * engagement data, and learned preferences.
6
+ * explicit interests, and local interaction history.
7
7
  */
8
8
  // =============================================================================
9
9
  // Tool Definition
10
10
  // =============================================================================
11
11
  export const developerDnaTool = {
12
12
  name: "developer_dna",
13
- description: `Export your Developer DNA — a comprehensive profile of your tech identity built from project scans, engagement data, and learned preferences. Includes primary stack, adjacent tech, top dependencies, engaged topics, blind spots, source engagement rates, and aggregate stats.`,
13
+ description: `Export your Developer DNA — a comprehensive profile of your tech identity built from project scans, explicit interests, and local interaction history. Includes primary stack, adjacent tech, top dependencies, engaged topics, blind spots, source engagement rates, and aggregate stats.`,
14
14
  inputSchema: {
15
15
  type: "object",
16
16
  properties: {
@@ -135,24 +135,40 @@ export function executeDeveloperDna(db, params) {
135
135
  // table may not exist
136
136
  }
137
137
  // -------------------------------------------------------------------------
138
- // 5. Top Engaged Topics — from topic_affinities
138
+ // 5. Top Engaged Topics — from explicit interactions (topic_affinities was
139
+ // dropped with the implicit-capture system in desktop schema 105; querying
140
+ // it returned empty forever).
139
141
  // -------------------------------------------------------------------------
140
142
  let topEngagedTopics = [];
141
143
  try {
142
- const affinityRows = rawDb
143
- .prepare(`SELECT topic, positive_signals, negative_signals, total_exposures
144
- FROM topic_affinities
145
- WHERE positive_signals > 0
146
- ORDER BY positive_signals DESC
147
- LIMIT ?`)
148
- .all(maxTopics);
149
- const totalPositive = affinityRows.reduce((sum, r) => sum + r.positive_signals, 0);
150
- topEngagedTopics = affinityRows.map((r) => ({
151
- topic: r.topic,
152
- interactions: r.positive_signals,
153
- percent_of_total: totalPositive > 0
154
- ? Math.round((r.positive_signals / totalPositive) * 10000) / 100
155
- : 0,
144
+ const interactionRows = rawDb
145
+ .prepare(`SELECT item_topics FROM interactions
146
+ WHERE action_type IN ('click', 'save') AND item_topics IS NOT NULL`)
147
+ .all();
148
+ const topicCounts = new Map();
149
+ for (const row of interactionRows) {
150
+ try {
151
+ const topics = JSON.parse(row.item_topics);
152
+ if (!Array.isArray(topics))
153
+ continue;
154
+ for (const t of topics) {
155
+ if (typeof t === "string" && t.length > 0) {
156
+ topicCounts.set(t, (topicCounts.get(t) || 0) + 1);
157
+ }
158
+ }
159
+ }
160
+ catch {
161
+ // Malformed topic JSON on one row — skip it
162
+ }
163
+ }
164
+ const totalPositive = [...topicCounts.values()].reduce((a, b) => a + b, 0);
165
+ topEngagedTopics = [...topicCounts.entries()]
166
+ .sort((a, b) => b[1] - a[1])
167
+ .slice(0, maxTopics)
168
+ .map(([topic, count]) => ({
169
+ topic,
170
+ interactions: count,
171
+ percent_of_total: totalPositive > 0 ? Math.round((count / totalPositive) * 10000) / 100 : 0,
156
172
  }));
157
173
  }
158
174
  catch {
@@ -175,25 +191,9 @@ export function executeDeveloperDna(db, params) {
175
191
  const now = Date.now();
176
192
  for (const dep of topDependencies) {
177
193
  const depLower = dep.name.toLowerCase();
178
- // Check if any interaction item's source mentions this dependency
179
- // Use a simpler heuristic: check if the dependency name appears in
180
- // topic_affinities with positive signals
181
- let lastEngagement = null;
182
- try {
183
- const affinityRow = rawDb
184
- .prepare(`SELECT topic, positive_signals
185
- FROM topic_affinities
186
- WHERE LOWER(topic) = ?
187
- AND positive_signals > 0`)
188
- .get(depLower);
189
- if (affinityRow) {
190
- continue; // Has engagement, not a blind spot
191
- }
192
- }
193
- catch {
194
- // Ignore
195
- }
196
194
  // Check for any source_items mentioning this dep that have interactions
195
+ // (the old topic_affinities shortcut queried a table dropped in schema
196
+ // 105 — it never fired).
197
197
  try {
198
198
  // Escape LIKE wildcards so a name containing % or _ matches literally.
199
199
  const likeDep = `%${depLower.replace(/[\\%_]/g, (c) => "\\" + c)}%`;
@@ -218,11 +218,12 @@ export function executeDeveloperDna(db, params) {
218
218
  }
219
219
  }
220
220
  else {
221
- // Never interacted with content about this dependency
221
+ // Never interacted with content about this dependency — null, not
222
+ // a -1 sentinel leaking into user-facing output.
222
223
  blindSpots.push({
223
224
  dependency: dep.name,
224
225
  severity: "low",
225
- days_stale: -1, // indicates "never engaged"
226
+ days_stale: null,
226
227
  });
227
228
  }
228
229
  }
@@ -294,14 +295,28 @@ export function executeDeveloperDna(db, params) {
294
295
  catch {
295
296
  // table may not exist
296
297
  }
298
+ // total_relevant / rejection_rate describe the PIPELINE's judgment, so they
299
+ // must come from its persisted verdicts — the old interaction-based count
300
+ // reported total_relevant: 0 / rejection_rate: 1.0 for any passive user,
301
+ // contradicting a corpus with hundreds of feed-relevant items.
302
+ let judgedCount = 0;
297
303
  try {
298
- // Items with at least one positive interaction = "relevant"
299
- const relevantRow = rawDb
300
- .prepare(`SELECT COUNT(DISTINCT item_id) as cnt
301
- FROM interactions
302
- WHERE action_type IN ('click', 'save')`)
303
- .get();
304
- totalRelevant = relevantRow.cnt;
304
+ if (db.hasColumn("source_items", "feed_relevant")) {
305
+ const row = rawDb
306
+ .prepare(`SELECT COUNT(*) as judged, SUM(CASE WHEN feed_relevant = 1 THEN 1 ELSE 0 END) as relevant
307
+ FROM source_items WHERE feed_relevant IS NOT NULL`)
308
+ .get();
309
+ judgedCount = row.judged;
310
+ totalRelevant = row.relevant ?? 0;
311
+ }
312
+ else if (db.hasColumn("source_items", "relevance_score")) {
313
+ const row = rawDb
314
+ .prepare(`SELECT COUNT(*) as judged, SUM(CASE WHEN relevance_score >= 0.4 THEN 1 ELSE 0 END) as relevant
315
+ FROM source_items WHERE relevance_score IS NOT NULL`)
316
+ .get();
317
+ judgedCount = row.judged;
318
+ totalRelevant = row.relevant ?? 0;
319
+ }
305
320
  }
306
321
  catch {
307
322
  // table may not exist
@@ -336,8 +351,10 @@ export function executeDeveloperDna(db, params) {
336
351
  catch {
337
352
  // table may not exist
338
353
  }
339
- const rejectionRate = totalItemsProcessed > 0
340
- ? Math.round(((totalItemsProcessed - totalRelevant) / totalItemsProcessed) * 10000) / 10000
354
+ // Rejection rate over the JUDGED set — unjudged items are "not yet curated",
355
+ // not rejected.
356
+ const rejectionRate = judgedCount > 0
357
+ ? Math.round(((judgedCount - totalRelevant) / judgedCount) * 10000) / 10000
341
358
  : 0;
342
359
  const stats = {
343
360
  total_items_processed: totalItemsProcessed,
@@ -406,4 +423,3 @@ function buildIdentitySummary(primaryStack, adjacentTech, interests, engagedTopi
406
423
  }
407
424
  return parts.join(". ") + ".";
408
425
  }
409
- //# sourceMappingURL=developer-dna.js.map
@@ -43,6 +43,11 @@ export declare const ecosystemPulseTool: {
43
43
  };
44
44
  };
45
45
  };
46
- export declare function executeEcosystemPulse(_db: FourDADatabase, params: EcosystemPulseParams, liveIntel: LiveIntelligence | null): EcosystemPulseResult;
46
+ /**
47
+ * Derive the tech stack used to filter HN headlines from the database:
48
+ * explicit tech_stack entries plus the languages of tracked dependencies.
49
+ * Works on both the desktop DB and the standalone minimal schema.
50
+ */
51
+ export declare function deriveTechStackForHeadlines(db: FourDADatabase): string[];
52
+ export declare function executeEcosystemPulse(db: FourDADatabase, params: EcosystemPulseParams, liveIntel: LiveIntelligence | null): Promise<EcosystemPulseResult>;
47
53
  export {};
48
- //# sourceMappingURL=ecosystem-pulse.d.ts.map
@@ -23,7 +23,35 @@ export const ecosystemPulseTool = {
23
23
  },
24
24
  },
25
25
  };
26
- export function executeEcosystemPulse(_db, params, liveIntel) {
26
+ /**
27
+ * Derive the tech stack used to filter HN headlines from the database:
28
+ * explicit tech_stack entries plus the languages of tracked dependencies.
29
+ * Works on both the desktop DB and the standalone minimal schema.
30
+ */
31
+ export function deriveTechStackForHeadlines(db) {
32
+ const stack = new Set();
33
+ const rawDb = db.getRawDb();
34
+ try {
35
+ for (const row of rawDb.prepare("SELECT technology FROM tech_stack").all()) {
36
+ if (row.technology)
37
+ stack.add(row.technology.toLowerCase());
38
+ }
39
+ }
40
+ catch {
41
+ // tech_stack may not exist on exotic DBs — languages below still apply.
42
+ }
43
+ try {
44
+ for (const row of rawDb.prepare("SELECT DISTINCT language FROM project_dependencies").all()) {
45
+ if (row.language)
46
+ stack.add(row.language.toLowerCase());
47
+ }
48
+ }
49
+ catch {
50
+ // project_dependencies may not exist — an empty stack yields an honest empty result.
51
+ }
52
+ return [...stack];
53
+ }
54
+ export async function executeEcosystemPulse(db, params, liveIntel) {
27
55
  if (!liveIntel) {
28
56
  return {
29
57
  headlines: [],
@@ -32,7 +60,16 @@ export function executeEcosystemPulse(_db, params, liveIntel) {
32
60
  note: "Live intelligence not available. Set FOURDA_OFFLINE=false and restart.",
33
61
  };
34
62
  }
35
- const headlines = liveIntel.getHeadlines();
63
+ // The startup prefetch only ever ran in standalone mode, so full-DB servers
64
+ // returned an empty cache forever. Fetch on demand when the cache is empty —
65
+ // the cache still serves warm repeat calls.
66
+ let headlines = liveIntel.getHeadlines();
67
+ if (headlines.length === 0 && liveIntel.isEnabled()) {
68
+ const techStack = deriveTechStackForHeadlines(db);
69
+ if (techStack.length > 0) {
70
+ headlines = await liveIntel.fetchHeadlines(techStack);
71
+ }
72
+ }
36
73
  const minPoints = params.min_points ?? 0;
37
74
  const limit = params.limit ?? 15;
38
75
  const filtered = headlines
@@ -57,4 +94,3 @@ export function executeEcosystemPulse(_db, params, liveIntel) {
57
94
  : `${filtered.length} headline${filtered.length !== 1 ? "s" : ""} relevant to your tech stack.`,
58
95
  };
59
96
  }
60
- //# sourceMappingURL=ecosystem-pulse.js.map
@@ -62,4 +62,3 @@ export declare function executeGetActionableSignals(db: FourDADatabase, params:
62
62
  summary: Record<string, number>;
63
63
  };
64
64
  export {};
65
- //# sourceMappingURL=get-actionable-signals.d.ts.map
@@ -359,4 +359,3 @@ export function executeGetActionableSignals(db, params) {
359
359
  ...(signals.length === 0 && items.length === 0 ? { note: "No source items in the database. Run the 4DA desktop app to fetch content, or use vulnerability_scan to check dependencies directly." } : {}),
360
360
  };
361
361
  }
362
- //# sourceMappingURL=get-actionable-signals.js.map
@@ -38,4 +38,3 @@ export declare function executeGetContext(db: FourDADatabase, params: GetContext
38
38
  isDev: boolean;
39
39
  }>;
40
40
  };
41
- //# sourceMappingURL=get-context.d.ts.map
@@ -15,7 +15,7 @@ export const getContextTool = {
15
15
  Returns information about:
16
16
  - Static Identity: User-declared role, tech stack, domains, interests, and exclusions
17
17
  - ACE Context (optional): Auto-detected technologies and active topics from recent activity
18
- - Learned Preferences (optional): Topic affinities learned from user behavior
18
+ - Learned Preferences (optional): retained compatibility field; implicit preference learning no longer contributes to scoring
19
19
 
20
20
  Use this to understand what the user is working on and interested in.`,
21
21
  inputSchema: {
@@ -28,7 +28,7 @@ Use this to understand what the user is working on and interested in.`,
28
28
  },
29
29
  include_learned: {
30
30
  type: "boolean",
31
- description: "Include learned preferences (topic affinities, anti-topics). Default: true",
31
+ description: "Include the retained learned-preferences compatibility field. Default: true",
32
32
  default: true,
33
33
  },
34
34
  },
@@ -58,4 +58,3 @@ export function executeGetContext(db, params) {
58
58
  }
59
59
  return context;
60
60
  }
61
- //# sourceMappingURL=get-context.js.map
@@ -41,4 +41,3 @@ export declare const getRelevantContentTool: {
41
41
  * Execute the get_relevant_content tool
42
42
  */
43
43
  export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): RelevantItem[];
44
- //# sourceMappingURL=get-relevant-content.d.ts.map
@@ -11,12 +11,11 @@ export const getRelevantContentTool = {
11
11
  name: "get_relevant_content",
12
12
  description: `Get filtered relevant content from 4DA's personalized feed.
13
13
 
14
- Returns content items that match the user's interests, tech stack, and learned preferences.
14
+ Returns content items that match the user's explicit interests, tech stack, and ACE-detected context.
15
15
  Items are scored based on:
16
16
  - Explicit interests declared by the user
17
17
  - Tech stack and domains
18
18
  - ACE-detected context (recent files, projects, git activity)
19
- - Learned preferences from past interactions
20
19
 
21
20
  Each item includes necessity fields (necessity_score, necessity_reason, necessity_category, necessity_urgency) indicating how critical the item is — e.g., security vulnerabilities affecting your deps, breaking changes in your stack, or deprecation notices. These are populated from the full PASIFA analysis pipeline when available.
22
21
 
@@ -67,4 +66,3 @@ export function executeGetRelevantContent(db, params) {
67
66
  }
68
67
  return items;
69
68
  }
70
- //# sourceMappingURL=get-relevant-content.js.map
@@ -21,4 +21,3 @@ export { decisionMemoryTool, executeDecisionMemory, } from "./decision-memory.js
21
21
  export { checkDecisionAlignmentTool, executeCheckDecisionAlignment, } from "./decision-enforcement.js";
22
22
  export { agentMemoryTool, executeAgentMemory, } from "./agent-memory.js";
23
23
  export { developerDnaTool, executeDeveloperDna, } from "./developer-dna.js";
24
- //# sourceMappingURL=index.d.ts.map
@@ -27,4 +27,3 @@ export { checkDecisionAlignmentTool, executeCheckDecisionAlignment, } from "./de
27
27
  export { agentMemoryTool, executeAgentMemory, } from "./agent-memory.js";
28
28
  // Identity
29
29
  export { developerDnaTool, executeDeveloperDna, } from "./developer-dna.js";
30
- //# sourceMappingURL=index.js.map
@@ -51,4 +51,3 @@ export declare function executeKnowledgeGaps(db: FourDADatabase, params: Knowled
51
51
  gaps_returned: number;
52
52
  summary: string;
53
53
  };
54
- //# sourceMappingURL=knowledge-gaps.d.ts.map
@@ -6,7 +6,18 @@
6
6
  */
7
7
  // Word-boundary matching prevents "cve" matching inside "achieve", "receiver", etc.
8
8
  function hasWordBoundary(text, term) {
9
- const regex = new RegExp(`\\b${term}\\b`, 'i');
9
+ const regex = new RegExp(`\\b${escapeRegExp(term)}\\b`, 'i');
10
+ return regex.test(text);
11
+ }
12
+ // Package names can contain regex metacharacters (@scope/name, c++, next.js).
13
+ function escapeRegExp(s) {
14
+ return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
15
+ }
16
+ // True when `text` mentions the package name as a whole word. Word characters
17
+ // for this purpose include - and _ (so dep "hono" does NOT match "hono-shim",
18
+ // and never matches "in HONOr of"). @scope/name matches as the full literal.
19
+ function mentionsPackage(text, pkg) {
20
+ const regex = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(pkg)}($|[^A-Za-z0-9_-])`, "i");
10
21
  return regex.test(text);
11
22
  }
12
23
  // Escape SQL LIKE wildcards so a package name containing % or _ (npm names may
@@ -36,9 +47,16 @@ export const knowledgeGapsTool = {
36
47
  };
37
48
  export function executeKnowledgeGaps(db, params) {
38
49
  const rawDb = db.getRawDb();
39
- // Get all tracked dependencies
50
+ // Feature-detect optional columns: a pure-standalone database has no scoring
51
+ // pipeline (no relevance_score) and may predate content_type — the tool must
52
+ // degrade to word-boundary + recency grounding there, never throw.
53
+ const hasRelevance = db.hasColumn("source_items", "relevance_score");
54
+ const hasContentType = db.hasColumn("source_items", "content_type");
55
+ const hasIsDirect = db.hasColumn("project_dependencies", "is_direct");
56
+ // Direct dependencies only — a transitive dep's news is not the user's
57
+ // reading backlog. Dev deps stay in: a vitest or eslint advisory is real.
40
58
  const deps = rawDb
41
- .prepare("SELECT package_name, version, project_path, language FROM project_dependencies LIMIT 100")
59
+ .prepare(`SELECT package_name, version, project_path, language FROM project_dependencies ${hasIsDirect ? "WHERE is_direct = 1 " : ""}LIMIT 100`)
42
60
  .all();
43
61
  if (deps.length === 0) {
44
62
  return {
@@ -47,41 +65,86 @@ export function executeKnowledgeGaps(db, params) {
47
65
  };
48
66
  }
49
67
  const gaps = [];
68
+ const seenPackages = new Set();
50
69
  for (const dep of deps) {
51
70
  // Names shorter than 3 chars (e.g. "c", "go", "ws") match too many unrelated
52
71
  // items via substring LIKE to be a trustworthy "mention" signal — skip them.
53
72
  if (!dep.package_name || dep.package_name.length < 3)
54
73
  continue;
74
+ // One gap per package: the same dep declared by several projects would
75
+ // otherwise repeat identical missed_items once per project_path.
76
+ const pkgKey = dep.package_name.toLowerCase();
77
+ if (seenPackages.has(pkgKey))
78
+ continue;
79
+ seenPackages.add(pkgKey);
55
80
  // Find source items mentioning this dependency. Engagement is recorded by the
56
81
  // app in interactions.item_id / .action_type (the canonical columns; the older
57
82
  // source_item_id / action columns are unused), so the NOT-IN suppression must
58
83
  // read those or it silently never fires.
84
+ //
85
+ // Grounding (each guard killed an observed false-positive class):
86
+ // - relevance_score >= 0.2: the scoring pipeline's above-noise band — drops
87
+ // off-topic chatter that merely contains the string (chocolate-bar class).
88
+ // - 30-day window: a "gap" is something you MISSED, not archaeology — a
89
+ // 2014 StackOverflow post is not missed intelligence.
90
+ // - LIKE is only a cheap candidate pre-filter; the real test is the
91
+ // word-boundary check below ("invite" must never evidence a vite gap).
92
+ // - feed_relevant is deliberately NOT filtered here: an item the feed gate
93
+ // rejected can still be a legitimate unread dep mention — surfacing those
94
+ // is this tool's niche. The relevance floor already excludes noise.
59
95
  const pattern = `%${escapeLike(dep.package_name)}%`;
60
- const mentionedItems = rawDb
61
- .prepare(`SELECT si.id, si.title, si.url, si.source_type, si.created_at
96
+ const candidates = rawDb
97
+ .prepare(`SELECT si.id, si.title, si.url, si.source_type, ${hasContentType ? "si.content_type" : "NULL AS content_type"}, si.created_at,
98
+ ${hasRelevance ? "si.relevance_score" : "NULL AS relevance_score"}, substr(COALESCE(si.content, ''), 1, 2000) AS content_head
62
99
  FROM source_items si
63
100
  WHERE (si.title LIKE ? ESCAPE '\\' OR si.content LIKE ? ESCAPE '\\')
101
+ ${hasRelevance ? "AND si.relevance_score IS NOT NULL AND si.relevance_score >= 0.2" : ""}
102
+ AND si.created_at >= datetime('now', '-30 days')
64
103
  AND si.id NOT IN (SELECT item_id FROM interactions WHERE action_type IN ('click', 'save'))
65
- ORDER BY si.created_at DESC LIMIT 5`)
104
+ ORDER BY si.created_at DESC LIMIT 25`)
66
105
  .all(pattern, pattern);
106
+ // Word-boundary verification: the mention must be the package name as a
107
+ // whole word in the title or the content head, not a substring.
108
+ const mentionedItems = candidates
109
+ .filter((item) => mentionsPackage(item.title || "", dep.package_name) ||
110
+ mentionsPackage(item.content_head || "", dep.package_name))
111
+ .slice(0, 5);
67
112
  if (mentionedItems.length > 0) {
68
- // Check if any are security-related
69
- const hasSecurityMention = mentionedItems.some((item) => hasWordBoundary(item.title || "", "cve") ||
70
- hasWordBoundary(item.title || "", "security") ||
71
- hasWordBoundary(item.title || "", "vulnerability"));
72
- const severity = hasSecurityMention
113
+ const isAdvisory = (item) => item.source_type === "cve" ||
114
+ item.source_type === "osv" ||
115
+ item.content_type === "security_advisory";
116
+ const securityKeywords = (title) => hasWordBoundary(title, "cve") ||
117
+ hasWordBoundary(title, "security") ||
118
+ hasWordBoundary(title, "vulnerability");
119
+ // Graded honestly:
120
+ // - critical: an actual advisory whose TITLE names this dependency — the
121
+ // advisory is about the dep, not merely co-mentioning it in a body.
122
+ // - high: a security-keyword item whose title names the dep.
123
+ // - medium: 3+ recent unread mentions; low: 1-2.
124
+ const advisoryAboutDep = mentionedItems.some((item) => isAdvisory(item) && mentionsPackage(item.title || "", dep.package_name));
125
+ const securityTitleMention = mentionedItems.some((item) => securityKeywords(item.title || "") &&
126
+ mentionsPackage(item.title || "", dep.package_name));
127
+ const severity = advisoryAboutDep
73
128
  ? "critical"
74
- : mentionedItems.length >= 3
129
+ : securityTitleMention
75
130
  ? "high"
76
- : "medium";
131
+ : mentionedItems.length >= 3
132
+ ? "medium"
133
+ : "low";
77
134
  gaps.push({
78
135
  dependency: dep.package_name,
79
136
  version: dep.version,
80
137
  project_path: dep.project_path,
81
138
  language: dep.language,
82
- missed_items: mentionedItems.map(item => ({
83
- ...item,
84
- title: item.title && item.title.length > 120 ? item.title.substring(0, 120) + "..." : item.title,
139
+ missed_items: mentionedItems.map((item) => ({
140
+ id: item.id,
141
+ title: item.title && item.title.length > 120
142
+ ? item.title.substring(0, 120) + "..."
143
+ : item.title,
144
+ url: item.url,
145
+ source_type: item.source_type,
146
+ created_at: item.created_at,
147
+ relevance_score: item.relevance_score,
85
148
  })),
86
149
  gap_severity: severity,
87
150
  missed_count: mentionedItems.length,
@@ -101,4 +164,3 @@ export function executeKnowledgeGaps(db, params) {
101
164
  summary: `${filtered.length} knowledge gaps across ${deps.length} tracked dependencies (showing top ${Math.min(filtered.length, maxGaps)})`,
102
165
  };
103
166
  }
104
- //# sourceMappingURL=knowledge-gaps.js.map
@@ -30,4 +30,3 @@ export declare function rankRowsByRecall<T>(rows: T[], query: string, fields: Re
30
30
  * like auth -> jwt).
31
31
  */
32
32
  export declare function createRelevanceScorer(query: string): (text: unknown) => number;
33
- //# sourceMappingURL=recall.d.ts.map
@@ -131,4 +131,3 @@ export function createRelevanceScorer(query) {
131
131
  function scoreRecallText(text, query) {
132
132
  return createRelevanceScorer(query)(text);
133
133
  }
134
- //# sourceMappingURL=recall.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * record_feedback tool
3
3
  *
4
- * Record user feedback on an item for learning.
4
+ * Record explicit user interaction history for an item.
5
5
  */
6
6
  import type { FourDADatabase } from "../db.js";
7
7
  import type { RecordFeedbackParams, FeedbackResult } from "../types.js";
@@ -36,4 +36,3 @@ export declare const recordFeedbackTool: {
36
36
  * Execute the record_feedback tool
37
37
  */
38
38
  export declare function executeRecordFeedback(db: FourDADatabase, params: RecordFeedbackParams): FeedbackResult;
39
- //# sourceMappingURL=record-feedback.d.ts.map