@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.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Changelog discovery and parsing for `upgrade_impact`.
|
|
4
|
+
*
|
|
5
|
+
* Changelogs follow no schema, so the parser recognises the version-heading
|
|
6
|
+
* styles actually found in registry archives rather than one spec:
|
|
7
|
+
* keep-a-changelog `## [1.2.3] - 2024-01-01`
|
|
8
|
+
* conventional `## [7.0.0](https://…/compare/…) (2025-06-24)`
|
|
9
|
+
* plain / prefixed `## 1.2.3`, `# v1.2.3 (2024-01-01)`, `### Version 1.2.3`, `## fastembed 5.0.0`
|
|
10
|
+
* setext `1.2.3` underlined with `===` / `---`
|
|
11
|
+
* History.md (express) `1.2.3 / 2020-01-01`
|
|
12
|
+
* A heading only opens a section when the rest of it looks like a trailer
|
|
13
|
+
* (date, link, "yanked") — `## Upgrading from 1.x` or `#### Rust 1.70 support`
|
|
14
|
+
* must stay sub-headings, or a migration guide would split a release in two.
|
|
15
|
+
*
|
|
16
|
+
* Entries are bullets, numbered items and loose paragraph lines; indented
|
|
17
|
+
* continuation lines join the entry above. Fenced code blocks and link
|
|
18
|
+
* reference definitions are skipped: they are examples and URLs, not changes.
|
|
19
|
+
*/
|
|
20
|
+
import { compareVersionPrecedence } from "./semver-precedence.js";
|
|
21
|
+
import { classifyEntry, classifyHeading, sanitizeEntry } from "./changelog-classify.js";
|
|
22
|
+
const NAME_STEMS = ["changelog", "changes", "history", "releases", "release-notes", "release_notes", "releasenotes", "news"];
|
|
23
|
+
const NAME_EXTS = ["", ".md", ".markdown", ".txt", ".rst"];
|
|
24
|
+
/** True for a root-level basename that names a changelog (case-insensitive). */
|
|
25
|
+
export function isChangelogName(basename) {
|
|
26
|
+
const lower = basename.toLowerCase();
|
|
27
|
+
return NAME_STEMS.some((stem) => NAME_EXTS.some((ext) => lower === stem + ext));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The changelog among archive paths, preferring CHANGELOG over the others and
|
|
31
|
+
* Markdown over plain text. Null when there is none.
|
|
32
|
+
*/
|
|
33
|
+
export function findChangelogFile(paths) {
|
|
34
|
+
let best = null;
|
|
35
|
+
for (const path of paths) {
|
|
36
|
+
const base = path.split("/").filter(Boolean).pop() ?? "";
|
|
37
|
+
if (!isChangelogName(base))
|
|
38
|
+
continue;
|
|
39
|
+
const lower = base.toLowerCase();
|
|
40
|
+
const stem = NAME_STEMS.findIndex((s) => lower.startsWith(s));
|
|
41
|
+
const ext = NAME_EXTS.indexOf(lower.slice(NAME_STEMS[stem].length));
|
|
42
|
+
const rank = stem * 10 + (ext === 1 || ext === 2 ? 0 : ext + 1);
|
|
43
|
+
if (!best || rank < best.rank)
|
|
44
|
+
best = { path, rank };
|
|
45
|
+
}
|
|
46
|
+
return best?.path ?? null;
|
|
47
|
+
}
|
|
48
|
+
const VERSION = String.raw `\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
|
|
49
|
+
const ISO_DATE = /\b(\d{4})[-/.](\d{2})[-/.](\d{2})\b/;
|
|
50
|
+
const MONTH_DATE = /\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.? \d{1,2}(?:st|nd|rd|th)?,? \d{4}\b|\b\d{1,2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*,? \d{4}\b/i;
|
|
51
|
+
/** `[optional prefix] version rest`: prefix is a keyword, a `v`, or a package name. */
|
|
52
|
+
const HEADING = new RegExp(String.raw `^\[?(?:(version|release)\s+|([A-Za-z@][\w@/.-]*)(?:\s+|@))?\[?v?(${VERSION})\]?(.*)$`, "i");
|
|
53
|
+
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
54
|
+
/** ISO date from a heading trailer: `2024-01-01`, `2024/01/01`, `August 1st, 2025`, `1 Aug 2025`. */
|
|
55
|
+
function extractDate(text) {
|
|
56
|
+
const iso = ISO_DATE.exec(text);
|
|
57
|
+
if (iso)
|
|
58
|
+
return `${iso[1]}-${iso[2]}-${iso[3]}`;
|
|
59
|
+
const named = MONTH_DATE.exec(text)?.[0];
|
|
60
|
+
if (!named)
|
|
61
|
+
return null;
|
|
62
|
+
const month = MONTHS.findIndex((m) => named.toLowerCase().includes(m)) + 1;
|
|
63
|
+
const day = /\b(\d{1,2})(?:st|nd|rd|th)?\b(?!\d)/.exec(named.replace(/\d{4}/, ""))?.[1];
|
|
64
|
+
const year = /\d{4}/.exec(named)?.[0];
|
|
65
|
+
if (!month || !day || !year)
|
|
66
|
+
return named;
|
|
67
|
+
return `${year}-${String(month).padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Version and date from a heading's text, or null when it is not a release
|
|
71
|
+
* heading. Exported for tests.
|
|
72
|
+
*/
|
|
73
|
+
export function parseVersionHeading(raw) {
|
|
74
|
+
const text = raw.trim().replace(/^\*\*|\*\*$/g, "");
|
|
75
|
+
const m = HEADING.exec(text);
|
|
76
|
+
if (!m)
|
|
77
|
+
return null;
|
|
78
|
+
const [, keyword, namePrefix, version, rest] = m;
|
|
79
|
+
// A free-form name prefix needs a full x.y.z — "Rust 1.70" is prose, not a release.
|
|
80
|
+
if (namePrefix && !keyword && !/^\d+\.\d+\.\d+/.test(version))
|
|
81
|
+
return null;
|
|
82
|
+
const trailer = rest
|
|
83
|
+
.replace(/\]?\([^)]*\)/g, " ")
|
|
84
|
+
.replace(/\[[^\]]*\]/g, " ")
|
|
85
|
+
.replace(ISO_DATE, " ")
|
|
86
|
+
.replace(MONTH_DATE, " ")
|
|
87
|
+
.replace(/\b(?:yanked|unreleased|released|latest|stable|on)\b/gi, " ")
|
|
88
|
+
.replace(/[-–—/:,.*_#()[\]<>|]/g, " ")
|
|
89
|
+
.trim();
|
|
90
|
+
if (trailer.split(/\s+/).filter(Boolean).length > 2)
|
|
91
|
+
return null;
|
|
92
|
+
return { version, date: extractDate(rest) };
|
|
93
|
+
}
|
|
94
|
+
const ATX = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
95
|
+
const SETEXT_RULE = /^\s*(=+|-+)\s*$/;
|
|
96
|
+
const BULLET = /^(\s*)(?:[-*+•]|\d+[.)])\s+(.*)$/;
|
|
97
|
+
const LINK_DEF = /^\s*\[[^\]]+\]:\s*\S+/;
|
|
98
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
99
|
+
const RULE = /^\s*(?:[-=*_]\s*){3,}$/;
|
|
100
|
+
/** A whole-line bold label ("**Breaking Changes:**") acts as a sub-heading. */
|
|
101
|
+
const BOLD_LABEL = /^\s*\*\*([^*]+)\*\*:?\s*$/;
|
|
102
|
+
/** A short unbulleted line ending in a colon: a label for what follows ("Security:", "Breaking changes:"). */
|
|
103
|
+
const PLAIN_LABEL = /^\s{0,3}([A-Za-z⚠][\w /&,'()⚠️-]{1,58}?)\s*:\s*$/;
|
|
104
|
+
/** A line that opens or closes layout markup rather than describing a change. */
|
|
105
|
+
const HTML_LAYOUT = /^<\/?(?:details|summary|div|p|br|hr|img|picture|source|table|tr|td|th|thead|tbody|center|sup|sub)\b/i;
|
|
106
|
+
/** A bullet that is only a label: "remove:", "**Breaking**:", "deps:". */
|
|
107
|
+
const LABEL_BULLET = /^\W*([A-Za-z][\w -]{0,30}?)\W*:\W*$/;
|
|
108
|
+
function flush(state) {
|
|
109
|
+
if (state.pending && state.current) {
|
|
110
|
+
const text = sanitizeEntry(state.pending.text);
|
|
111
|
+
const under = state.pending.under;
|
|
112
|
+
// Layout markup is not a change: actix-web 4 wraps pre-release notes in
|
|
113
|
+
// `<details> <summary>...</summary>`, and both lines were counted as removals.
|
|
114
|
+
if (text && !HTML_LAYOUT.test(text)) {
|
|
115
|
+
state.current.entries.push({ kind: classifyEntry(text, state.pending.context), text, ...(under ? { under } : {}) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
state.pending = null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Headings that name a kind of change (keep-a-changelog and conventional-commit
|
|
122
|
+
* categories), as opposed to a new document part ("Migration guide", "Older releases").
|
|
123
|
+
*/
|
|
124
|
+
const CATEGORY_HEADING = /^\W*(added|changed|changes|deprecated|deprecations|removed|removals|fixed|fixes|bug ?fixes|security|features?|new features|enhancements?|improvements?|performance( improvements)?|breaking( changes?)?|⚠️?\s*breaking( changes?)?|dependencies|dependency updates|documentation|docs|internal|misc(ellaneous)?|other( changes)?|chores?|refactor(ing)?|reverts?|build|tests?)\W*$/i;
|
|
125
|
+
function isCategoryHeading(raw) {
|
|
126
|
+
return CATEGORY_HEADING.test(raw.trim());
|
|
127
|
+
}
|
|
128
|
+
function openSection(state, heading, level) {
|
|
129
|
+
flush(state);
|
|
130
|
+
state.sectionLevel = level;
|
|
131
|
+
state.current = { version: heading.version, date: heading.date, entries: [] };
|
|
132
|
+
state.sections.push(state.current);
|
|
133
|
+
state.headingKind = null;
|
|
134
|
+
state.parentKind = null;
|
|
135
|
+
state.parentIndent = null;
|
|
136
|
+
state.headingText = null;
|
|
137
|
+
state.parentText = null;
|
|
138
|
+
}
|
|
139
|
+
/** A heading or parent bullet as context text: emphasis and code marks dropped, one line, at most 80 characters. */
|
|
140
|
+
function contextText(raw) {
|
|
141
|
+
return sanitizeEntry(raw.replace(/\*\*|__|`/g, "").replace(/:\s*$/, ""), 80) || null;
|
|
142
|
+
}
|
|
143
|
+
function addLine(state, line) {
|
|
144
|
+
const bullet = BULLET.exec(line);
|
|
145
|
+
if (bullet) {
|
|
146
|
+
flush(state);
|
|
147
|
+
const indent = bullet[1].length;
|
|
148
|
+
const text = bullet[2];
|
|
149
|
+
// The outermost bullet level of a list, wherever it is indented (express's
|
|
150
|
+
// History.md lists sit at two spaces), lends context to bullets nested under it.
|
|
151
|
+
const outer = state.parentIndent === null || indent <= state.parentIndent;
|
|
152
|
+
if (outer) {
|
|
153
|
+
state.parentIndent = indent;
|
|
154
|
+
// A label-only bullet ("* remove:", "- Breaking:") is a heading for its children, not an entry.
|
|
155
|
+
const label = LABEL_BULLET.exec(text);
|
|
156
|
+
if (label) {
|
|
157
|
+
state.parentKind = classifyHeading(label[1]) ?? state.headingKind;
|
|
158
|
+
state.parentText = contextText(label[1]);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// A breaking bullet lends "breaking" to its sub-points (date-fns 3.0: "**BREAKING**: Functions
|
|
162
|
+
// that accept Interval arguments ..." with one nested bullet per affected function); one
|
|
163
|
+
// ending in ":" lends any signalling kind.
|
|
164
|
+
const own = classifyEntry(text, state.headingKind);
|
|
165
|
+
state.parentKind = own === "breaking" || (/:\s*$/.test(text) && own !== "change") ? own : null;
|
|
166
|
+
state.parentText = contextText(text);
|
|
167
|
+
}
|
|
168
|
+
const context = !outer && state.parentKind ? state.parentKind : state.headingKind;
|
|
169
|
+
const under = !outer && state.parentText ? state.parentText : state.headingText;
|
|
170
|
+
state.pending = { text, context, indent, under };
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (line.trim() === "") {
|
|
174
|
+
flush(state);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (state.pending && /^\s+\S/.test(line)) {
|
|
178
|
+
state.pending.text += ` ${line.trim()}`;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (state.pending && state.pending.indent === -1) {
|
|
182
|
+
state.pending.text += ` ${line.trim()}`; // wrapped paragraph line
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
flush(state);
|
|
186
|
+
state.pending = { text: line, context: state.headingKind, indent: -1, under: state.headingText };
|
|
187
|
+
}
|
|
188
|
+
/** Parse a changelog into version sections, in document order (usually newest first). */
|
|
189
|
+
export function parseChangelog(text) {
|
|
190
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
191
|
+
const state = {
|
|
192
|
+
sections: [],
|
|
193
|
+
current: null,
|
|
194
|
+
sectionLevel: 0,
|
|
195
|
+
headingKind: null,
|
|
196
|
+
parentKind: null,
|
|
197
|
+
parentIndent: null,
|
|
198
|
+
headingText: null,
|
|
199
|
+
parentText: null,
|
|
200
|
+
pending: null,
|
|
201
|
+
};
|
|
202
|
+
let inFence = false;
|
|
203
|
+
let inComment = false;
|
|
204
|
+
for (let i = 0; i < lines.length; i++) {
|
|
205
|
+
const line = lines[i];
|
|
206
|
+
if (FENCE.test(line)) {
|
|
207
|
+
flush(state);
|
|
208
|
+
inFence = !inFence;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (inFence)
|
|
212
|
+
continue;
|
|
213
|
+
// HTML comments are skipped whole, including ones that span lines
|
|
214
|
+
// (release tooling leaves `<!--\n template notes\n-->` blocks in changelogs).
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
if (inComment) {
|
|
217
|
+
if (trimmed.includes("-->"))
|
|
218
|
+
inComment = false;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (trimmed.startsWith("<!--")) {
|
|
222
|
+
if (!trimmed.slice(4).includes("-->"))
|
|
223
|
+
inComment = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (LINK_DEF.test(line))
|
|
227
|
+
continue;
|
|
228
|
+
const atx = ATX.exec(line);
|
|
229
|
+
if (atx) {
|
|
230
|
+
const heading = parseVersionHeading(atx[2]);
|
|
231
|
+
const level = atx[1].length;
|
|
232
|
+
if (heading)
|
|
233
|
+
openSection(state, heading, level);
|
|
234
|
+
else {
|
|
235
|
+
flush(state);
|
|
236
|
+
// "## Migration guide" after the last release must not be read as part of it.
|
|
237
|
+
// A change-category heading ("## Changed", "## Breaking Changes") at the
|
|
238
|
+
// release's own level is still inside it: date-fns 3.0.0 writes
|
|
239
|
+
// "## v3.0.0" then "## Changed", and its ten BREAKING entries were dropped.
|
|
240
|
+
if (state.current && level <= state.sectionLevel && !isCategoryHeading(atx[2]))
|
|
241
|
+
state.current = null;
|
|
242
|
+
state.headingKind = classifyHeading(atx[2]);
|
|
243
|
+
state.headingText = contextText(atx[2]);
|
|
244
|
+
state.parentKind = null;
|
|
245
|
+
state.parentIndent = null;
|
|
246
|
+
state.parentText = null;
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const next = lines[i + 1];
|
|
251
|
+
if (next !== undefined && SETEXT_RULE.test(next) && line.trim() !== "" && !BULLET.test(line)) {
|
|
252
|
+
const heading = parseVersionHeading(line);
|
|
253
|
+
if (heading) {
|
|
254
|
+
openSection(state, heading, next.trim().startsWith("=") ? 1 : 2);
|
|
255
|
+
i++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// History.md style without an underline: `1.2.3 / 2020-01-01` alone on a line.
|
|
260
|
+
const history = new RegExp(String.raw `^v?(${VERSION})\s+/\s+(.+)$`).exec(line.trim());
|
|
261
|
+
if (history && extractDate(history[2])) {
|
|
262
|
+
openSection(state, { version: history[1], date: extractDate(history[2]) }, 2);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (RULE.test(line)) {
|
|
266
|
+
flush(state);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// A release as a top-level bullet with its changes nested under it
|
|
270
|
+
// (indexmap's RELEASES.md: "- 2.0.0" then " - **MSRV**: Rust 1.64.0 ...").
|
|
271
|
+
const versionBullet = BULLET.exec(line);
|
|
272
|
+
if (versionBullet && versionBullet[1].length === 0 && /^\[?v?\d/.test(versionBullet[2])) {
|
|
273
|
+
const heading = parseVersionHeading(versionBullet[2]);
|
|
274
|
+
if (heading && !/\s\w+\s\w+/.test(versionBullet[2].replace(ISO_DATE, "").replace(MONTH_DATE, ""))) {
|
|
275
|
+
openSection(state, heading, 7);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// A whole-line label, bold or plain ("**Breaking Changes**", "BREAKING CHANGES:",
|
|
280
|
+
// "Deprecations / Removals / API Changes:") is a sub-heading, not an entry.
|
|
281
|
+
// Plain ones were entries until 2026-10-03: 6 of 17 false "breaking" flags
|
|
282
|
+
// on the fresh corpus-4 panel (highlight.js 11, handlebars 4.6, ts-loader 9).
|
|
283
|
+
const bold = BOLD_LABEL.exec(line) ?? PLAIN_LABEL.exec(line);
|
|
284
|
+
if (bold && state.current) {
|
|
285
|
+
flush(state);
|
|
286
|
+
state.headingKind = classifyHeading(bold[1]);
|
|
287
|
+
state.headingText = contextText(bold[1]);
|
|
288
|
+
state.parentKind = null;
|
|
289
|
+
state.parentIndent = null;
|
|
290
|
+
state.parentText = null;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (state.current)
|
|
294
|
+
addLine(state, line);
|
|
295
|
+
}
|
|
296
|
+
flush(state);
|
|
297
|
+
return state.sections;
|
|
298
|
+
}
|
|
299
|
+
/** True when `version` lies in (from, to] by semver precedence; unreadable versions are out. */
|
|
300
|
+
export function inRange(version, from, to) {
|
|
301
|
+
const lower = compareVersionPrecedence(version, from);
|
|
302
|
+
const upper = compareVersionPrecedence(version, to);
|
|
303
|
+
return lower !== null && upper !== null && lower > 0 && upper <= 0;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Sections in (from, to], plus whether the changelog reaches both ends of the
|
|
307
|
+
* range: its newest section is at or past `to`, and its oldest at or before
|
|
308
|
+
* `firstInRange` (the earliest release the upgrade crosses). A changelog that
|
|
309
|
+
* stopped being maintained two majors ago is reported as not covering the
|
|
310
|
+
* range instead of silently presenting a partial history as the whole one.
|
|
311
|
+
*/
|
|
312
|
+
export function selectRange(sections, from, to, firstInRange) {
|
|
313
|
+
const readable = sections.filter((s) => compareVersionPrecedence(s.version, s.version) !== null);
|
|
314
|
+
const selected = readable.filter((s) => inRange(s.version, from, to));
|
|
315
|
+
const reachesTop = readable.some((s) => (compareVersionPrecedence(s.version, to) ?? -1) >= 0);
|
|
316
|
+
const reachesBottom = readable.some((s) => (compareVersionPrecedence(s.version, firstInRange) ?? 1) <= 0);
|
|
317
|
+
return { sections: selected, coversRange: selected.length > 0 && reachesTop && reachesBottom };
|
|
318
|
+
}
|
|
@@ -19,7 +19,9 @@ export class CratesRegistry {
|
|
|
19
19
|
this.rateLimiter = rateLimiter;
|
|
20
20
|
}
|
|
21
21
|
async getPackageInfo(name, currentVersion, isDev) {
|
|
22
|
-
|
|
22
|
+
// Keyed by the installed version too: the cached answer carries currentVersion
|
|
23
|
+
// and its distance, so a name-only key served one project's answer to another.
|
|
24
|
+
const cacheKey = `crates-reg:${name}@${currentVersion ?? ""}:${isDev ? 1 : 0}`;
|
|
23
25
|
const cached = this.cache.get(cacheKey);
|
|
24
26
|
if (cached !== null)
|
|
25
27
|
return cached;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crates.io release reader for dependency_check.
|
|
3
|
+
*
|
|
4
|
+
* Three name-only reads:
|
|
5
|
+
* - `crates.io/api/v1/crates/<name>/versions` (paged, newest semver first):
|
|
6
|
+
* publish time, publisher, `trustpub_data`, yanked.
|
|
7
|
+
* - the sparse index (`index.crates.io`, the CDN file cargo itself reads):
|
|
8
|
+
* each version's dependency list, which the versions endpoint lacks.
|
|
9
|
+
* - `crates.io/api/v1/crates/<name>`: when a newly introduced dependency
|
|
10
|
+
* was first published.
|
|
11
|
+
*
|
|
12
|
+
* crates.io asks API clients for a descriptive User-Agent and at most one
|
|
13
|
+
* request per second; every API request here waits for its slot. Paging
|
|
14
|
+
* stops as soon as the versions the caller asked about (and the release just
|
|
15
|
+
* below them) have been seen, so a check costs one or two pages, not the
|
|
16
|
+
* crate's whole history.
|
|
17
|
+
*
|
|
18
|
+
* Privacy: requests carry the crate NAME only, never the installed version.
|
|
19
|
+
*/
|
|
20
|
+
import type { LiveCache } from "./cache.js";
|
|
21
|
+
import type { RateLimiter } from "./rate-limiter.js";
|
|
22
|
+
import type { PackageReleaseIndex, RegistryLookup, ReleaseMetadataSource } from "./release-metadata.js";
|
|
23
|
+
export declare const CRATES_USER_AGENT = "4DA-MCP-Server/1.0 (https://4da.ai)";
|
|
24
|
+
export declare class CratesVersionsReader implements ReleaseMetadataSource {
|
|
25
|
+
private cache;
|
|
26
|
+
private rateLimiter;
|
|
27
|
+
/** crates.io's crawler policy: one request per second. Tests pass 0. */
|
|
28
|
+
private minIntervalMs;
|
|
29
|
+
/** Earliest time the next crates.io API request may start. Reserved synchronously so concurrent callers queue. */
|
|
30
|
+
private nextSlotMs;
|
|
31
|
+
constructor(cache: LiveCache, rateLimiter: RateLimiter,
|
|
32
|
+
/** crates.io's crawler policy: one request per second. Tests pass 0. */
|
|
33
|
+
minIntervalMs?: number);
|
|
34
|
+
getReleases(name: string, wanted: string[]): Promise<RegistryLookup<PackageReleaseIndex>>;
|
|
35
|
+
getCreatedAt(name: string): Promise<RegistryLookup<string | null>>;
|
|
36
|
+
private apiGet;
|
|
37
|
+
private waitForSlot;
|
|
38
|
+
/** version -> runtime dependency names (normal + build; renamed deps by their real crate name). */
|
|
39
|
+
private sparseDependencies;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True once every wanted version is present AND a release (not a prerelease)
|
|
43
|
+
* below all of them has been seen. Pages arrive in descending semver order,
|
|
44
|
+
* so at that point the release just below the target on its line — the
|
|
45
|
+
* new-dependency trust baseline — has been seen too, if it exists.
|
|
46
|
+
*/
|
|
47
|
+
export declare function pagesSatisfy(seen: string[], wanted: string[]): boolean;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* crates.io release reader for dependency_check.
|
|
4
|
+
*
|
|
5
|
+
* Three name-only reads:
|
|
6
|
+
* - `crates.io/api/v1/crates/<name>/versions` (paged, newest semver first):
|
|
7
|
+
* publish time, publisher, `trustpub_data`, yanked.
|
|
8
|
+
* - the sparse index (`index.crates.io`, the CDN file cargo itself reads):
|
|
9
|
+
* each version's dependency list, which the versions endpoint lacks.
|
|
10
|
+
* - `crates.io/api/v1/crates/<name>`: when a newly introduced dependency
|
|
11
|
+
* was first published.
|
|
12
|
+
*
|
|
13
|
+
* crates.io asks API clients for a descriptive User-Agent and at most one
|
|
14
|
+
* request per second; every API request here waits for its slot. Paging
|
|
15
|
+
* stops as soon as the versions the caller asked about (and the release just
|
|
16
|
+
* below them) have been seen, so a check costs one or two pages, not the
|
|
17
|
+
* crate's whole history.
|
|
18
|
+
*
|
|
19
|
+
* Privacy: requests carry the crate NAME only, never the installed version.
|
|
20
|
+
*/
|
|
21
|
+
import { fetchWithTimeout } from "./http-utils.js";
|
|
22
|
+
import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
|
|
23
|
+
const CRATES_API = "https://crates.io/api/v1/crates";
|
|
24
|
+
export const CRATES_USER_AGENT = "4DA-MCP-Server/1.0 (https://4da.ai)";
|
|
25
|
+
const TIMEOUT_MS = 15_000;
|
|
26
|
+
const MAX_PAGES = 25;
|
|
27
|
+
const VERSIONS_TTL = 3600;
|
|
28
|
+
const CREATED_TTL = 30 * 86_400; // a crate's creation date never changes
|
|
29
|
+
class HttpStatusError extends Error {
|
|
30
|
+
status;
|
|
31
|
+
constructor(status) {
|
|
32
|
+
super(`HTTP ${status}`);
|
|
33
|
+
this.status = status;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class CratesVersionsReader {
|
|
37
|
+
cache;
|
|
38
|
+
rateLimiter;
|
|
39
|
+
minIntervalMs;
|
|
40
|
+
/** Earliest time the next crates.io API request may start. Reserved synchronously so concurrent callers queue. */
|
|
41
|
+
nextSlotMs = 0;
|
|
42
|
+
constructor(cache, rateLimiter,
|
|
43
|
+
/** crates.io's crawler policy: one request per second. Tests pass 0. */
|
|
44
|
+
minIntervalMs = 1000) {
|
|
45
|
+
this.cache = cache;
|
|
46
|
+
this.rateLimiter = rateLimiter;
|
|
47
|
+
this.minIntervalMs = minIntervalMs;
|
|
48
|
+
}
|
|
49
|
+
async getReleases(name, wanted) {
|
|
50
|
+
const key = `crates-versions:${name}`;
|
|
51
|
+
const cached = this.cache.get(key);
|
|
52
|
+
if (cached && (cached.complete || pagesSatisfy(Object.keys(cached.index.releases), wanted))) {
|
|
53
|
+
return { status: "ok", data: cached.index };
|
|
54
|
+
}
|
|
55
|
+
const releases = {};
|
|
56
|
+
let query = "?per_page=100&sort=semver";
|
|
57
|
+
let pages = 0;
|
|
58
|
+
try {
|
|
59
|
+
while (query && pages < MAX_PAGES) {
|
|
60
|
+
const page = await this.apiGet(`${CRATES_API}/${encodeURIComponent(name)}/versions${query}`);
|
|
61
|
+
pages++;
|
|
62
|
+
for (const v of page.versions ?? [])
|
|
63
|
+
releases[v.num] = toRecord(v);
|
|
64
|
+
query = page.meta?.next_page ?? null;
|
|
65
|
+
if (query && pagesSatisfy(Object.keys(releases), wanted))
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof HttpStatusError && err.status === 404) {
|
|
71
|
+
return { status: "not_found", detail: `"${name}" is not on crates.io` };
|
|
72
|
+
}
|
|
73
|
+
return { status: "unreachable", detail: `crates.io: ${errorText(err)}` };
|
|
74
|
+
}
|
|
75
|
+
// Dependency lists come from the sparse index; without them the
|
|
76
|
+
// new-dependency signal reports "unknown" rather than "none".
|
|
77
|
+
const deps = await this.sparseDependencies(name);
|
|
78
|
+
if (deps) {
|
|
79
|
+
for (const [version, names] of Object.entries(deps)) {
|
|
80
|
+
if (releases[version])
|
|
81
|
+
releases[version].dependencies = names;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const index = { ecosystem: "crates.io", name, createdAt: null, releases };
|
|
85
|
+
this.cache.set(key, { index, complete: query === null }, "crates", VERSIONS_TTL);
|
|
86
|
+
return { status: "ok", data: index };
|
|
87
|
+
}
|
|
88
|
+
async getCreatedAt(name) {
|
|
89
|
+
const key = `crates-created:${name}`;
|
|
90
|
+
const cached = this.cache.get(key);
|
|
91
|
+
if (cached)
|
|
92
|
+
return { status: "ok", data: cached.createdAt };
|
|
93
|
+
try {
|
|
94
|
+
const body = await this.apiGet(`${CRATES_API}/${encodeURIComponent(name)}`);
|
|
95
|
+
const createdAt = body.crate?.created_at ?? null;
|
|
96
|
+
this.cache.set(key, { createdAt }, "crates", CREATED_TTL);
|
|
97
|
+
return { status: "ok", data: createdAt };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err instanceof HttpStatusError && err.status === 404) {
|
|
101
|
+
return { status: "not_found", detail: `"${name}" is not on crates.io` };
|
|
102
|
+
}
|
|
103
|
+
return { status: "unreachable", detail: `crates.io: ${errorText(err)}` };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async apiGet(url) {
|
|
107
|
+
if (!this.rateLimiter.consume("crates"))
|
|
108
|
+
throw new Error("crates.io rate limit reached for this minute");
|
|
109
|
+
await this.waitForSlot();
|
|
110
|
+
const response = await fetchWithTimeout(url, { headers: { Accept: "application/json", "User-Agent": CRATES_USER_AGENT } }, TIMEOUT_MS);
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw new HttpStatusError(response.status);
|
|
113
|
+
return (await response.json());
|
|
114
|
+
}
|
|
115
|
+
async waitForSlot() {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const start = Math.max(now, this.nextSlotMs);
|
|
118
|
+
this.nextSlotMs = start + this.minIntervalMs;
|
|
119
|
+
if (start > now)
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, start - now));
|
|
121
|
+
}
|
|
122
|
+
/** version -> runtime dependency names (normal + build; renamed deps by their real crate name). */
|
|
123
|
+
async sparseDependencies(name) {
|
|
124
|
+
const key = `crates-sparse-deps:${name}`;
|
|
125
|
+
const cached = this.cache.get(key);
|
|
126
|
+
if (cached)
|
|
127
|
+
return cached;
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetchWithTimeout(sparseIndexUrl(name), { headers: { "User-Agent": CRATES_USER_AGENT } }, TIMEOUT_MS);
|
|
130
|
+
if (!response.ok)
|
|
131
|
+
return null;
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const line of (await response.text()).split("\n")) {
|
|
134
|
+
if (!line.trim())
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
const entry = JSON.parse(line);
|
|
138
|
+
const names = (entry.deps ?? [])
|
|
139
|
+
.filter((d) => d.kind !== "dev")
|
|
140
|
+
.map((d) => d.package ?? d.name);
|
|
141
|
+
out[entry.vers] = [...new Set(names)];
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// skip a malformed line
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.cache.set(key, out, "crates", VERSIONS_TTL);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function toRecord(v) {
|
|
156
|
+
const tp = v.trustpub_data;
|
|
157
|
+
const who = v.published_by?.login ? ` (published by crates.io user "${v.published_by.login}")` : "";
|
|
158
|
+
return {
|
|
159
|
+
version: v.num,
|
|
160
|
+
publishedAt: v.created_at ?? null,
|
|
161
|
+
trust: tp ? 2 : 0,
|
|
162
|
+
trustEvidence: tp
|
|
163
|
+
? `trusted publisher (${[tp.provider, tp.repository].filter(Boolean).join(" ") || "OIDC"})`
|
|
164
|
+
: `no trusted publisher${who}`,
|
|
165
|
+
installScripts: null,
|
|
166
|
+
deprecated: null,
|
|
167
|
+
withdrawn: v.yanked ? "yanked" : null,
|
|
168
|
+
dependencies: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* True once every wanted version is present AND a release (not a prerelease)
|
|
173
|
+
* below all of them has been seen. Pages arrive in descending semver order,
|
|
174
|
+
* so at that point the release just below the target on its line — the
|
|
175
|
+
* new-dependency trust baseline — has been seen too, if it exists.
|
|
176
|
+
*/
|
|
177
|
+
export function pagesSatisfy(seen, wanted) {
|
|
178
|
+
if (wanted.length === 0)
|
|
179
|
+
return false;
|
|
180
|
+
const have = new Set(seen);
|
|
181
|
+
if (!wanted.every((w) => have.has(w)))
|
|
182
|
+
return false;
|
|
183
|
+
return seen.some((v) => {
|
|
184
|
+
const p = parseSemverPrecedence(v);
|
|
185
|
+
if (!p || p.prerelease.length > 0)
|
|
186
|
+
return false;
|
|
187
|
+
return wanted.every((w) => (compareVersionPrecedence(v, w) ?? 0) < 0);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function sparseIndexUrl(name) {
|
|
191
|
+
const n = name.toLowerCase();
|
|
192
|
+
switch (n.length) {
|
|
193
|
+
case 1: return `https://index.crates.io/1/${n}`;
|
|
194
|
+
case 2: return `https://index.crates.io/2/${n}`;
|
|
195
|
+
case 3: return `https://index.crates.io/3/${n[0]}/${n}`;
|
|
196
|
+
default: return `https://index.crates.io/${n.slice(0, 2)}/${n.slice(2, 4)}/${n}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function errorText(err) {
|
|
200
|
+
return err instanceof Error ? err.message : String(err);
|
|
201
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set operations over resolved dependencies, shared by the live layer.
|
|
3
|
+
* (Moved out of live/index.ts; behaviour unchanged except for the install
|
|
4
|
+
* fields, which did not exist before.)
|
|
5
|
+
*/
|
|
6
|
+
import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
|
|
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 declare function commonPathRoot(dirs: string[]): string | null;
|
|
12
|
+
export declare function dedupeDependencies(deps: ResolvedDependency[]): ResolvedDependency[];
|
|
13
|
+
export declare function emptyVulnResult(projectPath: string, offline: boolean): VulnerabilityScanResult;
|