@4da/mcp-server 5.0.4 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +233 -37
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +12 -1
  51. package/dist/live/osv-scanner.js +50 -15
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -2,11 +2,13 @@
2
2
  /**
3
3
  * Tool Dispatch Registry
4
4
  *
5
- * Map-based dispatch for the 14 active tools.
5
+ * Map-based dispatch for the 15 active tools.
6
6
  * Adding a new tool = add to this map + schema-registry + barrel export.
7
7
  */
8
8
  import { assertToolPermission } from "./auth-context.js";
9
- import { executeGetRelevantContent, executeGetContext, executeRecordFeedback, executeGetActionableSignals, executeKnowledgeGaps, executeDecisionMemory, executeCheckDecisionAlignment, executeAgentMemory, executeDeveloperDna, executeWhatShouldIKnow, executeVulnerabilityScan, executeEcosystemPulse, executeDependencyHealth, executeUpgradePlanner, } from "./tools/index.js";
9
+ import { checkBuildStaleness } from "./build-staleness.js";
10
+ import { cleanStrings } from "./tool-args.js";
11
+ import { executeGetRelevantContent, executeGetContext, executeRecordFeedback, executeGetActionableSignals, executeKnowledgeGaps, executeDecisionMemory, executeCheckDecisionAlignment, executeAgentMemory, executeDeveloperDna, executeWhatShouldIKnow, executeVulnerabilityScan, executeEcosystemPulse, executeDependencyHealth, executeUpgradePlanner, executeDependencyCheck, executeUpgradeImpact, } from "./tools/index.js";
10
12
  import { getLiveIntelligence } from "./live-singleton.js";
11
13
  /**
12
14
  * Dispatch map — tool name → execute function
@@ -16,13 +18,15 @@ const DISPATCH_MAP = {
16
18
  vulnerability_scan: (db, params) => executeVulnerabilityScan(db, params, getLiveIntelligence()),
17
19
  dependency_health: (db, params) => executeDependencyHealth(db, params, getLiveIntelligence()),
18
20
  upgrade_planner: (db, params) => executeUpgradePlanner(db, params, getLiveIntelligence()),
21
+ dependency_check: (db, params) => executeDependencyCheck(db, params, getLiveIntelligence()),
22
+ upgrade_impact: (db, params) => executeUpgradeImpact(db, params, getLiveIntelligence()),
19
23
  // Intelligence
20
24
  what_should_i_know: executeWhatShouldIKnow,
21
25
  ecosystem_pulse: (db, params) => executeEcosystemPulse(db, params, getLiveIntelligence()),
22
26
  get_context: executeGetContext,
23
27
  get_relevant_content: executeGetRelevantContent,
24
28
  get_actionable_signals: executeGetActionableSignals,
25
- knowledge_gaps: executeKnowledgeGaps,
29
+ knowledge_gaps: (db, params) => executeKnowledgeGaps(db, params, getLiveIntelligence()),
26
30
  record_feedback: executeRecordFeedback,
27
31
  // Decisions
28
32
  decision_memory: executeDecisionMemory,
@@ -79,9 +83,24 @@ export async function dispatchTool(name, db, args) {
79
83
  throw error;
80
84
  }
81
85
  }
82
- const payload = FRESHNESS_TOOLS.has(name) ? attachFreshness(db, result) : result;
86
+ // Feed freshness describes the desktop app's feed; a standalone install has
87
+ // none, and telling it to "run fourda-engine" is noise.
88
+ const withFreshness = FRESHNESS_TOOLS.has(name) && !db.isStandalone;
89
+ const payload = cleanStrings(withFreshness ? attachFreshness(db, result) : result);
90
+ // An executor that returns `{ error: "<message>" }` failed: say so in the
91
+ // protocol, not only in the body, so the host and model treat it as an error.
92
+ const failed = payload !== null && typeof payload === "object" && typeof payload.error === "string";
93
+ // The same payload as text AND as structuredContent: hosts disagree on which
94
+ // one reaches the model (Claude Code and VS Code read the structured form;
95
+ // Cursor, Zed and Gemini CLI read only the text), so neither may be partial.
96
+ // Compact JSON: indentation was ~20% of every response's tokens.
97
+ const structured = payload !== null && typeof payload === "object" && !Array.isArray(payload)
98
+ ? { structuredContent: payload }
99
+ : {};
83
100
  return {
84
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
101
+ content: [{ type: "text", text: JSON.stringify(payload) }],
102
+ ...structured,
103
+ ...(failed ? { isError: true } : {}),
85
104
  };
86
105
  }
87
106
  /**
@@ -98,13 +117,28 @@ function attachFreshness(db, result) {
98
117
  catch {
99
118
  return result;
100
119
  }
120
+ // Repo checkouts only (null in the published package): when the running
121
+ // dist/ predates src/, every DB-backed answer reflects code the repo has
122
+ // already replaced. Saying so on the payload is what would have caught the
123
+ // 2026-08-30 two-day-stale server before its bugs were re-diagnosed.
124
+ let server_build;
125
+ try {
126
+ const staleness = checkBuildStaleness();
127
+ if (staleness?.stale && staleness.note) {
128
+ server_build = { stale: true, note: staleness.note };
129
+ }
130
+ }
131
+ catch {
132
+ // Never let the self-check break a tool response.
133
+ }
134
+ const extras = server_build ? { server_build } : {};
101
135
  if (Array.isArray(result)) {
102
- return { data_freshness, item_count: result.length, items: result };
136
+ return { data_freshness, ...extras, item_count: result.length, items: result };
103
137
  }
104
138
  if (result && typeof result === "object") {
105
- return { data_freshness, ...result };
139
+ return { data_freshness, ...extras, ...result };
106
140
  }
107
- return { data_freshness, result };
141
+ return { data_freshness, ...extras, result };
108
142
  }
109
143
  /** Check if a tool exists in the dispatch map */
