devsmind-mcp 2.2.2 → 2.4.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 (54) hide show
  1. package/README.md +234 -606
  2. package/dist/cli/analyze.d.ts +13 -0
  3. package/dist/cli/analyze.js +143 -0
  4. package/dist/cli/analyze.js.map +1 -0
  5. package/dist/cli/index.js +62 -0
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cli/init.js +9 -0
  8. package/dist/cli/init.js.map +1 -1
  9. package/dist/cli/integrations/memory.js +9 -3
  10. package/dist/cli/integrations/memory.js.map +1 -1
  11. package/dist/cli/integrations/prompt.d.ts +2 -0
  12. package/dist/cli/integrations/prompt.js +21 -7
  13. package/dist/cli/integrations/prompt.js.map +1 -1
  14. package/dist/cli/prune.js +4 -3
  15. package/dist/cli/prune.js.map +1 -1
  16. package/dist/cli/rule.js +43 -83
  17. package/dist/cli/rule.js.map +1 -1
  18. package/dist/cli/sync.d.ts +7 -0
  19. package/dist/cli/sync.js +40 -7
  20. package/dist/cli/sync.js.map +1 -1
  21. package/dist/cli/workflow.d.ts +8 -0
  22. package/dist/cli/workflow.js +156 -0
  23. package/dist/cli/workflow.js.map +1 -0
  24. package/dist/db/analyze.d.ts +67 -0
  25. package/dist/db/analyze.js +167 -0
  26. package/dist/db/analyze.js.map +1 -0
  27. package/dist/db/database.d.ts +195 -2
  28. package/dist/db/database.js +870 -74
  29. package/dist/db/database.js.map +1 -1
  30. package/dist/db/schema.d.ts +28 -1
  31. package/dist/db/schema.js +34 -0
  32. package/dist/db/schema.js.map +1 -1
  33. package/dist/db/staging.d.ts +4 -0
  34. package/dist/db/staging.js +16 -2
  35. package/dist/db/staging.js.map +1 -1
  36. package/dist/db/workflow-import.d.ts +22 -0
  37. package/dist/db/workflow-import.js +116 -0
  38. package/dist/db/workflow-import.js.map +1 -0
  39. package/dist/mcp/server.d.ts +1 -1
  40. package/dist/mcp/server.js +641 -48
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/utils/ast.d.ts +98 -0
  43. package/dist/utils/ast.js +262 -10
  44. package/dist/utils/ast.js.map +1 -1
  45. package/dist/utils/config.d.ts +2 -0
  46. package/dist/utils/config.js +11 -0
  47. package/dist/utils/config.js.map +1 -1
  48. package/dist/utils/edit.d.ts +41 -0
  49. package/dist/utils/edit.js +163 -0
  50. package/dist/utils/edit.js.map +1 -0
  51. package/dist/utils/git.d.ts +14 -0
  52. package/dist/utils/git.js +43 -0
  53. package/dist/utils/git.js.map +1 -0
  54. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ import { DevMindDatabase } from './database';
