@4da/mcp-server 4.6.1 → 4.6.3

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 (139) hide show
  1. package/README.md +3 -1
  2. package/dist/db.d.ts +21 -2
  3. package/dist/db.js +66 -16
  4. package/dist/doctor.js +5 -3
  5. package/dist/embeddings.d.ts +88 -0
  6. package/dist/embeddings.js +292 -0
  7. package/dist/index.d.ts +9 -9
  8. package/dist/index.js +21 -15
  9. package/dist/live/cvss.d.ts +16 -0
  10. package/dist/live/cvss.js +62 -0
  11. package/dist/live/index.d.ts +9 -1
  12. package/dist/live/index.js +40 -3
  13. package/dist/live/npm-registry.js +29 -6
  14. package/dist/live/osv-scanner.d.ts +8 -0
  15. package/dist/live/osv-scanner.js +115 -30
  16. package/dist/live/platform.d.ts +32 -0
  17. package/dist/live/platform.js +156 -0
  18. package/dist/live/semver-utils.d.ts +8 -0
  19. package/dist/live/semver-utils.js +24 -0
  20. package/dist/live/types.d.ts +31 -9
  21. package/dist/live/version-resolver.d.ts +12 -2
  22. package/dist/live/version-resolver.js +63 -20
  23. package/dist/llm.js +1 -1
  24. package/dist/project-scanner.d.ts +2 -0
  25. package/dist/project-scanner.js +34 -0
  26. package/dist/scheduler/core.d.ts +25 -0
  27. package/dist/scheduler/core.js +61 -0
  28. package/dist/scheduler/freshness.d.ts +17 -0
  29. package/dist/scheduler/freshness.js +41 -0
  30. package/dist/scheduler/index.d.ts +21 -0
  31. package/dist/scheduler/index.js +32 -0
  32. package/dist/scheduler/refreshers.d.ts +21 -0
  33. package/dist/scheduler/refreshers.js +21 -0
  34. package/dist/schemas/get-relevant-content.json +1 -1
  35. package/dist/schemas/upgrade-planner.json +1 -1
  36. package/dist/schemas/what-should-i-know.json +1 -1
  37. package/dist/tools/agent-memory.d.ts +11 -1
  38. package/dist/tools/agent-memory.js +120 -14
  39. package/dist/tools/decision-enforcement.d.ts +17 -1
  40. package/dist/tools/decision-enforcement.js +109 -81
  41. package/dist/tools/decision-memory.d.ts +1 -1
  42. package/dist/tools/decision-memory.js +58 -28
  43. package/dist/tools/decision-recall.d.ts +59 -0
  44. package/dist/tools/decision-recall.js +87 -0
  45. package/dist/tools/dependency-health.js +6 -4
  46. package/dist/tools/developer-dna.js +4 -2
  47. package/dist/tools/knowledge-gaps.js +16 -4
  48. package/dist/tools/recall.d.ts +33 -0
  49. package/dist/tools/recall.js +134 -0
  50. package/dist/tools/upgrade-planner.d.ts +25 -1
  51. package/dist/tools/upgrade-planner.js +111 -30
  52. package/dist/tools/vulnerability-scan.d.ts +46 -0
  53. package/dist/tools/vulnerability-scan.js +64 -24
  54. package/dist/tools/what-should-i-know.d.ts +3 -1
  55. package/dist/tools/what-should-i-know.js +210 -106
  56. package/dist/types.d.ts +9 -0
  57. package/package.json +8 -3
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js.map +0 -1
  60. package/dist/db.d.ts.map +0 -1
  61. package/dist/db.js.map +0 -1
  62. package/dist/doctor.d.ts.map +0 -1
  63. package/dist/doctor.js.map +0 -1
  64. package/dist/http-transport.d.ts.map +0 -1
  65. package/dist/http-transport.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/live/cache.d.ts.map +0 -1
  69. package/dist/live/cache.js.map +0 -1
  70. package/dist/live/crates-registry.d.ts.map +0 -1
  71. package/dist/live/crates-registry.js.map +0 -1
  72. package/dist/live/go-registry.d.ts.map +0 -1
  73. package/dist/live/go-registry.js.map +0 -1
  74. package/dist/live/hn-fetcher.d.ts.map +0 -1
  75. package/dist/live/hn-fetcher.js.map +0 -1
  76. package/dist/live/http-utils.d.ts.map +0 -1
  77. package/dist/live/http-utils.js.map +0 -1
  78. package/dist/live/index.d.ts.map +0 -1
  79. package/dist/live/index.js.map +0 -1
  80. package/dist/live/npm-registry.d.ts.map +0 -1
  81. package/dist/live/npm-registry.js.map +0 -1
  82. package/dist/live/osv-scanner.d.ts.map +0 -1
  83. package/dist/live/osv-scanner.js.map +0 -1
  84. package/dist/live/pypi-registry.d.ts.map +0 -1
  85. package/dist/live/pypi-registry.js.map +0 -1
  86. package/dist/live/rate-limiter.d.ts.map +0 -1
  87. package/dist/live/rate-limiter.js.map +0 -1
  88. package/dist/live/semver-utils.d.ts.map +0 -1
  89. package/dist/live/semver-utils.js.map +0 -1
  90. package/dist/live/types.d.ts.map +0 -1
  91. package/dist/live/types.js.map +0 -1
  92. package/dist/live/version-resolver.d.ts.map +0 -1
  93. package/dist/live/version-resolver.js.map +0 -1
  94. package/dist/live-singleton.d.ts.map +0 -1
  95. package/dist/live-singleton.js.map +0 -1
  96. package/dist/llm.d.ts.map +0 -1
  97. package/dist/llm.js.map +0 -1
  98. package/dist/output-manager.d.ts.map +0 -1
  99. package/dist/output-manager.js.map +0 -1
  100. package/dist/project-scanner.d.ts.map +0 -1
  101. package/dist/project-scanner.js.map +0 -1
  102. package/dist/schema-registry.d.ts.map +0 -1
  103. package/dist/schema-registry.js.map +0 -1
  104. package/dist/setup.d.ts.map +0 -1
  105. package/dist/setup.js.map +0 -1
  106. package/dist/tool-dispatch.d.ts.map +0 -1
  107. package/dist/tool-dispatch.js.map +0 -1
  108. package/dist/tools/agent-memory.d.ts.map +0 -1
  109. package/dist/tools/agent-memory.js.map +0 -1
  110. package/dist/tools/decision-enforcement.d.ts.map +0 -1
  111. package/dist/tools/decision-enforcement.js.map +0 -1
  112. package/dist/tools/decision-memory.d.ts.map +0 -1
  113. package/dist/tools/decision-memory.js.map +0 -1
  114. package/dist/tools/dependency-health.d.ts.map +0 -1
  115. package/dist/tools/dependency-health.js.map +0 -1
  116. package/dist/tools/developer-dna.d.ts.map +0 -1
  117. package/dist/tools/developer-dna.js.map +0 -1
  118. package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
  119. package/dist/tools/ecosystem-pulse.js.map +0 -1
  120. package/dist/tools/get-actionable-signals.d.ts.map +0 -1
  121. package/dist/tools/get-actionable-signals.js.map +0 -1
  122. package/dist/tools/get-context.d.ts.map +0 -1
  123. package/dist/tools/get-context.js.map +0 -1
  124. package/dist/tools/get-relevant-content.d.ts.map +0 -1
  125. package/dist/tools/get-relevant-content.js.map +0 -1
  126. package/dist/tools/index.d.ts.map +0 -1
  127. package/dist/tools/index.js.map +0 -1
  128. package/dist/tools/knowledge-gaps.d.ts.map +0 -1
  129. package/dist/tools/knowledge-gaps.js.map +0 -1
  130. package/dist/tools/record-feedback.d.ts.map +0 -1
  131. package/dist/tools/record-feedback.js.map +0 -1
  132. package/dist/tools/upgrade-planner.d.ts.map +0 -1
  133. package/dist/tools/upgrade-planner.js.map +0 -1
  134. package/dist/tools/vulnerability-scan.d.ts.map +0 -1
  135. package/dist/tools/vulnerability-scan.js.map +0 -1
  136. package/dist/tools/what-should-i-know.d.ts.map +0 -1
  137. package/dist/tools/what-should-i-know.js.map +0 -1
  138. package/dist/types.d.ts.map +0 -1
  139. package/dist/types.js.map +0 -1
