@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.
- package/README.md +3 -1
- package/dist/db.d.ts +21 -2
- package/dist/db.js +66 -16
- package/dist/doctor.js +5 -3
- package/dist/embeddings.d.ts +88 -0
- package/dist/embeddings.js +292 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +21 -15
- package/dist/live/cvss.d.ts +16 -0
- package/dist/live/cvss.js +62 -0
- package/dist/live/index.d.ts +9 -1
- package/dist/live/index.js +40 -3
- package/dist/live/npm-registry.js +29 -6
- package/dist/live/osv-scanner.d.ts +8 -0
- package/dist/live/osv-scanner.js +115 -30
- package/dist/live/platform.d.ts +32 -0
- package/dist/live/platform.js +156 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +24 -0
- package/dist/live/types.d.ts +31 -9
- package/dist/live/version-resolver.d.ts +12 -2
- package/dist/live/version-resolver.js +63 -20
- package/dist/llm.js +1 -1
- package/dist/project-scanner.d.ts +2 -0
- package/dist/project-scanner.js +34 -0
- package/dist/scheduler/core.d.ts +25 -0
- package/dist/scheduler/core.js +61 -0
- package/dist/scheduler/freshness.d.ts +17 -0
- package/dist/scheduler/freshness.js +41 -0
- package/dist/scheduler/index.d.ts +21 -0
- package/dist/scheduler/index.js +32 -0
- package/dist/scheduler/refreshers.d.ts +21 -0
- package/dist/scheduler/refreshers.js +21 -0
- package/dist/schemas/get-relevant-content.json +1 -1
- package/dist/schemas/upgrade-planner.json +1 -1
- package/dist/schemas/what-should-i-know.json +1 -1
- package/dist/tools/agent-memory.d.ts +11 -1
- package/dist/tools/agent-memory.js +120 -14
- package/dist/tools/decision-enforcement.d.ts +17 -1
- package/dist/tools/decision-enforcement.js +109 -81
- package/dist/tools/decision-memory.d.ts +1 -1
- package/dist/tools/decision-memory.js +58 -28
- package/dist/tools/decision-recall.d.ts +59 -0
- package/dist/tools/decision-recall.js +87 -0
- package/dist/tools/dependency-health.js +6 -4
- package/dist/tools/developer-dna.js +4 -2
- package/dist/tools/knowledge-gaps.js +16 -4
- package/dist/tools/recall.d.ts +33 -0
- package/dist/tools/recall.js +134 -0
- package/dist/tools/upgrade-planner.d.ts +25 -1
- package/dist/tools/upgrade-planner.js +111 -30
- package/dist/tools/vulnerability-scan.d.ts +46 -0
- package/dist/tools/vulnerability-scan.js +64 -24
- package/dist/tools/what-should-i-know.d.ts +3 -1
- package/dist/tools/what-should-i-know.js +210 -106
- package/dist/types.d.ts +9 -0
- package/package.json +8 -3
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/http-transport.d.ts.map +0 -1
- package/dist/http-transport.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/live/cache.d.ts.map +0 -1
- package/dist/live/cache.js.map +0 -1
- package/dist/live/crates-registry.d.ts.map +0 -1
- package/dist/live/crates-registry.js.map +0 -1
- package/dist/live/go-registry.d.ts.map +0 -1
- package/dist/live/go-registry.js.map +0 -1
- package/dist/live/hn-fetcher.d.ts.map +0 -1
- package/dist/live/hn-fetcher.js.map +0 -1
- package/dist/live/http-utils.d.ts.map +0 -1
- package/dist/live/http-utils.js.map +0 -1
- package/dist/live/index.d.ts.map +0 -1
- package/dist/live/index.js.map +0 -1
- package/dist/live/npm-registry.d.ts.map +0 -1
- package/dist/live/npm-registry.js.map +0 -1
- package/dist/live/osv-scanner.d.ts.map +0 -1
- package/dist/live/osv-scanner.js.map +0 -1
- package/dist/live/pypi-registry.d.ts.map +0 -1
- package/dist/live/pypi-registry.js.map +0 -1
- package/dist/live/rate-limiter.d.ts.map +0 -1
- package/dist/live/rate-limiter.js.map +0 -1
- package/dist/live/semver-utils.d.ts.map +0 -1
- package/dist/live/semver-utils.js.map +0 -1
- package/dist/live/types.d.ts.map +0 -1
- package/dist/live/types.js.map +0 -1
- package/dist/live/version-resolver.d.ts.map +0 -1
- package/dist/live/version-resolver.js.map +0 -1
- package/dist/live-singleton.d.ts.map +0 -1
- package/dist/live-singleton.js.map +0 -1
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js.map +0 -1
- package/dist/output-manager.d.ts.map +0 -1
- package/dist/output-manager.js.map +0 -1
- package/dist/project-scanner.d.ts.map +0 -1
- package/dist/project-scanner.js.map +0 -1
- package/dist/schema-registry.d.ts.map +0 -1
- package/dist/schema-registry.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/tool-dispatch.d.ts.map +0 -1
- package/dist/tool-dispatch.js.map +0 -1
- package/dist/tools/agent-memory.d.ts.map +0 -1
- package/dist/tools/agent-memory.js.map +0 -1
- package/dist/tools/decision-enforcement.d.ts.map +0 -1
- package/dist/tools/decision-enforcement.js.map +0 -1
- package/dist/tools/decision-memory.d.ts.map +0 -1
- package/dist/tools/decision-memory.js.map +0 -1
- package/dist/tools/dependency-health.d.ts.map +0 -1
- package/dist/tools/dependency-health.js.map +0 -1
- package/dist/tools/developer-dna.d.ts.map +0 -1
- package/dist/tools/developer-dna.js.map +0 -1
- package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
- package/dist/tools/ecosystem-pulse.js.map +0 -1
- package/dist/tools/get-actionable-signals.d.ts.map +0 -1
- package/dist/tools/get-actionable-signals.js.map +0 -1
- package/dist/tools/get-context.d.ts.map +0 -1
- package/dist/tools/get-context.js.map +0 -1
- package/dist/tools/get-relevant-content.d.ts.map +0 -1
- package/dist/tools/get-relevant-content.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/knowledge-gaps.d.ts.map +0 -1
- package/dist/tools/knowledge-gaps.js.map +0 -1
- package/dist/tools/record-feedback.d.ts.map +0 -1
- package/dist/tools/record-feedback.js.map +0 -1
- package/dist/tools/upgrade-planner.d.ts.map +0 -1
- package/dist/tools/upgrade-planner.js.map +0 -1
- package/dist/tools/vulnerability-scan.d.ts.map +0 -1
- package/dist/tools/vulnerability-scan.js.map +0 -1
- package/dist/tools/what-should-i-know.d.ts.map +0 -1
- package/dist/tools/what-should-i-know.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- 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
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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(
|
|
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
|
-
//
|
|
47
|
-
|
|
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
|
|
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;
|