@4da/mcp-server 5.0.3 → 6.0.0

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 +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +242 -36
  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 +31 -1
  51. package/dist/live/osv-scanner.js +119 -10
  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 -1
  158. package/package.json +27 -30
  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,63 @@
1
+ /**
2
+ * Knowledge-gap scope: which projects count as active, and which items the
3
+ * dependency linker bound to which packages. Mirrors the desktop app's
4
+ * `temporal::{active_repo_roots, dep_within_active_root, shares_active_root}`
5
+ * and `knowledge_decay::{get_active_project_paths, linked_to}`.
6
+ */
7
+ import type BetterSqlite3 from "better-sqlite3";
8
+ import type { FourDADatabase } from "../db.js";
9
+ /** `stmt.all(...)`, or [] when the statement cannot run: a read here never throws out of the tool. */
10
+ export declare function allRows<T>(stmt: BetterSqlite3.Statement, ...args: unknown[]): T[];
11
+ /** Lowercase, forward slashes, no trailing slash (`knowledge_decay::normalize_project_path`). */
12
+ export declare function normPath(p: string): string;
13
+ /**
14
+ * Path-BOUNDARY match in either direction: `d:/4da` covers `d:/4da/src-tauri`
15
+ * (and a manifest recorded above its repo root still counts), but never
16
+ * `d:/4da-experiments` (`temporal::dep_within_active_root`).
17
+ */
18
+ export declare function withinRoots(path: string, roots: readonly string[]): boolean;
19
+ /** Do two project paths sit under one common active root (`temporal::shares_active_root`)? */
20
+ export declare function sharesRoot(a: string, b: string, roots: readonly string[]): boolean;
21
+ /** The app's two activity windows, loaded once per call. */
22
+ export interface ActiveScope {
23
+ /** `temporal::active_repo_roots`: commits in the last 60 days. Scopes each project ROW. */
24
+ rowRoots: string[];
25
+ /** `knowledge_decay::get_active_project_paths`: activity in the last 30 days. Scopes each DEPENDENCY. */
26
+ dependencyRoots: string[];
27
+ }
28
+ export declare function loadActiveScope(db: FourDADatabase): ActiveScope;
29
+ /**
30
+ * The project rows the app's dependency funnel keeps before knowledge decay
31
+ * ever sees them (`temporal::get_all_dependencies`): rows inside an active repo
32
+ * root. Without it a gap for a dependency the active repo shares with a
33
+ * dormant one is judged on the dormant copy too — vitest in `d:/4da` (4.1.11)
34
+ * beside navcal's untouched 3.2.4 would name navcal under a critical the app
35
+ * never shows. No recorded git activity means unscoped, and a scope that
36
+ * matches no row at all keeps every row rather than blanking the tool.
37
+ */
38
+ export declare function scopeRows<T extends {
39
+ project_path: string;
40
+ }>(rows: T[], scope: ActiveScope): T[];
41
+ /**
42
+ * `knowledge_decay::detect_knowledge_gaps` active-project scoping: a
43
+ * dependency whose declaring projects all sit outside every recently active
44
+ * path is skipped. Measured 2026-09-11: axios and socket.io were reported
45
+ * critical from `kairos-mvp/backend`, a project untouched since 2025-10.
46
+ */
47
+ export declare function dependencyIsActive(rows: ReadonlyArray<{
48
+ project_path: string;
49
+ }>, scope: ActiveScope): boolean;
50
+ /** Items the dependency linker bound to a package with STRUCTURED proof. */
51
+ export interface LinkerIndex {
52
+ /** Ids of the items bound to this package (case- and `-`/`_`-folded). */
53
+ itemsFor(packageName: string): ReadonlySet<number>;
54
+ }
55
+ /**
56
+ * `source_item_dependencies` rows carrying structured proof: `advisory` (the
57
+ * advisory's own affected-package list) or `exact_registry`. A registry
58
+ * advisory cites a dependency only through this proof, never through a title
59
+ * word (#618; `knowledge_decay::linked_to`). Null when the table or its
60
+ * `match_type` column is absent: callers then fall back to the advisory's
61
+ * subject package, since heuristic rows cannot be told apart from proof.
62
+ */
63
+ export declare function loadLinkerIndex(db: FourDADatabase): LinkerIndex | null;
@@ -0,0 +1,118 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Knowledge-gap scope: which projects count as active, and which items the
4
+ * dependency linker bound to which packages. Mirrors the desktop app's
5
+ * `temporal::{active_repo_roots, dep_within_active_root, shares_active_root}`
6
+ * and `knowledge_decay::{get_active_project_paths, linked_to}`.
7
+ */
8
+ /** `stmt.all(...)`, or [] when the statement cannot run: a read here never throws out of the tool. */
9
+ export function allRows(stmt, ...args) {
10
+ try {
11
+ return stmt.all(...args);
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ /** Lowercase, forward slashes, no trailing slash (`knowledge_decay::normalize_project_path`). */
18
+ export function normPath(p) {
19
+ return p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
20
+ }
21
+ /**
22
+ * Repo roots with git activity in the last `days` days, normalized. The
23
+ * `git_signals.repo_path` column is stored raw ("D:\4DA"). Empty when the
24
+ * table is absent or silent, which every caller reads as "unscoped".
25
+ */
26
+ function activeRoots(db, days, requireCommit) {
27
+ if (!db.hasColumn("git_signals", "repo_path") || !db.hasColumn("git_signals", "timestamp"))
28
+ return [];
29
+ const commit = requireCommit && db.hasColumn("git_signals", "commit_hash")
30
+ ? "AND commit_hash IS NOT NULL AND commit_hash != ''"
31
+ : "";
32
+ try {
33
+ const rows = db
34
+ .getRawDb()
35
+ .prepare(`SELECT DISTINCT repo_path FROM git_signals WHERE timestamp > datetime('now', ?) ${commit}`)
36
+ .all(`-${days} days`);
37
+ return [...new Set(rows.map((r) => normPath(r.repo_path ?? "")).filter((p) => p !== ""))];
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ /**
44
+ * Path-BOUNDARY match in either direction: `d:/4da` covers `d:/4da/src-tauri`
45
+ * (and a manifest recorded above its repo root still counts), but never
46
+ * `d:/4da-experiments` (`temporal::dep_within_active_root`).
47
+ */
48
+ export function withinRoots(path, roots) {
49
+ const p = normPath(path);
50
+ if (p === "")
51
+ return false;
52
+ return roots.some((root) => p === root || p.startsWith(`${root}/`) || root.startsWith(`${p}/`));
53
+ }
54
+ /** Do two project paths sit under one common active root (`temporal::shares_active_root`)? */
55
+ export function sharesRoot(a, b, roots) {
56
+ return roots.some((root) => withinRoots(a, [root]) && withinRoots(b, [root]));
57
+ }
58
+ export function loadActiveScope(db) {
59
+ return { rowRoots: activeRoots(db, 60, true), dependencyRoots: activeRoots(db, 30, false) };
60
+ }
61
+ /**
62
+ * The project rows the app's dependency funnel keeps before knowledge decay
63
+ * ever sees them (`temporal::get_all_dependencies`): rows inside an active repo
64
+ * root. Without it a gap for a dependency the active repo shares with a
65
+ * dormant one is judged on the dormant copy too — vitest in `d:/4da` (4.1.11)
66
+ * beside navcal's untouched 3.2.4 would name navcal under a critical the app
67
+ * never shows. No recorded git activity means unscoped, and a scope that
68
+ * matches no row at all keeps every row rather than blanking the tool.
69
+ */
70
+ export function scopeRows(rows, scope) {
71
+ if (scope.rowRoots.length === 0)
72
+ return rows;
73
+ const kept = rows.filter((r) => withinRoots(r.project_path, scope.rowRoots));
74
+ return kept.length > 0 ? kept : rows;
75
+ }
76
+ /**
77
+ * `knowledge_decay::detect_knowledge_gaps` active-project scoping: a
78
+ * dependency whose declaring projects all sit outside every recently active
79
+ * path is skipped. Measured 2026-09-11: axios and socket.io were reported
80
+ * critical from `kairos-mvp/backend`, a project untouched since 2025-10.
81
+ */
82
+ export function dependencyIsActive(rows, scope) {
83
+ return scope.dependencyRoots.length === 0 || rows.some((r) => withinRoots(r.project_path, scope.dependencyRoots));
84
+ }
85
+ const foldName = (name) => name.toLowerCase().replace(/_/g, "-");
86
+ const NO_ITEMS = new Set();
87
+ /**
88
+ * `source_item_dependencies` rows carrying structured proof: `advisory` (the
89
+ * advisory's own affected-package list) or `exact_registry`. A registry
90
+ * advisory cites a dependency only through this proof, never through a title
91
+ * word (#618; `knowledge_decay::linked_to`). Null when the table or its
92
+ * `match_type` column is absent: callers then fall back to the advisory's
93
+ * subject package, since heuristic rows cannot be told apart from proof.
94
+ */
95
+ export function loadLinkerIndex(db) {
96
+ const has = (column) => db.hasColumn("source_item_dependencies", column);
97
+ if (!has("source_item_id") || !has("package_name") || !has("match_type"))
98
+ return null;
99
+ const byPackage = new Map();
100
+ try {
101
+ const rows = db
102
+ .getRawDb()
103
+ .prepare("SELECT source_item_id, package_name FROM source_item_dependencies WHERE match_type IN ('advisory', 'exact_registry')")
104
+ .all();
105
+ for (const row of rows) {
106
+ if (!row.package_name)
107
+ continue;
108
+ const key = foldName(row.package_name);
109
+ const ids = byPackage.get(key) ?? new Set();
110
+ ids.add(row.source_item_id);
111
+ byPackage.set(key, ids);
112
+ }
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ return { itemsFor: (name) => byPackage.get(foldName(name)) ?? NO_ITEMS };
118
+ }
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * knowledge_gaps tool
3
3
  *
4
- * Detect knowledge gaps - dependencies with relevant content you haven't engaged with.
4
+ * Detect knowledge gaps: direct dependencies with unread, consequential
5
+ * intelligence. The desktop app computes the same concept in
6
+ * `src-tauri/src/knowledge_decay.rs::detect_knowledge_gaps`, and every rule
7
+ * here names the function it mirrors, because two implementations disagreeing
8
+ * is the failure this tool keeps being audited for. Measured 2026-09-11 on the
9
+ * founder machine: five `critical` gaps, four false or inflated, every one
10
+ * contradicting the app.
5
11
  */
6
12
  import type { FourDADatabase } from "../db.js";
13
+ import type { LiveIntelligence } from "../live/index.js";
7
14
  import type { SourceItemBriefRow } from "../types.js";
15
+ export { advisorySubject, gradeGap, registryVersionFromTitle, type Exposure, type GapSeverity, type GradableItem, } from "./knowledge-gap-grading.js";
16
+ export { versionInAnyRange, type AdvisoryRangeEvent } from "./knowledge-gap-ranges.js";
8
17
  export interface KnowledgeGapsParams {
9
18
  min_severity?: string;
10
19
  limit?: number;
@@ -31,14 +40,14 @@ export declare const knowledgeGapsTool: {
31
40
  };
32
41
  export interface KnowledgeGap {
33
42
  dependency: string;
34
- version: string;
43
+ version: string | null;
35
44
  project_path: string;
36
45
  language: string;
37
46
  missed_items: SourceItemBriefRow[];
38
47
  gap_severity: string;
39
48
  missed_count: number;
40
49
  }
41
- export declare function executeKnowledgeGaps(db: FourDADatabase, params: KnowledgeGapsParams): {
50
+ export declare function executeKnowledgeGaps(db: FourDADatabase, params: KnowledgeGapsParams, liveIntel?: Pick<LiveIntelligence, "getResolvedDeps" | "isInitialized"> | null): {
42
51
  gaps: never[];
43
52
  summary: string;
44
53
  total_dependencies?: undefined;
@@ -2,29 +2,23 @@
2
2
  /**
3
3
  * knowledge_gaps tool
4
4
  *
5
- * Detect knowledge gaps - dependencies with relevant content you haven't engaged with.
5
+ * Detect knowledge gaps: direct dependencies with unread, consequential
6
+ * intelligence. The desktop app computes the same concept in
7
+ * `src-tauri/src/knowledge_decay.rs::detect_knowledge_gaps`, and every rule
8
+ * here names the function it mirrors, because two implementations disagreeing
9
+ * is the failure this tool keeps being audited for. Measured 2026-09-11 on the
10
+ * founder machine: five `critical` gaps, four false or inflated, every one
11
+ * contradicting the app.
6
12
  */
7
- // Word-boundary matching prevents "cve" matching inside "achieve", "receiver", etc.
8
- function hasWordBoundary(text, term) {
9
- const regex = new RegExp(`\\b${escapeRegExp(term)}\\b`, 'i');
10
- return regex.test(text);
11
- }
12
- // Package names can contain regex metacharacters (@scope/name, c++, next.js).
13
- function escapeRegExp(s) {
14
- return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
15
- }
16
- // True when `text` mentions the package name as a whole word. Word characters
17
- // for this purpose include - and _ (so dep "hono" does NOT match "hono-shim",
18
- // and never matches "in HONOr of"). @scope/name matches as the full literal.
19
- function mentionsPackage(text, pkg) {
20
- const regex = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(pkg)}($|[^A-Za-z0-9_-])`, "i");
21
- return regex.test(text);
22
- }
23
- // Escape SQL LIKE wildcards so a package name containing % or _ (npm names may
24
- // contain _) is matched literally rather than as a pattern. Paired with ESCAPE '\'.
25
- function escapeLike(s) {
26
- return s.replace(/[\\%_]/g, (c) => "\\" + c);
27
- }
13
+ import { citesDependency, loadCandidates, normalizeGapTitle } from "./knowledge-gap-citations.js";
14
+ import { AdvisoryStore, gapExposure, installExposure, reachingTier, rowVerdict, } from "./knowledge-gap-exposure.js";
15
+ import { InstallResolver } from "./knowledge-gap-installs.js";
16
+ import { gradeGap, isAdvisoryRow, parsePublishedAt, registryVersionFromTitle, } from "./knowledge-gap-grading.js";
17
+ import { compareVersions } from "../live/version-compare.js";
18
+ import { dependencyIsActive, loadActiveScope, loadLinkerIndex, scopeRows } from "./knowledge-gap-scope.js";
19
+ // The grading and range rules live in their own modules; re-exported for existing importers.
20
+ export { advisorySubject, gradeGap, registryVersionFromTitle, } from "./knowledge-gap-grading.js";
21
+ export { versionInAnyRange } from "./knowledge-gap-ranges.js";
28
22
  export const knowledgeGapsTool = {
29
23
  name: "knowledge_gaps",
30
24
  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.`,
@@ -45,18 +39,132 @@ export const knowledgeGapsTool = {
45
39
  },
46
40
  },
47
41
  };
48
- export function executeKnowledgeGaps(db, params) {
49
- const rawDb = db.getRawDb();
50
- // Feature-detect optional columns: a pure-standalone database has no scoring
51
- // pipeline (no relevance_score) and may predate content_type — the tool must
52
- // degrade to word-boundary + recency grounding there, never throw.
53
- const hasRelevance = db.hasColumn("source_items", "relevance_score");
54
- const hasContentType = db.hasColumn("source_items", "content_type");
55
- const hasIsDirect = db.hasColumn("project_dependencies", "is_direct");
42
+ const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
43
+ const MAX_MISSED_ITEMS = 5;
44
+ /**
45
+ * published_at guard: OSV/CVE backfills ingest decades-old advisories whose
46
+ * created_at (discovery) is days old but whose published_at is ancient. A 2021
47
+ * advisory is not "missed intelligence" in 2026 — unless an install is
48
+ * positively inside it: a still-applying advisory is missed intelligence
49
+ * however old (live: the jsonwebtoken crate advisory, published February,
50
+ * still open against relay/'s 9.3.1 in September). NULL passes (many sources
51
+ * never set it).
52
+ */
53
+ const PUBLISHED_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
54
+ function passesPublishedCut(e, exposure, cutoff) {
55
+ const publishedAt = parsePublishedAt(e.item.published_at);
56
+ if (publishedAt === null || publishedAt >= cutoff)
57
+ return true;
58
+ return e.verdict === "reached" || (e.verdict === "unresolvable" && exposure === "exposed");
59
+ }
60
+ /**
61
+ * Does this evidence keep a security signal alive? A resolved row that reaches
62
+ * an install does; so, conservatively, does a row whose install cannot be read,
63
+ * and a row the mirror cannot resolve while the package is not positively
64
+ * patched (AD-040 rule 3: only exposed installs are findings, and an unknown
65
+ * installed version stays conservatively exposed).
66
+ */
67
+ function stillReaches(e, exposure) {
68
+ return e.verdict === "reached" || e.verdict === "unknown" || (e.verdict === "unresolvable" && exposure !== "safe");
69
+ }
70
+ function advisoryVerdict(item, name, installs, store) {
71
+ const entries = store.forRow(item.source_id, name);
72
+ return entries === null ? "unresolvable" : rowVerdict(entries, installs);
73
+ }
74
+ const REGISTRY_SOURCES = new Set(["crates_io", "npm_registry", "pypi", "go_modules"]);
75
+ /** A registry row's version is above every readable installed version (unknown versions keep it). */
76
+ function newerThanEveryInstall(item, installs) {
77
+ const released = registryVersionFromTitle(item.title || "");
78
+ if (!released)
79
+ return true;
80
+ const known = installs.filter((i) => i.version && i.ecosystem);
81
+ if (known.length === 0)
82
+ return true;
83
+ return known.every((i) => compareVersions(released, i.version, i.ecosystem) > 0);
84
+ }
85
+ function brief(item) {
86
+ return {
87
+ id: item.id,
88
+ title: item.title && item.title.length > 120 ? `${item.title.substring(0, 120)}...` : item.title,
89
+ url: item.url,
90
+ source_type: item.source_type,
91
+ created_at: item.created_at,
92
+ relevance_score: item.relevance_score,
93
+ };
94
+ }
95
+ /** One dependency's gap, judged on every project that declares it, or null when nothing is missed. */
96
+ function buildGap(name, rows, ctx) {
97
+ const cited = ctx.candidates.filter((item) => citesDependency(item, name, ctx.linker));
98
+ if (cited.length === 0)
99
+ return null;
100
+ const installs = ctx.installs.installsFor(rows);
101
+ const stored = ctx.advisories.forPackage(name);
102
+ const exposure = gapExposure(installs, stored);
103
+ // An advisory row is judged LIVE on its own advisories against the installs
104
+ // this gap names (`osv::exposure::advisory_row_reaches`, AD-045): a row every
105
+ // install is outside of is not a gap at all. A row the mirror cannot resolve
106
+ // stays in, conservatively. Reposts of one story count once, the newest
107
+ // (`keyword_misses_from`'s normalized-title dedup).
108
+ const evidence = [];
109
+ const seenTitles = new Set();
110
+ for (const item of cited) {
111
+ // A registry release you already run (or are past) was not missed: live
112
+ // 2026-10-01, "npm: react v19.3.0" was a gap for an installed react 19.3.0.
113
+ if (REGISTRY_SOURCES.has(item.source_type ?? "") && !newerThanEveryInstall(item, installs))
114
+ continue;
115
+ const verdict = isAdvisoryRow(item) ? advisoryVerdict(item, name, installs, ctx.advisories) : null;
116
+ if (verdict === "clear")
117
+ continue;
118
+ const e = { item, verdict };
119
+ if (!passesPublishedCut(e, exposure, ctx.publishedCutoff))
120
+ continue;
121
+ const titleKey = normalizeGapTitle(item.title || "");
122
+ if (seenTitles.has(titleKey))
123
+ continue;
124
+ seenTitles.add(titleKey);
125
+ evidence.push(e);
126
+ }
127
+ if (evidence.length === 0)
128
+ return null;
129
+ const vulnerable = evidence.some((e) => stillReaches(e, exposure));
130
+ const tier = vulnerable ? reachingTier(stored, installs) : null;
131
+ const versions = installs.map((i) => i.version).filter((v) => v !== null);
132
+ // Each item graded on its own: the gap's grade is the best of them, and the
133
+ // items shown lead with the ones that earn it.
134
+ const graded = evidence
135
+ .map((e, order) => ({
136
+ e,
137
+ order,
138
+ severity: gradeGap([{ ...e.item, cites: true }], name, stillReaches(e, exposure), versions, tier),
139
+ }))
140
+ .sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] || a.order - b.order);
141
+ const severity = graded[0].severity;
142
+ const shown = graded.slice(0, MAX_MISSED_ITEMS);
143
+ // AD-044: the row is true for every project it names. When the exposure is
144
+ // what makes this a gap, name the exposed projects with the lead one's own
145
+ // version (`affected_project_paths`); otherwise every declaring project.
146
+ const exposed = vulnerable ? installs.filter((i) => installExposure(i, stored) === "exposed") : [];
147
+ const named = exposed.length > 0 ? exposed : installs;
148
+ const lead = named[0];
149
+ return {
150
+ dependency: name,
151
+ version: lead.version,
152
+ project_path: named.length > 1 ? `${lead.projectPath} (+${named.length - 1} more)` : lead.projectPath,
153
+ language: lead.language,
154
+ missed_items: shown.map(({ e }) => brief(e.item)),
155
+ gap_severity: severity,
156
+ missed_count: shown.length,
157
+ };
158
+ }
159
+ export function executeKnowledgeGaps(db, params, liveIntel) {
56
160
  // Direct dependencies only — a transitive dep's news is not the user's
57
161
  // reading backlog. Dev deps stay in: a vitest or eslint advisory is real.
58
- const deps = rawDb
59
- .prepare(`SELECT package_name, version, project_path, language FROM project_dependencies ${hasIsDirect ? "WHERE is_direct = 1 " : ""}LIMIT 100`)
162
+ // Every direct dependency is scanned: a `LIMIT 100` here once left 43 of
163
+ // 143 unexamined.
164
+ const hasIsDirect = db.hasColumn("project_dependencies", "is_direct");
165
+ const deps = db
166
+ .getRawDb()
167
+ .prepare(`SELECT package_name, version, project_path, language FROM project_dependencies ${hasIsDirect ? "WHERE is_direct = 1" : ""}`)
60
168
  .all();
61
169
  if (deps.length === 0) {
62
170
  return {
@@ -64,100 +172,43 @@ export function executeKnowledgeGaps(db, params) {
64
172
  summary: "No project dependencies tracked. Add context directories to enable knowledge gap detection.",
65
173
  };
66
174
  }
67
- const gaps = [];
68
- const seenPackages = new Set();
69
- for (const dep of deps) {
70
- // Names shorter than 3 chars (e.g. "c", "go", "ws") match too many unrelated
71
- // items via substring LIKE to be a trustworthy "mention" signal — skip them.
72
- if (!dep.package_name || dep.package_name.length < 3)
175
+ const scope = loadActiveScope(db);
176
+ const ctx = {
177
+ candidates: loadCandidates(db),
178
+ linker: loadLinkerIndex(db),
179
+ advisories: new AdvisoryStore(db),
180
+ installs: new InstallResolver(db, liveIntel, scope.rowRoots),
181
+ publishedCutoff: Date.now() - PUBLISHED_WINDOW_MS,
182
+ };
183
+ // One gap per package however many projects declare it; each project's own
184
+ // install is judged inside the gap. Names shorter than 3 characters ("c",
185
+ // "go", "ws") match too much unrelated text to be evidence.
186
+ const byPackage = new Map();
187
+ for (const row of scopeRows(deps, scope)) {
188
+ if (!row.package_name || row.package_name.length < 3)
73
189
  continue;
74
- // One gap per package: the same dep declared by several projects would
75
- // otherwise repeat identical missed_items once per project_path.
76
- const pkgKey = dep.package_name.toLowerCase();
77
- if (seenPackages.has(pkgKey))
190
+ const key = row.package_name.toLowerCase();
191
+ const group = byPackage.get(key);
192
+ if (group)
193
+ group.push(row);
194
+ else
195
+ byPackage.set(key, [row]);
196
+ }
197
+ const gaps = [];
198
+ for (const rows of byPackage.values()) {
199
+ if (!dependencyIsActive(rows, scope))
78
200
  continue;
79
- seenPackages.add(pkgKey);
80
- // Find source items mentioning this dependency. Engagement is recorded by the
81
- // app in interactions.item_id / .action_type (the canonical columns; the older
82
- // source_item_id / action columns are unused), so the NOT-IN suppression must
83
- // read those or it silently never fires.
84
- //
85
- // Grounding (each guard killed an observed false-positive class):
86
- // - relevance_score >= 0.2: the scoring pipeline's above-noise band — drops
87
- // off-topic chatter that merely contains the string (chocolate-bar class).
88
- // - 30-day window: a "gap" is something you MISSED, not archaeology — a
89
- // 2014 StackOverflow post is not missed intelligence.
90
- // - LIKE is only a cheap candidate pre-filter; the real test is the
91
- // word-boundary check below ("invite" must never evidence a vite gap).
92
- // - feed_relevant is deliberately NOT filtered here: an item the feed gate
93
- // rejected can still be a legitimate unread dep mention — surfacing those
94
- // is this tool's niche. The relevance floor already excludes noise.
95
- const pattern = `%${escapeLike(dep.package_name)}%`;
96
- const candidates = rawDb
97
- .prepare(`SELECT si.id, si.title, si.url, si.source_type, ${hasContentType ? "si.content_type" : "NULL AS content_type"}, si.created_at,
98
- ${hasRelevance ? "si.relevance_score" : "NULL AS relevance_score"}, substr(COALESCE(si.content, ''), 1, 2000) AS content_head
99
- FROM source_items si
100
- WHERE (si.title LIKE ? ESCAPE '\\' OR si.content LIKE ? ESCAPE '\\')
101
- ${hasRelevance ? "AND si.relevance_score IS NOT NULL AND si.relevance_score >= 0.2" : ""}
102
- AND si.created_at >= datetime('now', '-30 days')
103
- AND si.id NOT IN (SELECT item_id FROM interactions WHERE action_type IN ('click', 'save'))
104
- ORDER BY si.created_at DESC LIMIT 25`)
105
- .all(pattern, pattern);
106
- // Word-boundary verification: the mention must be the package name as a
107
- // whole word in the title or the content head, not a substring.
108
- const mentionedItems = candidates
109
- .filter((item) => mentionsPackage(item.title || "", dep.package_name) ||
110
- mentionsPackage(item.content_head || "", dep.package_name))
111
- .slice(0, 5);
112
- if (mentionedItems.length > 0) {
113
- const isAdvisory = (item) => item.source_type === "cve" ||
114
- item.source_type === "osv" ||
115
- item.content_type === "security_advisory";
116
- const securityKeywords = (title) => hasWordBoundary(title, "cve") ||
117
- hasWordBoundary(title, "security") ||
118
- hasWordBoundary(title, "vulnerability");
119
- // Graded honestly:
120
- // - critical: an actual advisory whose TITLE names this dependency — the
121
- // advisory is about the dep, not merely co-mentioning it in a body.
122
- // - high: a security-keyword item whose title names the dep.
123
- // - medium: 3+ recent unread mentions; low: 1-2.
124
- const advisoryAboutDep = mentionedItems.some((item) => isAdvisory(item) && mentionsPackage(item.title || "", dep.package_name));
125
- const securityTitleMention = mentionedItems.some((item) => securityKeywords(item.title || "") &&
126
- mentionsPackage(item.title || "", dep.package_name));
127
- const severity = advisoryAboutDep
128
- ? "critical"
129
- : securityTitleMention
130
- ? "high"
131
- : mentionedItems.length >= 3
132
- ? "medium"
133
- : "low";
134
- gaps.push({
135
- dependency: dep.package_name,
136
- version: dep.version,
137
- project_path: dep.project_path,
138
- language: dep.language,
139
- missed_items: mentionedItems.map((item) => ({
140
- id: item.id,
141
- title: item.title && item.title.length > 120
142
- ? item.title.substring(0, 120) + "..."
143
- : item.title,
144
- url: item.url,
145
- source_type: item.source_type,
146
- created_at: item.created_at,
147
- relevance_score: item.relevance_score,
148
- })),
149
- gap_severity: severity,
150
- missed_count: mentionedItems.length,
151
- });
152
- }
201
+ const gap = buildGap(rows[0].package_name, rows, ctx);
202
+ if (gap)
203
+ gaps.push(gap);
153
204
  }
154
- // Filter by severity
155
- const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
156
- const minLevel = severityOrder[params.min_severity || "medium"] || 2;
157
- const filtered = gaps.filter((g) => (severityOrder[g.gap_severity] || 0) >= minLevel);
205
+ const minLevel = SEVERITY_ORDER[params.min_severity || "medium"] || 2;
206
+ const filtered = gaps.filter((g) => (SEVERITY_ORDER[g.gap_severity] || 0) >= minLevel);
158
207
  const maxGaps = Math.min(Math.max(1, params.limit || 15), 50);
159
208
  return {
160
- gaps: filtered.sort((a, b) => (severityOrder[b.gap_severity] || 0) - (severityOrder[a.gap_severity] || 0)).slice(0, maxGaps),
209
+ gaps: filtered
210
+ .sort((a, b) => (SEVERITY_ORDER[b.gap_severity] || 0) - (SEVERITY_ORDER[a.gap_severity] || 0))
211
+ .slice(0, maxGaps),
161
212
  total_dependencies: deps.length,
162
213
  gaps_found: filtered.length,
163
214
  gaps_returned: Math.min(filtered.length, maxGaps),
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What this project's lockfiles hold for a package someone asked about by name.
3
+ *
4
+ * `vulnerability_scan` and `upgrade_planner` both take `package`. When either
5
+ * has nothing to show, "no findings" must not read as "not a dependency": in
6
+ * the 2026-10-03 agent eval the planner told an agent that `diff`, a
7
+ * transitive devDependency with two advisories, was "not a dependency of this
8
+ * project (or is a devDependency: pass include_dev)" while include_dev was set.
9
+ */
10
+ import type { LiveIntelligence } from "../live/index.js";
11
+ export interface PackagePresence {
12
+ /** The registry name as the lockfile spells it, or null when no lockfile holds it. */
13
+ name: string | null;
14
+ versions: string[];
15
+ scope: "direct" | "transitive" | null;
16
+ /** Every copy is a direct devDependency (left out of runtime-scope scans and plans). */
17
+ directDevOnly: boolean;
18
+ }
19
+ export declare function packagePresence(query: string, liveIntel: LiveIntelligence): PackagePresence;
20
+ /**
21
+ * One sentence for a `package` query that produced no rows.
22
+ * `checked` says what an empty answer means for this tool ("no known advisories", "nothing to upgrade").
23
+ */
24
+ export declare function emptyAnswerNote(query: string, liveIntel: LiveIntelligence, includeDev: boolean, checked: string): string;
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * What this project's lockfiles hold for a package someone asked about by name.
4
+ *
5
+ * `vulnerability_scan` and `upgrade_planner` both take `package`. When either
6
+ * has nothing to show, "no findings" must not read as "not a dependency": in
7
+ * the 2026-10-03 agent eval the planner told an agent that `diff`, a
8
+ * transitive devDependency with two advisories, was "not a dependency of this
9
+ * project (or is a devDependency: pass include_dev)" while include_dev was set.
10
+ */
11
+ import { namesPackage } from "../live/version-compare.js";
12
+ export function packagePresence(query, liveIntel) {
13
+ const copies = liveIntel.getAuditDeps().filter((d) => namesPackage(query, d.name, d.ecosystem));
14
+ if (copies.length === 0)
15
+ return { name: null, versions: [], scope: null, directDevOnly: false };
16
+ return {
17
+ name: copies[0].name,
18
+ versions: [...new Set(copies.map((d) => d.version ?? "unknown"))],
19
+ scope: copies.some((d) => d.isDirect) ? "direct" : "transitive",
20
+ directDevOnly: copies.every((d) => d.isDirect && d.isDev),
21
+ };
22
+ }
23
+ /**
24
+ * One sentence for a `package` query that produced no rows.
25
+ * `checked` says what an empty answer means for this tool ("no known advisories", "nothing to upgrade").
26
+ */
27
+ export function emptyAnswerNote(query, liveIntel, includeDev, checked) {
28
+ const p = packagePresence(query, liveIntel);
29
+ if (!p.name)
30
+ return `"${query}" is not in this project's lockfiles`;
31
+ const held = `${p.name} ${p.versions.join(", ")} (${p.scope})`;
32
+ if (!includeDev && p.directDevOnly)
33
+ return `${held} is a direct devDependency, left out at this scope: pass include_dev: true`;
34
+ return `${held}: ${checked}`;
35
+ }
@@ -21,7 +21,6 @@ export declare const recordFeedbackTool: {
21
21
  source_type: {
22
22
  type: string;
23
23
  description: string;
24
- enum: string[];
25
24
  };
26
25
  action: {
27
26
  type: string;