@4da/mcp-server 5.0.4 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/README.md +70 -39
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -58
  6. package/dist/db.js +226 -299
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +12 -1
  51. package/dist/live/osv-scanner.js +50 -15
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -136
  158. package/package.json +35 -35
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -0,0 +1,197 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Which of this project's dependencies a task touches, and what is known
4
+ * about each — the core of the what_should_i_know briefing.
5
+ *
6
+ * Measured 2026-10-01: asked about "bump fastembed 5 -> 7", the briefing
7
+ * returned eleven advisories and none about fastembed. It never asked which
8
+ * packages the task names; it merged feed items by recency and keyword class,
9
+ * so a 7-day-old `fastembed v7.1.0` release row was outside its 72-hour window
10
+ * while an arXiv dataset paper and three OpenAI-company headlines were in.
11
+ * This module starts from the task instead: the dependencies it names (exact
12
+ * package names from the resolved dependency set), then facts about exactly
13
+ * those packages.
14
+ */
15
+ import { isActionableVulnerability } from "../live/maintenance.js";
16
+ import { presentedSeverity } from "../live/severity-scope.js";
17
+ import { compareVersions } from "../live/version-compare.js";
18
+ /**
19
+ * Package names that are also everyday words. One of these counts as named
20
+ * only with a qualifier beside it — "the next crate", "`next`", "upgrade
21
+ * next to 15" — so "the next step" does not drag the `next` package in.
22
+ */
23
+ export const COMMON_WORDS = new Set([
24
+ "next", "open", "debug", "ms", "ai", "test", "core", "util", "utils", "path", "fs", "os", "url", "http",
25
+ "log", "time", "rand", "bytes", "once", "glob", "which", "yes", "color", "colors", "async", "events",
26
+ "image", "regex", "net", "io", "fetch", "form", "data", "config", "cache", "queue", "stream",
27
+ "buffer", "assert", "base", "env", "error", "errors", "string", "table", "tree",
28
+ "safe", "lock", "slash", "sort", "type", "types", "web",
29
+ ]);
30
+ const QUALIFIERS = /^(?:crate|crates|package|packages|pkg|dependency|dependencies|dep|deps|library|lib|module|version|upgrade|upgrading|bump|bumping|update|updating|migrate|migrating|install|v?\d[\w.*-]*)$/i;
31
+ const VERSION_TOKEN = /^v?\d+(?:\.(?:\d+|x|\*))*(?:\.\d+)?(?:-[\w.]+)?$/i;
32
+ /** Lowercased word-ish tokens, keeping package punctuation (@scope/name, a-b_c.d). */
33
+ function tokenize(text) {
34
+ return (text.match(/[@A-Za-z0-9][\w./@*-]*/g) ?? []).map((t) => t.replace(/[.,;:)]+$/, "").toLowerCase());
35
+ }
36
+ /** Spellings a token may name a dependency by. */
37
+ function nameKeys(dep) {
38
+ const name = dep.name.toLowerCase();
39
+ const keys = new Set([name]);
40
+ if (dep.ecosystem === "crates.io") {
41
+ keys.add(name.replace(/-/g, "_"));
42
+ keys.add(name.replace(/_/g, "-"));
43
+ }
44
+ if (dep.ecosystem === "PyPI")
45
+ keys.add(name.replace(/[-_.]+/g, "-"));
46
+ if (dep.ecosystem === "Go") {
47
+ const last = name.split("/").pop() ?? name;
48
+ if (last.length >= 3 && !/^v\d+$/.test(last))
49
+ keys.add(last);
50
+ }
51
+ return [...keys];
52
+ }
53
+ /**
54
+ * The dependencies a task (and its file list) names. Exact names only; a
55
+ * common-word name needs a qualifier within two tokens or code quoting.
56
+ * Direct dependencies first; at most `limit` packages.
57
+ */
58
+ export function detectTaskPackages(task, files, deps, limit = 5) {
59
+ const quoted = new Set((task.match(/`([^`]+)`/g) ?? []).map((q) => q.slice(1, -1).toLowerCase()));
60
+ const tokens = tokenize(`${task} ${files.join(" ")}`);
61
+ const byKey = new Map();
62
+ for (const dep of deps) {
63
+ if (!dep.name)
64
+ continue;
65
+ for (const key of nameKeys(dep))
66
+ byKey.set(key, [...(byKey.get(key) ?? []), dep]);
67
+ }
68
+ const found = new Map();
69
+ tokens.forEach((token, i) => {
70
+ const matches = byKey.get(token);
71
+ if (!matches)
72
+ return;
73
+ if (COMMON_WORDS.has(token) && !quoted.has(token)) {
74
+ const window = [...tokens.slice(Math.max(0, i - 2), i), ...tokens.slice(i + 1, i + 3)];
75
+ if (!window.some((w) => QUALIFIERS.test(w)))
76
+ return;
77
+ }
78
+ const key = `${matches[0].ecosystem}\0${matches[0].name}`;
79
+ if (!found.has(key))
80
+ found.set(key, { deps: matches, at: i });
81
+ });
82
+ const packages = [...found.values()].map(({ deps: copies, at }) => {
83
+ const versions = tokens.slice(at + 1, at + 8).filter((t) => VERSION_TOKEN.test(t));
84
+ const first = copies[0];
85
+ return {
86
+ name: first.name,
87
+ ecosystem: first.ecosystem,
88
+ installed: [...new Set(copies.map((d) => d.version).filter((v) => Boolean(v)))],
89
+ direct: copies.some((d) => d.isDirect),
90
+ dev: copies.every((d) => d.isDev),
91
+ requested_from: versions.length >= 2 ? versions[0] : null,
92
+ requested_to: versions.length >= 1 ? versions[versions.length - 1] : null,
93
+ };
94
+ });
95
+ packages.sort((a, b) => Number(b.direct) - Number(a.direct));
96
+ return packages.slice(0, limit);
97
+ }
98
+ /** Major version of a version or a `7.x` / `v7` request; null when unreadable. */
99
+ export function majorOf(version) {
100
+ const m = version ? /^v?(\d+)/.exec(version) : null;
101
+ return m ? Number(m[1]) : null;
102
+ }
103
+ /** Actionable advisories on the installed copies of one package, highest presented severity first. */
104
+ export function advisoriesFor(pkg, vulns) {
105
+ const rank = { critical: 4, high: 3, medium: 2, low: 1, unknown: 0 };
106
+ return vulns
107
+ .filter((v) => v.package === pkg.name && v.ecosystem === pkg.ecosystem && isActionableVulnerability(v))
108
+ .map((v) => ({
109
+ id: v.vulnId,
110
+ installed: v.currentVersion,
111
+ severity: presentedSeverity(v),
112
+ fixed_version: v.fixedVersion,
113
+ summary: v.summary.length > 160 ? `${v.summary.slice(0, 157)}...` : v.summary,
114
+ }))
115
+ .sort((a, b) => rank[b.severity] - rank[a.severity]);
116
+ }
117
+ const RELEASE_SOURCES = {
118
+ "crates.io": { source: "crates_io", prefix: "crates.io: " },
119
+ npm: { source: "npm_registry", prefix: "npm: " },
120
+ };
121
+ /**
122
+ * Registry release rows the desktop app's feed holds for one package, newer
123
+ * than the installed version. Matched on the registry's own title format
124
+ * (`crates.io: fastembed v7.1.0`), never on a free-text mention. Empty in
125
+ * standalone mode (no feed) or for ecosystems the feed does not watch.
126
+ */
127
+ export function releasesFor(db, pkg) {
128
+ const spec = RELEASE_SOURCES[pkg.ecosystem];
129
+ if (!spec)
130
+ return [];
131
+ let rows;
132
+ try {
133
+ rows = db
134
+ .getRawDb()
135
+ .prepare(`SELECT title, url, created_at FROM source_items
136
+ WHERE source_type = ? AND title LIKE ? ORDER BY created_at DESC LIMIT 50`)
137
+ .all(spec.source, `${spec.prefix}${pkg.name} v%`);
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ const newest = pkg.installed.reduce((max, v) => (max === null || compareVersions(v, max, pkg.ecosystem) > 0 ? v : max), null);
143
+ const seen = new Set();
144
+ const out = [];
145
+ for (const row of rows) {
146
+ const version = row.title.slice(`${spec.prefix}${pkg.name} v`.length).trim();
147
+ if (!version || seen.has(version) || (row.title !== `${spec.prefix}${pkg.name} v${version}`))
148
+ continue;
149
+ if (newest && compareVersions(version, newest, pkg.ecosystem) <= 0)
150
+ continue;
151
+ seen.add(version);
152
+ out.push({ version, seen: row.created_at, url: row.url });
153
+ }
154
+ return out.sort((a, b) => compareVersions(b.version, a.version, pkg.ecosystem)).slice(0, 5);
155
+ }
156
+ /**
157
+ * Feed items about the task's packages that the app's relevance judge
158
+ * accepted (latest `llm_judgments` verdict >= 0.5) and that the feed kept
159
+ * (`feed_relevant` = 1). Exact package-name match on a word boundary in the
160
+ * title. Third-party text: reported as data. Empty without the desktop feed.
161
+ */
162
+ export function judgedReadingFor(db, packages, limit = 5) {
163
+ if (packages.length === 0)
164
+ return [];
165
+ const raw = db.getRawDb();
166
+ let rows;
167
+ try {
168
+ rows = raw
169
+ .prepare(`WITH latest AS (
170
+ SELECT source_item_id, relevance_score,
171
+ ROW_NUMBER() OVER (PARTITION BY source_item_id ORDER BY id DESC) AS rn
172
+ FROM llm_judgments)
173
+ SELECT s.id, s.title, s.url, s.source_type, l.relevance_score AS verdict, s.created_at
174
+ FROM source_items s JOIN latest l ON l.source_item_id = s.id AND l.rn = 1
175
+ WHERE s.feed_relevant = 1 AND l.relevance_score >= 0.5
176
+ AND s.created_at > datetime('now', '-30 days')
177
+ ORDER BY l.relevance_score DESC, s.created_at DESC LIMIT 2000`)
178
+ .all();
179
+ }
180
+ catch {
181
+ return [];
182
+ }
183
+ const patterns = packages.map((p) => ({
184
+ name: p.name,
185
+ re: new RegExp(`(^|[^\\w@/-])${p.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "i"),
186
+ }));
187
+ const out = [];
188
+ for (const row of rows) {
189
+ const hit = patterns.find((p) => p.re.test(row.title));
190
+ if (!hit)
191
+ continue;
192
+ out.push({ id: row.id, title: row.title, url: row.url, source: row.source_type, judge_relevance: Math.round(row.verdict * 100) / 100, package: hit.name });
193
+ if (out.length >= limit)
194
+ break;
195
+ }
196
+ return out;
197
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Wisdom retrieval for the what_should_i_know briefing.
3
+ *
4
+ * Ranks the developer's recorded decisions and agent memories against the
5
+ * task — lexically by default, blended with embedding similarity when a
6
+ * provider is configured. Split out of what-should-i-know.ts, which is the
7
+ * briefing's assembly and delegation logic.
8
+ */
9
+ import type { FourDADatabase } from "../db.js";
10
+ import { type EmbeddingConfig } from "../embeddings.js";
11
+ export interface WisdomEntry {
12
+ type: string;
13
+ subject: string;
14
+ detail: string;
15
+ }
16
+ export type WisdomRecallMode = "hybrid" | "ranked_lexical";
17
+ /**
18
+ * Lexical wisdom retrieval (the default, provider-free path): rank decisions and
19
+ * memories independently, merge by score, take the top entries.
20
+ */
21
+ export declare function getRelevantWisdom(db: FourDADatabase, task: string, files: string[]): WisdomEntry[];
22
+ /**
23
+ * Hybrid wisdom retrieval: blends alias-aware lexical scoring with embedding
24
+ * cosine similarity (per table, since each is normalized independently), so a
25
+ * paraphrased prior decision or memory surfaces in the briefing even with no
26
+ * shared words. Falls back to pure lexical (and reports it) when nothing embeds.
27
+ */
28
+ export declare function getRelevantWisdomHybrid(db: FourDADatabase, task: string, files: string[], config: EmbeddingConfig): Promise<{
29
+ wisdom: WisdomEntry[];
30
+ recall_mode: WisdomRecallMode;
31
+ }>;
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Wisdom retrieval for the what_should_i_know briefing.
4
+ *
5
+ * Ranks the developer's recorded decisions and agent memories against the
6
+ * task — lexically by default, blended with embedding similarity when a
7
+ * provider is configured. Split out of what-should-i-know.ts, which is the
8
+ * briefing's assembly and delegation logic.
9
+ */
10
+ import { rankRowsByRecall } from "./recall.js";
11
+ import { semanticScores } from "../embeddings.js";
12
+ import { decisionEmbedText } from "./decision-recall.js";
13
+ import { memoryEmbedText } from "./agent-memory.js";
14
+ /** Weighted fields for ranking decisions (mirrors the decision tools' weights). */
15
+ const WISDOM_DECISION_FIELDS = [
16
+ { name: "alternatives", weight: 5, value: (row) => row.alternatives_rejected },
17
+ { name: "subject", weight: 4, value: (row) => row.subject },
18
+ { name: "tags", weight: 3, value: (row) => row.context_tags },
19
+ { name: "decision", weight: 2, value: (row) => row.decision },
20
+ { name: "rationale", weight: 1, value: (row) => row.rationale },
21
+ ];
22
+ /** Weighted fields for ranking memories. */
23
+ const WISDOM_MEMORY_FIELDS = [
24
+ { name: "subject", weight: 4, value: (row) => row.subject },
25
+ { name: "tags", weight: 3, value: (row) => row.context_tags },
26
+ { name: "content", weight: 2, value: (row) => row.content },
27
+ { name: "type", weight: 1, value: (row) => row.memory_type },
28
+ ];
29
+ const WISDOM_LIMIT = 6;
30
+ /** Blend weight for semantic cosine vs normalized lexical score in hybrid wisdom. */
31
+ const WISDOM_BLEND = 0.65;
32
+ function loadWisdomDecisions(rawDb) {
33
+ try {
34
+ return rawDb
35
+ .prepare(`SELECT id, subject, decision, rationale, alternatives_rejected, context_tags, updated_at
36
+ FROM developer_decisions
37
+ WHERE status = 'active'
38
+ ORDER BY updated_at DESC
39
+ LIMIT 300`)
40
+ .all();
41
+ }
42
+ catch {
43
+ return []; // Older DBs may not have decision memory yet.
44
+ }
45
+ }
46
+ function loadWisdomMemories(rawDb) {
47
+ try {
48
+ return rawDb
49
+ .prepare(`SELECT id, memory_type, subject, content, context_tags, created_at
50
+ FROM agent_memory
51
+ WHERE (expires_at IS NULL OR expires_at > datetime('now'))
52
+ ORDER BY created_at DESC
53
+ LIMIT 300`)
54
+ .all();
55
+ }
56
+ catch {
57
+ return []; // Older DBs may not have agent memory yet.
58
+ }
59
+ }
60
+ /** Map a ranked decision/memory row to the public WisdomEntry shape. */
61
+ function toWisdomEntry(item) {
62
+ if (item.type === "decision") {
63
+ const row = item.row;
64
+ return {
65
+ type: "decision",
66
+ subject: row.subject,
67
+ detail: row.rationale ? `${row.decision} Rationale: ${row.rationale}` : row.decision,
68
+ };
69
+ }
70
+ const row = item.row;
71
+ return {
72
+ type: `memory:${row.memory_type}`,
73
+ subject: row.subject,
74
+ detail: row.content,
75
+ };
76
+ }
77
+ /**
78
+ * Lexical wisdom retrieval (the default, provider-free path): rank decisions and
79
+ * memories independently, merge by score, take the top entries.
80
+ */
81
+ export function getRelevantWisdom(db, task, files) {
82
+ const rawDb = db.getRawDb();
83
+ const query = [task, ...files].join(" ");
84
+ const entries = [
85
+ ...rankRowsByRecall(loadWisdomDecisions(rawDb), query, WISDOM_DECISION_FIELDS, WISDOM_LIMIT).map((item) => ({ ...item, type: "decision" })),
86
+ ...rankRowsByRecall(loadWisdomMemories(rawDb), query, WISDOM_MEMORY_FIELDS, WISDOM_LIMIT).map((item) => ({ ...item, type: "memory" })),
87
+ ];
88
+ return entries
89
+ .sort((a, b) => b.score - a.score)
90
+ .slice(0, WISDOM_LIMIT)
91
+ .map(toWisdomEntry);
92
+ }
93
+ /**
94
+ * Hybrid wisdom retrieval: blends alias-aware lexical scoring with embedding
95
+ * cosine similarity (per table, since each is normalized independently), so a
96
+ * paraphrased prior decision or memory surfaces in the briefing even with no
97
+ * shared words. Falls back to pure lexical (and reports it) when nothing embeds.
98
+ */
99
+ export async function getRelevantWisdomHybrid(db, task, files, config) {
100
+ const rawDb = db.getRawDb();
101
+ const query = [task, ...files].join(" ");
102
+ const decisions = loadWisdomDecisions(rawDb);
103
+ const memories = loadWisdomMemories(rawDb);
104
+ // Lexical baselines over ALL rows, per table, for normalization + fallback.
105
+ const decLex = rankRowsByRecall(decisions, query, WISDOM_DECISION_FIELDS, decisions.length);
106
+ const memLex = rankRowsByRecall(memories, query, WISDOM_MEMORY_FIELDS, memories.length);
107
+ const decLexById = new Map(decLex.map((i) => [i.row.id, i.score]));
108
+ const memLexById = new Map(memLex.map((i) => [i.row.id, i.score]));
109
+ const decMax = decLex.length ? decLex[0].score : 0;
110
+ const memMax = memLex.length ? memLex[0].score : 0;
111
+ const decSem = decisions.length
112
+ ? await semanticScores(db, "developer_decisions", query, decisions.map((d) => ({ id: d.id, text: decisionEmbedText(d) })), config)
113
+ : null;
114
+ const memSem = memories.length
115
+ ? await semanticScores(db, "agent_memory", query, memories.map((m) => ({ id: m.id, text: memoryEmbedText(m) })), config)
116
+ : null;
117
+ const anySemantic = (decSem?.embeddedCount ?? 0) > 0 || (memSem?.embeddedCount ?? 0) > 0;
118
+ if (!anySemantic) {
119
+ // Provider unreachable / nothing embedded -> behave exactly like lexical.
120
+ const entries = [
121
+ ...decLex.slice(0, WISDOM_LIMIT).map((i) => ({ ...i, type: "decision" })),
122
+ ...memLex.slice(0, WISDOM_LIMIT).map((i) => ({ ...i, type: "memory" })),
123
+ ];
124
+ return {
125
+ wisdom: entries.sort((a, b) => b.score - a.score).slice(0, WISDOM_LIMIT).map(toWisdomEntry),
126
+ recall_mode: "ranked_lexical",
127
+ };
128
+ }
129
+ const scored = [];
130
+ for (const d of decisions) {
131
+ const semantic = Math.max(0, decSem?.semanticById.get(d.id) ?? 0);
132
+ const lexNorm = decMax > 0 ? (decLexById.get(d.id) ?? 0) / decMax : 0;
133
+ const score = WISDOM_BLEND * semantic + (1 - WISDOM_BLEND) * lexNorm;
134
+ if (score > 0)
135
+ scored.push({ type: "decision", row: d, score });
136
+ }
137
+ for (const m of memories) {
138
+ const semantic = Math.max(0, memSem?.semanticById.get(m.id) ?? 0);
139
+ const lexNorm = memMax > 0 ? (memLexById.get(m.id) ?? 0) / memMax : 0;
140
+ const score = WISDOM_BLEND * semantic + (1 - WISDOM_BLEND) * lexNorm;
141
+ if (score > 0)
142
+ scored.push({ type: "memory", row: m, score });
143
+ }
144
+ return {
145
+ wisdom: scored.sort((a, b) => b.score - a.score).slice(0, WISDOM_LIMIT).map(toWisdomEntry),
146
+ recall_mode: "hybrid",
147
+ };
148
+ }
@@ -16,7 +16,6 @@ import type { FourDADatabase } from "../db.js";
16
16
  export interface CheckDecisionAlignmentParams {
17
17
  technology: string;
18
18
  pattern?: string;
19
- context?: string;
20
19
  }
