@4da/mcp-server 6.0.2 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +13 -10
  3. package/dist/db.d.ts +73 -8
  4. package/dist/db.js +180 -43
  5. package/dist/doctor.d.ts +1 -1
  6. package/dist/doctor.js +15 -9
  7. package/dist/embeddings.js +19 -7
  8. package/dist/gitignore.js +9 -3
  9. package/dist/index.js +43 -4
  10. package/dist/live/cache.d.ts +31 -2
  11. package/dist/live/cache.js +113 -1
  12. package/dist/live/changelog-classify.js +4 -1
  13. package/dist/live/changelog.d.ts +15 -0
  14. package/dist/live/changelog.js +67 -11
  15. package/dist/live/dev-scope.d.ts +2 -2
  16. package/dist/live/fix-paths.d.ts +118 -0
  17. package/dist/live/fix-paths.js +498 -0
  18. package/dist/live/go-registry.d.ts +5 -0
  19. package/dist/live/go-registry.js +1 -1
  20. package/dist/live/index.d.ts +30 -3
  21. package/dist/live/index.js +59 -6
  22. package/dist/live/lockfile-parents.d.ts +61 -0
  23. package/dist/live/lockfile-parents.js +360 -0
  24. package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
  25. package/dist/live/lockfile-parsers-pygo.js +178 -28
  26. package/dist/live/lockfile-parsers.d.ts +4 -1
  27. package/dist/live/lockfile-parsers.js +57 -24
  28. package/dist/live/lockfile-types.d.ts +12 -3
  29. package/dist/live/lockfile-types.js +2 -1
  30. package/dist/live/osv-ranges.d.ts +79 -0
  31. package/dist/live/osv-ranges.js +133 -0
  32. package/dist/live/osv-scanner.d.ts +1 -0
  33. package/dist/live/osv-scanner.js +85 -78
  34. package/dist/live/range-satisfies.d.ts +21 -0
  35. package/dist/live/range-satisfies.js +190 -0
  36. package/dist/live/rate-limiter.d.ts +6 -0
  37. package/dist/live/rate-limiter.js +5 -0
  38. package/dist/live/registry-health.js +2 -0
  39. package/dist/live/resolution.d.ts +11 -1
  40. package/dist/live/resolution.js +16 -6
  41. package/dist/live/semver-utils.d.ts +8 -0
  42. package/dist/live/semver-utils.js +10 -2
  43. package/dist/live/types.d.ts +17 -1
  44. package/dist/live/upgrade-sources.d.ts +2 -0
  45. package/dist/live/upgrade-sources.js +7 -2
  46. package/dist/memory-store.d.ts +19 -0
  47. package/dist/memory-store.js +8 -0
  48. package/dist/native-bindings.d.ts +7 -2
  49. package/dist/native-bindings.js +4 -0
  50. package/dist/project-tree.d.ts +50 -8
  51. package/dist/project-tree.js +121 -29
  52. package/dist/schema-registry.js +2 -2
  53. package/dist/sqlite-driver.d.ts +134 -0
  54. package/dist/sqlite-driver.js +359 -0
  55. package/dist/tool-dispatch.d.ts +8 -1
  56. package/dist/tool-dispatch.js +17 -2
  57. package/dist/tools/agent-memory.js +10 -2
  58. package/dist/tools/app-plan.d.ts +11 -1
  59. package/dist/tools/app-plan.js +32 -6
  60. package/dist/tools/briefing-task-scope.d.ts +21 -2
  61. package/dist/tools/briefing-task-scope.js +97 -4
  62. package/dist/tools/decision-memory.js +9 -3
  63. package/dist/tools/dependency-check-signals.d.ts +7 -0
  64. package/dist/tools/dependency-check-signals.js +17 -2
  65. package/dist/tools/dependency-check.d.ts +1 -1
  66. package/dist/tools/dependency-check.js +1 -1
  67. package/dist/tools/dependency-health.d.ts +1 -1
  68. package/dist/tools/dependency-health.js +5 -0
  69. package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
  70. package/dist/tools/knowledge-gap-ranges.js +1 -1
  71. package/dist/tools/knowledge-gap-scope.d.ts +2 -2
  72. package/dist/tools/project-scope.d.ts +45 -0
  73. package/dist/tools/project-scope.js +119 -0
  74. package/dist/tools/record-feedback.js +1 -1
  75. package/dist/tools/upgrade-impact-literals.js +1 -1
  76. package/dist/tools/upgrade-impact-report.d.ts +10 -0
  77. package/dist/tools/upgrade-impact-report.js +32 -2
  78. package/dist/tools/upgrade-impact.js +1 -1
  79. package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
  80. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  81. package/dist/tools/upgrade-planner.d.ts +31 -3
  82. package/dist/tools/upgrade-planner.js +74 -19
  83. package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
  84. package/dist/tools/vulnerability-scan-concise.js +15 -3
  85. package/dist/tools/vulnerability-scan-format.d.ts +58 -1
  86. package/dist/tools/vulnerability-scan-format.js +107 -11
  87. package/dist/tools/vulnerability-scan.d.ts +104 -13
  88. package/dist/tools/vulnerability-scan.js +27 -3
  89. package/dist/tools/what-should-i-know.d.ts +22 -2
  90. package/dist/tools/what-should-i-know.js +121 -11
  91. package/package.json +5 -27
