devsmind-mcp 2.4.0 → 3.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 (128) hide show
  1. package/README.md +326 -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 +128 -5
  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 +44 -0
  26. package/dist/cli/integrations/memory-topics.js +295 -0
  27. package/dist/cli/integrations/memory-topics.js.map +1 -0
  28. package/dist/cli/integrations/memory.d.ts +11 -5
  29. package/dist/cli/integrations/memory.js +132 -37
  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 +19 -5
  35. package/dist/cli/integrations/registry.js +23 -13
  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 +113 -32
  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.d.ts +200 -0
  56. package/dist/db/activity.js +347 -0
  57. package/dist/db/activity.js.map +1 -0
  58. package/dist/db/analyze.js +18 -6
  59. package/dist/db/analyze.js.map +1 -1
  60. package/dist/db/database.d.ts +583 -99
  61. package/dist/db/database.js +1825 -553
  62. package/dist/db/database.js.map +1 -1
  63. package/dist/db/edges.d.ts +24 -0
  64. package/dist/db/edges.js +84 -0
  65. package/dist/db/edges.js.map +1 -1
  66. package/dist/db/embedder.d.ts +39 -0
  67. package/dist/db/embedder.js +0 -0
  68. package/dist/db/embedder.js.map +1 -0
  69. package/dist/db/feedback.d.ts +128 -0
  70. package/dist/db/feedback.js +182 -0
  71. package/dist/db/feedback.js.map +1 -0
  72. package/dist/db/file-diff.d.ts +32 -0
  73. package/dist/db/file-diff.js +110 -0
  74. package/dist/db/file-diff.js.map +1 -0
  75. package/dist/db/grep.d.ts +78 -0
  76. package/dist/db/grep.js +475 -0
  77. package/dist/db/grep.js.map +1 -0
  78. package/dist/db/message-revert.d.ts +63 -0
  79. package/dist/db/message-revert.js +258 -0
  80. package/dist/db/message-revert.js.map +1 -0
  81. package/dist/db/revert.d.ts +31 -0
  82. package/dist/db/revert.js +108 -0
  83. package/dist/db/revert.js.map +1 -0
  84. package/dist/db/schema.d.ts +49 -4
  85. package/dist/db/schema.js +125 -73
  86. package/dist/db/schema.js.map +1 -1
  87. package/dist/db/search-index.d.ts +65 -0
  88. package/dist/db/search-index.js +74 -0
  89. package/dist/db/search-index.js.map +1 -0
  90. package/dist/db/staging.d.ts +90 -5
  91. package/dist/db/staging.js +148 -22
  92. package/dist/db/staging.js.map +1 -1
  93. package/dist/mcp/server.d.ts +29 -7
  94. package/dist/mcp/server.js +2584 -809
  95. package/dist/mcp/server.js.map +1 -1
  96. package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
  97. package/dist/mcp/vendor/force-graph.min.js +5 -0
  98. package/dist/mcp/vendor/model/model_int8.onnx +0 -0
  99. package/dist/mcp/vendor/model/vocab.txt +30522 -0
  100. package/dist/mcp/vendor/three.min.js +7 -0
  101. package/dist/mcp/view.css +419 -0
  102. package/dist/mcp/view.html +161 -0
  103. package/dist/mcp/view.js +245 -0
  104. package/dist/mcp/view_chat.js +382 -0
  105. package/dist/mcp/view_graph.js +576 -0
  106. package/dist/mcp/visualizer.d.ts +25 -2
  107. package/dist/mcp/visualizer.js +31 -4
  108. package/dist/mcp/visualizer.js.map +1 -1
  109. package/dist/utils/ast.d.ts +89 -0
  110. package/dist/utils/ast.js +378 -10
  111. package/dist/utils/ast.js.map +1 -1
  112. package/dist/utils/diff.d.ts +44 -0
  113. package/dist/utils/diff.js +78 -0
  114. package/dist/utils/diff.js.map +1 -0
  115. package/dist/utils/edit.d.ts +6 -0
  116. package/dist/utils/edit.js +47 -14
  117. package/dist/utils/edit.js.map +1 -1
  118. package/dist/utils/scanner.js +12 -4
  119. package/dist/utils/scanner.js.map +1 -1
  120. package/dist/utils/tokenize.d.ts +45 -0
  121. package/dist/utils/tokenize.js +129 -0
  122. package/dist/utils/tokenize.js.map +1 -0
  123. package/dist/utils/version.d.ts +14 -0
  124. package/dist/utils/version.js +61 -0
  125. package/dist/utils/version.js.map +1 -0
  126. package/package.json +18 -5
  127. package/dist/mcp/visualizer_2d.html +0 -635
  128. 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
  /**
@@ -56,7 +233,21 @@ export declare class DevMindDatabase {
56
233
  private db;
57
234
  private dbPath;
58
235
  private context;
59
- constructor(dbPath: string);
236
+ /**
237
+ * `onSyncProgress`: optional, fires during the constructor's initial `syncFromDisk()` pass —
238
+ * the one silent stretch every CLI command pays on `new DevMindDatabase(...)` before it can
239
+ * print anything else. Large `.devmind` folders (mainly `history/`, which grows one file per
240
+ * edit — much faster than node count) can make that pass take minutes; without this, a caller
241
+ * has no way to tell "still working" from "hung". Omit it for silent construction (the MCP
242
+ * server's normal path, where per-open console spam would be noise, not signal).
243
+ */
244
+ constructor(dbPath: string, opts?: {
245
+ onSyncProgress?: (phase: string, done: number, total: number) => void;
246
+ });
247
+ /** Throttles progress callbacks to ~100 updates across `total` items, regardless of scale —
248
+ * so a 500-file sync and a 500,000-file sync both report about as often, and the callback
249
+ * itself (a stdout write) never becomes the bottleneck it was being added to diagnose. */
250
+ private static shouldReport;
60
251
  private initSchema;
