dsh-plugin-dbgraph-store 0.0.0-stage → 0.1.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/lib/aliases.d.ts +34 -0
- package/lib/aliases.js +92 -0
- package/lib/connections.d.ts +42 -0
- package/lib/connections.js +172 -0
- package/lib/database.d.ts +61 -0
- package/lib/database.js +176 -0
- package/lib/index.d.ts +222 -0
- package/lib/index.js +414 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +23 -0
- package/lib/normalize.d.ts +59 -0
- package/lib/normalize.js +158 -0
- package/lib/paths.d.ts +83 -0
- package/lib/paths.js +130 -0
- package/lib/reader.d.ts +207 -0
- package/lib/reader.js +713 -0
- package/lib/resolve.d.ts +70 -0
- package/lib/resolve.js +762 -0
- package/lib/rust-engine.d.ts +108 -0
- package/lib/rust-engine.js +148 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +141 -0
- package/lib/terms.d.ts +93 -0
- package/lib/terms.js +146 -0
- package/lib/writer-js.d.ts +42 -0
- package/lib/writer-js.js +269 -0
- package/package.json +64 -3
- package/README.md +0 -3
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The offline store plugin: `connections.json`, the index reader, the resolver, and the JavaScript
|
|
3
|
+
* write path, mounted as one `DbgraphStore` on `ctx.dbgraph`.
|
|
4
|
+
*
|
|
5
|
+
* The store is host-local by construction — `node:sqlite` opens files on the machine running the
|
|
6
|
+
* harness — and it reaches no network at all: dialects own every round-trip. `write()` is the one
|
|
7
|
+
* mutating operation, and it follows the protocol the seam documents: staging file → rename → digest
|
|
8
|
+
* sidecar, never the other way round.
|
|
9
|
+
*
|
|
10
|
+
* Namespace plugin (named exports, no default). Lifecycle is effect-scoped: disposal unregisters from
|
|
11
|
+
* `ctx.dbgraph` and then closes every cached connection.
|
|
12
|
+
* @module dsh-plugin-dbgraph-store
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import type { AliasGroup, ColumnsRequest, ColumnsResult, ConnectionIdType, DbgraphStore, ImpactRequest, ImpactResult, IndexReport, IndexState, ObjectsRequest, ObjectsResult, RawCatalog, RefsRequest, RefsResult, Resolution, SchemaRequest, SchemaResult, FindRequest, StoredConnection, WriteOptions } from 'dsh-plugin-dbgraph-service';
|
|
16
|
+
export { ADDON_ENV, loadRustIndexer, parseRustReport, resolveAddonPath, serializePayload, } from './rust-engine.ts';
|
|
17
|
+
export type { RustBuildInput, RustBuildReport, RustIndexer, RustLoad, RustPayload } from './rust-engine.ts';
|
|
18
|
+
export { writeIndexJs } from './writer-js.ts';
|
|
19
|
+
export type { JsWriteInput } from './writer-js.ts';
|
|
20
|
+
export { assignCatalogIds, buildTerms, TERM_WEIGHTS } from './terms.ts';
|
|
21
|
+
export type { CatalogIds, SearchRow, TermBuild, TermRow } from './terms.ts';
|
|
22
|
+
export { buildEntityConcepts, DEFAULT_RESOLVE_OPTIONS } from './resolve.ts';
|
|
23
|
+
export type { ResolveOptions } from './resolve.ts';
|
|
24
|
+
export { ROOT_ENV, STALE_STAGING_MAX_AGE_MS, connectionDir, connectionsPath, indexRoot, indexPath, statusPath, stagingIndexPath, sweepStaleStaging, } from './paths.ts';
|
|
25
|
+
export { CONNECTIONS_FORMAT_VERSION, findConnection, readConnections, writeConnections } from './connections.ts';
|
|
26
|
+
export { STATUS_FORMAT_VERSION, readStatusFile } from './status.ts';
|
|
27
|
+
export type { StatusEntryInput } from './status.ts';
|
|
28
|
+
export { IndexReader, DEFAULT_READER_CONFIG } from './reader.ts';
|
|
29
|
+
export type { ColumnRecord, ObjectLookup, ObjectRecord, ReaderConfig } from './reader.ts';
|
|
30
|
+
export { IndexPool, openIndex } from './database.ts';
|
|
31
|
+
export type { OpenedIndex } from './database.ts';
|
|
32
|
+
/** Cordis plugin name, for loader diagnostics. */
|
|
33
|
+
export declare const name = "dbgraph-store";
|
|
34
|
+
/** Services this plugin needs before it can register. */
|
|
35
|
+
export declare const inject: string[];
|
|
36
|
+
/** Store identity this build reserves on the seam. */
|
|
37
|
+
export declare const DEFAULT_STORE_ID = "dbgraph-store";
|
|
38
|
+
/** Plugin configuration: the only bounds a deployment may retune. */
|
|
39
|
+
export interface Config {
|
|
40
|
+
/** Index root; defaults to `DSH_DBGRAPH_ROOT` or `~/.dsh/dbgraph`. */
|
|
41
|
+
readonly root?: string;
|
|
42
|
+
/** Hours after which an L2 snapshot is reported stale. Default 6. */
|
|
43
|
+
readonly indexStaleHours?: number;
|
|
44
|
+
/** Resolver gate below which an answer is `ambiguous`. Default 0.6. */
|
|
45
|
+
readonly minConfidence?: number;
|
|
46
|
+
/** Top-2 margin below which an answer is `ambiguous`. Default 0.15. */
|
|
47
|
+
readonly ambiguityGap?: number;
|
|
48
|
+
/** Alias table; defaults to the service's `ZH_ALIASES`. */
|
|
49
|
+
readonly aliases?: readonly AliasGroup[];
|
|
50
|
+
/** Index files kept open at once. Default 4. */
|
|
51
|
+
readonly maxOpenIndexes?: number;
|
|
52
|
+
/** Objects one `schema` call returns. Default 500. */
|
|
53
|
+
readonly maxSchemaObjects?: number;
|
|
54
|
+
/** Columns one object may expose to a match. Default 500. */
|
|
55
|
+
readonly maxColumns?: number;
|
|
56
|
+
/** Objects one `objects` call returns when the request names no limit. Default 50. */
|
|
57
|
+
readonly defaultObjectLimit?: number;
|
|
58
|
+
/** Digest entries written into the prompt sidecar. Default 120. */
|
|
59
|
+
readonly maxDigestObjects?: number;
|
|
60
|
+
/** Foreign-key hops one `impact` walk follows. Default 3. */
|
|
61
|
+
readonly impactDepth?: number;
|
|
62
|
+
/** Edges one `impact` or `refs` call may return. Default 200. */
|
|
63
|
+
readonly maxRelations?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One in-flight writer per connection.
|
|
67
|
+
*
|
|
68
|
+
* The staging path is unique per run, so two runs can no longer share a file — what still collides is
|
|
69
|
+
* the live index and the digest that describes it. Each run renames its own complete file onto
|
|
70
|
+
* `<connection>/index.db` and then writes a `status.json` entry; interleaved, the surviving index
|
|
71
|
+
* could be run A's while the digest reports run B's counts and timestamps, and one of the two reports
|
|
72
|
+
* would describe a file it did not leave behind. A run holds its connection id for that whole
|
|
73
|
+
* rename-then-digest sequence, and a second one is refused with `DBGRAPH_CONFLICT` instead of racing.
|
|
74
|
+
*/
|
|
75
|
+
export declare class WriteGuard {
|
|
76
|
+
private readonly active;
|
|
77
|
+
/**
|
|
78
|
+
* Whether a write for this connection is in flight.
|
|
79
|
+
* @param connection - connection identity.
|
|
80
|
+
* @returns true when a run holds it.
|
|
81
|
+
*/
|
|
82
|
+
has(connection: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Run `work` while holding the connection, releasing it on both outcomes.
|
|
85
|
+
* @param connection - connection identity.
|
|
86
|
+
* @param work - the write to run.
|
|
87
|
+
* @returns the write's result.
|
|
88
|
+
* @throws DbgraphError with `DBGRAPH_CONFLICT` when another write already holds the connection.
|
|
89
|
+
*/
|
|
90
|
+
run<T>(connection: string, work: () => Promise<T>): Promise<T>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The store implementation: one reader (which owns the connection pool) plus the two files it manages.
|
|
94
|
+
*/
|
|
95
|
+
export declare class DbgraphIndexStore implements DbgraphStore {
|
|
96
|
+
/** Store identity, as the seam's conflict message renders it. */
|
|
97
|
+
readonly id: string;
|
|
98
|
+
/** Absolute directory this store keeps its files under (also read by the tool's prompt digest). */
|
|
99
|
+
readonly root: string;
|
|
100
|
+
private readonly aliases;
|
|
101
|
+
private readonly reader;
|
|
102
|
+
/** One in-flight writer per connection; see {@link WriteGuard}. */
|
|
103
|
+
readonly writes: WriteGuard;
|
|
104
|
+
private readonly resolveOptions;
|
|
105
|
+
private readonly maxDigestObjects;
|
|
106
|
+
/**
|
|
107
|
+
* @param config - plugin configuration.
|
|
108
|
+
*/
|
|
109
|
+
constructor(config?: Config);
|
|
110
|
+
/**
|
|
111
|
+
* Every configured connection, in file order.
|
|
112
|
+
* @returns the list; empty when no file exists yet.
|
|
113
|
+
*/
|
|
114
|
+
connections(): Promise<readonly StoredConnection[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Replace the connection list (round-trip through {@link readConnections} to modify one entry).
|
|
117
|
+
* @param connections - the full list.
|
|
118
|
+
* @returns nothing.
|
|
119
|
+
*/
|
|
120
|
+
saveConnections(connections: readonly StoredConnection[]): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* One connection's index state; an unusable or absent index reports empty and stale.
|
|
123
|
+
* @param connection - connection identity.
|
|
124
|
+
* @returns the state.
|
|
125
|
+
*/
|
|
126
|
+
state(connection: string): Promise<IndexState>;
|
|
127
|
+
/**
|
|
128
|
+
* Structural overview, from the index.
|
|
129
|
+
* @param request - which connection.
|
|
130
|
+
* @returns the overview.
|
|
131
|
+
*/
|
|
132
|
+
schema(request: SchemaRequest): Promise<SchemaResult>;
|
|
133
|
+
/**
|
|
134
|
+
* Object listing, from the index.
|
|
135
|
+
* @param request - which connection and what to look for.
|
|
136
|
+
* @returns the listing.
|
|
137
|
+
*/
|
|
138
|
+
objects(request: ObjectsRequest): Promise<ObjectsResult>;
|
|
139
|
+
/**
|
|
140
|
+
* One object's columns, from the index.
|
|
141
|
+
* @param request - which object.
|
|
142
|
+
* @returns the listing.
|
|
143
|
+
*/
|
|
144
|
+
columns(request: ColumnsRequest): Promise<ColumnsResult>;
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a request to an object, its columns, and a parameterized predicate — no network, no
|
|
147
|
+
* execution. The service composes this with a dialect when it runs `find`.
|
|
148
|
+
* @param request - what the user asked about.
|
|
149
|
+
* @returns the resolution.
|
|
150
|
+
*/
|
|
151
|
+
resolve(request: FindRequest): Promise<Resolution>;
|
|
152
|
+
/**
|
|
153
|
+
* Objects a change to this one reaches.
|
|
154
|
+
* @param request - the object to trace.
|
|
155
|
+
* @returns the reached objects.
|
|
156
|
+
*/
|
|
157
|
+
impact(request: ImpactRequest): Promise<ImpactResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Objects directly referencing this one.
|
|
160
|
+
* @param request - the object to trace.
|
|
161
|
+
* @returns the referencing objects.
|
|
162
|
+
*/
|
|
163
|
+
refs(request: RefsRequest): Promise<RefsResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Write a snapshot, replacing the connection's index.
|
|
166
|
+
*
|
|
167
|
+
* Order is part of the contract: the index is renamed into place first, the digest sidecar second,
|
|
168
|
+
* so no prompt ever describes an index that is not there yet. A sidecar failure is reported as a
|
|
169
|
+
* warning on an otherwise successful report — the index itself is already durable and useful.
|
|
170
|
+
* @param connection - connection identity.
|
|
171
|
+
* @param catalog - the snapshot to write.
|
|
172
|
+
* @param options - writer preference and the alias table.
|
|
173
|
+
* @returns the run's report.
|
|
174
|
+
*/
|
|
175
|
+
write(connection: ConnectionIdType, catalog: RawCatalog, options: WriteOptions): Promise<IndexReport>;
|
|
176
|
+
/**
|
|
177
|
+
* The write body, run while this connection's in-flight slot is held.
|
|
178
|
+
* @param connection - connection identity, already validated.
|
|
179
|
+
* @param catalog - the snapshot to write.
|
|
180
|
+
* @param options - writer preference and the alias table.
|
|
181
|
+
* @returns the run's report.
|
|
182
|
+
*/
|
|
183
|
+
private writeLocked;
|
|
184
|
+
/**
|
|
185
|
+
* Write one index through the native kernel, then rename it into place.
|
|
186
|
+
*
|
|
187
|
+
* The addon owns exactly one file — the staging path — and deletes it on any failure. This method
|
|
188
|
+
* owns the two decisions the addon must not make: that a successful build is renamed over the live
|
|
189
|
+
* index, and that a failed or nonsensical one never is.
|
|
190
|
+
* @param engine - the loaded native kernel.
|
|
191
|
+
* @param connection - connection identity.
|
|
192
|
+
* @param catalog - the snapshot to write.
|
|
193
|
+
* @param aliases - the alias table.
|
|
194
|
+
* @param startedAt - `performance.now()` when the call began.
|
|
195
|
+
* @returns the run's report, with `writer: 'rust'`.
|
|
196
|
+
*/
|
|
197
|
+
private writeWithRust;
|
|
198
|
+
/**
|
|
199
|
+
* Close every cached connection. Idempotent; the store is unusable afterwards.
|
|
200
|
+
*/
|
|
201
|
+
dispose(): void;
|
|
202
|
+
/**
|
|
203
|
+
* A connection's display label, when `connections.json` has one. A malformed file must not stop an
|
|
204
|
+
* index write — the label is decoration, the index is the product.
|
|
205
|
+
* @param connection - connection identity.
|
|
206
|
+
* @returns the label, or undefined.
|
|
207
|
+
*/
|
|
208
|
+
private labelOf;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build a store without mounting it (tests, and callers that want the object directly).
|
|
212
|
+
* @param config - plugin configuration.
|
|
213
|
+
* @returns the store.
|
|
214
|
+
*/
|
|
215
|
+
export declare function createStore(config?: Config): DbgraphIndexStore;
|
|
216
|
+
/**
|
|
217
|
+
* Register the store on `ctx.dbgraph`.
|
|
218
|
+
* @param ctx - the plugin context (must inject `dbgraph`).
|
|
219
|
+
* @param config - resolved plugin configuration.
|
|
220
|
+
*/
|
|
221
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
222
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The offline store plugin: `connections.json`, the index reader, the resolver, and the JavaScript
|
|
3
|
+
* write path, mounted as one `DbgraphStore` on `ctx.dbgraph`.
|
|
4
|
+
*
|
|
5
|
+
* The store is host-local by construction — `node:sqlite` opens files on the machine running the
|
|
6
|
+
* harness — and it reaches no network at all: dialects own every round-trip. `write()` is the one
|
|
7
|
+
* mutating operation, and it follows the protocol the seam documents: staging file → rename → digest
|
|
8
|
+
* sidecar, never the other way round.
|
|
9
|
+
*
|
|
10
|
+
* Namespace plugin (named exports, no default). Lifecycle is effect-scoped: disposal unregisters from
|
|
11
|
+
* `ctx.dbgraph` and then closes every cached connection.
|
|
12
|
+
* @module dsh-plugin-dbgraph-store
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
import { performance } from 'node:perf_hooks';
|
|
17
|
+
import { DbgraphError, DEFAULT_INDEX_STALE_HOURS, DEFAULT_STATUS_DIGEST_OBJECTS, INDEX_META_KEYS, INDEX_SCHEMA_DDL, INDEX_SCHEMA_VERSION, ZH_ALIASES, isValidConnectionId, } from 'dsh-plugin-dbgraph-service';
|
|
18
|
+
import { findConnection, readConnections, writeConnections } from "./connections.js";
|
|
19
|
+
import { DEFAULT_READER_CONFIG, IndexReader } from "./reader.js";
|
|
20
|
+
import { buildStatusEntry, writeStatusEntry } from "./status.js";
|
|
21
|
+
import { indexPath, indexRoot, stagingIndexPath, sweepStaleStaging } from "./paths.js";
|
|
22
|
+
import { DEFAULT_RESOLVE_OPTIONS, resolveFind } from "./resolve.js";
|
|
23
|
+
import { writeIndexJs } from "./writer-js.js";
|
|
24
|
+
import { loadRustIndexer, parseRustReport } from "./rust-engine.js";
|
|
25
|
+
export { ADDON_ENV, loadRustIndexer, parseRustReport, resolveAddonPath, serializePayload, } from "./rust-engine.js";
|
|
26
|
+
export { writeIndexJs } from "./writer-js.js";
|
|
27
|
+
export { assignCatalogIds, buildTerms, TERM_WEIGHTS } from "./terms.js";
|
|
28
|
+
export { buildEntityConcepts, DEFAULT_RESOLVE_OPTIONS } from "./resolve.js";
|
|
29
|
+
export { ROOT_ENV, STALE_STAGING_MAX_AGE_MS, connectionDir, connectionsPath, indexRoot, indexPath, statusPath, stagingIndexPath, sweepStaleStaging, } from "./paths.js";
|
|
30
|
+
export { CONNECTIONS_FORMAT_VERSION, findConnection, readConnections, writeConnections } from "./connections.js";
|
|
31
|
+
export { STATUS_FORMAT_VERSION, readStatusFile } from "./status.js";
|
|
32
|
+
export { IndexReader, DEFAULT_READER_CONFIG } from "./reader.js";
|
|
33
|
+
export { IndexPool, openIndex } from "./database.js";
|
|
34
|
+
/** Cordis plugin name, for loader diagnostics. */
|
|
35
|
+
export const name = 'dbgraph-store';
|
|
36
|
+
/** Services this plugin needs before it can register. */
|
|
37
|
+
export const inject = ['dbgraph'];
|
|
38
|
+
/** Store identity this build reserves on the seam. */
|
|
39
|
+
export const DEFAULT_STORE_ID = 'dbgraph-store';
|
|
40
|
+
/**
|
|
41
|
+
* One thrown value's message.
|
|
42
|
+
* @param error - the thrown value.
|
|
43
|
+
* @returns a non-empty message.
|
|
44
|
+
*/
|
|
45
|
+
function messageOf(error) {
|
|
46
|
+
if (error instanceof Error)
|
|
47
|
+
return error.message;
|
|
48
|
+
if (typeof error === 'string')
|
|
49
|
+
return error;
|
|
50
|
+
return JSON.stringify(error) ?? 'unknown error';
|
|
51
|
+
}
|
|
52
|
+
/** Whether the automatic writer selection has already explained itself. One line per process. */
|
|
53
|
+
let autoFallbackReported = false;
|
|
54
|
+
/**
|
|
55
|
+
* Explain, once, that the automatic selection fell back to JavaScript. An unresponsive host is worse
|
|
56
|
+
* than a slow one, so a missing addon is never fatal — but it is never silent either.
|
|
57
|
+
* @param reason - why the native kernel could not be used.
|
|
58
|
+
*/
|
|
59
|
+
function reportAutoFallback(reason) {
|
|
60
|
+
if (autoFallbackReported)
|
|
61
|
+
return;
|
|
62
|
+
autoFallbackReported = true;
|
|
63
|
+
console.error(`[dbgraph-store] the native index writer is unavailable (${reason});`
|
|
64
|
+
+ ' index writes will use the JavaScript writer');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One in-flight writer per connection.
|
|
68
|
+
*
|
|
69
|
+
* The staging path is unique per run, so two runs can no longer share a file — what still collides is
|
|
70
|
+
* the live index and the digest that describes it. Each run renames its own complete file onto
|
|
71
|
+
* `<connection>/index.db` and then writes a `status.json` entry; interleaved, the surviving index
|
|
72
|
+
* could be run A's while the digest reports run B's counts and timestamps, and one of the two reports
|
|
73
|
+
* would describe a file it did not leave behind. A run holds its connection id for that whole
|
|
74
|
+
* rename-then-digest sequence, and a second one is refused with `DBGRAPH_CONFLICT` instead of racing.
|
|
75
|
+
*/
|
|
76
|
+
export class WriteGuard {
|
|
77
|
+
active = new Set();
|
|
78
|
+
/**
|
|
79
|
+
* Whether a write for this connection is in flight.
|
|
80
|
+
* @param connection - connection identity.
|
|
81
|
+
* @returns true when a run holds it.
|
|
82
|
+
*/
|
|
83
|
+
has(connection) {
|
|
84
|
+
return this.active.has(connection);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run `work` while holding the connection, releasing it on both outcomes.
|
|
88
|
+
* @param connection - connection identity.
|
|
89
|
+
* @param work - the write to run.
|
|
90
|
+
* @returns the write's result.
|
|
91
|
+
* @throws DbgraphError with `DBGRAPH_CONFLICT` when another write already holds the connection.
|
|
92
|
+
*/
|
|
93
|
+
async run(connection, work) {
|
|
94
|
+
if (this.active.has(connection)) {
|
|
95
|
+
throw new DbgraphError(`an index write for connection "${connection}" is already in progress; wait for it to finish`, 'DBGRAPH_CONFLICT');
|
|
96
|
+
}
|
|
97
|
+
this.active.add(connection);
|
|
98
|
+
try {
|
|
99
|
+
return await work();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.active.delete(connection);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The store implementation: one reader (which owns the connection pool) plus the two files it manages.
|
|
108
|
+
*/
|
|
109
|
+
export class DbgraphIndexStore {
|
|
110
|
+
/** Store identity, as the seam's conflict message renders it. */
|
|
111
|
+
id = DEFAULT_STORE_ID;
|
|
112
|
+
/** Absolute directory this store keeps its files under (also read by the tool's prompt digest). */
|
|
113
|
+
root;
|
|
114
|
+
aliases;
|
|
115
|
+
reader;
|
|
116
|
+
/** One in-flight writer per connection; see {@link WriteGuard}. */
|
|
117
|
+
writes = new WriteGuard();
|
|
118
|
+
resolveOptions;
|
|
119
|
+
maxDigestObjects;
|
|
120
|
+
/**
|
|
121
|
+
* @param config - plugin configuration.
|
|
122
|
+
*/
|
|
123
|
+
constructor(config = {}) {
|
|
124
|
+
this.root = config.root ?? indexRoot();
|
|
125
|
+
this.aliases = config.aliases ?? ZH_ALIASES;
|
|
126
|
+
this.maxDigestObjects = config.maxDigestObjects ?? DEFAULT_STATUS_DIGEST_OBJECTS;
|
|
127
|
+
const readerConfig = {
|
|
128
|
+
...DEFAULT_READER_CONFIG,
|
|
129
|
+
staleHours: config.indexStaleHours ?? DEFAULT_INDEX_STALE_HOURS,
|
|
130
|
+
maxOpenIndexes: config.maxOpenIndexes ?? DEFAULT_READER_CONFIG.maxOpenIndexes,
|
|
131
|
+
maxSchemaObjects: config.maxSchemaObjects ?? DEFAULT_READER_CONFIG.maxSchemaObjects,
|
|
132
|
+
maxColumns: config.maxColumns ?? DEFAULT_READER_CONFIG.maxColumns,
|
|
133
|
+
defaultObjectLimit: config.defaultObjectLimit ?? DEFAULT_READER_CONFIG.defaultObjectLimit,
|
|
134
|
+
maxDigestObjects: this.maxDigestObjects,
|
|
135
|
+
impactDepth: config.impactDepth ?? DEFAULT_READER_CONFIG.impactDepth,
|
|
136
|
+
maxRelations: config.maxRelations ?? DEFAULT_READER_CONFIG.maxRelations,
|
|
137
|
+
};
|
|
138
|
+
this.reader = new IndexReader(this.root, readerConfig);
|
|
139
|
+
this.resolveOptions = {
|
|
140
|
+
...DEFAULT_RESOLVE_OPTIONS,
|
|
141
|
+
aliases: this.aliases,
|
|
142
|
+
minConfidence: config.minConfidence ?? DEFAULT_RESOLVE_OPTIONS.minConfidence,
|
|
143
|
+
ambiguityGap: config.ambiguityGap ?? DEFAULT_RESOLVE_OPTIONS.ambiguityGap,
|
|
144
|
+
maxCandidates: DEFAULT_RESOLVE_OPTIONS.maxCandidates,
|
|
145
|
+
maxColumns: readerConfig.maxColumns,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Every configured connection, in file order.
|
|
150
|
+
* @returns the list; empty when no file exists yet.
|
|
151
|
+
*/
|
|
152
|
+
async connections() {
|
|
153
|
+
return readConnections(this.root);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Replace the connection list (round-trip through {@link readConnections} to modify one entry).
|
|
157
|
+
* @param connections - the full list.
|
|
158
|
+
* @returns nothing.
|
|
159
|
+
*/
|
|
160
|
+
async saveConnections(connections) {
|
|
161
|
+
writeConnections(connections, this.root);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* One connection's index state; an unusable or absent index reports empty and stale.
|
|
165
|
+
* @param connection - connection identity.
|
|
166
|
+
* @returns the state.
|
|
167
|
+
*/
|
|
168
|
+
async state(connection) {
|
|
169
|
+
return this.reader.state(connection);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Structural overview, from the index.
|
|
173
|
+
* @param request - which connection.
|
|
174
|
+
* @returns the overview.
|
|
175
|
+
*/
|
|
176
|
+
async schema(request) {
|
|
177
|
+
return this.reader.schema(request);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Object listing, from the index.
|
|
181
|
+
* @param request - which connection and what to look for.
|
|
182
|
+
* @returns the listing.
|
|
183
|
+
*/
|
|
184
|
+
async objects(request) {
|
|
185
|
+
return this.reader.objects(request);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* One object's columns, from the index.
|
|
189
|
+
* @param request - which object.
|
|
190
|
+
* @returns the listing.
|
|
191
|
+
*/
|
|
192
|
+
async columns(request) {
|
|
193
|
+
return this.reader.columns(request);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resolve a request to an object, its columns, and a parameterized predicate — no network, no
|
|
197
|
+
* execution. The service composes this with a dialect when it runs `find`.
|
|
198
|
+
* @param request - what the user asked about.
|
|
199
|
+
* @returns the resolution.
|
|
200
|
+
*/
|
|
201
|
+
async resolve(request) {
|
|
202
|
+
const opened = this.reader.open(request.connection);
|
|
203
|
+
const engine = opened.meta.get(INDEX_META_KEYS.engine);
|
|
204
|
+
if (engine === undefined) {
|
|
205
|
+
throw new DbgraphError('the index records no meta.engine', 'DBGRAPH_MALFORMED_INDEX');
|
|
206
|
+
}
|
|
207
|
+
return resolveFind(opened.db, engine, request, this.resolveOptions);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Objects a change to this one reaches.
|
|
211
|
+
* @param request - the object to trace.
|
|
212
|
+
* @returns the reached objects.
|
|
213
|
+
*/
|
|
214
|
+
async impact(request) {
|
|
215
|
+
return this.reader.impact(request);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Objects directly referencing this one.
|
|
219
|
+
* @param request - the object to trace.
|
|
220
|
+
* @returns the referencing objects.
|
|
221
|
+
*/
|
|
222
|
+
async refs(request) {
|
|
223
|
+
return this.reader.refs(request);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Write a snapshot, replacing the connection's index.
|
|
227
|
+
*
|
|
228
|
+
* Order is part of the contract: the index is renamed into place first, the digest sidecar second,
|
|
229
|
+
* so no prompt ever describes an index that is not there yet. A sidecar failure is reported as a
|
|
230
|
+
* warning on an otherwise successful report — the index itself is already durable and useful.
|
|
231
|
+
* @param connection - connection identity.
|
|
232
|
+
* @param catalog - the snapshot to write.
|
|
233
|
+
* @param options - writer preference and the alias table.
|
|
234
|
+
* @returns the run's report.
|
|
235
|
+
*/
|
|
236
|
+
async write(connection, catalog, options) {
|
|
237
|
+
if (!isValidConnectionId(connection)) {
|
|
238
|
+
throw new DbgraphError(`"${connection}" is not a usable connection id`, 'DBGRAPH_UNKNOWN_CONNECTION');
|
|
239
|
+
}
|
|
240
|
+
// One writer per connection: the staging file is per-run, but the live index and its digest are
|
|
241
|
+
// not, so the second run is refused rather than allowed to interleave its rename and status write.
|
|
242
|
+
return this.writes.run(connection, () => this.writeLocked(connection, catalog, options));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The write body, run while this connection's in-flight slot is held.
|
|
246
|
+
* @param connection - connection identity, already validated.
|
|
247
|
+
* @param catalog - the snapshot to write.
|
|
248
|
+
* @param options - writer preference and the alias table.
|
|
249
|
+
* @returns the run's report.
|
|
250
|
+
*/
|
|
251
|
+
async writeLocked(connection, catalog, options) {
|
|
252
|
+
const aliases = options.aliases.length === 0 ? this.aliases : options.aliases;
|
|
253
|
+
const startedAt = performance.now();
|
|
254
|
+
let report;
|
|
255
|
+
if (options.writer === 'js') {
|
|
256
|
+
report = writeIndexJs({ connection, catalog, aliases, root: this.root });
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const loaded = loadRustIndexer();
|
|
260
|
+
if (options.writer === 'rust') {
|
|
261
|
+
// Asked for explicitly: a silent JS fallback would make a parity check meaningless.
|
|
262
|
+
if (loaded.engine === undefined) {
|
|
263
|
+
throw new DbgraphError(`the rust writer is not available: ${loaded.reason}`, 'DBGRAPH_UNAVAILABLE');
|
|
264
|
+
}
|
|
265
|
+
report = await this.writeWithRust(loaded.engine, connection, catalog, aliases, startedAt);
|
|
266
|
+
}
|
|
267
|
+
else if (loaded.engine === undefined) {
|
|
268
|
+
reportAutoFallback(loaded.reason);
|
|
269
|
+
report = writeIndexJs({ connection, catalog, aliases, root: this.root });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
try {
|
|
273
|
+
report = await this.writeWithRust(loaded.engine, connection, catalog, aliases, startedAt);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
// An automatic run degrades instead of failing: a host that cannot write natively still
|
|
277
|
+
// owes the user an index. The staging file was already removed by writeWithRust.
|
|
278
|
+
reportAutoFallback(`the native writer failed: ${messageOf(error)}`);
|
|
279
|
+
report = writeIndexJs({ connection, catalog, aliases, root: this.root });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const merged = report;
|
|
284
|
+
const label = this.labelOf(connection);
|
|
285
|
+
try {
|
|
286
|
+
writeStatusEntry(this.root, connection, buildStatusEntry({
|
|
287
|
+
engine: catalog.engine,
|
|
288
|
+
...(label === undefined ? {} : { label }),
|
|
289
|
+
level: catalog.level,
|
|
290
|
+
fetchedAt: catalog.fetchedAt,
|
|
291
|
+
objects: catalog.objects,
|
|
292
|
+
columnCount: merged.columns,
|
|
293
|
+
maxDigestObjects: this.maxDigestObjects,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
return {
|
|
298
|
+
...merged,
|
|
299
|
+
warnings: [
|
|
300
|
+
...merged.warnings,
|
|
301
|
+
`the index was written but the prompt digest was not: ${messageOf(error)}`,
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Write one index through the native kernel, then rename it into place.
|
|
309
|
+
*
|
|
310
|
+
* The addon owns exactly one file — the staging path — and deletes it on any failure. This method
|
|
311
|
+
* owns the two decisions the addon must not make: that a successful build is renamed over the live
|
|
312
|
+
* index, and that a failed or nonsensical one never is.
|
|
313
|
+
* @param engine - the loaded native kernel.
|
|
314
|
+
* @param connection - connection identity.
|
|
315
|
+
* @param catalog - the snapshot to write.
|
|
316
|
+
* @param aliases - the alias table.
|
|
317
|
+
* @param startedAt - `performance.now()` when the call began.
|
|
318
|
+
* @returns the run's report, with `writer: 'rust'`.
|
|
319
|
+
*/
|
|
320
|
+
async writeWithRust(engine, connection, catalog, aliases, startedAt) {
|
|
321
|
+
const finalPath = indexPath(connection, this.root);
|
|
322
|
+
const stagingPath = stagingIndexPath(connection, this.root);
|
|
323
|
+
mkdirSync(dirname(finalPath), { recursive: true });
|
|
324
|
+
// Clean up staging files a previous run left behind (a killed build never returns to rename).
|
|
325
|
+
sweepStaleStaging(finalPath);
|
|
326
|
+
rmSync(stagingPath, { force: true });
|
|
327
|
+
let built;
|
|
328
|
+
try {
|
|
329
|
+
built = await engine.buildIndex({
|
|
330
|
+
dbPath: stagingPath,
|
|
331
|
+
schemaDdl: [...INDEX_SCHEMA_DDL],
|
|
332
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
333
|
+
connection,
|
|
334
|
+
catalogJson: JSON.stringify(catalog),
|
|
335
|
+
aliasesJson: JSON.stringify(aliases),
|
|
336
|
+
now: Date.now(),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
// The addon deletes its own partial file; this is defense in depth for a load-time failure
|
|
341
|
+
// that happened after the file was opened.
|
|
342
|
+
rmSync(stagingPath, { force: true });
|
|
343
|
+
throw new DbgraphError(`the rust writer failed for "${connection}": ${messageOf(error)}`, 'DBGRAPH_WRITE_FAILED', { cause: error });
|
|
344
|
+
}
|
|
345
|
+
const parsed = parseRustReport(built);
|
|
346
|
+
if (parsed === undefined) {
|
|
347
|
+
rmSync(stagingPath, { force: true });
|
|
348
|
+
throw new DbgraphError(`the rust writer returned an unusable report for "${connection}"`, 'DBGRAPH_WRITE_FAILED');
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
renameSync(stagingPath, finalPath);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
rmSync(stagingPath, { force: true });
|
|
355
|
+
throw new DbgraphError(`renaming the rust-written index for "${connection}" failed: ${messageOf(error)}`, 'DBGRAPH_WRITE_FAILED', { cause: error });
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
connection,
|
|
359
|
+
level: catalog.level,
|
|
360
|
+
objects: parsed.objects,
|
|
361
|
+
columns: parsed.columns,
|
|
362
|
+
edges: parsed.edges,
|
|
363
|
+
terms: parsed.terms,
|
|
364
|
+
writer: 'rust',
|
|
365
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
366
|
+
warnings: [...(catalog.warnings ?? [])],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Close every cached connection. Idempotent; the store is unusable afterwards.
|
|
371
|
+
*/
|
|
372
|
+
dispose() {
|
|
373
|
+
this.reader.close();
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A connection's display label, when `connections.json` has one. A malformed file must not stop an
|
|
377
|
+
* index write — the label is decoration, the index is the product.
|
|
378
|
+
* @param connection - connection identity.
|
|
379
|
+
* @returns the label, or undefined.
|
|
380
|
+
*/
|
|
381
|
+
labelOf(connection) {
|
|
382
|
+
try {
|
|
383
|
+
return findConnection(connection, this.root)?.label;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Build a store without mounting it (tests, and callers that want the object directly).
|
|
392
|
+
* @param config - plugin configuration.
|
|
393
|
+
* @returns the store.
|
|
394
|
+
*/
|
|
395
|
+
export function createStore(config = {}) {
|
|
396
|
+
return new DbgraphIndexStore(config);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Register the store on `ctx.dbgraph`.
|
|
400
|
+
* @param ctx - the plugin context (must inject `dbgraph`).
|
|
401
|
+
* @param config - resolved plugin configuration.
|
|
402
|
+
*/
|
|
403
|
+
export function apply(ctx, config = {}) {
|
|
404
|
+
const store = new DbgraphIndexStore(config);
|
|
405
|
+
ctx.effect(function* () {
|
|
406
|
+
// Registered first so it disposes last: the store unregisters before its connections close, and
|
|
407
|
+
// no call can be routed to a pool that is already shut.
|
|
408
|
+
yield () => {
|
|
409
|
+
store.dispose();
|
|
410
|
+
};
|
|
411
|
+
yield ctx.dbgraph.registerStore(store);
|
|
412
|
+
}, 'dbgraph-store');
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-dbgraph-store`.
|
|
3
|
+
* @module dsh-plugin-dbgraph-store/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "dbgraph-store-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-dbgraph-store`.
|
|
3
|
+
* @module dsh-plugin-dbgraph-store/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = 'dsh-plugin-dbgraph-store';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'dbgraph-store-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the store owns no mutable harness-visible value. Its connection pool is
|
|
12
|
+
* private, and the index it reads is a file another writer replaces wholesale, so there is no owned
|
|
13
|
+
* relation between two harness-visible values to compare.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => { };
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
/* jscpd:ignore-end */
|
|
23
|
+
//# sourceMappingURL=invariant.js.map
|