@@ -95,10 +95,103 @@ export function detectTaskPackages(task, files, deps, limit = 5) {
95
95
  packages.sort((a, b) => Number(b.direct) - Number(a.direct));
96
96
  return packages.slice(0, limit);
97
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;
98
+ /** Words after a name that make it a family: "tauri plugins", "@tauri-apps packages". */
99
+ const FAMILY_NOUNS = /^(?:plugins?|packages?|crates?|family|families|ecosystem|deps|dependencies|libraries|libs|modules|siblings)$/i;
100
+ /**
101
+ * The package families a task names, as base names: "all tauri plugins",
102
+ * "tauri and its plugins", "@tauri-apps packages", "tauri-plugin-*",
103
+ * "@tauri-apps/*". A name alone ("bump tauri") is not a family.
104
+ */
105
+ export function detectFamilyBases(task, files = []) {
106
+ const tokens = tokenize(`${task} ${files.join(" ")}`);
107
+ const bases = new Set();
108
+ let lastName = null;
109
+ tokens.forEach((token, i) => {
110
+ // Globs: "tauri-plugin-*" -> tauri, "@tauri-apps/*" -> @tauri-apps.
111
+ const glob = /^(@?[a-z0-9][\w.-]*?)(?:-plugin)?(?:[-_/]\*)$/.exec(token);
112
+ if (glob) {
113
+ bases.add(glob[1]);
114
+ return;
115
+ }
116
+ const next = tokens[i + 1];
117
+ if (next && FAMILY_NOUNS.test(next) && /^@?[a-z][\w.@/-]*$/.test(token) && !FAMILY_NOUNS.test(token)) {
118
+ if (!/^(?:all|every|its|their|the|other|any|some|these|those|related|official|first-party|in|of|for|with|from|our|my|your|and|or|to|dev|npm|cargo|pip|go|rust|js|node|python|workspace|root|outdated|vulnerable|new|old|unused|direct|transitive)$/.test(token)) {
119
+ bases.add(token);
120
+ }
121
+ else if (token === "its" || token === "their") {
122
+ // "tauri and its plugins": the name the possessive refers to.
123
+ if (lastName)
124
+ bases.add(lastName);
125
+ }
126
+ }
127
+ if (/^@?[a-z][\w.@/-]*$/.test(token) && !FAMILY_NOUNS.test(token) && !/^(?:and|or|its|their|all|every|to|the|bump|upgrade|update)$/.test(token)) {
128
+ lastName = token;
129
+ }
130
+ });
131
+ return [...bases];
132
+ }
133
+ /** True when `name` belongs to the family of `base`: base-*, base_*, @base/*, @base-apps/*. */
134
+ export function inFamily(name, base) {
135
+ const n = name.toLowerCase();
136
+ const b = base.toLowerCase();
137
+ if (n === b)
138
+ return false;
139
+ if (b.startsWith("@"))
140
+ return n.startsWith(`${b}/`);
141
+ return (n.startsWith(`${b}-`) ||
142
+ n.startsWith(`${b}_`) ||
143
+ n.startsWith(`@${b}/`) ||
144
+ n.startsWith(`@${b}-`) // @tauri-apps/api, @tauri-apps/plugin-shell
145
+ );
146
+ }
147
+ /**
148
+ * The direct dependencies in the families the task names, other than the
149
+ * packages it names exactly. Only packages this project actually declares:
150
+ * a family is expanded against the lockfiles, never guessed. Measured
151
+ * 2026-10-07: "bump tauri and all tauri plugins" matched only `tauri`.
152
+ */
153
+ export function detectFamilyPackages(task, files, deps, exact, limit = 25) {
154
+ const bases = detectFamilyBases(task, files);
155
+ if (bases.length === 0)
156
+ return [];
157
+ const taken = new Set(exact.map((p) => `${p.ecosystem}\0${p.name}`));
158
+ const groups = new Map();
159
+ for (const dep of deps) {
160
+ if (!dep.name || !dep.isDirect)
161
+ continue;
162
+ const key = `${dep.ecosystem}\0${dep.name}`;
163
+ if (taken.has(key))
164
+ continue;
165
+ const base = bases.find((b) => inFamily(dep.name, b));
166
+ if (!base)
167
+ continue;
168
+ const group = groups.get(key) ?? { base, copies: [] };
169
+ group.copies.push(dep);
170
+ groups.set(key, group);
171
+ }
172
+ const out = [...groups.values()].map(({ base, copies }) => ({
173
+ name: copies[0].name,
174
+ ecosystem: copies[0].ecosystem,
175
+ installed: [...new Set(copies.map((d) => d.version).filter((v) => Boolean(v)))],
176
+ direct: true,
177
+ dev: copies.every((d) => d.isDev),
178
+ requested_from: null,
179
+ requested_to: null,
180
+ family: base,
181
+ }));
182
+ out.sort((a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.name.localeCompare(b.name));
183
+ return out.slice(0, limit);
184
+ }
185
+ /** True when the task asks for the newest release ("to latest", "newest major"). */
186
+ export function wantsLatest(task) {
187
+ return /\b(?:latest|newest|most recent)\b/i.test(task);
188
+ }
189
+ /** The caret line a version sits on: "2" for 2.12.1, "0.13" for 0.13.5. */
190
+ export function caretLine(version) {
191
+ const m = version ? /^v?(\d+)\.(\d+)/.exec(version) : null;
192
+ if (!m)
193
+ return null;
194
+ return m[1] === "0" ? `0.${m[2]}` : m[1];
102
195
  }
103
196
  /** Actionable advisories on the installed copies of one package, highest presented severity first. */
104
197
  export function advisoriesFor(pkg, vulns) {
@@ -5,6 +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 { writeMeta, writerOf } from "../memory-store.js";
8
9
  import { executeCheckDecisionAlignment } from "./decision-enforcement.js";
9
10
  // ============================================================================
10
11
  // Tool Definition
@@ -132,7 +133,10 @@ export function executeDecisionMemory(db, params) {
132
133
  error: "subject and decision are required for record action",
133
134
  };
134
135
  }
135
- const stmt = rawDb.prepare(`INSERT INTO developer_decisions
136
+ // The app's own developer_decisions table (separate write connection), or
137
+ // the server's store when the app's schema cannot hold the row.
138
+ const to = writerOf(db, "developer_decisions", ["decision_type", "subject", "decision", "rationale", "alternatives_rejected", "context_tags", "confidence", "status"]);
139
+ const stmt = to.raw.prepare(`INSERT INTO developer_decisions
136
140
  (decision_type, subject, decision, rationale,
137
141
  alternatives_rejected, context_tags, confidence, status)
138
142
  VALUES (?, ?, ?, ?, ?, ?, ?, 'active')`);
@@ -141,6 +145,7 @@ export function executeDecisionMemory(db, params) {
141
145
  success: true,
142
146
  id: result.lastInsertRowid,
143
147
  message: `Decision recorded: ${params.subject}`,
148
+ ...writeMeta(to),
144
149
  };
145
150
  }
146
151
  case "list": {
@@ -207,7 +212,8 @@ export function executeDecisionMemory(db, params) {
207
212
  }
208
213
  sets.push("updated_at = datetime('now')");
209
214
  values.push(params.id);
210
- const updated = rawDb
215
+ const updater = writerOf(db, "developer_decisions", ["id", "updated_at", ...sets.map((s) => s.split(" ")[0])]);
216
+ const updated = updater.raw
211
217
  .prepare(`UPDATE developer_decisions SET ${sets.join(", ")} WHERE id = ?`)
212
218
  .run(...values);
213
219
  if (updated.changes === 0) {
@@ -229,7 +235,7 @@ export function executeDecisionMemory(db, params) {
229
235
  if (missing.length > 0) {
230
236
  return { error: `No decision with id ${missing.join(" or ")}. Use action "list" to find ids.` };
231
237
  }
232
- rawDb
238
+ writerOf(db, "developer_decisions", ["id", "status", "superseded_by", "updated_at"]).raw
233
239
  .prepare(`UPDATE developer_decisions
234
240
  SET status = 'superseded',
235
241
  superseded_by = ?,
@@ -65,6 +65,13 @@ export declare function publishTrustSignal(item: CheckItem, to: ReleaseRecord, b
65
65
  export declare function installScriptSignal(item: CheckItem, to: ReleaseRecord, from: ReleaseRecord | null): Signal;
66
66
  export declare function newDependenciesSignal(to: ReleaseRecord, base: ReturnType<typeof selectBaseline>, introduced: string[] | null, created: Map<string, CreatedAtFact>): Signal;
67
67
  export declare function withdrawnSignal(item: CheckItem, to: ReleaseRecord): Signal;
68
+ /**
69
+ * The bump's semver class. A breaking range (a major, or below 1.0 a minor or
70
+ * 0.0.x patch) is `review`: the supply-chain signals say nothing about whether
71
+ * the project's code survives it. Until 6.0.2 this was information only, so
72
+ * vitest 3 -> 5, reqwest 0.12 -> 0.13 and stripe 22 -> 23 all came back
73
+ * "proceed" and the summary told the agent to apply them (2026-10-07 eval).
74
+ */
68
75
  export declare function upgradeTypeSignal(item: CheckItem): Signal;
69
76
  export declare function upgradeType(from: string | undefined, to: string): "patch" | "minor" | "major" | "downgrade" | "none" | "new_dependency" | "unknown";
70
77
  /** The item verdict and its one-line reason, from the strongest signal effect. */
@@ -222,11 +222,26 @@ export function withdrawnSignal(item, to) {
222
222
  }
223
223
  return { id: "yanked_or_deprecated", value: null, effect: "proceed", evidence: `${item.to} is neither yanked nor deprecated.` };
224
224
  }
225
+ /**
226
+ * The bump's semver class. A breaking range (a major, or below 1.0 a minor or
227
+ * 0.0.x patch) is `review`: the supply-chain signals say nothing about whether
228
+ * the project's code survives it. Until 6.0.2 this was information only, so
229
+ * vitest 3 -> 5, reqwest 0.12 -> 0.13 and stripe 22 -> 23 all came back
230
+ * "proceed" and the summary told the agent to apply them (2026-10-07 eval).
231
+ */
225
232
  export function upgradeTypeSignal(item) {
226
233
  const kind = upgradeType(item.from, item.to);
227
234
  const text = item.from ? `${item.from} -> ${item.to}: ${kind}` : `new dependency at ${item.to}`;
228
- const zeroX = kind === "major" && /^0\./.test(item.from ?? "") ? " (0.x: a minor bump is breaking)" : "";
229
- return { id: "upgrade_type", value: kind, evidence: `${text}${zeroX}.`, effect: "info" };
235
+ if (kind === "major") {
236
+ const zeroX = /^0\./.test(item.from ?? "") ? " (below 1.0 a minor bump is a major one)" : "";
237
+ return {
238
+ id: "upgrade_type",
239
+ value: kind,
240
+ evidence: `${text}${zeroX}: breaking version range, run upgrade_impact to see what changes for this project.`,
241
+ effect: "review",
242
+ };
243
+ }
244
+ return { id: "upgrade_type", value: kind, evidence: `${text}.`, effect: "info" };
230
245
  }
231
246
  export function upgradeType(from, to) {
232
247
  if (!from)
@@ -77,7 +77,7 @@ interface ItemResult {
77
77
  reason: string;
78
78
  signals: Signal[];
79
79
  }
80
- export declare function executeDependencyCheck(_db: FourDADatabase, params: DependencyCheckParams, liveIntel: LiveIntelligence | null, opts?: {
80
+ export declare function executeDependencyCheck(_db: FourDADatabase | null, params: DependencyCheckParams, liveIntel: LiveIntelligence | null, opts?: {
81
81
  now?: Date;
82
82
  }): Promise<{
83
83
  checked_at: string;
@@ -45,7 +45,7 @@ export const dependencyCheckTool = {
45
45
  "- install_script_added (npm): `to` runs preinstall/install/postinstall and `from` did not -> review. For a new dependency, information only.",
46
46
  "- new_dependencies: runtime dependencies added since the baseline, each with its first-publish age. Any under 30 days old, or no longer on the registry -> review.",
47
47
  "- yanked_or_deprecated: `to` yanked, unpublished or deprecated -> avoid.",
48
- "- upgrade_type: patch | minor | major (a 0.x minor counts as major) | downgrade | new_dependency. Information only.",
48
+ "- upgrade_type: patch | minor | major (a 0.x minor counts as major) | downgrade | new_dependency. major (a breaking version range) -> review: call upgrade_impact for what it changes. The others are information only.",
49
49
  "",
50
50
  "Verdict precedence: avoid > review > unknown > wait > proceed. `unknown` means a registry or OSV.dev lookup failed, or the package is not on the public registry (private package, or a misspelled/hallucinated name); it is never a silent proceed. Apply only `proceed` items; report the others with their evidence. For WHAT the bump changes in your code, call upgrade_impact.",
51
51
  "",
@@ -41,4 +41,4 @@ export declare const dependencyHealthTool: {
41
41
  };
42
42
  };
43
43
  };
44
- export declare function executeDependencyHealth(_db: FourDADatabase, params: DependencyHealthParams, liveIntel: LiveIntelligence | null): Promise<DependencyHealthResult>;
44
+ export declare function executeDependencyHealth(_db: FourDADatabase | null, params: DependencyHealthParams, liveIntel: LiveIntelligence | null): Promise<DependencyHealthResult>;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { isActionableVulnerability } from "../live/maintenance.js";
10
10
  import { driftFor } from "./install-drift-notes.js";
11
+ import { discoveryNote } from "../project-tree.js";
11
12
  /** How long a health check waits for its own OSV scan before answering without CVE data (the scan keeps running). */
12
13
  const HEALTH_SCAN_TIMEOUT_MS = 60_000;
13
14
  export const dependencyHealthTool = {
@@ -209,6 +210,10 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
209
210
  if (hidden > 0) {
210
211
  parts.push(`${hidden} advisor${hidden !== 1 ? "ies" : "y"} not counted (platform-inactive or maintenance notices)`);
211
212
  }
213
+ // "all healthy" over a partial walk must say it is partial.
214
+ const partial = discoveryNote(liveIntel.getDiscovery());
215
+ if (partial)
216
+ parts.push(partial.replace(/[.]$/, ""));
212
217
  if (!vulnResult) {
213
218
  parts.push(liveIntel.isEnabled()
214
219
  ? `CVE data not loaded: the OSV scan did not finish within ${HEALTH_SCAN_TIMEOUT_MS / 1000}s or OSV was unreachable. Call again to use it once it completes`
@@ -35,7 +35,7 @@ export declare function rangesContain(ranges: AdvisoryRangeEvent[][], installed:
35
35
  * An advisory naming your dependency is only a gap if you are actually
36
36
  * exposed: the live tool once graded three Hono CVEs `critical` on hono
37
37
  * 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
38
- * pinned past via `pnpm.overrides`.
38
+ * pinned past via pnpm overrides.
39
39
  *
40
40
  * Conservative by construction: a missing or unreadable installed version
41
41
  * counts as AFFECTED. Never claim someone is safe on missing information.
@@ -105,7 +105,7 @@ export function rangesContain(ranges, installed) {
105
105
  * An advisory naming your dependency is only a gap if you are actually
106
106
  * exposed: the live tool once graded three Hono CVEs `critical` on hono
107
107
  * 4.13.2 when all three are fixed in 4.12.34, a version this repo had already
108
- * pinned past via `pnpm.overrides`.
108
+ * pinned past via pnpm overrides.
109
109
  *
110
110
  * Conservative by construction: a missing or unreadable installed version
111
111
  * counts as AFFECTED. Never claim someone is safe on missing information.
@@ -4,10 +4,10 @@
4
4
  * `temporal::{active_repo_roots, dep_within_active_root, shares_active_root}`
5
5
  * and `knowledge_decay::{get_active_project_paths, linked_to}`.
6
6
  */
7
- import type BetterSqlite3 from "better-sqlite3";
7
+ import type { SqliteStatement } from "../sqlite-driver.js";
8
8
  import type { FourDADatabase } from "../db.js";
9
9
  /** `stmt.all(...)`, or [] when the statement cannot run: a read here never throws out of the tool. */
10
- export declare function allRows<T>(stmt: BetterSqlite3.Statement, ...args: unknown[]): T[];
10
+ export declare function allRows<T>(stmt: SqliteStatement, ...args: unknown[]): T[];
11
11
  /** Lowercase, forward slashes, no trailing slash (`knowledge_decay::normalize_project_path`). */
12
12
  export declare function normPath(p: string): string;
13
13
  /**
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Which project a planning or briefing answer is about.
3
+ *
4
+ * Measured 2026-10-07: upgrade_planner called from D:\4DA returned the 4DA
5
+ * app's cross-project plan, and its top steps were for navcal, verax and
6
+ * 4da-ledger. upgrade_planner and what_should_i_know now take `project_path`:
7
+ * omitted, it is the project the server was started for (FOURDA_PROJECT_DIR,
8
+ * else the working directory) when that directory is a project; `"*"` asks
9
+ * for every project.
10
+ */
11
+ import type { FourDADatabase } from "../db.js";
12
+ import { LiveIntelligence } from "../live/index.js";
13
+ export declare const ALL_PROJECTS = "*";
14
+ /** The project the server was started for: FOURDA_PROJECT_DIR (an unsubstituted "${...}" template is ignored), else cwd. */
15
+ export declare function serverProjectDir(): string;
16
+ /** True when `dir` holds a manifest or lockfile of a supported ecosystem. */
17
+ export declare function isProjectDir(dir: string): boolean;
18
+ /** Lowercased, forward slashes, no trailing slash: the form the app's plan stores project paths in. */
19
+ export declare function normalizeProjectPath(p: string): string;
20
+ /** True when `candidate` is `root` or inside it. */
21
+ export declare function isWithin(candidate: string, root: string): boolean;
22
+ export type ProjectScope = {
23
+ kind: "project";
24
+ path: string;
25
+ source: "argument" | "server_project";
26
+ } | {
27
+ kind: "all";
28
+ reason: string;
29
+ } | {
30
+ kind: "error";
31
+ error: string;
32
+ };
33
+ /**
34
+ * The scope for one call. An explicit path must be a directory on this
35
+ * machine; `"*"` is every project; omitted, the server's project when it is
36
+ * one, otherwise every project (a server started in a home directory has no
37
+ * project to narrow to).
38
+ */
39
+ export declare function resolveProjectScope(projectPath: string | undefined): ProjectScope;
40
+ /**
41
+ * The live layer for `scope`: the server's own when it already covers that
42
+ * root, otherwise a fresh one resolved from the project tree at that path (as
43
+ * vulnerability_scan does for its `project_path`).
44
+ */
45
+ export declare function liveIntelFor<T extends Pick<LiveIntelligence, "getProjectRoot">>(liveIntel: T | null, db: FourDADatabase | null, scope: ProjectScope): T | LiveIntelligence | null;
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Which project a planning or briefing answer is about.
4
+ *
5
+ * Measured 2026-10-07: upgrade_planner called from D:\4DA returned the 4DA
6
+ * app's cross-project plan, and its top steps were for navcal, verax and
7
+ * 4da-ledger. upgrade_planner and what_should_i_know now take `project_path`:
8
+ * omitted, it is the project the server was started for (FOURDA_PROJECT_DIR,
9
+ * else the working directory) when that directory is a project; `"*"` asks
10
+ * for every project.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { getServerCacheDb } from "../live/cache.js";
15
+ import { LiveIntelligence } from "../live/index.js";
16
+ import { scanProjectTree, treeResolutionGroups } from "../project-tree.js";
17
+ /** Files that make a directory a project (manifests and lockfiles of the supported ecosystems). */
18
+ const PROJECT_MARKERS = [
19
+ "package.json",
20
+ "Cargo.toml",
21
+ "pyproject.toml",
22
+ "setup.py",
23
+ "requirements.txt",
24
+ "Pipfile",
25
+ "go.mod",
26
+ "pnpm-lock.yaml",
27
+ "package-lock.json",
28
+ "yarn.lock",
29
+ "bun.lock",
30
+ "Cargo.lock",
31
+ "poetry.lock",
32
+ "uv.lock",
33
+ ];
34
+ export const ALL_PROJECTS = "*";
35
+ /** The project the server was started for: FOURDA_PROJECT_DIR (an unsubstituted "${...}" template is ignored), else cwd. */
36
+ export function serverProjectDir() {
37
+ const dir = process.env.FOURDA_PROJECT_DIR?.trim();
38
+ if (dir && !dir.includes("${"))
39
+ return dir;
40
+ return process.cwd();
41
+ }
42
+ /** True when `dir` holds a manifest or lockfile of a supported ecosystem. */
43
+ export function isProjectDir(dir) {
44
+ return PROJECT_MARKERS.some((m) => {
45
+ try {
46
+ return fs.statSync(path.join(dir, m)).isFile();
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ });
52
+ }
53
+ /** Lowercased, forward slashes, no trailing slash: the form the app's plan stores project paths in. */
54
+ export function normalizeProjectPath(p) {
55
+ return path.resolve(p).replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
56
+ }
57
+ /** True when `candidate` is `root` or inside it. */
58
+ export function isWithin(candidate, root) {
59
+ const c = normalizeProjectPath(candidate);
60
+ const r = normalizeProjectPath(root);
61
+ return c === r || c.startsWith(`${r}/`);
62
+ }
63
+ /**
64
+ * The scope for one call. An explicit path must be a directory on this
65
+ * machine; `"*"` is every project; omitted, the server's project when it is
66
+ * one, otherwise every project (a server started in a home directory has no
67
+ * project to narrow to).
68
+ */
69
+ export function resolveProjectScope(projectPath) {
70
+ const given = projectPath?.trim();
71
+ if (given === ALL_PROJECTS)
72
+ return { kind: "all", reason: 'project_path "*" asks for every project' };
73
+ if (given) {
74
+ const abs = path.resolve(given);
75
+ let isDir = false;
76
+ try {
77
+ isDir = fs.statSync(abs).isDirectory();
78
+ }
79
+ catch {
80
+ isDir = false;
81
+ }
82
+ if (!isDir) {
83
+ return {
84
+ kind: "error",
85
+ error: `project_path "${given}" is not a directory on this machine. Pass the absolute path of a project root, "*" for every project, or omit it for ${serverProjectDir()}.`,
86
+ };
87
+ }
88
+ return { kind: "project", path: abs, source: "argument" };
89
+ }
90
+ const own = serverProjectDir();
91
+ if (isProjectDir(own))
92
+ return { kind: "project", path: path.resolve(own), source: "server_project" };
93
+ return { kind: "all", reason: `the server's directory ${own} is not a project (no manifest or lockfile)` };
94
+ }
95
+ /**
96
+ * The live layer for `scope`: the server's own when it already covers that
97
+ * root, otherwise a fresh one resolved from the project tree at that path (as
98
+ * vulnerability_scan does for its `project_path`).
99
+ */
100
+ export function liveIntelFor(liveIntel, db, scope) {
101
+ if (scope.kind !== "project" || !liveIntel)
102
+ return liveIntel;
103
+ const root = liveIntel.getProjectRoot();
104
+ if (root && normalizeProjectPath(root) === normalizeProjectPath(scope.path))
105
+ return liveIntel;
106
+ // A scope the server did not start for: resolve that tree. The connection
107
+ // is only used for its cache (LiveCache never writes to the app's database).
108
+ let raw;
109
+ try {
110
+ raw = db?.getRawDb();
111
+ }
112
+ catch {
113
+ raw = undefined;
114
+ }
115
+ const scoped = new LiveIntelligence(raw ?? getServerCacheDb());
116
+ const { entries, discovery } = scanProjectTree(scope.path);
117
+ scoped.initFromProjectTree(scope.path, treeResolutionGroups(entries), discovery);
118
+ return scoped;
119
+ }
@@ -17,7 +17,7 @@ Feedback actions:
17
17
  - "dismiss": User dismissed the item
18
18
  - "mark_irrelevant": User said the item is not relevant (also records a relevance label)
19
19
 
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.`,
20
+ Only "save" and "mark_irrelevant" are relevance labels. Use them only when the user actually says whether the item matters to them. This does not train content preferences. Recorded in the 4DA desktop app's own interactions/feedback tables.`,
21
21
  inputSchema: {
22
22
  type: "object",
23
23
  properties: {
@@ -74,7 +74,7 @@ export async function flagOldSyntax(sections, files, root) {
74
74
  const candidates = sections
75
75
  .flatMap((s) => s.entries)
76
76
  .filter((e) => e.kind === "breaking" || e.kind === "deprecation")
77
- .map((entry) => ({ entry, shapes: oldSyntaxShapes(entry.text) }))
77
+ .map((entry) => ({ entry, shapes: [entry.text, ...(entry.details ?? [])].flatMap(oldSyntaxShapes) }))
78
78
  .filter((c) => c.shapes.length > 0);
79
79
  if (!root || candidates.length === 0 || files.length === 0)
80
80
  return { newlyTouching: 0, examples: [] };
@@ -24,6 +24,8 @@ export interface ReportEntry {
24
24
  text: string;
25
25
  /** The changelog heading or parent bullet the entry sits under ("Removed", "Breaking Changes"). */
26
26
  under?: string;
27
+ /** Bullets nested under the entry: part of it, not counted as entries of their own. */
28
+ details?: string[];
27
29
  touches_your_code?: boolean;
28
30
  matched_symbols?: string[];
29
31
  /** String literals in your code that use the syntax this entry retires (route patterns and the like). */
@@ -62,6 +64,14 @@ export type UpgradeType = "patch" | "minor" | "major" | "prerelease" | "unknown"
62
64
  export declare function upgradeType(from: string, to: string): UpgradeType;
63
65
  /** Major versions crossed (0.x minors count as majors, matching `upgradeType`). */
64
66
  export declare function majorsCrossed(from: string, to: string): number;
67
+ /**
68
+ * `majorsCrossed` for versions as a task names them: "0.13", "7.x", "v2",
69
+ * "1.4.0". A partial version is read as the start of its line (0.13 ->
70
+ * 0.13.0, 7.x -> 7.0.0). Null when either side is unreadable. A plain
71
+ * major-number compare read reqwest 0.12 -> 0.13 as 0 majors crossed, and
72
+ * the briefing called a breaking upgrade safe to delegate.
73
+ */
74
+ export declare function majorsCrossedLoose(from: string | null, to: string | null): number | null;
65
75
  /** Up to `count` published versions nearest to `wanted` by precedence, for "version not found" errors. */
66
76
  export declare function nearestVersions(published: string[], requested: string, count?: number): string[];
67
77
  export interface SummaryInput {
@@ -50,9 +50,15 @@ export function shapeChangelog(sections, symbols, format) {
50
50
  return true;
51
51
  });
52
52
  const entries = unique.map((e) => {
53
- const entry = { kind: e.kind, text: e.text, ...(e.under ? { under: e.under } : {}) };
53
+ const entry = {
54
+ kind: e.kind,
55
+ text: e.text,
56
+ ...(e.under ? { under: e.under } : {}),
57
+ ...(e.details?.length ? { details: e.details } : {}),
58
+ };
54
59
  if (e.kind === "breaking" || e.kind === "deprecation") {
55
- const matched = matchSymbols(e.text, symbols);
60
+ // The nested points name the affected functions (date-fns 3.0) as often as the entry does.
61
+ const matched = matchSymbols([e.text, ...(e.details ?? [])].join(" "), symbols);
56
62
  if (matched.length > 0) {
57
63
  entry.touches_your_code = true;
58
64
  entry.matched_symbols = matched;
@@ -120,6 +126,9 @@ export function upgradeType(from, to) {
120
126
  return "major";
121
127
  if (a.major === 0 && a.minor !== b.minor)
122
128
  return "major";
129
+ // ^0.0.3 admits only 0.0.3: any 0.0.x change is breaking (dependency_health and dependency_check agree).
130
+ if (a.major === 0 && a.minor === 0 && a.patch !== b.patch)
131
+ return "major";
123
132
  if (a.minor !== b.minor)
124
133
  return "minor";
125
134
  return "patch";
@@ -134,6 +143,27 @@ export function majorsCrossed(from, to) {
134
143
  return Math.max(0, b.minor - a.minor);
135
144
  return Math.max(0, b.major - a.major);
136
145
  }
146
+ /**
147
+ * `majorsCrossed` for versions as a task names them: "0.13", "7.x", "v2",
148
+ * "1.4.0". A partial version is read as the start of its line (0.13 ->
149
+ * 0.13.0, 7.x -> 7.0.0). Null when either side is unreadable. A plain
150
+ * major-number compare read reqwest 0.12 -> 0.13 as 0 majors crossed, and
151
+ * the briefing called a breaking upgrade safe to delegate.
152
+ */
153
+ export function majorsCrossedLoose(from, to) {
154
+ const pad = (v) => {
155
+ const m = v ? /^v?(\d+)(?:\.(\d+|[x*]))?(?:\.(\d+|[x*]))?/i.exec(v.trim()) : null;
156
+ if (!m)
157
+ return null;
158
+ const num = (s) => (s === undefined || /[x*]/i.test(s) ? "0" : s);
159
+ return `${m[1]}.${num(m[2])}.${num(m[3])}`;
160
+ };
161
+ const a = pad(from);
162
+ const b = pad(to);
163
+ if (!a || !b)
164
+ return null;
165
+ return majorsCrossed(a, b);
166
+ }
137
167
  /** Up to `count` published versions nearest to `wanted` by precedence, for "version not found" errors. */
138
168
  export function nearestVersions(published, requested, count = 5) {
139
169
  // "7" / "7.x" / "7.1" are not semver: read them as the start of that line,
@@ -287,7 +287,7 @@ function cacheFor(db) {
287
287
  return cache;
288
288
  }
289
289
  catch {
290
- return null; // a read-only or closed database: run uncached rather than fail
290
+ return null; // a closed database: run uncached rather than fail (a read-only one uses the server cache file)
291
291
  }
292
292
  }
293
293
  export async function executeUpgradeImpact(db, params, liveIntel) {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Fix-path verification for upgrade_planner steps (see live/fix-paths.ts):
3
+ * the target must clear EVERY known advisory of the package, and a
4
+ * transitive step is "waiting on upstream" only when a parent's declared
5
+ * requirement actually excludes every clean version.
6
+ *
7
+ * Runs on the steps the plan returns, within a time budget. A step whose
8
+ * lookups fail or run out of time keeps its unverified answer and says so in
9
+ * `fixPathChecked`.
10
+ */
11
+ import type { LiveIntelligence } from "../live/index.js";
12
+ /** The planner fields this step reads and rewrites. */
13
+ export interface FixPathStep {
14
+ package: string;
15
+ ecosystem: string;
16
+ currentVersion: string | null;
17
+ targetVersion: string | null;
18
+ latestVersion?: string | null;
19
+ upgradeType: "patch" | "minor" | "major" | "unknown";
20
+ reasons: string[];
21
+ breaking: boolean;
22
+ scope: "direct" | "transitive";
23
+ action: string;
24
+ /** How the target was checked: against every advisory of the package, or only the installed version's. */
25
+ fixPathChecked?: "all_advisories" | "installed_advisories_only";
26
+ /** lockfile_refresh: the command per lockfile directory. */
27
+ refreshCommands?: Array<{
28
+ dir: string;
29
+ command: string;
30
+ }>;
31
+ }
32
+ export interface FixPathInput {
33
+ step: FixPathStep;
34
+ /** Per-advisory fixes on the installed line (each advisory's own fixedVersion). */
35
+ fixes: string[];
36
+ /** Manifest directories that pin this copy (the scan's sourceDirs). */
37
+ sourceDirs: string[];
38
+ }
39
+ /** Verify and rewrite the steps in place. Returns how many could not be verified (offline, failure, budget). */
40
+ export declare function verifyFixPaths(inputs: FixPathInput[], liveIntel: Pick<LiveIntelligence, "getFixPathSources" | "lockfileFor" | "getProjectRoot">, budgetMs?: number): Promise<number>;