61
252
  getContext(): ProjectContext | null;
62
253
  getSystemMeta(key: string): string | null;
@@ -102,12 +293,65 @@ export declare class DevMindDatabase {
102
293
  name: string;
103
294
  file_path: string;
104
295
  signature?: string | null;
296
+ description?: string | null;
297
+ aliases?: string[];
105
298
  }): void;
299
+ /**
300
+ * Adds one alias to a node WITHOUT touching any it already has — the merge-safe counterpart to
301
+ * `upsertNode`'s replace-if-given aliases. This is what the batch graph-fix session's
302
+ * `record_alias` correction tool (Phase E) uses: it should never be able to accidentally drop an
303
+ * alias a deterministic detector pass already attached.
304
+ */
305
+ addAlias(nodeId: string, alias: string): void;
306
+ /**
307
+ * Stores a node's semantic vector and writes it to the committed `vectors/*.json` tree.
308
+ * `vector` must already be int8-quantized (`embedTextInt8`/`embedTextsInt8` in embedder.ts) and
309
+ * `descriptionHash` must be `hashDescription()` of the exact description it was computed from —
310
+ * this is the staleness key `getNodesNeedingEmbedding` checks against.
311
+ */
312
+ upsertNodeVector(nodeId: string, vector: Int8Array, descriptionHash: string): void;
313
+ getNodeVector(nodeId: string): {
314
+ modelId: string;
315
+ dim: number;
316
+ descriptionHash: string;
317
+ vector: Int8Array;
318
+ } | null;
319
+ /**
320
+ * Every non-deprecated, described node whose vector is missing, from a different model
321
+ * (`model_id` mismatch — e.g. a mismatched vector ignored during `syncFromDisk`), or stale
322
+ * (its description changed since the vector was computed). This is the work queue for both
323
+ * `devsmind embed` and the auto-embed hooks in `describe`/`add_description` — resumable and
324
+ * idempotent by construction, same shape as `describe.ts`'s own `WHERE description IS NULL`.
325
+ * `force: true` returns every described node regardless of vector state (model upgrades).
326
+ */
327
+ getNodesNeedingEmbedding(force?: boolean): DbNode[];
328
+ /**
329
+ * The `nodes.aliases` column is a JSON-array-in-TEXT blob — better-sqlite3 hands it back as a
330
+ * raw string, not a parsed array, on every `stmt.all()`/`stmt.get()`. A JSON string masquerading
331
+ * as `string[]` is a silent-wrong-answer hazard (both have `.length`, so a bug here would not
332
+ * throw, just quietly misbehave — e.g. counting characters instead of aliases). EVERY raw SQL
333
+ * read of the `nodes` table must route its rows through {@link parseNodeRow}/{@link
334
+ * parseNodeRows}, never cast `as DbNode`/`as DbNode[]` directly.
335
+ */
336
+ private static parseNodeAliases;
337
+ private static parseNodeRow;
338
+ private static parseNodeRows;
106
339
  getNode(id: string): DbNode | null;
107
340
  deleteNode(id: string): void;
108
341
  deprecateNode(id: string): void;