110
144
  export function hasDispatchTool(name) {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The 4DA app's persisted Upgrade Plan, read for `upgrade_planner` (AD-049).
3
+ *
4
+ * The desktop app writes its ranked plan to `kv_store` under
5
+ * `upgrade_plan_snapshot` after every plan compute (the same envelope
6
+ * `4da plan --json` prints). From schema 4 the envelope carries `steps`: a
7
+ * machine-readable work order keyed by plan item id, built in Rust from the
8
+ * same per-line targets as the item titles. This module only READS it — no
9
+ * range matching, target walking or scope grading is reimplemented here, so
10
+ * the agent gets the app's answer, not a second opinion.
11
+ *
12
+ * Fail-closed like the Rust reader: any schema version other than
13
+ * APP_PLAN_SCHEMA_VERSION is treated as absent and the caller falls back to
14
+ * its standalone heuristic.
15
+ */
16
+ import type { FourDADatabase } from "../db.js";
17
+ /** The snapshot schema this reader understands (Rust `PLAN_SCHEMA_VERSION`). */
18
+ export declare const APP_PLAN_SCHEMA_VERSION = 4;
19
+ export interface AppPlanSite {
20
+ project: string;
21
+ direct: boolean;
22
+ dev: boolean;
23
+ }
24
+ export interface AppPlanLine {
25
+ installed: string;
26
+ target: string | null;
27
+ upgrade_type: "patch" | "minor" | "major" | null;
28
+ clears_all_known: boolean;
29
+ sites: AppPlanSite[];
30
+ }
31
+ export interface AppPlanStep {
32
+ item_id: string;
33
+ ecosystem: string;
34
+ package: string;
35
+ lines: AppPlanLine[];
36
+ mechanism: "manifest_bump" | "lockfile_or_parent_update" | "mixed" | "no_fix";
37
+ advisory_ids: string[];
38
+ verification: string;
39
+ }
40
+ /** A step with the title and urgency of the plan item it belongs to. */
41
+ export interface AppPlanStepView extends AppPlanStep {
42
+ title: string | null;
43
+ urgency: string | null;
44
+ }
45
+ export interface AppPlanSnapshot {
46
+ schemaVersion: number;
47
+ generatedAt: string;
48
+ expiresAt: string;
49
+ generatorVersion: string | null;
50
+ sourceFreshness: string | null;
51
+ multiVersionCoverage: boolean | null;
52
+ validationDropCount: number | null;
53
+ itemCount: number;
54
+ steps: AppPlanStepView[];
55
+ }
56
+ export type AppPlanRead = {
57
+ kind: "plan";
58
+ snapshot: AppPlanSnapshot;
59
+ stale: boolean;
60
+ } | {
61
+ kind: "absent";
62
+ reason: string;
63
+ };
64
+ /**
65
+ * Read the app's plan from the shared database. Never throws: a missing
66
+ * database, a standalone database without `kv_store`, no snapshot, bad JSON
67
+ * or another schema version all come back as `absent` with the reason.
68
+ */
69
+ export declare function readAppPlan(db: FourDADatabase | null | undefined, now?: Date): AppPlanRead;
70
+ export interface AppPlanResult {
71
+ generatedAt: string;
72
+ expiresAt: string;
73
+ /** True past `expiresAt`: the security data the plan read may have moved on. */
74
+ stale: boolean;
75
+ staleness: string | null;
76
+ totalSteps: number;
77
+ steps: AppPlanStepView[];
78
+ summary: string;
79
+ snapshot: Omit<AppPlanSnapshot, "steps" | "generatedAt" | "expiresAt">;
80
+ provenance: {
81
+ mode: "app_plan";
82
+ note: string;
83
+ };
84
+ }
85
+ /** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
86
+ export declare function formatAppPlan(read: Extract<AppPlanRead, {
87
+ kind: "plan";
88
+ }>, params: {
89
+ risk_threshold?: string;
90
+ max_recommendations?: number;
91
+ package?: string;
92
+ }): AppPlanResult;
@@ -0,0 +1,155 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * The 4DA app's persisted Upgrade Plan, read for `upgrade_planner` (AD-049).
4
+ *
5
+ * The desktop app writes its ranked plan to `kv_store` under
6
+ * `upgrade_plan_snapshot` after every plan compute (the same envelope
7
+ * `4da plan --json` prints). From schema 4 the envelope carries `steps`: a
8
+ * machine-readable work order keyed by plan item id, built in Rust from the
9
+ * same per-line targets as the item titles. This module only READS it — no
10
+ * range matching, target walking or scope grading is reimplemented here, so
11
+ * the agent gets the app's answer, not a second opinion.
12
+ *
13
+ * Fail-closed like the Rust reader: any schema version other than
14
+ * APP_PLAN_SCHEMA_VERSION is treated as absent and the caller falls back to
15
+ * its standalone heuristic.
16
+ */
17
+ /** The snapshot schema this reader understands (Rust `PLAN_SCHEMA_VERSION`). */
18
+ export const APP_PLAN_SCHEMA_VERSION = 4;
19
+ const PLAN_KV_KEY = "upgrade_plan_snapshot";
20
+ /**
21
+ * Read the app's plan from the shared database. Never throws: a missing
22
+ * database, a standalone database without `kv_store`, no snapshot, bad JSON
23
+ * or another schema version all come back as `absent` with the reason.
24
+ */
25
+ export function readAppPlan(db, now = new Date()) {
26
+ if (!db)
27
+ return { kind: "absent", reason: "no 4DA database" };
28
+ let raw;
29
+ try {
30
+ const row = db
31
+ .getRawDb()
32
+ .prepare("SELECT value FROM kv_store WHERE key = ?")
33
+ .get(PLAN_KV_KEY);
34
+ raw = typeof row?.value === "string" ? row.value : undefined;
35
+ }
36
+ catch {
37
+ return { kind: "absent", reason: "the database has no plan store (the 4DA app has not run on it)" };
38
+ }
39
+ if (raw === undefined) {
40
+ return { kind: "absent", reason: "the 4DA app has not computed an upgrade plan yet" };
41
+ }
42
+ let env;
43
+ try {
44
+ const parsed = JSON.parse(raw);
45
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
46
+ return { kind: "absent", reason: "the app's plan snapshot is unreadable" };
47
+ }
48
+ env = parsed;
49
+ }
50
+ catch {
51
+ return { kind: "absent", reason: "the app's plan snapshot is unreadable" };
52
+ }
53
+ const version = env.schema_version;
54
+ if (version !== APP_PLAN_SCHEMA_VERSION) {
55
+ return {
56
+ kind: "absent",
57
+ reason: `the app's plan snapshot is schema ${String(version)}, this server reads schema ${APP_PLAN_SCHEMA_VERSION}`,
58
+ };
59
+ }
60
+ if (!Array.isArray(env.steps) || !Array.isArray(env.items)) {
61
+ return { kind: "absent", reason: "the app's plan snapshot has no work order" };
62
+ }
63
+ if (typeof env.generated_at !== "string" || typeof env.expires_at !== "string") {
64
+ return { kind: "absent", reason: "the app's plan snapshot has no freshness stamp" };
65
+ }
66
+ const items = new Map();
67
+ for (const item of env.items) {
68
+ if (item && typeof item.id === "string")
69
+ items.set(item.id, item);
70
+ }
71
+ const steps = env.steps.map((step) => {
72
+ const item = items.get(step.item_id);
73
+ return { ...step, title: item?.title ?? null, urgency: item?.urgency ?? null };
74
+ });
75
+ const expires = Date.parse(env.expires_at);
76
+ // An unparseable horizon cannot prove freshness: treat it as stale.
77
+ const stale = Number.isNaN(expires) || now.getTime() > expires;
78
+ return {
79
+ kind: "plan",
80
+ stale,
81
+ snapshot: {
82
+ schemaVersion: APP_PLAN_SCHEMA_VERSION,
83
+ generatedAt: env.generated_at,
84
+ expiresAt: env.expires_at,
85
+ generatorVersion: typeof env.generator_version === "string" ? env.generator_version : null,
86
+ sourceFreshness: typeof env.source_freshness === "string" ? env.source_freshness : null,
87
+ multiVersionCoverage: typeof env.multi_version_coverage === "boolean" ? env.multi_version_coverage : null,
88
+ validationDropCount: typeof env.validation_drop_count === "number" ? env.validation_drop_count : null,
89
+ itemCount: items.size,
90
+ steps,
91
+ },
92
+ };
93
+ }
94
+ const URGENCY_RISK = { critical: 3, high: 2, medium: 1, watch: 0 };
95
+ const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
96
+ const APP_PROVENANCE_NOTE = "The 4DA desktop app's own cross-project, version-confirmed Upgrade Plan, read from its database " +
97
+ "(the same plan the app shows and `4da plan --json` prints). Each step is keyed by the app's item id.";
98
+ /** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
99
+ export function formatAppPlan(read, params) {
100
+ const { snapshot, stale } = read;
101
+ const threshold = params.risk_threshold ?? "all";
102
+ let steps = snapshot.steps;
103
+ // `package` narrows the app's plan the same way it narrows the standalone one.
104
+ const wanted = params.package?.trim().toLowerCase();
105
+ if (wanted)
106
+ steps = steps.filter((s) => s.package.toLowerCase() === wanted);
107
+ if (threshold !== "all" && threshold in RISK_LEVELS) {
108
+ steps = steps.filter((s) => (URGENCY_RISK[s.urgency ?? ""] ?? 1) >= RISK_LEVELS[threshold]);
109
+ }
110
+ // The same default as the standalone plan (20). All 53 steps of a real plan
111
+ // came to ~11k tokens, past the size Claude Code warns about (2026-10-03).
112
+ const selected = steps.slice(0, params.max_recommendations ?? 20);
113
+ const count = (m) => selected.filter((s) => s.mechanism === m).length;
114
+ const majors = selected.filter((s) => s.lines.some((l) => l.upgrade_type === "major")).length;
115
+ const parts = [
116
+ `${selected.length} step${selected.length !== 1 ? "s" : ""} from the 4DA app's plan (generated ${snapshot.generatedAt})`,
117
+ ];
118
+ if (count("manifest_bump") > 0)
119
+ parts.push(`${count("manifest_bump")} manifest bump(s)`);
120
+ if (count("lockfile_or_parent_update") > 0) {
121
+ parts.push(`${count("lockfile_or_parent_update")} lockfile or parent update(s) (transitive only)`);
122
+ }
123
+ if (count("mixed") > 0)
124
+ parts.push(`${count("mixed")} mixed (direct in some projects, transitive in others)`);
125
+ if (count("no_fix") > 0)
126
+ parts.push(`${count("no_fix")} with no fix published`);
127
+ if (majors > 0)
128
+ parts.push(`${majors} cross a major version`);
129
+ if (selected.length < snapshot.steps.length) {
130
+ parts.push(`${snapshot.steps.length} in the full plan (raise max_recommendations for the rest)`);
131
+ }
132
+ const staleness = stale
133
+ ? `STALE: past its freshness horizon (${snapshot.expiresAt}). The advisories or installs may have changed since; open the 4DA app to recompute.`
134
+ : null;
135
+ if (staleness)
136
+ parts.push(staleness);
137
+ return {
138
+ generatedAt: snapshot.generatedAt,
139
+ expiresAt: snapshot.expiresAt,
140
+ stale,
141
+ staleness,
142
+ totalSteps: snapshot.steps.length,
143
+ steps: selected,
144
+ summary: parts.join(". ") + ".",
145
+ snapshot: {
146
+ schemaVersion: snapshot.schemaVersion,
147
+ generatorVersion: snapshot.generatorVersion,
148
+ sourceFreshness: snapshot.sourceFreshness,
149
+ multiVersionCoverage: snapshot.multiVersionCoverage,
150
+ validationDropCount: snapshot.validationDropCount,
151
+ itemCount: snapshot.itemCount,
152
+ },
153
+ provenance: { mode: "app_plan", note: APP_PROVENANCE_NOTE },
154
+ };
155
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Advisory-database rows in the 4DA feed that name a package a task touches.
3
+ *
4
+ * Only the feed's advisory sources count (`cve`, `osv`): their titles name the
5
+ * affected package. A headline that happens to contain a package name is not
6
+ * an advisory — that is how OpenAI-the-company news became "Critical: Security
7
+ * issue affects openai" (2026-10-01). Rows the relevance judge rejected
8
+ * (`feed_relevant = 0`) are left out. These rows are NOT matched to installed
9
+ * versions; the briefing says so, and with a ready scan keeps them only for
10
+ * packages the scan confirms vulnerable.
11
+ */
12
+ import type { FourDADatabase } from "../db.js";
13
+ import type { TaskPackage } from "./briefing-task-scope.js";
14
+ export interface StoredAdvisory {
15
+ package: string;
16
+ title: string;
17
+ url: string | null;
18
+ priority: string;
19
+ }
20
+ export declare function storedAdvisoriesFor(db: FourDADatabase, packages: TaskPackage[], limit?: number): StoredAdvisory[];
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Advisory-database rows in the 4DA feed that name a package a task touches.
4
+ *
5
+ * Only the feed's advisory sources count (`cve`, `osv`): their titles name the
6
+ * affected package. A headline that happens to contain a package name is not
7
+ * an advisory — that is how OpenAI-the-company news became "Critical: Security
8
+ * issue affects openai" (2026-10-01). Rows the relevance judge rejected
9
+ * (`feed_relevant = 0`) are left out. These rows are NOT matched to installed
10
+ * versions; the briefing says so, and with a ready scan keeps them only for
11
+ * packages the scan confirms vulnerable.
12
+ */
13
+ import { normalizeStoredPriority } from "./signal-classifier.js";
14
+ const ADVISORY_SOURCES = ["cve", "osv"];
15
+ export function storedAdvisoriesFor(db, packages, limit = 5) {
16
+ if (packages.length === 0)
17
+ return [];
18
+ const raw = db.getRawDb();
19
+ const hasFeedRelevant = (() => {
20
+ try {
21
+ return raw.prepare("PRAGMA table_info(source_items)").all().some((c) => c.name === "feed_relevant");
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ })();
27
+ const out = [];
28
+ for (const pkg of packages) {
29
+ let rows;
30
+ try {
31
+ rows = raw
32
+ .prepare(`SELECT title, url, signal_priority FROM source_items
33
+ WHERE source_type IN (${ADVISORY_SOURCES.map(() => "?").join(", ")})
34
+ AND created_at > datetime('now', '-90 days')
35
+ AND LOWER(title) LIKE ?
36
+ ${hasFeedRelevant ? "AND (feed_relevant IS NULL OR feed_relevant = 1)" : ""}
37
+ ORDER BY created_at DESC LIMIT 20`)
38
+ .all(...ADVISORY_SOURCES, `%${pkg.name.toLowerCase()}%`);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ const word = new RegExp(`(^|[^\\w@/-])${pkg.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "i");
44
+ for (const row of rows) {
45
+ if (!word.test(row.title))
46
+ continue;
47
+ out.push({
48
+ package: pkg.name,
49
+ title: row.title,
50
+ url: row.url,
51
+ priority: row.signal_priority ? normalizeStoredPriority(row.signal_priority) : "medium",
52
+ });
53
+ if (out.length >= limit)
54
+ return out;
55
+ }
56
+ }
57
+ return out;
58
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Which of this project's dependencies a task touches, and what is known
3
+ * about each — the core of the what_should_i_know briefing.
4
+ *
5
+ * Measured 2026-10-01: asked about "bump fastembed 5 -> 7", the briefing
6
+ * returned eleven advisories and none about fastembed. It never asked which
7
+ * packages the task names; it merged feed items by recency and keyword class,
8
+ * so a 7-day-old `fastembed v7.1.0` release row was outside its 72-hour window
9
+ * while an arXiv dataset paper and three OpenAI-company headlines were in.
10
+ * This module starts from the task instead: the dependencies it names (exact
11
+ * package names from the resolved dependency set), then facts about exactly
12
+ * those packages.
13
+ */
14
+ import type { FourDADatabase } from "../db.js";
15
+ import type { ResolvedDependency, VulnerabilityEntry } from "../live/types.js";
16
+ /** A dependency the task names, with every installed copy. */
17
+ export interface TaskPackage {
18
+ name: string;
19
+ ecosystem: string;
20
+ installed: string[];
21
+ direct: boolean;
22
+ dev: boolean;
23
+ /** Versions the task text names after the package ("5.x to 7.x" -> from 5.x, to 7.x). */
24
+ requested_from: string | null;
25
+ requested_to: string | null;
26
+ }
27
+ /**
28
+ * Package names that are also everyday words. One of these counts as named
29
+ * only with a qualifier beside it — "the next crate", "`next`", "upgrade
30
+ * next to 15" — so "the next step" does not drag the `next` package in.
31
+ */
32
+ export declare const COMMON_WORDS: Set<string>;
33
+ /**
34
+ * The dependencies a task (and its file list) names. Exact names only; a
35
+ * common-word name needs a qualifier within two tokens or code quoting.
36
+ * Direct dependencies first; at most `limit` packages.
37
+ */
38
+ export declare function detectTaskPackages(task: string, files: string[], deps: ResolvedDependency[], limit?: number): TaskPackage[];
39
+ /** Major version of a version or a `7.x` / `v7` request; null when unreadable. */
40
+ export declare function majorOf(version: string | null): number | null;
41
+ /** Actionable advisories on the installed copies of one package, highest presented severity first. */
42
+ export declare function advisoriesFor(pkg: TaskPackage, vulns: VulnerabilityEntry[]): {
43
+ id: string;
44
+ installed: string;
45
+ severity: "critical" | "high" | "low" | "medium" | "unknown";
46
+ fixed_version: string | null;
47
+ summary: string;
48
+ }[];
49
+ /**
50
+ * Registry release rows the desktop app's feed holds for one package, newer
51
+ * than the installed version. Matched on the registry's own title format
52
+ * (`crates.io: fastembed v7.1.0`), never on a free-text mention. Empty in
53
+ * standalone mode (no feed) or for ecosystems the feed does not watch.
54
+ */
55
+ export declare function releasesFor(db: FourDADatabase, pkg: TaskPackage): Array<{
56
+ version: string;
57
+ seen: string;
58
+ url: string | null;
59
+ }>;
60
+ /**
61
+ * Feed items about the task's packages that the app's relevance judge
62
+ * accepted (latest `llm_judgments` verdict >= 0.5) and that the feed kept
63
+ * (`feed_relevant` = 1). Exact package-name match on a word boundary in the
64
+ * title. Third-party text: reported as data. Empty without the desktop feed.
65
+ */
66
+ export declare function judgedReadingFor(db: FourDADatabase, packages: TaskPackage[], limit?: number): {
67
+ id: number;
68
+ title: string;
69
+ url: string | null;
70
+ source: string;
71
+ judge_relevance: number;
72
+ package: string;
73
+ }[];