@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,197 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Which of this project's dependencies a task touches, and what is known
|
|
4
|
+
* about each — the core of the what_should_i_know briefing.
|
|
5
|
+
*
|
|
6
|
+
* Measured 2026-10-01: asked about "bump fastembed 5 -> 7", the briefing
|
|
7
|
+
* returned eleven advisories and none about fastembed. It never asked which
|
|
8
|
+
* packages the task names; it merged feed items by recency and keyword class,
|
|
9
|
+
* so a 7-day-old `fastembed v7.1.0` release row was outside its 72-hour window
|
|
10
|
+
* while an arXiv dataset paper and three OpenAI-company headlines were in.
|
|
11
|
+
* This module starts from the task instead: the dependencies it names (exact
|
|
12
|
+
* package names from the resolved dependency set), then facts about exactly
|
|
13
|
+
* those packages.
|
|
14
|
+
*/
|
|
15
|
+
import { isActionableVulnerability } from "../live/maintenance.js";
|
|
16
|
+
import { presentedSeverity } from "../live/severity-scope.js";
|
|
17
|
+
import { compareVersions } from "../live/version-compare.js";
|
|
18
|
+
/**
|
|
19
|
+
* Package names that are also everyday words. One of these counts as named
|
|
20
|
+
* only with a qualifier beside it — "the next crate", "`next`", "upgrade
|
|
21
|
+
* next to 15" — so "the next step" does not drag the `next` package in.
|
|
22
|
+
*/
|
|
23
|
+
export const COMMON_WORDS = new Set([
|
|
24
|
+
"next", "open", "debug", "ms", "ai", "test", "core", "util", "utils", "path", "fs", "os", "url", "http",
|
|
25
|
+
"log", "time", "rand", "bytes", "once", "glob", "which", "yes", "color", "colors", "async", "events",
|
|
26
|
+
"image", "regex", "net", "io", "fetch", "form", "data", "config", "cache", "queue", "stream",
|
|
27
|
+
"buffer", "assert", "base", "env", "error", "errors", "string", "table", "tree",
|
|
28
|
+
"safe", "lock", "slash", "sort", "type", "types", "web",
|
|
29
|
+
]);
|
|
30
|
+
const QUALIFIERS = /^(?:crate|crates|package|packages|pkg|dependency|dependencies|dep|deps|library|lib|module|version|upgrade|upgrading|bump|bumping|update|updating|migrate|migrating|install|v?\d[\w.*-]*)$/i;
|
|
31
|
+
const VERSION_TOKEN = /^v?\d+(?:\.(?:\d+|x|\*))*(?:\.\d+)?(?:-[\w.]+)?$/i;
|
|
32
|
+
/** Lowercased word-ish tokens, keeping package punctuation (@scope/name, a-b_c.d). */
|
|
33
|
+
function tokenize(text) {
|
|
34
|
+
return (text.match(/[@A-Za-z0-9][\w./@*-]*/g) ?? []).map((t) => t.replace(/[.,;:)]+$/, "").toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
/** Spellings a token may name a dependency by. */
|
|
37
|
+
function nameKeys(dep) {
|
|
38
|
+
const name = dep.name.toLowerCase();
|
|
39
|
+
const keys = new Set([name]);
|
|
40
|
+
if (dep.ecosystem === "crates.io") {
|
|
41
|
+
keys.add(name.replace(/-/g, "_"));
|
|
42
|
+
keys.add(name.replace(/_/g, "-"));
|
|
43
|
+
}
|
|
44
|
+
if (dep.ecosystem === "PyPI")
|
|
45
|
+
keys.add(name.replace(/[-_.]+/g, "-"));
|
|
46
|
+
if (dep.ecosystem === "Go") {
|
|
47
|
+
const last = name.split("/").pop() ?? name;
|
|
48
|
+
if (last.length >= 3 && !/^v\d+$/.test(last))
|
|
49
|
+
keys.add(last);
|
|
50
|
+
}
|
|
51
|
+
return [...keys];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The dependencies a task (and its file list) names. Exact names only; a
|
|
55
|
+
* common-word name needs a qualifier within two tokens or code quoting.
|
|
56
|
+
* Direct dependencies first; at most `limit` packages.
|
|
57
|
+
*/
|
|
58
|
+
export function detectTaskPackages(task, files, deps, limit = 5) {
|
|
59
|
+
const quoted = new Set((task.match(/`([^`]+)`/g) ?? []).map((q) => q.slice(1, -1).toLowerCase()));
|
|
60
|
+
const tokens = tokenize(`${task} ${files.join(" ")}`);
|
|
61
|
+
const byKey = new Map();
|
|
62
|
+
for (const dep of deps) {
|
|
63
|
+
if (!dep.name)
|
|
64
|
+
continue;
|
|
65
|
+
for (const key of nameKeys(dep))
|
|
66
|
+
byKey.set(key, [...(byKey.get(key) ?? []), dep]);
|
|
67
|
+
}
|
|
68
|
+
const found = new Map();
|
|
69
|
+
tokens.forEach((token, i) => {
|
|
70
|
+
const matches = byKey.get(token);
|
|
71
|
+
if (!matches)
|
|
72
|
+
return;
|
|
73
|
+
if (COMMON_WORDS.has(token) && !quoted.has(token)) {
|
|
74
|
+
const window = [...tokens.slice(Math.max(0, i - 2), i), ...tokens.slice(i + 1, i + 3)];
|
|
75
|
+
if (!window.some((w) => QUALIFIERS.test(w)))
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const key = `${matches[0].ecosystem}\0${matches[0].name}`;
|
|
79
|
+
if (!found.has(key))
|
|
80
|
+
found.set(key, { deps: matches, at: i });
|
|
81
|
+
});
|
|
82
|
+
const packages = [...found.values()].map(({ deps: copies, at }) => {
|
|
83
|
+
const versions = tokens.slice(at + 1, at + 8).filter((t) => VERSION_TOKEN.test(t));
|
|
84
|
+
const first = copies[0];
|
|
85
|
+
return {
|
|
86
|
+
name: first.name,
|
|
87
|
+
ecosystem: first.ecosystem,
|
|
88
|
+
installed: [...new Set(copies.map((d) => d.version).filter((v) => Boolean(v)))],
|
|
89
|
+
direct: copies.some((d) => d.isDirect),
|
|
90
|
+
dev: copies.every((d) => d.isDev),
|
|
91
|
+
requested_from: versions.length >= 2 ? versions[0] : null,
|
|
92
|
+
requested_to: versions.length >= 1 ? versions[versions.length - 1] : null,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
packages.sort((a, b) => Number(b.direct) - Number(a.direct));
|
|
96
|
+
return packages.slice(0, limit);
|
|
97
|
+
}
|
|
98
|
+
/** Major version of a version or a `7.x` / `v7` request; null when unreadable. */
|
|
99
|
+
export function majorOf(version) {
|
|
100
|
+
const m = version ? /^v?(\d+)/.exec(version) : null;
|
|
101
|
+
return m ? Number(m[1]) : null;
|
|
102
|
+
}
|
|
103
|
+
/** Actionable advisories on the installed copies of one package, highest presented severity first. */
|
|
104
|
+
export function advisoriesFor(pkg, vulns) {
|
|
105
|
+
const rank = { critical: 4, high: 3, medium: 2, low: 1, unknown: 0 };
|
|
106
|
+
return vulns
|
|
107
|
+
.filter((v) => v.package === pkg.name && v.ecosystem === pkg.ecosystem && isActionableVulnerability(v))
|
|
108
|
+
.map((v) => ({
|
|
109
|
+
id: v.vulnId,
|
|
110
|
+
installed: v.currentVersion,
|
|
111
|
+
severity: presentedSeverity(v),
|
|
112
|
+
fixed_version: v.fixedVersion,
|
|
113
|
+
summary: v.summary.length > 160 ? `${v.summary.slice(0, 157)}...` : v.summary,
|
|
114
|
+
}))
|
|
115
|
+
.sort((a, b) => rank[b.severity] - rank[a.severity]);
|
|
116
|
+
}
|
|
117
|
+
const RELEASE_SOURCES = {
|
|
118
|
+
"crates.io": { source: "crates_io", prefix: "crates.io: " },
|
|
119
|
+
npm: { source: "npm_registry", prefix: "npm: " },
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Registry release rows the desktop app's feed holds for one package, newer
|
|
123
|
+
* than the installed version. Matched on the registry's own title format
|
|
124
|
+
* (`crates.io: fastembed v7.1.0`), never on a free-text mention. Empty in
|
|
125
|
+
* standalone mode (no feed) or for ecosystems the feed does not watch.
|
|
126
|
+
*/
|
|
127
|
+
export function releasesFor(db, pkg) {
|
|
128
|
+
const spec = RELEASE_SOURCES[pkg.ecosystem];
|
|
129
|
+
if (!spec)
|
|
130
|
+
return [];
|
|
131
|
+
let rows;
|
|
132
|
+
try {
|
|
133
|
+
rows = db
|
|
134
|
+
.getRawDb()
|
|
135
|
+
.prepare(`SELECT title, url, created_at FROM source_items
|
|
136
|
+
WHERE source_type = ? AND title LIKE ? ORDER BY created_at DESC LIMIT 50`)
|
|
137
|
+
.all(spec.source, `${spec.prefix}${pkg.name} v%`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const newest = pkg.installed.reduce((max, v) => (max === null || compareVersions(v, max, pkg.ecosystem) > 0 ? v : max), null);
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const version = row.title.slice(`${spec.prefix}${pkg.name} v`.length).trim();
|
|
147
|
+
if (!version || seen.has(version) || (row.title !== `${spec.prefix}${pkg.name} v${version}`))
|
|
148
|
+
continue;
|
|
149
|
+
if (newest && compareVersions(version, newest, pkg.ecosystem) <= 0)
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(version);
|
|
152
|
+
out.push({ version, seen: row.created_at, url: row.url });
|
|
153
|
+
}
|
|
154
|
+
return out.sort((a, b) => compareVersions(b.version, a.version, pkg.ecosystem)).slice(0, 5);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Feed items about the task's packages that the app's relevance judge
|
|
158
|
+
* accepted (latest `llm_judgments` verdict >= 0.5) and that the feed kept
|
|
159
|
+
* (`feed_relevant` = 1). Exact package-name match on a word boundary in the
|
|
160
|
+
* title. Third-party text: reported as data. Empty without the desktop feed.
|
|
161
|
+
*/
|
|
162
|
+
export function judgedReadingFor(db, packages, limit = 5) {
|
|
163
|
+
if (packages.length === 0)
|
|
164
|
+
return [];
|
|
165
|
+
const raw = db.getRawDb();
|
|
166
|
+
let rows;
|
|
167
|
+
try {
|
|
168
|
+
rows = raw
|
|
169
|
+
.prepare(`WITH latest AS (
|
|
170
|
+
SELECT source_item_id, relevance_score,
|
|
171
|
+
ROW_NUMBER() OVER (PARTITION BY source_item_id ORDER BY id DESC) AS rn
|
|
172
|
+
FROM llm_judgments)
|
|
173
|
+
SELECT s.id, s.title, s.url, s.source_type, l.relevance_score AS verdict, s.created_at
|
|
174
|
+
FROM source_items s JOIN latest l ON l.source_item_id = s.id AND l.rn = 1
|
|
175
|
+
WHERE s.feed_relevant = 1 AND l.relevance_score >= 0.5
|
|
176
|
+
AND s.created_at > datetime('now', '-30 days')
|
|
177
|
+
ORDER BY l.relevance_score DESC, s.created_at DESC LIMIT 2000`)
|
|
178
|
+
.all();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const patterns = packages.map((p) => ({
|
|
184
|
+
name: p.name,
|
|
185
|
+
re: new RegExp(`(^|[^\\w@/-])${p.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "i"),
|
|
186
|
+
}));
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const hit = patterns.find((p) => p.re.test(row.title));
|
|
190
|
+
if (!hit)
|
|
191
|
+
continue;
|
|
192
|
+
out.push({ id: row.id, title: row.title, url: row.url, source: row.source_type, judge_relevance: Math.round(row.verdict * 100) / 100, package: hit.name });
|
|
193
|
+
if (out.length >= limit)
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wisdom retrieval for the what_should_i_know briefing.
|
|
3
|
+
*
|
|
4
|
+
* Ranks the developer's recorded decisions and agent memories against the
|
|
5
|
+
* task — lexically by default, blended with embedding similarity when a
|
|
6
|
+
* provider is configured. Split out of what-should-i-know.ts, which is the
|
|
7
|
+
* briefing's assembly and delegation logic.
|
|
8
|
+
*/
|
|
9
|
+
import type { FourDADatabase } from "../db.js";
|
|
10
|
+
import { type EmbeddingConfig } from "../embeddings.js";
|
|
11
|
+
export interface WisdomEntry {
|
|
12
|
+
type: string;
|
|
13
|
+
subject: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
}
|
|
16
|
+
export type WisdomRecallMode = "hybrid" | "ranked_lexical";
|
|
17
|
+
/**
|
|
18
|
+
* Lexical wisdom retrieval (the default, provider-free path): rank decisions and
|
|
19
|
+
* memories independently, merge by score, take the top entries.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getRelevantWisdom(db: FourDADatabase, task: string, files: string[]): WisdomEntry[];
|
|
22
|
+
/**
|
|
23
|
+
* Hybrid wisdom retrieval: blends alias-aware lexical scoring with embedding
|
|
24
|
+
* cosine similarity (per table, since each is normalized independently), so a
|
|
25
|
+
* paraphrased prior decision or memory surfaces in the briefing even with no
|
|
26
|
+
* shared words. Falls back to pure lexical (and reports it) when nothing embeds.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getRelevantWisdomHybrid(db: FourDADatabase, task: string, files: string[], config: EmbeddingConfig): Promise<{
|
|
29
|
+
wisdom: WisdomEntry[];
|
|
30
|
+
recall_mode: WisdomRecallMode;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Wisdom retrieval for the what_should_i_know briefing.
|
|
4
|
+
*
|
|
5
|
+
* Ranks the developer's recorded decisions and agent memories against the
|
|
6
|
+
* task — lexically by default, blended with embedding similarity when a
|
|
7
|
+
* provider is configured. Split out of what-should-i-know.ts, which is the
|
|
8
|
+
* briefing's assembly and delegation logic.
|
|
9
|
+
*/
|
|
10
|
+
import { rankRowsByRecall } from "./recall.js";
|
|
11
|
+
import { semanticScores } from "../embeddings.js";
|
|
12
|
+
import { decisionEmbedText } from "./decision-recall.js";
|
|
13
|
+
import { memoryEmbedText } from "./agent-memory.js";
|
|
14
|
+
/** Weighted fields for ranking decisions (mirrors the decision tools' weights). */
|
|
15
|
+
const WISDOM_DECISION_FIELDS = [
|
|
16
|
+
{ name: "alternatives", weight: 5, value: (row) => row.alternatives_rejected },
|
|
17
|
+
{ name: "subject", weight: 4, value: (row) => row.subject },
|
|
18
|
+
{ name: "tags", weight: 3, value: (row) => row.context_tags },
|
|
19
|
+
{ name: "decision", weight: 2, value: (row) => row.decision },
|
|
20
|
+
{ name: "rationale", weight: 1, value: (row) => row.rationale },
|
|
21
|
+
];
|
|
22
|
+
/** Weighted fields for ranking memories. */
|
|
23
|
+
const WISDOM_MEMORY_FIELDS = [
|
|
24
|
+
{ name: "subject", weight: 4, value: (row) => row.subject },
|
|
25
|
+
{ name: "tags", weight: 3, value: (row) => row.context_tags },
|
|
26
|
+
{ name: "content", weight: 2, value: (row) => row.content },
|
|
27
|
+
{ name: "type", weight: 1, value: (row) => row.memory_type },
|
|
28
|
+
];
|
|
29
|
+
const WISDOM_LIMIT = 6;
|
|
30
|
+
/** Blend weight for semantic cosine vs normalized lexical score in hybrid wisdom. */
|
|
31
|
+
const WISDOM_BLEND = 0.65;
|
|
32
|
+
function loadWisdomDecisions(rawDb) {
|
|
33
|
+
try {
|
|
34
|
+
return rawDb
|
|
35
|
+
.prepare(`SELECT id, subject, decision, rationale, alternatives_rejected, context_tags, updated_at
|
|
36
|
+
FROM developer_decisions
|
|
37
|
+
WHERE status = 'active'
|
|
38
|
+
ORDER BY updated_at DESC
|
|
39
|
+
LIMIT 300`)
|
|
40
|
+
.all();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return []; // Older DBs may not have decision memory yet.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function loadWisdomMemories(rawDb) {
|
|
47
|
+
try {
|
|
48
|
+
return rawDb
|
|
49
|
+
.prepare(`SELECT id, memory_type, subject, content, context_tags, created_at
|
|
50
|
+
FROM agent_memory
|
|
51
|
+
WHERE (expires_at IS NULL OR expires_at > datetime('now'))
|
|
52
|
+
ORDER BY created_at DESC
|
|
53
|
+
LIMIT 300`)
|
|
54
|
+
.all();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return []; // Older DBs may not have agent memory yet.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Map a ranked decision/memory row to the public WisdomEntry shape. */
|
|
61
|
+
function toWisdomEntry(item) {
|
|
62
|
+
if (item.type === "decision") {
|
|
63
|
+
const row = item.row;
|
|
64
|
+
return {
|
|
65
|
+
type: "decision",
|
|
66
|
+
subject: row.subject,
|
|
67
|
+
detail: row.rationale ? `${row.decision} Rationale: ${row.rationale}` : row.decision,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const row = item.row;
|
|
71
|
+
return {
|
|
72
|
+
type: `memory:${row.memory_type}`,
|
|
73
|
+
subject: row.subject,
|
|
74
|
+
detail: row.content,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Lexical wisdom retrieval (the default, provider-free path): rank decisions and
|
|
79
|
+
* memories independently, merge by score, take the top entries.
|
|
80
|
+
*/
|
|
81
|
+
export function getRelevantWisdom(db, task, files) {
|
|
82
|
+
const rawDb = db.getRawDb();
|
|
83
|
+
const query = [task, ...files].join(" ");
|
|
84
|
+
const entries = [
|
|
85
|
+
...rankRowsByRecall(loadWisdomDecisions(rawDb), query, WISDOM_DECISION_FIELDS, WISDOM_LIMIT).map((item) => ({ ...item, type: "decision" })),
|
|
86
|
+
...rankRowsByRecall(loadWisdomMemories(rawDb), query, WISDOM_MEMORY_FIELDS, WISDOM_LIMIT).map((item) => ({ ...item, type: "memory" })),
|
|
87
|
+
];
|
|
88
|
+
return entries
|
|
89
|
+
.sort((a, b) => b.score - a.score)
|
|
90
|
+
.slice(0, WISDOM_LIMIT)
|
|
91
|
+
.map(toWisdomEntry);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Hybrid wisdom retrieval: blends alias-aware lexical scoring with embedding
|
|
95
|
+
* cosine similarity (per table, since each is normalized independently), so a
|
|
96
|
+
* paraphrased prior decision or memory surfaces in the briefing even with no
|
|
97
|
+
* shared words. Falls back to pure lexical (and reports it) when nothing embeds.
|
|
98
|
+
*/
|
|
99
|
+
export async function getRelevantWisdomHybrid(db, task, files, config) {
|
|
100
|
+
const rawDb = db.getRawDb();
|
|
101
|
+
const query = [task, ...files].join(" ");
|
|
102
|
+
const decisions = loadWisdomDecisions(rawDb);
|
|
103
|
+
const memories = loadWisdomMemories(rawDb);
|
|
104
|
+
// Lexical baselines over ALL rows, per table, for normalization + fallback.
|
|
105
|
+
const decLex = rankRowsByRecall(decisions, query, WISDOM_DECISION_FIELDS, decisions.length);
|
|
106
|
+
const memLex = rankRowsByRecall(memories, query, WISDOM_MEMORY_FIELDS, memories.length);
|
|
107
|
+
const decLexById = new Map(decLex.map((i) => [i.row.id, i.score]));
|
|
108
|
+
const memLexById = new Map(memLex.map((i) => [i.row.id, i.score]));
|
|
109
|
+
const decMax = decLex.length ? decLex[0].score : 0;
|
|
110
|
+
const memMax = memLex.length ? memLex[0].score : 0;
|
|
111
|
+
const decSem = decisions.length
|
|
112
|
+
? await semanticScores(db, "developer_decisions", query, decisions.map((d) => ({ id: d.id, text: decisionEmbedText(d) })), config)
|
|
113
|
+
: null;
|
|
114
|
+
const memSem = memories.length
|
|
115
|
+
? await semanticScores(db, "agent_memory", query, memories.map((m) => ({ id: m.id, text: memoryEmbedText(m) })), config)
|
|
116
|
+
: null;
|
|
117
|
+
const anySemantic = (decSem?.embeddedCount ?? 0) > 0 || (memSem?.embeddedCount ?? 0) > 0;
|
|
118
|
+
if (!anySemantic) {
|
|
119
|
+
// Provider unreachable / nothing embedded -> behave exactly like lexical.
|
|
120
|
+
const entries = [
|
|
121
|
+
...decLex.slice(0, WISDOM_LIMIT).map((i) => ({ ...i, type: "decision" })),
|
|
122
|
+
...memLex.slice(0, WISDOM_LIMIT).map((i) => ({ ...i, type: "memory" })),
|
|
123
|
+
];
|
|
124
|
+
return {
|
|
125
|
+
wisdom: entries.sort((a, b) => b.score - a.score).slice(0, WISDOM_LIMIT).map(toWisdomEntry),
|
|
126
|
+
recall_mode: "ranked_lexical",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const scored = [];
|
|
130
|
+
for (const d of decisions) {
|
|
131
|
+
const semantic = Math.max(0, decSem?.semanticById.get(d.id) ?? 0);
|
|
132
|
+
const lexNorm = decMax > 0 ? (decLexById.get(d.id) ?? 0) / decMax : 0;
|
|
133
|
+
const score = WISDOM_BLEND * semantic + (1 - WISDOM_BLEND) * lexNorm;
|
|
134
|
+
if (score > 0)
|
|
135
|
+
scored.push({ type: "decision", row: d, score });
|
|
136
|
+
}
|
|
137
|
+
for (const m of memories) {
|
|
138
|
+
const semantic = Math.max(0, memSem?.semanticById.get(m.id) ?? 0);
|
|
139
|
+
const lexNorm = memMax > 0 ? (memLexById.get(m.id) ?? 0) / memMax : 0;
|
|
140
|
+
const score = WISDOM_BLEND * semantic + (1 - WISDOM_BLEND) * lexNorm;
|
|
141
|
+
if (score > 0)
|
|
142
|
+
scored.push({ type: "memory", row: m, score });
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
wisdom: scored.sort((a, b) => b.score - a.score).slice(0, WISDOM_LIMIT).map(toWisdomEntry),
|
|
146
|
+
recall_mode: "hybrid",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -16,7 +16,6 @@ import type { FourDADatabase } from "../db.js";
|
|
|
16
16
|
export interface CheckDecisionAlignmentParams {
|
|
17
17
|
technology: string;
|
|
18
18
|
pattern?: string;
|
|
19
|
-
context?: string;
|
|
20
19
|
}
|
|
21
20
|
interface RelevantDecision {
|
|
22
21
|
id: number;
|
|
@@ -62,10 +61,6 @@ export declare const checkDecisionAlignmentTool: {
|
|
|
62
61
|
type: string;
|
|
63
62
|
description: string;
|
|
64
63
|
};
|
|
65
|
-
context: {
|
|
66
|
-
type: string;
|
|
67
|
-
description: string;
|
|
68
|
-
};
|
|
69
64
|
};
|
|
70
65
|
required: string[];
|
|
71
66
|
};
|
|
@@ -33,10 +33,6 @@ export const checkDecisionAlignmentTool = {
|
|
|
33
33
|
type: "string",
|
|
34
34
|
description: "Architecture pattern to check (e.g., 'microservices', 'event-driven')",
|
|
35
35
|
},
|
|
36
|
-
context: {
|
|
37
|
-
type: "string",
|
|
38
|
-
description: "Additional context about the proposed change",
|
|
39
|
-
},
|
|
40
36
|
},
|
|
41
37
|
required: ["technology"],
|
|
42
38
|
},
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* Manage developer decisions - record, list, check alignment, update, and supersede.
|
|
6
6
|
* Decisions persist and inform signal classification, tech radar, and agent context.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { getEmbeddingConfig } from "../embeddings.js";
|
|
10
|
-
import { rankDecisionsLexical, hybridDecisionRecall, POSSIBLE_CONFLICT_THRESHOLD, } from "./decision-recall.js";
|
|
8
|
+
import { executeCheckDecisionAlignment } from "./decision-enforcement.js";
|
|
11
9
|
// ============================================================================
|
|
12
10
|
// Tool Definition
|
|
13
11
|
// ============================================================================
|
|
@@ -73,11 +71,11 @@ export const decisionMemoryTool = {
|
|
|
73
71
|
},
|
|
74
72
|
technology: {
|
|
75
73
|
type: "string",
|
|
76
|
-
description: "Technology to check alignment for",
|
|
74
|
+
description: "Technology to check alignment for (check_alignment)",
|
|
77
75
|
},
|
|
78
76
|
pattern: {
|
|
79
77
|
type: "string",
|
|
80
|
-
description: "
|
|
78
|
+
description: "Architecture pattern to check alongside the technology (check_alignment)",
|
|
81
79
|
},
|
|
82
80
|
id: {
|
|
83
81
|
type: "number",
|
|
@@ -122,47 +120,6 @@ function parseDecisionRow(row) {
|
|
|
122
120
|
context_tags: JSON.parse(row.context_tags || "[]"),
|
|
123
121
|
};
|
|
124
122
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Build the check_alignment response from retrieved decisions. Hard conflicts are
|
|
127
|
-
* lexical/alias-aware (grounded); semantically-close decisions that rejected
|
|
128
|
-
* alternatives are surfaced as advisory `possible_conflicts`.
|
|
129
|
-
*/
|
|
130
|
-
function buildAlignmentResponse(rows, semanticById, recallMode, technology) {
|
|
131
|
-
const conflicts = [];
|
|
132
|
-
const possibleConflicts = [];
|
|
133
|
-
const relevant = [];
|
|
134
|
-
for (const row of rows) {
|
|
135
|
-
const alts = JSON.parse(row.alternatives_rejected || "[]");
|
|
136
|
-
const isRejected = alts.some((alt) => matchesRecallQuery(alt, technology));
|
|
137
|
-
if (isRejected) {
|
|
138
|
-
conflicts.push({
|
|
139
|
-
decision_id: row.id,
|
|
140
|
-
subject: row.subject,
|
|
141
|
-
reason: `'${technology}' was rejected in favor of '${row.decision}' (rationale: ${row.rationale || "none"})`,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
const sim = semanticById.get(row.id) ?? 0;
|
|
146
|
-
if (sim >= POSSIBLE_CONFLICT_THRESHOLD && alts.length > 0) {
|
|
147
|
-
possibleConflicts.push({
|
|
148
|
-
decision_id: row.id,
|
|
149
|
-
subject: row.subject,
|
|
150
|
-
similarity: Math.round(sim * 1000) / 1000,
|
|
151
|
-
reason: `'${technology}' is semantically close (${Math.round(sim * 100)}%) to a decision that rejected: ${alts.join(", ")}. Review before proceeding.`,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
relevant.push(parseDecisionRow(row));
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
aligned: conflicts.length === 0,
|
|
159
|
-
relevant_decisions: relevant,
|
|
160
|
-
conflicts,
|
|
161
|
-
possible_conflicts: possibleConflicts,
|
|
162
|
-
confidence: relevant.length > 0 ? Math.max(...relevant.map((r) => r.confidence)) : 0.5,
|
|
163
|
-
recall_mode: recallMode,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
123
|
// ============================================================================
|
|
167
124
|
// Execute
|
|
168
125
|
// ============================================================================
|
|
@@ -216,25 +173,12 @@ export function executeDecisionMemory(db, params) {
|
|
|
216
173
|
error: "technology is required for check_alignment action",
|
|
217
174
|
};
|
|
218
175
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
ORDER BY updated_at DESC
|
|
226
|
-
LIMIT 500`)
|
|
227
|
-
.all();
|
|
228
|
-
const technology = params.technology;
|
|
229
|
-
const limit = params.limit || 20;
|
|
230
|
-
const config = getEmbeddingConfig();
|
|
231
|
-
// No provider -> synchronous lexical retrieval (classic behaviour).
|
|
232
|
-
if (!config) {
|
|
233
|
-
const rows = rankDecisionsLexical(candidateRows, technology, limit);
|
|
234
|
-
return buildAlignmentResponse(rows, new Map(), "ranked_lexical", technology);
|
|
235
|
-
}
|
|
236
|
-
// Provider configured -> hybrid retrieval (returns a Promise; dispatch awaits).
|
|
237
|
-
return hybridDecisionRecall(db, technology, candidateRows, limit, config).then(({ ranked, semanticById, recall_mode }) => buildAlignmentResponse(ranked, semanticById, recall_mode, technology));
|
|
176
|
+
// One implementation for both entry points. This action kept its own
|
|
177
|
+
// copy, which ignored `pattern` and drifted from check_decision_alignment.
|
|
178
|
+
return executeCheckDecisionAlignment(db, {
|
|
179
|
+
technology: params.technology,
|
|
180
|
+
pattern: params.pattern,
|
|
181
|
+
});
|
|
238
182
|
}
|
|
239
183
|
case "update": {
|
|
240
184
|
if (!params.id) {
|
|
@@ -263,9 +207,12 @@ export function executeDecisionMemory(db, params) {
|
|
|
263
207
|
}
|
|
264
208
|
sets.push("updated_at = datetime('now')");
|
|
265
209
|
values.push(params.id);
|
|
266
|
-
rawDb
|
|
210
|
+
const updated = rawDb
|
|
267
211
|
.prepare(`UPDATE developer_decisions SET ${sets.join(", ")} WHERE id = ?`)
|
|
268
212
|
.run(...values);
|
|
213
|
+
if (updated.changes === 0) {
|
|
214
|
+
return { error: `No decision with id ${params.id}. Use action "list" to find ids.` };
|
|
215
|
+
}
|
|
269
216
|
return {
|
|
270
217
|
success: true,
|
|
271
218
|
message: `Decision ${params.id} updated`,
|
|
@@ -277,6 +224,11 @@ export function executeDecisionMemory(db, params) {
|
|
|
277
224
|
error: "old_id and new_id are required for supersede action",
|
|
278
225
|
};
|
|
279
226
|
}
|
|
227
|
+
const exists = (id) => rawDb.prepare(`SELECT 1 FROM developer_decisions WHERE id = ?`).get(id) !== undefined;
|
|
228
|
+
const missing = [params.old_id, params.new_id].filter((id) => !exists(id));
|
|
229
|
+
if (missing.length > 0) {
|
|
230
|
+
return { error: `No decision with id ${missing.join(" or ")}. Use action "list" to find ids.` };
|
|
231
|
+
}
|
|
280
232
|
rawDb
|
|
281
233
|
.prepare(`UPDATE developer_decisions
|
|
282
234
|
SET status = 'superseded',
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency_check signal evaluation — pure functions, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* Each signal states a fact with its evidence and the verdict that fact
|
|
5
|
+
* implies on its own (`effect`). The item verdict is the strongest effect by
|
|
6
|
+
* a fixed precedence. `unknown` sits above `wait`/`proceed` and below
|
|
7
|
+
* `review`/`avoid`: a concrete negative finding stays actionable even when
|
|
8
|
+
* another source was unreachable, but a missing source never lets the
|
|
9
|
+
* verdict fall through to `proceed`.
|
|
10
|
+
*/
|
|
11
|
+
import type { PackageReleaseIndex, ReleaseRecord } from "../live/release-metadata.js";
|
|
12
|
+
export type Verdict = "proceed" | "wait" | "review" | "avoid" | "unknown";
|
|
13
|
+
export type SignalEffect = Verdict | "info";
|
|
14
|
+
export interface CheckItem {
|
|
15
|
+
ecosystem: "npm" | "crates.io";
|
|
16
|
+
package: string;
|
|
17
|
+
to: string;
|
|
18
|
+
from?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Signal {
|
|
21
|
+
id: "advisories" | "release_age" | "publish_trust" | "install_script_added" | "new_dependencies" | "yanked_or_deprecated" | "upgrade_type";
|
|
22
|
+
value: unknown;
|
|
23
|
+
evidence: string;
|
|
24
|
+
effect: SignalEffect;
|
|
25
|
+
}
|
|
26
|
+
export interface AdvisoryRef {
|
|
27
|
+
id: string;
|
|
28
|
+
severity: "critical" | "high" | "medium" | "low" | "unknown";
|
|
29
|
+
summary: string;
|
|
30
|
+
/** Unmaintained-crate notice rather than an exploitable vulnerability. */
|
|
31
|
+
maintenance: boolean;
|
|
32
|
+
}
|
|
33
|
+
export type AdvisoryFacts = {
|
|
34
|
+
available: true;
|
|
35
|
+
to: AdvisoryRef[];
|
|
36
|
+
from: AdvisoryRef[];
|
|
37
|
+
} | {
|
|
38
|
+
available: false;
|
|
39
|
+
detail: string;
|
|
40
|
+
};
|
|
41
|
+
/** A newly introduced dependency's first-publish lookup. */
|
|
42
|
+
export type CreatedAtFact = {
|
|
43
|
+
status: "ok";
|
|
44
|
+
createdAt: string | null;
|
|
45
|
+
} | {
|
|
46
|
+
status: "not_found";
|
|
47
|
+
detail: string;
|
|
48
|
+
} | {
|
|
49
|
+
status: "unreachable";
|
|
50
|
+
detail: string;
|
|
51
|
+
};
|
|
52
|
+
export declare const WAIT_DAYS = 3;
|
|
53
|
+
export declare const NEW_DEP_MIN_AGE_DAYS = 30;
|
|
54
|
+
/** The release `to` is judged against: the installed `from`, else the previous release on `to`'s line. */
|
|
55
|
+
export declare function selectBaseline(index: PackageReleaseIndex, item: CheckItem): {
|
|
56
|
+
record: ReleaseRecord | null;
|
|
57
|
+
kind: "from" | "previous_on_line";
|
|
58
|
+
note: string;
|
|
59
|
+
};
|
|
60
|
+
/** Runtime dependency names in `to` that the baseline did not have; null when either list is unknown. */
|
|
61
|
+
export declare function introducedDependencies(to: ReleaseRecord, base: ReleaseRecord | null): string[] | null;
|
|
62
|
+
export declare function advisoriesSignal(item: CheckItem, facts: AdvisoryFacts): Signal;
|
|
63
|
+
export declare function releaseAgeSignal(item: CheckItem, to: ReleaseRecord, advisories: Signal, now: Date): Signal;
|
|
64
|
+
export declare function publishTrustSignal(item: CheckItem, to: ReleaseRecord, base: ReturnType<typeof selectBaseline>): Signal;
|
|
65
|
+
export declare function installScriptSignal(item: CheckItem, to: ReleaseRecord, from: ReleaseRecord | null): Signal;
|
|
66
|
+
export declare function newDependenciesSignal(to: ReleaseRecord, base: ReturnType<typeof selectBaseline>, introduced: string[] | null, created: Map<string, CreatedAtFact>): Signal;
|
|
67
|
+
export declare function withdrawnSignal(item: CheckItem, to: ReleaseRecord): Signal;
|
|
68
|
+
export declare function upgradeTypeSignal(item: CheckItem): Signal;
|
|
69
|
+
export declare function upgradeType(from: string | undefined, to: string): "patch" | "minor" | "major" | "downgrade" | "none" | "new_dependency" | "unknown";
|
|
70
|
+
/** The item verdict and its one-line reason, from the strongest signal effect. */
|
|
71
|
+
export declare function decide(signals: Signal[]): {
|
|
72
|
+
verdict: Verdict;
|
|
73
|
+
reason: string;
|
|
74
|
+
};
|