@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,91 @@
1
+ /**
2
+ * Knowledge-gap grading: the pure rules that decide whether an unread item is
3
+ * evidence about a dependency and how severe the gap is. The database walk
4
+ * lives in knowledge-gaps.ts; advisory ranges in knowledge-gap-ranges.ts.
5
+ */
6
+ import type { AdvisoryTier } from "./knowledge-gap-ranges.js";
7
+ export declare function mentionsPackage(text: string, pkg: string): boolean;
8
+ /** A knowledge gap's severity. */
9
+ export type GapSeverity = "critical" | "high" | "medium" | "low";
10
+ /** The subset of an item this grading needs. */
11
+ export interface GradableItem {
12
+ title: string | null;
13
+ source_type?: string | null;
14
+ content_type?: string | null;
15
+ /**
16
+ * Set by a caller that has already decided whether this item cites the
17
+ * dependency: the dependency linker's structured proof says what a title
18
+ * cannot. Unset, the title decides.
19
+ */
20
+ cites?: boolean;
21
+ }
22
+ /** Package-registry sources: every row is a published version of a package. */
23
+ export declare const REGISTRY_SOURCES: Set<string>;
24
+ /** Advisory sources: every row is a security advisory. */
25
+ export declare const ADVISORY_SOURCES: Set<string>;
26
+ /**
27
+ * A registry advisory row (`knowledge_decay::is_advisory_row`). An editorial
28
+ * story the pipeline typed `security_advisory` is not one: it cites by title
29
+ * like any other story and is never proof of exposure (AD-040 rule 4).
30
+ */
31
+ export declare function isAdvisoryRow(item: GradableItem): boolean;
32
+ /**
33
+ * The subject package of an advisory title in the ingested "[ID] package:
34
+ * summary" shape ("[GHSA-h395-gr6q-cpjc] jsonwebtoken: ..."), or null when
35
+ * the title has another shape and cannot be read that way.
36
+ */
37
+ export declare function advisorySubject(title: string): string | null;
38
+ /** Same package name, ignoring case and the crates.io `-`/`_` split. */
39
+ export declare function samePackage(a: string, b: string): boolean;
40
+ /** The released version a registry row announces ("npm: react v19.3.0" → "19.3.0"), if any. */
41
+ export declare function registryVersionFromTitle(title: string): string | null;
42
+ /**
43
+ * Is an install inside a known advisory range?
44
+ * - exposed: a readable same-ecosystem advisory range contains the version
45
+ * - safe: every readable same-ecosystem range excludes it
46
+ * - unknown: nothing readable stored for its ecosystem, or no readable
47
+ * installed version — no claim either way
48
+ */
49
+ export type Exposure = "exposed" | "safe" | "unknown";
50
+ /** Parse a source_items.published_at value (SQLite "YYYY-MM-DD HH:MM:SS" or ISO). */
51
+ export declare function parsePublishedAt(value: string | null | undefined): number | null;
52
+ /**
53
+ * Grade a knowledge gap by CONSEQUENCE, never by volume, on the tiers the
54
+ * desktop app draws (`knowledge_decay::classify_severity`):
55
+ *
56
+ * - `critical`: an advisory citing the dependency still reaches an install,
57
+ * and the most severe advisory reaching an install is critical or high by
58
+ * its OWN grade — CVSS band, else the source's curated label (AD-040 rule 2).
59
+ * - `high`: the same with a medium, low or ungraded advisory; or a title
60
+ * naming the dependency with a breaking change, deprecation or end of life.
61
+ * - `medium`: a registry release newer than an install, or a title naming the
62
+ * dependency with a release, update or upgrade.
63
+ * - `low`: everything else, including a large pile of passing mentions.
64
+ *
65
+ * `critical` used to mean "an advisory names the dependency", whatever the
66
+ * advisory's own tier: the jsonwebtoken crate's GHSA-h395-gr6q-cpjc, graded
67
+ * medium by its source and High by the app, was a critical gap here (measured
68
+ * 2026-09-11). Earlier still, `medium` meant "3+ recent unread mentions"
69
+ * matched on the content body: a `tracing` gap evidenced by "The Matrix:
70
+ * Writing Code That Doesn't Need Comments", a `typescript` gap by a
71
+ * Databricks job posting, fourteen of fifteen gaps noise. Two implementations
72
+ * of one concept disagreeing is what this mirrors the app to avoid.
73
+ */
74
+ export declare function gradeGap(items: GradableItem[], packageName: string,
75
+ /**
76
+ * An advisory citing this dependency still reaches an install, or cannot be
77
+ * ruled out. False when every such advisory is fixed at or below the
78
+ * installed versions: you cannot be "critically behind" on something you
79
+ * already patched. Defaults to true so callers without version data keep
80
+ * the conservative grade.
81
+ */
82
+ stillVulnerable?: boolean,
83
+ /**
84
+ * The installed version(s), when known. A registry row is only a version
85
+ * UPDATE when it is newer than an install (AD-041): the row for the version
86
+ * every project already runs is not missed intelligence. Unknown keeps the
87
+ * conservative grade.
88
+ */
89
+ installedVersion?: string | readonly string[] | null,
90
+ /** The tier of the most severe stored advisory reaching an install; null when ungraded. */
91
+ advisoryTier?: AdvisoryTier | null): GapSeverity;
@@ -0,0 +1,157 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Knowledge-gap grading: the pure rules that decide whether an unread item is
4
+ * evidence about a dependency and how severe the gap is. The database walk
5
+ * lives in knowledge-gaps.ts; advisory ranges in knowledge-gap-ranges.ts.
6
+ */
7
+ import { comparePrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
8
+ // Word-boundary matching prevents "cve" matching inside "achieve", "receiver", etc.
9
+ function hasWordBoundary(text, term) {
10
+ const regex = new RegExp(`\\b${escapeRegExp(term)}\\b`, "i");
11
+ return regex.test(text);
12
+ }
13
+ // Package names can contain regex metacharacters (@scope/name, c++, next.js).
14
+ function escapeRegExp(s) {
15
+ return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
16
+ }
17
+ // True when `text` mentions the package name as a whole word. Word characters
18
+ // for this purpose include - and _ (so dep "hono" does NOT match "hono-shim",
19
+ // and never matches "in HONOr of"). @scope/name matches as the full literal.
20
+ export function mentionsPackage(text, pkg) {
21
+ const regex = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(pkg)}($|[^A-Za-z0-9_-])`, "i");
22
+ return regex.test(text);
23
+ }
24
+ /** Package-registry sources: every row is a published version of a package. */
25
+ export const REGISTRY_SOURCES = new Set(["crates_io", "npm_registry", "pypi", "go_modules"]);
26
+ /** Advisory sources: every row is a security advisory. */
27
+ export const ADVISORY_SOURCES = new Set(["osv", "cve"]);
28
+ /**
29
+ * A registry advisory row (`knowledge_decay::is_advisory_row`). An editorial
30
+ * story the pipeline typed `security_advisory` is not one: it cites by title
31
+ * like any other story and is never proof of exposure (AD-040 rule 4).
32
+ */
33
+ export function isAdvisoryRow(item) {
34
+ return ADVISORY_SOURCES.has(item.source_type ?? "");
35
+ }
36
+ /**
37
+ * The subject package of an advisory title in the ingested "[ID] package:
38
+ * summary" shape ("[GHSA-h395-gr6q-cpjc] jsonwebtoken: ..."), or null when
39
+ * the title has another shape and cannot be read that way.
40
+ */
41
+ export function advisorySubject(title) {
42
+ const match = /^\s*\[[^\]]+\]\s*([^:\s]+)\s*:/.exec(title);
43
+ return match ? match[1] : null;
44
+ }
45
+ /** Same package name, ignoring case and the crates.io `-`/`_` split. */
46
+ export function samePackage(a, b) {
47
+ const norm = (s) => s.toLowerCase().replace(/_/g, "-");
48
+ return norm(a) === norm(b);
49
+ }
50
+ /** The released version a registry row announces ("npm: react v19.3.0" → "19.3.0"), if any. */
51
+ export function registryVersionFromTitle(title) {
52
+ const versions = title.match(/\bv?\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?\b/g);
53
+ if (!versions || versions.length === 0)
54
+ return null;
55
+ return versions[versions.length - 1].replace(/^v/, "");
56
+ }
57
+ /** Parse a source_items.published_at value (SQLite "YYYY-MM-DD HH:MM:SS" or ISO). */
58
+ export function parsePublishedAt(value) {
59
+ if (!value)
60
+ return null;
61
+ const iso = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
62
+ const ms = Date.parse(iso);
63
+ return Number.isNaN(ms) ? null : ms;
64
+ }
65
+ /**
66
+ * Does `item` cite `packageName` by its title alone? An advisory row cites
67
+ * only its SUBJECT package and never falls back to a word in its title
68
+ * ("[CVE-2026-63642] MagicMirror newsfeed Socket.IO notification ..." is not
69
+ * a socket.io advisory); every other row names the package in its title.
70
+ */
71
+ function citesByTitle(item, packageName) {
72
+ const title = item.title || "";
73
+ if (isAdvisoryRow(item)) {
74
+ const subject = advisorySubject(title);
75
+ return subject !== null && samePackage(subject, packageName);
76
+ }
77
+ return mentionsPackage(title, packageName);
78
+ }
79
+ const RETIREMENT_KEYWORDS = ["breaking", "deprecated", "deprecation", "eol"];
80
+ const CONSEQUENCE_KEYWORDS = ["release", "released", "update", "upgrade"];
81
+ /**
82
+ * Grade a knowledge gap by CONSEQUENCE, never by volume, on the tiers the
83
+ * desktop app draws (`knowledge_decay::classify_severity`):
84
+ *
85
+ * - `critical`: an advisory citing the dependency still reaches an install,
86
+ * and the most severe advisory reaching an install is critical or high by
87
+ * its OWN grade — CVSS band, else the source's curated label (AD-040 rule 2).
88
+ * - `high`: the same with a medium, low or ungraded advisory; or a title
89
+ * naming the dependency with a breaking change, deprecation or end of life.
90
+ * - `medium`: a registry release newer than an install, or a title naming the
91
+ * dependency with a release, update or upgrade.
92
+ * - `low`: everything else, including a large pile of passing mentions.
93
+ *
94
+ * `critical` used to mean "an advisory names the dependency", whatever the
95
+ * advisory's own tier: the jsonwebtoken crate's GHSA-h395-gr6q-cpjc, graded
96
+ * medium by its source and High by the app, was a critical gap here (measured
97
+ * 2026-09-11). Earlier still, `medium` meant "3+ recent unread mentions"
98
+ * matched on the content body: a `tracing` gap evidenced by "The Matrix:
99
+ * Writing Code That Doesn't Need Comments", a `typescript` gap by a
100
+ * Databricks job posting, fourteen of fifteen gaps noise. Two implementations
101
+ * of one concept disagreeing is what this mirrors the app to avoid.
102
+ */
103
+ export function gradeGap(items, packageName,
104
+ /**
105
+ * An advisory citing this dependency still reaches an install, or cannot be
106
+ * ruled out. False when every such advisory is fixed at or below the
107
+ * installed versions: you cannot be "critically behind" on something you
108
+ * already patched. Defaults to true so callers without version data keep
109
+ * the conservative grade.
110
+ */
111
+ stillVulnerable = true,
112
+ /**
113
+ * The installed version(s), when known. A registry row is only a version
114
+ * UPDATE when it is newer than an install (AD-041): the row for the version
115
+ * every project already runs is not missed intelligence. Unknown keeps the
116
+ * conservative grade.
117
+ */
118
+ installedVersion = null,
119
+ /** The tier of the most severe stored advisory reaching an install; null when ungraded. */
120
+ advisoryTier = null) {
121
+ const cites = (item) => item.cites ?? citesByTitle(item, packageName);
122
+ // The tiers below the security tier read the TITLE, as the app's do: it
123
+ // must name the dependency (an advisory's subject counts).
124
+ const titleNames = (item) => mentionsPackage(item.title || "", packageName) || (isAdvisoryRow(item) && citesByTitle(item, packageName));
125
+ // "Announcing <thing> <version>" is the canonical release phrasing and names
126
+ // no other keyword. The version token is REQUIRED, matching the rule
127
+ // `content_dna_classifiers` settled on: it keeps "Announcing axum 0.8.0" and
128
+ // rejects "Announcing Toasty, an async ORM" and "Announcing our Series B".
129
+ const announcesAVersion = (title) => (hasWordBoundary(title, "announcing") || hasWordBoundary(title, "introducing")) && /\bv?\d+\.\d+/.test(title);
130
+ const retires = (title) => RETIREMENT_KEYWORDS.some((kw) => hasWordBoundary(title, kw)) || /\bend[\s-]of[\s-]life\b/i.test(title);
131
+ const carriesConsequence = (title) => CONSEQUENCE_KEYWORDS.some((kw) => hasWordBoundary(title, kw)) || announcesAVersion(title);
132
+ // A registry row IS a version update: "crates.io: serde v1.0.220" names no
133
+ // consequence keyword, but an unread release of a direct dependency is the
134
+ // exact thing `knowledge_decay::gap_is_substantive` counts — provided it is
135
+ // newer than an install (live: "npm: @tauri-apps/api v2.11.1" on 2.11.1).
136
+ // Compared by semver precedence, so 0.10.0-rc.19 is newer than rc.18.
137
+ const installed = (typeof installedVersion === "string" ? [installedVersion] : (installedVersion ?? []))
138
+ .map((v) => parseSemverPrecedence(v))
139
+ .filter((v) => v !== null);
140
+ const isNewerRelease = (item) => {
141
+ if (!REGISTRY_SOURCES.has(item.source_type ?? ""))
142
+ return false;
143
+ const released = parseSemverPrecedence(registryVersionFromTitle(item.title || "") ?? "");
144
+ if (released === null || installed.length === 0)
145
+ return true;
146
+ return installed.some((v) => comparePrecedence(released, v) > 0);
147
+ };
148
+ if (stillVulnerable && items.some((item) => isAdvisoryRow(item) && cites(item))) {
149
+ return advisoryTier === "critical" || advisoryTier === "high" ? "critical" : "high";
150
+ }
151
+ if (items.some((item) => cites(item) && titleNames(item) && retires(item.title || "")))
152
+ return "high";
153
+ if (items.some((item) => cites(item) && (isNewerRelease(item) || (titleNames(item) && carriesConsequence(item.title || ""))))) {
154
+ return "medium";
155
+ }
156
+ return "low";
157
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Knowledge-gap installs: the install each declaring project carries, with
3
+ * its ecosystem (AD-045: a package is (ecosystem, name)). The MCP twin of the
4
+ * version join in `temporal::get_all_dependencies` (tiers i and ii),
5
+ * `knowledge_decay::installs_for` and `osv::exposure::canonical`.
6
+ *
7
+ * The first cut resolved ONE version per package name (first seen across the
8
+ * machine) and displayed it beside whichever project row came first, so a gap
9
+ * could judge one project's install and name another's.
10
+ */
11
+ import type { FourDADatabase } from "../db.js";
12
+ import type { LiveIntelligence } from "../live/index.js";
13
+ /** A `project_dependencies` row as the gap walk reads it. */
14
+ export interface DeclaringRow {
15
+ package_name: string;
16
+ version: string | null;
17
+ project_path: string;
18
+ language: string;
19
+ }
20
+ /** One declaring project's install of a dependency. */
21
+ export interface Install {
22
+ projectPath: string;
23
+ language: string;
24
+ /** The OSV ecosystem ("npm", "crates.io", ...); null when the label names none the app knows. */
25
+ ecosystem: string | null;
26
+ version: string | null;
27
+ }
28
+ /**
29
+ * The OSV ecosystem a `project_dependencies.language` or
30
+ * `user_dependencies.ecosystem` label names, or null when it names none
31
+ * (`osv::exposure::canonical`). `mapEcosystem` falls back to npm for a label
32
+ * it does not know; that fallback must not decide which advisories apply.
33
+ */
34
+ export declare function osvEcosystem(label: string | null | undefined): string | null;
35
+ export declare const sameEcosystem: (a: string, b: string) => boolean;
36
+ /** Does a stored name denote this dependency? crates.io treats `-` and `_` as one namespace. */
37
+ export declare function denotes(storedName: string, ecosystem: string | null, depName: string): boolean;
38
+ /** The other crates.io spelling of a name (`http_body_util` / `http-body-util`). */
39
+ export declare const otherSpelling: (name: string) => string;
40
+ /**
41
+ * Resolves the install each declaring project carries: the manifest's own
42
+ * version when it is a real version; else the lockfile-resolved one in
43
+ * `user_dependencies` (the same project and ecosystem family, then a lockfile
44
+ * under the same active repo root — a workspace member resolved by the
45
+ * workspace lockfile); else the live resolver's version for that project.
46
+ * Never across repo roots or ecosystems: a project is never judged on a
47
+ * sibling's version.
48
+ */
49
+ export declare class InstallResolver {
50
+ private readonly lockStmt;
51
+ private readonly lockRowsByName;
52
+ private readonly resolved;
53
+ private readonly activeRoots;
54
+ constructor(db: FourDADatabase, liveIntel: Pick<LiveIntelligence, "getResolvedDeps" | "isInitialized"> | null | undefined, activeRoots: readonly string[]);
55
+ /** One install per declaring project and ecosystem. */
56
+ installsFor(rows: DeclaringRow[]): Install[];
57
+ private versionOf;
58
+ private lockRows;
59
+ /**
60
+ * The live resolver's version for THIS project: the resolution whose
61
+ * manifest directory is the project. Without a lockfile table (standalone
62
+ * mode, one project) the resolver's version for the package is the install.
63
+ */
64
+ private fromResolver;
65
+ }
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Knowledge-gap installs: the install each declaring project carries, with
4
+ * its ecosystem (AD-045: a package is (ecosystem, name)). The MCP twin of the
5
+ * version join in `temporal::get_all_dependencies` (tiers i and ii),
6
+ * `knowledge_decay::installs_for` and `osv::exposure::canonical`.
7
+ *
8
+ * The first cut resolved ONE version per package name (first seen across the
9
+ * machine) and displayed it beside whichever project row came first, so a gap
10
+ * could judge one project's install and name another's.
11
+ */
12
+ import { parseSemverPrecedence } from "../live/semver-precedence.js";
13
+ import { mapEcosystem } from "../live/version-resolver.js";
14
+ import { samePackage } from "./knowledge-gap-grading.js";
15
+ import { allRows, normPath, sharesRoot } from "./knowledge-gap-scope.js";
16
+ const NPM_LABELS = new Set(["npm", "javascript", "typescript", "node", "js", "ts"]);
17
+ /**
18
+ * The OSV ecosystem a `project_dependencies.language` or
19
+ * `user_dependencies.ecosystem` label names, or null when it names none
20
+ * (`osv::exposure::canonical`). `mapEcosystem` falls back to npm for a label
21
+ * it does not know; that fallback must not decide which advisories apply.
22
+ */
23
+ export function osvEcosystem(label) {
24
+ const lower = (label ?? "").trim().toLowerCase();
25
+ const mapped = mapEcosystem(lower);
26
+ return mapped === "npm" && !NPM_LABELS.has(lower) ? null : mapped;
27
+ }
28
+ /** The family a lockfile join compares: an unknown label is its own family (`temporal::ecosystem_family`). */
29
+ function ecosystemFamily(label) {
30
+ return osvEcosystem(label) ?? `other:${(label ?? "").trim().toLowerCase()}`;
31
+ }
32
+ export const sameEcosystem = (a, b) => a.toLowerCase() === b.toLowerCase();
33
+ /** Does a stored name denote this dependency? crates.io treats `-` and `_` as one namespace. */
34
+ export function denotes(storedName, ecosystem, depName) {
35
+ if (storedName.toLowerCase() === depName.toLowerCase())
36
+ return true;
37
+ return ecosystem !== null && sameEcosystem(ecosystem, "crates.io") && samePackage(storedName, depName);
38
+ }
39
+ /** The other crates.io spelling of a name (`http_body_util` / `http-body-util`). */
40
+ export const otherSpelling = (name) => name.includes("_") ? name.replace(/_/g, "-") : name.replace(/-/g, "_");
41
+ /**
42
+ * Resolves the install each declaring project carries: the manifest's own
43
+ * version when it is a real version; else the lockfile-resolved one in
44
+ * `user_dependencies` (the same project and ecosystem family, then a lockfile
45
+ * under the same active repo root — a workspace member resolved by the
46
+ * workspace lockfile); else the live resolver's version for that project.
47
+ * Never across repo roots or ecosystems: a project is never judged on a
48
+ * sibling's version.
49
+ */
50
+ export class InstallResolver {
51
+ lockStmt = null;
52
+ lockRowsByName = new Map();
53
+ resolved;
54
+ activeRoots;
55
+ constructor(db, liveIntel, activeRoots) {
56
+ this.activeRoots = activeRoots;
57
+ this.resolved = liveIntel && liveIntel.isInitialized() ? liveIntel.getResolvedDeps() : [];
58
+ const has = (column) => db.hasColumn("user_dependencies", column);
59
+ if (!["project_path", "package_name", "version", "ecosystem"].every(has))
60
+ return;
61
+ const order = [has("is_direct") ? "is_direct DESC" : "", has("last_seen_at") ? "last_seen_at DESC" : ""]
62
+ .filter(Boolean)
63
+ .join(", ");
64
+ try {
65
+ this.lockStmt = db.getRawDb().prepare(`SELECT project_path, package_name, version, ecosystem FROM user_dependencies
66
+ WHERE lower(package_name) IN (lower(?), lower(?)) AND version IS NOT NULL AND version != ''
67
+ ${order ? `ORDER BY ${order}` : ""}`);
68
+ }
69
+ catch {
70
+ // No readable lockfile table: fall through to the resolver.
71
+ }
72
+ }
73
+ /** One install per declaring project and ecosystem. */
74
+ installsFor(rows) {
75
+ const seen = new Set();
76
+ const installs = [];
77
+ for (const row of rows) {
78
+ const key = `${normPath(row.project_path)}\0${ecosystemFamily(row.language)}`;
79
+ if (seen.has(key))
80
+ continue;
81
+ seen.add(key);
82
+ installs.push({
83
+ projectPath: row.project_path,
84
+ language: row.language,
85
+ ecosystem: osvEcosystem(row.language),
86
+ version: this.versionOf(row),
87
+ });
88
+ }
89
+ return installs;
90
+ }
91
+ versionOf(row) {
92
+ // A manifest records a RANGE when it records anything ("^4.1.5"); only a
93
+ // real version is an install.
94
+ if (row.version && parseSemverPrecedence(row.version) !== null)
95
+ return row.version;
96
+ const family = ecosystemFamily(row.language);
97
+ const locked = this.lockRows(row.package_name).filter((l) => ecosystemFamily(l.ecosystem) === family && denotes(l.package_name, family, row.package_name));
98
+ const here = normPath(row.project_path);
99
+ const own = locked.find((l) => normPath(l.project_path) === here);
100
+ if (own)
101
+ return own.version;
102
+ const workspace = locked.find((l) => sharesRoot(row.project_path, l.project_path, this.activeRoots));
103
+ if (workspace)
104
+ return workspace.version;
105
+ return this.fromResolver(row);
106
+ }
107
+ lockRows(name) {
108
+ if (!this.lockStmt)
109
+ return [];
110
+ const key = name.toLowerCase();
111
+ let rows = this.lockRowsByName.get(key);
112
+ if (!rows) {
113
+ rows = allRows(this.lockStmt, name, otherSpelling(name));
114
+ this.lockRowsByName.set(key, rows);
115
+ }
116
+ return rows;
117
+ }
118
+ /**
119
+ * The live resolver's version for THIS project: the resolution whose
120
+ * manifest directory is the project. Without a lockfile table (standalone
121
+ * mode, one project) the resolver's version for the package is the install.
122
+ */
123
+ fromResolver(row) {
124
+ const ecosystem = osvEcosystem(row.language);
125
+ if (ecosystem === null)
126
+ return null;
127
+ const matches = this.resolved.filter((d) => d.version && sameEcosystem(d.ecosystem, ecosystem) && denotes(d.name, ecosystem, row.package_name));
128
+ const here = normPath(row.project_path);
129
+ const own = matches.find((d) => (d.sourceDirs ?? []).some((dir) => normPath(dir) === here));
130
+ if (own)
131
+ return own.version;
132
+ return this.lockStmt === null ? (matches[0]?.version ?? null) : null;
133
+ }
134
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * OSV affected-range evaluation for knowledge gaps: the TypeScript twin of
3
+ * `src-tauri/src/osv/matching.rs::check_version_affected`, so the desktop app
4
+ * and this server answer "is this install inside that advisory?" the same way.
5
+ *
6
+ * The first version here understood only `introduced`/`fixed`. GHSA-9crc-q9x8-hgqq
7
+ * stores vitest's pre-1.0 window as `[{"introduced":"0"},{"last_affected":"0.0.125"}]`;
8
+ * the loop never saw a `fixed`, read the trailing `introduced: "0"` as
9
+ * "affected from 0 onward", and put EVERY vitest version inside a CVSS 9.6
10
+ * advisory (measured 2026-09-11 against vitest 4.1.11, whose one real exposure
11
+ * is fixed at exactly 4.1.11).
12
+ */
13
+ /** One event of an OSV affected range; OSV sets exactly one key per event. */
14
+ export interface AdvisoryRangeEvent {
15
+ introduced?: string;
16
+ fixed?: string;
17
+ last_affected?: string;
18
+ limit?: string;
19
+ }
20
+ /** An advisory's own severity tier. */
21
+ export type AdvisoryTier = "critical" | "high" | "medium" | "low";
22
+ /**
23
+ * The SEMVER/ECOSYSTEM ranges of a stored `osv_advisories.affected_ranges`
24
+ * value as event lists, or null when the value is missing or unreadable.
25
+ * `[]` is readable and contains nothing. GIT ranges carry commit hashes and
26
+ * are skipped, as the Rust reference skips them; a range with no `type` is
27
+ * read as SEMVER.
28
+ */
29
+ export declare function parseAffectedRanges(json: string | null | undefined): AdvisoryRangeEvent[][] | null;
30
+ /** Is `installed` inside any of `ranges`? Null when the installed version is unreadable. */
31
+ export declare function rangesContain(ranges: AdvisoryRangeEvent[][], installed: string): boolean | null;
32
+ /**
33
+ * Is `installed` inside any of these advisory ranges?
34
+ *
35
+ * An advisory naming your dependency is only a gap if you are actually
36
+ * exposed: the live tool once graded three Hono CVEs `critical` on hono
37
+ * 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
38
+ * pinned past via `pnpm.overrides`.
39
+ *
40
+ * Conservative by construction: a missing or unreadable installed version
41
+ * counts as AFFECTED. Never claim someone is safe on missing information.
42
+ */
43
+ export declare function versionInAnyRange(ranges: AdvisoryRangeEvent[][], installed: string | null | undefined): boolean;
44
+ /**
45
+ * An advisory's tier: its CVSS band first (>= 9 critical, >= 7 high, >= 4
46
+ * medium, else low: `osv::types::cvss_band`), else the source's curated label,
47
+ * else null (ungraded). Mirrors `knowledge_decay::advisory_tier_for`.
48
+ */
49
+ export declare function advisoryTier(cvssScore: number | null | undefined, label: string | null | undefined): AdvisoryTier | null;
50
+ /** The more severe of two tiers; null (ungraded) ranks below every tier. */
51
+ export declare function moreSevereTier(a: AdvisoryTier | null, b: AdvisoryTier | null): AdvisoryTier | null;
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * OSV affected-range evaluation for knowledge gaps: the TypeScript twin of
4
+ * `src-tauri/src/osv/matching.rs::check_version_affected`, so the desktop app
5
+ * and this server answer "is this install inside that advisory?" the same way.
6
+ *
7
+ * The first version here understood only `introduced`/`fixed`. GHSA-9crc-q9x8-hgqq
8
+ * stores vitest's pre-1.0 window as `[{"introduced":"0"},{"last_affected":"0.0.125"}]`;
9
+ * the loop never saw a `fixed`, read the trailing `introduced: "0"` as
10
+ * "affected from 0 onward", and put EVERY vitest version inside a CVSS 9.6
11
+ * advisory (measured 2026-09-11 against vitest 4.1.11, whose one real exposure
12
+ * is fixed at exactly 4.1.11).
13
+ */
14
+ import { comparePrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
15
+ /**
16
+ * The SEMVER/ECOSYSTEM ranges of a stored `osv_advisories.affected_ranges`
17
+ * value as event lists, or null when the value is missing or unreadable.
18
+ * `[]` is readable and contains nothing. GIT ranges carry commit hashes and
19
+ * are skipped, as the Rust reference skips them; a range with no `type` is
20
+ * read as SEMVER.
21
+ */
22
+ export function parseAffectedRanges(json) {
23
+ if (typeof json !== "string" || json.trim() === "")
24
+ return null;
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(json);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ if (!Array.isArray(parsed))
33
+ return null;
34
+ const ranges = [];
35
+ for (const range of parsed) {
36
+ if (typeof range !== "object" || range === null)
37
+ continue;
38
+ const { type, events } = range;
39
+ if (typeof type === "string" && type !== "SEMVER" && type !== "ECOSYSTEM")
40
+ continue;
41
+ if (!Array.isArray(events))
42
+ continue;
43
+ ranges.push(events.filter((e) => typeof e === "object" && e !== null));
44
+ }
45
+ return ranges;
46
+ }
47
+ /**
48
+ * OSV's marker for a boundary nobody knows ("2.5.0-NA", from PYSEC imports).
49
+ * `matching.rs::is_unknown_bound`: such a bound grounds no match.
50
+ */
51
+ function isUnknownBound(v) {
52
+ const t = v.trim();
53
+ return t.endsWith("-NA") || t.endsWith("-na");
54
+ }
55
+ function closingEvent(event) {
56
+ const read = (v) => (isUnknownBound(v) ? null : parseSemverPrecedence(v));
57
+ if (typeof event.fixed === "string")
58
+ return { bound: read(event.fixed), inclusive: false };
59
+ if (typeof event.last_affected === "string")
60
+ return { bound: read(event.last_affected), inclusive: true };
61
+ if (typeof event.limit === "string") {
62
+ return { bound: event.limit.trim() === "*" ? "unbounded" : read(event.limit), inclusive: false };
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Is `version` inside this one range? Events pair up in order: `introduced`
68
+ * opens, `fixed` and `limit` close exclusively, `last_affected` closes
69
+ * inclusively, and one range may hold several pairs. A closing event always
70
+ * ends the pair it closes, even when its bound is unknown or unreadable: such
71
+ * a bound matches nothing and leaves no open range behind. Only an
72
+ * `introduced` that nothing closed means "affected from there on".
73
+ */
74
+ function eventsContain(events, version) {
75
+ const atOrAbove = (lower) => lower === "zero" || comparePrecedence(version, lower) >= 0;
76
+ let lower = null;
77
+ for (const event of events) {
78
+ if (typeof event.introduced === "string") {
79
+ lower = event.introduced.trim() === "0" ? "zero" : parseSemverPrecedence(event.introduced);
80
+ }
81
+ const upper = closingEvent(event);
82
+ if (upper === null)
83
+ continue;
84
+ if (lower !== null && upper.bound !== null && atOrAbove(lower)) {
85
+ if (upper.bound === "unbounded")
86
+ return true;
87
+ const cmp = comparePrecedence(version, upper.bound);
88
+ if (upper.inclusive ? cmp <= 0 : cmp < 0)
89
+ return true;
90
+ }
91
+ lower = null;
92
+ }
93
+ return lower !== null && atOrAbove(lower);
94
+ }
95
+ /** Is `installed` inside any of `ranges`? Null when the installed version is unreadable. */
96
+ export function rangesContain(ranges, installed) {
97
+ const version = parseSemverPrecedence(installed);
98
+ if (version === null)
99
+ return null;
100
+ return ranges.some((events) => eventsContain(events, version));
101
+ }
102
+ /**
103
+ * Is `installed` inside any of these advisory ranges?
104
+ *
105
+ * An advisory naming your dependency is only a gap if you are actually
106
+ * exposed: the live tool once graded three Hono CVEs `critical` on hono
107
+ * 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
108
+ * pinned past via `pnpm.overrides`.
109
+ *
110
+ * Conservative by construction: a missing or unreadable installed version
111
+ * counts as AFFECTED. Never claim someone is safe on missing information.
112
+ */
113
+ export function versionInAnyRange(ranges, installed) {
114
+ if (!installed)
115
+ return true;
116
+ return rangesContain(ranges, installed) ?? true;
117
+ }
118
+ const TIERS = new Set(["critical", "high", "medium", "low"]);
119
+ const isTier = (s) => TIERS.has(s);
120
+ /**
121
+ * An advisory's tier: its CVSS band first (>= 9 critical, >= 7 high, >= 4
122
+ * medium, else low: `osv::types::cvss_band`), else the source's curated label,
123
+ * else null (ungraded). Mirrors `knowledge_decay::advisory_tier_for`.
124
+ */
125
+ export function advisoryTier(cvssScore, label) {
126
+ if (typeof cvssScore === "number" && Number.isFinite(cvssScore)) {
127
+ if (cvssScore >= 9)
128
+ return "critical";
129
+ if (cvssScore >= 7)
130
+ return "high";
131
+ if (cvssScore >= 4)
132
+ return "medium";
133
+ return "low";
134
+ }
135
+ const l = typeof label === "string" ? label.trim().toLowerCase() : "";
136
+ return isTier(l) ? l : null;
137
+ }
138
+ const TIER_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
139
+ /** The more severe of two tiers; null (ungraded) ranks below every tier. */
140
+ export function moreSevereTier(a, b) {
141
+ if (a === null)
142
+ return b;
143
+ if (b === null)
144
+ return a;
145
+ return TIER_RANK[b] > TIER_RANK[a] ? b : a;
146
+ }