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.
- package/README.md +326 -234
- package/dist/cli/activity.d.ts +9 -0
- package/dist/cli/activity.js +69 -0
- package/dist/cli/activity.js.map +1 -0
- package/dist/cli/describe.d.ts +31 -0
- package/dist/cli/describe.js +201 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/diff.d.ts +14 -0
- package/dist/cli/diff.js +161 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/embed.d.ts +6 -0
- package/dist/cli/embed.js +109 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/extract-agent.d.ts +66 -0
- package/dist/cli/extract-agent.js +192 -0
- package/dist/cli/extract-agent.js.map +1 -0
- package/dist/cli/feedback.d.ts +11 -0
- package/dist/cli/feedback.js +90 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/index.js +128 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +30 -0
- package/dist/cli/init.js +89 -18
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory-topics.d.ts +44 -0
- package/dist/cli/integrations/memory-topics.js +295 -0
- package/dist/cli/integrations/memory-topics.js.map +1 -0
- package/dist/cli/integrations/memory.d.ts +11 -5
- package/dist/cli/integrations/memory.js +132 -37
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/integrations/prompt.d.ts +8 -0
- package/dist/cli/integrations/prompt.js +24 -3
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +19 -5
- package/dist/cli/integrations/registry.js +23 -13
- package/dist/cli/integrations/registry.js.map +1 -1
- package/dist/cli/llm-client.d.ts +116 -0
- package/dist/cli/llm-client.js +322 -0
- package/dist/cli/llm-client.js.map +1 -0
- package/dist/cli/rule.d.ts +22 -2
- package/dist/cli/rule.js +113 -32
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/runner.d.ts +21 -0
- package/dist/cli/runner.js +94 -276
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/sync-progress.d.ts +9 -0
- package/dist/cli/sync-progress.js +41 -0
- package/dist/cli/sync-progress.js.map +1 -0
- package/dist/cli/sync.js +5 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/view.js +3 -2
- package/dist/cli/view.js.map +1 -1
- package/dist/cli/workflow.js +26 -20
- package/dist/cli/workflow.js.map +1 -1
- package/dist/db/activity.d.ts +200 -0
- package/dist/db/activity.js +347 -0
- package/dist/db/activity.js.map +1 -0
- package/dist/db/analyze.js +18 -6
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +583 -99
- package/dist/db/database.js +1825 -553
- package/dist/db/database.js.map +1 -1
- package/dist/db/edges.d.ts +24 -0
- package/dist/db/edges.js +84 -0
- package/dist/db/edges.js.map +1 -1
- package/dist/db/embedder.d.ts +39 -0
- package/dist/db/embedder.js +0 -0
- package/dist/db/embedder.js.map +1 -0
- package/dist/db/feedback.d.ts +128 -0
- package/dist/db/feedback.js +182 -0
- package/dist/db/feedback.js.map +1 -0
- package/dist/db/file-diff.d.ts +32 -0
- package/dist/db/file-diff.js +110 -0
- package/dist/db/file-diff.js.map +1 -0
- package/dist/db/grep.d.ts +78 -0
- package/dist/db/grep.js +475 -0
- package/dist/db/grep.js.map +1 -0
- package/dist/db/message-revert.d.ts +63 -0
- package/dist/db/message-revert.js +258 -0
- package/dist/db/message-revert.js.map +1 -0
- package/dist/db/revert.d.ts +31 -0
- package/dist/db/revert.js +108 -0
- package/dist/db/revert.js.map +1 -0
- package/dist/db/schema.d.ts +49 -4
- package/dist/db/schema.js +125 -73
- package/dist/db/schema.js.map +1 -1
- package/dist/db/search-index.d.ts +65 -0
- package/dist/db/search-index.js +74 -0
- package/dist/db/search-index.js.map +1 -0
- package/dist/db/staging.d.ts +90 -5
- package/dist/db/staging.js +148 -22
- package/dist/db/staging.js.map +1 -1
- package/dist/mcp/server.d.ts +29 -7
- package/dist/mcp/server.js +2584 -809
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
- package/dist/mcp/vendor/force-graph.min.js +5 -0
- package/dist/mcp/vendor/model/model_int8.onnx +0 -0
- package/dist/mcp/vendor/model/vocab.txt +30522 -0
- package/dist/mcp/vendor/three.min.js +7 -0
- package/dist/mcp/view.css +419 -0
- package/dist/mcp/view.html +161 -0
- package/dist/mcp/view.js +245 -0
- package/dist/mcp/view_chat.js +382 -0
- package/dist/mcp/view_graph.js +576 -0
- package/dist/mcp/visualizer.d.ts +25 -2
- package/dist/mcp/visualizer.js +31 -4
- package/dist/mcp/visualizer.js.map +1 -1
- package/dist/utils/ast.d.ts +89 -0
- package/dist/utils/ast.js +378 -10
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/diff.d.ts +44 -0
- package/dist/utils/diff.js +78 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/edit.d.ts +6 -0
- package/dist/utils/edit.js +47 -14
- package/dist/utils/edit.js.map +1 -1
- package/dist/utils/scanner.js +12 -4
- package/dist/utils/scanner.js.map +1 -1
- package/dist/utils/tokenize.d.ts +45 -0
- package/dist/utils/tokenize.js +129 -0
- package/dist/utils/tokenize.js.map +1 -0
- package/dist/utils/version.d.ts +14 -0
- package/dist/utils/version.js +61 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +18 -5
- package/dist/mcp/visualizer_2d.html +0 -635
- package/dist/mcp/visualizer_3d.html +0 -613
package/dist/db/database.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*/
|
|
199
|
-
private
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
822
|
+
limit?: number;
|
|
823
|
+
offset?: number;
|
|
824
|
+
last_n?: number;
|
|
328
825
|
}): {
|
|
329
826
|
workflow: DbWorkflow;
|
|
330
827
|
steps: DbWorkflowStep[];
|
|
331
|
-
|
|
332
|
-
|
|
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
|
}
|