21
20
  interface RelevantDecision {
22
21
  id: number;
@@ -62,10 +61,6 @@ export declare const checkDecisionAlignmentTool: {
62
61
  type: string;
63
62
  description: string;
64
63
  };
65
- context: {
66
- type: string;
67
- description: string;
68
- };
69
64
  };
70
65
  required: string[];
71
66
  };
@@ -33,10 +33,6 @@ export const checkDecisionAlignmentTool = {
33
33
  type: "string",
34
34
  description: "Architecture pattern to check (e.g., 'microservices', 'event-driven')",
35
35
  },
36
- context: {
37
- type: "string",
38
- description: "Additional context about the proposed change",
39
- },
40
36
  },
41
37
  required: ["technology"],
42
38
  },
@@ -5,9 +5,7 @@
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
+ import { executeCheckDecisionAlignment } from "./decision-enforcement.js";
11
9
  // ============================================================================
12
10
  // Tool Definition
13
11
  // ============================================================================
@@ -73,11 +71,11 @@ export const decisionMemoryTool = {
73
71
  },
74
72
  technology: {
75
73
  type: "string",
76
- description: "Technology to check alignment for",
74
+ description: "Technology to check alignment for (check_alignment)",
77
75
  },
78
76
  pattern: {
79
77
  type: "string",
80
- description: "Pattern to check alignment for",
78
+ description: "Architecture pattern to check alongside the technology (check_alignment)",
81
79
  },
