devsmind-mcp 2.4.0 → 4.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 (135) hide show
  1. package/README.md +317 -234
  2. package/dist/cli/activity.d.ts +9 -0
  3. package/dist/cli/activity.js +69 -0
  4. package/dist/cli/activity.js.map +1 -0
  5. package/dist/cli/describe.d.ts +31 -0
  6. package/dist/cli/describe.js +201 -0
  7. package/dist/cli/describe.js.map +1 -0
  8. package/dist/cli/diff.d.ts +14 -0
  9. package/dist/cli/diff.js +161 -0
  10. package/dist/cli/diff.js.map +1 -0
  11. package/dist/cli/embed.d.ts +6 -0
  12. package/dist/cli/embed.js +109 -0
  13. package/dist/cli/embed.js.map +1 -0
  14. package/dist/cli/extract-agent.d.ts +66 -0
  15. package/dist/cli/extract-agent.js +192 -0
  16. package/dist/cli/extract-agent.js.map +1 -0
  17. package/dist/cli/feedback.d.ts +11 -0
  18. package/dist/cli/feedback.js +90 -0
  19. package/dist/cli/feedback.js.map +1 -0
  20. package/dist/cli/index.js +139 -9
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/init.d.ts +30 -0
  23. package/dist/cli/init.js +89 -18
  24. package/dist/cli/init.js.map +1 -1
  25. package/dist/cli/integrations/memory-topics.d.ts +55 -0
  26. package/dist/cli/integrations/memory-topics.js +318 -0
  27. package/dist/cli/integrations/memory-topics.js.map +1 -0
  28. package/dist/cli/integrations/memory.d.ts +17 -6
  29. package/dist/cli/integrations/memory.js +52 -139
  30. package/dist/cli/integrations/memory.js.map +1 -1
  31. package/dist/cli/integrations/prompt.d.ts +8 -0
  32. package/dist/cli/integrations/prompt.js +24 -3
  33. package/dist/cli/integrations/prompt.js.map +1 -1
  34. package/dist/cli/integrations/registry.d.ts +26 -9
  35. package/dist/cli/integrations/registry.js +61 -30
  36. package/dist/cli/integrations/registry.js.map +1 -1
  37. package/dist/cli/llm-client.d.ts +116 -0
  38. package/dist/cli/llm-client.js +322 -0
  39. package/dist/cli/llm-client.js.map +1 -0
  40. package/dist/cli/rule.d.ts +22 -2
  41. package/dist/cli/rule.js +110 -34
  42. package/dist/cli/rule.js.map +1 -1
  43. package/dist/cli/runner.d.ts +21 -0
  44. package/dist/cli/runner.js +94 -276
  45. package/dist/cli/runner.js.map +1 -1
  46. package/dist/cli/sync-progress.d.ts +9 -0
  47. package/dist/cli/sync-progress.js +41 -0
  48. package/dist/cli/sync-progress.js.map +1 -0
  49. package/dist/cli/sync.js +5 -2
  50. package/dist/cli/sync.js.map +1 -1
  51. package/dist/cli/view.js +3 -2
  52. package/dist/cli/view.js.map +1 -1
  53. package/dist/cli/workflow.js +26 -20
  54. package/dist/cli/workflow.js.map +1 -1
  55. package/dist/db/activity-graph.d.ts +55 -0
  56. package/dist/db/activity-graph.js +314 -0
  57. package/dist/db/activity-graph.js.map +1 -0
  58. package/dist/db/activity.d.ts +221 -0
  59. package/dist/db/activity.js +348 -0
  60. package/dist/db/activity.js.map +1 -0
  61. package/dist/db/analyze.js +18 -6
  62. package/dist/db/analyze.js.map +1 -1
  63. package/dist/db/database.d.ts +654 -102
  64. package/dist/db/database.js +1911 -558
  65. package/dist/db/database.js.map +1 -1
  66. package/dist/db/edges.d.ts +24 -0
  67. package/dist/db/edges.js +84 -0
  68. package/dist/db/edges.js.map +1 -1
  69. package/dist/db/embedder.d.ts +39 -0
  70. package/dist/db/embedder.js +0 -0
  71. package/dist/db/embedder.js.map +1 -0
  72. package/dist/db/feedback.d.ts +128 -0
  73. package/dist/db/feedback.js +182 -0
  74. package/dist/db/feedback.js.map +1 -0
  75. package/dist/db/file-diff.d.ts +32 -0
  76. package/dist/db/file-diff.js +110 -0
  77. package/dist/db/file-diff.js.map +1 -0
  78. package/dist/db/grep.d.ts +78 -0
  79. package/dist/db/grep.js +475 -0
  80. package/dist/db/grep.js.map +1 -0
  81. package/dist/db/index-build.d.ts +75 -0
  82. package/dist/db/index-build.js +177 -0
  83. package/dist/db/index-build.js.map +1 -0
  84. package/dist/db/message-revert.d.ts +63 -0
  85. package/dist/db/message-revert.js +258 -0
  86. package/dist/db/message-revert.js.map +1 -0
  87. package/dist/db/revert.d.ts +31 -0
  88. package/dist/db/revert.js +108 -0
  89. package/dist/db/revert.js.map +1 -0
  90. package/dist/db/schema.d.ts +49 -4
  91. package/dist/db/schema.js +125 -73
  92. package/dist/db/schema.js.map +1 -1
  93. package/dist/db/search-index.d.ts +65 -0
  94. package/dist/db/search-index.js +74 -0
  95. package/dist/db/search-index.js.map +1 -0
  96. package/dist/db/staging.d.ts +91 -5
  97. package/dist/db/staging.js +148 -22
  98. package/dist/db/staging.js.map +1 -1
  99. package/dist/mcp/server.d.ts +29 -7
  100. package/dist/mcp/server.js +2637 -878
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
  103. package/dist/mcp/vendor/force-graph.min.js +5 -0
  104. package/dist/mcp/vendor/model/model_int8.onnx +0 -0
  105. package/dist/mcp/vendor/model/vocab.txt +30522 -0
  106. package/dist/mcp/vendor/three.min.js +7 -0
  107. package/dist/mcp/view.css +419 -0
  108. package/dist/mcp/view.html +161 -0
  109. package/dist/mcp/view.js +245 -0
  110. package/dist/mcp/view_chat.js +382 -0
  111. package/dist/mcp/view_graph.js +576 -0
  112. package/dist/mcp/visualizer.d.ts +25 -2
  113. package/dist/mcp/visualizer.js +31 -4
  114. package/dist/mcp/visualizer.js.map +1 -1
  115. package/dist/utils/ast.d.ts +89 -0
  116. package/dist/utils/ast.js +378 -10
  117. package/dist/utils/ast.js.map +1 -1
  118. package/dist/utils/diff.d.ts +44 -0
  119. package/dist/utils/diff.js +78 -0
  120. package/dist/utils/diff.js.map +1 -0
  121. package/dist/utils/edit.d.ts +6 -0
  122. package/dist/utils/edit.js +47 -14
  123. package/dist/utils/edit.js.map +1 -1
  124. package/dist/utils/scanner.d.ts +6 -4
  125. package/dist/utils/scanner.js +18 -8
  126. package/dist/utils/scanner.js.map +1 -1
  127. package/dist/utils/tokenize.d.ts +45 -0
  128. package/dist/utils/tokenize.js +129 -0
  129. package/dist/utils/tokenize.js.map +1 -0
  130. package/dist/utils/version.d.ts +14 -0
  131. package/dist/utils/version.js +61 -0
  132. package/dist/utils/version.js.map +1 -0
  133. package/package.json +18 -5
  134. package/dist/mcp/visualizer_2d.html +0 -635
  135. package/dist/mcp/visualizer_3d.html +0 -613