109
342
  /** `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
343
  renameNode(oldId: string, newId: string, newName?: string, newFilePath?: string): void;
344
+ /**
345
+ * Merges `fromId` into `intoId` — the batch graph-fix session's `merge_nodes` correction, for
346
+ * when curation (or a human reviewing feedback) decides two node candidates were never really
347
+ * distinct entities. Unlike `renameNode` (which moves everything to a FRESH id), `intoId`
348
+ * already exists with its own rows here: `fromId`'s connections (both directions) and history
349
+ * are reassigned onto it, `fromId`'s aliases (plus its own name, so old references by that name
350
+ * still resolve) are folded into `intoId`'s alias set, and `fromId` is deprecated — not hard
351
+ * deleted, so its history stays reachable and the merge itself stays a reversible correction,
352
+ * not a destructive one.
353
+ */
354
+ mergeNodes(fromId: string, intoId: string): void;
111
355
  /**
112
356
  * Rewrites a history/[id].json file's identifying fields (node_id, node_metadata) in
113
357
  * place, leaving code_snapshot/reasoning/timestamps untouched. Used after a rename so
@@ -137,14 +381,65 @@ export declare class DevMindDatabase {
137
381
  private deleteHistoryFilesForNode;
138
382
  addConnection(sourceNodeId: string, targetNodeId: string): void;
139
383
  removeConnection(sourceNodeId: string, targetNodeId: string): void;
140
- getConnections(nodeId: string): {
384
+ /**
385
+ * `opts.limit`/`opts.offset` page a hub node's caller/callee list deterministically —
386
+ * `ORDER BY file_path, name` so a repeated call with the same offset returns the same slice,
387
+ * and so the local/nearby callers a reader actually wants tend to sort ahead of a scattered
388
+ * cross-repo tail (same file_path groups together). Omitting `opts` returns every row, exactly
389
+ * as before — every pre-existing call site keeps working unchanged.
390
+ */
391
+ getConnections(nodeId: string, opts?: {
392
+ limit?: number;
393
+ offset?: number;
394
+ }): {
141
395
  uses: DbNode[];
142
396
  usedBy: DbNode[];
143
397
  };
398
+ /**
399
+ * Batched, COUNT-only connection degree for many nodes at once — the search-result drill-in
400
+ * hooks need this for ~20 nodes per call, and `getConnections` per-node would mean 20 pairs of
401
+ * full-row-fetching queries. One grouped COUNT each way instead (same shape as the degree
402
+ * subquery in {@link getGodEntities}). IDs not present in `node_connections` still get a
403
+ * `{uses:0, usedBy:0}` entry so callers never need an existence check.
404
+ */
405
+ getConnectionCounts(ids: string[]): Map<string, {
406
+ uses: number;
407
+ usedBy: number;
408
+ }>;
409
+ /** Batched history-entry count for many nodes at once — metadata only, no disk reads. */
410
+ getHistoryCounts(ids: string[]): Map<string, number>;
411
+ /**
412
+ * Batched most-recent history timestamp for many nodes at once. Deliberately SQL-only (`MAX`
413
+ * over the indexed `updated_at` column) — unlike {@link getLatestHistory}, this never touches
414
+ * `populateHistoryFromDisk`, so it costs nothing beyond the query itself.
415
+ */
416
+ getLastUpdatedMap(ids: string[]): Map<string, string>;
144
417
  getLatestHistory(nodeId: string): DbHistory | null;
145
- listHistory(nodeId: string): Omit<DbHistory, 'code_snapshot' | 'reasoning'>[];
418
+ listHistory(nodeId: string): Omit<DbHistory, 'code_snapshot' | 'reasoning' | 'edits'>[];
146
419
  getHistoryEntry(id: string): DbHistory | null;
147
420
  getFullHistory(nodeId: string): DbHistory[];
421
+ /**
422
+ * The last `limit` history entries' reasoning + timestamps only — no `code_snapshot`/`edits`.
423
+ * Built for `get_node_code`'s default `history:"recent"` mode, which already returns the
424
+ * CURRENT code: repeating past snapshots inline would just duplicate what's already in the
425
+ * response. The full trail (snapshots + diffable edits) is `history:"full"`, served by
426
+ * {@link getHistoryPage}. This answers "why does this look the way it does" cheaply enough to
427
+ * attach to every get_node_code call by default, instead of leaving that as a round trip an AI
428
+ * has to remember to make (or skip, and re-break a decision it never saw).
429
+ */
430
+ getRecentHistorySummaries(nodeId: string, limit: number): Array<Pick<DbHistory, 'id' | 'session_id' | 'created_at' | 'updated_at' | 'reasoning'>>;
431
+ /**
432
+ * `history:"full"`'s backing query — the same full-fidelity payload as {@link getFullHistory}
433
+ * (code_snapshot + diffable edits per entry), but LIMIT/OFFSET applied IN SQL before any disk
434
+ * read happens, not by slicing an already-fully-loaded array. `getFullHistory` reads every
435
+ * revision's JSON off disk unconditionally; for a node with dozens of revisions that is dozens
436
+ * of synchronous file reads to serve a request for the newest 5. `total` is the true count
437
+ * before paging, same honesty contract as `nodes_total`/`files_total` elsewhere.
438
+ */
439
+ getHistoryPage(nodeId: string, limit: number, offset: number): {
440
+ entries: DbHistory[];
441
+ total: number;
442
+ };
148
443
  /** Distinct source node ids of edges pointing INTO this node (its "used-by" callers). */