@@ -126,5 +126,5 @@ export declare const decisionMemoryTool: {
126
126
  required: string[];
127
127
  };
128
128
  };
129
- export declare function executeDecisionMemory(db: FourDADatabase, params: DecisionMemoryParams): object;
129
+ export declare function executeDecisionMemory(db: FourDADatabase, params: DecisionMemoryParams): object | Promise<object>;
130
130
  //# sourceMappingURL=decision-memory.d.ts.map
@@ -5,6 +5,9 @@
5
5
  * Manage developer decisions - record, list, check alignment, update, and supersede.
6
6
  * Decisions persist and inform signal classification, tech radar, and agent context.
7
7
  */
8
+ import { matchesRecallQuery } from "./recall.js";
9
+ import { getEmbeddingConfig } from "../embeddings.js";
10
+ import { rankDecisionsLexical, hybridDecisionRecall, POSSIBLE_CONFLICT_THRESHOLD, } from "./decision-recall.js";
8
11
  // ============================================================================
9
12
  // Tool Definition
10
13
  // ============================================================================
@@ -119,6 +122,47 @@ function parseDecisionRow(row) {
119
122
  context_tags: JSON.parse(row.context_tags || "[]"),
120
123
  };
121
124
  }
125
+ /**
126
+ * Build the check_alignment response from retrieved decisions. Hard conflicts are
127
+ * lexical/alias-aware (grounded); semantically-close decisions that rejected
128
+ * alternatives are surfaced as advisory `possible_conflicts`.
129
+ */
130
+ function buildAlignmentResponse(rows, semanticById, recallMode, technology) {
131
+ const conflicts = [];
132
+ const possibleConflicts = [];
133
+ const relevant = [];
134
+ for (const row of rows) {
135
+ const alts = JSON.parse(row.alternatives_rejected || "[]");
136
+ const isRejected = alts.some((alt) => matchesRecallQuery(alt, technology));
137
+ if (isRejected) {
138
+ conflicts.push({
139
+ decision_id: row.id,
140
+ subject: row.subject,
141
+ reason: `'${technology}' was rejected in favor of '${row.decision}' (rationale: ${row.rationale || "none"})`,
142
+ });
143
+ }
144
+ else {
145
+ const sim = semanticById.get(row.id) ?? 0;
146
+ if (sim >= POSSIBLE_CONFLICT_THRESHOLD && alts.length > 0) {
147
+ possibleConflicts.push({
148
+ decision_id: row.id,
149
+ subject: row.subject,
150
+ similarity: Math.round(sim * 1000) / 1000,
151
+ reason: `'${technology}' is semantically close (${Math.round(sim * 100)}%) to a decision that rejected: ${alts.join(", ")}. Review before proceeding.`,
152
+ });
153
+ }
154
+ }
155
+ relevant.push(parseDecisionRow(row));
156
+ }
157
+ return {
158
+ aligned: conflicts.length === 0,
159
+ relevant_decisions: relevant,
160
+ conflicts,
161
+ possible_conflicts: possibleConflicts,
162
+ confidence: relevant.length > 0 ? Math.max(...relevant.map((r) => r.confidence)) : 0.5,
163
+ recall_mode: recallMode,
164
+ };
165
+ }
122
166
  // ============================================================================