@@ -1,5 +1,6 @@
1
1
  import { DbNode, DbHistory, DbConnection, DbWorkflow, DbWorkflowStep, DbWorkflowArtifact } from './schema';
2
2
  import { ProjectContext } from '../utils/config';
3
+ import { RankedFile } from './grep';
3
4
  export interface ReasoningObject {
4
5
  what_changed: string;
5
6
  why: string;
@@ -23,24 +24,200 @@ export interface LiveCodeResult {
23
24
  updated_at?: string;
24
25
  message?: string;
25
26
  }
27
+ /** `depth` is hops from the root IN THE TRAVERSAL THAT FOUND IT, not a call-stack distance — with
28
+ * `direction:"both"` a path can alternate caller→callee, so it is not a directional "N calls
29
+ * deep" number. Absent only for the synthetic case where a node id was never resolvable
30
+ * (see `getGraph`'s dangling-edge handling), which never reaches the returned `nodes` array. */
26
31
  export type GraphNode = DbNode & {
27
32
  code?: string;
28
33
  code_source?: CodeSource;
34
+ depth?: number;
29
35
  };
36
+ /** One matching code-body line attached to a node found (or corroborated) by the grep layer. */
37
+ export interface CodeMatchLine {
38
+ line_number: number;
39
+ line_content: string;
40
+ }
41
+ /** Which search layer(s) surfaced a node — the honest trust signal. A node found by several
42
+ * independent layers is far more likely right than one found by a single weak one. */
43
+ export type SearchLayer = 'name' | 'keyword' | 'meaning' | 'code';
44
+ /** How much to trust a result, in plain terms — NOT the raw RRF fusion float, which is a tiny,
45
+ * uninterpretable number (a #1-ranked hit tops out around 0.03) that made every good match look
46
+ * like a weak guess. `high` = an exact identifier, ≥2 layers agreeing, or a strong semantic match;
47
+ * `low` = a single weak signal near the floor. */
48
+ export type Confidence = 'high' | 'medium' | 'low';
49
+ /** Bumped when the shape written to `.devmind/workflows/<id>/workflow.json` changes. v2 replaced
50
+ * per-step `history_ids`/`pending_tasks` with `reasoning`/`node_ids`/`doc_paths`, and workflow
51
+ * `status`/`is_active` with `archived`. */
52
+ export declare const WORKFLOW_SCHEMA_VERSION = 2;
53
+ /** The half of a workflow a v1 client has no field for, and therefore cannot overwrite when it
54
+ * rewrites `workflow.json` from its own columns. See `writeWorkflowToDisk`. */
55
+ export declare const WORKFLOW_SIDECAR_FILE = "v2.json";
56
+ export declare const NO_STATIC_CALLERS_NOTE = "no static callers found \u2014 may be used via dynamic dispatch or a generated binding; verify before assuming unused";
57
+ /** Drill-in hooks attached to every search result — the signal that tells the caller whether it's
58
+ * worth pulling in more graph/history in the same `get_node_code` call before this result is a
59
+ * dead end. */
60
+ export interface DrillInHooks {
61
+ /** Outgoing connection count (what this node calls/uses). */
62
+ uses: number;
63
+ /** Incoming connection count (who calls/uses this node). A found-by-static-AST count only — see
64
+ * `used_by_note`. */
65
+ used_by: number;
66
+ /** Total history/revision entries for this node. */
67
+ history_count: number;
68
+ /** Most recent history update timestamp, if any. */
69
+ last_updated?: string;
70
+ /** Present ONLY when `used_by === 0` — a confident "0 callers" on dynamic-dispatch or
71
+ * generated-binding code (RTK hooks, DI containers) is a false negative, not a real answer. This
72
+ * turns a misleading zero into an honest "unverified" instead of silently asserting "unused". */
73
+ used_by_note?: string;
74
+ }
75
+ /** A node in the primary `nodes` bucket of {@link DevMindDatabase.searchNodes}. Either an exact
76
+ * identifier hit, or a ranked hit fused from the BM25 / vector / code layers. */
77
+ export type RankedNode = (DbNode & DrillInHooks & {
78
+ matched_via: 'identifier';
79
+ found_by: SearchLayer[];
80
+ confidence: 'high';
81
+ relevance: number;
82
+ }) | (DbNode & DrillInHooks & {
83
+ matched_via: 'fuzzy' | 'semantic' | 'code';
84
+ /** Every layer that matched this node — corroboration across layers is the real confidence. */
85
+ found_by: SearchLayer[];
86
+ /** Plain-language trust level; replaces the old opaque `score`/`low_confidence` pair. */
87
+ confidence: Confidence;
88
+ /** 0-100, relative to the top hit in THIS response — an intuitive ordering aid, not an
89
+ * absolute probability (that's what `confidence` is for). */
90
+ relevance: number;
91
+ matched_terms: string[];
92
+ /** Present when the node's code body matched — the lines that did, for eyeballing. */
93
+ code_matches?: CodeMatchLine[];
94
+ });
95
+ /**
96
+ * The two-bucket result of {@link DevMindDatabase.searchNodes}: `nodes` (the indexed graph,
97
+ * primary) and `files` (raw filesystem grep hits, last resort — the coverage for CSS/JSON/config
98
+ * and anything the graph doesn't model). `hint` is set only when BOTH buckets are empty;
99
+ * `truncated` when the grep walk hit its deadline and returned partial file results.
100
+ *
101
+ * `files`/`nodes` stay BARE ARRAYS (never wrapped in `{total, items}`) — an empty search must
102
+ * still return `files: []`, not `files: {items: [], total: 0}`. The true counts before either
103
+ * bucket's cap ship as SIBLING fields instead, so a capped result stays distinguishable from a
104
+ * complete one without changing the shape of the arrays themselves.
105
+ */
106
+ export interface SearchNodesResult {
107
+ nodes: RankedNode[];
108
+ files: RankedFile[];
109
+ /** True total distinct files that matched, before the `files_offset`/page-size cap — lets the
110
+ * caller tell "that's everything" from "more exists, ask for the next page". */
111
+ files_total: number;
112
+ /** The offset this page of `files` started at (echoes back what was requested; default 0). */
113
+ files_offset: number;
114
+ /** True total ranked nodes found across all layers, before the top-20 fusion cap. */
115
+ nodes_total: number;
116
+ hint?: string;
117
+ truncated?: boolean;
118
+ /** Set only when an explicit `path` scope pointed AT a file excluded from search by default
119
+ * (a lockfile or build artifact). Distinct from `hint`, which means "nothing matched anywhere":
120
+ * this means "nothing was SCANNED", and no amount of re-querying will change that. */
121
+ scope_note?: string;
122
+ }
123
+ /** A `search_nodes` node after compaction — everything needed to decide "is this the one?" and
124
+ * nothing else. Field ORDER is deliberate and mirrors the full shape: trust signals first. */
125
+ export interface CompactRankedNode {
126
+ id: string;
127
+ name: string;
128
+ type: string;
129
+ confidence: Confidence;
130
+ relevance: number;
131
+ found_by: SearchLayer[];
132
+ file_path: string;
133
+ signature: string | null;
134
+ description: string | null;
135
+ uses: number;
136
+ used_by: number;
137
+ history_count: number;
138
+ code_matches?: CodeMatchLine[];
139
+ }
140
+ /** A `search_nodes` file after compaction. `match_counts` is the big drop: a map keyed by every
141
+ * distinct lowercased substring that matched, which is bulk without being a decision input. */
142
+ export interface CompactRankedFile {
143
+ file_path: string;
144
+ total_matches: number;
145
+ sample_lines?: {
146
+ line_number: number;
147
+ line_content: string;
148
+ symbol?: string;
149
+ }[];
150
+ }
151
+ /**
152
+ * The compacted form of {@link SearchNodesResult}. Every COUNT survives untouched — a trimmed
153
+ * result must never be mistakable for a complete one, the same contract the buckets already keep.
154
+ */
155
+ export interface CompactSearchNodesResult {
156
+ nodes: CompactRankedNode[];
157
+ files: CompactRankedFile[];
158
+ files_total: number;
159
+ files_offset: number;
160
+ nodes_total: number;
161
+ hint?: string;
162
+ truncated?: boolean;
163
+ scope_note?: string;
164
+ /** Which tier was applied, and what it cost — always present when compaction ran, so the caller
165
+ * can never mistake a trimmed response for the full one. */
166
+ compacted?: string;
167
+ }
168
+ /**
169
+ * Shrink a search result to fit, in two tiers.
170
+ *
171
+ * Why two rather than an on/off switch: the sample lines and `code_matches` are genuinely the
172
+ * most useful part of a result — real agent feedback credits them with catching a live bug — so
173
+ * throwing all of them away at the first byte over a threshold overcorrects. Tier 1 drops what is
174
+ * bulk-without-signal (`match_counts`, `matched_terms`, `aliases`, `created_at`, `deprecated`, and
175
+ * the repeated `used_by_note` boilerplate) and thins the rest; only tier 2 gives up the evidence
176
+ * lines entirely and becomes a pure triage list.
177
+ *
178
+ * `confidence`/`relevance`/`found_by` and the `uses`/`used_by`/`history_count` drill-in hooks
179
+ * survive BOTH tiers on purpose. They are a handful of bytes each and they are precisely what a
180
+ * caller uses to decide which result to open next — dropping them would make a compact response
181
+ * smaller and useless at the same time.
182
+ *
183
+ * Pure: no DB access, no I/O. Kept here rather than in the MCP handler so it is unit-testable
184
+ * directly, and so it sits inside the coverage gate.
185
+ */
186
+ export declare function toCompactSearchResult(result: SearchNodesResult, tier: 1 | 2): CompactSearchNodesResult;
30
187
  export interface GraphOptions {
31
188
  /** 'out' = callees only (call-flow trace), 'in' = callers only, 'both' = neighborhood. */
32
189
  direction?: 'out' | 'in' | 'both';
33
190
  includeCode?: boolean;
34
191
  codeCharBudget?: number;
192
+ /** Node-count safety valve for the BFS walk (default 500). Callers embedding a graph inside a
193
+ * cheaper response (get_node_code) pass a smaller cap than a dedicated graph call would. */
194
+ maxNodes?: number;
35
195
  }
36
196
  export interface GraphResult {
37
197
  nodes: GraphNode[];
38
198
  connections: DbConnection[];
39
199
  /** Total characters of code attached (only set when includeCode is true). */
40
200
  code_chars?: number;
41
- /** Set when some nodes came back without code (budget exhausted, or no code available). */
201
+ /** Set when some nodes came back without code for EITHER reason below. */
42
202
  code_truncated?: boolean;
203
+ /** Total nodes without code, both causes combined. Kept for callers that only need the count;
204
+ * the two fields below are what tell you which cause, and whether acting on it is possible. */
43
205
  nodes_without_code?: number;
206
+ /** Nodes whose code genuinely could not be found (symbol gone from disk, no cached snapshot).
207
+ * Raising `codeCharBudget` will NOT bring these back — that's the point of splitting them out. */
208
+ nodes_no_code_available?: number;
209
+ /** Nodes whose code EXISTS but was dropped to stay inside `codeCharBudget`, by id (capped).
210
+ * Reported as ids rather than a count so the caller can fetch exactly these — an id stays valid
211
+ * across calls, where a positional cursor into a re-derived BFS array does not. */
212
+ code_omitted_node_ids?: string[];
213
+ /** True when the walk hit `maxNodes` before the queue emptied — more of the graph exists than
214
+ * was returned. Previously this happened silently; a capped result was indistinguishable from
215
+ * a complete one. */
216
+ nodes_truncated?: boolean;
217
+ /** True when one or more discovered edges were dropped because an endpoint could not be
218
+ * resolved to a real node (a connections row surviving a deleted/renamed node) — never a
219
+ * dangling reference into `nodes` for an id that isn't actually there. */
220
+ connections_truncated?: boolean;
44
221
  }
45
222
  export declare function formatReasoning(r: string | ReasoningObject): string;
46
223
  /**
@@ -52,11 +229,54 @@ export declare function formatReasoning(r: string | ReasoningObject): string;
52
229
  * labels to read; rather than drop it, the whole chunk is surfaced as `what_changed`.
53
230
  */
54
231
  export declare function parseReasoningBlocks(raw: string): ReasoningObject[];
232
+ /** One reasoning block recovered from an accumulated history blob, with the time it was written. */
233
+ export interface TimedReasoningBlock {
234
+ /**
235
+ * The block's verbatim trimmed text. This is the GROUPING KEY for reconstructing a commit from
236
+ * history: `commitStagedChanges` passes one `reasoning` object to `updateHistory` for every node
237
+ * in the batch, so every node touched by a single commit_changes call ends up carrying a
238
+ * byte-identical block. Nothing else in the shared record identifies a commit — `history.id` is
239
+ * per-node, and the timestamps differ by milliseconds because each node's `updateHistory` call
240
+ * stamps its own `new Date()`.
241
+ */
242
+ text: string;
243
+ /** When this block was written — the `── Update @ … ──` separator's own timestamp, or the row's
244
+ * `created_at` for the first block, which has no separator preceding it. */
245
+ at: string;
246
+ parsed: ReasoningObject;
247
+ }
248
+ /**
249
+ * {@link parseReasoningBlocks} plus the one thing it throws away: each block's timestamp.
250
+ *
251
+ * The separator that `updateHistory` writes between merged blocks embeds the moment of the update,
252
+ * so an accumulated blob is already a dated log — but the existing parser splits on a
253
+ * non-capturing pattern and returns bare objects, which is all `get_node_code` ever needed.
254
+ * Reconstructing per-commit activity from shared history does need the dates (that's the whole
255
+ * time-filter), hence a sibling rather than a breaking change to a parser with other callers.
256
+ *
257
+ * Returned OLDEST FIRST — accumulation order, the opposite of `parseReasoningBlocks` — because the
258
+ * caller here regroups across rows by timestamp rather than reading "the latest" off the front.
259
+ */
260
+ export declare function parseReasoningBlocksTimed(raw: string, createdAt: string): TimedReasoningBlock[];
55
261
  export declare class DevMindDatabase {
56
262
  private db;
57
263
  private dbPath;
58
264
  private context;
59
- constructor(dbPath: string);
265
+ /**
266
+ * `onSyncProgress`: optional, fires during the constructor's initial `syncFromDisk()` pass —
267
+ * the one silent stretch every CLI command pays on `new DevMindDatabase(...)` before it can
268
+ * print anything else. Large `.devmind` folders (mainly `history/`, which grows one file per
269
+ * edit — much faster than node count) can make that pass take minutes; without this, a caller
270
+ * has no way to tell "still working" from "hung". Omit it for silent construction (the MCP
271
+ * server's normal path, where per-open console spam would be noise, not signal).
272
+ */
273
+ constructor(dbPath: string, opts?: {
274
+ onSyncProgress?: (phase: string, done: number, total: number) => void;
275
+ });
276
+ /** Throttles progress callbacks to ~100 updates across `total` items, regardless of scale —
277
+ * so a 500-file sync and a 500,000-file sync both report about as often, and the callback
278
+ * itself (a stdout write) never becomes the bottleneck it was being added to diagnose. */
279
+ private static shouldReport;
60
280
  private initSchema;
61
281
  getContext(): ProjectContext | null;
62
282
  getSystemMeta(key: string): string | null;
@@ -102,12 +322,65 @@ export declare class DevMindDatabase {
102
322
  name: string;
103
323
  file_path: string;
104
324
  signature?: string | null;
325
+ description?: string | null;
326
+ aliases?: string[];
105
327
  }): void;
328
+ /**
329
+ * Adds one alias to a node WITHOUT touching any it already has — the merge-safe counterpart to
330
+ * `upsertNode`'s replace-if-given aliases. This is what the batch graph-fix session's
331
+ * `record_alias` correction tool (Phase E) uses: it should never be able to accidentally drop an
332
+ * alias a deterministic detector pass already attached.
333
+ */
334
+ addAlias(nodeId: string, alias: string): void;
335
+ /**
336
+ * Stores a node's semantic vector and writes it to the committed `vectors/*.json` tree.
337
+ * `vector` must already be int8-quantized (`embedTextInt8`/`embedTextsInt8` in embedder.ts) and
338
+ * `descriptionHash` must be `hashDescription()` of the exact description it was computed from —
339
+ * this is the staleness key `getNodesNeedingEmbedding` checks against.
340
+ */
341
+ upsertNodeVector(nodeId: string, vector: Int8Array, descriptionHash: string): void;
342
+ getNodeVector(nodeId: string): {
343
+ modelId: string;
344
+ dim: number;
345
+ descriptionHash: string;
346
+ vector: Int8Array;
347
+ } | null;
348
+ /**
349
+ * Every non-deprecated, described node whose vector is missing, from a different model
350
+ * (`model_id` mismatch — e.g. a mismatched vector ignored during `syncFromDisk`), or stale
351
+ * (its description changed since the vector was computed). This is the work queue for both
352
+ * `devsmind embed` and the auto-embed hooks in `describe`/`add_description` — resumable and
353
+ * idempotent by construction, same shape as `describe.ts`'s own `WHERE description IS NULL`.
354
+ * `force: true` returns every described node regardless of vector state (model upgrades).
355
+ */
356
+ getNodesNeedingEmbedding(force?: boolean): DbNode[];
357
+ /**
358
+ * The `nodes.aliases` column is a JSON-array-in-TEXT blob — better-sqlite3 hands it back as a
359
+ * raw string, not a parsed array, on every `stmt.all()`/`stmt.get()`. A JSON string masquerading
360
+ * as `string[]` is a silent-wrong-answer hazard (both have `.length`, so a bug here would not
361
+ * throw, just quietly misbehave — e.g. counting characters instead of aliases). EVERY raw SQL
362
+ * read of the `nodes` table must route its rows through {@link parseNodeRow}/{@link
363
+ * parseNodeRows}, never cast `as DbNode`/`as DbNode[]` directly.
364
+ */
365
+ private static parseNodeAliases;
366
+ private static parseNodeRow;
367
+ private static parseNodeRows;
106
368
  getNode(id: string): DbNode | null;
107
369
  deleteNode(id: string): void;
108
370
  deprecateNode(id: string): void;
109
371
  /** `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
372
  renameNode(oldId: string, newId: string, newName?: string, newFilePath?: string): void;
373
+ /**
374
+ * Merges `fromId` into `intoId` — the batch graph-fix session's `merge_nodes` correction, for
375
+ * when curation (or a human reviewing feedback) decides two node candidates were never really
376
+ * distinct entities. Unlike `renameNode` (which moves everything to a FRESH id), `intoId`
377
+ * already exists with its own rows here: `fromId`'s connections (both directions) and history
378
+ * are reassigned onto it, `fromId`'s aliases (plus its own name, so old references by that name
379
+ * still resolve) are folded into `intoId`'s alias set, and `fromId` is deprecated — not hard
380
+ * deleted, so its history stays reachable and the merge itself stays a reversible correction,
381
+ * not a destructive one.
382
+ */
383
+ mergeNodes(fromId: string, intoId: string): void;
111
384
  /**
112
385
  * Rewrites a history/[id].json file's identifying fields (node_id, node_metadata) in
113
386
  * place, leaving code_snapshot/reasoning/timestamps untouched. Used after a rename so
@@ -137,14 +410,103 @@ export declare class DevMindDatabase {
137
410
  private deleteHistoryFilesForNode;
138
411
  addConnection(sourceNodeId: string, targetNodeId: string): void;
139
412
  removeConnection(sourceNodeId: string, targetNodeId: string): void;
140
- getConnections(nodeId: string): {
413
+ /**
414
+ * `opts.limit`/`opts.offset` page a hub node's caller/callee list deterministically —
415
+ * `ORDER BY file_path, name` so a repeated call with the same offset returns the same slice,
416
+ * and so the local/nearby callers a reader actually wants tend to sort ahead of a scattered
417
+ * cross-repo tail (same file_path groups together). Omitting `opts` returns every row, exactly
418
+ * as before — every pre-existing call site keeps working unchanged.
419
+ */
420
+ getConnections(nodeId: string, opts?: {
421
+ limit?: number;
422
+ offset?: number;
423
+ }): {
141
424
  uses: DbNode[];
142
425
  usedBy: DbNode[];
143
426
  };
427
+ /**
428
+ * Batched, COUNT-only connection degree for many nodes at once — the search-result drill-in
429
+ * hooks need this for ~20 nodes per call, and `getConnections` per-node would mean 20 pairs of
430
+ * full-row-fetching queries. One grouped COUNT each way instead (same shape as the degree
431
+ * subquery in {@link getGodEntities}). IDs not present in `node_connections` still get a
432
+ * `{uses:0, usedBy:0}` entry so callers never need an existence check.
433
+ */
434
+ getConnectionCounts(ids: string[]): Map<string, {
435
+ uses: number;
436
+ usedBy: number;
437
+ }>;
438
+ /** Batched history-entry count for many nodes at once — metadata only, no disk reads. */
439
+ getHistoryCounts(ids: string[]): Map<string, number>;
440
+ /**
441
+ * Batched most-recent history timestamp for many nodes at once. Deliberately SQL-only (`MAX`
442
+ * over the indexed `updated_at` column) — unlike {@link getLatestHistory}, this never touches
443
+ * `populateHistoryFromDisk`, so it costs nothing beyond the query itself.
444
+ */
445
+ getLastUpdatedMap(ids: string[]): Map<string, string>;
144
446
  getLatestHistory(nodeId: string): DbHistory | null;
145
- listHistory(nodeId: string): Omit<DbHistory, 'code_snapshot' | 'reasoning'>[];
447
+ listHistory(nodeId: string): Omit<DbHistory, 'code_snapshot' | 'reasoning' | 'edits'>[];
146
448
  getHistoryEntry(id: string): DbHistory | null;
147
449
  getFullHistory(nodeId: string): DbHistory[];
450
+ /**
451
+ * The last `limit` history entries' reasoning + timestamps only — no `code_snapshot`/`edits`.
452
+ * Built for `get_node_code`'s default `history:"recent"` mode, which already returns the
453
+ * CURRENT code: repeating past snapshots inline would just duplicate what's already in the
454
+ * response. The full trail (snapshots + diffable edits) is `history:"full"`, served by
455
+ * {@link getHistoryPage}. This answers "why does this look the way it does" cheaply enough to
456
+ * attach to every get_node_code call by default, instead of leaving that as a round trip an AI
457
+ * has to remember to make (or skip, and re-break a decision it never saw).
458
+ */
459
+ getRecentHistorySummaries(nodeId: string, limit: number): Array<Pick<DbHistory, 'id' | 'session_id' | 'created_at' | 'updated_at' | 'reasoning'>>;
460
+ /**
461
+ * `history:"full"`'s backing query — the same full-fidelity payload as {@link getFullHistory}
462
+ * (code_snapshot + diffable edits per entry), but LIMIT/OFFSET applied IN SQL before any disk
463
+ * read happens, not by slicing an already-fully-loaded array. `getFullHistory` reads every
464
+ * revision's JSON off disk unconditionally; for a node with dozens of revisions that is dozens
465
+ * of synchronous file reads to serve a request for the newest 5. `total` is the true count
466
+ * before paging, same honesty contract as `nodes_total`/`files_total` elsewhere.
467
+ */
468
+ getHistoryPage(nodeId: string, limit: number, offset: number): {
469
+ entries: DbHistory[];
470
+ total: number;
471
+ };
472
+ /**
473
+ * Every history row in a time/session window, joined to its node's file path — the SHARED
474
+ * counterpart to the local activity log, and the backing query for `get_activity_log`'s graph
475
+ * fallback (see db/activity-graph.ts).
476
+ *
477
+ * Deliberately reads SQLite ONLY, never `populateHistoryFromDisk`. Everything the fallback needs
478
+ * — reasoning (which carries developer + requirement, see formatReasoning), both timestamps,
479
+ * session_id, and the file path — is already in columns; the disk JSON adds only `code_snapshot`
480
+ * and `edits`, neither of which an activity listing reports. That matters because this scans
481
+ * ROWS, not one node's history: routing it through the per-row file read would turn a single
482
+ * indexed query into one synchronous readFileSync per revision in the window.
483
+ *
484
+ * The date bounds test the row's [created_at, updated_at] span against the window rather than a
485
+ * single point. One row accumulates blocks for up to an hour past `created_at` (the merge rule),
486
+ * so `created_at >= since` would silently drop a row whose in-window blocks were appended to an
487
+ * out-of-window row. This over-selects instead, and the caller filters per block, where the real
488
+ * timestamps live.
489
+ *
490
+ * `file_path` is null when the node is gone (history outlives its node — a hard delete leaves
491
+ * rows behind). Nulls are the caller's to skip; dropping them here would silently shrink the
492
+ * edit counts a fallback entry reports.
493
+ */
494
+ queryHistoryForActivity(opts?: {
495
+ sessionId?: string;
496
+ since?: string;
497
+ until?: string;
498
+ /** Hard cap on rows scanned, newest-updated first. Bounds a full-table scan on a mature repo;
499
+ * the caller reports when it bites rather than passing off a truncated log as complete. */
500
+ limit?: number;
501
+ }): {
502
+ id: string;
503
+ node_id: string;
504
+ session_id: string;
505
+ created_at: string;
506
+ updated_at: string;
507
+ reasoning: string;
508
+ file_path: string | null;
509
+ }[];
148
510
  /** Distinct source node ids of edges pointing INTO this node (its "used-by" callers). */
149
511
  getInboundSources(nodeId: string): string[];
150
512
  getLatestCode(nodeId: string): {
@@ -169,69 +531,168 @@ export declare class DevMindDatabase {
169
531
  updateHistory(params: {
170
532
  node_id: string;
171
533
  code_snapshot: string;
534
+ /**
535
+ * The entity's text before this edit, when the caller knows it (`edit_node` does; it holds
536
+ * the pre-edit file). `null` means the entity did not exist yet — a pure addition. `undefined`
537
+ * means the caller has no before-state at all (the legacy `update_history` path, or an
538
+ * initial index snapshot), and no edit is recorded to
539
+ * the trail: an entry with nothing to compare against gets no diff and no revert.
540
+ */
541
+ code_before?: string | null;
172
542
  reasoning: string | ReasoningObject;
173
543
  session_id?: string;
174
544
  }): DbHistory;
175
545
  /**
176
- * Search for nodes by name/id/reasoning first (cheap, SQL-only). If that finds
177
- * nothing, transparently fall back to a code-content search (same engine as
178
- * {@link searchCode}) so a query like "alipay" still succeeds even when no
179
- * node's name/id/reasoning mentions it but the code itself does. Every result
180
- * is tagged `matched_via` so the caller knows which path found it.
546
+ * Removes the newest recorded edit from a history entry, leaving no trace of it.
547
+ *
548
+ * There used to be a citation guard here: a history row cited by a workflow step was emptied
549
+ * rather than deleted, so the step was not left pointing at nothing. Workflow steps record
550
+ * `node_ids` now, not history ids, so nothing cites a history row any more and the guard had
551
+ * nothing left to check. Keeping it would have meant keeping the `history_ids` column alive
552
+ * purely to protect a reference nothing makes.
553
+ *
554
+ * Callers are expected to have restored the file already; this only unwinds what was written
555
+ * about it.
181
556
  */
182
- searchNodes(query: string, opts?: {
183
- is_regex?: boolean;
557
+ eraseLastEdit(historyId: string): {
558
+ erased: boolean;
559
+ entry_deleted: boolean;
560
+ reason?: string;
561
+ };
562
+ /**
563
+ * The one search tool, covering both worlds in a single call:
564
+ * - a primary `nodes` bucket — the indexed graph, found by exact identifier, then by three
565
+ * fused rankers (BM25 over metadata, vector over descriptions, and code-body match), and
566
+ * - a last-resort `files` bucket — a real filesystem grep of the configured repos, so files
567
+ * the graph never models (CSS, JSON, config, markup, un-indexed code) are finally covered
568
+ * in the same call instead of sending the caller off to an external grep.
569
+ *
570
+ * Inputs play to each layer's strength: the natural-language `query` drives the semantic vector
571
+ * layer (and BM25); `opts.keywords` (literal, OR) drive grep and the code-body match, and also
572
+ * feed BM25. If no keywords are given they're derived from the query's significant tokens, so a
573
+ * natural-only call still gets code + file coverage.
574
+ *
575
+ * Speed is the point — the two slow layers (vector, grep) run concurrently, and the old ~9k
576
+ * per-node snapshot read (the tool's former ~10-30s cost) is gone: code-body matching now rides
577
+ * the single grep walk. See `grep.ts` and Phase 4 of the plan.
578
+ */
579
+ /**
580
+ * Attaches the drill-in hooks (`uses`/`used_by`/`history_count`/`last_updated`) to a batch of
581
+ * search results in place, via ONE grouped query per hook instead of per-node fetches — see
582
+ * {@link getConnectionCounts}/{@link getHistoryCounts}/{@link getLastUpdatedMap}. This is the
583
+ * signal that turns a search result from a dead end into something worth drilling into with
584
+ * `get_node_graph`/`get_node_history` — without it, nothing hints there's more to find.
585
+ */
586
+ private attachDrillInHooks;
587
+ searchNodes(query: string | undefined, opts?: {
588
+ pattern?: string;
589
+ path?: string;
184
590
  case_insensitive?: boolean;
185
- }): Array<(DbNode & {
186
- matched_via: 'identifier';
187
- }) | (ReturnType<DevMindDatabase['searchCode']>[number] & {
188
- matched_via: 'code';
189
- }) | (DbNode & {
190
- matched_via: 'fuzzy';
191
- matched_terms: string[];
192
- score: number;
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;
210
- getRecentChanges(hours?: number, analyzeImpact?: boolean): {
211
- node_id: string;
212
- node_name: string;
213
- file_path: string;
214
- updated_at: string;
215
- reasoning: string;
216
- downstream_impact?: {
217
- node_id: string;
218
- node_name: string;
219
- file_path: string;
220
- status: 'stale_warning' | 'already_updated';
221
- }[];
222
- }[];
223
- getDeveloperActivity(developer: string, limit?: number): {
224
- node_id: string;
225
- node_name: string;
226
- updated_at: string;
227
- reasoning: string;
228
- }[];
229
- getChangesByRequirement(requirementId: string): {
230
- node_id: string;
231
- node_name: string;
232
- updated_at: string;
233
- reasoning: string;
234
- }[];
591
+ offset?: number;
592
+ limit?: number;
593
+ compact?: boolean;
594
+ }): Promise<SearchNodesResult>;
595
+ /**
596
+ * The token-ranked (BM25) half of {@link searchNodes}. Looks up every query/keyword token in
597
+ * the local `node_tokens` index (rebuilding it first if stale — see {@link ensureSearchIndexFresh}),
598
+ * scores each candidate with {@link scoreCandidate} (IDF-weighted, saturating TF, per-field
599
+ * weights favoring `description`), and applies the noise floor. Metadata only — identifier / id
600
+ * / path / description / reasoning. Code-body matching is NO LONGER folded in here: it used to
601
+ * read one history-JSON per node (~9k serial reads = the tool's ~10-30s cost), and is now
602
+ * served far faster by the real filesystem grep in `searchNodes` (see `mapGrepHitsToNodes`).
603
+ * Returns nodes tagged `matched_via:'fuzzy'`, ranked; `searchNodes` re-fuses them by RRF.
604
+ */
605
+ private tokenSearchNodes;
606
+ /** Absolute filesystem roots for every configured repo — the search space for `grepRepos`. */
607
+ private repoRoots;
608
+ /**
609
+ * Resolves and validates `search_nodes`' optional `path` scope a single folder or file the
610
+ * grep walk restricts to, instead of always walking every configured repo. A path outside every
611
+ * repo root is REJECTED (thrown, not silently widened to "search everything") a scope that
612
+ * can't be honored should fail loudly, since silently ignoring it would search far more than
613
+ * the caller asked for. `canonicalizePath` (already used throughout this file for path
614
+ * comparisons) lowercases the Windows drive letter so `C:\...` and `c:\...` compare equal.
615
+ */
616
+ private resolveSearchScopePath;
617
+ /**
618
+ * Maps raw grep hits back to graph nodes — this is "code search that returns nodes", rebuilt
619
+ * on top of the single filesystem walk instead of the old ~9k per-node snapshot reads. For each
620
+ * file that had hits AND contains indexed nodes: an AST-parseable file is resolved precisely
621
+ * (each hit line → the one node whose line-range contains it, via {@link locateNodeInFile}), so
622
+ * a keyword hitting one method in a 10-method file surfaces THAT node, not all ten. A non-AST
623
+ * indexed file (a staged `.py`/`.go` node) falls back to coarse file→node. Returns node ids in
624
+ * match-strength order (files with more hits first) plus the matching lines, for RRF + display.
625
+ *
626
+ * Only the top {@link CODE_MATCH_FILE_CAP} files by hit count are AST-resolved — measured on a
627
+ * real 8-repo, 15k-grep-hit query, this step alone was 8.2s of a 9.6s total before the cap, one
628
+ * `locateNodeInFile` AST walk per node per matched file with no bound on how many files that
629
+ * could be. It doesn't cost result quality: the fused ranking downstream keeps only the top ~20
630
+ * nodes anyway, and files with more hits are exactly the ones most likely to place there — files
631
+ * past the cap would almost never have survived to the final result even fully resolved.
632
+ */
633
+ private mapGrepHitsToNodes;
634
+ /**
635
+ * Resolves every indexed node's line span within one AST-parseable file — extracted out of
636
+ * `mapGrepHitsToNodes` so `annotateSampleLinesWithSymbol` can reuse the identical span logic
637
+ * instead of re-deriving it. Computed ONCE per file (not per line/hit): a file with several
638
+ * sample lines would otherwise repeat the same `locateNodeInFile` AST walk once per line.
639
+ * `nodes` is accepted rather than re-fetched so an existing `getNodesByFilePath` result (as
640
+ * `mapGrepHitsToNodes` already has) isn't queried twice.
641
+ */
642
+ private computeSymbolSpans;
643
+ /**
644
+ * Mutates each `RankedFile`'s `sample_lines` in place, tagging every line with the function/class
645
+ * that contains it — the insight a plain filesystem grep can never give: not just "line 87
646
+ * matched" but "line 87, inside `onLikeTap`". This is the annotation `search_nodes` offers that
647
+ * makes it worth more than grep for a hit that lands inside real source.
648
+ *
649
+ * Deliberately bounded to the ALREADY-CAPPED page (`files`, ≤`maxFiles` entries × ≤5 sample
650
+ * lines each — at most ~125 lookups) rather than every raw grep hit. `mapGrepHitsToNodes` is
651
+ * measured at 8.2s of a 9.6s query on a real 8-repo search (see its doc comment) precisely from
652
+ * unbounded per-hit AST resolution; annotating only what's actually returned keeps this at a
653
+ * small, constant added cost regardless of how broad the pattern was or how many total hits it
654
+ * produced. Silently no-ops for a file that isn't indexed or isn't AST-parseable — a `symbol` on
655
+ * a sample line is a bonus, never a requirement.
656
+ */
657
+ private annotateSampleLinesWithSymbol;
658
+ /**
659
+ * The semantic half of hybrid search: embeds the query with the same vendored ONNX model used
660
+ * to embed every node's description (see embedder.ts), then linear-scans `node_vectors` for
661
+ * cosine similarity. Linear is fine at this scale — ~9k nodes × 384 int8 dims is a few million
662
+ * integer multiplications, well under a millisecond, far below the model's own inference time
663
+ * for the query itself; revisit only past ~100k nodes. Returns each surviving node's raw cosine
664
+ * alongside its id — the ORDER drives RRF, but the cosine value is kept so `searchNodes` can turn
665
+ * it into a human-meaningful confidence (a 0.7 cosine is a strong match; a 0.36 is a weak one —
666
+ * the fused RRF float can't express that). Returns [] — never throws — if the embedder is
667
+ * unavailable, so `searchNodes` degrades to BM25+grep exactly as before.
668
+ */
669
+ private vectorSearchNodes;
670
+ /**
671
+ * Cheap, single-pass signal for "has the graph changed since node_tokens was last built":
672
+ * how many non-deprecated nodes exist, the total length of every description (catches a
673
+ * description being added OR edited — length almost never stays identical), and how many
674
+ * history rows exist (catches new reasoning). Deliberately NOT based on an `updated_at`
675
+ * column on `nodes` — there isn't one, and `syncFromDisk`'s destructive graph-rebuild pass
676
+ * writes nodes via raw SQL, not through a single method that could easily be hooked, so a
677
+ * write-path-by-write-path invalidation scheme would silently miss that path (and did, in an
678
+ * earlier draft of this). A fingerprint checked lazily at query time can't be missed the same
679
+ * way — it doesn't matter HOW the data changed, only THAT it did.
680
+ */
681
+ private searchIndexFingerprint;
682
+ /** Rebuilds `node_tokens` from scratch for every non-deprecated node's current
683
+ * identifier/id/path/description and EVERY revision's reasoning (not just the latest — a
684
+ * decision recorded three revisions ago must stay findable by `search_nodes`, since there is
685
+ * no separate decisions-only search tool anymore; `GROUP_CONCAT` folds every history row's
686
+ * reasoning into one field before tokenizing, so an older "Decision: …" is searchable exactly
687
+ * like the newest one). Cheap at realistic node counts (hundreds to low-thousands) — a single
688
+ * scan plus one batched transaction, not something that needs to be avoided; simplicity here
689
+ * is worth more than incremental upkeep that a bypassed write path could silently defeat. */
690
+ private rebuildSearchIndex;
691
+ /** Compares the current fingerprint against what `node_tokens` was last built from, stored in
692
+ * `system_meta`; rebuilds and updates the stored fingerprint only on a mismatch. A momentarily
693
+ * stale index (between a real change and the next search call) only costs ranking quality on
694
+ * that one call, never correctness — the next call rebuilds it. */
695
+ private ensureSearchIndexFresh;
235
696
  searchDecisions(query: string): {
236
697
  node_id: string;
237
698
  node_name: string;
@@ -256,10 +717,35 @@ export declare class DevMindDatabase {
256
717
  }[];
257
718
  getOrphanedNodes(): DbNode[];
258
719
  getAllNodes(): DbNode[];
720
+ /**
721
+ * The shared WHERE clause behind {@link listNodes} and {@link countNodes}. Extracted so the page
722
+ * and its total can never drift apart — a `total` computed from even slightly different criteria
723
+ * than the rows it describes is worse than no total at all, since it reads as authoritative.
724
+ */
725
+ private buildNodeFilterSql;
726
+ /**
727
+ * Total nodes matching a filter, independent of any page. Exists so `list_nodes` can tell
728
+ * "that's everything" from "there is more" — the same honesty contract `nodes_total` and
729
+ * `files_total` already keep for search.
730
+ */
731
+ countNodes(filter?: {
732
+ type?: string;
733
+ file_path?: string;
734
+ include_deprecated?: boolean;
735
+ }): number;
736
+ /**
737
+ * `limit`/`offset` are OPTIONAL and, when omitted, this returns every matching row exactly as
738
+ * before — the internal analysis callers (`analyze.ts`, `edges.ts`) legitimately need the whole
739
+ * graph, so paging is opt-in rather than a default that would silently truncate them. Ordering
740
+ * is applied only when paging, since an unordered LIMIT is a lottery: without it, "page 2" is
741
+ * not guaranteed to exclude what "page 1" already returned.
742
+ */
259
743
  listNodes(filter?: {
260
744
  type?: string;
261
745
  file_path?: string;
262
746
  include_deprecated?: boolean;
747
+ limit?: number;
748
+ offset?: number;
263
749
  }): DbNode[];
264
750
  getAllConnections(): DbConnection[];
265
751
  getAllHistory(): DbHistory[];
@@ -299,21 +785,86 @@ export declare class DevMindDatabase {
299
785
  updated_at: string;
300
786
  }[];
301
787
  private workflowsDir;
302
- /** Serializes the workflow + its steps + artifact index to disk so teammates can sync it via git. */
788
+ /**
789
+ * One-time backfill of `node_ids`/`reasoning` for steps written before v2, resolving each old
790
+ * `history_ids` entry to the node it belongs to. Runs at open, right after the ALTERs, and is
791
+ * self-limiting: it only touches rows where `node_ids IS NULL AND history_ids IS NOT NULL`, so
792
+ * the second open finds nothing and the query costs one indexed scan.
793
+ *
794
+ * Best-effort by nature. Because of the 1-hour history merge, an old step's `history_ids` can
795
+ * include rows an ADJACENT commit created, so a backfilled node list can be broader than what
796
+ * that step actually touched. That is acceptable for pre-v2 rows — they were already imprecise,
797
+ * which is exactly why the format changed — but it is why nothing presents backfilled data as
798
+ * exact. A step whose history rows have since been pruned keeps its `summary` and gets an empty
799
+ * list rather than being skipped, so it still appears on the timeline.
800
+ */
801
+ private backfillWorkflowStepNodeIds;
802
+ /**
803
+ * Serializes the workflow + its steps + artifact index to disk so teammates can sync it via git.
804
+ *
805
+ * Written as TWO files, and the split is the whole point:
806
+ *
807
+ * - `workflow.json` keeps the shape a v1 client understands, so an older build reading it loses
808
+ * nothing it ever had.
809
+ * - `v2.json` holds everything v1 has no field for (`archived`, and per-step `reasoning` /
810
+ * `node_ids` / `doc_paths`).
811
+ *
812
+ * A single file could not be made safe. `devsmind sync` calls `syncToDisk`, which re-serializes
813
+ * every workflow.json from whatever columns the local build knows about — so a teammate who
814
+ * pulls on an older version and syncs would rewrite every workflow and silently strip the new
815
+ * fields, then commit that loss for everyone. An older build has no idea `v2.json` exists, so it
816
+ * cannot rewrite it; the data survives the round trip and is merged back on the next read.
817
+ */
303
818
  private writeWorkflowToDisk;
819
+ /**
820
+ * Creates a workflow. Note what it deliberately does NOT do any more: set a global "active"
821
+ * pointer. Which workflow you are working on is a property of YOUR session, held locally — a
822
+ * workflow is a shared record, and one shared pointer meant two sessions (or two teammates,
823
+ * since the pointer synced through git) silently stole it from each other mid-work.
824
+ */
304
825
  createWorkflow(name: string, description: string): DbWorkflow;
305
826
  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;
827
+ /**
828
+ * Workflows newest-touched first which is the ordering that replaces the old `status` field.
829
+ * Live work floats up and abandoned threads sink on their own, so nothing has to be marked
830
+ * "completed" by hand (nobody ever did, and a lifecycle field nobody maintains just lies).
831
+ *
832
+ * `query` matches name AND description, the search `searchWorkflows` never actually did: it
833
+ * scanned step summaries and artifact names only, so looking a workflow up by its own name
834
+ * returned nothing. Paging mirrors `listNodes` — `total` is the true count before the page.
835
+ */
836
+ listWorkflows(opts?: {
837
+ query?: string;
838
+ includeArchived?: boolean;
839
+ limit?: number;
840
+ offset?: number;
841
+ }): DbWorkflow[];
842
+ countWorkflows(opts?: {
843
+ query?: string;
844
+ includeArchived?: boolean;
845
+ }): number;
846
+ /** Shared WHERE builder, so a page and its `total` can never describe different criteria. */
847
+ private buildWorkflowFilterSql;
848
+ /**
849
+ * Hides a workflow from the default listing. Deliberately NOT called "complete": a feature is
850
+ * never finished, it just stops being worked on, and the old `completed` status promised a
851
+ * lifecycle nobody maintained. Archiving claims only what it delivers, and is reversible.
852
+ */
853
+ setWorkflowArchived(id: string, archived: boolean): DbWorkflow;
854
+ /**
855
+ * Appends one step. A step is either a COMMIT (summary + reasoning + the node ids it touched) or
856
+ * a RESEARCH finding (summary + reasoning + the docs behind it, no nodes) — the second is the
857
+ * only record of work that produced a decision but no code, which nothing else in DevsMind
858
+ * captures: git has the diff and history has the per-node reasoning, but neither can tell you
859
+ * what was evaluated and rejected.
860
+ *
861
+ * `reasoning` is stored, not joined from `history`, deliberately — see DbWorkflowStep.
862
+ */
313
863
  addWorkflowStep(workflowId: string, opts: {
314
864
  summary: string;
315
- pendingTasks?: string;
316
- historyIds?: string[];
865
+ reasoning?: string;
866
+ nodeIds?: string[];
867
+ docPaths?: string[];
317
868
  sessionId?: string;
318
869
  }): DbWorkflowStep;
319
870
  /** Writes `content` to `.devmind/workflows/<workflowId>/<artifactId>_<sourceName>` and records the DB row. */
@@ -323,14 +874,28 @@ export declare class DevMindDatabase {
323
874
  sourceName: string;
324
875
  content: string;
325
876
  }): DbWorkflowArtifact;
877
+ /**
878
+ * The workflow's story: its steps in order, plus the docs attached to it.
879
+ *
880
+ * Paged, because this is now the ONLY read (it absorbed the old `workflow_get_steps`) and steps
881
+ * carry their own reasoning, so an unbounded version of it would be the largest response the
882
+ * server can produce. `steps_total` is exact regardless of the page — a short page must never
883
+ * read as "that is the whole story".
884
+ *
885
+ * Artifact CONTENT is deliberately not returned. It used to be inlined whole, which on an
886
+ * imported architecture doc is trivially tens of KB; the file path is enough, since the file is
887
+ * on disk and the caller can read exactly the part it needs.
888
+ */
326
889
  getWorkflowContext(id: string, opts?: {
327
- includeArtifactContent?: boolean;
890
+ limit?: number;
891
+ offset?: number;
892
+ last_n?: number;
328
893
  }): {
329
894
  workflow: DbWorkflow;
330
895
  steps: DbWorkflowStep[];
331
- artifacts: (DbWorkflowArtifact & {
332
- content?: string;
333
- })[];
896
+ steps_total: number;
897
+ steps_offset: number;
898
+ artifacts: DbWorkflowArtifact[];
334
899
  };
335
900
  /**
336
901
  * Returns steps for a workflow with optional pagination.
@@ -342,29 +907,6 @@ export declare class DevMindDatabase {
342
907
  offset?: number;
343
908
  last_n?: number;
344
909
  }): 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
910
  /**
369
911
  * Imports an existing flow/architecture doc as a paused workflow (not active — importing
370
912
  * a doc isn't the same as declaring active work). Idempotent on `name`: re-importing the
@@ -398,6 +940,8 @@ export declare class DevMindDatabase {
398
940
  };
399
941
  private populateHistoryFromDisk;
400
942
  private writeHistoryToDisk;
943
+ /** The configured developer identity (`.devmind/.env`'s DEVELOPER_NAME), or null if unset. */
944
+ getDeveloperName(): string | null;
401
945
  toRepoRelativePath(absolutePath: string): string;
402
946
  /**
403
947
  * Rejects a resolved path that escapes its expected root (e.g. via a stored
@@ -408,14 +952,14 @@ export declare class DevMindDatabase {
408
952
  private clampToRoot;
409
953
  /**
410
954
  * 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
955
+ * Used to reject `edit_node`/`update_history` file paths that would otherwise let a
412
956
  * tool call read/write any file on disk (absolute path, or a `../` escape) instead of
413
957
  * just repo source — nothing upstream of this validates that the AI-supplied path is
414
958
  * actually inside the project.
415
959
  */
416
960
  /**
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
961
+ * Gate for every AI-facing write (edit_node, the legacy update_history): true only for
962
+ * paths inside a configured repo. `.devmind` itself — this project's OWN
419
963
  * config, brain.db, and cached graph JSON — is never writable through these tools, even
420
964
  * though it sits next to (and, before this check, was indistinguishable from) real source:
421
965
  * without this, a write tool built to "never refuse a file type" would just as happily
@@ -423,10 +967,18 @@ export declare class DevMindDatabase {
423
967
  */
424
968
  isPathAllowed(absPath: string): boolean;
425
969
  toAbsolutePath(repoRelativePath: string): string;
426
- syncFromDisk(): void;
970
+ syncFromDisk(onProgress?: (phase: string, done: number, total: number) => void): void;
427
971
  /** Escape LIKE metacharacters so a path is matched literally (use with ESCAPE '\\'). */
428
972
  private likeEscape;
429
973
  writeGraphToDisk(filePath: string): void;
974
+ /**
975
+ * Mirrors `writeGraphToDisk` exactly (same file-matching logic, same directory shape) but into
976
+ * a separate `vectors/` tree rather than inside `graph/*.json` — deliberately, so opaque base64
977
+ * blobs never pollute the human-readable, merge-friendly graph JSON. Deprecated nodes are
978
+ * skipped here (unlike the graph, which keeps them): `searchNodes` never queries a deprecated
979
+ * node's vector, so writing one is pure dead weight.
980
+ */
981
+ writeVectorsToDisk(filePath: string): void;
430
982
  /** Force-syncs all database nodes and workflows to disk JSON files. */
431
983
  syncToDisk(): void;
432
984
  }