@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,51 @@
1
+ /**
2
+ * Words for install drift (node_modules holding a different version than the
3
+ * lockfile pins), shared by vulnerability_scan, what_should_i_know,
4
+ * get_actionable_signals, dependency_health and upgrade_planner so every
5
+ * surface says the same thing about the same drifted package.
6
+ *
7
+ * The case this exists for (measured 2026-09-10): the lockfile pinned the
8
+ * patched hono 4.13.5 while node_modules still held the vulnerable 4.13.1.
9
+ * "Upgrade hono to 4.13.4" would be wrong advice there, because the lockfile
10
+ * is already past it. The fix is a reinstall, and the output has to say so.
11
+ */
12
+ import type { InstallDriftRecord, VulnerabilityEntry, VulnerabilityScanResult } from "../live/types.js";
13
+ /** A relative directory label for a sentence: the scan root reads as words, not ".". */
14
+ export declare function placeLabel(label: string): string;
15
+ /** The directories a row names, spelled for a message. */
16
+ export declare function dirsLabel(dirs: string[], relative?: (dir: string) => string): string;
17
+ /** Drift records for one npm lockfile instance (package at its lockfile version). */
18
+ export declare function driftFor(drift: InstallDriftRecord[], name: string, lockfileVersion: string | null): InstallDriftRecord[];
19
+ /** The action for a scan row about an installed version the lockfile does not pin. */
20
+ export declare function driftAction(v: VulnerabilityEntry): string;
21
+ /** Whether the lockfile's own version of a drifted package carries this same advisory. */
22
+ export declare function lockfileAlsoAffected(v: VulnerabilityEntry, all: VulnerabilityEntry[]): boolean;
23
+ /** Group the actionable drift rows of a scan per (package, installed, locked). */
24
+ export declare function groupDriftRows(vulns: VulnerabilityEntry[]): VulnerabilityEntry[][];
25
+ export interface DriftAdvisory {
26
+ title: string;
27
+ signal_type: "security_alert";
28
+ priority: "critical" | "high" | "medium";
29
+ action: string;
30
+ url: null;
31
+ }
32
+ /**
33
+ * One briefing advisory per drifted package whose installed copy OSV lists as
34
+ * vulnerable, graded the way the app grades its install-drift Preemption row
35
+ * (`evidence::install_drift`): high when the reinstall clears an advisory the
36
+ * running copy has, medium when the lockfile's pinned version is exposed too
37
+ * (the reinstall alone is then not the fix, and each advisory's own signal
38
+ * carries its grade). Always a security alert, so the verdict is at least
39
+ * review_needed and a vulnerable installed copy is never reported clean.
40
+ *
41
+ * Measured 2026-09-10: the hono incident's three advisories are medium on
42
+ * their own, and the installed copy stayed vulnerable for 25 days while a
43
+ * one-line reinstall would have cleared all three.
44
+ */
45
+ export declare function installDriftAdvisories(scan: VulnerabilityScanResult): DriftAdvisory[];
46
+ /** The `install_drift` rows and per-vulnerability annotations for one scan. */
47
+ export interface DriftView {
48
+ rows: Array<Record<string, unknown>>;
49
+ annotate(v: VulnerabilityEntry): Record<string, unknown>;
50
+ }
51
+ export declare function buildDriftView(result: VulnerabilityScanResult, drift: InstallDriftRecord[], includeDev: boolean, relative: (dir: string) => string): DriftView;
@@ -0,0 +1,172 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Words for install drift (node_modules holding a different version than the
4
+ * lockfile pins), shared by vulnerability_scan, what_should_i_know,
5
+ * get_actionable_signals, dependency_health and upgrade_planner so every
6
+ * surface says the same thing about the same drifted package.
7
+ *
8
+ * The case this exists for (measured 2026-09-10): the lockfile pinned the
9
+ * patched hono 4.13.5 while node_modules still held the vulnerable 4.13.1.
10
+ * "Upgrade hono to 4.13.4" would be wrong advice there, because the lockfile
11
+ * is already past it. The fix is a reinstall, and the output has to say so.
12
+ */
13
+ import { isActionableVulnerability } from "../live/maintenance.js";
14
+ import { presentedSeverity, SEVERITY_RANK } from "../live/severity-scope.js";
15
+ const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
16
+ /** A relative directory label for a sentence: the scan root reads as words, not ".". */
17
+ export function placeLabel(label) {
18
+ return label === "." ? "the project root" : label;
19
+ }
20
+ /** The directories a row names, spelled for a message. */
21
+ export function dirsLabel(dirs, relative = (d) => d.replace(/\\/g, "/")) {
22
+ const labels = [...new Set(dirs.map(relative))].map(placeLabel).sort();
23
+ return labels.length > 0 ? labels.join(", ") : "the project";
24
+ }
25
+ /** Drift records for one npm lockfile instance (package at its lockfile version). */
26
+ export function driftFor(drift, name, lockfileVersion) {
27
+ return drift.filter((r) => r.package === name && r.lockfileVersion === lockfileVersion);
28
+ }
29
+ /** The action for a scan row about an installed version the lockfile does not pin. */
30
+ export function driftAction(v) {
31
+ const command = v.installFix ?? "a reinstall";
32
+ return `Run \`${command}\` in ${dirsLabel(v.sourceDirs)} — node_modules has ${v.package}@${v.currentVersion}; the lockfile pins ${v.installDriftOf}`;
33
+ }
34
+ /** Whether the lockfile's own version of a drifted package carries this same advisory. */
35
+ export function lockfileAlsoAffected(v, all) {
36
+ if (!v.installDriftOf)
37
+ return false;
38
+ const ids = new Set([v.vulnId, ...v.aliases]);
39
+ return all.some((x) => x.package === v.package &&
40
+ x.ecosystem === v.ecosystem &&
41
+ x.currentVersion === v.installDriftOf &&
42
+ isActionableVulnerability(x) &&
43
+ [x.vulnId, ...x.aliases].some((id) => ids.has(id)));
44
+ }
45
+ const topTier = (entries) => entries.map(presentedSeverity).reduce((a, b) => (SEVERITY_RANK[b] > SEVERITY_RANK[a] ? b : a), "unknown");
46
+ /** Group the actionable drift rows of a scan per (package, installed, locked). */
47
+ export function groupDriftRows(vulns) {
48
+ const groups = new Map();
49
+ for (const v of vulns) {
50
+ if (!v.installDriftOf || !isActionableVulnerability(v))
51
+ continue;
52
+ const key = `${v.ecosystem}\0${v.package}\0${v.currentVersion}\0${v.installDriftOf}`;
53
+ const list = groups.get(key);
54
+ if (list)
55
+ list.push(v);
56
+ else
57
+ groups.set(key, [v]);
58
+ }
59
+ return [...groups.values()];
60
+ }
61
+ /**
62
+ * One briefing advisory per drifted package whose installed copy OSV lists as
63
+ * vulnerable, graded the way the app grades its install-drift Preemption row
64
+ * (`evidence::install_drift`): high when the reinstall clears an advisory the
65
+ * running copy has, medium when the lockfile's pinned version is exposed too
66
+ * (the reinstall alone is then not the fix, and each advisory's own signal
67
+ * carries its grade). Always a security alert, so the verdict is at least
68
+ * review_needed and a vulnerable installed copy is never reported clean.
69
+ *
70
+ * Measured 2026-09-10: the hono incident's three advisories are medium on
71
+ * their own, and the installed copy stayed vulnerable for 25 days while a
72
+ * one-line reinstall would have cleared all three.
73
+ */
74
+ export function installDriftAdvisories(scan) {
75
+ return groupDriftRows(scan.vulnerabilities).map((entries) => {
76
+ const first = entries[0];
77
+ const count = entries.length;
78
+ const pinExposed = entries.some((e) => lockfileAlsoAffected(e, scan.vulnerabilities));
79
+ const clearsAny = entries.some((e) => !lockfileAlsoAffected(e, scan.vulnerabilities));
80
+ return {
81
+ title: `${first.package}: the installed ${first.currentVersion} in node_modules has ${count} known vulnerabilit${count === 1 ? "y" : "ies"}; the lockfile pins ${first.installDriftOf}`,
82
+ signal_type: "security_alert",
83
+ priority: clearsAny ? "high" : "medium",
84
+ action: `${driftAction(first)}${!pinExposed
85
+ ? " (the lockfile version is not affected; the reinstall is the whole fix)"
86
+ : clearsAny
87
+ ? " (the reinstall clears some of these; the lockfile version is still affected by the rest, so upgrade it too)"
88
+ : " (the lockfile version is also affected; upgrade it, then reinstall)"}`,
89
+ url: null,
90
+ };
91
+ });
92
+ }
93
+ export function buildDriftView(result, drift, includeDev, relative) {
94
+ const actionable = result.vulnerabilities.filter(isActionableVulnerability);
95
+ // OSV's answer for one npm (package, version), whichever dependency asked it.
96
+ const affected = (pkg, version) => actionable.some((v) => v.ecosystem === "npm" && v.package === pkg && v.currentVersion === version);
97
+ // Null when the version was not in the scanned set (a dev dependency without
98
+ // include_dev) or the scan went offline with nothing to say about it.
99
+ const answer = (record, version) => {
100
+ if (record.isDev && !includeDev)
101
+ return null;
102
+ if (affected(record.package, version))
103
+ return true;
104
+ return result.offline ? null : false;
105
+ };
106
+ const rows = drift.map((record) => {
107
+ const vulnerableInstalled = answer(record, record.installedVersion);
108
+ const lockfileVulnerable = answer(record, record.lockfileVersion);
109
+ const dir = relative(record.dir);
110
+ const where = placeLabel(dir);
111
+ let note;
112
+ if (vulnerableInstalled === true && lockfileVulnerable === false) {
113
+ note = `The lockfile is patched but node_modules is not: run \`${record.fix}\` in ${where}.`;
114
+ }
115
+ else if (vulnerableInstalled === true) {
116
+ note = `The installed copy is vulnerable and so is the lockfile's version: upgrade, then run \`${record.fix}\` in ${where}.`;
117
+ }
118
+ else if (vulnerableInstalled === false) {
119
+ note = `node_modules is out of step with the lockfile (no known advisory affects the installed version): run \`${record.fix}\` in ${where}.`;
120
+ }
121
+ else {
122
+ const why = record.isDev && !includeDev ? "a dev dependency; pass include_dev to check it" : "the scan was offline";
123
+ note = `node_modules is out of step with the lockfile; the installed version was not checked (${why}): run \`${record.fix}\` in ${where}.`;
124
+ }
125
+ return {
126
+ package: record.package,
127
+ dir,
128
+ lockfile_version: record.lockfileVersion,
129
+ installed_version: record.installedVersion,
130
+ vulnerable_installed: vulnerableInstalled,
131
+ lockfile_version_vulnerable: lockfileVulnerable,
132
+ fix: record.fix,
133
+ note,
134
+ };
135
+ });
136
+ const inDirs = (dir, dirs) => dirs.some((d) => norm(d) === norm(dir));
137
+ const annotate = (v) => {
138
+ if (v.installDriftOf) {
139
+ const command = v.installFix ?? "a reinstall";
140
+ const where = dirsLabel(v.sourceDirs, relative);
141
+ return {
142
+ installed_version: v.currentVersion,
143
+ lockfile_version: v.installDriftOf,
144
+ install_note: lockfileAlsoAffected(v, result.vulnerabilities)
145
+ ? `This is the copy in node_modules; the lockfile's ${v.installDriftOf} is also affected — upgrade, then run \`${command}\` in ${where}.`
146
+ : `This is the copy in node_modules: the lockfile is patched (pins ${v.installDriftOf}) but node_modules still has ${v.currentVersion} — run \`${command}\` in ${where}.`,
147
+ };
148
+ }
149
+ if (v.ecosystem !== "npm")
150
+ return {};
151
+ // A lockfile row whose package node_modules holds at another version.
152
+ const behind = drift.filter((r) => r.package === v.package && r.lockfileVersion === v.currentVersion && inDirs(r.dir, v.sourceDirs));
153
+ if (behind.length > 0) {
154
+ const installed = [...new Set(behind.map((r) => r.installedVersion))].join(", ");
155
+ return {
156
+ installed_version: installed,
157
+ install_note: `node_modules has ${installed}, not this lockfile version — run \`${behind[0].fix}\` in ${dirsLabel(behind.map((r) => r.dir), relative)}; advisories against the installed copy are listed under its own version.`,
158
+ };
159
+ }
160
+ // A lockfile row that is also what a drifted workspace has installed.
161
+ const holders = drift.filter((r) => r.package === v.package && r.installedVersion === v.currentVersion && inDirs(r.dir, v.sourceDirs));
162
+ if (holders.length > 0) {
163
+ return {
164
+ installed_version: v.currentVersion,
165
+ lockfile_version: [...new Set(holders.map((r) => r.lockfileVersion))].join(", "),
166
+ install_note: `Also the copy node_modules holds in ${dirsLabel(holders.map((r) => r.dir), relative)}, where the lockfile pins a different version — run \`${holders[0].fix}\` there.`,
167
+ };
168
+ }
169
+ return {};
170
+ };
171
+ return { rows, annotate };
172
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Knowledge-gap citations: which unread items are evidence about which
3
+ * dependency. Mirrors `knowledge_decay::{load_gap_candidates,
4
+ * keyword_misses_from, normalize_gap_title}` in the desktop app.
5
+ */
6
+ import type { FourDADatabase } from "../db.js";
7
+ import type { SourceItemBriefRow } from "../types.js";
8
+ import type { LinkerIndex } from "./knowledge-gap-scope.js";
9
+ /** An unread item eligible to become a missed signal. */
10
+ export type GapCandidate = SourceItemBriefRow & {
11
+ content_type: string | null;
12
+ relevance_score: number | null;
13
+ published_at: string | null;
14
+ /** osv rows: the advisory id; cve rows: the CVE id. */
15
+ source_id: string | null;
16
+ content_head: string;
17
+ title_lower: string;
18
+ };
19
+ /**
20
+ * Every unread candidate item, loaded ONCE and matched per dependency.
21
+ * Grounding (each guard killed an observed false-positive class):
22
+ * - relevance_score >= 0.2, the scoring pipeline's above-noise band, when the
23
+ * database has a scoring pipeline at all;
24
+ * - a 30-day window: a gap is something you MISSED, not archaeology;
25
+ * - engagement: an item clicked or saved here (`interactions.item_id` /
26
+ * `.action_type`, the canonical columns) or given feedback in the app;
27
+ * - the content types above, as the app excludes them.
28
+ * Optional columns are feature-detected: a standalone database may lack the
29
+ * scoring pipeline, and the tool must degrade, never throw.
30
+ */
31
+ export declare function loadCandidates(db: FourDADatabase): GapCandidate[];
32
+ /**
33
+ * Does this unread item cite `packageName`?
34
+ *
35
+ * An advisory row (osv/cve) cites a dependency only through the dependency
36
+ * linker's structured proof when the linker table exists, and otherwise only
37
+ * when the package is the advisory's SUBJECT ("[ID] package: ..."). Never
38
+ * through a word in its title: "[CVE-2026-63642] MagicMirror newsfeed
39
+ * Socket.IO notification allows blind server-side request forgery" has no
40
+ * linker row and no parseable subject, and it minted a critical socket.io gap
41
+ * on the word "Socket.IO" (measured 2026-09-11).
42
+ *
43
+ * Every other row cites a dependency when its TITLE names it at a word
44
+ * boundary, as the app matches titles only. The body does not count: four
45
+ * reposts of "I built 59 free browser-based dev tools in vanilla JS" evidenced
46
+ * a critical `rsa` gap through a "RSA & ECC Key Generator" line 1,330
47
+ * characters into the post. A package named by an ordinary English word also
48
+ * needs an ecosystem cue, which may come from the body.
49
+ */
50
+ export declare function citesDependency(item: GapCandidate, packageName: string, linker: LinkerIndex | null): boolean;
51
+ /**
52
+ * `knowledge_decay::normalize_gap_title`: lowercase, punctuation stripped,
53
+ * first ten words. Reposts of one story share it.
54
+ */
55
+ export declare function normalizeGapTitle(title: string): string;
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Knowledge-gap citations: which unread items are evidence about which
4
+ * dependency. Mirrors `knowledge_decay::{load_gap_candidates,
5
+ * keyword_misses_from, normalize_gap_title}` in the desktop app.
6
+ */
7
+ import { advisorySubject, isAdvisoryRow, mentionsPackage, samePackage } from "./knowledge-gap-grading.js";
8
+ /**
9
+ * Content types the app never counts as missed intelligence
10
+ * (`load_gap_candidates`): someone else's project, a tutorial, a question.
11
+ */
12
+ const NON_GAP_CONTENT_TYPES = ["show_and_tell", "tutorial", "question", "help_request", "hiring", "clickbait"];
13
+ /** Sources whose rows are structured facts about a package (advisory records, registry releases). */
14
+ const STRUCTURED_EVIDENCE = ["cve", "osv", "crates_io", "npm_registry", "pypi", "go_modules"];
15
+ /**
16
+ * Every unread candidate item, loaded ONCE and matched per dependency.
17
+ * Grounding (each guard killed an observed false-positive class):
18
+ * - relevance_score >= 0.2, the scoring pipeline's above-noise band, when the
19
+ * database has a scoring pipeline at all;
20
+ * - a 30-day window: a gap is something you MISSED, not archaeology;
21
+ * - engagement: an item clicked or saved here (`interactions.item_id` /
22
+ * `.action_type`, the canonical columns) or given feedback in the app;
23
+ * - the content types above, as the app excludes them.
24
+ * Optional columns are feature-detected: a standalone database may lack the
25
+ * scoring pipeline, and the tool must degrade, never throw.
26
+ */
27
+ export function loadCandidates(db) {
28
+ const has = (column) => db.hasColumn("source_items", column);
29
+ const hasContentType = has("content_type");
30
+ const hasRelevance = has("relevance_score");
31
+ // Prose evidence (news, social, papers) must have passed the relevance
32
+ // judge: its latest verdict >= 0.5, or, without judgments, the feed kept it.
33
+ // Structured rows (advisories, registry releases) are matched by subject and
34
+ // version instead. Measured 2026-10-01: "Breaking Windows Malware
35
+ // Detection" (judge 0.1) evidenced a high `windows` crate gap, and
36
+ // judge-rejected OpenAI company news an `openai` one.
37
+ const structured = `si.source_type IN (${STRUCTURED_EVIDENCE.map((s) => `'${s}'`).join(", ")})`;
38
+ const judgedIn = db.hasColumn("llm_judgments", "source_item_id")
39
+ ? `AND (${structured} OR (SELECT relevance_score FROM llm_judgments lj WHERE lj.source_item_id = si.id ORDER BY lj.id DESC LIMIT 1) >= 0.5)`
40
+ : has("feed_relevant")
41
+ ? `AND (${structured} OR si.feed_relevant = 1)`
42
+ : "";
43
+ const engaged = db.hasColumn("interactions", "item_id") && db.hasColumn("interactions", "action_type")
44
+ ? "AND si.id NOT IN (SELECT item_id FROM interactions WHERE item_id IS NOT NULL AND action_type IN ('click', 'save'))"
45
+ : "";
46
+ const judged = db.hasColumn("feedback", "source_item_id")
47
+ ? "AND si.id NOT IN (SELECT source_item_id FROM feedback WHERE source_item_id IS NOT NULL)"
48
+ : "";
49
+ const excludedTypes = NON_GAP_CONTENT_TYPES.map((t) => `'${t}'`).join(", ");
50
+ const rows = db
51
+ .getRawDb()
52
+ .prepare(`SELECT si.id, si.title, si.url, si.source_type, si.created_at,
53
+ ${hasContentType ? "si.content_type" : "NULL AS content_type"},
54
+ ${hasRelevance ? "si.relevance_score" : "NULL AS relevance_score"},
55
+ ${has("published_at") ? "si.published_at" : "NULL AS published_at"},
56
+ ${has("source_id") ? "si.source_id" : "NULL AS source_id"},
57
+ substr(COALESCE(si.content, ''), 1, 2000) AS content_head
58
+ FROM source_items si
59
+ WHERE si.created_at >= datetime('now', '-30 days')
60
+ ${hasRelevance ? "AND si.relevance_score IS NOT NULL AND si.relevance_score >= 0.2" : ""}
61
+ ${hasContentType ? `AND (si.content_type IS NULL OR si.content_type NOT IN (${excludedTypes}))` : ""}
62
+ ${engaged}
63
+ ${judged}
64
+ ${judgedIn}
65
+ ORDER BY si.created_at DESC, si.id DESC`)
66
+ .all();
67
+ return rows.map((row) => ({ ...row, title_lower: (row.title || "").toLowerCase() }));
68
+ }
69
+ /**
70
+ * Dependency names that are ordinary English words. A word-boundary match on
71
+ * these is no evidence the item is about the PACKAGE: live 2026-08-30, a
72
+ * `tower` gap was evidenced by a one-tap tower-stacker game and a drivable-car
73
+ * post. For these names the item must also carry an ecosystem cue.
74
+ */
75
+ const GENERIC_WORD_DEPS = new Set([
76
+ "tower", "base64", "image", "time", "rand", "log", "tracing", "url", "zip",
77
+ "tar", "glob", "regex", "chrono", "notify", "either", "bytes", "flate",
78
+ // Company, product and protocol names a headline uses for the thing itself
79
+ // (2026-10-01: "RSA has launched Agent ID", "Windows 11 emergency update",
80
+ // "OpenAI halts release of Astra 6.1" evidenced rsa, windows, openai gaps).
81
+ "rsa", "windows", "openai", "stripe", "docker", "redis", "sentry", "next", "ai", "anthropic",
82
+ ]);
83
+ // A token that ties the text to software packaging rather than the English word:
84
+ // an ecosystem noun, a security noun, or a version number.
85
+ const ECOSYSTEM_CUE = /\b(crate|crates\.io|cargo|rust|npm|node|package|library|dependenc|version|release[ds]?|upgrad|deprecat|cve|advisory|vulnerab)|\bv?\d+\.\d+/i;
86
+ function hasEcosystemCue(item) {
87
+ return ECOSYSTEM_CUE.test(`${item.title || ""} ${item.content_head || ""}`);
88
+ }
89
+ /**
90
+ * Does this unread item cite `packageName`?
91
+ *
92
+ * An advisory row (osv/cve) cites a dependency only through the dependency
93
+ * linker's structured proof when the linker table exists, and otherwise only
94
+ * when the package is the advisory's SUBJECT ("[ID] package: ..."). Never
95
+ * through a word in its title: "[CVE-2026-63642] MagicMirror newsfeed
96
+ * Socket.IO notification allows blind server-side request forgery" has no
97
+ * linker row and no parseable subject, and it minted a critical socket.io gap
98
+ * on the word "Socket.IO" (measured 2026-09-11).
99
+ *
100
+ * Every other row cites a dependency when its TITLE names it at a word
101
+ * boundary, as the app matches titles only. The body does not count: four
102
+ * reposts of "I built 59 free browser-based dev tools in vanilla JS" evidenced
103
+ * a critical `rsa` gap through a "RSA & ECC Key Generator" line 1,330
104
+ * characters into the post. A package named by an ordinary English word also
105
+ * needs an ecosystem cue, which may come from the body.
106
+ */
107
+ export function citesDependency(item, packageName, linker) {
108
+ if (isAdvisoryRow(item)) {
109
+ if (linker)
110
+ return linker.itemsFor(packageName).has(item.id);
111
+ const subject = advisorySubject(item.title || "");
112
+ return subject !== null && samePackage(subject, packageName);
113
+ }
114
+ if (!item.title_lower.includes(packageName.toLowerCase()))
115
+ return false;
116
+ if (!mentionsPackage(item.title || "", packageName))
117
+ return false;
118
+ return !GENERIC_WORD_DEPS.has(packageName.toLowerCase()) || hasEcosystemCue(item);
119
+ }
120
+ /**
121
+ * `knowledge_decay::normalize_gap_title`: lowercase, punctuation stripped,
122
+ * first ten words. Reposts of one story share it.
123
+ */
124
+ export function normalizeGapTitle(title) {
125
+ return title
126
+ .toLowerCase()
127
+ .replace(/[^\p{Alphabetic}\p{N}\s]/gu, "")
128
+ .split(/\s+/)
129
+ .filter(Boolean)
130
+ .slice(0, 10)
131
+ .join(" ");
132
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Knowledge-gap exposure: which stored advisories reach the installs a gap
3
+ * names, and each advisory ROW's live verdict. The MCP twin of the app's
4
+ * `osv::exposure::advisory_row_reaches` and `knowledge_decay::{
5
+ * installs_still_vulnerable, affected_project_paths, advisory_tier_for}`
6
+ * (AD-045: an advisory is judged against the install it names, in the
7
+ * install's own ecosystem).
8
+ *
9
+ * The first cut read one package-wide verdict from every stored advisory at
10
+ * once, so one unreadable row, one sibling project's version, or another
11
+ * ecosystem's same-named package decided the gap for every project in it.
12
+ */
13
+ import type { FourDADatabase } from "../db.js";
14
+ import { type Exposure } from "./knowledge-gap-grading.js";
15
+ import { type Install } from "./knowledge-gap-installs.js";
16
+ import { type AdvisoryRangeEvent, type AdvisoryTier } from "./knowledge-gap-ranges.js";
17
+ /** One stored advisory record for one (package, ecosystem). */
18
+ export interface AdvisoryEntry {
19
+ packageName: string;
20
+ /** Null when the table predates its ecosystem column. */
21
+ ecosystem: string | null;
22
+ /** Null when `affected_ranges` is missing or unreadable. */
23
+ ranges: AdvisoryRangeEvent[][] | null;
24
+ tier: AdvisoryTier | null;
25
+ }
26
+ /**
27
+ * An advisory row's verdict against the installs its gap names.
28
+ * - `reached`: some install sits inside one of the row's advisories.
29
+ * - `clear`: every install is outside them — fixed at or below the installed
30
+ * version, or a package of the same name in another ecosystem.
31
+ * - `unknown`: an install whose version (or the row's ranges) cannot be read.
32
+ * Kept conservatively: an unknown install stays exposed (AD-040 rule 3).
33
+ */
34
+ export type RowVerdict = "reached" | "clear" | "unknown";
35
+ /**
36
+ * One install against every stored advisory for its package, judged advisory
37
+ * by advisory: exposed if any readable advisory of its ecosystem contains it,
38
+ * safe if every readable one excludes it, unknown only when nothing readable
39
+ * exists (or its version cannot be read). An unreadable row is simply not
40
+ * evidence: it no longer turns a patched package into an unknown one, and
41
+ * unknown into "vulnerable".
42
+ */
43
+ export declare function installExposure(install: Install, entries: AdvisoryEntry[]): Exposure;
44
+ /** A gap's exposure: exposed if any install is, unknown if any install is, else safe. */
45
+ export declare function gapExposure(installs: Install[], entries: AdvisoryEntry[]): Exposure;
46
+ /** An advisory row's entries against the installs its gap names (`advisory_row_reaches`). */
47
+ export declare function rowVerdict(entries: AdvisoryEntry[], installs: Install[]): RowVerdict;
48
+ /**
49
+ * The tier of the most severe stored advisory that contains an install the
50
+ * gap names, in that install's ecosystem (`knowledge_decay::advisory_tier_for`),
51
+ * or null when none of the reaching advisories is graded.
52
+ */
53
+ export declare function reachingTier(entries: AdvisoryEntry[], installs: Install[]): AdvisoryTier | null;
54
+ /** Read access to the OSV mirror (`osv_advisories`), feature-detected column by column. */
55
+ export declare class AdvisoryStore {
56
+ private readonly byPackage;
57
+ private readonly byId;
58
+ private readonly idMatchesAliases;
59
+ private readonly rowsById;
60
+ constructor(db: FourDADatabase);
61
+ /** Every live advisory stored for this dependency, in any ecosystem. */
62
+ forPackage(name: string): AdvisoryEntry[];
63
+ /**
64
+ * The entries for dependency `name` that an advisory ROW stands for
65
+ * (`osv::exposure::advisory_records`). An osv row carries its advisory id in
66
+ * `source_items.source_id`; a cve row carries the CVE id, which the mirror
67
+ * keeps in `aliases` (a JSON array). Names compare case-insensitively with
68
+ * `-` equal to `_`. Null when the row resolves to no stored entry for this
69
+ * dependency: the caller keeps its conservative fallback.
70
+ */
71
+ forRow(sourceId: string | null | undefined, name: string): AdvisoryEntry[] | null;
72
+ }
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Knowledge-gap exposure: which stored advisories reach the installs a gap
4
+ * names, and each advisory ROW's live verdict. The MCP twin of the app's
5
+ * `osv::exposure::advisory_row_reaches` and `knowledge_decay::{
6
+ * installs_still_vulnerable, affected_project_paths, advisory_tier_for}`
7
+ * (AD-045: an advisory is judged against the install it names, in the
8
+ * install's own ecosystem).
9
+ *
10
+ * The first cut read one package-wide verdict from every stored advisory at
11
+ * once, so one unreadable row, one sibling project's version, or another
12
+ * ecosystem's same-named package decided the gap for every project in it.
13
+ */
14
+ import { parseSemverPrecedence } from "../live/semver-precedence.js";
15
+ import { samePackage } from "./knowledge-gap-grading.js";
16
+ import { denotes, otherSpelling, sameEcosystem } from "./knowledge-gap-installs.js";
17
+ import { advisoryTier, moreSevereTier, parseAffectedRanges, rangesContain, } from "./knowledge-gap-ranges.js";
18
+ import { allRows } from "./knowledge-gap-scope.js";
19
+ /**
20
+ * An advisory counts against an install only for the install's own
21
+ * ecosystem; when either side's ecosystem is unknown it is judged against
22
+ * every record, as `advisory_row_reaches` does.
23
+ */
24
+ function appliesTo(entry, install) {
25
+ return entry.ecosystem === null || install.ecosystem === null || sameEcosystem(entry.ecosystem, install.ecosystem);
26
+ }
27
+ /** Inside the entry's readable ranges: true/false; null when either side cannot be read. */
28
+ function contains(entry, install) {
29
+ if (entry.ranges === null || install.version === null)
30
+ return null;
31
+ return rangesContain(entry.ranges, install.version);
32
+ }
33
+ /**
34
+ * One install against every stored advisory for its package, judged advisory
35
+ * by advisory: exposed if any readable advisory of its ecosystem contains it,
36
+ * safe if every readable one excludes it, unknown only when nothing readable
37
+ * exists (or its version cannot be read). An unreadable row is simply not
38
+ * evidence: it no longer turns a patched package into an unknown one, and
39
+ * unknown into "vulnerable".
40
+ */
41
+ export function installExposure(install, entries) {
42
+ if (install.version === null || parseSemverPrecedence(install.version) === null)
43
+ return "unknown";
44
+ const readable = entries.filter((e) => appliesTo(e, install) && e.ranges !== null);
45
+ if (readable.length === 0)
46
+ return "unknown";
47
+ return readable.some((e) => contains(e, install) === true) ? "exposed" : "safe";
48
+ }
49
+ /** A gap's exposure: exposed if any install is, unknown if any install is, else safe. */
50
+ export function gapExposure(installs, entries) {
51
+ const verdicts = installs.map((install) => installExposure(install, entries));
52
+ if (verdicts.includes("exposed"))
53
+ return "exposed";
54
+ return verdicts.length === 0 || verdicts.includes("unknown") ? "unknown" : "safe";
55
+ }
56
+ /** An advisory row's entries against the installs its gap names (`advisory_row_reaches`). */
57
+ export function rowVerdict(entries, installs) {
58
+ let unknown = installs.length === 0;
59
+ for (const install of installs) {
60
+ const applicable = entries.filter((e) => appliesTo(e, install));
61
+ if (applicable.length === 0)
62
+ continue; // another ecosystem's package of the same name
63
+ let judged = false;
64
+ for (const entry of applicable) {
65
+ const inside = contains(entry, install);
66
+ if (inside === true)
67
+ return "reached";
68
+ if (inside === false)
69
+ judged = true;
70
+ }
71
+ if (!judged)
72
+ unknown = true;
73
+ }
74
+ return unknown ? "unknown" : "clear";
75
+ }
76
+ /**
77
+ * The tier of the most severe stored advisory that contains an install the
78
+ * gap names, in that install's ecosystem (`knowledge_decay::advisory_tier_for`),
79
+ * or null when none of the reaching advisories is graded.
80
+ */
81
+ export function reachingTier(entries, installs) {
82
+ let best = null;
83
+ for (const entry of entries) {
84
+ if (entry.tier === null)
85
+ continue;
86
+ if (installs.some((install) => appliesTo(entry, install) && contains(entry, install) === true)) {
87
+ best = moreSevereTier(best, entry.tier);
88
+ }
89
+ }
90
+ return best;
91
+ }
92
+ function toEntry(row) {
93
+ return {
94
+ packageName: row.package_name,
95
+ ecosystem: row.ecosystem,
96
+ ranges: parseAffectedRanges(row.affected_ranges),
97
+ tier: advisoryTier(row.cvss_score, row.severity_label),
98
+ };
99
+ }
100
+ /** Read access to the OSV mirror (`osv_advisories`), feature-detected column by column. */
101
+ export class AdvisoryStore {
102
+ byPackage = null;
103
+ byId = null;
104
+ idMatchesAliases = false;
105
+ rowsById = new Map();
106
+ constructor(db) {
107
+ const has = (column) => db.hasColumn("osv_advisories", column);
108
+ if (!has("package_name") || !has("affected_ranges"))
109
+ return;
110
+ const columns = [
111
+ "package_name",
112
+ has("ecosystem") ? "ecosystem" : "NULL AS ecosystem",
113
+ "affected_ranges",
114
+ has("cvss_score") ? "cvss_score" : "NULL AS cvss_score",
115
+ has("severity_label") ? "severity_label" : "NULL AS severity_label",
116
+ ].join(", ");
117
+ const live = has("withdrawn_at") ? "AND withdrawn_at IS NULL" : "";
118
+ const raw = db.getRawDb();
119
+ try {
120
+ this.byPackage = raw.prepare(`SELECT ${columns} FROM osv_advisories WHERE lower(package_name) IN (lower(?), lower(?)) ${live}`);
121
+ if (has("advisory_id")) {
122
+ this.idMatchesAliases = has("aliases");
123
+ const match = this.idMatchesAliases ? "(advisory_id = ? OR aliases LIKE ? ESCAPE '\\')" : "advisory_id = ?";
124
+ this.byId = raw.prepare(`SELECT ${columns} FROM osv_advisories WHERE ${match} ${live}`);
125
+ }
126
+ }
127
+ catch {
128
+ // An unreadable mirror is no mirror: every verdict stays conservative.
129
+ }
130
+ }
131
+ /** Every live advisory stored for this dependency, in any ecosystem. */
132
+ forPackage(name) {
133
+ if (!this.byPackage)
134
+ return [];
135
+ return allRows(this.byPackage, name, otherSpelling(name))
136
+ .filter((row) => denotes(row.package_name, row.ecosystem, name))
137
+ .map(toEntry);
138
+ }
139
+ /**
140
+ * The entries for dependency `name` that an advisory ROW stands for
141
+ * (`osv::exposure::advisory_records`). An osv row carries its advisory id in
142
+ * `source_items.source_id`; a cve row carries the CVE id, which the mirror
143
+ * keeps in `aliases` (a JSON array). Names compare case-insensitively with
144
+ * `-` equal to `_`. Null when the row resolves to no stored entry for this
145
+ * dependency: the caller keeps its conservative fallback.
146
+ */
147
+ forRow(sourceId, name) {
148
+ const id = (sourceId ?? "").trim();
149
+ if (!this.byId || id === "")
150
+ return null;
151
+ let rows = this.rowsById.get(id);
152
+ if (!rows) {
153
+ const args = this.idMatchesAliases ? [id, `%"${id.replace(/[\\%_]/g, "\\$&")}"%`] : [id];
154
+ rows = allRows(this.byId, ...args);
155
+ this.rowsById.set(id, rows);
156
+ }
157
+ const mine = rows.filter((row) => samePackage(row.package_name, name));
158
+ return mine.length > 0 ? mine.map(toEntry) : null;
159
+ }
160
+ }