123
167
  // Execute
124
168
  // ============================================================================
@@ -172,39 +216,25 @@ export function executeDecisionMemory(db, params) {
172
216
  error: "technology is required for check_alignment action",
173
217
  };
174
218
  }
175
- const search = `%${params.technology.toLowerCase()}%`;
176
- const rows = rawDb
219
+ const candidateRows = rawDb
177
220
  .prepare(`SELECT id, decision_type, subject, decision, rationale,
178
221
  alternatives_rejected, context_tags, confidence,
179
- status, created_at
222
+ status, superseded_by, created_at, updated_at
180
223
  FROM developer_decisions
181
224
  WHERE status = 'active'
182
- AND (LOWER(subject) LIKE ?
183
- OR LOWER(context_tags) LIKE ?
184
- OR LOWER(alternatives_rejected) LIKE ?)`)
185
- .all(search, search, search);
186
- const conflicts = [];
187
- const relevant = [];
188
- for (const row of rows) {
189
- const alts = JSON.parse(row.alternatives_rejected || "[]");
190
- const isRejected = alts.some((alt) => alt.toLowerCase().includes(params.technology.toLowerCase()));
191
- if (isRejected) {
192
- conflicts.push({
193
- decision_id: row.id,
194
- subject: row.subject,
195
- reason: `'${params.technology}' was rejected in favor of '${row.decision}' (rationale: ${row.rationale || "none"})`,
196
- });
197
- }
198
- relevant.push(parseDecisionRow(row));
225
+ ORDER BY updated_at DESC
226
+ LIMIT 500`)
227
+ .all();
228
+ const technology = params.technology;
229
+ const limit = params.limit || 20;
230
+ const config = getEmbeddingConfig();
231
+ // No provider -> synchronous lexical retrieval (classic behaviour).
232
+ if (!config) {
233
+ const rows = rankDecisionsLexical(candidateRows, technology, limit);
234
+ return buildAlignmentResponse(rows, new Map(), "ranked_lexical", technology);
199
235
  }
200
- return {
201
- aligned: conflicts.length === 0,
202
- relevant_decisions: relevant,
203
- conflicts,
204
- confidence: relevant.length > 0
205
- ? Math.max(...relevant.map((r) => r.confidence))
206
- : 0.5,
207
- };
236
+ // Provider configured -> hybrid retrieval (returns a Promise; dispatch awaits).
237
+ return hybridDecisionRecall(db, technology, candidateRows, limit, config).then(({ ranked, semanticById, recall_mode }) => buildAlignmentResponse(ranked, semanticById, recall_mode, technology));
208
238
  }
209
239
  case "update": {
210
240
  if (!params.id) {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Shared relevance retrieval for developer_decisions.
3
+ *
4
+ * Both decision tools (check_decision_alignment and decision_memory's
5
+ * check_alignment) retrieve "decisions relevant to X" the same way, so the
6
+ * ranking lives here once. Retrieval is lexical by default and OPTIONALLY hybrid
7
+ * (alias-aware lexical blended with provider-backed embedding similarity) when an
8
+ * embedding provider is configured — letting an agent surface a decision that is
9
+ * a paraphrase of what it is about to propose, even with zero shared words.
10
+ *
11
+ * Note: this affects RETRIEVAL only. Hard "X was rejected" conflict detection
12
+ * stays lexical/alias-aware in the tools, so semantic similarity can never
13
+ * fabricate a grounded conflict claim.
14
+ */
15
+ import type { FourDADatabase } from "../db.js";
16
+ import { type RecallField } from "./recall.js";
17
+ import { type EmbeddingConfig } from "../embeddings.js";
18
+ /** Minimum text fields a decision row must expose for ranking + embedding. */
19
+ export interface DecisionLike {
20
+ id: number;
21
+ subject: string;
22
+ decision: string;
23
+ rationale: string | null;
24
+ /** JSON-encoded string[] */
25
+ alternatives_rejected: string;
26
+ /** JSON-encoded string[] */
27
+ context_tags: string;
28
+ }
29
+ /** Weighted fields for lexical ranking — rejected alternatives matter most. */
30
+ export declare const DECISION_RECALL_FIELDS: RecallField<DecisionLike>[];
31
+ /**
32
+ * A decision retrieved with at least this cosine similarity to the query, that
33
+ * also has rejected alternatives, is surfaced as a POSSIBLE (paraphrase) conflict
34
+ * for human review — distinct from a hard, lexically-grounded conflict.
35
+ */
36
+ export declare const POSSIBLE_CONFLICT_THRESHOLD = 0.6;
37
+ export interface DecisionRecallResult<T extends DecisionLike> {
38
+ /** Decisions ranked by relevance, capped to `limit`. */
39
+ ranked: T[];
40
+ /** id -> cosine similarity to the query (empty in pure-lexical mode). */
41
+ semanticById: Map<number, number>;
42
+ recall_mode: "hybrid" | "ranked_lexical";
43
+ }
44
+ /** Pure lexical ranking (the default, provider-free path). */
45
+ export declare function rankDecisionsLexical<T extends DecisionLike>(rows: T[], query: string, limit: number): T[];
46
+ /**
47
+ * The text embedded for a decision: everything that carries its meaning.
48
+ * Exported so every surface that embeds a decision row (decision tools AND the
49
+ * what_should_i_know briefing) uses IDENTICAL text — otherwise they would write
50
+ * conflicting vectors into the shared developer_decisions.embedding column.
51
+ */
52
+ export declare function decisionEmbedText(row: DecisionLike): string;
53
+ /**
54
+ * Hybrid retrieval: blends alias-aware lexical scoring with embedding cosine
55
+ * similarity. Falls back to pure lexical (and reports it) when the query cannot
56
+ * be embedded.
57
+ */
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
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Shared relevance retrieval for developer_decisions.
4
+ *
5
+ * Both decision tools (check_decision_alignment and decision_memory's
6
+ * check_alignment) retrieve "decisions relevant to X" the same way, so the
7
+ * ranking lives here once. Retrieval is lexical by default and OPTIONALLY hybrid
8
+ * (alias-aware lexical blended with provider-backed embedding similarity) when an
9
+ * embedding provider is configured — letting an agent surface a decision that is
10
+ * a paraphrase of what it is about to propose, even with zero shared words.
11
+ *
12
+ * Note: this affects RETRIEVAL only. Hard "X was rejected" conflict detection
13
+ * stays lexical/alias-aware in the tools, so semantic similarity can never
14
+ * fabricate a grounded conflict claim.
15
+ */
16
+ import { rankRowsByRecall } from "./recall.js";
17
+ import { semanticScores } from "../embeddings.js";
18
+ /** Weighted fields for lexical ranking — rejected alternatives matter most. */
19
+ export const DECISION_RECALL_FIELDS = [
20
+ { name: "alternatives", weight: 5, value: (row) => row.alternatives_rejected },
21
+ { name: "subject", weight: 4, value: (row) => row.subject },
22
+ { name: "tags", weight: 3, value: (row) => row.context_tags },
23
+ { name: "decision", weight: 2, value: (row) => row.decision },
24
+ { name: "rationale", weight: 1, value: (row) => row.rationale },
25
+ ];
26
+ /** Blend weight for semantic cosine vs normalized lexical score. */
27
+ const SEMANTIC_BLEND = 0.65;
28
+ /**
29
+ * A decision retrieved with at least this cosine similarity to the query, that
30
+ * also has rejected alternatives, is surfaced as a POSSIBLE (paraphrase) conflict
31
+ * for human review — distinct from a hard, lexically-grounded conflict.
32
+ */
33
+ export const POSSIBLE_CONFLICT_THRESHOLD = 0.6;
34
+ /** Pure lexical ranking (the default, provider-free path). */
35
+ export function rankDecisionsLexical(rows, query, limit) {
36
+ return rankRowsByRecall(rows, query, DECISION_RECALL_FIELDS, limit).map((item) => item.row);
37
+ }
38
+ /**
39
+ * The text embedded for a decision: everything that carries its meaning.
40
+ * Exported so every surface that embeds a decision row (decision tools AND the
41
+ * what_should_i_know briefing) uses IDENTICAL text — otherwise they would write
42
+ * conflicting vectors into the shared developer_decisions.embedding column.
43
+ */
44
+ export function decisionEmbedText(row) {
45
+ let alts = "";
46
+ try {
47
+ alts = JSON.parse(row.alternatives_rejected || "[]").join(", ");
48
+ }
49
+ catch {
50
+ // Malformed JSON — just skip the alternatives in the embed text.
51
+ }
52
+ return [row.subject, row.decision, row.rationale || "", alts].filter(Boolean).join("\n");
53
+ }
54
+ /**
55
+ * Hybrid retrieval: blends alias-aware lexical scoring with embedding cosine
56
+ * similarity. Falls back to pure lexical (and reports it) when the query cannot
57
+ * be embedded.
58
+ */
59
+ export async function hybridDecisionRecall(db, query, rows, limit, config) {
60
+ // Lexical baseline over ALL candidates (for blending + fallback).
61
+ const lexical = rankRowsByRecall(rows, query, DECISION_RECALL_FIELDS, rows.length);
62
+ const lexById = new Map();
63
+ for (const item of lexical)
64
+ lexById.set(item.row.id, item.score);
65
+ const maxLex = lexical.length ? lexical[0].score : 0;
66
+ const sem = await semanticScores(db, "developer_decisions", query, rows.map((row) => ({ id: row.id, text: decisionEmbedText(row) })), config);
67
+ if (!sem.queryEmbedded || sem.embeddedCount === 0) {
68
+ return {
69
+ ranked: rankDecisionsLexical(rows, query, limit),
70
+ semanticById: new Map(),
71
+ recall_mode: "ranked_lexical",
72
+ };
73
+ }
74
+ const ranked = rows
75
+ .map((row) => {
76
+ const semantic = Math.max(0, sem.semanticById.get(row.id) ?? 0);
77
+ const lexNorm = maxLex > 0 ? (lexById.get(row.id) ?? 0) / maxLex : 0;
78
+ const score = SEMANTIC_BLEND * semantic + (1 - SEMANTIC_BLEND) * lexNorm;
79
+ return { row, score };
80
+ })
81
+ .filter((x) => x.score > 0)
82
+ .sort((a, b) => b.score - a.score)
83
+ .slice(0, limit)
84
+ .map((x) => x.row);
85
+ return { ranked, semanticById: sem.semanticById, recall_mode: "hybrid" };
86
+ }
87
+ //# sourceMappingURL=decision-recall.js.map
@@ -75,15 +75,17 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
75
75
  // Compute stats
76
76
  let outdated = 0;
77
77
  let deprecated = 0;
78
- let vulnerable = 0;
79
78
  for (const dep of registryData) {
80
79
  if (dep.versionsBehind && dep.versionsBehind.label !== "up-to-date")
81
80
  outdated++;
82
81
  if (dep.deprecated)
83
82
  deprecated++;
84
- if (vulnMap.has(dep.name))
85
- vulnerable++;
86
83
  }
84
+ // Count vulnerable packages from the full scanned set (the OSV scan covers
85
+ // transitives), so the "N vulnerable" summary agrees with the severity
86
+ // breakdown shown beside it. registryData is direct deps only — counting there
87
+ // undercounts CVEs that live in transitive dependencies.
88
+ const vulnerable = vulnMap.size;
87
89
  // Sort
88
90
  const sortBy = params.sort_by ?? "risk";
89
91
  const sorted = [...registryData].sort((a, b) => {
@@ -135,7 +137,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
135
137
  parts.push("all healthy");
136
138
  return {
137
139
  scannedAt: new Date().toISOString(),
138
- projectPath: process.cwd(),
140
+ projectPath: liveIntel.getProjectRoot() ?? process.cwd(),
139
141
  ecosystemsScanned: ecosystems,
140
142
  totalDeps: total,
141
143
  outdatedCount: outdated,
@@ -195,15 +195,17 @@ export function executeDeveloperDna(db, params) {
195
195
  }
196
196
  // Check for any source_items mentioning this dep that have interactions
197
197
  try {
198
+ // Escape LIKE wildcards so a name containing % or _ matches literally.
199
+ const likeDep = `%${depLower.replace(/[\\%_]/g, (c) => "\\" + c)}%`;
198
200
  const mentionedInteraction = rawDb
199
201
  .prepare(`SELECT i.timestamp
200
202
  FROM interactions i
201
203
  JOIN source_items si ON i.item_id = si.id AND i.item_source = si.source_type
202
- WHERE (LOWER(si.title) LIKE ? OR LOWER(si.content) LIKE ?)
204
+ WHERE (LOWER(si.title) LIKE ? ESCAPE '\\' OR LOWER(si.content) LIKE ? ESCAPE '\\')
203
205
  AND i.action_type IN ('click', 'save')
204
206
  ORDER BY i.timestamp DESC
205
207
  LIMIT 1`)
206
- .get(`%${depLower}%`, `%${depLower}%`);
208
+ .get(likeDep, likeDep);
207
209
  if (mentionedInteraction) {
208
210
  const interactionTime = new Date(mentionedInteraction.timestamp).getTime();
209
211
  const daysSince = Math.floor((now - interactionTime) / (1000 * 60 * 60 * 24));
@@ -9,6 +9,11 @@ function hasWordBoundary(text, term) {
9
9
  const regex = new RegExp(`\\b${term}\\b`, 'i');
10
10
  return regex.test(text);
11
11
  }
12
+ // Escape SQL LIKE wildcards so a package name containing % or _ (npm names may
13
+ // contain _) is matched literally rather than as a pattern. Paired with ESCAPE '\'.
14
+ function escapeLike(s) {
15
+ return s.replace(/[\\%_]/g, (c) => "\\" + c);
16
+ }
12
17
  export const knowledgeGapsTool = {
13
18
  name: "knowledge_gaps",
14
19
  description: `Detect knowledge gaps by cross-referencing your project dependencies with source items you haven't engaged with. Identifies things you should know about but might have missed.`,
@@ -43,13 +48,20 @@ export function executeKnowledgeGaps(db, params) {
43
48
  }
44
49
  const gaps = [];
45
50
  for (const dep of deps) {
46
- // Find source items mentioning this dependency
47
- const pattern = `%${dep.package_name}%`;
51
+ // Names shorter than 3 chars (e.g. "c", "go", "ws") match too many unrelated
52
+ // items via substring LIKE to be a trustworthy "mention" signal — skip them.
53
+ if (!dep.package_name || dep.package_name.length < 3)
54
+ continue;
55
+ // Find source items mentioning this dependency. Engagement is recorded by the
56
+ // app in interactions.item_id / .action_type (the canonical columns; the older
57
+ // source_item_id / action columns are unused), so the NOT-IN suppression must
58
+ // read those or it silently never fires.
59
+ const pattern = `%${escapeLike(dep.package_name)}%`;
48
60
  const mentionedItems = rawDb
49
61
  .prepare(`SELECT si.id, si.title, si.url, si.source_type, si.created_at
50
62
  FROM source_items si
51
- WHERE (si.title LIKE ? OR si.content LIKE ?)
52
- AND si.id NOT IN (SELECT source_item_id FROM interactions WHERE action IN ('click', 'save'))
63
+ WHERE (si.title LIKE ? ESCAPE '\\' OR si.content LIKE ? ESCAPE '\\')
64
+ AND si.id NOT IN (SELECT item_id FROM interactions WHERE action_type IN ('click', 'save'))
53
65
  ORDER BY si.created_at DESC LIMIT 5`)
54
66
  .all(pattern, pattern);
55
67
  if (mentionedItems.length > 0) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Ranked lexical recall for small local knowledge tables.
3
+ *
4
+ * This is not embedding search. It is a deterministic bridge that searches
5
+ * all meaningful text fields and ranks matches until decision/memory
6
+ * embeddings get their own schema and vec index.
7
+ */
8
+ export interface RecallField<T> {
9
+ name: string;
10
+ weight: number;
11
+ value: (row: T) => unknown;
12
+ }
13
+ export interface RankedRecall<T> {
14
+ row: T;
15
+ score: number;
16
+ matched_fields: string[];
17
+ }
18
+ export declare function normalizeRecallText(input: unknown): string;
19
+ export declare function recallTerms(query: string): string[];
20
+ export declare function matchesRecallQuery(text: unknown, query: string): boolean;
21
+ export declare function rankRowsByRecall<T>(rows: T[], query: string, fields: RecallField<T>[], limit: number): RankedRecall<T>[];
22
+ /**
23
+ * Build a reusable relevance scorer for a single query. The query's normalized
24
+ * phrase and alias-expanded terms are computed ONCE, so callers that score many
25
+ * texts against the same query (ranking 300+ rows x 5 fields, or filtering a
26
+ * briefing's advisories/windows/news) do not re-tokenize the query per text.
27
+ *
28
+ * Exported so every relevance check in the server shares one alias-aware scorer
29
+ * instead of re-implementing substring matching (which silently drops aliases
30
+ * like auth -> jwt).
31
+ */
32
+ export declare function createRelevanceScorer(query: string): (text: unknown) => number;
33
+ //# sourceMappingURL=recall.d.ts.map
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Ranked lexical recall for small local knowledge tables.
4
+ *
5
+ * This is not embedding search. It is a deterministic bridge that searches
6
+ * all meaningful text fields and ranks matches until decision/memory
7
+ * embeddings get their own schema and vec index.
8
+ */
9
+ const STOP_WORDS = new Set([
10
+ "about",
11
+ "after",
12
+ "again",
13
+ "against",
14
+ "before",
15
+ "between",
16
+ "from",
17
+ "into",
18
+ "should",
19
+ "that",
20
+ "their",
21
+ "there",
22
+ "this",
23
+ "with",
24
+ "would",
25
+ ]);
26
+ const TERM_ALIASES = {
27
+ auth: ["authentication", "authorization", "jwt", "oauth"],
28
+ authentication: ["auth"],
29
+ authorization: ["auth", "authz"],
30
+ authz: ["authorization"],
31
+ js: ["javascript"],
32
+ javascript: ["js"],
33
+ postgres: ["postgresql"],
34
+ postgresql: ["postgres"],
35
+ py: ["python"],
36
+ python: ["py"],
37
+ sqlite: ["sqlite3"],
38
+ sqlite3: ["sqlite"],
39
+ ts: ["typescript"],
40
+ typescript: ["ts"],
41
+ };
42
+ export function normalizeRecallText(input) {
43
+ if (input === null || input === undefined)
44
+ return "";
45
+ const text = Array.isArray(input) ? input.join(" ") : String(input);
46
+ return text
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9+#]+/g, " ")
49
+ .replace(/\s+/g, " ")
50
+ .trim();
51
+ }
52
+ export function recallTerms(query) {
53
+ const terms = new Set();
54
+ const normalized = normalizeRecallText(query);
55
+ for (const token of normalized.split(" ")) {
56
+ if (token.length < 3 || STOP_WORDS.has(token))
57
+ continue;
58
+ terms.add(token);
59
+ for (const alias of TERM_ALIASES[token] || []) {
60
+ terms.add(alias);
61
+ }
62
+ }
63
+ return [...terms];
64
+ }
65
+ export function matchesRecallQuery(text, query) {
66
+ return scoreRecallText(text, query) > 0;
67
+ }
68
+ export function rankRowsByRecall(rows, query, fields, limit) {
69
+ const scoreText = createRelevanceScorer(query);
70
+ const ranked = rows
71
+ .map((row) => {
72
+ let score = 0;
73
+ const matched = new Set();
74
+ for (const field of fields) {
75
+ const fieldScore = scoreText(safeValue(field, row));
76
+ if (fieldScore > 0) {
77
+ score += fieldScore * field.weight;
78
+ matched.add(field.name);
79
+ }
80
+ }
81
+ return { row, score, matched_fields: [...matched] };
82
+ })
83
+ .filter((item) => item.score > 0)
84
+ .sort((a, b) => b.score - a.score);
85
+ return ranked.slice(0, Math.max(1, limit));
86
+ }
87
+ function safeValue(field, row) {
88
+ try {
89
+ return field.value(row);
90
+ }
91
+ catch {
92
+ return "";
93
+ }
94
+ }
95
+ /**
96
+ * Build a reusable relevance scorer for a single query. The query's normalized
97
+ * phrase and alias-expanded terms are computed ONCE, so callers that score many
98
+ * texts against the same query (ranking 300+ rows x 5 fields, or filtering a
99
+ * briefing's advisories/windows/news) do not re-tokenize the query per text.
100
+ *
101
+ * Exported so every relevance check in the server shares one alias-aware scorer
102
+ * instead of re-implementing substring matching (which silently drops aliases
103
+ * like auth -> jwt).
104
+ */
105
+ export function createRelevanceScorer(query) {
106
+ const phrase = normalizeRecallText(query);
107
+ const hasPhrase = phrase.length >= 3;
108
+ const terms = recallTerms(query);
109
+ return (text) => {
110
+ if (!hasPhrase && terms.length === 0)
111
+ return 0;
112
+ const haystack = normalizeRecallText(text);
113
+ if (!haystack)
114
+ return 0;
115
+ let score = 0;
116
+ if (hasPhrase && haystack.includes(phrase)) {
117
+ score += 4;
118
+ }
119
+ const haystackTerms = new Set(haystack.split(" "));
120
+ for (const term of terms) {
121
+ if (haystackTerms.has(term)) {
122
+ score += 2;
123
+ }
124
+ else if (haystack.includes(term)) {
125
+ score += 1;
126
+ }
127
+ }
128
+ return score;
129
+ };
130
+ }
131
+ function scoreRecallText(text, query) {
132
+ return createRelevanceScorer(query)(text);
133
+ }
134
+ //# sourceMappingURL=recall.js.map
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * upgrade_planner tool
3
3
  *
4
- * Ranked upgrade recommendations for project dependencies.
4
+ * Ranked standalone upgrade recommendations for project dependencies.
5
5
  * Combines version freshness, vulnerability data, and deprecation status
6
6
  * to produce a prioritized upgrade plan.
7
+ *
8
+ * Honesty contract (Phase 0 hardening):
9
+ * - Transitive CVEs are surfaced as `waiting_on_upstream` steps instead of
10
+ * being counted-but-never-recommended. No dependency-graph parsing happens
11
+ * here — parent mapping is the desktop core's job; the honest label is ours.
12
+ * - Output carries explicit heuristic provenance: this is a point-in-time
13
+ * standalone plan, not the 4DA app's cross-project, version-confirmed plan.
14
+ * - `projectPath` reports the directory set the dependencies were actually
15
+ * resolved from, not whatever `process.cwd()` happens to be.
16
+ * - When no vulnerability scan has run, the plan says so instead of silently
17
+ * producing a CVE-blind ranking.
7
18
  */
8
19
  import type { FourDADatabase } from "../db.js";
9
20
  import type { LiveIntelligence } from "../live/index.js";
@@ -22,6 +33,12 @@ interface UpgradeRecommendation {
22
33
  reasons: string[];
23
34
  breaking: boolean;
24
35
  isDev: boolean;
36
+ /** Whether this package is declared in a manifest (direct) or only present via the lockfile (transitive). */
37
+ scope: "direct" | "transitive";
38
+ /** Direct deps you can bump yourself; transitive fixes arrive via a parent update or lockfile refresh. */
39
+ action: "upgrade_direct" | "waiting_on_upstream";
40
+ /** False when the dependency is gated behind a target spec not active on this host (label, never suppressed). */
41
+ platformActive: boolean;
25
42
  }
26
43
  interface UpgradePlanResult {
27
44
  generatedAt: string;
@@ -31,6 +48,13 @@ interface UpgradePlanResult {
31
48
  summary: string;
32
49
  quickWins: number;
33
50
  breakingChanges: number;
51
+ waitingOnUpstream: number;
52
+ /** False when no vulnerability scan has run this session — the ranking is then CVE-blind and says so. */
53
+ vulnerabilityDataAvailable: boolean;
54
+ provenance: {
55
+ mode: "standalone_heuristic";
56
+ note: string;
57
+ };
34
58
  }
35
59
  export declare const upgradePlannerTool: {
36
60
  name: string;