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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the optional native index kernel (`crates/dbgraph-indexer`).
|
|
3
|
+
*
|
|
4
|
+
* The addon is optional at every level, the same posture `dsh-plugin-codegraph` takes with its Rust
|
|
5
|
+
* extractor: a missing artifact, an unsupported platform, an unloadable binary, or an unusable answer
|
|
6
|
+
* all mean "the JavaScript writer runs instead, it says so once, and nothing else changes".
|
|
7
|
+
*
|
|
8
|
+
* One deliberate exception to that posture: the caller that *asked* for `writer: 'rust'` gets an error
|
|
9
|
+
* rather than a silent fallback. A parity check that quietly compared JS against JS would be worse
|
|
10
|
+
* than a failed one.
|
|
11
|
+
*
|
|
12
|
+
* Artifact naming follows the build script (`dbgraph-indexer.<platform>-<arch>.node`, produced by
|
|
13
|
+
* `crates/dbgraph-indexer/build-addon.sh`). `napi build` names some targets with a qualifier
|
|
14
|
+
* (`linux-x64-gnu`, `win32-x64-msvc`), so both spellings are probed. `DSH_DBGRAPH_RUST_ADDON` may
|
|
15
|
+
* point at an explicit artifact — a test hook, and an escape hatch for a deployment that ships the
|
|
16
|
+
* binary outside the checkout.
|
|
17
|
+
* @module dsh-plugin-dbgraph-store/rust-engine
|
|
18
|
+
*/
|
|
19
|
+
import type { AliasGroup, RawCatalog } from 'dsh-plugin-dbgraph-service';
|
|
20
|
+
/** Environment variable that overrides the addon path. */
|
|
21
|
+
export declare const ADDON_ENV = "DSH_DBGRAPH_RUST_ADDON";
|
|
22
|
+
/** What the addon's `buildIndex` returns. */
|
|
23
|
+
export interface RustBuildReport {
|
|
24
|
+
/** Objects written. */
|
|
25
|
+
readonly objects: number;
|
|
26
|
+
/** Columns written. */
|
|
27
|
+
readonly columns: number;
|
|
28
|
+
/** Edges written. */
|
|
29
|
+
readonly edges: number;
|
|
30
|
+
/** Search terms written. */
|
|
31
|
+
readonly terms: number;
|
|
32
|
+
/** Wall-clock duration of the native call, in milliseconds. */
|
|
33
|
+
readonly elapsedMs: number;
|
|
34
|
+
}
|
|
35
|
+
/** What the addon's `buildIndex` accepts. */
|
|
36
|
+
export interface RustBuildInput {
|
|
37
|
+
/** Absolute path of the `.tmp` index file. The caller renames it into place. */
|
|
38
|
+
readonly dbPath: string;
|
|
39
|
+
/** `INDEX_SCHEMA_DDL`, executed verbatim. */
|
|
40
|
+
readonly schemaDdl: readonly string[];
|
|
41
|
+
/** `INDEX_SCHEMA_VERSION`. */
|
|
42
|
+
readonly schemaVersion: number;
|
|
43
|
+
/** Connection identity stored in `meta.connection`. */
|
|
44
|
+
readonly connection: string;
|
|
45
|
+
/** `JSON.stringify(catalog)` — text transport keeps the conversion off the event loop. */
|
|
46
|
+
readonly catalogJson: string;
|
|
47
|
+
/** `JSON.stringify(aliases)`. */
|
|
48
|
+
readonly aliasesJson: string;
|
|
49
|
+
/** Epoch milliseconds to stamp on every row, so the caller controls time. */
|
|
50
|
+
readonly now: number;
|
|
51
|
+
}
|
|
52
|
+
/** The narrow surface this module needs from the addon. */
|
|
53
|
+
export interface RustIndexer {
|
|
54
|
+
/** Write one complete index to `input.dbPath`. */
|
|
55
|
+
buildIndex(input: RustBuildInput): Promise<unknown>;
|
|
56
|
+
}
|
|
57
|
+
/** The result of one load attempt. */
|
|
58
|
+
export interface RustLoad {
|
|
59
|
+
/** The loaded kernel, when the artifact is usable. */
|
|
60
|
+
readonly engine?: RustIndexer;
|
|
61
|
+
/** Why it could not be loaded; empty on success. */
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
/** Absolute artifact path that was loaded, when one was found. */
|
|
64
|
+
readonly path?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Where this host's artifact should be, and whether it is there.
|
|
68
|
+
*
|
|
69
|
+
* A missing file is not cached: an addon built while the host is running starts working on the next
|
|
70
|
+
* call without a restart. A file that exists but throws while loading *is* cached — a corrupt binary
|
|
71
|
+
* does not repair itself mid-process.
|
|
72
|
+
* @returns the first existing candidate, or the reason none did.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveAddonPath(): {
|
|
75
|
+
readonly path?: string;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Load the native kernel, or explain why it is not available.
|
|
80
|
+
* @returns the loaded kernel and the reason string for diagnostics.
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadRustIndexer(): RustLoad;
|
|
83
|
+
/**
|
|
84
|
+
* Validate and normalize the addon's answer.
|
|
85
|
+
*
|
|
86
|
+
* An addon that resolves with nonsense is treated exactly like an addon that threw: no partial answer
|
|
87
|
+
* ever reaches the rename, so `index.db` keeps describing the previous run.
|
|
88
|
+
* @param value - the resolved value of `buildIndex`.
|
|
89
|
+
* @returns the report, or undefined when it is not a usable one.
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseRustReport(value: unknown): RustBuildReport | undefined;
|
|
92
|
+
/** One catalog, ready to cross the addon boundary as text. */
|
|
93
|
+
export interface RustPayload {
|
|
94
|
+
/** The snapshot. */
|
|
95
|
+
readonly catalog: RawCatalog;
|
|
96
|
+
/** The alias table the writer expands. */
|
|
97
|
+
readonly aliases: readonly AliasGroup[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Serialize one payload for the text transport.
|
|
101
|
+
* @param payload - the catalog and aliases.
|
|
102
|
+
* @returns the two JSON strings the addon expects.
|
|
103
|
+
*/
|
|
104
|
+
export declare function serializePayload(payload: RustPayload): {
|
|
105
|
+
catalogJson: string;
|
|
106
|
+
aliasesJson: string;
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=rust-engine.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the optional native index kernel (`crates/dbgraph-indexer`).
|
|
3
|
+
*
|
|
4
|
+
* The addon is optional at every level, the same posture `dsh-plugin-codegraph` takes with its Rust
|
|
5
|
+
* extractor: a missing artifact, an unsupported platform, an unloadable binary, or an unusable answer
|
|
6
|
+
* all mean "the JavaScript writer runs instead, it says so once, and nothing else changes".
|
|
7
|
+
*
|
|
8
|
+
* One deliberate exception to that posture: the caller that *asked* for `writer: 'rust'` gets an error
|
|
9
|
+
* rather than a silent fallback. A parity check that quietly compared JS against JS would be worse
|
|
10
|
+
* than a failed one.
|
|
11
|
+
*
|
|
12
|
+
* Artifact naming follows the build script (`dbgraph-indexer.<platform>-<arch>.node`, produced by
|
|
13
|
+
* `crates/dbgraph-indexer/build-addon.sh`). `napi build` names some targets with a qualifier
|
|
14
|
+
* (`linux-x64-gnu`, `win32-x64-msvc`), so both spellings are probed. `DSH_DBGRAPH_RUST_ADDON` may
|
|
15
|
+
* point at an explicit artifact — a test hook, and an escape hatch for a deployment that ships the
|
|
16
|
+
* binary outside the checkout.
|
|
17
|
+
* @module dsh-plugin-dbgraph-store/rust-engine
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
/** Environment variable that overrides the addon path. */
|
|
24
|
+
export const ADDON_ENV = 'DSH_DBGRAPH_RUST_ADDON';
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
/** `<repo>/crates/dbgraph-indexer/artifacts` — three levels up from `src/` or `lib/`. */
|
|
28
|
+
const artifactsDirectory = join(moduleDirectory, '..', '..', '..', 'crates', 'dbgraph-indexer', 'artifacts');
|
|
29
|
+
/** Every artifact suffix this host could plausibly use, most specific build first. */
|
|
30
|
+
function artifactSuffixes() {
|
|
31
|
+
const base = `${process.platform}-${process.arch}`;
|
|
32
|
+
const suffixes = [base];
|
|
33
|
+
if (process.platform === 'linux')
|
|
34
|
+
suffixes.push(`${base}-gnu`, `${base}-musl`);
|
|
35
|
+
else if (process.platform === 'win32')
|
|
36
|
+
suffixes.push(`${base}-msvc`);
|
|
37
|
+
return suffixes;
|
|
38
|
+
}
|
|
39
|
+
/** One load attempt's outcome is reused for the process lifetime, keyed by path. */
|
|
40
|
+
let cachedPath;
|
|
41
|
+
let cachedLoad;
|
|
42
|
+
/**
|
|
43
|
+
* Where this host's artifact should be, and whether it is there.
|
|
44
|
+
*
|
|
45
|
+
* A missing file is not cached: an addon built while the host is running starts working on the next
|
|
46
|
+
* call without a restart. A file that exists but throws while loading *is* cached — a corrupt binary
|
|
47
|
+
* does not repair itself mid-process.
|
|
48
|
+
* @returns the first existing candidate, or the reason none did.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveAddonPath() {
|
|
51
|
+
const override = process.env[ADDON_ENV]?.trim();
|
|
52
|
+
if (override !== undefined && override !== '') {
|
|
53
|
+
if (!existsSync(override)) {
|
|
54
|
+
return { reason: `${ADDON_ENV} points at ${override}, which does not exist` };
|
|
55
|
+
}
|
|
56
|
+
return { path: override, reason: `loaded from ${ADDON_ENV}` };
|
|
57
|
+
}
|
|
58
|
+
const tried = artifactSuffixes().map(suffix => join(artifactsDirectory, `dbgraph-indexer.${suffix}.node`));
|
|
59
|
+
for (const candidate of tried) {
|
|
60
|
+
if (existsSync(candidate))
|
|
61
|
+
return { path: candidate, reason: `found at ${candidate}` };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
reason: `no native artifact for ${process.platform}-${process.arch}; looked for ${tried.join(', ')}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load the native kernel, or explain why it is not available.
|
|
69
|
+
* @returns the loaded kernel and the reason string for diagnostics.
|
|
70
|
+
*/
|
|
71
|
+
export function loadRustIndexer() {
|
|
72
|
+
const resolution = resolveAddonPath();
|
|
73
|
+
if (resolution.path === undefined)
|
|
74
|
+
return { reason: resolution.reason };
|
|
75
|
+
if (cachedLoad !== undefined && cachedPath === resolution.path)
|
|
76
|
+
return cachedLoad;
|
|
77
|
+
const path = resolution.path;
|
|
78
|
+
try {
|
|
79
|
+
const loaded = require(path);
|
|
80
|
+
const buildIndex = typeof loaded === 'object' && loaded !== null
|
|
81
|
+
? loaded.buildIndex
|
|
82
|
+
: undefined;
|
|
83
|
+
if (typeof buildIndex !== 'function') {
|
|
84
|
+
const failed = { reason: `the addon at ${path} exports no buildIndex function`, path };
|
|
85
|
+
cachedPath = path;
|
|
86
|
+
cachedLoad = failed;
|
|
87
|
+
return failed;
|
|
88
|
+
}
|
|
89
|
+
const engine = {
|
|
90
|
+
buildIndex: async (input) => buildIndex.call(loaded, input),
|
|
91
|
+
};
|
|
92
|
+
const ready = { engine, reason: `loaded ${path}`, path };
|
|
93
|
+
cachedPath = path;
|
|
94
|
+
cachedLoad = ready;
|
|
95
|
+
return ready;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const failed = {
|
|
99
|
+
reason: `the addon at ${path} failed to load: ${error instanceof Error ? error.message : String(error)}`,
|
|
100
|
+
path,
|
|
101
|
+
};
|
|
102
|
+
cachedPath = path;
|
|
103
|
+
cachedLoad = failed;
|
|
104
|
+
return failed;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate and normalize the addon's answer.
|
|
109
|
+
*
|
|
110
|
+
* An addon that resolves with nonsense is treated exactly like an addon that threw: no partial answer
|
|
111
|
+
* ever reaches the rename, so `index.db` keeps describing the previous run.
|
|
112
|
+
* @param value - the resolved value of `buildIndex`.
|
|
113
|
+
* @returns the report, or undefined when it is not a usable one.
|
|
114
|
+
*/
|
|
115
|
+
export function parseRustReport(value) {
|
|
116
|
+
if (typeof value !== 'object' || value === null)
|
|
117
|
+
return undefined;
|
|
118
|
+
const record = value;
|
|
119
|
+
const counts = [];
|
|
120
|
+
for (const key of ['objects', 'columns', 'edges', 'terms']) {
|
|
121
|
+
const count = record[key];
|
|
122
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
counts.push(count);
|
|
125
|
+
}
|
|
126
|
+
const elapsedMs = record.elapsedMs;
|
|
127
|
+
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
return {
|
|
130
|
+
objects: counts[0],
|
|
131
|
+
columns: counts[1],
|
|
132
|
+
edges: counts[2],
|
|
133
|
+
terms: counts[3],
|
|
134
|
+
elapsedMs,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Serialize one payload for the text transport.
|
|
139
|
+
* @param payload - the catalog and aliases.
|
|
140
|
+
* @returns the two JSON strings the addon expects.
|
|
141
|
+
*/
|
|
142
|
+
export function serializePayload(payload) {
|
|
143
|
+
return {
|
|
144
|
+
catalogJson: JSON.stringify(payload.catalog),
|
|
145
|
+
aliasesJson: JSON.stringify(payload.aliases),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=rust-engine.js.map
|
package/lib/status.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt-digest sidecar (`status.json`): a small JSON document every connection's index state is
|
|
3
|
+
* mirrored into, so the system-prompt section can answer "what does this database look like" with one
|
|
4
|
+
* synchronous `readFileSync` instead of an SQLite open per prompt assembly.
|
|
5
|
+
*
|
|
6
|
+
* It is a *derived* copy. The index file stays authoritative; a missing or unreadable sidecar only
|
|
7
|
+
* degrades one sentence. The one ordering rule that matters is placement: the writer renames the index
|
|
8
|
+
* into place first and records the digest second, so a reader can never describe an index that is not
|
|
9
|
+
* there yet (it may describe an older one, which is why `l1IndexedAt` is carried per entry).
|
|
10
|
+
* @module dsh-plugin-dbgraph-store/status
|
|
11
|
+
*/
|
|
12
|
+
import type { IndexLevel, RawObject, StatusConnectionEntry, StatusFile } from 'dsh-plugin-dbgraph-service';
|
|
13
|
+
/** Format version of `status.json`; a reader that sees another one falls back to the weaker sentence. */
|
|
14
|
+
export declare const STATUS_FORMAT_VERSION = 1;
|
|
15
|
+
/**
|
|
16
|
+
* Render one object's display name the way every other surface does.
|
|
17
|
+
* @param object - a raw catalog object.
|
|
18
|
+
* @returns `schema.name` when a schema is present, else `name`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function objectDisplayName(object: RawObject): string;
|
|
21
|
+
/** What {@link buildStatusEntry} needs from a writer's run. */
|
|
22
|
+
export interface StatusEntryInput {
|
|
23
|
+
/** Database family. */
|
|
24
|
+
readonly engine: string;
|
|
25
|
+
/** Human label from `connections.json`, when one is configured. */
|
|
26
|
+
readonly label?: string;
|
|
27
|
+
/** Which level the snapshot covers. */
|
|
28
|
+
readonly level: IndexLevel;
|
|
29
|
+
/** Epoch milliseconds the snapshot was taken. */
|
|
30
|
+
readonly fetchedAt: number;
|
|
31
|
+
/** Objects written. */
|
|
32
|
+
readonly objects: readonly RawObject[];
|
|
33
|
+
/** Column rows written (equal to the sum of the objects' columns unless a dialect disagreed). */
|
|
34
|
+
readonly columnCount: number;
|
|
35
|
+
/** Digest cap; defaults to {@link DEFAULT_STATUS_DIGEST_OBJECTS}. */
|
|
36
|
+
readonly maxDigestObjects?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the sidecar entry for a writer's run.
|
|
40
|
+
* @param input - the catalog-derived facts.
|
|
41
|
+
* @returns the entry to store under the connection id.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildStatusEntry(input: StatusEntryInput): StatusConnectionEntry;
|
|
44
|
+
/**
|
|
45
|
+
* Read the sidecar.
|
|
46
|
+
*
|
|
47
|
+
* Never throws: prompt assembly runs this on every request, so a corrupt or half-written file means
|
|
48
|
+
* "no digest", not "the prompt fails to assemble".
|
|
49
|
+
* @param root - index root.
|
|
50
|
+
* @returns the document, or undefined when it is absent or unusable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readStatusFile(root: string): StatusFile | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Record one connection's entry, preserving every other connection's entry.
|
|
55
|
+
*
|
|
56
|
+
* A document at another format version is replaced rather than merged: this build cannot promise it
|
|
57
|
+
* understands a future entry's shape, and a fresh document is honest about being partial.
|
|
58
|
+
* @param root - index root.
|
|
59
|
+
* @param connectionId - the connection whose entry changed.
|
|
60
|
+
* @param entry - the fresh entry.
|
|
61
|
+
* @param now - epoch milliseconds of the write; defaults to now.
|
|
62
|
+
*/
|
|
63
|
+
export declare function writeStatusEntry(root: string, connectionId: string, entry: StatusConnectionEntry, now?: number): void;
|
|
64
|
+
//# sourceMappingURL=status.d.ts.map
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt-digest sidecar (`status.json`): a small JSON document every connection's index state is
|
|
3
|
+
* mirrored into, so the system-prompt section can answer "what does this database look like" with one
|
|
4
|
+
* synchronous `readFileSync` instead of an SQLite open per prompt assembly.
|
|
5
|
+
*
|
|
6
|
+
* It is a *derived* copy. The index file stays authoritative; a missing or unreadable sidecar only
|
|
7
|
+
* degrades one sentence. The one ordering rule that matters is placement: the writer renames the index
|
|
8
|
+
* into place first and records the digest second, so a reader can never describe an index that is not
|
|
9
|
+
* there yet (it may describe an older one, which is why `l1IndexedAt` is carried per entry).
|
|
10
|
+
* @module dsh-plugin-dbgraph-store/status
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
14
|
+
import { DEFAULT_STATUS_DIGEST_OBJECTS } from 'dsh-plugin-dbgraph-service';
|
|
15
|
+
import { normalizeComment } from "./normalize.js";
|
|
16
|
+
import { statusPath } from "./paths.js";
|
|
17
|
+
/** Format version of `status.json`; a reader that sees another one falls back to the weaker sentence. */
|
|
18
|
+
export const STATUS_FORMAT_VERSION = 1;
|
|
19
|
+
/**
|
|
20
|
+
* Render one object's display name the way every other surface does.
|
|
21
|
+
* @param object - a raw catalog object.
|
|
22
|
+
* @returns `schema.name` when a schema is present, else `name`.
|
|
23
|
+
*/
|
|
24
|
+
export function objectDisplayName(object) {
|
|
25
|
+
const schema = object.schema ?? '';
|
|
26
|
+
return schema === '' ? object.name : `${schema}.${object.name}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rank and cap the digest: the widest objects first (a column count is the one size signal every
|
|
30
|
+
* engine reports), name as the stable tie-break.
|
|
31
|
+
* @param objects - the snapshot's objects.
|
|
32
|
+
* @param maxEntries - the cap.
|
|
33
|
+
* @returns the digest and how many objects were left out.
|
|
34
|
+
*/
|
|
35
|
+
function buildDigest(objects, maxEntries) {
|
|
36
|
+
const ranked = objects
|
|
37
|
+
.map(object => {
|
|
38
|
+
const comment = normalizeComment(object.comment);
|
|
39
|
+
return {
|
|
40
|
+
object: objectDisplayName(object),
|
|
41
|
+
...(comment === undefined ? {} : { comment }),
|
|
42
|
+
columns: object.columns.length,
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.sort((left, right) => right.columns - left.columns || (left.object < right.object ? -1 : left.object > right.object ? 1 : 0));
|
|
46
|
+
const digest = ranked.slice(0, maxEntries);
|
|
47
|
+
return { digest, omitted: ranked.length - digest.length };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the sidecar entry for a writer's run.
|
|
51
|
+
* @param input - the catalog-derived facts.
|
|
52
|
+
* @returns the entry to store under the connection id.
|
|
53
|
+
*/
|
|
54
|
+
export function buildStatusEntry(input) {
|
|
55
|
+
const { digest, omitted } = buildDigest(input.objects, input.maxDigestObjects ?? DEFAULT_STATUS_DIGEST_OBJECTS);
|
|
56
|
+
let commented = 0;
|
|
57
|
+
for (const object of input.objects) {
|
|
58
|
+
for (const column of object.columns) {
|
|
59
|
+
if (normalizeComment(column.comment) !== undefined)
|
|
60
|
+
commented += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const total = input.columnCount;
|
|
64
|
+
return {
|
|
65
|
+
engine: input.engine,
|
|
66
|
+
...(input.label === undefined ? {} : { label: input.label }),
|
|
67
|
+
l1IndexedAt: input.fetchedAt,
|
|
68
|
+
...(input.level === 'l2' ? { l2IndexedAt: input.fetchedAt } : {}),
|
|
69
|
+
objects: input.objects.length,
|
|
70
|
+
columns: total,
|
|
71
|
+
commentCoverage: total === 0 ? 0 : commented / total,
|
|
72
|
+
digest,
|
|
73
|
+
digestOmitted: omitted,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read the sidecar.
|
|
78
|
+
*
|
|
79
|
+
* Never throws: prompt assembly runs this on every request, so a corrupt or half-written file means
|
|
80
|
+
* "no digest", not "the prompt fails to assemble".
|
|
81
|
+
* @param root - index root.
|
|
82
|
+
* @returns the document, or undefined when it is absent or unusable.
|
|
83
|
+
*/
|
|
84
|
+
export function readStatusFile(root) {
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = readFileSync(statusPath(root), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(text);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
100
|
+
return undefined;
|
|
101
|
+
const candidate = parsed;
|
|
102
|
+
if (typeof candidate.version !== 'number')
|
|
103
|
+
return undefined;
|
|
104
|
+
if (typeof candidate.connections !== 'object' || candidate.connections === null)
|
|
105
|
+
return undefined;
|
|
106
|
+
return {
|
|
107
|
+
version: candidate.version,
|
|
108
|
+
updatedAt: typeof candidate.updatedAt === 'number' ? candidate.updatedAt : 0,
|
|
109
|
+
connections: candidate.connections,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Record one connection's entry, preserving every other connection's entry.
|
|
114
|
+
*
|
|
115
|
+
* A document at another format version is replaced rather than merged: this build cannot promise it
|
|
116
|
+
* understands a future entry's shape, and a fresh document is honest about being partial.
|
|
117
|
+
* @param root - index root.
|
|
118
|
+
* @param connectionId - the connection whose entry changed.
|
|
119
|
+
* @param entry - the fresh entry.
|
|
120
|
+
* @param now - epoch milliseconds of the write; defaults to now.
|
|
121
|
+
*/
|
|
122
|
+
export function writeStatusEntry(root, connectionId, entry, now = Date.now()) {
|
|
123
|
+
const existing = readStatusFile(root);
|
|
124
|
+
const connections = existing?.version === STATUS_FORMAT_VERSION
|
|
125
|
+
? { ...existing.connections }
|
|
126
|
+
: {};
|
|
127
|
+
connections[connectionId] = entry;
|
|
128
|
+
const document = { version: STATUS_FORMAT_VERSION, updatedAt: now, connections };
|
|
129
|
+
const target = statusPath(root);
|
|
130
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
131
|
+
const staging = `${target}.tmp`;
|
|
132
|
+
writeFileSync(staging, `${JSON.stringify(document)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
133
|
+
try {
|
|
134
|
+
renameSync(staging, target);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
rmSync(staging, { force: true });
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=status.js.map
|
package/lib/terms.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic layer of the index: `terms` rows and the FTS `search` documents.
|
|
3
|
+
*
|
|
4
|
+
* Two lookups answer the same question from opposite directions:
|
|
5
|
+
*
|
|
6
|
+
* - `terms` is the resolver's table — one row per (word, object[, column]) with a weight that says how
|
|
7
|
+
* strong the evidence is: a column's name token beats its comment, an alias expansion beats neither,
|
|
8
|
+
* a Chinese bigram beats nothing at all, it is partial evidence by construction.
|
|
9
|
+
* - `search` is the FTS5 document — the tokenizer's output for an object's name and comment (one row)
|
|
10
|
+
* and for each column's name and comment (one row per column), used for free-text listing and as a
|
|
11
|
+
* recall net behind the resolver.
|
|
12
|
+
*
|
|
13
|
+
* Weights live in one exported table so the resolver's expectations and the writer's output cannot
|
|
14
|
+
* drift apart silently.
|
|
15
|
+
* @module dsh-plugin-dbgraph-store/terms
|
|
16
|
+
*/
|
|
17
|
+
import type { AliasGroup, RawCatalog } from 'dsh-plugin-dbgraph-service';
|
|
18
|
+
/**
|
|
19
|
+
* Evidence weight per token source. A column-level row is finally weighed again by the resolver
|
|
20
|
+
* (a column hit is weaker evidence about the *table* than the table's own name), so these numbers are
|
|
21
|
+
* "how much does this word describe the name it was found in", not final confidence.
|
|
22
|
+
*/
|
|
23
|
+
export declare const TERM_WEIGHTS: {
|
|
24
|
+
/** A token of the object's own name. */
|
|
25
|
+
readonly objectNameAscii: 1;
|
|
26
|
+
/** A CJK bigram of the object's name — partial by nature. */
|
|
27
|
+
readonly objectNameCjk: 0.75;
|
|
28
|
+
/** An ASCII token of the object's comment. */
|
|
29
|
+
readonly objectCommentAscii: 0.75;
|
|
30
|
+
/** A CJK bigram of the object's comment. */
|
|
31
|
+
readonly objectCommentCjk: 0.5;
|
|
32
|
+
/** An alias-group expansion on an object's name or comment. */
|
|
33
|
+
readonly objectAlias: 0.6;
|
|
34
|
+
/** An ASCII token of the column's own name. */
|
|
35
|
+
readonly columnNameAscii: 0.9;
|
|
36
|
+
/** A CJK bigram of the column's name. */
|
|
37
|
+
readonly columnNameCjk: 0.75;
|
|
38
|
+
/** An ASCII token of the column's comment. */
|
|
39
|
+
readonly columnCommentAscii: 0.7;
|
|
40
|
+
/** A CJK bigram of the column's comment. */
|
|
41
|
+
readonly columnCommentCjk: 0.5;
|
|
42
|
+
/** An alias-group expansion on a column's name or comment. */
|
|
43
|
+
readonly columnAlias: 0.65;
|
|
44
|
+
};
|
|
45
|
+
/** Object ids assigned in catalog order, and per-object column ids assigned in a global sequence. */
|
|
46
|
+
export interface CatalogIds {
|
|
47
|
+
/** One one-based SQLite rowid per object, in catalog order. */
|
|
48
|
+
readonly objects: readonly number[];
|
|
49
|
+
/** Per object (same order), the one-based ids of its columns. */
|
|
50
|
+
readonly columns: readonly (readonly number[])[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Assign the ids the writer will use, so terms and rows cannot disagree about identity.
|
|
54
|
+
* @param catalog - the snapshot being written.
|
|
55
|
+
* @returns object ids by position and column ids by object/column position.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assignCatalogIds(catalog: RawCatalog): CatalogIds;
|
|
58
|
+
/** One row of the `terms` table. */
|
|
59
|
+
export interface TermRow {
|
|
60
|
+
/** The token, as the service tokenizer emits it. */
|
|
61
|
+
readonly term: string;
|
|
62
|
+
/** Evidence strength 0..1; see {@link TERM_WEIGHTS}. */
|
|
63
|
+
readonly weight: number;
|
|
64
|
+
/** Object this term describes, when it describes one. */
|
|
65
|
+
readonly objectId: number | null;
|
|
66
|
+
/** Column this term describes, when it describes one. */
|
|
67
|
+
readonly columnId: number | null;
|
|
68
|
+
}
|
|
69
|
+
/** One row of the `search` table (FTS5). */
|
|
70
|
+
export interface SearchRow {
|
|
71
|
+
/** Pre-tokenized document, space-joined. */
|
|
72
|
+
readonly text: string;
|
|
73
|
+
/** Object identity for an object document; null for a column document. */
|
|
74
|
+
readonly objectId: number | null;
|
|
75
|
+
/** Column identity for a column document; null for an object document. */
|
|
76
|
+
readonly columnId: number | null;
|
|
77
|
+
}
|
|
78
|
+
/** Everything the semantic layer produces for one write. */
|
|
79
|
+
export interface TermBuild {
|
|
80
|
+
/** Deduplicated `terms` rows. */
|
|
81
|
+
readonly terms: readonly TermRow[];
|
|
82
|
+
/** One object document per object, one column document per column. */
|
|
83
|
+
readonly search: readonly SearchRow[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build every `terms` row and both `search` documents for one catalog.
|
|
87
|
+
* @param catalog - the snapshot being written.
|
|
88
|
+
* @param ids - ids from {@link assignCatalogIds}.
|
|
89
|
+
* @param aliases - the alias table handed in by the caller.
|
|
90
|
+
* @returns deduplicated term rows and search documents.
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildTerms(catalog: RawCatalog, ids: CatalogIds, aliases: readonly AliasGroup[]): TermBuild;
|
|
93
|
+
//# sourceMappingURL=terms.d.ts.map
|