@4da/mcp-server 5.0.4 → 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 +233 -37
  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 -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,95 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Set operations over resolved dependencies, shared by the live layer.
4
+ * (Moved out of live/index.ts; behaviour unchanged except for the install
5
+ * fields, which did not exist before.)
6
+ */
7
+ /**
8
+ * Deepest common ancestor of a set of directories (segment-wise, both slash
9
+ * styles). Null for an empty set; a single dir is its own root.
10
+ */
11
+ export function commonPathRoot(dirs) {
12
+ if (dirs.length === 0)
13
+ return null;
14
+ const split = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
15
+ let common = split(dirs[0]);
16
+ for (const dir of dirs.slice(1)) {
17
+ const parts = split(dir);
18
+ let i = 0;
19
+ while (i < common.length && i < parts.length && common[i].toLowerCase() === parts[i].toLowerCase())
20
+ i++;
21
+ common = common.slice(0, i);
22
+ if (common.length === 0)
23
+ break;
24
+ }
25
+ return common.length > 0 ? common.join("/") : null;
26
+ }
27
+ export function dedupeDependencies(deps) {
28
+ const unique = new Map();
29
+ for (const dep of deps) {
30
+ const key = `${dep.ecosystem}\0${dep.name}\0${dep.version ?? ""}`;
31
+ const existing = unique.get(key);
32
+ if (existing) {
33
+ existing.isDirect ||= dep.isDirect;
34
+ existing.devScopeKnown &&= dep.devScopeKnown;
35
+ existing.isDev = existing.devScopeKnown && existing.isDev && dep.isDev;
36
+ // A crate reachable via ANY active path is active, and its target label
37
+ // is then the active copy's: rsa 0.9.10, built for relay and not for
38
+ // src-tauri on Windows, read "not built for x86_64-pc-windows-msvc"
39
+ // beside platform_active: true (2026-10-02 agent eval).
40
+ if (dep.platformActive && !existing.platformActive)
41
+ existing.target = dep.target;
42
+ else if (existing.platformActive === dep.platformActive)
43
+ existing.target = existing.target ?? dep.target;
44
+ existing.platformActive ||= dep.platformActive;
45
+ // Union the provenance rather than discarding it — the same version can
46
+ // legitimately be pinned by several workspaces, and the reader needs all
47
+ // of them to know where to apply the fix.
48
+ for (const dir of dep.sourceDirs ?? []) {
49
+ if (!existing.sourceDirs.includes(dir))
50
+ existing.sourceDirs.push(dir);
51
+ }
52
+ mergeInstallContext(existing, dep);
53
+ }
54
+ else {
55
+ unique.set(key, { ...dep, sourceDirs: [...(dep.sourceDirs ?? [])] });
56
+ }
57
+ }
58
+ return [...unique.values()];
59
+ }
60
+ /**
61
+ * A drift entry exists only because node_modules holds a version the lockfile
62
+ * does not pin. When a lockfile really does pin that same version somewhere,
63
+ * the merged entry is a lockfile instance, not drift, and must not read as
64
+ * "only in node_modules". Two drift entries keep the first one's context.
65
+ * A drifted install reading wins over a matching one, so a merge never hides
66
+ * a disagreement.
67
+ */
68
+ function mergeInstallContext(existing, dep) {
69
+ if (existing.installDriftOf !== undefined && dep.installDriftOf === undefined) {
70
+ existing.installDriftOf = undefined;
71
+ existing.installFix = undefined;
72
+ }
73
+ if (dep.installedVersion === undefined)
74
+ return;
75
+ const drifted = (d) => d.installedVersion !== undefined && d.installedVersion !== null && d.installedVersion !== d.version;
76
+ if (existing.installedVersion === undefined || (drifted(dep) && !drifted(existing))) {
77
+ existing.installedVersion = dep.installedVersion;
78
+ }
79
+ }
80
+ export function emptyVulnResult(projectPath, offline) {
81
+ return {
82
+ scannedAt: new Date().toISOString(),
83
+ projectPath,
84
+ ecosystemsScanned: [],
85
+ totalScanned: 0,
86
+ totalVulnerable: 0,
87
+ platformInactiveVulnerable: 0,
88
+ bySeverity: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 },
89
+ vulnerabilities: [],
90
+ cleanCount: 0,
91
+ scanDurationMs: 0,
92
+ cached: false,
93
+ offline,
94
+ };
95
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Transitive dev/runtime scope from the desktop app's dependency inventory.
3
+ *
4
+ * A lockfile read tells the MCP server whether a package is direct. For a
5
+ * transitive it cannot tell dev from runtime, so under the shared severity
6
+ * rule a transitive gets the clamp but never the dev discount. In
7
+ * full-database mode the app's `dependency_instances` table records every
8
+ * installed instance with `is_dev` and `scope`; this module reads it.
9
+ *
10
+ * Only a real determination counts: `is_dev = 1`, or a `scope` of runtime,
11
+ * dev or build. The app's current writers stamp every row `is_dev = 0,
12
+ * scope = 'unknown'` as a placeholder. Reading that as "known runtime" would
13
+ * claim a certainty the app itself does not have. The grade would not change
14
+ * (unknown and runtime both get no discount), but `dev_scope_known` would lie.
15
+ */
16
+ import type Database from "better-sqlite3";
17
+ import type { ResolvedDependency } from "./types.js";
18
+ /**
19
+ * A project path in the app's storage form: forward slashes, lowercased on
20
+ * Windows only. Must match `project_inclusion::canonical_storage_path` in the
21
+ * app, or the lookup misses every row.
22
+ */
23
+ export declare function canonicalStoragePath(p: string): string;
24
+ /**
25
+ * Stamp transitive dependencies with the dev scope the app determined for
26
+ * them. A dependency pinned in several workspaces is dev-only only when every
27
+ * one of them says dev, and known only when every one of them was determined.
28
+ * Anything short of that keeps the resolver's own (unknown) scope. Direct
29
+ * dependencies are untouched: their manifest already says. Returns new
30
+ * objects; never throws; a database without the table changes nothing.
31
+ */
32
+ export declare function applyInstanceDevScope(db: Database.Database | null, deps: ResolvedDependency[]): ResolvedDependency[];
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Transitive dev/runtime scope from the desktop app's dependency inventory.
4
+ *
5
+ * A lockfile read tells the MCP server whether a package is direct. For a
6
+ * transitive it cannot tell dev from runtime, so under the shared severity
7
+ * rule a transitive gets the clamp but never the dev discount. In
8
+ * full-database mode the app's `dependency_instances` table records every
9
+ * installed instance with `is_dev` and `scope`; this module reads it.
10
+ *
11
+ * Only a real determination counts: `is_dev = 1`, or a `scope` of runtime,
12
+ * dev or build. The app's current writers stamp every row `is_dev = 0,
13
+ * scope = 'unknown'` as a placeholder. Reading that as "known runtime" would
14
+ * claim a certainty the app itself does not have. The grade would not change
15
+ * (unknown and runtime both get no discount), but `dev_scope_known` would lie.
16
+ */
17
+ const DETERMINED_SCOPES = new Set(["runtime", "dev", "build"]);
18
+ /**
19
+ * A project path in the app's storage form: forward slashes, lowercased on
20
+ * Windows only. Must match `project_inclusion::canonical_storage_path` in the
21
+ * app, or the lookup misses every row.
22
+ */
23
+ export function canonicalStoragePath(p) {
24
+ const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
25
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
26
+ }
27
+ function instanceKey(ecosystem, name, version) {
28
+ const normalized = ecosystem === "PyPI" ? name.toLowerCase() : name;
29
+ return `${normalized}\0${version}`;
30
+ }
31
+ function hasInstancesTable(db) {
32
+ try {
33
+ return (db
34
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'dependency_instances'")
35
+ .get() !== undefined);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Every instance row for one (project, ecosystem), keyed by name + version. Null when unreadable. */
42
+ function loadScopeIndex(db, dir, ecosystem) {
43
+ const project = canonicalStoragePath(dir);
44
+ let rows;
45
+ try {
46
+ rows = db
47
+ .prepare("SELECT package_name, version, is_dev, scope FROM dependency_instances WHERE project_path = ? AND ecosystem = ?")
48
+ .all(project, ecosystem);
49
+ }
50
+ catch {
51
+ try {
52
+ // A table without the `scope` column: only `is_dev = 1` can count.
53
+ rows = db
54
+ .prepare("SELECT package_name, version, is_dev FROM dependency_instances WHERE project_path = ? AND ecosystem = ?")
55
+ .all(project, ecosystem);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ const index = new Map();
62
+ for (const row of rows) {
63
+ const scope = (row.scope ?? "").toLowerCase();
64
+ const isDev = row.is_dev === 1 || scope === "dev";
65
+ index.set(instanceKey(ecosystem, row.package_name, row.version), {
66
+ known: row.is_dev === 1 || DETERMINED_SCOPES.has(scope),
67
+ isDev,
68
+ });
69
+ }
70
+ return index;
71
+ }
72
+ /**
73
+ * Stamp transitive dependencies with the dev scope the app determined for
74
+ * them. A dependency pinned in several workspaces is dev-only only when every
75
+ * one of them says dev, and known only when every one of them was determined.
76
+ * Anything short of that keeps the resolver's own (unknown) scope. Direct
77
+ * dependencies are untouched: their manifest already says. Returns new
78
+ * objects; never throws; a database without the table changes nothing.
79
+ */
80
+ export function applyInstanceDevScope(db, deps) {
81
+ if (!db || !hasInstancesTable(db))
82
+ return deps;
83
+ const indexes = new Map();
84
+ const indexFor = (dir, ecosystem) => {
85
+ const key = `${canonicalStoragePath(dir)}\0${ecosystem}`;
86
+ if (!indexes.has(key))
87
+ indexes.set(key, loadScopeIndex(db, dir, ecosystem));
88
+ return indexes.get(key) ?? null;
89
+ };
90
+ return deps.map((dep) => {
91
+ if (dep.isDirect || !dep.version || dep.sourceDirs.length === 0)
92
+ return dep;
93
+ let allDev = true;
94
+ for (const dir of dep.sourceDirs) {
95
+ const row = indexFor(dir, dep.ecosystem)?.get(instanceKey(dep.ecosystem, dep.name, dep.version));
96
+ if (!row || !row.known)
97
+ return dep;
98
+ allDev &&= row.isDev;
99
+ }
100
+ return { ...dep, devScopeKnown: true, isDev: allDev };
101
+ });
102
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A cheap identity for a file at one moment, so the live layer can notice a
3
+ * change with stat calls alone: "absent", or mtime + size + inode.
4
+ *
5
+ * The inode matters for node_modules. npm packs every tarball file with one
6
+ * fixed mtime, and pnpm swaps a package version by re-pointing a link at a
7
+ * different store file, so two versions can share an mtime. They cannot share
8
+ * an inode.
9
+ */
10
+ export type FileSignature = string;
11
+ export declare const ABSENT: FileSignature;
12
+ export declare function fileSignature(filePath: string): FileSignature;
13
+ /** The modification time a signature recorded, or null for an absent file. */
14
+ export declare function signatureMtime(signature: FileSignature | undefined): number | null;
15
+ /** Signatures for a set of paths, taken now. */
16
+ export declare function signatureMap(paths: Iterable<string>): Map<string, FileSignature>;
17
+ /** True when any recorded file changed, appeared, or disappeared since its signature was taken. */
18
+ export declare function anySignatureChanged(snapshot: Map<string, FileSignature>): boolean;
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * A cheap identity for a file at one moment, so the live layer can notice a
4
+ * change with stat calls alone: "absent", or mtime + size + inode.
5
+ *
6
+ * The inode matters for node_modules. npm packs every tarball file with one
7
+ * fixed mtime, and pnpm swaps a package version by re-pointing a link at a
8
+ * different store file, so two versions can share an mtime. They cannot share
9
+ * an inode.
10
+ */
11
+ import * as fs from "node:fs";
12
+ export const ABSENT = "absent";
13
+ export function fileSignature(filePath) {
14
+ try {
15
+ const s = fs.statSync(filePath);
16
+ return `${s.mtimeMs}:${s.size}:${s.ino}`;
17
+ }
18
+ catch {
19
+ return ABSENT;
20
+ }
21
+ }
22
+ /** The modification time a signature recorded, or null for an absent file. */
23
+ export function signatureMtime(signature) {
24
+ if (!signature || signature === ABSENT)
25
+ return null;
26
+ const mtime = Number(signature.split(":")[0]);
27
+ return Number.isFinite(mtime) ? mtime : null;
28
+ }
29
+ /** Signatures for a set of paths, taken now. */
30
+ export function signatureMap(paths) {
31
+ const out = new Map();
32
+ for (const p of paths)
33
+ out.set(p, fileSignature(p));
34
+ return out;
35
+ }
36
+ /** True when any recorded file changed, appeared, or disappeared since its signature was taken. */
37
+ export function anySignatureChanged(snapshot) {
38
+ for (const [p, recorded] of snapshot) {
39
+ if (fileSignature(p) !== recorded)
40
+ return true;
41
+ }
42
+ return false;
43
+ }
@@ -21,7 +21,9 @@ export class GoRegistry {
21
21
  this.rateLimiter = rateLimiter;
22
22
  }
23
23
  async getPackageInfo(name, currentVersion, isDev) {
24
- const cacheKey = `go-reg:${name}`;
24
+ // Keyed by the installed version too: the cached answer carries currentVersion
25
+ // and its distance, so a name-only key served one project's answer to another.
26
+ const cacheKey = `go-reg:${name}@${currentVersion ?? ""}:${isDev ? 1 : 0}`;
25
27
  const cached = this.cache.get(cacheKey);
26
28
  if (cached !== null)
27
29
  return cached;
@@ -10,7 +10,7 @@
10
10
  const HN_ALGOLIA_URL = "https://hn.algolia.com/api/v1/search";
11
11
  const HN_TIMEOUT_MS = 8_000;
12
12
  const HN_CACHE_TTL = 1800; // 30 minutes
13
- const MAX_QUERIES = 5;
13
+ const MAX_QUERIES = 8;
14
14
  const RESULTS_PER_QUERY = 10;
15
15
  const MIN_POINTS = 10;
16
16
  export class HNFetcher {
@@ -110,7 +110,9 @@ function scoreAndMap(hits, techStack) {
110
110
  // "reactors" and weather stories arrived this way), not ecosystem news.
111
111
  .filter(({ matchedTech }) => matchedTech.length > 0)
112
112
  .map(({ hit, matchedTech }) => {
113
- const techBoost = Math.min(matchedTech.length * 0.2, 0.4);
113
+ // A headline naming a dependency outranks language-level news.
114
+ const named = matchedTech.filter((t) => !LANGUAGE_TERMS.has(t));
115
+ const techBoost = named.length > 0 ? Math.min(0.3 + named.length * 0.1, 0.5) : 0.1;
114
116
  const pointsBoost = Math.min(hit.points / 500, 0.3);
115
117
  const commentsBoost = Math.min(hit.num_comments / 200, 0.2);
116
118
  const score = Math.min(0.1 + techBoost + pointsBoost + commentsBoost, 1.0);
@@ -123,12 +125,16 @@ function scoreAndMap(hits, techStack) {
123
125
  comments: hit.num_comments,
124
126
  published: hit.created_at,
125
127
  relevanceScore: Math.round(score * 100) / 100,
126
- relevanceReason: `Matches your stack: ${matchedTech.join(", ")}`,
128
+ relevanceReason: named.length > 0
129
+ ? `Names ${named.join(", ")} (used in your projects)`
130
+ : `Language-level news: ${matchedTech.join(", ")}`,
127
131
  };
128
132
  })
129
133
  .sort((a, b) => b.relevanceScore - a.relevanceScore)
130
134
  .slice(0, 15);
131
135
  }
136
+ /** Search terms that name a language rather than a dependency. */
137
+ const LANGUAGE_TERMS = new Set(["rust", "python", "zig", "kotlin", "swift", "ruby", "elixir"]);
132
138
  const SKIP_TERMS = new Set([
133
139
  "javascript", "typescript", // too generic, floods results
134
140
  "docker", // too broad
@@ -8,9 +8,12 @@
8
8
  * Set FOURDA_OFFLINE=true to disable all network calls.
9
9
  */
10
10
  import type Database from "better-sqlite3";
11
- import type { ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
12
- export type { VulnerabilityScanResult, VulnerabilityEntry, LiveHeadline, LiveIntelligenceStatus, RegistryPackageInfo, DependencyHealthResult } from "./types.js";
11
+ import type { ReleaseMetadataSource } from "./release-metadata.js";
12
+ import { type ResolutionGroup } from "./resolution.js";
13
+ import type { InstallDriftRecord, ResolutionProvenance, ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
14
+ export type { VulnerabilityScanResult, VulnerabilityEntry, LiveHeadline, LiveIntelligenceStatus, RegistryPackageInfo, DependencyHealthResult, InstallDriftRecord, ResolutionProvenance, } from "./types.js";
13
15
  export declare class LiveIntelligence {
16
+ private db;
14
17
  private cache;
15
18
  private rateLimiter;
16
19
  private osvScanner;
@@ -19,13 +22,45 @@ export declare class LiveIntelligence {
19
22
  private cratesRegistry;
20
23
  private pypiRegistry;
21
24
  private goRegistry;
25
+ private npmPackuments;
26
+ private cratesVersions;
22
27
  private enabled;
23
28
  private lastVulnScan;
29
+ /** Whether the stored scan included direct devDependencies. */
30
+ private lastVulnScanIncludesDev;
31
+ /**
32
+ * Order of scan REQUESTS. A scan's result is stored only if no later request
33
+ * has stored one already: the init warmup (runtime scope) used to finish
34
+ * after an agent's `include_dev` scan and replace it, so `upgrade_planner`
35
+ * read a scan without the dev dependencies the agent had just scanned and
36
+ * sent node-fetch 2.6.0 to the ESM-only 3.3.2 with no advisories (2 of 6
37
+ * runs, 2026-10-03 pre-publish verification).
38
+ */
39
+ private scanRequests;
40
+ private storedScanRequest;
24
41
  private lastHeadlines;
25
42
  private resolvedDeps;
26
43
  private auditDeps;
27
44
  private initialized;
28
45
  private projectRoot;
46
+ /** One entry per resolution group: its inputs, result, and the file signatures that say when it went stale. */
47
+ private groupResults;
48
+ /** When the dependency set in use was assembled: at init, or at the last re-resolution. */
49
+ private resolvedAt;
50
+ /**
51
+ * Bumped by every re-resolution. A scan started under an older generation
52
+ * describes a dependency set that no longer exists: it may still be handed
53
+ * to the call that asked for it, but it is never stored or served later.
54
+ */
55
+ private generation;
56
+ /**
57
+ * The background vulnerability scan started at server init. Tools that need
58
+ * scan data before answering (`what_should_i_know`) await this through
59
+ * `ensureVulnerabilities` instead of reading the cache and finding it empty.
60
+ */
61
+ private warmup;
62
+ /** When the last warmup came back offline; gates the retry so an offline host is not re-probed on every call. */
63
+ private warmupFailedAt;
29
64
  constructor(db: Database.Database);
30
65
  /**
31
66
  * Initialize with project data. Call once after project scan.
@@ -41,6 +76,12 @@ export declare class LiveIntelligence {
41
76
  deps: string[];
42
77
  devDeps: string[];
43
78
  }>, depTargets?: Record<string, string>): void;
79
+ /**
80
+ * Initialize from every independently-locked project under `root` (see
81
+ * project-tree.ts): each group resolves from its own directory and keeps
82
+ * its manifest's platform targets; `root` is the reported project root.
83
+ */
84
+ initFromProjectTree(root: string, groups: ResolutionGroup[]): void;
44
85
  /**
45
86
  * Initialize from dependency groups that each carry their own resolution
46
87
  * directory. Used in 4DA database mode, where dependencies span multiple
@@ -57,6 +98,22 @@ export declare class LiveIntelligence {
57
98
  deps: string[];
58
99
  devDeps: string[];
59
100
  }>): void;
101
+ private applyGroups;
102
+ private rebuildDependencySet;
103
+ /**
104
+ * Re-resolve every group whose inputs changed since they were read (a
105
+ * lockfile rewritten, created or deleted; the fallback manifest; for npm,
106
+ * the node_modules install state), and drop every answer that described the
107
+ * old set: the stored scan and the warmup. Returns true when anything was
108
+ * re-resolved. Stat calls only, so it is cheap enough for the top of every
109
+ * tool call. A process that started before a `git pull` no longer answers
110
+ * for the dependency set it saw at startup.
111
+ */
112
+ refreshIfLockfilesChanged(): boolean;
113
+ /** When the dependency set in use was resolved, and from which files. Null before init. */
114
+ getResolutionProvenance(): ResolutionProvenance | null;
115
+ /** npm direct dependencies whose installed copy differs from the lockfile, per manifest directory. */
116
+ getInstallDrift(): InstallDriftRecord[];
60
117
  /**
61
118
  * Root directory the dependency set was resolved from, or null before init.
62
119
  * For multi-manifest (grouped) init this is the deepest common ancestor of
@@ -66,11 +123,43 @@ export declare class LiveIntelligence {
66
123
  getProjectRoot(): string | null;
67
124
  /**
68
125
  * Run vulnerability scan (returns cached if fresh, fetches otherwise).
126
+ *
127
+ * Without `includeDev`, only DIRECT devDependencies are left out (the
128
+ * documented contract). A transitive stays in whatever its scope: an
129
+ * unknown scope must not be hidden, and a known dev-only one is graded
130
+ * down by the shared severity rule, not dropped.
69
131
  */
70
132
  scanVulnerabilities(projectPath: string, options?: {
71
133
  includeDev?: boolean;
72
134
  forceRefresh?: boolean;
73
135
  }): Promise<VulnerabilityScanResult>;
136
+ /**
137
+ * Start the vulnerability scan in the background without blocking startup.
138
+ * Never rejects: a failed scan resolves to an empty OFFLINE result so the
139
+ * warmup can be awaited by anyone. Idempotent while a warmup is in flight.
140
+ *
141
+ * Before this existed, full-database mode initialised the dependency set
142
+ * and warmed headlines but never scanned, so `lastVulnScan` stayed null
143
+ * until some tool happened to call `vulnerability_scan` — and the first
144
+ * `what_should_i_know` of a session read an empty cache and reported
145
+ * "safe_to_delegate" for a task naming a package with an open advisory.
146
+ */
147
+ startVulnerabilityWarmup(projectPath: string): void;
148
+ /**
149
+ * A usable vulnerability scan, or null. Re-resolves first when a lockfile
150
+ * changed. Returns the stored scan when one exists, otherwise awaits the
151
+ * warmup (starting one if none is running) for at most `timeoutMs`. Null
152
+ * means the scan is not available: disabled, still running past the
153
+ * deadline, finished offline (an offline scan makes no clean-dependency
154
+ * claims, so it cannot support a "safe" verdict), or overtaken by a
155
+ * re-resolution while it ran. Never throws. A timed-out warmup keeps running
156
+ * so a later call can use it.
157
+ */
158
+ ensureVulnerabilities(projectPath: string, timeoutMs: number, options?: {
159
+ includeDev?: boolean;
160
+ }): Promise<VulnerabilityScanResult | null>;
161
+ /** A scan's result within `timeoutMs`, or null when it times out, comes back offline, or is overtaken by a re-resolution. */
162
+ private awaitScan;
74
163
  /**
75
164
  * Fetch relevant headlines for the user's tech stack.
76
165
  */
@@ -79,6 +168,8 @@ export declare class LiveIntelligence {
79
168
  * Get last vulnerability scan result (from cache/memory, no network).
80
169
  */
81
170
  getVulnerabilities(): VulnerabilityScanResult | null;
171
+ /** Whether the stored scan covered direct devDependencies (false when there is none). */
172
+ lastScanIncludesDev(): boolean;
82
173
  /**
83
174
  * Get last headlines (from cache/memory, no network).
84
175
  */
@@ -91,5 +182,17 @@ export declare class LiveIntelligence {
91
182
  isEnabled(): boolean;
92
183
  isInitialized(): boolean;
93
184
  fetchRegistryHealth(deps: ResolvedDependency[]): Promise<RegistryPackageInfo[]>;
185
+ /** Per-release registry readers for dependency_check (name-only requests). */
186
+ getReleaseSources(): {
187
+ npm: ReleaseMetadataSource;
188
+ "crates.io": ReleaseMetadataSource;
189
+ };
190
+ /**
191
+ * OSV advisories for explicit (ecosystem, name, version) triples that need
192
+ * not be in the project, e.g. a version an agent is about to install. Same
193
+ * scanner, cache and rate limit as vulnerability_scan; never touches the
194
+ * stored project scan. Disabled or failed yields an `offline` result.
195
+ */
196
+ queryAdvisories(deps: ResolvedDependency[]): Promise<VulnerabilityScanResult>;
94
197
  getStatus(): LiveIntelligenceStatus;
95
198
  }