82
80
  id: {
83
81
  type: "number",
@@ -122,47 +120,6 @@ function parseDecisionRow(row) {
122
120
  context_tags: JSON.parse(row.context_tags || "[]"),
123
121
  };
124
122
  }
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
- }
166
123
  // ============================================================================
167
124
  // Execute
168
125
  // ============================================================================
@@ -216,25 +173,12 @@ export function executeDecisionMemory(db, params) {
216
173
  error: "technology is required for check_alignment action",
217
174
  };
218
175
  }
219
- const candidateRows = rawDb
220
- .prepare(`SELECT id, decision_type, subject, decision, rationale,
221
- alternatives_rejected, context_tags, confidence,
222
- status, superseded_by, created_at, updated_at
223
- FROM developer_decisions
224
- WHERE status = 'active'
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);
235
- }
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));
176
+ // One implementation for both entry points. This action kept its own
177
+ // copy, which ignored `pattern` and drifted from check_decision_alignment.
178
+ return executeCheckDecisionAlignment(db, {
179
+ technology: params.technology,
180
+ pattern: params.pattern,
181
+ });
238
182
  }
239
183
  case "update": {
240
184
  if (!params.id) {
@@ -263,9 +207,12 @@ export function executeDecisionMemory(db, params) {
263
207
  }
264
208
  sets.push("updated_at = datetime('now')");
265
209
  values.push(params.id);
266
- rawDb
210
+ const updated = rawDb
267
211
  .prepare(`UPDATE developer_decisions SET ${sets.join(", ")} WHERE id = ?`)
268
212
  .run(...values);
213
+ if (updated.changes === 0) {
214
+ return { error: `No decision with id ${params.id}. Use action "list" to find ids.` };
215
+ }
269
216
  return {
270
217
  success: true,
271
218
  message: `Decision ${params.id} updated`,
@@ -277,6 +224,11 @@ export function executeDecisionMemory(db, params) {
277
224
  error: "old_id and new_id are required for supersede action",
278
225
  };
279
226
  }
227
+ const exists = (id) => rawDb.prepare(`SELECT 1 FROM developer_decisions WHERE id = ?`).get(id) !== undefined;
228
+ const missing = [params.old_id, params.new_id].filter((id) => !exists(id));
229
+ if (missing.length > 0) {
230
+ return { error: `No decision with id ${missing.join(" or ")}. Use action "list" to find ids.` };
231
+ }
280
232
  rawDb
281
233
  .prepare(`UPDATE developer_decisions
282
234
  SET status = 'superseded',
@@ -0,0 +1,74 @@
1
+ /**
2
+ * dependency_check signal evaluation — pure functions, no I/O.
3
+ *
4
+ * Each signal states a fact with its evidence and the verdict that fact
5
+ * implies on its own (`effect`). The item verdict is the strongest effect by
6
+ * a fixed precedence. `unknown` sits above `wait`/`proceed` and below
7
+ * `review`/`avoid`: a concrete negative finding stays actionable even when
8
+ * another source was unreachable, but a missing source never lets the
9
+ * verdict fall through to `proceed`.
10
+ */
11
+ import type { PackageReleaseIndex, ReleaseRecord } from "../live/release-metadata.js";
12
+ export type Verdict = "proceed" | "wait" | "review" | "avoid" | "unknown";
13
+ export type SignalEffect = Verdict | "info";
14
+ export interface CheckItem {
15
+ ecosystem: "npm" | "crates.io";
16
+ package: string;
17
+ to: string;
18
+ from?: string;
19
+ }
20
+ export interface Signal {
21
+ id: "advisories" | "release_age" | "publish_trust" | "install_script_added" | "new_dependencies" | "yanked_or_deprecated" | "upgrade_type";
22
+ value: unknown;
23
+ evidence: string;
24
+ effect: SignalEffect;
25
+ }
26
+ export interface AdvisoryRef {
27
+ id: string;
28
+ severity: "critical" | "high" | "medium" | "low" | "unknown";
29
+ summary: string;
30
+ /** Unmaintained-crate notice rather than an exploitable vulnerability. */
31
+ maintenance: boolean;
32
+ }
33
+ export type AdvisoryFacts = {
34
+ available: true;
35
+ to: AdvisoryRef[];
36
+ from: AdvisoryRef[];
37
+ } | {
38
+ available: false;
39
+ detail: string;
40
+ };
41
+ /** A newly introduced dependency's first-publish lookup. */
42
+ export type CreatedAtFact = {
43
+ status: "ok";
44
+ createdAt: string | null;
45
+ } | {
46
+ status: "not_found";
47
+ detail: string;
48
+ } | {
49
+ status: "unreachable";
50
+ detail: string;
51
+ };
52
+ export declare const WAIT_DAYS = 3;
53
+ export declare const NEW_DEP_MIN_AGE_DAYS = 30;
54
+ /** The release `to` is judged against: the installed `from`, else the previous release on `to`'s line. */
55
+ export declare function selectBaseline(index: PackageReleaseIndex, item: CheckItem): {
56
+ record: ReleaseRecord | null;
57
+ kind: "from" | "previous_on_line";
58
+ note: string;
59
+ };
60
+ /** Runtime dependency names in `to` that the baseline did not have; null when either list is unknown. */
61
+ export declare function introducedDependencies(to: ReleaseRecord, base: ReleaseRecord | null): string[] | null;
62
+ export declare function advisoriesSignal(item: CheckItem, facts: AdvisoryFacts): Signal;
63
+ export declare function releaseAgeSignal(item: CheckItem, to: ReleaseRecord, advisories: Signal, now: Date): Signal;
64
+ export declare function publishTrustSignal(item: CheckItem, to: ReleaseRecord, base: ReturnType<typeof selectBaseline>): Signal;
65
+ export declare function installScriptSignal(item: CheckItem, to: ReleaseRecord, from: ReleaseRecord | null): Signal;
66
+ export declare function newDependenciesSignal(to: ReleaseRecord, base: ReturnType<typeof selectBaseline>, introduced: string[] | null, created: Map<string, CreatedAtFact>): Signal;
67
+ export declare function withdrawnSignal(item: CheckItem, to: ReleaseRecord): Signal;
68
+ export declare function upgradeTypeSignal(item: CheckItem): Signal;
69
+ export declare function upgradeType(from: string | undefined, to: string): "patch" | "minor" | "major" | "downgrade" | "none" | "new_dependency" | "unknown";
70
+ /** The item verdict and its one-line reason, from the strongest signal effect. */
71
+ export declare function decide(signals: Signal[]): {
72
+ verdict: Verdict;
73
+ reason: string;
74
+ };