@askdb/rag 0.2.0-beta.23 → 0.2.0-beta.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/bin/askdb-rag.js +8 -1
- package/dist/chunker/chunker.d.ts +5 -1
- package/dist/chunker/chunker.d.ts.map +1 -1
- package/dist/chunker/chunker.js +198 -130
- package/dist/chunker/chunker.js.map +1 -1
- package/dist/chunker/ids.d.ts +15 -0
- package/dist/chunker/ids.d.ts.map +1 -0
- package/dist/chunker/ids.js +22 -0
- package/dist/chunker/ids.js.map +1 -0
- package/dist/chunker/index.d.ts +1 -0
- package/dist/chunker/index.d.ts.map +1 -1
- package/dist/chunker/index.js +1 -0
- package/dist/chunker/index.js.map +1 -1
- package/dist/chunker/sources.d.ts.map +1 -1
- package/dist/chunker/sources.js +1 -0
- package/dist/chunker/sources.js.map +1 -1
- package/dist/embedders/ai-sdk.d.ts +15 -0
- package/dist/embedders/ai-sdk.d.ts.map +1 -1
- package/dist/embedders/ai-sdk.js +9 -0
- package/dist/embedders/ai-sdk.js.map +1 -1
- package/dist/embedders/dimensions.d.ts +9 -0
- package/dist/embedders/dimensions.d.ts.map +1 -0
- package/dist/embedders/dimensions.js +18 -0
- package/dist/embedders/dimensions.js.map +1 -0
- package/dist/embedders/openai.d.ts +1 -1
- package/dist/embedders/openai.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/indexer/hash.d.ts +7 -0
- package/dist/indexer/hash.d.ts.map +1 -1
- package/dist/indexer/hash.js +13 -0
- package/dist/indexer/hash.js.map +1 -1
- package/dist/indexer/index.d.ts +10 -4
- package/dist/indexer/index.d.ts.map +1 -1
- package/dist/indexer/index.js +167 -30
- package/dist/indexer/index.js.map +1 -1
- package/dist/indexer/lock-file.d.ts +85 -4
- package/dist/indexer/lock-file.d.ts.map +1 -1
- package/dist/indexer/lock-file.js +108 -9
- package/dist/indexer/lock-file.js.map +1 -1
- package/dist/log-events.d.ts +11 -0
- package/dist/log-events.d.ts.map +1 -1
- package/dist/log-events.js +11 -0
- package/dist/log-events.js.map +1 -1
- package/dist/stores/file.d.ts +7 -1
- package/dist/stores/file.d.ts.map +1 -1
- package/dist/stores/file.js +52 -10
- package/dist/stores/file.js.map +1 -1
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +31 -3
- package/dist/stores/memory.js.map +1 -1
- package/dist/stores/pgvector.d.ts +36 -3
- package/dist/stores/pgvector.d.ts.map +1 -1
- package/dist/stores/pgvector.js +125 -17
- package/dist/stores/pgvector.js.map +1 -1
- package/dist/types.d.ts +69 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -12
- package/dist/bin.d.ts +0 -3
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js +0 -7
- package/dist/bin.js.map +0 -1
- package/dist/cli.d.ts +0 -11
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -356
- package/dist/cli.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,14 +7,14 @@ Deterministic retrieval over AskDB Schema v2 artifacts. `@askdb/rag` chunks the
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
pnpm add @askdb/rag @askdb/core
|
|
10
|
+
pnpm add @askdb/rag @askdb/core ai
|
|
11
11
|
# only if you use the pgvector adapter:
|
|
12
12
|
pnpm add pg
|
|
13
|
-
# only if you use the OpenAI embedder helper
|
|
14
|
-
pnpm add
|
|
13
|
+
# only if you use the OpenAI embedder helper:
|
|
14
|
+
pnpm add @ai-sdk/openai
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`pg`, `ai`, and `@ai-sdk/openai` are optional peer dependencies
|
|
17
|
+
`ai` is a required peer dependency of `@askdb/core`. For `@askdb/rag` itself, `pg`, `ai`, and `@ai-sdk/openai` are optional peer dependencies — the chunker, in-memory store, and file store do not require them.
|
|
18
18
|
|
|
19
19
|
## Quickstart
|
|
20
20
|
|
|
@@ -52,13 +52,15 @@ const { sql } = await ask({
|
|
|
52
52
|
|
|
53
53
|
## CLI
|
|
54
54
|
|
|
55
|
+
`@askdb/rag` is a library. The command line for it is `askdb rag` in the [`askdb`](https://www.npmjs.com/package/askdb) package, which reads `rag.*` and `ai.embedding` from `askdb.config.*`:
|
|
56
|
+
|
|
55
57
|
```bash
|
|
56
|
-
askdb
|
|
57
|
-
askdb
|
|
58
|
+
npx askdb rag index fixtures/schemas/orders-users.schema --store file
|
|
59
|
+
npx askdb rag query fixtures/schemas/orders-users.schema \
|
|
58
60
|
--question "How much revenue did we make last month?"
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
See the [CLI reference](https://askdb.tools/reference/cli/#askdb-rag) for every flag. The `askdb-rag` binary this package used to ship now only points at `askdb rag`, and is removed at 1.0.
|
|
62
64
|
|
|
63
65
|
## Public Surface
|
|
64
66
|
|
|
@@ -76,14 +78,16 @@ All exports are available from the root `@askdb/rag` import. Sub-path imports ar
|
|
|
76
78
|
|---|---|---|
|
|
77
79
|
| `createMemoryStore` | `@askdb/rag/stores/memory` | In-memory cosine store. Zero deps. |
|
|
78
80
|
| `createFileStore` | `@askdb/rag/stores/file` | Binary embedding file + JSON metadata. |
|
|
79
|
-
| `createPgvectorStore` | `@askdb/rag/stores/pgvector` | pgvector adapter with documented setup SQL. Requires `pg`. |
|
|
81
|
+
| `createPgvectorStore` | `@askdb/rag/stores/pgvector` | pgvector adapter with documented setup SQL. Requires `pg`. Needs `dimensions` only to create its table; `ensureSchema()` refuses an existing table of another width with a `PgvectorDimensionMismatchError`. |
|
|
80
82
|
|
|
81
83
|
### Embedders
|
|
82
84
|
|
|
83
85
|
| Root import | Sub-path import | Description |
|
|
84
86
|
|---|---|---|
|
|
85
87
|
| `createAiSdkEmbedder` | `@askdb/rag/embedders/ai-sdk` | Generic AI SDK `EmbeddingModel` adapter. Requires `ai`. |
|
|
86
|
-
| `
|
|
88
|
+
| `detectEmbeddingDimensions` | — | Learns an embedder's vector width by embedding one short text. Use it for a new pgvector table instead of hard-coding a width. |
|
|
89
|
+
| `aiSdkEmbedderId` | `@askdb/rag/embedders/ai-sdk` | The `embedderId` Studio and `askdb rag` record for an `ai.embedding` model (`ai-sdk:<provider>:<model>:<dimensions or default>`), so an index built by one is accepted by the other. |
|
|
90
|
+
| `createOpenAiEmbedder` | `@askdb/rag/embedders/openai` | **Deprecated.** OpenAI convenience helper. Use `createAiSdkEmbedder` with an AI SDK embedding model (e.g. from `@ai-sdk/openai`) or one built by the `@askdb/ai` registry instead. Removed in 1.0. |
|
|
87
91
|
|
|
88
92
|
### Import examples
|
|
89
93
|
|
|
@@ -101,7 +105,7 @@ import { createMemoryStore } from "@askdb/rag/stores/memory";
|
|
|
101
105
|
import { createFileStore } from "@askdb/rag/stores/file";
|
|
102
106
|
import { createPgvectorStore } from "@askdb/rag/stores/pgvector";
|
|
103
107
|
import { createAiSdkEmbedder } from "@askdb/rag/embedders/ai-sdk";
|
|
104
|
-
// Recommended: bring your own model
|
|
108
|
+
// Recommended: bring your own embedding model (e.g. @ai-sdk/openai, or @askdb/ai's registry) + createAiSdkEmbedder
|
|
105
109
|
// import { createOpenAiEmbedder } from "@askdb/rag/embedders/openai"; // deprecated, removed in 1.0
|
|
106
110
|
```
|
|
107
111
|
|
package/bin/askdb-rag.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Deprecated: the RAG CLI moved into the `askdb` package. Removed before 1.0.
|
|
3
|
+
// Names only the command: the other arguments can hold a connection string's password.
|
|
4
|
+
const command = ["index", "query", "setup-store"].includes(process.argv[2]) ? ` ${process.argv[2]}` : "";
|
|
5
|
+
process.stderr.write(
|
|
6
|
+
"askdb-rag has moved: use `npx askdb rag" + command + "` (see `npx askdb rag --help` for its flags).\n" +
|
|
7
|
+
"`askdb rag` reads rag.store, rag.storeConfig and ai.embedding from askdb.config.*; flags still override.\n",
|
|
8
|
+
);
|
|
9
|
+
process.exit(1);
|
|
@@ -11,7 +11,11 @@ import type { ChunkerSources } from "./sources.js";
|
|
|
11
11
|
export type ChunkStats = {
|
|
12
12
|
totalChunks: number;
|
|
13
13
|
byType: Record<Chunk["type"], number>;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Chunks dropped, plus chunks emitted with part of their describable text
|
|
16
|
+
* (a description, alias, primary entity, enum values, or note) dropped,
|
|
17
|
+
* because the content belongs to or mentions a sensitive column/table.
|
|
18
|
+
*/
|
|
15
19
|
sensitiveExcluded: number;
|
|
16
20
|
/** Describable-layer chunks emitted **only because** `includeSensitiveDescribable: true` was set. */
|
|
17
21
|
sensitiveIncluded: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../src/chunker/chunker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../src/chunker/chunker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACtC;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qGAAqG;IACrG,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,qCAAqC;AACrC,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,KAAK,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,YAAiB,GACzB,WAAW,CAqQb"}
|
package/dist/chunker/chunker.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createMentionMatcher, sensitiveMentionNames } from "@askdb/core";
|
|
1
2
|
import { DEFAULT_CHUNK_MAX_CHARS, } from "./options.js";
|
|
3
|
+
import { chunkId } from "./ids.js";
|
|
2
4
|
/**
|
|
3
5
|
* Deterministic chunker for a Schema v2 artifact.
|
|
4
6
|
*
|
|
@@ -30,22 +32,34 @@ export function chunkSchema(sources, options = {}) {
|
|
|
30
32
|
sensitiveIncluded: 0,
|
|
31
33
|
};
|
|
32
34
|
const { schema, tables: tableMarkdowns, concepts } = sources;
|
|
35
|
+
// Names whose mention marks describable text as sensitive, schema-wide
|
|
36
|
+
// (ADR 0017): every table's text, concept, and tenant policy section is
|
|
37
|
+
// checked against them.
|
|
38
|
+
const sensitiveMentions = createMentionMatcher(sensitiveMentionNames(schema));
|
|
33
39
|
for (const table of schema.tables) {
|
|
34
40
|
if (table.tracked === false) {
|
|
35
41
|
continue;
|
|
36
42
|
}
|
|
37
43
|
const md = tableMarkdowns[table.id];
|
|
38
|
-
const sensitiveColumnNames = table.columns
|
|
39
|
-
.filter((c) => c.sensitive)
|
|
40
|
-
.map((c) => c.name);
|
|
41
44
|
if (table.sensitive && !includeSensitive) {
|
|
42
45
|
stats.sensitiveExcluded++;
|
|
43
46
|
}
|
|
44
47
|
else {
|
|
45
|
-
|
|
48
|
+
const filter = describableFilter(sensitiveMentions, includeSensitive);
|
|
49
|
+
const tableChunk = buildTableChunk(table, schema.schemaId, includeSensitive, schema, filter);
|
|
50
|
+
// A non-sensitive table whose description/aliases/column headlines
|
|
51
|
+
// mention a sensitive column is only embedded verbatim in opt-in mode.
|
|
52
|
+
if (filter.dropped)
|
|
53
|
+
stats.sensitiveExcluded++;
|
|
54
|
+
// Opt-in: a sensitive table's chunk is emitted only because of it.
|
|
55
|
+
if (table.sensitive || filter.included)
|
|
56
|
+
stats.sensitiveIncluded++;
|
|
57
|
+
chunks.push(tableChunk);
|
|
46
58
|
}
|
|
47
59
|
// Column chunks — one per column. Identifier + type always; describable
|
|
48
|
-
// fields only when not sensitive (or when opted in).
|
|
60
|
+
// fields only when not sensitive (or when opted in). A non-sensitive
|
|
61
|
+
// column whose describable fields name a sensitive column is treated
|
|
62
|
+
// like a sensitive column's describable layer.
|
|
49
63
|
for (const col of table.columns) {
|
|
50
64
|
const colNote = readColumnNote(md, col.name);
|
|
51
65
|
const colSensitive = col.sensitive || table.sensitive;
|
|
@@ -53,27 +67,46 @@ export function chunkSchema(sources, options = {}) {
|
|
|
53
67
|
stats.sensitiveExcluded++;
|
|
54
68
|
continue;
|
|
55
69
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
const describable = columnDescribableTexts(col, colNote);
|
|
71
|
+
if (colSensitive) {
|
|
72
|
+
// Opt-in mode: the default leaves this chunk out entirely.
|
|
73
|
+
stats.sensitiveIncluded++;
|
|
74
|
+
chunks.push(buildColumnChunk(table, col, schema.schemaId, colNote, true, true));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const describableMentionsSensitive = describable.some((text) => sensitiveMentions.mentionsAny(text));
|
|
78
|
+
if (describableMentionsSensitive && !includeSensitive) {
|
|
79
|
+
// Keep the identifier + type; drop the whole describable layer.
|
|
80
|
+
stats.sensitiveExcluded++;
|
|
81
|
+
chunks.push(buildColumnChunk(table, col, schema.schemaId, colNote, false, false));
|
|
82
|
+
continue;
|
|
60
83
|
}
|
|
61
|
-
|
|
84
|
+
if (describableMentionsSensitive)
|
|
85
|
+
stats.sensitiveIncluded++;
|
|
86
|
+
chunks.push(buildColumnChunk(table, col, schema.schemaId, colNote, true, describableMentionsSensitive));
|
|
62
87
|
}
|
|
63
88
|
// CQL chunk — excluded entirely if the body mentions a sensitive column by name.
|
|
64
89
|
if (table.commonQueryLanguage) {
|
|
65
|
-
const mentionsSensitive =
|
|
90
|
+
const mentionsSensitive = sensitiveMentions.mentionsAny(table.commonQueryLanguage);
|
|
66
91
|
const tableLevelSensitive = table.sensitive;
|
|
67
92
|
const skip = (tableLevelSensitive || mentionsSensitive) && !includeSensitive;
|
|
93
|
+
// Counted per chunk: a long body splits into several.
|
|
94
|
+
const parts = splitLong(table.commonQueryLanguage, maxChars);
|
|
68
95
|
if (skip) {
|
|
69
|
-
stats.sensitiveExcluded
|
|
96
|
+
stats.sensitiveExcluded += parts.length;
|
|
70
97
|
}
|
|
71
98
|
else {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
// The heading repeats the table's aliases; one naming a sensitive
|
|
100
|
+
// column is dropped like it is from the table chunk.
|
|
101
|
+
const filter = describableFilter(sensitiveMentions, includeSensitive);
|
|
102
|
+
const aliases = (table.aliases ?? []).filter((a) => filter.allow(a));
|
|
103
|
+
const sensitive = tableLevelSensitive || mentionsSensitive || filter.included;
|
|
104
|
+
if (filter.dropped)
|
|
105
|
+
stats.sensitiveExcluded += parts.length;
|
|
106
|
+
if (sensitive)
|
|
107
|
+
stats.sensitiveIncluded += parts.length;
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
chunks.push(buildCqlChunk(table, aliases, part.text, part.suffix, schema.schemaId, sensitive));
|
|
77
110
|
}
|
|
78
111
|
}
|
|
79
112
|
}
|
|
@@ -81,8 +114,8 @@ export function chunkSchema(sources, options = {}) {
|
|
|
81
114
|
if (md) {
|
|
82
115
|
const questions = extractExampleQuestions(md);
|
|
83
116
|
const businessContext = md.sections["Business context"]?.trim() ?? "";
|
|
84
|
-
const questionsMentionSensitive = questions.some((q) =>
|
|
85
|
-
const bizMentionsSensitive =
|
|
117
|
+
const questionsMentionSensitive = questions.some((q) => sensitiveMentions.mentionsAny(q));
|
|
118
|
+
const bizMentionsSensitive = sensitiveMentions.mentionsAny(businessContext);
|
|
86
119
|
// Per-table sensitive gate: if table is sensitive OR mentions exist, skip
|
|
87
120
|
// the *whole* describable layer for that table by default. Authoring rule
|
|
88
121
|
// in the contract notes the chunker excludes whole chunks rather than
|
|
@@ -93,25 +126,34 @@ export function chunkSchema(sources, options = {}) {
|
|
|
93
126
|
stats.sensitiveExcluded += questions.length;
|
|
94
127
|
}
|
|
95
128
|
else {
|
|
96
|
-
|
|
129
|
+
// Every question heading repeats the primary entity; one naming a
|
|
130
|
+
// sensitive column is dropped like it is from the table chunk.
|
|
131
|
+
const filter = describableFilter(sensitiveMentions, includeSensitive);
|
|
132
|
+
const primaryEntity = filter.allow(table.primaryEntity) ? table.primaryEntity : undefined;
|
|
133
|
+
if (filter.dropped)
|
|
134
|
+
stats.sensitiveExcluded += questions.length;
|
|
135
|
+
// The questions are gated together, so in opt-in mode one naming a
|
|
136
|
+
// sensitive column makes every question chunk of the table sensitive.
|
|
137
|
+
const questionsSensitive = table.sensitive || questionsMentionSensitive || filter.included;
|
|
138
|
+
if (questionsSensitive)
|
|
97
139
|
stats.sensitiveIncluded += questions.length;
|
|
98
|
-
}
|
|
99
140
|
questions.forEach((q, i) => {
|
|
100
|
-
chunks.push(buildQuestionChunk(table, q, i + 1, schema.schemaId,
|
|
141
|
+
chunks.push(buildQuestionChunk(table, primaryEntity, q, i + 1, schema.schemaId, questionsSensitive));
|
|
101
142
|
});
|
|
102
143
|
}
|
|
103
144
|
}
|
|
104
145
|
// Business-context chunks are part of the describable layer; same gate.
|
|
105
146
|
if (businessContext) {
|
|
106
147
|
const skipBiz = (table.sensitive || bizMentionsSensitive) && !includeSensitive;
|
|
148
|
+
const parts = splitLong(businessContext, maxChars);
|
|
107
149
|
if (skipBiz) {
|
|
108
|
-
stats.sensitiveExcluded
|
|
150
|
+
stats.sensitiveExcluded += parts.length;
|
|
109
151
|
}
|
|
110
152
|
else {
|
|
111
153
|
if (table.sensitive || bizMentionsSensitive) {
|
|
112
|
-
stats.sensitiveIncluded
|
|
154
|
+
stats.sensitiveIncluded += parts.length;
|
|
113
155
|
}
|
|
114
|
-
for (const part of
|
|
156
|
+
for (const part of parts) {
|
|
115
157
|
chunks.push(buildBusinessContextChunk(table, part.text, part.suffix, schema.schemaId, table.sensitive || bizMentionsSensitive));
|
|
116
158
|
}
|
|
117
159
|
}
|
|
@@ -134,30 +176,45 @@ export function chunkSchema(sources, options = {}) {
|
|
|
134
176
|
}
|
|
135
177
|
}
|
|
136
178
|
}
|
|
137
|
-
// Concept chunks.
|
|
138
|
-
//
|
|
179
|
+
// Concept chunks. A concept that links to a sensitive column/table, or
|
|
180
|
+
// whose label/synonyms/description names a sensitive column (matched
|
|
181
|
+
// case-insensitively, like @askdb/enrich), is excluded by default.
|
|
139
182
|
if (concepts?.frontmatter.concepts) {
|
|
140
|
-
const allSensitiveColumnNames = collectSensitiveColumnNames(schema);
|
|
141
183
|
for (const concept of concepts.frontmatter.concepts) {
|
|
142
|
-
const conceptResult = buildConceptChunk(concept, schema.schemaId, schema,
|
|
143
|
-
if (conceptResult.
|
|
184
|
+
const conceptResult = buildConceptChunk(concept, schema.schemaId, schema, sensitiveMentions);
|
|
185
|
+
if (conceptResult.sensitive && !includeSensitive) {
|
|
144
186
|
stats.sensitiveExcluded++;
|
|
145
|
-
|
|
146
|
-
stats.sensitiveIncluded++;
|
|
147
|
-
chunks.push(conceptResult.chunk);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
if (conceptResult.sensitive)
|
|
152
|
-
stats.sensitiveIncluded++;
|
|
153
|
-
chunks.push(conceptResult.chunk);
|
|
187
|
+
continue;
|
|
154
188
|
}
|
|
189
|
+
if (conceptResult.sensitive)
|
|
190
|
+
stats.sensitiveIncluded++;
|
|
191
|
+
chunks.push(conceptResult.chunk);
|
|
155
192
|
}
|
|
156
193
|
}
|
|
157
|
-
// Tenant policy body chunks — one per H2 section.
|
|
194
|
+
// Tenant policy body chunks — one per H2 section. The policy is
|
|
195
|
+
// schema-level prose, so like concepts it is checked against every
|
|
196
|
+
// sensitive column in the schema; a section naming one is excluded by
|
|
197
|
+
// default.
|
|
158
198
|
if (sources.tenantPolicy) {
|
|
159
|
-
for (const
|
|
160
|
-
|
|
199
|
+
for (const section of tenantPolicySections(sources.tenantPolicy)) {
|
|
200
|
+
const sensitive = sensitiveMentions.mentionsAny(section.body);
|
|
201
|
+
const parts = splitLong(section.body, maxChars);
|
|
202
|
+
if (sensitive && !includeSensitive) {
|
|
203
|
+
stats.sensitiveExcluded += parts.length;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (sensitive)
|
|
207
|
+
stats.sensitiveIncluded += parts.length;
|
|
208
|
+
for (const part of parts) {
|
|
209
|
+
chunks.push({
|
|
210
|
+
id: chunkId(schema.schemaId, `tenant-policy#${section.slug}${part.suffix}`),
|
|
211
|
+
type: "tenant-policy",
|
|
212
|
+
text: `${section.heading}\n${part.text}`,
|
|
213
|
+
schemaId: schema.schemaId,
|
|
214
|
+
refs: [],
|
|
215
|
+
sensitive,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
161
218
|
}
|
|
162
219
|
}
|
|
163
220
|
// Final ordering: stable by id (string compare). The chunker itself emits in
|
|
@@ -171,16 +228,20 @@ export function chunkSchema(sources, options = {}) {
|
|
|
171
228
|
return { chunks, stats };
|
|
172
229
|
}
|
|
173
230
|
// ---------- chunk builders ----------
|
|
174
|
-
function buildTableChunk(table, schemaId, includeSensitive, schema) {
|
|
231
|
+
function buildTableChunk(table, schemaId, includeSensitive, schema, filter) {
|
|
175
232
|
const lines = [];
|
|
176
233
|
const qualified = `${table.schema}.${table.name}`;
|
|
234
|
+
// Describable text that names a sensitive column is dropped by default and
|
|
235
|
+
// flags the chunk as sensitive when kept in opt-in mode.
|
|
236
|
+
const { allow } = filter;
|
|
177
237
|
lines.push(`# ${qualified}`);
|
|
178
238
|
if (!table.sensitive) {
|
|
179
|
-
if (table.description)
|
|
239
|
+
if (allow(table.description))
|
|
180
240
|
lines.push(table.description);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
241
|
+
const aliases = (table.aliases ?? []).filter((a) => allow(a));
|
|
242
|
+
if (aliases.length)
|
|
243
|
+
lines.push(`Aliases: ${aliases.join(", ")}`);
|
|
244
|
+
if (allow(table.primaryEntity))
|
|
184
245
|
lines.push(`Primary entity: ${table.primaryEntity}`);
|
|
185
246
|
}
|
|
186
247
|
if (table.relationships?.length) {
|
|
@@ -206,31 +267,36 @@ function buildTableChunk(table, schemaId, includeSensitive, schema) {
|
|
|
206
267
|
flags.push("PK");
|
|
207
268
|
flags.push(c.nullable ? "NULL" : "NOT NULL");
|
|
208
269
|
const tag = c.sensitive || table.sensitive ? " (sensitive)" : "";
|
|
209
|
-
const desc = !table.sensitive && !c.sensitive && c.description
|
|
270
|
+
const desc = !table.sensitive && !c.sensitive && allow(c.description)
|
|
210
271
|
? ` — ${c.description}`
|
|
211
272
|
: "";
|
|
212
273
|
lines.push(`- ${c.name} ${c.type} (${flags.join(" ")})${tag}${desc}`);
|
|
213
274
|
}
|
|
214
275
|
return {
|
|
215
|
-
id:
|
|
276
|
+
id: chunkId(schemaId, table.id),
|
|
216
277
|
type: "table",
|
|
217
278
|
text: lines.join("\n").trim(),
|
|
218
279
|
schemaId,
|
|
219
280
|
refs: [table.id, ...referencedColumns],
|
|
220
|
-
sensitive: table.sensitive,
|
|
281
|
+
sensitive: table.sensitive || filter.included,
|
|
221
282
|
};
|
|
222
283
|
}
|
|
223
|
-
|
|
224
|
-
|
|
284
|
+
/**
|
|
285
|
+
* @param includeDescribable emit description / aliases / enum values / note.
|
|
286
|
+
* @param describableSensitive the emitted describable layer references
|
|
287
|
+
* sensitive content (opt-in mode only); sets `chunk.sensitive`.
|
|
288
|
+
*/
|
|
289
|
+
function buildColumnChunk(table, col, schemaId, columnNote, includeDescribable, describableSensitive) {
|
|
290
|
+
const columnSensitive = col.sensitive || table.sensitive;
|
|
225
291
|
const flags = [];
|
|
226
292
|
if (col.primaryKey)
|
|
227
293
|
flags.push("PK");
|
|
228
294
|
flags.push(col.nullable ? "NULL" : "NOT NULL");
|
|
229
295
|
const lines = [];
|
|
230
296
|
const qualified = `${table.schema}.${table.name}.${col.name}`;
|
|
231
|
-
lines.push(`Column: ${qualified} ${col.type} (${flags.join(" ")})${
|
|
297
|
+
lines.push(`Column: ${qualified} ${col.type} (${flags.join(" ")})${columnSensitive ? " (sensitive)" : ""}`);
|
|
232
298
|
lines.push(`Id: ${col.id}`);
|
|
233
|
-
if (
|
|
299
|
+
if (includeDescribable) {
|
|
234
300
|
if (col.description)
|
|
235
301
|
lines.push(col.description);
|
|
236
302
|
if (col.aliases?.length)
|
|
@@ -241,20 +307,30 @@ function buildColumnChunk(table, col, schemaId, columnNote, includeSensitive) {
|
|
|
241
307
|
lines.push(`Note: ${columnNote}`);
|
|
242
308
|
}
|
|
243
309
|
return {
|
|
244
|
-
id:
|
|
310
|
+
id: chunkId(schemaId, col.id),
|
|
245
311
|
type: "column",
|
|
246
312
|
text: lines.join("\n").trim(),
|
|
247
313
|
schemaId,
|
|
248
314
|
refs: [table.id, col.id],
|
|
249
|
-
sensitive,
|
|
315
|
+
sensitive: columnSensitive || (includeDescribable && describableSensitive),
|
|
250
316
|
};
|
|
251
317
|
}
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
318
|
+
function columnDescribableTexts(col, columnNote) {
|
|
319
|
+
const out = [];
|
|
320
|
+
if (col.description)
|
|
321
|
+
out.push(col.description);
|
|
322
|
+
if (col.aliases?.length)
|
|
323
|
+
out.push(...col.aliases);
|
|
324
|
+
if (col.enum?.length)
|
|
325
|
+
out.push(...col.enum.map(String));
|
|
326
|
+
if (columnNote)
|
|
327
|
+
out.push(columnNote);
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function buildCqlChunk(table, aliases, body, suffix, schemaId, sensitive) {
|
|
331
|
+
const aliasNote = aliases.length ? ` (also: ${aliases.join(", ")})` : "";
|
|
256
332
|
const text = `# ${table.schema}.${table.name}${aliasNote} — common query language\n${body.trim()}`;
|
|
257
|
-
const id =
|
|
333
|
+
const id = chunkId(schemaId, `${table.id}#cql${suffix}`);
|
|
258
334
|
return {
|
|
259
335
|
id,
|
|
260
336
|
type: "cql",
|
|
@@ -264,11 +340,11 @@ function buildCqlChunk(table, body, suffix, schemaId, sensitive) {
|
|
|
264
340
|
sensitive,
|
|
265
341
|
};
|
|
266
342
|
}
|
|
267
|
-
function buildQuestionChunk(table, question, index, schemaId, sensitive) {
|
|
268
|
-
const entity =
|
|
343
|
+
function buildQuestionChunk(table, primaryEntity, question, index, schemaId, sensitive) {
|
|
344
|
+
const entity = primaryEntity ? ` [${primaryEntity}]` : "";
|
|
269
345
|
const text = `# ${table.schema}.${table.name}${entity} — example question\n${question.trim()}`;
|
|
270
346
|
return {
|
|
271
|
-
id:
|
|
347
|
+
id: chunkId(schemaId, `${table.id}#q:${index}`),
|
|
272
348
|
type: "question",
|
|
273
349
|
text,
|
|
274
350
|
schemaId,
|
|
@@ -279,7 +355,7 @@ function buildQuestionChunk(table, question, index, schemaId, sensitive) {
|
|
|
279
355
|
function buildBusinessContextChunk(table, body, suffix, schemaId, sensitive) {
|
|
280
356
|
const text = `# ${table.schema}.${table.name} — business context\n${body.trim()}`;
|
|
281
357
|
return {
|
|
282
|
-
id:
|
|
358
|
+
id: chunkId(schemaId, `${table.id}#biz${suffix}`),
|
|
283
359
|
type: "table",
|
|
284
360
|
text,
|
|
285
361
|
schemaId,
|
|
@@ -295,9 +371,13 @@ function buildRelationshipChunk(table, rel, schema, schemaId) {
|
|
|
295
371
|
const fromName = fromTable ? `${fromTable.schema}.${fromTable.name}` : fromCol.tableId;
|
|
296
372
|
const toName = toTable ? `${toTable.schema}.${toTable.name}` : toCol.tableId;
|
|
297
373
|
const text = `Relationship: ${fromName}.${fromCol.column} references ${toName}.${toCol.column}`;
|
|
298
|
-
|
|
374
|
+
// Sensitive when either side's table *or column* is sensitive.
|
|
375
|
+
const sensitive = (fromTable?.sensitive ?? false) ||
|
|
376
|
+
(toTable?.sensitive ?? false) ||
|
|
377
|
+
isSensitiveId(rel.from, schema) ||
|
|
378
|
+
isSensitiveId(rel.to, schema);
|
|
299
379
|
return {
|
|
300
|
-
id:
|
|
380
|
+
id: chunkId(schemaId, `${rel.from}->${rel.to}`),
|
|
301
381
|
type: "relationship",
|
|
302
382
|
text,
|
|
303
383
|
schemaId,
|
|
@@ -305,12 +385,15 @@ function buildRelationshipChunk(table, rel, schema, schemaId) {
|
|
|
305
385
|
sensitive,
|
|
306
386
|
};
|
|
307
387
|
}
|
|
308
|
-
function buildConceptChunk(concept, schemaId, schema,
|
|
388
|
+
function buildConceptChunk(concept, schemaId, schema, sensitiveMentions) {
|
|
309
389
|
const links = (concept.links ?? []).filter((id) => !isUntrackedId(id, schema));
|
|
310
390
|
const linkedSensitive = links.some((id) => isSensitiveId(id, schema));
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
391
|
+
const textMentionsSensitive = [
|
|
392
|
+
concept.label,
|
|
393
|
+
...(concept.synonyms ?? []),
|
|
394
|
+
concept.description ?? "",
|
|
395
|
+
].some((text) => sensitiveMentions.mentionsAny(text));
|
|
396
|
+
const sensitive = linkedSensitive || textMentionsSensitive;
|
|
314
397
|
const lines = [];
|
|
315
398
|
lines.push(`# Concept: ${concept.label}`);
|
|
316
399
|
lines.push(`Id: ${concept.id}`);
|
|
@@ -318,56 +401,65 @@ function buildConceptChunk(concept, schemaId, schema, allSensitiveColumnNames, i
|
|
|
318
401
|
lines.push(`Synonyms: ${concept.synonyms.join(", ")}`);
|
|
319
402
|
if (links.length)
|
|
320
403
|
lines.push(`Links: ${links.join(", ")}`);
|
|
321
|
-
|
|
322
|
-
if (concept.description && (!descriptionMentionsSensitive || includeSensitive)) {
|
|
404
|
+
if (concept.description)
|
|
323
405
|
lines.push(concept.description);
|
|
324
|
-
}
|
|
325
406
|
return {
|
|
326
407
|
chunk: {
|
|
327
|
-
id:
|
|
408
|
+
id: chunkId(schemaId, concept.id),
|
|
328
409
|
type: "concept",
|
|
329
410
|
text: lines.join("\n").trim(),
|
|
330
411
|
schemaId,
|
|
331
412
|
refs: [concept.id, ...links],
|
|
332
|
-
sensitive
|
|
413
|
+
sensitive,
|
|
333
414
|
},
|
|
334
|
-
sensitive
|
|
335
|
-
excluded: excludeForSensitive,
|
|
415
|
+
sensitive,
|
|
336
416
|
};
|
|
337
417
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
for (const [sectionName, sectionBody] of Object.entries(sections)) {
|
|
418
|
+
/** Tenant-policy sections to chunk: one per non-empty H2, else the whole body. */
|
|
419
|
+
function tenantPolicySections(tenantPolicy) {
|
|
420
|
+
const out = [];
|
|
421
|
+
for (const [sectionName, sectionBody] of Object.entries(tenantPolicy.sections)) {
|
|
342
422
|
if (!sectionBody?.trim())
|
|
343
423
|
continue;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
text: `# Tenant policy — ${sectionName}\n${part.text}`,
|
|
350
|
-
schemaId,
|
|
351
|
-
refs: [],
|
|
352
|
-
sensitive: false,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
424
|
+
out.push({
|
|
425
|
+
slug: sectionName.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
|
426
|
+
heading: `# Tenant policy — ${sectionName}`,
|
|
427
|
+
body: sectionBody.trim(),
|
|
428
|
+
});
|
|
355
429
|
}
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
chunks.push({
|
|
359
|
-
id: `chunk:tenant-policy#body${part.suffix}`,
|
|
360
|
-
type: "tenant-policy",
|
|
361
|
-
text: `# Tenant policy\n${part.text}`,
|
|
362
|
-
schemaId,
|
|
363
|
-
refs: [],
|
|
364
|
-
sensitive: false,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
430
|
+
if (out.length === 0 && tenantPolicy.body.trim()) {
|
|
431
|
+
out.push({ slug: "body", heading: "# Tenant policy", body: tenantPolicy.body.trim() });
|
|
367
432
|
}
|
|
368
|
-
return
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Per-chunk gate for describable text (descriptions, aliases, primary
|
|
437
|
+
* entity) that the chunker drops piece by piece instead of excluding the
|
|
438
|
+
* whole chunk.
|
|
439
|
+
*/
|
|
440
|
+
function describableFilter(sensitiveMentions, includeSensitive) {
|
|
441
|
+
let dropped = false;
|
|
442
|
+
let included = false;
|
|
443
|
+
return {
|
|
444
|
+
allow(text) {
|
|
445
|
+
if (!text)
|
|
446
|
+
return false;
|
|
447
|
+
if (!sensitiveMentions.mentionsAny(text))
|
|
448
|
+
return true;
|
|
449
|
+
if (includeSensitive)
|
|
450
|
+
included = true;
|
|
451
|
+
else
|
|
452
|
+
dropped = true;
|
|
453
|
+
return includeSensitive;
|
|
454
|
+
},
|
|
455
|
+
get dropped() {
|
|
456
|
+
return dropped;
|
|
457
|
+
},
|
|
458
|
+
get included() {
|
|
459
|
+
return included;
|
|
460
|
+
},
|
|
461
|
+
};
|
|
369
462
|
}
|
|
370
|
-
// ---------- helpers ----------
|
|
371
463
|
function readColumnNote(md, columnName) {
|
|
372
464
|
const body = md?.sections["Column notes"];
|
|
373
465
|
if (!body)
|
|
@@ -400,20 +492,6 @@ function extractExampleQuestions(md) {
|
|
|
400
492
|
}
|
|
401
493
|
return out;
|
|
402
494
|
}
|
|
403
|
-
function mentionsAnyName(text, names) {
|
|
404
|
-
if (!text || names.length === 0)
|
|
405
|
-
return false;
|
|
406
|
-
for (const name of names) {
|
|
407
|
-
// Word-boundary match (also matches when wrapped in backticks).
|
|
408
|
-
const pattern = new RegExp(`(^|[^a-zA-Z0-9_])${escapeRegex(name)}([^a-zA-Z0-9_]|$)`);
|
|
409
|
-
if (pattern.test(text))
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
function escapeRegex(s) {
|
|
415
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
416
|
-
}
|
|
417
495
|
/** Split a long body on paragraph boundaries; suffix is `""` for single-chunk, `#bc:N` (1-indexed) otherwise. */
|
|
418
496
|
function splitLong(body, maxChars) {
|
|
419
497
|
const trimmed = body.trim();
|
|
@@ -451,16 +529,6 @@ function parseColumnId(columnId) {
|
|
|
451
529
|
column: columnId.slice(hashIdx + 1),
|
|
452
530
|
};
|
|
453
531
|
}
|
|
454
|
-
function collectSensitiveColumnNames(schema) {
|
|
455
|
-
const names = new Set();
|
|
456
|
-
for (const t of schema.tables) {
|
|
457
|
-
for (const c of t.columns) {
|
|
458
|
-
if (c.sensitive || t.sensitive)
|
|
459
|
-
names.add(c.name);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return Array.from(names);
|
|
463
|
-
}
|
|
464
532
|
function isSensitiveId(id, schema) {
|
|
465
533
|
for (const t of schema.tables) {
|
|
466
534
|
if (t.id === id)
|