@askdb/rag 0.2.0-beta.3 → 0.2.0-beta.30
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 +57 -15
- package/dist/chunker/chunker.d.ts +5 -1
- package/dist/chunker/chunker.d.ts.map +1 -1
- package/dist/chunker/chunker.js +225 -103
- 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 +2 -1
- package/dist/chunker/sources.d.ts.map +1 -1
- package/dist/chunker/sources.js +17 -3
- package/dist/chunker/sources.js.map +1 -1
- package/dist/embedders/ai-sdk.d.ts +22 -1
- package/dist/embedders/ai-sdk.d.ts.map +1 -1
- package/dist/embedders/ai-sdk.js +10 -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 +4 -0
- package/dist/embedders/openai.d.ts.map +1 -1
- package/dist/embedders/openai.js +14 -4
- package/dist/embedders/openai.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -4
- 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 +38 -3
- package/dist/stores/pgvector.d.ts.map +1 -1
- package/dist/stores/pgvector.js +129 -16
- package/dist/stores/pgvector.js.map +1 -1
- package/dist/types.d.ts +70 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -23
- package/bin/askdb-rag.js +0 -2
- 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 -332
- package/dist/cli.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,21 +7,25 @@ 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
|
|
|
21
21
|
```ts
|
|
22
22
|
import { ask, loadSchema } from "@askdb/core";
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
buildSchemaIndex,
|
|
25
|
+
loadChunkerSourcesFromDir,
|
|
26
|
+
createMemoryStore,
|
|
27
|
+
createAiSdkEmbedder,
|
|
28
|
+
} from "@askdb/rag";
|
|
25
29
|
|
|
26
30
|
const schemaDir = "./fixtures/schemas/orders-users.schema";
|
|
27
31
|
const schema = loadSchema(schemaDir);
|
|
@@ -48,24 +52,62 @@ const { sql } = await ask({
|
|
|
48
52
|
|
|
49
53
|
## CLI
|
|
50
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
|
+
|
|
51
57
|
```bash
|
|
52
|
-
askdb
|
|
53
|
-
askdb
|
|
58
|
+
npx askdb rag index fixtures/schemas/orders-users.schema --store file
|
|
59
|
+
npx askdb rag query fixtures/schemas/orders-users.schema \
|
|
54
60
|
--question "How much revenue did we make last month?"
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
See the [CLI reference](https://askdb.tools/reference/cli/#askdb-rag) for every flag. This package no longer ships an `askdb-rag` binary.
|
|
58
64
|
|
|
59
65
|
## Public Surface
|
|
60
66
|
|
|
67
|
+
All exports are available from the root `@askdb/rag` import. Sub-path imports are also supported and point to the same modules — use whichever style you prefer.
|
|
68
|
+
|
|
69
|
+
### Core
|
|
70
|
+
|
|
61
71
|
- `chunkSchema`, `chunkSchemaDir`, `chunkSchemaBundle` — deterministic Schema v2 chunking.
|
|
62
72
|
- `buildSchemaIndex` — chunk, embed, upsert, write `schema.lock.json`, and return a retriever.
|
|
63
73
|
- `createRetriever` — bind an existing store and embedder without indexing.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
|
|
75
|
+
### Stores
|
|
76
|
+
|
|
77
|
+
| Root import | Sub-path import | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `createMemoryStore` | `@askdb/rag/stores/memory` | In-memory cosine store. Zero deps. |
|
|
80
|
+
| `createFileStore` | `@askdb/rag/stores/file` | Binary embedding file + JSON metadata. |
|
|
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`. |
|
|
82
|
+
|
|
83
|
+
### Embedders
|
|
84
|
+
|
|
85
|
+
| Root import | Sub-path import | Description |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `createAiSdkEmbedder` | `@askdb/rag/embedders/ai-sdk` | Generic AI SDK `EmbeddingModel` adapter. Requires `ai`. |
|
|
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. |
|
|
91
|
+
|
|
92
|
+
### Import examples
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Everything from the root — simplest DX
|
|
96
|
+
import {
|
|
97
|
+
buildSchemaIndex,
|
|
98
|
+
createMemoryStore,
|
|
99
|
+
createAiSdkEmbedder,
|
|
100
|
+
createPgvectorStore,
|
|
101
|
+
} from "@askdb/rag";
|
|
102
|
+
|
|
103
|
+
// Sub-path imports — same modules, explicit namespacing
|
|
104
|
+
import { createMemoryStore } from "@askdb/rag/stores/memory";
|
|
105
|
+
import { createFileStore } from "@askdb/rag/stores/file";
|
|
106
|
+
import { createPgvectorStore } from "@askdb/rag/stores/pgvector";
|
|
107
|
+
import { createAiSdkEmbedder } from "@askdb/rag/embedders/ai-sdk";
|
|
108
|
+
// Recommended: bring your own embedding model (e.g. @ai-sdk/openai, or @askdb/ai's registry) + createAiSdkEmbedder
|
|
109
|
+
// import { createOpenAiEmbedder } from "@askdb/rag/embedders/openai"; // deprecated, removed in 1.0
|
|
110
|
+
```
|
|
69
111
|
|
|
70
112
|
## Sensitive Fields
|
|
71
113
|
|
|
@@ -73,4 +115,4 @@ Sensitive describable-layer content and sensitive identifiers are excluded from
|
|
|
73
115
|
|
|
74
116
|
## License
|
|
75
117
|
|
|
76
|
-
Apache-2.0. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
118
|
+
Apache-2.0 © [Yahya Gilany](https://yahyagilany.io). See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
@@ -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
|
*
|
|
@@ -24,24 +26,40 @@ export function chunkSchema(sources, options = {}) {
|
|
|
24
26
|
question: 0,
|
|
25
27
|
concept: 0,
|
|
26
28
|
relationship: 0,
|
|
29
|
+
"tenant-policy": 0,
|
|
27
30
|
},
|
|
28
31
|
sensitiveExcluded: 0,
|
|
29
32
|
sensitiveIncluded: 0,
|
|
30
33
|
};
|
|
31
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));
|
|
32
39
|
for (const table of schema.tables) {
|
|
40
|
+
if (table.tracked === false) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
33
43
|
const md = tableMarkdowns[table.id];
|
|
34
|
-
const sensitiveColumnNames = table.columns
|
|
35
|
-
.filter((c) => c.sensitive)
|
|
36
|
-
.map((c) => c.name);
|
|
37
44
|
if (table.sensitive && !includeSensitive) {
|
|
38
45
|
stats.sensitiveExcluded++;
|
|
39
46
|
}
|
|
40
47
|
else {
|
|
41
|
-
|
|
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);
|
|
42
58
|
}
|
|
43
59
|
// Column chunks — one per column. Identifier + type always; describable
|
|
44
|
-
// 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.
|
|
45
63
|
for (const col of table.columns) {
|
|
46
64
|
const colNote = readColumnNote(md, col.name);
|
|
47
65
|
const colSensitive = col.sensitive || table.sensitive;
|
|
@@ -49,27 +67,46 @@ export function chunkSchema(sources, options = {}) {
|
|
|
49
67
|
stats.sensitiveExcluded++;
|
|
50
68
|
continue;
|
|
51
69
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
76
|
}
|
|
57
|
-
|
|
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;
|
|
83
|
+
}
|
|
84
|
+
if (describableMentionsSensitive)
|
|
85
|
+
stats.sensitiveIncluded++;
|
|
86
|
+
chunks.push(buildColumnChunk(table, col, schema.schemaId, colNote, true, describableMentionsSensitive));
|
|
58
87
|
}
|
|
59
88
|
// CQL chunk — excluded entirely if the body mentions a sensitive column by name.
|
|
60
89
|
if (table.commonQueryLanguage) {
|
|
61
|
-
const mentionsSensitive =
|
|
90
|
+
const mentionsSensitive = sensitiveMentions.mentionsAny(table.commonQueryLanguage);
|
|
62
91
|
const tableLevelSensitive = table.sensitive;
|
|
63
92
|
const skip = (tableLevelSensitive || mentionsSensitive) && !includeSensitive;
|
|
93
|
+
// Counted per chunk: a long body splits into several.
|
|
94
|
+
const parts = splitLong(table.commonQueryLanguage, maxChars);
|
|
64
95
|
if (skip) {
|
|
65
|
-
stats.sensitiveExcluded
|
|
96
|
+
stats.sensitiveExcluded += parts.length;
|
|
66
97
|
}
|
|
67
98
|
else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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));
|
|
73
110
|
}
|
|
74
111
|
}
|
|
75
112
|
}
|
|
@@ -77,8 +114,8 @@ export function chunkSchema(sources, options = {}) {
|
|
|
77
114
|
if (md) {
|
|
78
115
|
const questions = extractExampleQuestions(md);
|
|
79
116
|
const businessContext = md.sections["Business context"]?.trim() ?? "";
|
|
80
|
-
const questionsMentionSensitive = questions.some((q) =>
|
|
81
|
-
const bizMentionsSensitive =
|
|
117
|
+
const questionsMentionSensitive = questions.some((q) => sensitiveMentions.mentionsAny(q));
|
|
118
|
+
const bizMentionsSensitive = sensitiveMentions.mentionsAny(businessContext);
|
|
82
119
|
// Per-table sensitive gate: if table is sensitive OR mentions exist, skip
|
|
83
120
|
// the *whole* describable layer for that table by default. Authoring rule
|
|
84
121
|
// in the contract notes the chunker excludes whole chunks rather than
|
|
@@ -89,25 +126,34 @@ export function chunkSchema(sources, options = {}) {
|
|
|
89
126
|
stats.sensitiveExcluded += questions.length;
|
|
90
127
|
}
|
|
91
128
|
else {
|
|
92
|
-
|
|
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)
|
|
93
139
|
stats.sensitiveIncluded += questions.length;
|
|
94
|
-
}
|
|
95
140
|
questions.forEach((q, i) => {
|
|
96
|
-
chunks.push(buildQuestionChunk(table, q, i + 1, schema.schemaId,
|
|
141
|
+
chunks.push(buildQuestionChunk(table, primaryEntity, q, i + 1, schema.schemaId, questionsSensitive));
|
|
97
142
|
});
|
|
98
143
|
}
|
|
99
144
|
}
|
|
100
145
|
// Business-context chunks are part of the describable layer; same gate.
|
|
101
146
|
if (businessContext) {
|
|
102
147
|
const skipBiz = (table.sensitive || bizMentionsSensitive) && !includeSensitive;
|
|
148
|
+
const parts = splitLong(businessContext, maxChars);
|
|
103
149
|
if (skipBiz) {
|
|
104
|
-
stats.sensitiveExcluded
|
|
150
|
+
stats.sensitiveExcluded += parts.length;
|
|
105
151
|
}
|
|
106
152
|
else {
|
|
107
153
|
if (table.sensitive || bizMentionsSensitive) {
|
|
108
|
-
stats.sensitiveIncluded
|
|
154
|
+
stats.sensitiveIncluded += parts.length;
|
|
109
155
|
}
|
|
110
|
-
for (const part of
|
|
156
|
+
for (const part of parts) {
|
|
111
157
|
chunks.push(buildBusinessContextChunk(table, part.text, part.suffix, schema.schemaId, table.sensitive || bizMentionsSensitive));
|
|
112
158
|
}
|
|
113
159
|
}
|
|
@@ -116,6 +162,9 @@ export function chunkSchema(sources, options = {}) {
|
|
|
116
162
|
// Optional relationship chunks
|
|
117
163
|
if (emitRelationships && table.relationships) {
|
|
118
164
|
for (const rel of table.relationships) {
|
|
165
|
+
if (isUntrackedId(rel.from, schema) || isUntrackedId(rel.to, schema)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
119
168
|
const chunk = buildRelationshipChunk(table, rel, schema, schema.schemaId);
|
|
120
169
|
if (chunk.sensitive && !includeSensitive) {
|
|
121
170
|
stats.sensitiveExcluded++;
|
|
@@ -127,23 +176,44 @@ export function chunkSchema(sources, options = {}) {
|
|
|
127
176
|
}
|
|
128
177
|
}
|
|
129
178
|
}
|
|
130
|
-
// Concept chunks.
|
|
131
|
-
//
|
|
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.
|
|
132
182
|
if (concepts?.frontmatter.concepts) {
|
|
133
|
-
const allSensitiveColumnNames = collectSensitiveColumnNames(schema);
|
|
134
183
|
for (const concept of concepts.frontmatter.concepts) {
|
|
135
|
-
const conceptResult = buildConceptChunk(concept, schema.schemaId, schema,
|
|
136
|
-
if (conceptResult.
|
|
184
|
+
const conceptResult = buildConceptChunk(concept, schema.schemaId, schema, sensitiveMentions);
|
|
185
|
+
if (conceptResult.sensitive && !includeSensitive) {
|
|
137
186
|
stats.sensitiveExcluded++;
|
|
138
|
-
|
|
139
|
-
stats.sensitiveIncluded++;
|
|
140
|
-
chunks.push(conceptResult.chunk);
|
|
141
|
-
}
|
|
187
|
+
continue;
|
|
142
188
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
if (conceptResult.sensitive)
|
|
190
|
+
stats.sensitiveIncluded++;
|
|
191
|
+
chunks.push(conceptResult.chunk);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
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.
|
|
198
|
+
if (sources.tenantPolicy) {
|
|
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
|
+
});
|
|
147
217
|
}
|
|
148
218
|
}
|
|
149
219
|
}
|
|
@@ -158,21 +228,28 @@ export function chunkSchema(sources, options = {}) {
|
|
|
158
228
|
return { chunks, stats };
|
|
159
229
|
}
|
|
160
230
|
// ---------- chunk builders ----------
|
|
161
|
-
function buildTableChunk(table, schemaId, includeSensitive, schema) {
|
|
231
|
+
function buildTableChunk(table, schemaId, includeSensitive, schema, filter) {
|
|
162
232
|
const lines = [];
|
|
163
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;
|
|
164
237
|
lines.push(`# ${qualified}`);
|
|
165
238
|
if (!table.sensitive) {
|
|
166
|
-
if (table.description)
|
|
239
|
+
if (allow(table.description))
|
|
167
240
|
lines.push(table.description);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
241
|
+
const aliases = (table.aliases ?? []).filter((a) => allow(a));
|
|
242
|
+
if (aliases.length)
|
|
243
|
+
lines.push(`Aliases: ${aliases.join(", ")}`);
|
|
244
|
+
if (allow(table.primaryEntity))
|
|
171
245
|
lines.push(`Primary entity: ${table.primaryEntity}`);
|
|
172
246
|
}
|
|
173
247
|
if (table.relationships?.length) {
|
|
174
248
|
lines.push("Relationships:");
|
|
175
249
|
for (const r of table.relationships) {
|
|
250
|
+
if (isUntrackedId(r.from, schema) || isUntrackedId(r.to, schema)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
176
253
|
if (!includeSensitive && (isSensitiveId(r.from, schema) || isSensitiveId(r.to, schema))) {
|
|
177
254
|
continue;
|
|
178
255
|
}
|
|
@@ -190,31 +267,36 @@ function buildTableChunk(table, schemaId, includeSensitive, schema) {
|
|
|
190
267
|
flags.push("PK");
|
|
191
268
|
flags.push(c.nullable ? "NULL" : "NOT NULL");
|
|
192
269
|
const tag = c.sensitive || table.sensitive ? " (sensitive)" : "";
|
|
193
|
-
const desc = !table.sensitive && !c.sensitive && c.description
|
|
270
|
+
const desc = !table.sensitive && !c.sensitive && allow(c.description)
|
|
194
271
|
? ` — ${c.description}`
|
|
195
272
|
: "";
|
|
196
273
|
lines.push(`- ${c.name} ${c.type} (${flags.join(" ")})${tag}${desc}`);
|
|
197
274
|
}
|
|
198
275
|
return {
|
|
199
|
-
id:
|
|
276
|
+
id: chunkId(schemaId, table.id),
|
|
200
277
|
type: "table",
|
|
201
278
|
text: lines.join("\n").trim(),
|
|
202
279
|
schemaId,
|
|
203
280
|
refs: [table.id, ...referencedColumns],
|
|
204
|
-
sensitive: table.sensitive,
|
|
281
|
+
sensitive: table.sensitive || filter.included,
|
|
205
282
|
};
|
|
206
283
|
}
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
291
|
const flags = [];
|
|
210
292
|
if (col.primaryKey)
|
|
211
293
|
flags.push("PK");
|
|
212
294
|
flags.push(col.nullable ? "NULL" : "NOT NULL");
|
|
213
295
|
const lines = [];
|
|
214
296
|
const qualified = `${table.schema}.${table.name}.${col.name}`;
|
|
215
|
-
lines.push(`Column: ${qualified} ${col.type} (${flags.join(" ")})${
|
|
297
|
+
lines.push(`Column: ${qualified} ${col.type} (${flags.join(" ")})${columnSensitive ? " (sensitive)" : ""}`);
|
|
216
298
|
lines.push(`Id: ${col.id}`);
|
|
217
|
-
if (
|
|
299
|
+
if (includeDescribable) {
|
|
218
300
|
if (col.description)
|
|
219
301
|
lines.push(col.description);
|
|
220
302
|
if (col.aliases?.length)
|
|
@@ -225,20 +307,30 @@ function buildColumnChunk(table, col, schemaId, columnNote, includeSensitive) {
|
|
|
225
307
|
lines.push(`Note: ${columnNote}`);
|
|
226
308
|
}
|
|
227
309
|
return {
|
|
228
|
-
id:
|
|
310
|
+
id: chunkId(schemaId, col.id),
|
|
229
311
|
type: "column",
|
|
230
312
|
text: lines.join("\n").trim(),
|
|
231
313
|
schemaId,
|
|
232
314
|
refs: [table.id, col.id],
|
|
233
|
-
sensitive,
|
|
315
|
+
sensitive: columnSensitive || (includeDescribable && describableSensitive),
|
|
234
316
|
};
|
|
235
317
|
}
|
|
236
|
-
function
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
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(", ")})` : "";
|
|
240
332
|
const text = `# ${table.schema}.${table.name}${aliasNote} — common query language\n${body.trim()}`;
|
|
241
|
-
const id =
|
|
333
|
+
const id = chunkId(schemaId, `${table.id}#cql${suffix}`);
|
|
242
334
|
return {
|
|
243
335
|
id,
|
|
244
336
|
type: "cql",
|
|
@@ -248,11 +340,11 @@ function buildCqlChunk(table, body, suffix, schemaId, sensitive) {
|
|
|
248
340
|
sensitive,
|
|
249
341
|
};
|
|
250
342
|
}
|
|
251
|
-
function buildQuestionChunk(table, question, index, schemaId, sensitive) {
|
|
252
|
-
const entity =
|
|
343
|
+
function buildQuestionChunk(table, primaryEntity, question, index, schemaId, sensitive) {
|
|
344
|
+
const entity = primaryEntity ? ` [${primaryEntity}]` : "";
|
|
253
345
|
const text = `# ${table.schema}.${table.name}${entity} — example question\n${question.trim()}`;
|
|
254
346
|
return {
|
|
255
|
-
id:
|
|
347
|
+
id: chunkId(schemaId, `${table.id}#q:${index}`),
|
|
256
348
|
type: "question",
|
|
257
349
|
text,
|
|
258
350
|
schemaId,
|
|
@@ -263,7 +355,7 @@ function buildQuestionChunk(table, question, index, schemaId, sensitive) {
|
|
|
263
355
|
function buildBusinessContextChunk(table, body, suffix, schemaId, sensitive) {
|
|
264
356
|
const text = `# ${table.schema}.${table.name} — business context\n${body.trim()}`;
|
|
265
357
|
return {
|
|
266
|
-
id:
|
|
358
|
+
id: chunkId(schemaId, `${table.id}#biz${suffix}`),
|
|
267
359
|
type: "table",
|
|
268
360
|
text,
|
|
269
361
|
schemaId,
|
|
@@ -279,9 +371,13 @@ function buildRelationshipChunk(table, rel, schema, schemaId) {
|
|
|
279
371
|
const fromName = fromTable ? `${fromTable.schema}.${fromTable.name}` : fromCol.tableId;
|
|
280
372
|
const toName = toTable ? `${toTable.schema}.${toTable.name}` : toCol.tableId;
|
|
281
373
|
const text = `Relationship: ${fromName}.${fromCol.column} references ${toName}.${toCol.column}`;
|
|
282
|
-
|
|
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);
|
|
283
379
|
return {
|
|
284
|
-
id:
|
|
380
|
+
id: chunkId(schemaId, `${rel.from}->${rel.to}`),
|
|
285
381
|
type: "relationship",
|
|
286
382
|
text,
|
|
287
383
|
schemaId,
|
|
@@ -289,36 +385,81 @@ function buildRelationshipChunk(table, rel, schema, schemaId) {
|
|
|
289
385
|
sensitive,
|
|
290
386
|
};
|
|
291
387
|
}
|
|
292
|
-
function buildConceptChunk(concept, schemaId, schema,
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
388
|
+
function buildConceptChunk(concept, schemaId, schema, sensitiveMentions) {
|
|
389
|
+
const links = (concept.links ?? []).filter((id) => !isUntrackedId(id, schema));
|
|
390
|
+
const linkedSensitive = links.some((id) => isSensitiveId(id, schema));
|
|
391
|
+
const textMentionsSensitive = [
|
|
392
|
+
concept.label,
|
|
393
|
+
...(concept.synonyms ?? []),
|
|
394
|
+
concept.description ?? "",
|
|
395
|
+
].some((text) => sensitiveMentions.mentionsAny(text));
|
|
396
|
+
const sensitive = linkedSensitive || textMentionsSensitive;
|
|
297
397
|
const lines = [];
|
|
298
398
|
lines.push(`# Concept: ${concept.label}`);
|
|
299
399
|
lines.push(`Id: ${concept.id}`);
|
|
300
400
|
if (concept.synonyms?.length)
|
|
301
401
|
lines.push(`Synonyms: ${concept.synonyms.join(", ")}`);
|
|
302
|
-
if (
|
|
303
|
-
lines.push(`Links: ${
|
|
304
|
-
|
|
305
|
-
if (concept.description && (!descriptionMentionsSensitive || includeSensitive)) {
|
|
402
|
+
if (links.length)
|
|
403
|
+
lines.push(`Links: ${links.join(", ")}`);
|
|
404
|
+
if (concept.description)
|
|
306
405
|
lines.push(concept.description);
|
|
307
|
-
}
|
|
308
406
|
return {
|
|
309
407
|
chunk: {
|
|
310
|
-
id:
|
|
408
|
+
id: chunkId(schemaId, concept.id),
|
|
311
409
|
type: "concept",
|
|
312
410
|
text: lines.join("\n").trim(),
|
|
313
411
|
schemaId,
|
|
314
|
-
refs: [concept.id, ...
|
|
315
|
-
sensitive
|
|
412
|
+
refs: [concept.id, ...links],
|
|
413
|
+
sensitive,
|
|
414
|
+
},
|
|
415
|
+
sensitive,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
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)) {
|
|
422
|
+
if (!sectionBody?.trim())
|
|
423
|
+
continue;
|
|
424
|
+
out.push({
|
|
425
|
+
slug: sectionName.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
|
426
|
+
heading: `# Tenant policy — ${sectionName}`,
|
|
427
|
+
body: sectionBody.trim(),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (out.length === 0 && tenantPolicy.body.trim()) {
|
|
431
|
+
out.push({ slug: "body", heading: "# Tenant policy", body: tenantPolicy.body.trim() });
|
|
432
|
+
}
|
|
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;
|
|
316
460
|
},
|
|
317
|
-
sensitive: linkedSensitive || descriptionMentionsSensitive,
|
|
318
|
-
excluded: excludeForSensitive,
|
|
319
461
|
};
|
|
320
462
|
}
|
|
321
|
-
// ---------- helpers ----------
|
|
322
463
|
function readColumnNote(md, columnName) {
|
|
323
464
|
const body = md?.sections["Column notes"];
|
|
324
465
|
if (!body)
|
|
@@ -351,20 +492,6 @@ function extractExampleQuestions(md) {
|
|
|
351
492
|
}
|
|
352
493
|
return out;
|
|
353
494
|
}
|
|
354
|
-
function mentionsAnyName(text, names) {
|
|
355
|
-
if (!text || names.length === 0)
|
|
356
|
-
return false;
|
|
357
|
-
for (const name of names) {
|
|
358
|
-
// Word-boundary match (also matches when wrapped in backticks).
|
|
359
|
-
const pattern = new RegExp(`(^|[^a-zA-Z0-9_])${escapeRegex(name)}([^a-zA-Z0-9_]|$)`);
|
|
360
|
-
if (pattern.test(text))
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
function escapeRegex(s) {
|
|
366
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
367
|
-
}
|
|
368
495
|
/** Split a long body on paragraph boundaries; suffix is `""` for single-chunk, `#bc:N` (1-indexed) otherwise. */
|
|
369
496
|
function splitLong(body, maxChars) {
|
|
370
497
|
const trimmed = body.trim();
|
|
@@ -402,16 +529,6 @@ function parseColumnId(columnId) {
|
|
|
402
529
|
column: columnId.slice(hashIdx + 1),
|
|
403
530
|
};
|
|
404
531
|
}
|
|
405
|
-
function collectSensitiveColumnNames(schema) {
|
|
406
|
-
const names = new Set();
|
|
407
|
-
for (const t of schema.tables) {
|
|
408
|
-
for (const c of t.columns) {
|
|
409
|
-
if (c.sensitive || t.sensitive)
|
|
410
|
-
names.add(c.name);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return Array.from(names);
|
|
414
|
-
}
|
|
415
532
|
function isSensitiveId(id, schema) {
|
|
416
533
|
for (const t of schema.tables) {
|
|
417
534
|
if (t.id === id)
|
|
@@ -423,4 +540,9 @@ function isSensitiveId(id, schema) {
|
|
|
423
540
|
}
|
|
424
541
|
return false;
|
|
425
542
|
}
|
|
543
|
+
function isUntrackedId(id, schema) {
|
|
544
|
+
const tableId = id.includes("#") ? parseColumnId(id).tableId : id;
|
|
545
|
+
const table = schema.tables.find((t) => t.id === tableId);
|
|
546
|
+
return table?.tracked === false;
|
|
547
|
+
}
|
|
426
548
|
//# sourceMappingURL=chunker.js.map
|