2
+ export interface AnalysisOptions {
3
+ fix?: boolean;
4
+ godEntityThreshold?: number;
5
+ }
6
+ export interface AnalysisReport {
7
+ fixed: boolean;
8
+ summary: Record<string, number>;
9
+ god_entities: {
10
+ id: string;
11
+ name: string;
12
+ file_path: string;
13
+ degree: number;
14
+ }[];
15
+ circular_dependencies: string[][];
16
+ orphaned_nodes: {
17
+ id: string;
18
+ name: string;
19
+ file_path: string;
20
+ }[];
21
+ dangling_edges: {
22
+ source_node_id: string;
23
+ target_node_id: string;
24
+ }[];
25
+ duplicate_ids: {
26
+ lower_id: string;
27
+ ids: string[];
28
+ }[];
29
+ missing_developer_attribution: {
30
+ id: string;
31
+ node_id: string;
32
+ updated_at: string;
33
+ }[];
34
+ empty_code_snapshots: {
35
+ id: string;
36
+ node_id: string;
37
+ updated_at: string;
38
+ }[];
39
+ spurious_nodes: {
40
+ id: string;
41
+ name: string;
42
+ file_path: string;
43
+ }[];
44
+ missing_files: {
45
+ id: string;
46
+ name: string;
47
+ file_path: string;
48
+ }[];
49
+ renamed_files: {
50
+ repo: string;
51
+ from: string;
52
+ to: string;
53
+ migrated: boolean;
54
+ }[];
55
+ untracked_files: {
56
+ repo: string;
57
+ file: string;
58
+ }[];
59
+ }
60
+ /**
61
+ * Runs every local, zero-AI health check against the graph and — when `fix` is true —
62
+ * applies only the safe/reversible fixes (soft-deprecate dead nodes, delete dangling
63
+ * edges, migrate renames). Everything else is report-only: god entities, cycles,
64
+ * duplicate ids, missing developer attribution, empty snapshots, and untracked files
65
+ * all need human/AI judgement a mechanical fixer shouldn't make on its own.
66
+ */
67
+ export declare function runAnalysis(db: DevMindDatabase, workspaceRoot: string, opts?: AnalysisOptions): AnalysisReport;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runAnalysis = runAnalysis;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const config_1 = require("../utils/config");
40
+ const git_1 = require("../utils/git");
41
+ const scanner_1 = require("../utils/scanner");
42
+ const LAST_ANALYSIS_KEY = 'last_analysis_at';
43
+ const DEFAULT_LOOKBACK_DAYS = 30;
44
+ /**
45
+ * Runs every local, zero-AI health check against the graph and — when `fix` is true —
46
+ * applies only the safe/reversible fixes (soft-deprecate dead nodes, delete dangling
47
+ * edges, migrate renames). Everything else is report-only: god entities, cycles,
48
+ * duplicate ids, missing developer attribution, empty snapshots, and untracked files
49
+ * all need human/AI judgement a mechanical fixer shouldn't make on its own.
50
+ */
51
+ function runAnalysis(db, workspaceRoot, opts = {}) {
52
+ const fix = opts.fix === true;
53
+ const godEntityThreshold = opts.godEntityThreshold ?? 15;
54
+ const god_entities = db.getGodEntities(godEntityThreshold);
55
+ const circular_dependencies = db.getCircularDependencies();
56
+ const orphaned_nodes = db.getOrphanedNodes().map(n => ({ id: n.id, name: n.name, file_path: n.file_path }));
57
+ const dangling_edges = db.getDanglingEdges().map(c => ({ source_node_id: c.source_node_id, target_node_id: c.target_node_id }));
58
+ const duplicate_ids = db.getDuplicateNodeIds().map(d => ({ lower_id: d.lowerId, ids: d.ids }));
59
+ const missing_developer_attribution = db.getHistoryMissingDeveloper();
60
+ const empty_code_snapshots = db.getEmptyCodeSnapshots();
61
+ const { spurious: spurious_nodes, missingFile: missing_files } = db.findSpuriousAndMissingFileNodes(workspaceRoot);
62
+ const lastAnalysisAt = db.getSystemMeta(LAST_ANALYSIS_KEY);
63
+ const lookbackIso = lastAnalysisAt || new Date(Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1000).toISOString();
64
+ const renamed_files = [];
65
+ const untracked_files = [];
66
+ const context = db.getContext();
67
+ if (context) {
68
+ for (const repo of context.config.repos) {
69
+ const repoPath = (0, config_1.resolveRepoPath)(context, repo.name);
70
+ if (!repoPath || !fs.existsSync(repoPath))
71
+ continue;
72
+ for (const r of (0, git_1.getRenamedFilesSince)(repoPath, lookbackIso)) {
73
+ renamed_files.push({ repo: repo.name, from: r.from, to: r.to, migrated: false });
74
+ }
75
+ const changed = (0, git_1.getChangedFilesSince)(repoPath, lookbackIso);
76
+ // Canonicalized on the way in: a raw path.resolve() comparison misses on Windows whenever
77
+ // the two sides disagree on drive-letter case (a stored node path vs. one freshly resolved
78
+ // from cwd), which silently reported every file in the repo as untracked — same bug class
79
+ // already fixed for getNodesByFilePath, not previously applied here.
80
+ const knownFiles = new Set(db.listNodes({ include_deprecated: true }).flatMap(n => (n.file_path || '').split(',').map(p => (0, config_1.canonicalizePath)(p.trim())).filter(Boolean)));
81
+ for (const relFile of changed) {
82
+ const ext = path.extname(relFile).toLowerCase();
83
+ if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext))
84
+ continue;
85
+ const absFile = (0, config_1.canonicalizePath)(path.resolve(repoPath, relFile));
86
+ if (!fs.existsSync(absFile))
87
+ continue; // deleted since — not a blind spot
88
+ const hasNode = knownFiles.has(absFile);
89
+ if (!hasNode)
90
+ untracked_files.push({ repo: repo.name, file: relFile });
91
+ }
92
+ }
93
+ }
94
+ if (fix) {
95
+ for (const n of [...orphaned_nodes, ...spurious_nodes, ...missing_files]) {
96
+ db.deprecateNode(n.id);
97
+ }
98
+ for (const e of dangling_edges) {
99
+ db.deleteDanglingEdge(e.source_node_id, e.target_node_id);
100
+ }
101
+ if (context) {
102
+ for (const r of renamed_files) {
103
+ const migrated = migrateRename(db, context, r.repo, r.from, r.to);
104
+ r.migrated = migrated;
105
+ }
106
+ }
107
+ db.setSystemMeta(LAST_ANALYSIS_KEY, new Date().toISOString());
108
+ }
109
+ const summary = {
110
+ god_entities: god_entities.length,
111
+ circular_dependencies: circular_dependencies.length,
112
+ orphaned_nodes: orphaned_nodes.length,
113
+ dangling_edges: dangling_edges.length,
114
+ duplicate_ids: duplicate_ids.length,
115
+ missing_developer_attribution: missing_developer_attribution.length,
116
+ empty_code_snapshots: empty_code_snapshots.length,
117
+ spurious_nodes: spurious_nodes.length,
118
+ missing_files: missing_files.length,
119
+ renamed_files: renamed_files.length,
120
+ untracked_files: untracked_files.length,
121
+ };
122
+ return {
123
+ fixed: fix,
124
+ summary,
125
+ god_entities,
126
+ circular_dependencies,
127
+ orphaned_nodes,
128
+ dangling_edges,
129
+ duplicate_ids,
130
+ missing_developer_attribution,
131
+ empty_code_snapshots,
132
+ spurious_nodes,
133
+ missing_files,
134
+ renamed_files,
135
+ untracked_files,
136
+ };
137
+ }
138
+ /** Migrates every node whose file_path resolves to `from` onto `to`, cascading the id/connections/history via `renameNode`. Returns true if anything was migrated. */
139
+ function migrateRename(db, context, repoName, from, to) {
140
+ if (!context)
141
+ return false;
142
+ const repoPath = (0, config_1.resolveRepoPath)(context, repoName);
143
+ if (!repoPath)
144
+ return false;
145
+ const oldAbs = path.resolve(repoPath, from);
146
+ const newAbs = path.resolve(repoPath, to);
147
+ // Deliberately excludes deprecated nodes — renameNode's insert doesn't preserve the
148
+ // deprecated flag, so migrating a dead node would silently resurrect it as active.
149
+ // A deprecated node's file_path accuracy doesn't matter once it's already excluded
150
+ // from use.
151
+ const affected = db.listNodes().filter(n => (n.file_path || '').split(',').map(p => p.trim()).some(p => path.resolve(p) === oldAbs));
152
+ let migrated = false;
153
+ for (const node of affected) {
154
+ const newId = node.id.replace(from, to);
155
+ if (newId === node.id)
156
+ continue;
157
+ try {
158
+ db.renameNode(node.id, newId, undefined, newAbs);
159
+ migrated = true;
160
+ }
161
+ catch {
162
+ // Leave this node untouched — analyze reports it again next run rather than failing the whole batch.
163
+ }
164
+ }
165
+ return migrated;
166
+ }
167
+ //# sourceMappingURL=analyze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/db/analyze.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { DbNode, DbHistory, DbConnection } from './schema';
1
+ import { DbNode, DbHistory, DbConnection, DbWorkflow, DbWorkflowStep, DbWorkflowArtifact } from './schema';
2
+ import { ProjectContext } from '../utils/config';
2
3
  export interface ReasoningObject {
3
4
  what_changed: string;
4
5
  why: string;
@@ -42,14 +43,31 @@ export interface GraphResult {
42
43
  nodes_without_code?: number;
43
44
  }
44
45
  export declare function formatReasoning(r: string | ReasoningObject): string;
46
+ /**
47
+ * Inverse of `formatReasoning`. A single history row accumulates every later update appended
48
+ * under a `── Update @ … ──` separator, so one stored blob can hold several changes — this
49
+ * splits them back apart and returns them NEWEST FIRST.
50
+ *
51
+ * Reasoning written before the structured format (or by a caller passing a bare string) has no
52
+ * labels to read; rather than drop it, the whole chunk is surfaced as `what_changed`.
53
+ */
54
+ export declare function parseReasoningBlocks(raw: string): ReasoningObject[];
45
55
  export declare class DevMindDatabase {
46
56
  private db;
47
57
  private dbPath;
48
58
  private context;
49
59
  constructor(dbPath: string);
50
60
  private initSchema;
61
+ getContext(): ProjectContext | null;
51
62
  getSystemMeta(key: string): string | null;
52
63
  setSystemMeta(key: string, value: string): void;
64
+ /**
65
+ * Nodes declared in one file. Both sides are folded to a canonical form before comparing:
66
+ * a stored `c:\x\y.ts` and a caller's `C:/x/y.ts` are the same file on Windows, and a raw
67
+ * `=` match silently returns nothing — which reads as "this file has no nodes" rather than
68
+ * as an error. There is no index on file_path, so this was already a full scan; normalizing
69
+ * in SQL costs nothing extra.
70
+ */
53
71
  getNodesByFilePath(filePath: string): DbNode[];
54
72
  close(): void;
55
73
  /** Snapshot of active-node / connection / history row counts (used by `devsmind sync`). */
@@ -88,7 +106,8 @@ export declare class DevMindDatabase {
88
106
  getNode(id: string): DbNode | null;
89
107
  deleteNode(id: string): void;
90
108
  deprecateNode(id: string): void;
91
- renameNode(oldId: string, newId: string, newName?: string): void;
109
+ /** `newFilePath`: pass when the rename is a file move (analyze's rename migration), leave undefined for a pure symbol-id rename where the file itself is unchanged. */
110
+ renameNode(oldId: string, newId: string, newName?: string, newFilePath?: string): void;
92
111
  /**
93
112
  * Rewrites a history/[id].json file's identifying fields (node_id, node_metadata) in
94
113
  * place, leaving code_snapshot/reasoning/timestamps untouched. Used after a rename so
@@ -167,7 +186,27 @@ export declare class DevMindDatabase {
167
186
  matched_via: 'identifier';
168
187
  }) | (ReturnType<DevMindDatabase['searchCode']>[number] & {
169
188
  matched_via: 'code';
189
+ }) | (DbNode & {
190
+ matched_via: 'fuzzy';
191
+ matched_terms: string[];
192
+ score: number;
170
193
  })>;
194
+ /**
195
+ * Splits a query string into lowercase word tokens for the fuzzy fallback
196
+ * stage of {@link searchNodes}. This is request-scoped tokenization only —
197
+ * nothing is persisted or indexed; the result is discarded after the call.
198
+ */
199
+ private tokenizeQuery;
200
+ /**
201
+ * Word-split relevance-ranked fallback for {@link searchNodes}. Runs only
202
+ * when the exact identifier and code stages both return nothing. Scores
203
+ * every non-deprecated node by how many distinct query tokens appear as a
204
+ * substring of its file_path/name/id (highest signal), latest reasoning,
205
+ * or code content (lowest signal, one point per matching line). No new
206
+ * data is written or synced — this is a plain in-memory scan reusing the
207
+ * same node/history sources searchCode already reads.
208
+ */
209
+ private fuzzySearchNodes;
171
210
  getRecentChanges(hours?: number, analyzeImpact?: boolean): {
172
211
  node_id: string;
173
212
  node_name: string;
@@ -224,6 +263,135 @@ export declare class DevMindDatabase {
224
263
  }): DbNode[];
225
264
  getAllConnections(): DbConnection[];
226
265
  getAllHistory(): DbHistory[];
266
+ /** Nodes whose total (in + out) connection degree meets/exceeds `threshold` — architectural bottleneck candidates. */
267
+ getGodEntities(threshold?: number): {
268
+ id: string;
269
+ name: string;
270
+ file_path: string;
271
+ degree: number;
272
+ }[];
273
+ /** DFS cycle detection over the connection graph, capped at `maxCycles` reported paths. */
274
+ getCircularDependencies(maxCycles?: number): string[][];
275
+ /** node_connections rows whose source or target no longer exists in `nodes` (broken by a non-transactional delete, or a sync race). */
276
+ getDanglingEdges(): DbConnection[];
277
+ /** Deletes a single dangling `node_connections` row. The edge itself is invalid data — no history/graph JSON to rewrite. */
278
+ deleteDanglingEdge(sourceId: string, targetId: string): void;
279
+ /** Node ids that differ only by case — a real collision risk on Windows's case-insensitive filesystem. */
280
+ getDuplicateNodeIds(): {
281
+ lowerId: string;
282
+ ids: string[];
283
+ }[];
284
+ /** History rows whose flattened `reasoning` text has no non-empty `Developer:` line — can't be attributed to anyone. */
285
+ getHistoryMissingDeveloper(): {
286
+ id: string;
287
+ node_id: string;
288
+ updated_at: string;
289
+ }[];
290
+ /**
291
+ * History rows with a blank code snapshot — usually a silent AST extraction failure.
292
+ * The `history.code_snapshot` DB column is always written as `''` (the real content
293
+ * lives only in the per-row JSON on disk, see `populateHistoryFromDisk`), so this
294
+ * must read through the populated rows rather than querying the column directly.
295
+ */
296
+ getEmptyCodeSnapshots(): {
297
+ id: string;
298
+ node_id: string;
299
+ updated_at: string;
300
+ }[];
301
+ private workflowsDir;
302
+ /** Serializes the workflow + its steps + artifact index to disk so teammates can sync it via git. */
303
+ private writeWorkflowToDisk;
304
+ createWorkflow(name: string, description: string): DbWorkflow;
305
+ getWorkflow(id: string): DbWorkflow | null;
306
+ getActiveWorkflow(): DbWorkflow | null;
307
+ listWorkflows(status?: 'active' | 'paused' | 'completed'): DbWorkflow[];
308
+ /** Pauses the currently active workflow (if any) and clears the active pointer. */
309
+ pauseWorkflow(): DbWorkflow | null;
310
+ /** Resumes `id`, auto-pausing whatever was previously active (only one workflow is active at a time). */
311
+ resumeWorkflow(id: string): DbWorkflow;
312
+ completeWorkflow(id: string): DbWorkflow;
313
+ addWorkflowStep(workflowId: string, opts: {
314
+ summary: string;
315
+ pendingTasks?: string;
316
+ historyIds?: string[];
317
+ sessionId?: string;
318
+ }): DbWorkflowStep;
319
+ /** Writes `content` to `.devmind/workflows/<workflowId>/<artifactId>_<sourceName>` and records the DB row. */
320
+ addWorkflowArtifact(workflowId: string, opts: {
321
+ stepId?: string;
322
+ type: string;
323
+ sourceName: string;
324
+ content: string;
325
+ }): DbWorkflowArtifact;
326
+ getWorkflowContext(id: string, opts?: {
327
+ includeArtifactContent?: boolean;
328
+ }): {
329
+ workflow: DbWorkflow;
330
+ steps: DbWorkflowStep[];
331
+ artifacts: (DbWorkflowArtifact & {
332
+ content?: string;
333
+ })[];
334
+ };
335
+ /**
336
+ * Returns steps for a workflow with optional pagination.
337
+ * Use `last_n` to get only the most recent N steps (tail), or `limit`/`offset` for
338
+ * arbitrary pagination. Without any option, all steps are returned.
339
+ */
340
+ getWorkflowSteps(workflowId: string, opts?: {
341
+ limit?: number;
342
+ offset?: number;
343
+ last_n?: number;
344
+ }): DbWorkflowStep[];
345
+ /**
346
+ * Reads a single workflow artifact's file content from disk.
347
+ * Accepts either an artifact_id or a source_name (first match used).
348
+ */
349
+ readWorkflowArtifact(workflowId: string, artifactId: string): {
350
+ artifact: DbWorkflowArtifact;
351
+ content: string;
352
+ };
353
+ /**
354
+ * Full-text keyword search across all workflows' step summaries, pending_tasks,
355
+ * and artifact source names. Optionally also searches artifact file content.
356
+ * Returns a list of matches grouped by workflow.
357
+ */
358
+ searchWorkflows(query: string, opts?: {
359
+ include_artifact_content?: boolean;
360
+ status?: 'active' | 'paused' | 'completed';
361
+ }): Array<{
362
+ workflow: DbWorkflow;
363
+ matched_steps: DbWorkflowStep[];
364
+ matched_artifacts: (DbWorkflowArtifact & {
365
+ content_snippet?: string;
366
+ })[];
367
+ }>;
368
+ /**
369
+ * Imports an existing flow/architecture doc as a paused workflow (not active — importing
370
+ * a doc isn't the same as declaring active work). Idempotent on `name`: re-importing the
371
+ * same doc overwrites its existing `imported_doc` artifact file in place instead of
372
+ * creating a duplicate workflow every time the source docs are re-imported.
373
+ */
374
+ importWorkflowDoc(name: string, description: string, content: string, sourceFileName: string): {
375
+ workflow: DbWorkflow;
376
+ created: boolean;
377
+ };
378
+ private static readonly SPURIOUS_NODE_NAMES;
379
+ /**
380
+ * Read-only detection shared by `pruneSpuriousNodes` (which acts on it) and `devsmind
381
+ * analyze`'s dry-run report (which just lists it). Never mutates the DB.
382
+ */
383
+ findSpuriousAndMissingFileNodes(workspaceRoot: string): {
384
+ spurious: {
385
+ id: string;
386
+ name: string;
387
+ file_path: string;
388
+ }[];
389
+ missingFile: {
390
+ id: string;
391
+ name: string;
392
+ file_path: string;
393
+ }[];
394
+ };
227
395
  pruneSpuriousNodes(workspaceRoot: string): {
228
396
  prunedCount: number;
229
397
  prunedNodes: string[];
@@ -231,9 +399,34 @@ export declare class DevMindDatabase {
231
399
  private populateHistoryFromDisk;
232
400
  private writeHistoryToDisk;
233
401
  toRepoRelativePath(absolutePath: string): string;
402
+ /**
403
+ * Rejects a resolved path that escapes its expected root (e.g. via a stored
404
+ * `{repo}/../../..` path traveling outside the repo) by clamping it back to
405
+ * the root itself. node_id/file_path values flow in from AI-supplied tool
406
+ * calls, so a resolve must never be trusted to stay inside root on its own.
407
+ */
408
+ private clampToRoot;
409
+ /**
410
+ * True if `absPath` sits inside a configured repo root or the workspace root itself.
411
+ * Used to reject `stage_change`/`update_history` file paths that would otherwise let a
412
+ * tool call read/write any file on disk (absolute path, or a `../` escape) instead of
413
+ * just repo source — nothing upstream of this validates that the AI-supplied path is
414
+ * actually inside the project.
415
+ */
416
+ /**
417
+ * Gate for every AI-facing write (edit_node, stage_change, the legacy update_history):
418
+ * true only for paths inside a configured repo. `.devmind` itself — this project's OWN
419
+ * config, brain.db, and cached graph JSON — is never writable through these tools, even
420
+ * though it sits next to (and, before this check, was indistinguishable from) real source:
421
+ * without this, a write tool built to "never refuse a file type" would just as happily
422
+ * rewrite devsmind's own config.json as it would application source.
423
+ */
424
+ isPathAllowed(absPath: string): boolean;
234
425
  toAbsolutePath(repoRelativePath: string): string;
235
426
  syncFromDisk(): void;
236
427
  /** Escape LIKE metacharacters so a path is matched literally (use with ESCAPE '\\'). */
237
428
  private likeEscape;
238
429
  writeGraphToDisk(filePath: string): void;
430
+ /** Force-syncs all database nodes and workflows to disk JSON files. */
431
+ syncToDisk(): void;
239
432
  }