@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
|
@@ -13,11 +13,11 @@ export const recordFeedbackTool = {
|
|
|
13
13
|
|
|
14
14
|
Feedback actions:
|
|
15
15
|
- "click": User clicked/opened the item
|
|
16
|
-
- "save": User saved/bookmarked the item
|
|
16
|
+
- "save": User saved/bookmarked the item, or said it is relevant (also records a relevance label)
|
|
17
17
|
- "dismiss": User dismissed the item
|
|
18
|
-
- "mark_irrelevant": User
|
|
18
|
+
- "mark_irrelevant": User said the item is not relevant (also records a relevance label)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Only "save" and "mark_irrelevant" are relevance labels; they are what 4DA's accuracy measurement reads. Use them only when the user actually says whether the item matters to them. This does not train content preferences.`,
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: "object",
|
|
23
23
|
properties: {
|
|
@@ -27,8 +27,7 @@ This records what happened; it does not train content preferences (interaction c
|
|
|
27
27
|
},
|
|
28
28
|
source_type: {
|
|
29
29
|
type: "string",
|
|
30
|
-
description:
|
|
31
|
-
enum: ["hackernews", "arxiv", "reddit"],
|
|
30
|
+
description: "Optional. The item's source type (e.g. \"hackernews\", \"crates_io\", \"lobsters\"); if given it must match the item.",
|
|
32
31
|
},
|
|
33
32
|
action: {
|
|
34
33
|
type: "string",
|
|
@@ -36,7 +35,7 @@ This records what happened; it does not train content preferences (interaction c
|
|
|
36
35
|
enum: ["click", "save", "dismiss", "mark_irrelevant"],
|
|
37
36
|
},
|
|
38
37
|
},
|
|
39
|
-
required: ["item_id", "
|
|
38
|
+
required: ["item_id", "action"],
|
|
40
39
|
},
|
|
41
40
|
};
|
|
42
41
|
const validActions = ["click", "save", "dismiss", "mark_irrelevant"];
|
|
@@ -44,10 +43,10 @@ const validActions = ["click", "save", "dismiss", "mark_irrelevant"];
|
|
|
44
43
|
* Execute the record_feedback tool
|
|
45
44
|
*/
|
|
46
45
|
export function executeRecordFeedback(db, params) {
|
|
47
|
-
if (!params.item_id || !params.
|
|
46
|
+
if (!params.item_id || !params.action) {
|
|
48
47
|
return {
|
|
49
48
|
success: false,
|
|
50
|
-
message: "item_id
|
|
49
|
+
message: "item_id and action are required",
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
if (!validActions.includes(params.action)) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal vocabulary for get_actionable_signals: the types, and the mapping
|
|
3
|
+
* from the desktop pipeline's stored priority tiers onto this tool's.
|
|
4
|
+
*
|
|
5
|
+
* This file used to hold a keyword classifier that labelled unstamped feed
|
|
6
|
+
* items. It was removed on 2026-10-02: two keywords such as "cve" and "patch"
|
|
7
|
+
* made an item a security alert, and any language name in the stack
|
|
8
|
+
* ("rust", "javascript") counted as a stack match, so russh and Angular CVEs
|
|
9
|
+
* no project used came out "high — affects your rust stack" and drove the
|
|
10
|
+
* briefing to human_only. The server no longer invents classifications;
|
|
11
|
+
* it reports the pipeline's, gated by the relevance judge.
|
|
12
|
+
*/
|
|
13
|
+
export type SignalType = "security_alert" | "breaking_change" | "tool_discovery" | "tech_trend" | "learning" | "competitive_intel";
|
|
14
|
+
export type SignalPriority = "critical" | "high" | "medium" | "low";
|
|
15
|
+
/**
|
|
16
|
+
* The desktop pipeline stores its own notification tiers in
|
|
17
|
+
* `source_items.signal_priority` — critical, alert, advisory, watch — not this
|
|
18
|
+
* tool's critical/high/medium/low. The reader used to cast the stored string
|
|
19
|
+
* straight through, so 92 of the 93 stamped rows in the live corpus carried a
|
|
20
|
+
* priority no filter, sort, or briefing rule recognised (they sorted last and
|
|
21
|
+
* an "alert"-tier security alert could never reach the human_only rule).
|
|
22
|
+
* Mapped by tier order; an unrecognised value lands in the middle rather than
|
|
23
|
+
* being silently dropped or silently promoted.
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeStoredPriority(stored: string): SignalPriority;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Signal vocabulary for get_actionable_signals: the types, and the mapping
|
|
4
|
+
* from the desktop pipeline's stored priority tiers onto this tool's.
|
|
5
|
+
*
|
|
6
|
+
* This file used to hold a keyword classifier that labelled unstamped feed
|
|
7
|
+
* items. It was removed on 2026-10-02: two keywords such as "cve" and "patch"
|
|
8
|
+
* made an item a security alert, and any language name in the stack
|
|
9
|
+
* ("rust", "javascript") counted as a stack match, so russh and Angular CVEs
|
|
10
|
+
* no project used came out "high — affects your rust stack" and drove the
|
|
11
|
+
* briefing to human_only. The server no longer invents classifications;
|
|
12
|
+
* it reports the pipeline's, gated by the relevance judge.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The desktop pipeline stores its own notification tiers in
|
|
16
|
+
* `source_items.signal_priority` — critical, alert, advisory, watch — not this
|
|
17
|
+
* tool's critical/high/medium/low. The reader used to cast the stored string
|
|
18
|
+
* straight through, so 92 of the 93 stamped rows in the live corpus carried a
|
|
19
|
+
* priority no filter, sort, or briefing rule recognised (they sorted last and
|
|
20
|
+
* an "alert"-tier security alert could never reach the human_only rule).
|
|
21
|
+
* Mapped by tier order; an unrecognised value lands in the middle rather than
|
|
22
|
+
* being silently dropped or silently promoted.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeStoredPriority(stored) {
|
|
25
|
+
switch (stored.trim().toLowerCase()) {
|
|
26
|
+
case "critical":
|
|
27
|
+
return "critical";
|
|
28
|
+
case "alert":
|
|
29
|
+
case "high":
|
|
30
|
+
return "high";
|
|
31
|
+
case "advisory":
|
|
32
|
+
case "medium":
|
|
33
|
+
return "medium";
|
|
34
|
+
case "watch":
|
|
35
|
+
case "low":
|
|
36
|
+
return "low";
|
|
37
|
+
default:
|
|
38
|
+
return "medium";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call-site scan for `upgrade_impact`: which files in THIS project use the
|
|
3
|
+
* package, and which of its symbols they name.
|
|
4
|
+
*
|
|
5
|
+
* The symbols are what make a changelog actionable: "6 breaking changes" is
|
|
6
|
+
* noise until one of them says `InitOptions` and the project imports
|
|
7
|
+
* `InitOptions`. The scan is local-only (nothing leaves the machine) and
|
|
8
|
+
* bounded, because it runs inside a tool call: at most 6000 candidate files,
|
|
9
|
+
* none over 1 MB, and the usual generated / vendored directories skipped.
|
|
10
|
+
* Symlinks are not followed, so a link cycle cannot stall the walk.
|
|
11
|
+
*
|
|
12
|
+
* It is a lexical scan, not a type-checker: an import inside a comment
|
|
13
|
+
* counts, and a re-exported symbol used through a local barrel does not.
|
|
14
|
+
* Both errors are tolerable for prioritising a changelog — the entries are
|
|
15
|
+
* all still listed; this only decides which come first.
|
|
16
|
+
*/
|
|
17
|
+
import type { UpgradeEcosystem } from "../live/upgrade-sources.js";
|
|
18
|
+
export interface CallSiteReport {
|
|
19
|
+
total_files: number;
|
|
20
|
+
files: Array<{
|
|
21
|
+
path: string;
|
|
22
|
+
matches: number;
|
|
23
|
+
}>;
|
|
24
|
+
symbols_used: string[];
|
|
25
|
+
/** Present when the walk stopped at the file cap: counts are then lower bounds. */
|
|
26
|
+
truncated?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Count npm usages of `pkg` (or `pkg/sub`) in one source file and collect imported names. */
|
|
29
|
+
export declare function scanNpmSource(source: string, pkg: string, symbols: Set<string>): number;
|
|
30
|
+
/** Count Rust usages of a crate (`-` read as `_`) in one file and collect the item names it reaches. */
|
|
31
|
+
export declare function scanRustSource(source: string, crateName: string, symbols: Set<string>): number;
|
|
32
|
+
/**
|
|
33
|
+
* Bounded scan of `root` for usages of `pkg`: concurrent reads under a time
|
|
34
|
+
* budget. Measured 2026-10-02 on 4DA's tree (856 TS/JS, 721 Rust files on a
|
|
35
|
+
* nearly full disk): one-at-a-time synchronous reads took 20-26 s cold, so the
|
|
36
|
+
* reads now overlap, a file is regex-scanned only when it contains the name at
|
|
37
|
+
* all, and a scan that runs out of time says its counts are lower bounds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scanCallSites(root: string, pkg: string, ecosystem: UpgradeEcosystem, opts?: {
|
|
40
|
+
timeBudgetMs?: number;
|
|
41
|
+
concurrency?: number;
|
|
42
|
+
}): Promise<CallSiteReport>;
|
|
43
|
+
/**
|
|
44
|
+
* Symbols (≥ 3 chars) that `text` names at a word boundary, case-sensitively.
|
|
45
|
+
* Shorter names (`fs`, `Ok`) would match ordinary prose, so they never flag.
|
|
46
|
+
* A plain lowercase word (`new`, `params`, `default` — Rust paths and
|
|
47
|
+
* CommonJS bindings yield plenty) counts only when the entry marks it as
|
|
48
|
+
* code: in backticks, called `name(`, or reached through `::` / `.`.
|
|
49
|
+
* Measured on the first live run: `TextEmbedding::new` put `new` in the
|
|
50
|
+
* symbol set, which would have flagged every "Add new model" entry.
|
|
51
|
+
*/
|
|
52
|
+
export declare function matchSymbols(text: string, symbols: string[]): string[];
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Call-site scan for `upgrade_impact`: which files in THIS project use the
|
|
4
|
+
* package, and which of its symbols they name.
|
|
5
|
+
*
|
|
6
|
+
* The symbols are what make a changelog actionable: "6 breaking changes" is
|
|
7
|
+
* noise until one of them says `InitOptions` and the project imports
|
|
8
|
+
* `InitOptions`. The scan is local-only (nothing leaves the machine) and
|
|
9
|
+
* bounded, because it runs inside a tool call: at most 6000 candidate files,
|
|
10
|
+
* none over 1 MB, and the usual generated / vendored directories skipped.
|
|
11
|
+
* Symlinks are not followed, so a link cycle cannot stall the walk.
|
|
12
|
+
*
|
|
13
|
+
* It is a lexical scan, not a type-checker: an import inside a comment
|
|
14
|
+
* counts, and a re-exported symbol used through a local barrel does not.
|
|
15
|
+
* Both errors are tolerable for prioritising a changelog — the entries are
|
|
16
|
+
* all still listed; this only decides which come first.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { IgnoreRules } from "../gitignore.js";
|
|
21
|
+
const SKIP_DIRS = new Set([
|
|
22
|
+
"node_modules", "target", ".git", "dist", "build", "out", ".next",
|
|
23
|
+
"coverage", "vendor", ".claude", ".venv",
|
|
24
|
+
]);
|
|
25
|
+
const NPM_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".svelte"]);
|
|
26
|
+
const RUST_EXTS = new Set([".rs"]);
|
|
27
|
+
const MAX_FILES = 6000;
|
|
28
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
29
|
+
const MAX_REPORTED_FILES = 50;
|
|
30
|
+
const MAX_SYMBOLS = 100;
|
|
31
|
+
function escapeRegex(text) {
|
|
32
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
}
|
|
34
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
35
|
+
/** Names from an import clause: `Def`, `* as ns`, `{ a, b as c, type D }`. Imported names, not local aliases. */
|
|
36
|
+
function parseImportClause(clause, into) {
|
|
37
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
38
|
+
if (braces) {
|
|
39
|
+
for (const part of braces[1].split(",")) {
|
|
40
|
+
const name = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
41
|
+
if (IDENT.test(name) && name !== "default")
|
|
42
|
+
into.add(name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const outside = clause.replace(/\{[^}]*\}/g, "").replace(/^\s*type\s+/, "");
|
|
46
|
+
const ns = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(outside);
|
|
47
|
+
if (ns)
|
|
48
|
+
into.add(ns[1]);
|
|
49
|
+
const def = /^\s*([A-Za-z_$][\w$]*)\s*(?:,|$)/.exec(outside);
|
|
50
|
+
if (def)
|
|
51
|
+
into.add(def[1]);
|
|
52
|
+
}
|
|
53
|
+
/** Count npm usages of `pkg` (or `pkg/sub`) in one source file and collect imported names. */
|
|
54
|
+
export function scanNpmSource(source, pkg, symbols) {
|
|
55
|
+
const spec = `${escapeRegex(pkg)}(?:/[^'"\`\\s]*)?`;
|
|
56
|
+
let matches = 0;
|
|
57
|
+
const fromRe = new RegExp(String.raw `\b(?:import|export)\s+([^;'"\`]*?)\s*from\s*['"]${spec}['"]`, "g");
|
|
58
|
+
for (const m of source.matchAll(fromRe)) {
|
|
59
|
+
matches++;
|
|
60
|
+
if (!/^\s*\*\s*$/.test(m[1]))
|
|
61
|
+
parseImportClause(m[1], symbols);
|
|
62
|
+
}
|
|
63
|
+
const bareRe = new RegExp(String.raw `\bimport\s*['"]${spec}['"]`, "g");
|
|
64
|
+
matches += [...source.matchAll(bareRe)].length;
|
|
65
|
+
const dynRe = new RegExp(String.raw `\bimport\s*\(\s*['"]${spec}['"]\s*\)`, "g");
|
|
66
|
+
matches += [...source.matchAll(dynRe)].length;
|
|
67
|
+
const reqRe = new RegExp(String.raw `(?:(?:const|let|var)\s+(\{[^}]*\}|[A-Za-z_$][\w$]*)\s*=\s*)?\brequire\s*\(\s*['"]${spec}['"]\s*\)`, "g");
|
|
68
|
+
for (const m of source.matchAll(reqRe)) {
|
|
69
|
+
matches++;
|
|
70
|
+
const binding = m[1];
|
|
71
|
+
if (!binding)
|
|
72
|
+
continue;
|
|
73
|
+
if (binding.startsWith("{")) {
|
|
74
|
+
for (const part of binding.slice(1, -1).split(",")) {
|
|
75
|
+
const name = part.split(":")[0].trim();
|
|
76
|
+
if (IDENT.test(name))
|
|
77
|
+
symbols.add(name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
symbols.add(binding);
|
|
82
|
+
}
|
|
83
|
+
return matches;
|
|
84
|
+
}
|
|
85
|
+
/** Leaf names of a Rust `use` tree: `{A, b::{C, D as E}, self, *}` -> A, C, D. */
|
|
86
|
+
function useTreeLeaves(tree, into) {
|
|
87
|
+
const flat = tree.replace(/[{}]/g, ",");
|
|
88
|
+
for (const part of flat.split(",")) {
|
|
89
|
+
const pathPart = part.trim().split(/\s+as\s+/)[0].trim();
|
|
90
|
+
if (pathPart.endsWith("::"))
|
|
91
|
+
continue; // `models::{…}` — the group's members are the leaves
|
|
92
|
+
const leaf = pathPart.split("::").filter(Boolean).pop();
|
|
93
|
+
if (leaf && /^[A-Za-z_]\w*$/.test(leaf) && leaf !== "self" && leaf !== "super" && leaf !== "crate") {
|
|
94
|
+
into.add(leaf);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Count Rust usages of a crate (`-` read as `_`) in one file and collect the item names it reaches. */
|
|
99
|
+
export function scanRustSource(source, crateName, symbols) {
|
|
100
|
+
const ident = escapeRegex(crateName.replace(/-/g, "_"));
|
|
101
|
+
let matches = 0;
|
|
102
|
+
const useRe = new RegExp(String.raw `\buse\s+(?:::)?${ident}::([^;]+);`, "g");
|
|
103
|
+
const withoutUses = source.replace(useRe, (_all, tree) => {
|
|
104
|
+
matches++;
|
|
105
|
+
useTreeLeaves(tree, symbols);
|
|
106
|
+
return "";
|
|
107
|
+
});
|
|
108
|
+
const externRe = new RegExp(String.raw `\bextern\s+crate\s+${ident}\b`, "g");
|
|
109
|
+
matches += [...withoutUses.matchAll(externRe)].length;
|
|
110
|
+
// Qualified paths outside `use` lines; `crate::x::` / `self::x::` are local modules, hence the lookbehind.
|
|
111
|
+
const pathRe = new RegExp(String.raw `(?<![\w:])${ident}::((?:[A-Za-z_]\w*::)*[A-Za-z_]\w*)`, "g");
|
|
112
|
+
for (const m of withoutUses.matchAll(pathRe)) {
|
|
113
|
+
matches++;
|
|
114
|
+
for (const seg of m[1].split("::"))
|
|
115
|
+
symbols.add(seg);
|
|
116
|
+
}
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
/** Source files under `root`, skipping installed code, build output, hidden and .gitignored directories. */
|
|
120
|
+
function listSources(root, exts, budget) {
|
|
121
|
+
const files = [];
|
|
122
|
+
const ignore = new IgnoreRules(root);
|
|
123
|
+
const stack = [[root, ""]];
|
|
124
|
+
while (stack.length > 0 && budget.left > 0) {
|
|
125
|
+
const [dir, rel] = stack.pop();
|
|
126
|
+
ignore.load(rel);
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry.isSymbolicLink())
|
|
136
|
+
continue;
|
|
137
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || ignore.ignores(childRel, true))
|
|
140
|
+
continue;
|
|
141
|
+
stack.push([path.join(dir, entry.name), childRel]);
|
|
142
|
+
}
|
|
143
|
+
else if (entry.isFile() && exts.has(path.extname(entry.name).toLowerCase())) {
|
|
144
|
+
if (budget.left-- <= 0)
|
|
145
|
+
break;
|
|
146
|
+
files.push(path.join(dir, entry.name));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Bounded scan of `root` for usages of `pkg`: concurrent reads under a time
|
|
154
|
+
* budget. Measured 2026-10-02 on 4DA's tree (856 TS/JS, 721 Rust files on a
|
|
155
|
+
* nearly full disk): one-at-a-time synchronous reads took 20-26 s cold, so the
|
|
156
|
+
* reads now overlap, a file is regex-scanned only when it contains the name at
|
|
157
|
+
* all, and a scan that runs out of time says its counts are lower bounds.
|
|
158
|
+
*/
|
|
159
|
+
export async function scanCallSites(root, pkg, ecosystem, opts = {}) {
|
|
160
|
+
const exts = ecosystem === "npm" ? NPM_EXTS : RUST_EXTS;
|
|
161
|
+
const budget = { left: MAX_FILES };
|
|
162
|
+
const symbols = new Set();
|
|
163
|
+
const hits = [];
|
|
164
|
+
const needle = ecosystem === "npm" ? pkg : pkg.replace(/-/g, "_");
|
|
165
|
+
const deadline = Date.now() + (opts.timeBudgetMs ?? CALLSITE_TIME_BUDGET_MS);
|
|
166
|
+
const files = listSources(root, exts, budget);
|
|
167
|
+
let next = 0;
|
|
168
|
+
let timedOut = false;
|
|
169
|
+
const worker = async () => {
|
|
170
|
+
while (next < files.length) {
|
|
171
|
+
if (Date.now() > deadline) {
|
|
172
|
+
timedOut = true;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const file = files[next++];
|
|
176
|
+
let source;
|
|
177
|
+
try {
|
|
178
|
+
const stat = await fs.promises.stat(file);
|
|
179
|
+
if (stat.size > MAX_FILE_BYTES)
|
|
180
|
+
continue;
|
|
181
|
+
source = await fs.promises.readFile(file, "utf8");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!source.includes(needle))
|
|
187
|
+
continue;
|
|
188
|
+
const matches = ecosystem === "npm" ? scanNpmSource(source, pkg, symbols) : scanRustSource(source, pkg, symbols);
|
|
189
|
+
if (matches > 0)
|
|
190
|
+
hits.push({ path: path.relative(root, file).split(path.sep).join("/"), matches });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
await Promise.all(Array.from({ length: Math.min(opts.concurrency ?? 32, files.length) }, worker));
|
|
194
|
+
hits.sort((a, b) => b.matches - a.matches || a.path.localeCompare(b.path));
|
|
195
|
+
const report = {
|
|
196
|
+
total_files: hits.length,
|
|
197
|
+
files: hits.slice(0, MAX_REPORTED_FILES),
|
|
198
|
+
symbols_used: [...symbols].sort().slice(0, MAX_SYMBOLS),
|
|
199
|
+
};
|
|
200
|
+
if (budget.left <= 0)
|
|
201
|
+
report.truncated = `stopped after ${MAX_FILES} source files; counts are lower bounds`;
|
|
202
|
+
else if (timedOut)
|
|
203
|
+
report.truncated = `stopped after ${Math.round((opts.timeBudgetMs ?? CALLSITE_TIME_BUDGET_MS) / 1000)} s with ${files.length - next} of ${files.length} files unread; counts are lower bounds`;
|
|
204
|
+
return report;
|
|
205
|
+
}
|
|
206
|
+
const CALLSITE_TIME_BUDGET_MS = 10_000;
|
|
207
|
+
/**
|
|
208
|
+
* Symbols (≥ 3 chars) that `text` names at a word boundary, case-sensitively.
|
|
209
|
+
* Shorter names (`fs`, `Ok`) would match ordinary prose, so they never flag.
|
|
210
|
+
* A plain lowercase word (`new`, `params`, `default` — Rust paths and
|
|
211
|
+
* CommonJS bindings yield plenty) counts only when the entry marks it as
|
|
212
|
+
* code: in backticks, called `name(`, or reached through `::` / `.`.
|
|
213
|
+
* Measured on the first live run: `TextEmbedding::new` put `new` in the
|
|
214
|
+
* symbol set, which would have flagged every "Add new model" entry.
|
|
215
|
+
*/
|
|
216
|
+
export function matchSymbols(text, symbols) {
|
|
217
|
+
return symbols.filter((s) => {
|
|
218
|
+
if (s.length < 3)
|
|
219
|
+
return false;
|
|
220
|
+
const e = escapeRegex(s);
|
|
221
|
+
const pattern = /^[a-z]+$/.test(s)
|
|
222
|
+
? String.raw `\x60${e}(?:\(\))?\x60|(?<![\w$])${e}\(|(?:::|\.)${e}(?![\w$])|(?<![\w$])${e}::`
|
|
223
|
+
: String.raw `(?<![\w$])${e}(?![\w$])`;
|
|
224
|
+
return new RegExp(pattern).test(text);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breaking changes that live in string literals: route and pattern syntax.
|
|
3
|
+
*
|
|
4
|
+
* Symbol matching cannot see these. axum 0.8 changed path parameters from
|
|
5
|
+
* `/:single` and `/*many` to `/{single}` and `/{*many}`; a project's code
|
|
6
|
+
* imports `Router`, and the old syntax sits only in strings like
|
|
7
|
+
* `.route("/users/:id", ...)`. express 5 (path-to-regexp 8) and react-router
|
|
8
|
+
* changed route syntax the same way.
|
|
9
|
+
*
|
|
10
|
+
* So: take the backticked syntax fragments on the "from" side of a breaking
|
|
11
|
+
* entry ("changed from `/:single` ... to `/{single}`"), turn each into a shape
|
|
12
|
+
* (identifiers become placeholders, punctuation stays literal: `/:\w+`), and
|
|
13
|
+
* look for that shape inside string literals of the files that import the
|
|
14
|
+
* package. Only fragments that are mostly syntax qualify; a code path like
|
|
15
|
+
* `Router::route` or `res.json()` is symbol matching's job.
|
|
16
|
+
*/
|
|
17
|
+
import type { ReportSection } from "./upgrade-impact-report.js";
|
|
18
|
+
export interface LiteralHit {
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
literal: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shapes of the syntax an entry says is going away. Only the text before
|
|
25
|
+
* " to " when the entry reads "from X to Y", so the new syntax (which a
|
|
26
|
+
* migrated project already uses) is never reported as needing a change.
|
|
27
|
+
*/
|
|
28
|
+
export declare function oldSyntaxShapes(entryText: string): RegExp[];
|
|
29
|
+
/** String literals of one source file, with their line numbers. */
|
|
30
|
+
export declare function stringLiterals(source: string): Array<{
|
|
31
|
+
text: string;
|
|
32
|
+
line: number;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Flag breaking/deprecation entries whose old syntax appears in a string
|
|
36
|
+
* literal of a file that imports the package. Mutates the entries (adds
|
|
37
|
+
* `touches_your_code` and `matched_literals`) and returns how many breaking
|
|
38
|
+
* entries became touching, plus up to five example literals for the summary.
|
|
39
|
+
*/
|
|
40
|
+
export declare function flagOldSyntax(sections: ReportSection[], files: Array<{
|
|
41
|
+
path: string;
|
|
42
|
+
}>, root: string | null): Promise<{
|
|
43
|
+
newlyTouching: number;
|
|
44
|
+
examples: string[];
|
|
45
|
+
}>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Breaking changes that live in string literals: route and pattern syntax.
|
|
4
|
+
*
|
|
5
|
+
* Symbol matching cannot see these. axum 0.8 changed path parameters from
|
|
6
|
+
* `/:single` and `/*many` to `/{single}` and `/{*many}`; a project's code
|
|
7
|
+
* imports `Router`, and the old syntax sits only in strings like
|
|
8
|
+
* `.route("/users/:id", ...)`. express 5 (path-to-regexp 8) and react-router
|
|
9
|
+
* changed route syntax the same way.
|
|
10
|
+
*
|
|
11
|
+
* So: take the backticked syntax fragments on the "from" side of a breaking
|
|
12
|
+
* entry ("changed from `/:single` ... to `/{single}`"), turn each into a shape
|
|
13
|
+
* (identifiers become placeholders, punctuation stays literal: `/:\w+`), and
|
|
14
|
+
* look for that shape inside string literals of the files that import the
|
|
15
|
+
* package. Only fragments that are mostly syntax qualify; a code path like
|
|
16
|
+
* `Router::route` or `res.json()` is symbol matching's job.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
/** Punctuation that makes a fragment syntax rather than a name. */
|
|
21
|
+
const SYNTAX_CHARS = /[/:*{}?$%@#<>]/g;
|
|
22
|
+
const WORD = /[A-Za-z_][\w-]*/g;
|
|
23
|
+
const MAX_FILES = 300;
|
|
24
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
25
|
+
const MAX_HITS_PER_ENTRY = 5;
|
|
26
|
+
function escapeRegex(text) {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Shapes of the syntax an entry says is going away. Only the text before
|
|
31
|
+
* " to " when the entry reads "from X to Y", so the new syntax (which a
|
|
32
|
+
* migrated project already uses) is never reported as needing a change.
|
|
33
|
+
*/
|
|
34
|
+
export function oldSyntaxShapes(entryText) {
|
|
35
|
+
const fromTo = /\bfrom\b([\s\S]*?)\bto\b/i.exec(entryText);
|
|
36
|
+
const scope = fromTo ? fromTo[1] : entryText;
|
|
37
|
+
const shapes = [];
|
|
38
|
+
for (const [, fragment] of scope.matchAll(/`([^`\s]{2,40})`/g)) {
|
|
39
|
+
// A path or call (`Router::route`, `res.json()`, `a.b`) is a symbol, not syntax.
|
|
40
|
+
if (/^[\w$.:]+(?:\(\))?$/.test(fragment) || fragment.includes("("))
|
|
41
|
+
continue;
|
|
42
|
+
const syntax = fragment.match(SYNTAX_CHARS) ?? [];
|
|
43
|
+
const words = fragment.match(WORD) ?? [];
|
|
44
|
+
if (syntax.length < 2 || words.length === 0)
|
|
45
|
+
continue;
|
|
46
|
+
let pattern = "";
|
|
47
|
+
let last = 0;
|
|
48
|
+
for (const m of fragment.matchAll(WORD)) {
|
|
49
|
+
pattern += escapeRegex(fragment.slice(last, m.index)) + "[A-Za-z_][\\w-]*";
|
|
50
|
+
last = (m.index ?? 0) + m[0].length;
|
|
51
|
+
}
|
|
52
|
+
pattern += escapeRegex(fragment.slice(last));
|
|
53
|
+
shapes.push(new RegExp(pattern));
|
|
54
|
+
}
|
|
55
|
+
return shapes;
|
|
56
|
+
}
|
|
57
|
+
const STRING_LITERAL = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
|
|
58
|
+
/** String literals of one source file, with their line numbers. */
|
|
59
|
+
export function stringLiterals(source) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const m of source.matchAll(STRING_LITERAL)) {
|
|
62
|
+
const start = m.index ?? 0;
|
|
63
|
+
out.push({ text: m[0].slice(1, -1), line: source.slice(0, start).split("\n").length });
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Flag breaking/deprecation entries whose old syntax appears in a string
|
|
69
|
+
* literal of a file that imports the package. Mutates the entries (adds
|
|
70
|
+
* `touches_your_code` and `matched_literals`) and returns how many breaking
|
|
71
|
+
* entries became touching, plus up to five example literals for the summary.
|
|
72
|
+
*/
|
|
73
|
+
export async function flagOldSyntax(sections, files, root) {
|
|
74
|
+
const candidates = sections
|
|
75
|
+
.flatMap((s) => s.entries)
|
|
76
|
+
.filter((e) => e.kind === "breaking" || e.kind === "deprecation")
|
|
77
|
+
.map((entry) => ({ entry, shapes: oldSyntaxShapes(entry.text) }))
|
|
78
|
+
.filter((c) => c.shapes.length > 0);
|
|
79
|
+
if (!root || candidates.length === 0 || files.length === 0)
|
|
80
|
+
return { newlyTouching: 0, examples: [] };
|
|
81
|
+
const literals = [];
|
|
82
|
+
for (const file of files.slice(0, MAX_FILES)) {
|
|
83
|
+
const abs = path.resolve(root, file.path);
|
|
84
|
+
try {
|
|
85
|
+
const stat = await fs.stat(abs);
|
|
86
|
+
if (stat.size > MAX_FILE_BYTES)
|
|
87
|
+
continue;
|
|
88
|
+
for (const lit of stringLiterals(await fs.readFile(abs, "utf8"))) {
|
|
89
|
+
// Routes and patterns have no whitespace; code and prose inside strings do (a test
|
|
90
|
+
// fixture in 4DA's watcher.rs holding Rust source matched `/*many` on the first live run).
|
|
91
|
+
if (lit.text.length <= 200 && !/\s|\\[nrt]/.test(lit.text))
|
|
92
|
+
literals.push({ file: file.path, ...lit });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// A file that vanished since the scan is simply not searched.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let newlyTouching = 0;
|
|
100
|
+
const examples = new Set();
|
|
101
|
+
for (const { entry, shapes } of candidates) {
|
|
102
|
+
const hits = [];
|
|
103
|
+
for (const lit of literals) {
|
|
104
|
+
if (hits.length >= MAX_HITS_PER_ENTRY)
|
|
105
|
+
break;
|
|
106
|
+
if (shapes.some((s) => s.test(lit.text)))
|
|
107
|
+
hits.push({ file: lit.file, line: lit.line, literal: lit.text.slice(0, 80) });
|
|
108
|
+
}
|
|
109
|
+
if (hits.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
if (entry.kind === "breaking" && !entry.touches_your_code)
|
|
112
|
+
newlyTouching++;
|
|
113
|
+
entry.touches_your_code = true;
|
|
114
|
+
entry.matched_literals = hits;
|
|
115
|
+
hits.forEach((h) => examples.size < 5 && examples.add(`"${h.literal}"`));
|
|
116
|
+
}
|
|
117
|
+
return { newlyTouching, examples: [...examples] };
|
|
118
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output shaping for `upgrade_impact`: cross-referencing changelog entries
|
|
3
|
+
* with the project's symbols, concise vs detailed trimming, and the one-line
|
|
4
|
+
* summary an agent reads first.
|
|
5
|
+
*
|
|
6
|
+
* Concise is the default because a multi-major upgrade (vite 6 -> 7 crosses
|
|
7
|
+
* dozens of releases) produces hundreds of "fix typo"-class entries that
|
|
8
|
+
* would bury the five that matter. Concise keeps EVERY breaking, deprecation
|
|
9
|
+
* and security entry; of the plain changes it keeps up to 20 per version that
|
|
10
|
+
* sit under an API or neutral heading ("Changed", "Methods", "Types", or none)
|
|
11
|
+
* and up to 3 under an additive one ("Added", "Fixed", "Docs"), and states how
|
|
12
|
+
* many it left out — omission is always visible, never silent.
|
|
13
|
+
*
|
|
14
|
+
* The neutral allowance is where the classifier's misses live: on the
|
|
15
|
+
* 2026-10-03 panel, 11 of 15 breaking changes it called plain sat under
|
|
16
|
+
* actix-web 4's "Functions" / "Methods" / "Types" headings, and three-per-version
|
|
17
|
+
* hid them from the agent entirely.
|
|
18
|
+
*/
|
|
19
|
+
import { type EntryKind } from "../live/changelog-classify.js";
|
|
20
|
+
import type { ChangelogSection } from "../live/changelog.js";
|
|
21
|
+
export type ResponseFormat = "concise" | "detailed";
|
|
22
|
+
export interface ReportEntry {
|
|
23
|
+
kind: EntryKind;
|
|
24
|
+
text: string;
|
|
25
|
+
/** The changelog heading or parent bullet the entry sits under ("Removed", "Breaking Changes"). */
|
|
26
|
+
under?: string;
|
|
27
|
+
touches_your_code?: boolean;
|
|
28
|
+
matched_symbols?: string[];
|
|
29
|
+
/** String literals in your code that use the syntax this entry retires (route patterns and the like). */
|
|
30
|
+
matched_literals?: Array<{
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
literal: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export interface ReportSection {
|
|
37
|
+
version: string;
|
|
38
|
+
date: string | null;
|
|
39
|
+
entries: ReportEntry[];
|
|
40
|
+
/** Concise mode only: how many plain `change` entries were left out. */
|
|
41
|
+
omitted_changes?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ShapedChangelog {
|
|
44
|
+
sections: ReportSection[];
|
|
45
|
+
breaking: number;
|
|
46
|
+
deprecations: number;
|
|
47
|
+
security: number;
|
|
48
|
+
/** Breaking entries naming a symbol this project uses (deprecations are flagged per entry but not counted here). */
|
|
49
|
+
touching: number;
|
|
50
|
+
touchingSymbols: string[];
|
|
51
|
+
truncated?: string;
|
|
52
|
+
/** Entries left out because an earlier (newer) section already listed the same change. */
|
|
53
|
+
duplicates?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Flag, order and trim changelog sections. Counts always cover every entry, trimmed or not. */
|
|
56
|
+
export declare function shapeChangelog(sections: ChangelogSection[], symbols: string[], format: ResponseFormat): ShapedChangelog;
|
|
57
|
+
export type UpgradeType = "patch" | "minor" | "major" | "prerelease" | "unknown";
|
|
58
|
+
/**
|
|
59
|
+
* Upgrade class by semver. A 0.x minor bump is reported as "major": under
|
|
60
|
+
* caret rules (npm and Cargo alike) 0.32 -> 0.37 is as breaking as 1 -> 2.
|
|
61
|
+
*/
|
|
62
|
+
export declare function upgradeType(from: string, to: string): UpgradeType;
|
|
63
|
+
/** Major versions crossed (0.x minors count as majors, matching `upgradeType`). */
|
|
64
|
+
export declare function majorsCrossed(from: string, to: string): number;
|
|
65
|
+
/** Up to `count` published versions nearest to `wanted` by precedence, for "version not found" errors. */
|
|
66
|
+
export declare function nearestVersions(published: string[], requested: string, count?: number): string[];
|
|
67
|
+
export interface SummaryInput {
|
|
68
|
+
pkg: string;
|
|
69
|
+
from: string;
|
|
70
|
+
to: string;
|
|
71
|
+
releases: number;
|
|
72
|
+
breaking: number;
|
|
73
|
+
touching: number;
|
|
74
|
+
touchingSymbols: string[];
|
|
75
|
+
/** missing = no changelog file; no_entries = a file with no section for these releases; partial = some of them. */
|
|
76
|
+
changelog: "missing" | "no_entries" | "partial" | "complete";
|
|
77
|
+
advisoriesFixed: number | null;
|
|
78
|
+
}
|
|
79
|
+
/** The one sentence a human or agent reads first. */
|
|
80
|
+
export declare function summarize(input: SummaryInput): string;
|