149
444
  getInboundSources(nodeId: string): string[];
150
445
  getLatestCode(nodeId: string): {
@@ -169,69 +464,167 @@ export declare class DevMindDatabase {
169
464
  updateHistory(params: {
170
465
  node_id: string;
171
466
  code_snapshot: string;
467
+ /**
468
+ * The entity's text before this edit, when the caller knows it (`edit_node` does; it holds
469
+ * the pre-edit file). `null` means the entity did not exist yet — a pure addition. `undefined`
470
+ * means the caller has no before-state at all (`stage_change`), and no edit is recorded to
471
+ * the trail: an entry with nothing to compare against gets no diff and no revert.
472
+ */
473
+ code_before?: string | null;
172
474
  reasoning: string | ReasoningObject;
173
475
  session_id?: string;
174
476
  }): DbHistory;
175
477
  /**
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.
478
+ * Removes the newest recorded edit from a history entry, leaving no trace of it.
479
+ *
480
+ * There used to be a citation guard here: a history row cited by a workflow step was emptied
481
+ * rather than deleted, so the step was not left pointing at nothing. Workflow steps record
482
+ * `node_ids` now, not history ids, so nothing cites a history row any more and the guard had
483
+ * nothing left to check. Keeping it would have meant keeping the `history_ids` column alive
484
+ * purely to protect a reference nothing makes.
485
+ *
486
+ * Callers are expected to have restored the file already; this only unwinds what was written
487
+ * about it.
181
488
  */
182
- searchNodes(query: string, opts?: {
183
- is_regex?: boolean;
489
+ eraseLastEdit(historyId: string): {
490
+ erased: boolean;
491
+ entry_deleted: boolean;
492
+ reason?: string;
493
+ };
494
+ /**
495
+ * The one search tool, covering both worlds in a single call:
496
+ * - a primary `nodes` bucket — the indexed graph, found by exact identifier, then by three
497
+ * fused rankers (BM25 over metadata, vector over descriptions, and code-body match), and
498
+ * - a last-resort `files` bucket — a real filesystem grep of the configured repos, so files
499
+ * the graph never models (CSS, JSON, config, markup, un-indexed code) are finally covered
500
+ * in the same call instead of sending the caller off to an external grep.
501
+ *
502
+ * Inputs play to each layer's strength: the natural-language `query` drives the semantic vector
503
+ * layer (and BM25); `opts.keywords` (literal, OR) drive grep and the code-body match, and also
504
+ * feed BM25. If no keywords are given they're derived from the query's significant tokens, so a
505
+ * natural-only call still gets code + file coverage.
506
+ *
507
+ * Speed is the point — the two slow layers (vector, grep) run concurrently, and the old ~9k
508
+ * per-node snapshot read (the tool's former ~10-30s cost) is gone: code-body matching now rides
509
+ * the single grep walk. See `grep.ts` and Phase 4 of the plan.
510
+ */
511
+ /**
512
+ * Attaches the drill-in hooks (`uses`/`used_by`/`history_count`/`last_updated`) to a batch of
513
+ * search results in place, via ONE grouped query per hook instead of per-node fetches — see
514
+ * {@link getConnectionCounts}/{@link getHistoryCounts}/{@link getLastUpdatedMap}. This is the
515
+ * signal that turns a search result from a dead end into something worth drilling into with
516
+ * `get_node_graph`/`get_node_history` — without it, nothing hints there's more to find.
517
+ */
518
+ private attachDrillInHooks;
519
+ searchNodes(query: string | undefined, opts?: {
520
+ pattern?: string;
521
+ path?: string;
184
522
  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
- }[];
523
+ offset?: number;
524
+ limit?: number;
525
+ compact?: boolean;
526
+ }): Promise<SearchNodesResult>;
527
+ /**
528
+ * The token-ranked (BM25) half of {@link searchNodes}. Looks up every query/keyword token in
529
+ * the local `node_tokens` index (rebuilding it first if stale — see {@link ensureSearchIndexFresh}),
530
+ * scores each candidate with {@link scoreCandidate} (IDF-weighted, saturating TF, per-field
531
+ * weights favoring `description`), and applies the noise floor. Metadata only — identifier / id
532
+ * / path / description / reasoning. Code-body matching is NO LONGER folded in here: it used to
533
+ * read one history-JSON per node (~9k serial reads = the tool's ~10-30s cost), and is now
534
+ * served far faster by the real filesystem grep in `searchNodes` (see `mapGrepHitsToNodes`).
535
+ * Returns nodes tagged `matched_via:'fuzzy'`, ranked; `searchNodes` re-fuses them by RRF.
536
+ */
537
+ private tokenSearchNodes;
538
+ /** Absolute filesystem roots for every configured repo — the search space for `grepRepos`. */
539
+ private repoRoots;
540
+ /**
541
+ * Resolves and validates `search_nodes`' optional `path` scope a single folder or file the
542
+ * grep walk restricts to, instead of always walking every configured repo. A path outside every
543
+ * repo root is REJECTED (thrown, not silently widened to "search everything") a scope that
544
+ * can't be honored should fail loudly, since silently ignoring it would search far more than
545
+ * the caller asked for. `canonicalizePath` (already used throughout this file for path
546
+ * comparisons) lowercases the Windows drive letter so `C:\...` and `c:\...` compare equal.
547
+ */
548
+ private resolveSearchScopePath;
549
+ /**
550
+ * Maps raw grep hits back to graph nodes — this is "code search that returns nodes", rebuilt
551
+ * on top of the single filesystem walk instead of the old ~9k per-node snapshot reads. For each
552
+ * file that had hits AND contains indexed nodes: an AST-parseable file is resolved precisely
553
+ * (each hit line → the one node whose line-range contains it, via {@link locateNodeInFile}), so
554
+ * a keyword hitting one method in a 10-method file surfaces THAT node, not all ten. A non-AST
555
+ * indexed file (a staged `.py`/`.go` node) falls back to coarse file→node. Returns node ids in
556
+ * match-strength order (files with more hits first) plus the matching lines, for RRF + display.
557
+ *
558
+ * Only the top {@link CODE_MATCH_FILE_CAP} files by hit count are AST-resolved — measured on a
559
+ * real 8-repo, 15k-grep-hit query, this step alone was 8.2s of a 9.6s total before the cap, one
560
+ * `locateNodeInFile` AST walk per node per matched file with no bound on how many files that
561
+ * could be. It doesn't cost result quality: the fused ranking downstream keeps only the top ~20
562
+ * nodes anyway, and files with more hits are exactly the ones most likely to place there — files
563
+ * past the cap would almost never have survived to the final result even fully resolved.
564
+ */
565
+ private mapGrepHitsToNodes;
566
+ /**
567
+ * Resolves every indexed node's line span within one AST-parseable file — extracted out of
568
+ * `mapGrepHitsToNodes` so `annotateSampleLinesWithSymbol` can reuse the identical span logic
569
+ * instead of re-deriving it. Computed ONCE per file (not per line/hit): a file with several
570
+ * sample lines would otherwise repeat the same `locateNodeInFile` AST walk once per line.
571
+ * `nodes` is accepted rather than re-fetched so an existing `getNodesByFilePath` result (as
572
+ * `mapGrepHitsToNodes` already has) isn't queried twice.
573
+ */
574
+ private computeSymbolSpans;
575
+ /**
576
+ * Mutates each `RankedFile`'s `sample_lines` in place, tagging every line with the function/class
577
+ * that contains it — the insight a plain filesystem grep can never give: not just "line 87
578
+ * matched" but "line 87, inside `onLikeTap`". This is the annotation `search_nodes` offers that
579
+ * makes it worth more than grep for a hit that lands inside real source.
580
+ *
581
+ * Deliberately bounded to the ALREADY-CAPPED page (`files`, ≤`maxFiles` entries × ≤5 sample
582
+ * lines each — at most ~125 lookups) rather than every raw grep hit. `mapGrepHitsToNodes` is
583
+ * measured at 8.2s of a 9.6s query on a real 8-repo search (see its doc comment) precisely from
584
+ * unbounded per-hit AST resolution; annotating only what's actually returned keeps this at a
585
+ * small, constant added cost regardless of how broad the pattern was or how many total hits it
586
+ * produced. Silently no-ops for a file that isn't indexed or isn't AST-parseable — a `symbol` on
587
+ * a sample line is a bonus, never a requirement.
588
+ */
589
+ private annotateSampleLinesWithSymbol;
590
+ /**
591
+ * The semantic half of hybrid search: embeds the query with the same vendored ONNX model used
592
+ * to embed every node's description (see embedder.ts), then linear-scans `node_vectors` for
593
+ * cosine similarity. Linear is fine at this scale — ~9k nodes × 384 int8 dims is a few million
594
+ * integer multiplications, well under a millisecond, far below the model's own inference time
595
+ * for the query itself; revisit only past ~100k nodes. Returns each surviving node's raw cosine
596
+ * alongside its id — the ORDER drives RRF, but the cosine value is kept so `searchNodes` can turn
597
+ * it into a human-meaningful confidence (a 0.7 cosine is a strong match; a 0.36 is a weak one —
598
+ * the fused RRF float can't express that). Returns [] — never throws — if the embedder is
599
+ * unavailable, so `searchNodes` degrades to BM25+grep exactly as before.
600
+ */
601
+ private vectorSearchNodes;
602
+ /**
603
+ * Cheap, single-pass signal for "has the graph changed since node_tokens was last built":
604
+ * how many non-deprecated nodes exist, the total length of every description (catches a
605
+ * description being added OR edited — length almost never stays identical), and how many
606
+ * history rows exist (catches new reasoning). Deliberately NOT based on an `updated_at`
607
+ * column on `nodes` — there isn't one, and `syncFromDisk`'s destructive graph-rebuild pass
608
+ * writes nodes via raw SQL, not through a single method that could easily be hooked, so a
609
+ * write-path-by-write-path invalidation scheme would silently miss that path (and did, in an
610
+ * earlier draft of this). A fingerprint checked lazily at query time can't be missed the same
611
+ * way — it doesn't matter HOW the data changed, only THAT it did.
612
+ */
613
+ private searchIndexFingerprint;
614
+ /** Rebuilds `node_tokens` from scratch for every non-deprecated node's current
615
+ * identifier/id/path/description and EVERY revision's reasoning (not just the latest — a
616
+ * decision recorded three revisions ago must stay findable by `search_nodes`, since there is
617
+ * no separate decisions-only search tool anymore; `GROUP_CONCAT` folds every history row's
618
+ * reasoning into one field before tokenizing, so an older "Decision: …" is searchable exactly
619
+ * like the newest one). Cheap at realistic node counts (hundreds to low-thousands) — a single
620
+ * scan plus one batched transaction, not something that needs to be avoided; simplicity here
621
+ * is worth more than incremental upkeep that a bypassed write path could silently defeat. */
622
+ private rebuildSearchIndex;
623
+ /** Compares the current fingerprint against what `node_tokens` was last built from, stored in
624
+ * `system_meta`; rebuilds and updates the stored fingerprint only on a mismatch. A momentarily
625
+ * stale index (between a real change and the next search call) only costs ranking quality on
626
+ * that one call, never correctness — the next call rebuilds it. */
627
+ private ensureSearchIndexFresh;
235
628
  searchDecisions(query: string): {
236
629
  node_id: string;
237
630
  node_name: string;
@@ -256,10 +649,35 @@ export declare class DevMindDatabase {
256
649
  }[];
257
650
  getOrphanedNodes(): DbNode[];
258
651
  getAllNodes(): DbNode[];
652
+ /**
653
+ * The shared WHERE clause behind {@link listNodes} and {@link countNodes}. Extracted so the page
654
+ * and its total can never drift apart — a `total` computed from even slightly different criteria
655
+ * than the rows it describes is worse than no total at all, since it reads as authoritative.
656
+ */
657
+ private buildNodeFilterSql;
658
+ /**
659
+ * Total nodes matching a filter, independent of any page. Exists so `list_nodes` can tell
660
+ * "that's everything" from "there is more" — the same honesty contract `nodes_total` and
661
+ * `files_total` already keep for search.
662
+ */
663
+ countNodes(filter?: {
664
+ type?: string;
665
+ file_path?: string;
666
+ include_deprecated?: boolean;
667
+ }): number;
668
+ /**
669
+ * `limit`/`offset` are OPTIONAL and, when omitted, this returns every matching row exactly as
670
+ * before — the internal analysis callers (`analyze.ts`, `edges.ts`) legitimately need the whole
671
+ * graph, so paging is opt-in rather than a default that would silently truncate them. Ordering
672
+ * is applied only when paging, since an unordered LIMIT is a lottery: without it, "page 2" is
673
+ * not guaranteed to exclude what "page 1" already returned.
674
+ */
259
675
  listNodes(filter?: {
260
676
  type?: string;
261
677
  file_path?: string;
262
678
  include_deprecated?: boolean;
679
+ limit?: number;
680
+ offset?: number;
263
681
  }): DbNode[];
264
682
  getAllConnections(): DbConnection[];
265
683
  getAllHistory(): DbHistory[];
@@ -299,21 +717,86 @@ export declare class DevMindDatabase {
299
717
  updated_at: string;
300
718
  }[];
301
719
  private workflowsDir;
302
- /** Serializes the workflow + its steps + artifact index to disk so teammates can sync it via git. */
720
+ /**
721
+ * One-time backfill of `node_ids`/`reasoning` for steps written before v2, resolving each old
722
+ * `history_ids` entry to the node it belongs to. Runs at open, right after the ALTERs, and is
723
+ * self-limiting: it only touches rows where `node_ids IS NULL AND history_ids IS NOT NULL`, so
724
+ * the second open finds nothing and the query costs one indexed scan.
725
+ *
726
+ * Best-effort by nature. Because of the 1-hour history merge, an old step's `history_ids` can
727
+ * include rows an ADJACENT commit created, so a backfilled node list can be broader than what
728
+ * that step actually touched. That is acceptable for pre-v2 rows — they were already imprecise,
729
+ * which is exactly why the format changed — but it is why nothing presents backfilled data as
730
+ * exact. A step whose history rows have since been pruned keeps its `summary` and gets an empty
731
+ * list rather than being skipped, so it still appears on the timeline.
732
+ */
733
+ private backfillWorkflowStepNodeIds;
734
+ /**
735
+ * Serializes the workflow + its steps + artifact index to disk so teammates can sync it via git.
736
+ *
737
+ * Written as TWO files, and the split is the whole point:
738
+ *
739
+ * - `workflow.json` keeps the shape a v1 client understands, so an older build reading it loses
740
+ * nothing it ever had.
741
+ * - `v2.json` holds everything v1 has no field for (`archived`, and per-step `reasoning` /
742
+ * `node_ids` / `doc_paths`).
743
+ *
744
+ * A single file could not be made safe. `devsmind sync` calls `syncToDisk`, which re-serializes
745
+ * every workflow.json from whatever columns the local build knows about — so a teammate who
746
+ * pulls on an older version and syncs would rewrite every workflow and silently strip the new
747
+ * fields, then commit that loss for everyone. An older build has no idea `v2.json` exists, so it
748
+ * cannot rewrite it; the data survives the round trip and is merged back on the next read.
749
+ */
303
750
  private writeWorkflowToDisk;
751
+ /**
752
+ * Creates a workflow. Note what it deliberately does NOT do any more: set a global "active"
753
+ * pointer. Which workflow you are working on is a property of YOUR session, held locally — a
754
+ * workflow is a shared record, and one shared pointer meant two sessions (or two teammates,
755
+ * since the pointer synced through git) silently stole it from each other mid-work.
756
+ */
304
757
  createWorkflow(name: string, description: string): DbWorkflow;
305
758
  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;
759
+ /**
760
+ * Workflows newest-touched first which is the ordering that replaces the old `status` field.
761
+ * Live work floats up and abandoned threads sink on their own, so nothing has to be marked
762
+ * "completed" by hand (nobody ever did, and a lifecycle field nobody maintains just lies).
763
+ *
764
+ * `query` matches name AND description, the search `searchWorkflows` never actually did: it
765
+ * scanned step summaries and artifact names only, so looking a workflow up by its own name
766
+ * returned nothing. Paging mirrors `listNodes` — `total` is the true count before the page.
767
+ */
768
+ listWorkflows(opts?: {
769
+ query?: string;
770
+ includeArchived?: boolean;
771
+ limit?: number;
772
+ offset?: number;
773
+ }): DbWorkflow[];
774
+ countWorkflows(opts?: {
775
+ query?: string;
776
+ includeArchived?: boolean;
777
+ }): number;
778
+ /** Shared WHERE builder, so a page and its `total` can never describe different criteria. */
779
+ private buildWorkflowFilterSql;
780
+ /**
781
+ * Hides a workflow from the default listing. Deliberately NOT called "complete": a feature is
782
+ * never finished, it just stops being worked on, and the old `completed` status promised a
783
+ * lifecycle nobody maintained. Archiving claims only what it delivers, and is reversible.
784
+ */
785
+ setWorkflowArchived(id: string, archived: boolean): DbWorkflow;
786
+ /**
787
+ * Appends one step. A step is either a COMMIT (summary + reasoning + the node ids it touched) or
788
+ * a RESEARCH finding (summary + reasoning + the docs behind it, no nodes) — the second is the
789
+ * only record of work that produced a decision but no code, which nothing else in DevsMind
790
+ * captures: git has the diff and history has the per-node reasoning, but neither can tell you
791
+ * what was evaluated and rejected.
792
+ *
793
+ * `reasoning` is stored, not joined from `history`, deliberately — see DbWorkflowStep.
794
+ */
313
795
  addWorkflowStep(workflowId: string, opts: {
314
796
  summary: string;
315
- pendingTasks?: string;
316
- historyIds?: string[];
797
+ reasoning?: string;
798
+ nodeIds?: string[];
799
+ docPaths?: string[];
317
800
  sessionId?: string;
318
801
  }): DbWorkflowStep;
319
802
  /** Writes `content` to `.devmind/workflows/<workflowId>/<artifactId>_<sourceName>` and records the DB row. */
@@ -323,14 +806,28 @@ export declare class DevMindDatabase {
323
806
  sourceName: string;
324
807
  content: string;
325
808
  }): DbWorkflowArtifact;
809
+ /**
810
+ * The workflow's story: its steps in order, plus the docs attached to it.
811
+ *
812
+ * Paged, because this is now the ONLY read (it absorbed the old `workflow_get_steps`) and steps
813
+ * carry their own reasoning, so an unbounded version of it would be the largest response the
814
+ * server can produce. `steps_total` is exact regardless of the page — a short page must never
815
+ * read as "that is the whole story".
816
+ *
817
+ * Artifact CONTENT is deliberately not returned. It used to be inlined whole, which on an
818
+ * imported architecture doc is trivially tens of KB; the file path is enough, since the file is
819
+ * on disk and the caller can read exactly the part it needs.
820
+ */
326
821
  getWorkflowContext(id: string, opts?: {
327
- includeArtifactContent?: boolean;
822
+ limit?: number;
823
+ offset?: number;
824
+ last_n?: number;
328
825
  }): {
329
826
  workflow: DbWorkflow;
330
827
  steps: DbWorkflowStep[];
331
- artifacts: (DbWorkflowArtifact & {
332
- content?: string;
333
- })[];
828
+ steps_total: number;
829
+ steps_offset: number;
830
+ artifacts: DbWorkflowArtifact[];
334
831
  };
335
832
  /**
336
833
  * Returns steps for a workflow with optional pagination.
@@ -342,29 +839,6 @@ export declare class DevMindDatabase {
342
839
  offset?: number;
343
840
  last_n?: number;
344
841
  }): 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
842
  /**
369
843
  * Imports an existing flow/architecture doc as a paused workflow (not active — importing
370
844
  * a doc isn't the same as declaring active work). Idempotent on `name`: re-importing the
@@ -398,6 +872,8 @@ export declare class DevMindDatabase {
398
872
  };
399
873
  private populateHistoryFromDisk;
400
874
  private writeHistoryToDisk;
875
+ /** The configured developer identity (`.devmind/.env`'s DEVELOPER_NAME), or null if unset. */
876
+ getDeveloperName(): string | null;
401
877
  toRepoRelativePath(absolutePath: string): string;
402
878
  /**
403
879
  * Rejects a resolved path that escapes its expected root (e.g. via a stored
@@ -423,10 +899,18 @@ export declare class DevMindDatabase {
423
899
  */
424
900
  isPathAllowed(absPath: string): boolean;
425
901
  toAbsolutePath(repoRelativePath: string): string;
426
- syncFromDisk(): void;
902
+ syncFromDisk(onProgress?: (phase: string, done: number, total: number) => void): void;
427
903
  /** Escape LIKE metacharacters so a path is matched literally (use with ESCAPE '\\'). */
428
904
  private likeEscape;
429
905
  writeGraphToDisk(filePath: string): void;
906
+ /**
907
+ * Mirrors `writeGraphToDisk` exactly (same file-matching logic, same directory shape) but into
908
+ * a separate `vectors/` tree rather than inside `graph/*.json` — deliberately, so opaque base64
909
+ * blobs never pollute the human-readable, merge-friendly graph JSON. Deprecated nodes are
910
+ * skipped here (unlike the graph, which keeps them): `searchNodes` never queries a deprecated
911
+ * node's vector, so writing one is pure dead weight.
912
+ */
913
+ writeVectorsToDisk(filePath: string): void;
430
914
  /** Force-syncs all database nodes and workflows to disk JSON files. */
431
915
  syncToDisk(): void;
432
916
  }