dsh-plugin-dbgraph-tool 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/command.d.ts +86 -0
- package/lib/command.js +186 -0
- package/lib/index.d.ts +95 -0
- package/lib/index.js +586 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +23 -0
- package/lib/prompt.d.ts +93 -0
- package/lib/prompt.js +337 -0
- package/lib/render.d.ts +34 -0
- package/lib/render.js +244 -0
- package/lib/schema.d.ts +782 -0
- package/lib/schema.js +315 -0
- package/package.json +78 -3
- package/README.md +0 -3
package/lib/prompt.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema digest this plugin contributes to the system prompt on every request.
|
|
3
|
+
*
|
|
4
|
+
* A model that has to call `SHOW TABLES` and then `DESC` before every question spends tool
|
|
5
|
+
* round-trips learning something the host already knows at assembly time. `ctx.systemPrompt.section`
|
|
6
|
+
* takes a function for exactly this case, and it is called synchronously, so this module reads the
|
|
7
|
+
* `status.json` sidecar an indexing run wrote rather than opening the index or reaching a server.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here is authoritative: the file is a cache of one run's summary, the index is the truth. A
|
|
10
|
+
* missing file therefore means "describing it would take a query" — never "there is no index".
|
|
11
|
+
* @module dsh-plugin-dbgraph-tool/prompt
|
|
12
|
+
*/
|
|
13
|
+
import type { StatusFile } from 'dsh-plugin-dbgraph-service';
|
|
14
|
+
/** The stable system-prompt guidance positioning dbgraph against ad-hoc schema queries. */
|
|
15
|
+
export declare const DBGRAPH_PROMPT_TEXT: string;
|
|
16
|
+
/** Objects the prompt never lists more than this many of, across all connections. */
|
|
17
|
+
export declare const DEFAULT_MAX_PROMPT_TABLES = 40;
|
|
18
|
+
/** Characters the whole digest may occupy, so one huge database cannot crowd out the prompt. */
|
|
19
|
+
export declare const DEFAULT_MAX_PROMPT_CHARS = 6000;
|
|
20
|
+
/**
|
|
21
|
+
* The directory every dbgraph file lives under: `~/.dsh/dbgraph`, or `DSH_DBGRAPH_ROOT` when a
|
|
22
|
+
* deployment (or a test) points somewhere else.
|
|
23
|
+
* @param env - the environment to read; defaults to this process's.
|
|
24
|
+
* @returns the absolute index root.
|
|
25
|
+
*/
|
|
26
|
+
export declare function dbgraphRoot(env?: NodeJS.ProcessEnv): string;
|
|
27
|
+
/**
|
|
28
|
+
* Read an indexing run's persisted summary.
|
|
29
|
+
*
|
|
30
|
+
* A prompt line must never fail an assembly, so an absent, unreadable, or badly shaped file all
|
|
31
|
+
* answer `undefined`; a single malformed connection entry is dropped rather than discarding the
|
|
32
|
+
* whole digest, because the other connections are still worth naming.
|
|
33
|
+
* @param root - the index root; defaults to {@link dbgraphRoot}.
|
|
34
|
+
* @returns the validated summary, or `undefined` when there is none to trust.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readStatusFile(root?: string): StatusFile | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The connection directories under the index root that actually hold an index file.
|
|
39
|
+
*
|
|
40
|
+
* Used only to tell "an index exists but its summary is missing" apart from "nothing has ever been
|
|
41
|
+
* indexed" — collapsing those two would invite rebuilding an index that is already there. One
|
|
42
|
+
* synchronous directory listing is cheap enough for assembly; a query is not.
|
|
43
|
+
* @param root - the index root; defaults to {@link dbgraphRoot}.
|
|
44
|
+
* @returns the connection directory names that contain an index, sorted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readIndexDirectories(root?: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Render an elapsed span the way a model reads it: one unit, no arithmetic left to do.
|
|
49
|
+
* @param elapsedMs - milliseconds since the event; a negative value reads as `刚刚`.
|
|
50
|
+
* @returns a short Chinese age phrase.
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatAge(elapsedMs: number): string;
|
|
53
|
+
/** Bounds one digest render; every field is overridable so a deployment can retune the prompt. */
|
|
54
|
+
export interface DigestLimits {
|
|
55
|
+
/** Objects listed at most across all connections (default {@link DEFAULT_MAX_PROMPT_TABLES}). */
|
|
56
|
+
readonly maxPromptTables?: number;
|
|
57
|
+
/** Characters the digest may occupy (default {@link DEFAULT_MAX_PROMPT_CHARS}). */
|
|
58
|
+
readonly maxPromptChars?: number;
|
|
59
|
+
/** Hours after which the newest index is marked stale (default 6, the seam's default). */
|
|
60
|
+
readonly staleHours?: number;
|
|
61
|
+
/** "Now" for the age sentence; defaults to `Date.now()`, and exists so a test is deterministic. */
|
|
62
|
+
readonly now?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render the whole digest: one header per connection, one line of its largest tables, and the
|
|
66
|
+
* sentence that says how to ask for what is not listed.
|
|
67
|
+
*
|
|
68
|
+
* The budget is applied in the order it protects the prompt: first at most
|
|
69
|
+
* {@link DigestLimits.maxPromptTables} objects across every connection, largest first, so the
|
|
70
|
+
* tables a question is most likely about survive; then characters, dropping the smallest remaining
|
|
71
|
+
* tables until the block fits. Every dropped table is counted into the `还有 N 张未列出` note, and
|
|
72
|
+
* `digestOmitted` from the sidecar is added to it, so the count is always the complete truth.
|
|
73
|
+
* @param status - the summary an indexing run wrote.
|
|
74
|
+
* @param limits - the budget and the moment to describe.
|
|
75
|
+
* @returns the digest text, ending with the usage sentence.
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderStatusDigest(status: StatusFile, limits?: DigestLimits): string;
|
|
78
|
+
/** What the caller of {@link dbgraphPromptHint} may override: where to look, and the budget. */
|
|
79
|
+
export interface PromptHintOptions extends DigestLimits {
|
|
80
|
+
/** Index root override; defaults to {@link dbgraphRoot}. */
|
|
81
|
+
readonly root?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The state of the configured connections, phrased for a system prompt.
|
|
85
|
+
*
|
|
86
|
+
* Three outcomes, deliberately distinguished: a summary to render, an index that exists without a
|
|
87
|
+
* summary (built by an older run, or before the sidecar was introduced), and no index at all.
|
|
88
|
+
* Collapsing the middle case into "no index" would invite rebuilding an index that is already there.
|
|
89
|
+
* @param options - the index root and the digest budget.
|
|
90
|
+
* @returns the digest, or one sentence naming the state and how to change it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function dbgraphPromptHint(options?: PromptHintOptions): string;
|
|
93
|
+
//# sourceMappingURL=prompt.d.ts.map
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema digest this plugin contributes to the system prompt on every request.
|
|
3
|
+
*
|
|
4
|
+
* A model that has to call `SHOW TABLES` and then `DESC` before every question spends tool
|
|
5
|
+
* round-trips learning something the host already knows at assembly time. `ctx.systemPrompt.section`
|
|
6
|
+
* takes a function for exactly this case, and it is called synchronously, so this module reads the
|
|
7
|
+
* `status.json` sidecar an indexing run wrote rather than opening the index or reaching a server.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here is authoritative: the file is a cache of one run's summary, the index is the truth. A
|
|
10
|
+
* missing file therefore means "describing it would take a query" — never "there is no index".
|
|
11
|
+
* @module dsh-plugin-dbgraph-tool/prompt
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { CONNECTIONS_FILE_NAME, DEFAULT_INDEX_STALE_HOURS, INDEX_FILE_NAME, INDEX_ROOT_DIRNAME, STATUS_FILE_NAME, } from 'dsh-plugin-dbgraph-service';
|
|
17
|
+
/** The stable system-prompt guidance positioning dbgraph against ad-hoc schema queries. */
|
|
18
|
+
export const DBGRAPH_PROMPT_TEXT = '用 dbgraph 回答数据库结构与取数问题:它读本地预建的索引,知道每个连接的库、表、列、注释、外键与样例值。'
|
|
19
|
+
+ '看上面的表清单就能直接点名表,不要先用 SHOW TABLES / DESC 摸底。最常用的是 dbgraph find —— 给它中文业务词、字段名和条件,'
|
|
20
|
+
+ '它离线把词定位到具体的表与列,再执行一次参数化查询,一次调用就出答案;只问结构用 dbgraph columns / objects / schema,'
|
|
21
|
+
+ '看依赖关系用 dbgraph impact / refs,必须自己写 SQL 时才用 dbgraph sql。索引缺失或过期时先调用 dbgraph_index(它有自己的长超时预算;'
|
|
22
|
+
+ '大库会转成后台 job,用 job_output 读报告)。查询结果里被判定为敏感的列会被遮蔽,这是预期行为。';
|
|
23
|
+
/** Objects the prompt never lists more than this many of, across all connections. */
|
|
24
|
+
export const DEFAULT_MAX_PROMPT_TABLES = 40;
|
|
25
|
+
/** Characters the whole digest may occupy, so one huge database cannot crowd out the prompt. */
|
|
26
|
+
export const DEFAULT_MAX_PROMPT_CHARS = 6000;
|
|
27
|
+
/** The sentence every digest ends with: how to ask for what is not on the list. */
|
|
28
|
+
const USAGE = '问字段用 dbgraph columns <表>;查值用 dbgraph find。';
|
|
29
|
+
/** The sentence appended when the newest index is past its freshness window. */
|
|
30
|
+
const STALE_USAGE = '索引已过期,运行 dbgraph_index 刷新。';
|
|
31
|
+
/**
|
|
32
|
+
* The directory every dbgraph file lives under: `~/.dsh/dbgraph`, or `DSH_DBGRAPH_ROOT` when a
|
|
33
|
+
* deployment (or a test) points somewhere else.
|
|
34
|
+
* @param env - the environment to read; defaults to this process's.
|
|
35
|
+
* @returns the absolute index root.
|
|
36
|
+
*/
|
|
37
|
+
export function dbgraphRoot(env = process.env) {
|
|
38
|
+
const override = env['DSH_DBGRAPH_ROOT'];
|
|
39
|
+
if (override !== undefined && override.trim() !== '')
|
|
40
|
+
return override;
|
|
41
|
+
return join(homedir(), '.dsh', INDEX_ROOT_DIRNAME);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read an indexing run's persisted summary.
|
|
45
|
+
*
|
|
46
|
+
* A prompt line must never fail an assembly, so an absent, unreadable, or badly shaped file all
|
|
47
|
+
* answer `undefined`; a single malformed connection entry is dropped rather than discarding the
|
|
48
|
+
* whole digest, because the other connections are still worth naming.
|
|
49
|
+
* @param root - the index root; defaults to {@link dbgraphRoot}.
|
|
50
|
+
* @returns the validated summary, or `undefined` when there is none to trust.
|
|
51
|
+
*/
|
|
52
|
+
export function readStatusFile(root = dbgraphRoot()) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(readFileSync(join(root, STATUS_FILE_NAME), 'utf8'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Absent, unreadable, or not JSON: all three mean "no summary to trust", which the caller
|
|
59
|
+
// renders as the weaker sentence rather than as an error.
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (!isRecord(parsed))
|
|
63
|
+
return undefined;
|
|
64
|
+
const version = numberField(parsed, 'version');
|
|
65
|
+
const updatedAt = numberField(parsed, 'updatedAt');
|
|
66
|
+
const connections = parsed['connections'];
|
|
67
|
+
if (version === undefined || updatedAt === undefined || !isRecord(connections))
|
|
68
|
+
return undefined;
|
|
69
|
+
const entries = {};
|
|
70
|
+
for (const [id, raw] of Object.entries(connections)) {
|
|
71
|
+
const entry = connectionEntry(raw);
|
|
72
|
+
if (entry !== undefined)
|
|
73
|
+
entries[id] = entry;
|
|
74
|
+
}
|
|
75
|
+
return { version, updatedAt, connections: entries };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The connection directories under the index root that actually hold an index file.
|
|
79
|
+
*
|
|
80
|
+
* Used only to tell "an index exists but its summary is missing" apart from "nothing has ever been
|
|
81
|
+
* indexed" — collapsing those two would invite rebuilding an index that is already there. One
|
|
82
|
+
* synchronous directory listing is cheap enough for assembly; a query is not.
|
|
83
|
+
* @param root - the index root; defaults to {@link dbgraphRoot}.
|
|
84
|
+
* @returns the connection directory names that contain an index, sorted.
|
|
85
|
+
*/
|
|
86
|
+
export function readIndexDirectories(root = dbgraphRoot()) {
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const found = [];
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.isDirectory())
|
|
97
|
+
continue;
|
|
98
|
+
if (existsSync(join(root, entry.name, INDEX_FILE_NAME)))
|
|
99
|
+
found.push(entry.name);
|
|
100
|
+
}
|
|
101
|
+
return found.sort();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Render an elapsed span the way a model reads it: one unit, no arithmetic left to do.
|
|
105
|
+
* @param elapsedMs - milliseconds since the event; a negative value reads as `刚刚`.
|
|
106
|
+
* @returns a short Chinese age phrase.
|
|
107
|
+
*/
|
|
108
|
+
export function formatAge(elapsedMs) {
|
|
109
|
+
if (elapsedMs < 60_000)
|
|
110
|
+
return '刚刚';
|
|
111
|
+
const minutes = Math.floor(elapsedMs / 60_000);
|
|
112
|
+
if (minutes < 60)
|
|
113
|
+
return `${minutes} 分钟前`;
|
|
114
|
+
const hours = Math.floor(minutes / 60);
|
|
115
|
+
if (hours < 24)
|
|
116
|
+
return `${hours} 小时前`;
|
|
117
|
+
return `${Math.floor(hours / 24)} 天前`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render one connection's digest header: engine, label, age, size, comment coverage, staleness.
|
|
121
|
+
* @param id - the connection id, which is also the argument the model passes to every tool.
|
|
122
|
+
* @param entry - the connection's indexed summary.
|
|
123
|
+
* @param staleHours - the freshness window.
|
|
124
|
+
* @param now - the moment the sentence describes.
|
|
125
|
+
* @returns the bracket-headed line.
|
|
126
|
+
*/
|
|
127
|
+
function connectionHeader(id, entry, staleHours, now) {
|
|
128
|
+
const parts = [entry.engine];
|
|
129
|
+
if (entry.label !== undefined && entry.label !== '')
|
|
130
|
+
parts.push(entry.label);
|
|
131
|
+
const newest = newestIndexedAt(entry);
|
|
132
|
+
parts.push(`索引 ${newest === undefined ? '未记录' : formatAge(now - newest)}`);
|
|
133
|
+
if (newest !== undefined && now - newest > staleHours * 3_600_000)
|
|
134
|
+
parts.push('已过期');
|
|
135
|
+
parts.push(`${entry.objects} 表 / ${entry.columns} 列`);
|
|
136
|
+
if (entry.columns > 0)
|
|
137
|
+
parts.push(`注释覆盖 ${Math.round(entry.commentCoverage * 100)}%`);
|
|
138
|
+
return `[dbgraph] ${id} (${parts.join(', ')})`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The newest indexed timestamp an entry carries, across the two index levels.
|
|
142
|
+
* @param entry - the connection's summary.
|
|
143
|
+
* @returns the epoch milliseconds, or `undefined` when the entry records neither level.
|
|
144
|
+
*/
|
|
145
|
+
function newestIndexedAt(entry) {
|
|
146
|
+
const candidates = [];
|
|
147
|
+
if (entry.l2IndexedAt !== undefined)
|
|
148
|
+
candidates.push(entry.l2IndexedAt);
|
|
149
|
+
if (entry.l1IndexedAt !== undefined)
|
|
150
|
+
candidates.push(entry.l1IndexedAt);
|
|
151
|
+
return candidates.length === 0 ? undefined : Math.max(...candidates);
|
|
152
|
+
}
|
|
153
|
+
/** One object's table token, e.g. `sys_user 系统用户(18)`. */
|
|
154
|
+
function tableToken(entry) {
|
|
155
|
+
const comment = entry.comment === undefined || entry.comment === '' ? '' : ` ${entry.comment}`;
|
|
156
|
+
return `${entry.object}${comment}(${entry.columns})`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Render the whole digest: one header per connection, one line of its largest tables, and the
|
|
160
|
+
* sentence that says how to ask for what is not listed.
|
|
161
|
+
*
|
|
162
|
+
* The budget is applied in the order it protects the prompt: first at most
|
|
163
|
+
* {@link DigestLimits.maxPromptTables} objects across every connection, largest first, so the
|
|
164
|
+
* tables a question is most likely about survive; then characters, dropping the smallest remaining
|
|
165
|
+
* tables until the block fits. Every dropped table is counted into the `还有 N 张未列出` note, and
|
|
166
|
+
* `digestOmitted` from the sidecar is added to it, so the count is always the complete truth.
|
|
167
|
+
* @param status - the summary an indexing run wrote.
|
|
168
|
+
* @param limits - the budget and the moment to describe.
|
|
169
|
+
* @returns the digest text, ending with the usage sentence.
|
|
170
|
+
*/
|
|
171
|
+
export function renderStatusDigest(status, limits = {}) {
|
|
172
|
+
const maxTables = Math.max(1, Math.trunc(limits.maxPromptTables ?? DEFAULT_MAX_PROMPT_TABLES));
|
|
173
|
+
const maxChars = Math.max(1, Math.trunc(limits.maxPromptChars ?? DEFAULT_MAX_PROMPT_CHARS));
|
|
174
|
+
const staleHours = limits.staleHours ?? DEFAULT_INDEX_STALE_HOURS;
|
|
175
|
+
const now = limits.now ?? Date.now();
|
|
176
|
+
const ids = Object.keys(status.connections);
|
|
177
|
+
if (ids.length === 0)
|
|
178
|
+
return `[dbgraph] 还没有配置连接\n${USAGE}`;
|
|
179
|
+
const headers = ids.map(id => connectionHeader(id, status.connections[id], staleHours, now));
|
|
180
|
+
const anyStale = ids.some((id) => {
|
|
181
|
+
const newest = newestIndexedAt(status.connections[id]);
|
|
182
|
+
return newest !== undefined && now - newest > staleHours * 3_600_000;
|
|
183
|
+
});
|
|
184
|
+
const usage = anyStale ? `${USAGE}${STALE_USAGE}` : USAGE;
|
|
185
|
+
// Every object across every connection, largest first. A global sort is what makes the table cap
|
|
186
|
+
// keep the tables a question is most likely about, whichever connection they belong to.
|
|
187
|
+
const pooled = ids.flatMap(id => status.connections[id].digest.map(entry => ({ id, entry })));
|
|
188
|
+
const sorted = [...pooled].sort((left, right) => right.entry.columns - left.entry.columns
|
|
189
|
+
|| left.id.localeCompare(right.id)
|
|
190
|
+
|| left.entry.object.localeCompare(right.entry.object));
|
|
191
|
+
let kept = sorted.slice(0, maxTables);
|
|
192
|
+
let text = assembleDigest(ids, headers, kept, status, usage);
|
|
193
|
+
// Character budget second: drop the smallest remaining tables until the block fits. The count of
|
|
194
|
+
// dropped tables lands in the same note, so a shortened list never reads as a complete one.
|
|
195
|
+
while (kept.length > 0 && text.length > maxChars) {
|
|
196
|
+
kept = kept.slice(0, -1);
|
|
197
|
+
text = assembleDigest(ids, headers, kept, status, usage);
|
|
198
|
+
}
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Assemble headers, per-connection table lines, and the usage sentence for one kept set.
|
|
203
|
+
* @param ids - connection ids in configuration order.
|
|
204
|
+
* @param headers - one rendered header per id, in the same order.
|
|
205
|
+
* @param kept - the surviving objects, largest first.
|
|
206
|
+
* @param status - the summary the omitted counts come from.
|
|
207
|
+
* @param usage - the closing sentence.
|
|
208
|
+
* @returns the digest text.
|
|
209
|
+
*/
|
|
210
|
+
function assembleDigest(ids, headers, kept, status, usage) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
const multi = ids.length > 1;
|
|
213
|
+
for (const [index, id] of ids.entries()) {
|
|
214
|
+
// Header then its own tables: with several connections, interleaving is what keeps a reader
|
|
215
|
+
// from attributing one database's table list to another.
|
|
216
|
+
lines.push(headers[index]);
|
|
217
|
+
const entry = status.connections[id];
|
|
218
|
+
const shown = kept.filter(candidate => candidate.id === id);
|
|
219
|
+
const omitted = entry.digestOmitted + (entry.digest.length - shown.length);
|
|
220
|
+
const prefix = multi ? `${id}: ` : '';
|
|
221
|
+
const tokens = shown.map(candidate => tableToken(candidate.entry));
|
|
222
|
+
if (tokens.length === 0) {
|
|
223
|
+
lines.push(omitted > 0 ? `${prefix}(还有 ${omitted} 张未列出)` : `${prefix}(没有可列出的对象)`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const note = omitted > 0 ? `(还有 ${omitted} 张未列出)` : '';
|
|
227
|
+
lines.push(`${prefix}${tokens.join(' | ')}${note}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push(usage);
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The state of the configured connections, phrased for a system prompt.
|
|
234
|
+
*
|
|
235
|
+
* Three outcomes, deliberately distinguished: a summary to render, an index that exists without a
|
|
236
|
+
* summary (built by an older run, or before the sidecar was introduced), and no index at all.
|
|
237
|
+
* Collapsing the middle case into "no index" would invite rebuilding an index that is already there.
|
|
238
|
+
* @param options - the index root and the digest budget.
|
|
239
|
+
* @returns the digest, or one sentence naming the state and how to change it.
|
|
240
|
+
*/
|
|
241
|
+
export function dbgraphPromptHint(options = {}) {
|
|
242
|
+
const root = options.root ?? dbgraphRoot();
|
|
243
|
+
const status = readStatusFile(root);
|
|
244
|
+
if (status !== undefined) {
|
|
245
|
+
if (Object.keys(status.connections).length > 0)
|
|
246
|
+
return renderStatusDigest(status, options);
|
|
247
|
+
return `[dbgraph] 索引根目录存在,但还没有配置任何连接:在 ${join(root, CONNECTIONS_FILE_NAME)} 里添加连接,再运行 dbgraph_index。`;
|
|
248
|
+
}
|
|
249
|
+
const directories = readIndexDirectories(root);
|
|
250
|
+
if (directories.length > 0) {
|
|
251
|
+
return `已建立 dbgraph 索引(${directories.length} 个连接目录:${directories.join(', ')}),但缺少 ${STATUS_FILE_NAME} 摘要`
|
|
252
|
+
+ ' —— 运行 dbgraph_index 刷新一次即可恢复表清单;结构查询本身已经可用。';
|
|
253
|
+
}
|
|
254
|
+
return '还没有建立数据库索引:先运行 dbgraph_index(或 /dbgraph index)建立索引,之后这里会自动列出表清单。';
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* One numeric field of a parsed object, or `undefined` when it is absent or not a finite number.
|
|
258
|
+
* @param record - the parsed object.
|
|
259
|
+
* @param field - the field name to read.
|
|
260
|
+
* @returns the number, or `undefined`.
|
|
261
|
+
*/
|
|
262
|
+
function numberField(record, field) {
|
|
263
|
+
const value = record[field];
|
|
264
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Narrow a parsed JSON value to a plain object.
|
|
268
|
+
* @param value - the parsed value.
|
|
269
|
+
* @returns whether it is a non-null, non-array object.
|
|
270
|
+
*/
|
|
271
|
+
function isRecord(value) {
|
|
272
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Validate one connection entry of `status.json`.
|
|
276
|
+
* @param raw - the parsed entry.
|
|
277
|
+
* @returns the typed entry, or `undefined` when a required field is missing or mistyped.
|
|
278
|
+
*/
|
|
279
|
+
function connectionEntry(raw) {
|
|
280
|
+
if (!isRecord(raw))
|
|
281
|
+
return undefined;
|
|
282
|
+
const engine = raw['engine'];
|
|
283
|
+
const objects = numberField(raw, 'objects');
|
|
284
|
+
const columns = numberField(raw, 'columns');
|
|
285
|
+
const commentCoverage = numberField(raw, 'commentCoverage');
|
|
286
|
+
const digestOmitted = numberField(raw, 'digestOmitted');
|
|
287
|
+
const digestRaw = raw['digest'];
|
|
288
|
+
if (typeof engine !== 'string' || objects === undefined || columns === undefined
|
|
289
|
+
|| commentCoverage === undefined || digestOmitted === undefined || !Array.isArray(digestRaw))
|
|
290
|
+
return undefined;
|
|
291
|
+
const digest = [];
|
|
292
|
+
for (const item of digestRaw) {
|
|
293
|
+
const entry = digestEntry(item);
|
|
294
|
+
if (entry !== undefined)
|
|
295
|
+
digest.push(entry);
|
|
296
|
+
}
|
|
297
|
+
const label = stringField(raw, 'label');
|
|
298
|
+
const l1IndexedAt = numberField(raw, 'l1IndexedAt');
|
|
299
|
+
const l2IndexedAt = numberField(raw, 'l2IndexedAt');
|
|
300
|
+
return {
|
|
301
|
+
engine,
|
|
302
|
+
...label === undefined ? {} : { label },
|
|
303
|
+
...l1IndexedAt === undefined ? {} : { l1IndexedAt },
|
|
304
|
+
...l2IndexedAt === undefined ? {} : { l2IndexedAt },
|
|
305
|
+
objects,
|
|
306
|
+
columns,
|
|
307
|
+
commentCoverage,
|
|
308
|
+
digest,
|
|
309
|
+
digestOmitted,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Validate one object of a connection's digest list.
|
|
314
|
+
* @param raw - the parsed entry.
|
|
315
|
+
* @returns the typed entry, or `undefined` when it cannot be named.
|
|
316
|
+
*/
|
|
317
|
+
function digestEntry(raw) {
|
|
318
|
+
if (!isRecord(raw))
|
|
319
|
+
return undefined;
|
|
320
|
+
const object = raw['object'];
|
|
321
|
+
const columns = numberField(raw, 'columns');
|
|
322
|
+
if (typeof object !== 'string' || object === '' || columns === undefined)
|
|
323
|
+
return undefined;
|
|
324
|
+
const comment = stringField(raw, 'comment');
|
|
325
|
+
return { object, ...comment === undefined ? {} : { comment }, columns };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* One optional string field of a parsed object.
|
|
329
|
+
* @param record - the parsed object.
|
|
330
|
+
* @param field - the field name to read.
|
|
331
|
+
* @returns the string, or `undefined` when it is absent or empty.
|
|
332
|
+
*/
|
|
333
|
+
function stringField(record, field) {
|
|
334
|
+
const value = record[field];
|
|
335
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=prompt.js.map
|
package/lib/render.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing text for each tool's canonical value.
|
|
3
|
+
*
|
|
4
|
+
* Two rules, both learned from what a model does with an answer: a capped list always says it is
|
|
5
|
+
* capped (a list that reads as complete is worse than a short one, because the model stops asking),
|
|
6
|
+
* and a row set is rendered as rows — never as JSON, which buries the same bytes in punctuation.
|
|
7
|
+
* @module dsh-plugin-dbgraph-tool/render
|
|
8
|
+
*/
|
|
9
|
+
import type { DbgraphIndexToolValue, DbgraphToolValue } from './schema.ts';
|
|
10
|
+
/**
|
|
11
|
+
* One connection's index state, phrased for a model deciding whether to reindex.
|
|
12
|
+
* @param index - the seam's index state.
|
|
13
|
+
* @returns a short clause, never empty.
|
|
14
|
+
*/
|
|
15
|
+
export declare function describeIndexState(index: {
|
|
16
|
+
readonly l1_indexed_at?: number;
|
|
17
|
+
readonly l2_indexed_at?: number;
|
|
18
|
+
readonly objects: number;
|
|
19
|
+
readonly columns: number;
|
|
20
|
+
readonly stale: boolean;
|
|
21
|
+
}): string;
|
|
22
|
+
/**
|
|
23
|
+
* Render one query-side result as the text the model reads.
|
|
24
|
+
* @param value - the canonical value the operation returned.
|
|
25
|
+
* @returns the rendered text.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderDbgraph(value: DbgraphToolValue): string;
|
|
28
|
+
/**
|
|
29
|
+
* Render one build-side result as the text the model reads.
|
|
30
|
+
* @param value - the canonical value the index tool returned.
|
|
31
|
+
* @returns the rendered text.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderDbgraphIndex(value: DbgraphIndexToolValue): string;
|
|
34
|
+
//# sourceMappingURL=render.d.ts.map
|