kirograph 0.20.1 → 0.22.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.
Files changed (58) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/commands/data.js +52 -0
  3. package/dist/bin/commands/data.js.map +2 -2
  4. package/dist/bin/commands/docs.js +2 -1
  5. package/dist/bin/commands/docs.js.map +2 -2
  6. package/dist/bin/commands/gain.js +21 -1
  7. package/dist/bin/commands/gain.js.map +2 -2
  8. package/dist/bin/commands/status.js +23 -1
  9. package/dist/bin/commands/status.js.map +2 -2
  10. package/dist/bin/installer/config-prompt.js +30 -1
  11. package/dist/bin/installer/config-prompt.js.map +2 -2
  12. package/dist/bin/installer/index.js +108 -0
  13. package/dist/bin/installer/index.js.map +2 -2
  14. package/dist/bin/kirograph.js +1 -1
  15. package/dist/config.js +19 -2
  16. package/dist/config.js.map +2 -2
  17. package/dist/core/pipeline.js +3 -1
  18. package/dist/core/pipeline.js.map +2 -2
  19. package/dist/data/indexer.js +4 -4
  20. package/dist/data/indexer.js.map +2 -2
  21. package/dist/data/linker.js +9 -5
  22. package/dist/data/linker.js.map +2 -2
  23. package/dist/data/linker.test.js +60 -0
  24. package/dist/data/linker.test.js.map +7 -0
  25. package/dist/data/parsers/__fixtures__/pdf-inspector-stub.js +41 -0
  26. package/dist/data/parsers/__fixtures__/pdf-inspector-stub.js.map +7 -0
  27. package/dist/data/parsers/index.js +3 -1
  28. package/dist/data/parsers/index.js.map +2 -2
  29. package/dist/data/parsers/pdf.js +95 -0
  30. package/dist/data/parsers/pdf.js.map +7 -0
  31. package/dist/data/parsers/pdf.test.js +125 -0
  32. package/dist/data/parsers/pdf.test.js.map +7 -0
  33. package/dist/data/queries.js +39 -2
  34. package/dist/data/queries.js.map +2 -2
  35. package/dist/data/types.js.map +1 -1
  36. package/dist/db/data-schema.sql +2 -1
  37. package/dist/db/database.js +7 -0
  38. package/dist/db/database.js.map +2 -2
  39. package/dist/docs/indexer.js +2 -1
  40. package/dist/docs/indexer.js.map +2 -2
  41. package/dist/docs/queries.js +2 -1
  42. package/dist/docs/queries.js.map +2 -2
  43. package/dist/docs/vectors.js +46 -5
  44. package/dist/docs/vectors.js.map +2 -2
  45. package/dist/index.js +1 -0
  46. package/dist/index.js.map +2 -2
  47. package/dist/mcp/tools.js +1 -1
  48. package/dist/mcp/tools.js.map +2 -2
  49. package/dist/memory/index.js +2 -1
  50. package/dist/memory/index.js.map +2 -2
  51. package/dist/memory/vectors.js +52 -8
  52. package/dist/memory/vectors.js.map +3 -3
  53. package/dist/types.js.map +2 -2
  54. package/dist/vectors/index.js +66 -3
  55. package/dist/vectors/index.js.map +2 -2
  56. package/dist/vectors/turbovec-index.js +189 -0
  57. package/dist/vectors/turbovec-index.js.map +7 -0
  58. package/package.json +7 -1
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var pdf_exports = {};
20
+ __export(pdf_exports, {
21
+ _resetPdfInspectorForTesting: () => _resetPdfInspectorForTesting,
22
+ _setPdfInspectorForTesting: () => _setPdfInspectorForTesting,
23
+ pdfParser: () => pdfParser
24
+ });
25
+ module.exports = __toCommonJS(pdf_exports);
26
+ var import_fs = require("fs");
27
+ let pdfModule = null;
28
+ let pdfChecked = false;
29
+ function tryLoadPdfInspector() {
30
+ if (pdfChecked) return pdfModule !== null;
31
+ pdfChecked = true;
32
+ try {
33
+ pdfModule = require("@firecrawl/pdf-inspector");
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ function _setPdfInspectorForTesting(mod) {
40
+ pdfModule = mod;
41
+ pdfChecked = true;
42
+ }
43
+ function _resetPdfInspectorForTesting() {
44
+ pdfModule = null;
45
+ pdfChecked = false;
46
+ }
47
+ const pdfParser = {
48
+ name: "pdf",
49
+ extensions: [".pdf"],
50
+ isAvailable() {
51
+ return tryLoadPdfInspector();
52
+ },
53
+ async parse(filePath, opts) {
54
+ if (!tryLoadPdfInspector()) {
55
+ return { columns: [], totalRows: 0 };
56
+ }
57
+ const { extractPagesMarkdown, processPdf } = pdfModule;
58
+ const buffer = (0, import_fs.readFileSync)(filePath);
59
+ const result = extractPagesMarkdown(buffer);
60
+ let metadataJson = null;
61
+ try {
62
+ const meta = processPdf(buffer);
63
+ metadataJson = JSON.stringify({
64
+ pdfType: meta.pdfType ?? null,
65
+ confidence: meta.confidence ?? null,
66
+ title: meta.title ?? null,
67
+ hasEncodingIssues: meta.hasEncodingIssues ?? false,
68
+ isComplexLayout: meta.isComplexLayout ?? false
69
+ });
70
+ } catch {
71
+ }
72
+ const columns = ["page", "content", "needs_ocr", "has_tables", "has_columns"];
73
+ const batch = [];
74
+ for (const p of result.pages) {
75
+ batch.push({
76
+ page: String(p.page),
77
+ content: p.markdown ?? "",
78
+ needs_ocr: String(p.needsOcr ?? false),
79
+ has_tables: String(result.pagesWithTables?.includes(p.page) ?? false),
80
+ has_columns: String(result.pagesWithColumns?.includes(p.page) ?? false)
81
+ });
82
+ }
83
+ if (batch.length > 0) {
84
+ opts.onBatch(batch, columns);
85
+ }
86
+ return { columns, totalRows: batch.length, metadataJson };
87
+ }
88
+ };
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ _resetPdfInspectorForTesting,
92
+ _setPdfInspectorForTesting,
93
+ pdfParser
94
+ });
95
+ //# sourceMappingURL=pdf.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/data/parsers/pdf.ts"],
4
+ "sourcesContent": ["/**\n * PDF Parser (Optional Dependency)\n *\n * Handles: .pdf\n * Requires: @firecrawl/pdf-inspector (optional dependency, Rust/NAPI)\n * Gracefully returns isAvailable()=false if not installed or unsupported platform.\n *\n * Each page becomes one row: page, content, needs_ocr, has_tables, has_columns.\n * Text-based PDFs process in under 200ms locally.\n * Scanned/mixed pages get needs_ocr='true' and are included (not skipped).\n */\n\nimport { readFileSync } from 'fs';\nimport type { DataFormatParser, ParsedRow } from '../types';\n\nlet pdfModule: any = null;\nlet pdfChecked = false;\n\nfunction tryLoadPdfInspector(): boolean {\n if (pdfChecked) return pdfModule !== null;\n pdfChecked = true;\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n pdfModule = require('@firecrawl/pdf-inspector');\n return true;\n } catch {\n return false;\n }\n}\n\n/** Testing hook \u2014 allows injecting a mock module without module system tricks. */\nexport function _setPdfInspectorForTesting(mod: any): void {\n pdfModule = mod;\n pdfChecked = true;\n}\n\nexport function _resetPdfInspectorForTesting(): void {\n pdfModule = null;\n pdfChecked = false;\n}\n\nexport const pdfParser: DataFormatParser = {\n name: 'pdf',\n extensions: ['.pdf'],\n\n isAvailable(): boolean {\n return tryLoadPdfInspector();\n },\n\n async parse(filePath, opts) {\n if (!tryLoadPdfInspector()) {\n return { columns: [], totalRows: 0 };\n }\n\n const { extractPagesMarkdown, processPdf } = pdfModule;\n const buffer = readFileSync(filePath);\n\n // Per-page content + OCR flags\n const result = extractPagesMarkdown(buffer);\n\n // Document-level metadata (non-critical)\n let metadataJson: string | null = null;\n try {\n const meta = processPdf(buffer);\n metadataJson = JSON.stringify({\n pdfType: meta.pdfType ?? null,\n confidence: meta.confidence ?? null,\n title: meta.title ?? null,\n hasEncodingIssues: meta.hasEncodingIssues ?? false,\n isComplexLayout: meta.isComplexLayout ?? false,\n });\n } catch {\n // non-critical \u2014 index proceeds without metadata\n }\n\n const columns = ['page', 'content', 'needs_ocr', 'has_tables', 'has_columns'];\n const batch: ParsedRow[] = [];\n\n for (const p of result.pages) {\n batch.push({\n page: String(p.page),\n content: p.markdown ?? '',\n needs_ocr: String(p.needsOcr ?? false),\n has_tables: String(result.pagesWithTables?.includes(p.page) ?? false),\n has_columns: String(result.pagesWithColumns?.includes(p.page) ?? false),\n });\n }\n\n if (batch.length > 0) {\n opts.onBatch(batch, columns);\n }\n\n return { columns, totalRows: batch.length, metadataJson };\n },\n};\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,gBAA6B;AAG7B,IAAI,YAAiB;AACrB,IAAI,aAAa;AAEjB,SAAS,sBAA+B;AACtC,MAAI,WAAY,QAAO,cAAc;AACrC,eAAa;AACb,MAAI;AAEF,gBAAY,QAAQ,0BAA0B;AAC9C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,2BAA2B,KAAgB;AACzD,cAAY;AACZ,eAAa;AACf;AAEO,SAAS,+BAAqC;AACnD,cAAY;AACZ,eAAa;AACf;AAEO,MAAM,YAA8B;AAAA,EACzC,MAAM;AAAA,EACN,YAAY,CAAC,MAAM;AAAA,EAEnB,cAAuB;AACrB,WAAO,oBAAoB;AAAA,EAC7B;AAAA,EAEA,MAAM,MAAM,UAAU,MAAM;AAC1B,QAAI,CAAC,oBAAoB,GAAG;AAC1B,aAAO,EAAE,SAAS,CAAC,GAAG,WAAW,EAAE;AAAA,IACrC;AAEA,UAAM,EAAE,sBAAsB,WAAW,IAAI;AAC7C,UAAM,aAAS,wBAAa,QAAQ;AAGpC,UAAM,SAAS,qBAAqB,MAAM;AAG1C,QAAI,eAA8B;AAClC,QAAI;AACF,YAAM,OAAO,WAAW,MAAM;AAC9B,qBAAe,KAAK,UAAU;AAAA,QAC5B,SAAS,KAAK,WAAW;AAAA,QACzB,YAAY,KAAK,cAAc;AAAA,QAC/B,OAAO,KAAK,SAAS;AAAA,QACrB,mBAAmB,KAAK,qBAAqB;AAAA,QAC7C,iBAAiB,KAAK,mBAAmB;AAAA,MAC3C,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAEA,UAAM,UAAU,CAAC,QAAQ,WAAW,aAAa,cAAc,aAAa;AAC5E,UAAM,QAAqB,CAAC;AAE5B,eAAW,KAAK,OAAO,OAAO;AAC5B,YAAM,KAAK;AAAA,QACT,MAAM,OAAO,EAAE,IAAI;AAAA,QACnB,SAAS,EAAE,YAAY;AAAA,QACvB,WAAW,OAAO,EAAE,YAAY,KAAK;AAAA,QACrC,YAAY,OAAO,OAAO,iBAAiB,SAAS,EAAE,IAAI,KAAK,KAAK;AAAA,QACpE,aAAa,OAAO,OAAO,kBAAkB,SAAS,EAAE,IAAI,KAAK,KAAK;AAAA,MACxE,CAAC;AAAA,IACH;AAEA,QAAI,MAAM,SAAS,GAAG;AACpB,WAAK,QAAQ,OAAO,OAAO;AAAA,IAC7B;AAEA,WAAO,EAAE,SAAS,WAAW,MAAM,QAAQ,aAAa;AAAA,EAC1D;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_pdf = require("./pdf");
4
+ import_vitest.vi.mock("fs", async (importOriginal) => {
5
+ const actual = await importOriginal();
6
+ return { ...actual, readFileSync: import_vitest.vi.fn().mockReturnValue(Buffer.from("fake-pdf")) };
7
+ });
8
+ const mockExtractPages = import_vitest.vi.fn();
9
+ const mockProcessPdf = import_vitest.vi.fn();
10
+ function makeMockResult(pages, opts) {
11
+ return {
12
+ pages,
13
+ pagesWithTables: opts?.pagesWithTables ?? [],
14
+ pagesWithColumns: opts?.pagesWithColumns ?? []
15
+ };
16
+ }
17
+ (0, import_vitest.describe)("pdfParser", () => {
18
+ (0, import_vitest.beforeEach)(() => {
19
+ import_vitest.vi.clearAllMocks();
20
+ (0, import_pdf._setPdfInspectorForTesting)({ extractPagesMarkdown: mockExtractPages, processPdf: mockProcessPdf });
21
+ });
22
+ (0, import_vitest.afterEach)(() => {
23
+ (0, import_pdf._resetPdfInspectorForTesting)();
24
+ });
25
+ (0, import_vitest.it)("isAvailable returns true when module is injected", () => {
26
+ (0, import_vitest.expect)(import_pdf.pdfParser.isAvailable()).toBe(true);
27
+ });
28
+ (0, import_vitest.it)("isAvailable returns false when module is not available", () => {
29
+ (0, import_pdf._resetPdfInspectorForTesting)();
30
+ (0, import_vitest.expect)(import_pdf.pdfParser.isAvailable()).toBe(false);
31
+ });
32
+ (0, import_vitest.it)("parse returns empty result when library is unavailable", async () => {
33
+ (0, import_pdf._resetPdfInspectorForTesting)();
34
+ const batches = [];
35
+ const result = await import_pdf.pdfParser.parse("/fake/doc.pdf", {
36
+ maxRows: 1e3,
37
+ onBatch: (rows, cols) => batches.push({ rows, cols })
38
+ });
39
+ (0, import_vitest.expect)(result.columns).toEqual([]);
40
+ (0, import_vitest.expect)(result.totalRows).toBe(0);
41
+ (0, import_vitest.expect)(batches).toHaveLength(0);
42
+ });
43
+ (0, import_vitest.it)("happy path: text-based PDF produces correct rows and metadata", async () => {
44
+ mockExtractPages.mockReturnValue(makeMockResult([
45
+ { page: 0, markdown: "# Title\n\nSome content here.", needsOcr: false },
46
+ { page: 1, markdown: "Page two content.", needsOcr: false }
47
+ ], { pagesWithTables: [1], pagesWithColumns: [] }));
48
+ mockProcessPdf.mockReturnValue({
49
+ pdfType: "TextBased",
50
+ confidence: 0.97,
51
+ title: "Test Doc",
52
+ hasEncodingIssues: false,
53
+ isComplexLayout: false
54
+ });
55
+ const batches = [];
56
+ const result = await import_pdf.pdfParser.parse("/fake/report.pdf", {
57
+ maxRows: 1e3,
58
+ onBatch: (rows, cols) => batches.push({ rows, cols })
59
+ });
60
+ (0, import_vitest.expect)(result.columns).toEqual(["page", "content", "needs_ocr", "has_tables", "has_columns"]);
61
+ (0, import_vitest.expect)(result.totalRows).toBe(2);
62
+ (0, import_vitest.expect)(batches).toHaveLength(1);
63
+ (0, import_vitest.expect)(batches[0].rows[0]).toMatchObject({ page: "0", needs_ocr: "false", has_tables: "false" });
64
+ (0, import_vitest.expect)(batches[0].rows[1]).toMatchObject({ page: "1", needs_ocr: "false", has_tables: "true" });
65
+ (0, import_vitest.expect)(result.metadataJson).toBeTruthy();
66
+ const meta = JSON.parse(result.metadataJson);
67
+ (0, import_vitest.expect)(meta.pdfType).toBe("TextBased");
68
+ (0, import_vitest.expect)(meta.confidence).toBe(0.97);
69
+ (0, import_vitest.expect)(meta.hasEncodingIssues).toBe(false);
70
+ });
71
+ (0, import_vitest.it)("mixed PDF: text pages indexed normally, scanned pages flagged", async () => {
72
+ mockExtractPages.mockReturnValue(makeMockResult([
73
+ { page: 0, markdown: "Text content", needsOcr: false },
74
+ { page: 1, markdown: "", needsOcr: true },
75
+ { page: 2, markdown: "More text", needsOcr: false },
76
+ { page: 3, markdown: "", needsOcr: true }
77
+ ]));
78
+ mockProcessPdf.mockReturnValue({ pdfType: "Mixed", confidence: 0.85, hasEncodingIssues: false, isComplexLayout: false });
79
+ const batches = [];
80
+ await import_pdf.pdfParser.parse("/fake/mixed.pdf", {
81
+ maxRows: 1e3,
82
+ onBatch: (rows2) => batches.push({ rows: rows2 })
83
+ });
84
+ (0, import_vitest.expect)(batches).toHaveLength(1);
85
+ const rows = batches[0].rows;
86
+ (0, import_vitest.expect)(rows).toHaveLength(4);
87
+ (0, import_vitest.expect)(rows.filter((r) => r.needs_ocr === "true")).toHaveLength(2);
88
+ (0, import_vitest.expect)(rows.filter((r) => r.needs_ocr === "false")).toHaveLength(2);
89
+ });
90
+ (0, import_vitest.it)("hasEncodingIssues is captured in metadataJson", async () => {
91
+ mockExtractPages.mockReturnValue(makeMockResult([{ page: 0, markdown: "garbled", needsOcr: false }]));
92
+ mockProcessPdf.mockReturnValue({ pdfType: "TextBased", confidence: 0.4, hasEncodingIssues: true, isComplexLayout: false });
93
+ const result = await import_pdf.pdfParser.parse("/fake/garbled.pdf", { maxRows: 1e3, onBatch: () => {
94
+ } });
95
+ (0, import_vitest.expect)(result.metadataJson).toBeTruthy();
96
+ const meta = JSON.parse(result.metadataJson);
97
+ (0, import_vitest.expect)(meta.hasEncodingIssues).toBe(true);
98
+ });
99
+ (0, import_vitest.it)("empty PDF returns no rows and no batch", async () => {
100
+ mockExtractPages.mockReturnValue({ pages: [], pagesWithTables: [], pagesWithColumns: [] });
101
+ mockProcessPdf.mockReturnValue({ pdfType: "TextBased", confidence: 1, hasEncodingIssues: false, isComplexLayout: false });
102
+ const batches = [];
103
+ const result = await import_pdf.pdfParser.parse("/fake/empty.pdf", {
104
+ maxRows: 1e3,
105
+ onBatch: (rows, cols) => batches.push({ rows, cols })
106
+ });
107
+ (0, import_vitest.expect)(result.totalRows).toBe(0);
108
+ (0, import_vitest.expect)(batches).toHaveLength(0);
109
+ });
110
+ (0, import_vitest.it)("gracefully handles processPdf failure \u2014 still returns rows", async () => {
111
+ mockExtractPages.mockReturnValue(makeMockResult([{ page: 0, markdown: "content", needsOcr: false }]));
112
+ mockProcessPdf.mockImplementation(() => {
113
+ throw new Error("processPdf failed");
114
+ });
115
+ const batches = [];
116
+ const result = await import_pdf.pdfParser.parse("/fake/broken-meta.pdf", {
117
+ maxRows: 1e3,
118
+ onBatch: (rows, cols) => batches.push({ rows, cols })
119
+ });
120
+ (0, import_vitest.expect)(result.totalRows).toBe(1);
121
+ (0, import_vitest.expect)(batches).toHaveLength(1);
122
+ (0, import_vitest.expect)(result.metadataJson).toBeNull();
123
+ });
124
+ });
125
+ //# sourceMappingURL=pdf.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/data/parsers/pdf.test.ts"],
4
+ "sourcesContent": ["import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { pdfParser, _setPdfInspectorForTesting, _resetPdfInspectorForTesting } from './pdf';\n\n// \u2500\u2500 Mock fs.readFileSync so tests never touch the filesystem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nvi.mock('fs', async (importOriginal) => {\n const actual = await importOriginal<typeof import('fs')>();\n return { ...actual, readFileSync: vi.fn().mockReturnValue(Buffer.from('fake-pdf')) };\n});\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst mockExtractPages = vi.fn();\nconst mockProcessPdf = vi.fn();\n\nfunction makeMockResult(\n pages: Array<{ page: number; markdown: string; needsOcr: boolean }>,\n opts?: { pagesWithTables?: number[]; pagesWithColumns?: number[] },\n) {\n return {\n pages,\n pagesWithTables: opts?.pagesWithTables ?? [],\n pagesWithColumns: opts?.pagesWithColumns ?? [],\n };\n}\n\n// \u2500\u2500 Tests \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndescribe('pdfParser', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n _setPdfInspectorForTesting({ extractPagesMarkdown: mockExtractPages, processPdf: mockProcessPdf });\n });\n\n afterEach(() => {\n _resetPdfInspectorForTesting();\n });\n\n it('isAvailable returns true when module is injected', () => {\n expect(pdfParser.isAvailable()).toBe(true);\n });\n\n it('isAvailable returns false when module is not available', () => {\n _resetPdfInspectorForTesting(); // clears pdfModule\n // Simulate require() failure by resetting the module\n expect(pdfParser.isAvailable()).toBe(false);\n });\n\n it('parse returns empty result when library is unavailable', async () => {\n _resetPdfInspectorForTesting();\n const batches: any[] = [];\n const result = await pdfParser.parse('/fake/doc.pdf', {\n maxRows: 1000,\n onBatch: (rows, cols) => batches.push({ rows, cols }),\n });\n expect(result.columns).toEqual([]);\n expect(result.totalRows).toBe(0);\n expect(batches).toHaveLength(0);\n });\n\n it('happy path: text-based PDF produces correct rows and metadata', async () => {\n mockExtractPages.mockReturnValue(makeMockResult([\n { page: 0, markdown: '# Title\\n\\nSome content here.', needsOcr: false },\n { page: 1, markdown: 'Page two content.', needsOcr: false },\n ], { pagesWithTables: [1], pagesWithColumns: [] }));\n mockProcessPdf.mockReturnValue({\n pdfType: 'TextBased', confidence: 0.97, title: 'Test Doc',\n hasEncodingIssues: false, isComplexLayout: false,\n });\n\n const batches: Array<{ rows: any[]; cols: string[] }> = [];\n const result = await pdfParser.parse('/fake/report.pdf', {\n maxRows: 1000,\n onBatch: (rows, cols) => batches.push({ rows, cols }),\n });\n\n expect(result.columns).toEqual(['page', 'content', 'needs_ocr', 'has_tables', 'has_columns']);\n expect(result.totalRows).toBe(2);\n expect(batches).toHaveLength(1);\n expect(batches[0]!.rows[0]).toMatchObject({ page: '0', needs_ocr: 'false', has_tables: 'false' });\n expect(batches[0]!.rows[1]).toMatchObject({ page: '1', needs_ocr: 'false', has_tables: 'true' });\n\n expect(result.metadataJson).toBeTruthy();\n const meta = JSON.parse(result.metadataJson!);\n expect(meta.pdfType).toBe('TextBased');\n expect(meta.confidence).toBe(0.97);\n expect(meta.hasEncodingIssues).toBe(false);\n });\n\n it('mixed PDF: text pages indexed normally, scanned pages flagged', async () => {\n mockExtractPages.mockReturnValue(makeMockResult([\n { page: 0, markdown: 'Text content', needsOcr: false },\n { page: 1, markdown: '', needsOcr: true },\n { page: 2, markdown: 'More text', needsOcr: false },\n { page: 3, markdown: '', needsOcr: true },\n ]));\n mockProcessPdf.mockReturnValue({ pdfType: 'Mixed', confidence: 0.85, hasEncodingIssues: false, isComplexLayout: false });\n\n const batches: Array<{ rows: any[] }> = [];\n await pdfParser.parse('/fake/mixed.pdf', {\n maxRows: 1000,\n onBatch: (rows) => batches.push({ rows }),\n });\n\n expect(batches).toHaveLength(1);\n const rows = batches[0]!.rows;\n expect(rows).toHaveLength(4);\n expect(rows.filter((r: any) => r.needs_ocr === 'true')).toHaveLength(2);\n expect(rows.filter((r: any) => r.needs_ocr === 'false')).toHaveLength(2);\n });\n\n it('hasEncodingIssues is captured in metadataJson', async () => {\n mockExtractPages.mockReturnValue(makeMockResult([{ page: 0, markdown: 'garbled', needsOcr: false }]));\n mockProcessPdf.mockReturnValue({ pdfType: 'TextBased', confidence: 0.4, hasEncodingIssues: true, isComplexLayout: false });\n\n const result = await pdfParser.parse('/fake/garbled.pdf', { maxRows: 1000, onBatch: () => {} });\n\n expect(result.metadataJson).toBeTruthy();\n const meta = JSON.parse(result.metadataJson!);\n expect(meta.hasEncodingIssues).toBe(true);\n });\n\n it('empty PDF returns no rows and no batch', async () => {\n mockExtractPages.mockReturnValue({ pages: [], pagesWithTables: [], pagesWithColumns: [] });\n mockProcessPdf.mockReturnValue({ pdfType: 'TextBased', confidence: 1, hasEncodingIssues: false, isComplexLayout: false });\n\n const batches: any[] = [];\n const result = await pdfParser.parse('/fake/empty.pdf', {\n maxRows: 1000,\n onBatch: (rows, cols) => batches.push({ rows, cols }),\n });\n\n expect(result.totalRows).toBe(0);\n expect(batches).toHaveLength(0);\n });\n\n it('gracefully handles processPdf failure \u2014 still returns rows', async () => {\n mockExtractPages.mockReturnValue(makeMockResult([{ page: 0, markdown: 'content', needsOcr: false }]));\n mockProcessPdf.mockImplementation(() => { throw new Error('processPdf failed'); });\n\n const batches: any[] = [];\n const result = await pdfParser.parse('/fake/broken-meta.pdf', {\n maxRows: 1000,\n onBatch: (rows, cols) => batches.push({ rows, cols }),\n });\n\n expect(result.totalRows).toBe(1);\n expect(batches).toHaveLength(1);\n expect(result.metadataJson).toBeNull();\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["rows"]
7
+ }
@@ -38,7 +38,8 @@ class DataQueries {
38
38
  fileSize: r.file_size,
39
39
  contentHash: r.content_hash,
40
40
  summary: r.summary,
41
- indexedAt: r.indexed_at
41
+ indexedAt: r.indexed_at,
42
+ metadataJson: r.metadata_json ?? null
42
43
  }));
43
44
  }
44
45
  // ── Describe ────────────────────────────────────────────────────────────────
@@ -59,7 +60,8 @@ class DataQueries {
59
60
  fileSize: ds.file_size,
60
61
  contentHash: ds.content_hash,
61
62
  summary: ds.summary,
62
- indexedAt: ds.indexed_at
63
+ indexedAt: ds.indexed_at,
64
+ metadataJson: ds.metadata_json ?? null
63
65
  },
64
66
  columns: cols.map((c) => ({
65
67
  id: c.id,
@@ -373,6 +375,41 @@ class DataQueries {
373
375
  );
374
376
  if (cols.length === 0) return [];
375
377
  const results = [];
378
+ if (ds.format === "pdf") {
379
+ const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
380
+ try {
381
+ const ocrRow = this.db.get(`SELECT COUNT(*) as cnt FROM "${tableName}" WHERE needs_ocr = 'true'`);
382
+ const ocrCount = ocrRow?.cnt ?? 0;
383
+ if (ocrCount > 0) {
384
+ const totalPages = ds.row_count ?? 0;
385
+ results.push({
386
+ column: "needs_ocr",
387
+ riskScore: Math.min(ocrCount / Math.max(totalPages, 1), 1),
388
+ nullRisk: 0,
389
+ cardinalityRisk: 0,
390
+ typeRisk: 0,
391
+ issues: [`${ocrCount} of ${totalPages} pages need OCR (scanned/image-only content)`]
392
+ });
393
+ }
394
+ } catch {
395
+ }
396
+ if (ds.metadata_json) {
397
+ try {
398
+ const meta = JSON.parse(ds.metadata_json);
399
+ if (meta.hasEncodingIssues) {
400
+ results.push({
401
+ column: "content",
402
+ riskScore: 0.3,
403
+ nullRisk: 0,
404
+ cardinalityRisk: 0,
405
+ typeRisk: 0.3,
406
+ issues: ["PDF has encoding issues \u2014 text extraction may be garbled; consider OCR pre-processing"]
407
+ });
408
+ }
409
+ } catch {
410
+ }
411
+ }
412
+ }
376
413
  for (const col of cols) {
377
414
  const issues = [];
378
415
  let nullRisk = 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/data/queries.ts"],
4
- "sourcesContent": ["/**\n * Data Queries\n *\n * Read-side helpers for MCP tools and CLI commands.\n * Handles: list, describe, query (with filters), aggregate, search.\n */\n\nimport type { DataSet, DataColumn, QueryFilter, AggregateMetric, CorrelationPair, JoinType, ColumnQuality } from './types';\nimport { buildWhereClause } from './filters';\n\nexport interface ValidationRule {\n column: string;\n rules: string[];\n}\n\nexport interface SampleHint {\n column: string;\n hint: string;\n}\n\nexport class DataQueries {\n private readonly db: any;\n\n constructor(db: any) {\n this.db = db;\n }\n\n // \u2500\u2500 List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n listDatasets(): DataSet[] {\n const rows = this.db.all('SELECT * FROM data_datasets ORDER BY file_path') as any[];\n return rows.map(r => ({\n id: r.id,\n filePath: r.file_path,\n format: r.format,\n rowCount: r.row_count,\n columnCount: r.column_count,\n fileSize: r.file_size,\n contentHash: r.content_hash,\n summary: r.summary,\n indexedAt: r.indexed_at,\n }));\n }\n\n // \u2500\u2500 Describe \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n describeDataset(datasetId: string): { dataset: DataSet; columns: DataColumn[] } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const cols = this.db.all(\n 'SELECT * FROM data_columns WHERE dataset_id = ? ORDER BY position',\n [datasetId],\n ) as any[];\n\n return {\n dataset: {\n id: ds.id, filePath: ds.file_path, format: ds.format,\n rowCount: ds.row_count, columnCount: ds.column_count,\n fileSize: ds.file_size, contentHash: ds.content_hash,\n summary: ds.summary, indexedAt: ds.indexed_at,\n },\n columns: cols.map(c => ({\n id: c.id, datasetId: c.dataset_id, name: c.name, position: c.position,\n inferredType: c.inferred_type, nullable: !!c.nullable,\n nullCount: c.null_count, nullPct: c.null_pct, cardinality: c.cardinality,\n minValue: c.min_value, maxValue: c.max_value, meanValue: c.mean_value,\n sampleValues: c.sample_values ? JSON.parse(c.sample_values) : [],\n summary: c.summary, updatedAt: c.updated_at,\n })),\n };\n }\n\n describeColumn(datasetId: string, columnName: string): DataColumn | null {\n const col = this.db.get(\n 'SELECT * FROM data_columns WHERE dataset_id = ? AND name = ?',\n [datasetId, columnName],\n );\n if (!col) return null;\n return {\n id: col.id, datasetId: col.dataset_id, name: col.name, position: col.position,\n inferredType: col.inferred_type, nullable: !!col.nullable,\n nullCount: col.null_count, nullPct: col.null_pct, cardinality: col.cardinality,\n minValue: col.min_value, maxValue: col.max_value, meanValue: col.mean_value,\n sampleValues: col.sample_values ? JSON.parse(col.sample_values) : [],\n summary: col.summary, updatedAt: col.updated_at,\n };\n }\n\n // \u2500\u2500 Query (filtered rows) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n queryRows(\n datasetId: string,\n opts?: { filters?: QueryFilter[]; columns?: string[]; limit?: number; offset?: number },\n ): { rows: any[]; totalMatching: number } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get valid columns\n const validCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [datasetId]) as any[]).map(r => r.name),\n );\n\n // Build WHERE clause\n const { where, params } = buildWhereClause(opts?.filters ?? [], validCols);\n\n // Column projection\n let selectCols = '*';\n if (opts?.columns && opts.columns.length > 0) {\n for (const c of opts.columns) {\n if (!validCols.has(c)) throw new Error(`Invalid column: \"${c}\"`);\n }\n selectCols = opts.columns.map(c => `\"${c.replace(/\"/g, '\"\"')}\"`).join(', ');\n }\n\n const limit = Math.min(opts?.limit ?? 500, 500); // hard cap\n const offset = opts?.offset ?? 0;\n\n // Count total matching\n const countRow = this.db.get(`SELECT COUNT(*) as cnt FROM \"${tableName}\" ${where}`, params);\n const totalMatching = countRow?.cnt ?? 0;\n\n // Fetch rows\n const rows = this.db.all(\n `SELECT ${selectCols} FROM \"${tableName}\" ${where} LIMIT ? OFFSET ?`,\n [...params, limit, offset],\n );\n\n return { rows, totalMatching };\n }\n\n // \u2500\u2500 Aggregate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n aggregate(\n datasetId: string,\n opts: { groupBy: string[]; metrics: AggregateMetric[]; filters?: QueryFilter[] },\n ): { rows: any[] } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get valid columns\n const validCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [datasetId]) as any[]).map(r => r.name),\n );\n\n // Validate group by columns\n for (const col of opts.groupBy) {\n if (!validCols.has(col)) throw new Error(`Invalid group-by column: \"${col}\"`);\n }\n\n // Build SELECT clause\n const selectParts: string[] = [];\n for (const col of opts.groupBy) {\n selectParts.push(`\"${col.replace(/\"/g, '\"\"')}\"`);\n }\n for (const metric of opts.metrics) {\n if (!validCols.has(metric.column) && metric.op !== 'count') {\n throw new Error(`Invalid metric column: \"${metric.column}\"`);\n }\n const colRef = `\"${metric.column.replace(/\"/g, '\"\"')}\"`;\n const alias = metric.alias ?? `${metric.op}_${metric.column}`;\n switch (metric.op) {\n case 'count': selectParts.push(`COUNT(${colRef}) as \"${alias}\"`); break;\n case 'count_distinct': selectParts.push(`COUNT(DISTINCT ${colRef}) as \"${alias}\"`); break;\n case 'sum': selectParts.push(`SUM(CAST(${colRef} AS REAL)) as \"${alias}\"`); break;\n case 'avg': selectParts.push(`AVG(CAST(${colRef} AS REAL)) as \"${alias}\"`); break;\n case 'min': selectParts.push(`MIN(${colRef}) as \"${alias}\"`); break;\n case 'max': selectParts.push(`MAX(${colRef}) as \"${alias}\"`); break;\n }\n }\n\n // Build WHERE\n const { where, params } = buildWhereClause(opts.filters ?? [], validCols);\n\n // Build GROUP BY\n const groupByClause = opts.groupBy.length > 0\n ? `GROUP BY ${opts.groupBy.map(c => `\"${c.replace(/\"/g, '\"\"')}\"`).join(', ')}`\n : '';\n\n const sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\" ${where} ${groupByClause} LIMIT 1000`;\n const rows = this.db.all(sql, params);\n\n return { rows };\n }\n\n // \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n searchColumns(datasetId: string, query: string): DataColumn[] {\n const pattern = `%${query.toLowerCase()}%`;\n const cols = this.db.all(`\n SELECT * FROM data_columns\n WHERE dataset_id = ? AND (LOWER(name) LIKE ? OR LOWER(sample_values) LIKE ? OR LOWER(summary) LIKE ?)\n ORDER BY position\n `, [datasetId, pattern, pattern, pattern]) as any[];\n\n return cols.map(c => ({\n id: c.id, datasetId: c.dataset_id, name: c.name, position: c.position,\n inferredType: c.inferred_type, nullable: !!c.nullable,\n nullCount: c.null_count, nullPct: c.null_pct, cardinality: c.cardinality,\n minValue: c.min_value, maxValue: c.max_value, meanValue: c.mean_value,\n sampleValues: c.sample_values ? JSON.parse(c.sample_values) : [],\n summary: c.summary, updatedAt: c.updated_at,\n }));\n }\n\n // \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getStats(): { datasets: number; totalRows: number; totalColumns: number } {\n const datasets = this.db.get('SELECT COUNT(*) as cnt FROM data_datasets')?.cnt ?? 0;\n const totalRows = this.db.get('SELECT SUM(row_count) as total FROM data_datasets')?.total ?? 0;\n const totalColumns = this.db.get('SELECT COUNT(*) as cnt FROM data_columns')?.cnt ?? 0;\n return { datasets, totalRows, totalColumns };\n }\n\n // \u2500\u2500 Join \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n join(opts: {\n left: string;\n right: string;\n leftColumn: string;\n rightColumn: string;\n type?: JoinType;\n columns?: string[];\n limit?: number;\n }): { rows: any[]; totalMatching: number } | null {\n // Verify both datasets exist\n const leftDs = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [opts.left]);\n const rightDs = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [opts.right]);\n if (!leftDs) return null;\n if (!rightDs) return null;\n\n const leftTable = `data_rows_${opts.left.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n const rightTable = `data_rows_${opts.right.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Validate join columns exist\n const leftCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [opts.left]) as any[]).map(r => r.name),\n );\n const rightCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [opts.right]) as any[]).map(r => r.name),\n );\n\n if (!leftCols.has(opts.leftColumn)) throw new Error(`Column \"${opts.leftColumn}\" not found in left dataset \"${opts.left}\"`);\n if (!rightCols.has(opts.rightColumn)) throw new Error(`Column \"${opts.rightColumn}\" not found in right dataset \"${opts.right}\"`);\n\n // Build join type\n const joinType = opts.type ?? 'inner';\n const joinKeyword = joinType === 'left' ? 'LEFT JOIN' : joinType === 'right' ? 'RIGHT JOIN' : 'INNER JOIN';\n\n // Column projection: prefix with L. or R. for disambiguation\n let selectCols: string;\n if (opts.columns && opts.columns.length > 0) {\n selectCols = opts.columns.map(c => {\n if (c.startsWith(`${opts.left}.`)) {\n const col = c.slice(opts.left.length + 1);\n if (!leftCols.has(col)) throw new Error(`Invalid column: \"${c}\"`);\n return `L.\"${col.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${col}\"`;\n } else if (c.startsWith(`${opts.right}.`)) {\n const col = c.slice(opts.right.length + 1);\n if (!rightCols.has(col)) throw new Error(`Invalid column: \"${c}\"`);\n return `R.\"${col.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${col}\"`;\n }\n // Try left first, then right\n if (leftCols.has(c)) return `L.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${c}\"`;\n if (rightCols.has(c)) return `R.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${c}\"`;\n throw new Error(`Column \"${c}\" not found in either dataset`);\n }).join(', ');\n } else {\n // Select all columns from both, prefixed\n const leftSelect = Array.from(leftCols).map(c => `L.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${c}\"`);\n const rightSelect = Array.from(rightCols).map(c => `R.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${c}\"`);\n selectCols = [...leftSelect, ...rightSelect].join(', ');\n }\n\n const limit = Math.min(opts.limit ?? 100, 500);\n const leftCol = `L.\"${opts.leftColumn.replace(/\"/g, '\"\"')}\"`;\n const rightCol = `R.\"${opts.rightColumn.replace(/\"/g, '\"\"')}\"`;\n\n // Count\n const countSql = `SELECT COUNT(*) as cnt FROM \"${leftTable}\" L ${joinKeyword} \"${rightTable}\" R ON ${leftCol} = ${rightCol}`;\n const countRow = this.db.get(countSql);\n const totalMatching = countRow?.cnt ?? 0;\n\n // Fetch\n const sql = `SELECT ${selectCols} FROM \"${leftTable}\" L ${joinKeyword} \"${rightTable}\" R ON ${leftCol} = ${rightCol} LIMIT ?`;\n const rows = this.db.all(sql, [limit]);\n\n return { rows, totalMatching };\n }\n\n // \u2500\u2500 Correlations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n correlations(datasetId: string, threshold = 0.3): CorrelationPair[] | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get numeric columns\n const numericCols = (this.db.all(\n `SELECT name FROM data_columns WHERE dataset_id = ? AND inferred_type IN ('integer', 'float')`,\n [datasetId],\n ) as any[]).map(r => r.name);\n\n if (numericCols.length < 2) return [];\n\n // Compute pairwise Pearson correlations using SQL\n const results: CorrelationPair[] = [];\n\n for (let i = 0; i < numericCols.length; i++) {\n for (let j = i + 1; j < numericCols.length; j++) {\n const col1 = numericCols[i]!;\n const col2 = numericCols[j]!;\n const c1 = `\"${col1.replace(/\"/g, '\"\"')}\"`;\n const c2 = `\"${col2.replace(/\"/g, '\"\"')}\"`;\n\n // Pearson correlation via SQL:\n // r = (n*sum(xy) - sum(x)*sum(y)) / sqrt((n*sum(x\u00B2) - sum(x)\u00B2) * (n*sum(y\u00B2) - sum(y)\u00B2))\n const row = this.db.get(`\n SELECT\n COUNT(*) as n,\n SUM(CAST(${c1} AS REAL) * CAST(${c2} AS REAL)) as sum_xy,\n SUM(CAST(${c1} AS REAL)) as sum_x,\n SUM(CAST(${c2} AS REAL)) as sum_y,\n SUM(CAST(${c1} AS REAL) * CAST(${c1} AS REAL)) as sum_x2,\n SUM(CAST(${c2} AS REAL) * CAST(${c2} AS REAL)) as sum_y2\n FROM \"${tableName}\"\n WHERE ${c1} IS NOT NULL AND ${c2} IS NOT NULL\n AND typeof(${c1}) != 'text' AND typeof(${c2}) != 'text'\n `);\n\n if (!row || row.n < 3) continue;\n\n const n = row.n;\n const numerator = n * row.sum_xy - row.sum_x * row.sum_y;\n const denomX = n * row.sum_x2 - row.sum_x * row.sum_x;\n const denomY = n * row.sum_y2 - row.sum_y * row.sum_y;\n const denom = Math.sqrt(denomX * denomY);\n\n if (denom === 0) continue;\n\n const correlation = numerator / denom;\n const absCorr = Math.abs(correlation);\n\n if (absCorr < threshold) continue;\n\n const strength: CorrelationPair['strength'] =\n absCorr >= 0.7 ? 'strong' :\n absCorr >= 0.4 ? 'moderate' :\n absCorr >= 0.2 ? 'weak' : 'negligible';\n\n results.push({ column1: col1, column2: col2, correlation: Math.round(correlation * 10000) / 10000, strength });\n }\n }\n\n // Sort by absolute correlation descending\n results.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation));\n return results;\n }\n\n // \u2500\u2500 History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getHistory(datasetId: string, limit = 20): Array<{\n id: number;\n datasetId: string;\n snapshotAt: number;\n rowCount: number;\n columnCount: number;\n columns: Array<{ name: string; type: string; cardinality: number; nullPct: number }>;\n contentHash: string;\n }> | null {\n const ds = this.db.get('SELECT id FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const rows = this.db.all(\n 'SELECT * FROM data_dataset_history WHERE dataset_id = ? ORDER BY snapshot_at DESC LIMIT ?',\n [datasetId, limit],\n ) as any[];\n\n return rows.map(r => ({\n id: r.id,\n datasetId: r.dataset_id,\n snapshotAt: r.snapshot_at,\n rowCount: r.row_count,\n columnCount: r.column_count,\n columns: JSON.parse(r.columns_json),\n contentHash: r.content_hash,\n }));\n }\n\n // \u2500\u2500 Drift Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n detectDrift(datasetId: string): {\n hasDrift: boolean;\n addedColumns: string[];\n removedColumns: string[];\n changedColumns: Array<{ name: string; changes: string[] }>;\n rowCountDelta: number;\n } | null {\n const ds = this.db.get('SELECT id FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const snapshots = this.db.all(\n 'SELECT * FROM data_dataset_history WHERE dataset_id = ? ORDER BY snapshot_at DESC LIMIT 2',\n [datasetId],\n ) as any[];\n\n if (snapshots.length < 2) {\n return { hasDrift: false, addedColumns: [], removedColumns: [], changedColumns: [], rowCountDelta: 0 };\n }\n\n const latest = snapshots[0];\n const previous = snapshots[1];\n const latestCols: Array<{ name: string; type: string; cardinality: number; nullPct: number }> = JSON.parse(latest.columns_json);\n const prevCols: Array<{ name: string; type: string; cardinality: number; nullPct: number }> = JSON.parse(previous.columns_json);\n\n const latestMap = new Map(latestCols.map(c => [c.name, c]));\n const prevMap = new Map(prevCols.map(c => [c.name, c]));\n\n const addedColumns: string[] = [];\n const removedColumns: string[] = [];\n const changedColumns: Array<{ name: string; changes: string[] }> = [];\n\n // Find added columns\n for (const col of latestCols) {\n if (!prevMap.has(col.name)) addedColumns.push(col.name);\n }\n\n // Find removed columns\n for (const col of prevCols) {\n if (!latestMap.has(col.name)) removedColumns.push(col.name);\n }\n\n // Find changed columns\n for (const col of latestCols) {\n const prev = prevMap.get(col.name);\n if (!prev) continue;\n const changes: string[] = [];\n if (col.type !== prev.type) changes.push(`type: ${prev.type} \u2192 ${col.type}`);\n if (col.cardinality !== prev.cardinality) changes.push(`cardinality: ${prev.cardinality} \u2192 ${col.cardinality}`);\n if (Math.abs(col.nullPct - prev.nullPct) > 0.01) changes.push(`nullPct: ${prev.nullPct.toFixed(1)}% \u2192 ${col.nullPct.toFixed(1)}%`);\n if (changes.length > 0) changedColumns.push({ name: col.name, changes });\n }\n\n const rowCountDelta = latest.row_count - previous.row_count;\n const hasDrift = addedColumns.length > 0 || removedColumns.length > 0 || changedColumns.length > 0;\n\n return { hasDrift, addedColumns, removedColumns, changedColumns, rowCountDelta };\n }\n\n // \u2500\u2500 Quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n quality(datasetId: string): ColumnQuality[] | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const cols = this.db.all(\n 'SELECT * FROM data_columns WHERE dataset_id = ? ORDER BY position',\n [datasetId],\n ) as any[];\n\n if (cols.length === 0) return [];\n\n const results: ColumnQuality[] = [];\n\n for (const col of cols) {\n const issues: string[] = [];\n let nullRisk = 0;\n let cardinalityRisk = 0;\n let typeRisk = 0;\n\n // Null rate risk\n const nullPct = col.null_pct ?? 0;\n if (nullPct > 50) {\n issues.push(`High null rate (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.4;\n } else if (nullPct > 20) {\n issues.push(`Moderate null rate (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.2;\n } else if (nullPct > 5) {\n issues.push(`Some nulls (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.1;\n }\n\n // Cardinality anomalies\n const cardinality = col.cardinality ?? 0;\n const rowCount = ds.row_count ?? 1;\n\n if (cardinality === 1 && rowCount > 10) {\n issues.push('Constant value (cardinality=1)');\n cardinalityRisk = 0.3;\n } else if (cardinality / rowCount > 0.95 && rowCount > 100) {\n issues.push('Near-unique (>95% distinct)');\n cardinalityRisk = 0.1;\n }\n\n // Type issues\n if (col.inferred_type === 'string' && col.cardinality < 20 && rowCount > 100) {\n issues.push('Low-cardinality string (consider enum/category)');\n typeRisk = 0.05;\n }\n\n const riskScore = Math.min(nullRisk + cardinalityRisk + typeRisk, 1.0);\n\n if (issues.length > 0) {\n results.push({ column: col.name, riskScore, nullRisk, cardinalityRisk, typeRisk, issues });\n }\n }\n\n // Sort by risk score descending\n results.sort((a, b) => b.riskScore - a.riskScore);\n return results;\n }\n\n // \u2500\u2500 Validation Rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Infer validation rules from column profiles.\n * Returns rules the code should apply when reading/writing this dataset.\n */\n validationRules(datasetId: string): ValidationRule[] | null {\n const info = this.describeDataset(datasetId);\n if (!info) return null;\n\n const results: ValidationRule[] = [];\n\n for (const col of info.columns) {\n const rules: string[] = [];\n\n // Required/nullable\n if (!col.nullable) {\n rules.push('required (never null)');\n } else if (col.nullPct < 0.01) {\n rules.push('rarely null (<1%) \u2014 consider required');\n }\n\n // Type constraint\n rules.push(`type: ${col.inferredType}`);\n\n // Range constraints for numeric\n if ((col.inferredType === 'integer' || col.inferredType === 'float') && col.minValue != null && col.maxValue != null) {\n rules.push(`range: ${col.minValue} to ${col.maxValue}`);\n }\n\n // Enum constraint for low-cardinality strings\n if (col.inferredType === 'string' && col.cardinality <= 20 && info.dataset.rowCount > 50) {\n rules.push(`enum (${col.cardinality} distinct values): ${col.sampleValues.join(', ')}`);\n }\n\n // Uniqueness\n if (col.cardinality === info.dataset.rowCount && info.dataset.rowCount > 10) {\n rules.push('unique (all values distinct)');\n }\n\n if (rules.length > 1) { // always has at least the type rule\n results.push({ column: col.name, rules });\n }\n }\n\n return results;\n }\n\n // \u2500\u2500 Sample Data Generation Hints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * From column profiles, provide hints for generating realistic test data.\n */\n sampleHints(datasetId: string): SampleHint[] | null {\n const info = this.describeDataset(datasetId);\n if (!info) return null;\n\n const results: SampleHint[] = [];\n\n for (const col of info.columns) {\n let hint: string;\n\n switch (col.inferredType) {\n case 'integer':\n if (col.minValue != null && col.maxValue != null) {\n hint = `Random integer between ${col.minValue} and ${col.maxValue}`;\n } else {\n hint = 'Random integer';\n }\n if (col.cardinality === info.dataset.rowCount) hint += ' (must be unique)';\n break;\n\n case 'float':\n if (col.minValue != null && col.maxValue != null) {\n hint = `Random float between ${col.minValue} and ${col.maxValue}`;\n if (col.meanValue != null) hint += ` (mean \u2248 ${col.meanValue.toFixed(2)})`;\n } else {\n hint = 'Random float';\n }\n break;\n\n case 'boolean':\n hint = 'Random boolean (true/false)';\n break;\n\n case 'date':\n if (col.minValue && col.maxValue) {\n hint = `Random date between ${col.minValue} and ${col.maxValue}`;\n } else {\n hint = 'Random ISO date string';\n }\n break;\n\n case 'string':\n if (col.cardinality <= 10 && info.dataset.rowCount > 20) {\n hint = `Pick from: ${col.sampleValues.join(', ')}`;\n } else if (col.cardinality === info.dataset.rowCount) {\n // Looks like an ID or email\n const name = col.name.toLowerCase();\n if (name.includes('email')) hint = 'Generate unique email (e.g. user{n}@example.com)';\n else if (name.includes('id') || name.includes('uuid')) hint = 'Generate unique ID/UUID';\n else if (name.includes('name')) hint = 'Generate unique name string';\n else if (name.includes('url') || name.includes('link')) hint = 'Generate unique URL';\n else hint = `Generate unique string (${col.cardinality} distinct in source)`;\n } else {\n hint = `Random string (${col.cardinality} distinct values in source)`;\n if (col.sampleValues.length > 0) hint += `. Examples: ${col.sampleValues.slice(0, 3).join(', ')}`;\n }\n break;\n\n default:\n hint = `Generate ${col.inferredType} value`;\n }\n\n if (col.nullable && col.nullPct > 0.05) {\n hint += ` \u2014 ${(col.nullPct * 100).toFixed(0)}% chance of null`;\n }\n\n results.push({ column: col.name, hint });\n }\n\n return results;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Data Queries\n *\n * Read-side helpers for MCP tools and CLI commands.\n * Handles: list, describe, query (with filters), aggregate, search.\n */\n\nimport type { DataSet, DataColumn, QueryFilter, AggregateMetric, CorrelationPair, JoinType, ColumnQuality } from './types';\nimport { buildWhereClause } from './filters';\n\nexport interface ValidationRule {\n column: string;\n rules: string[];\n}\n\nexport interface SampleHint {\n column: string;\n hint: string;\n}\n\nexport class DataQueries {\n private readonly db: any;\n\n constructor(db: any) {\n this.db = db;\n }\n\n // \u2500\u2500 List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n listDatasets(): DataSet[] {\n const rows = this.db.all('SELECT * FROM data_datasets ORDER BY file_path') as any[];\n return rows.map(r => ({\n id: r.id,\n filePath: r.file_path,\n format: r.format,\n rowCount: r.row_count,\n columnCount: r.column_count,\n fileSize: r.file_size,\n contentHash: r.content_hash,\n summary: r.summary,\n indexedAt: r.indexed_at,\n metadataJson: r.metadata_json ?? null,\n }));\n }\n\n // \u2500\u2500 Describe \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n describeDataset(datasetId: string): { dataset: DataSet; columns: DataColumn[] } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const cols = this.db.all(\n 'SELECT * FROM data_columns WHERE dataset_id = ? ORDER BY position',\n [datasetId],\n ) as any[];\n\n return {\n dataset: {\n id: ds.id, filePath: ds.file_path, format: ds.format,\n rowCount: ds.row_count, columnCount: ds.column_count,\n fileSize: ds.file_size, contentHash: ds.content_hash,\n summary: ds.summary, indexedAt: ds.indexed_at,\n metadataJson: ds.metadata_json ?? null,\n },\n columns: cols.map(c => ({\n id: c.id, datasetId: c.dataset_id, name: c.name, position: c.position,\n inferredType: c.inferred_type, nullable: !!c.nullable,\n nullCount: c.null_count, nullPct: c.null_pct, cardinality: c.cardinality,\n minValue: c.min_value, maxValue: c.max_value, meanValue: c.mean_value,\n sampleValues: c.sample_values ? JSON.parse(c.sample_values) : [],\n summary: c.summary, updatedAt: c.updated_at,\n })),\n };\n }\n\n describeColumn(datasetId: string, columnName: string): DataColumn | null {\n const col = this.db.get(\n 'SELECT * FROM data_columns WHERE dataset_id = ? AND name = ?',\n [datasetId, columnName],\n );\n if (!col) return null;\n return {\n id: col.id, datasetId: col.dataset_id, name: col.name, position: col.position,\n inferredType: col.inferred_type, nullable: !!col.nullable,\n nullCount: col.null_count, nullPct: col.null_pct, cardinality: col.cardinality,\n minValue: col.min_value, maxValue: col.max_value, meanValue: col.mean_value,\n sampleValues: col.sample_values ? JSON.parse(col.sample_values) : [],\n summary: col.summary, updatedAt: col.updated_at,\n };\n }\n\n // \u2500\u2500 Query (filtered rows) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n queryRows(\n datasetId: string,\n opts?: { filters?: QueryFilter[]; columns?: string[]; limit?: number; offset?: number },\n ): { rows: any[]; totalMatching: number } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get valid columns\n const validCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [datasetId]) as any[]).map(r => r.name),\n );\n\n // Build WHERE clause\n const { where, params } = buildWhereClause(opts?.filters ?? [], validCols);\n\n // Column projection\n let selectCols = '*';\n if (opts?.columns && opts.columns.length > 0) {\n for (const c of opts.columns) {\n if (!validCols.has(c)) throw new Error(`Invalid column: \"${c}\"`);\n }\n selectCols = opts.columns.map(c => `\"${c.replace(/\"/g, '\"\"')}\"`).join(', ');\n }\n\n const limit = Math.min(opts?.limit ?? 500, 500); // hard cap\n const offset = opts?.offset ?? 0;\n\n // Count total matching\n const countRow = this.db.get(`SELECT COUNT(*) as cnt FROM \"${tableName}\" ${where}`, params);\n const totalMatching = countRow?.cnt ?? 0;\n\n // Fetch rows\n const rows = this.db.all(\n `SELECT ${selectCols} FROM \"${tableName}\" ${where} LIMIT ? OFFSET ?`,\n [...params, limit, offset],\n );\n\n return { rows, totalMatching };\n }\n\n // \u2500\u2500 Aggregate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n aggregate(\n datasetId: string,\n opts: { groupBy: string[]; metrics: AggregateMetric[]; filters?: QueryFilter[] },\n ): { rows: any[] } | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get valid columns\n const validCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [datasetId]) as any[]).map(r => r.name),\n );\n\n // Validate group by columns\n for (const col of opts.groupBy) {\n if (!validCols.has(col)) throw new Error(`Invalid group-by column: \"${col}\"`);\n }\n\n // Build SELECT clause\n const selectParts: string[] = [];\n for (const col of opts.groupBy) {\n selectParts.push(`\"${col.replace(/\"/g, '\"\"')}\"`);\n }\n for (const metric of opts.metrics) {\n if (!validCols.has(metric.column) && metric.op !== 'count') {\n throw new Error(`Invalid metric column: \"${metric.column}\"`);\n }\n const colRef = `\"${metric.column.replace(/\"/g, '\"\"')}\"`;\n const alias = metric.alias ?? `${metric.op}_${metric.column}`;\n switch (metric.op) {\n case 'count': selectParts.push(`COUNT(${colRef}) as \"${alias}\"`); break;\n case 'count_distinct': selectParts.push(`COUNT(DISTINCT ${colRef}) as \"${alias}\"`); break;\n case 'sum': selectParts.push(`SUM(CAST(${colRef} AS REAL)) as \"${alias}\"`); break;\n case 'avg': selectParts.push(`AVG(CAST(${colRef} AS REAL)) as \"${alias}\"`); break;\n case 'min': selectParts.push(`MIN(${colRef}) as \"${alias}\"`); break;\n case 'max': selectParts.push(`MAX(${colRef}) as \"${alias}\"`); break;\n }\n }\n\n // Build WHERE\n const { where, params } = buildWhereClause(opts.filters ?? [], validCols);\n\n // Build GROUP BY\n const groupByClause = opts.groupBy.length > 0\n ? `GROUP BY ${opts.groupBy.map(c => `\"${c.replace(/\"/g, '\"\"')}\"`).join(', ')}`\n : '';\n\n const sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\" ${where} ${groupByClause} LIMIT 1000`;\n const rows = this.db.all(sql, params);\n\n return { rows };\n }\n\n // \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n searchColumns(datasetId: string, query: string): DataColumn[] {\n const pattern = `%${query.toLowerCase()}%`;\n const cols = this.db.all(`\n SELECT * FROM data_columns\n WHERE dataset_id = ? AND (LOWER(name) LIKE ? OR LOWER(sample_values) LIKE ? OR LOWER(summary) LIKE ?)\n ORDER BY position\n `, [datasetId, pattern, pattern, pattern]) as any[];\n\n return cols.map(c => ({\n id: c.id, datasetId: c.dataset_id, name: c.name, position: c.position,\n inferredType: c.inferred_type, nullable: !!c.nullable,\n nullCount: c.null_count, nullPct: c.null_pct, cardinality: c.cardinality,\n minValue: c.min_value, maxValue: c.max_value, meanValue: c.mean_value,\n sampleValues: c.sample_values ? JSON.parse(c.sample_values) : [],\n summary: c.summary, updatedAt: c.updated_at,\n }));\n }\n\n // \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getStats(): { datasets: number; totalRows: number; totalColumns: number } {\n const datasets = this.db.get('SELECT COUNT(*) as cnt FROM data_datasets')?.cnt ?? 0;\n const totalRows = this.db.get('SELECT SUM(row_count) as total FROM data_datasets')?.total ?? 0;\n const totalColumns = this.db.get('SELECT COUNT(*) as cnt FROM data_columns')?.cnt ?? 0;\n return { datasets, totalRows, totalColumns };\n }\n\n // \u2500\u2500 Join \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n join(opts: {\n left: string;\n right: string;\n leftColumn: string;\n rightColumn: string;\n type?: JoinType;\n columns?: string[];\n limit?: number;\n }): { rows: any[]; totalMatching: number } | null {\n // Verify both datasets exist\n const leftDs = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [opts.left]);\n const rightDs = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [opts.right]);\n if (!leftDs) return null;\n if (!rightDs) return null;\n\n const leftTable = `data_rows_${opts.left.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n const rightTable = `data_rows_${opts.right.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Validate join columns exist\n const leftCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [opts.left]) as any[]).map(r => r.name),\n );\n const rightCols = new Set<string>(\n (this.db.all('SELECT name FROM data_columns WHERE dataset_id = ?', [opts.right]) as any[]).map(r => r.name),\n );\n\n if (!leftCols.has(opts.leftColumn)) throw new Error(`Column \"${opts.leftColumn}\" not found in left dataset \"${opts.left}\"`);\n if (!rightCols.has(opts.rightColumn)) throw new Error(`Column \"${opts.rightColumn}\" not found in right dataset \"${opts.right}\"`);\n\n // Build join type\n const joinType = opts.type ?? 'inner';\n const joinKeyword = joinType === 'left' ? 'LEFT JOIN' : joinType === 'right' ? 'RIGHT JOIN' : 'INNER JOIN';\n\n // Column projection: prefix with L. or R. for disambiguation\n let selectCols: string;\n if (opts.columns && opts.columns.length > 0) {\n selectCols = opts.columns.map(c => {\n if (c.startsWith(`${opts.left}.`)) {\n const col = c.slice(opts.left.length + 1);\n if (!leftCols.has(col)) throw new Error(`Invalid column: \"${c}\"`);\n return `L.\"${col.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${col}\"`;\n } else if (c.startsWith(`${opts.right}.`)) {\n const col = c.slice(opts.right.length + 1);\n if (!rightCols.has(col)) throw new Error(`Invalid column: \"${c}\"`);\n return `R.\"${col.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${col}\"`;\n }\n // Try left first, then right\n if (leftCols.has(c)) return `L.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${c}\"`;\n if (rightCols.has(c)) return `R.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${c}\"`;\n throw new Error(`Column \"${c}\" not found in either dataset`);\n }).join(', ');\n } else {\n // Select all columns from both, prefixed\n const leftSelect = Array.from(leftCols).map(c => `L.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.left}.${c}\"`);\n const rightSelect = Array.from(rightCols).map(c => `R.\"${c.replace(/\"/g, '\"\"')}\" as \"${opts.right}.${c}\"`);\n selectCols = [...leftSelect, ...rightSelect].join(', ');\n }\n\n const limit = Math.min(opts.limit ?? 100, 500);\n const leftCol = `L.\"${opts.leftColumn.replace(/\"/g, '\"\"')}\"`;\n const rightCol = `R.\"${opts.rightColumn.replace(/\"/g, '\"\"')}\"`;\n\n // Count\n const countSql = `SELECT COUNT(*) as cnt FROM \"${leftTable}\" L ${joinKeyword} \"${rightTable}\" R ON ${leftCol} = ${rightCol}`;\n const countRow = this.db.get(countSql);\n const totalMatching = countRow?.cnt ?? 0;\n\n // Fetch\n const sql = `SELECT ${selectCols} FROM \"${leftTable}\" L ${joinKeyword} \"${rightTable}\" R ON ${leftCol} = ${rightCol} LIMIT ?`;\n const rows = this.db.all(sql, [limit]);\n\n return { rows, totalMatching };\n }\n\n // \u2500\u2500 Correlations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n correlations(datasetId: string, threshold = 0.3): CorrelationPair[] | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n\n // Get numeric columns\n const numericCols = (this.db.all(\n `SELECT name FROM data_columns WHERE dataset_id = ? AND inferred_type IN ('integer', 'float')`,\n [datasetId],\n ) as any[]).map(r => r.name);\n\n if (numericCols.length < 2) return [];\n\n // Compute pairwise Pearson correlations using SQL\n const results: CorrelationPair[] = [];\n\n for (let i = 0; i < numericCols.length; i++) {\n for (let j = i + 1; j < numericCols.length; j++) {\n const col1 = numericCols[i]!;\n const col2 = numericCols[j]!;\n const c1 = `\"${col1.replace(/\"/g, '\"\"')}\"`;\n const c2 = `\"${col2.replace(/\"/g, '\"\"')}\"`;\n\n // Pearson correlation via SQL:\n // r = (n*sum(xy) - sum(x)*sum(y)) / sqrt((n*sum(x\u00B2) - sum(x)\u00B2) * (n*sum(y\u00B2) - sum(y)\u00B2))\n const row = this.db.get(`\n SELECT\n COUNT(*) as n,\n SUM(CAST(${c1} AS REAL) * CAST(${c2} AS REAL)) as sum_xy,\n SUM(CAST(${c1} AS REAL)) as sum_x,\n SUM(CAST(${c2} AS REAL)) as sum_y,\n SUM(CAST(${c1} AS REAL) * CAST(${c1} AS REAL)) as sum_x2,\n SUM(CAST(${c2} AS REAL) * CAST(${c2} AS REAL)) as sum_y2\n FROM \"${tableName}\"\n WHERE ${c1} IS NOT NULL AND ${c2} IS NOT NULL\n AND typeof(${c1}) != 'text' AND typeof(${c2}) != 'text'\n `);\n\n if (!row || row.n < 3) continue;\n\n const n = row.n;\n const numerator = n * row.sum_xy - row.sum_x * row.sum_y;\n const denomX = n * row.sum_x2 - row.sum_x * row.sum_x;\n const denomY = n * row.sum_y2 - row.sum_y * row.sum_y;\n const denom = Math.sqrt(denomX * denomY);\n\n if (denom === 0) continue;\n\n const correlation = numerator / denom;\n const absCorr = Math.abs(correlation);\n\n if (absCorr < threshold) continue;\n\n const strength: CorrelationPair['strength'] =\n absCorr >= 0.7 ? 'strong' :\n absCorr >= 0.4 ? 'moderate' :\n absCorr >= 0.2 ? 'weak' : 'negligible';\n\n results.push({ column1: col1, column2: col2, correlation: Math.round(correlation * 10000) / 10000, strength });\n }\n }\n\n // Sort by absolute correlation descending\n results.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation));\n return results;\n }\n\n // \u2500\u2500 History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getHistory(datasetId: string, limit = 20): Array<{\n id: number;\n datasetId: string;\n snapshotAt: number;\n rowCount: number;\n columnCount: number;\n columns: Array<{ name: string; type: string; cardinality: number; nullPct: number }>;\n contentHash: string;\n }> | null {\n const ds = this.db.get('SELECT id FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const rows = this.db.all(\n 'SELECT * FROM data_dataset_history WHERE dataset_id = ? ORDER BY snapshot_at DESC LIMIT ?',\n [datasetId, limit],\n ) as any[];\n\n return rows.map(r => ({\n id: r.id,\n datasetId: r.dataset_id,\n snapshotAt: r.snapshot_at,\n rowCount: r.row_count,\n columnCount: r.column_count,\n columns: JSON.parse(r.columns_json),\n contentHash: r.content_hash,\n }));\n }\n\n // \u2500\u2500 Drift Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n detectDrift(datasetId: string): {\n hasDrift: boolean;\n addedColumns: string[];\n removedColumns: string[];\n changedColumns: Array<{ name: string; changes: string[] }>;\n rowCountDelta: number;\n } | null {\n const ds = this.db.get('SELECT id FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const snapshots = this.db.all(\n 'SELECT * FROM data_dataset_history WHERE dataset_id = ? ORDER BY snapshot_at DESC LIMIT 2',\n [datasetId],\n ) as any[];\n\n if (snapshots.length < 2) {\n return { hasDrift: false, addedColumns: [], removedColumns: [], changedColumns: [], rowCountDelta: 0 };\n }\n\n const latest = snapshots[0];\n const previous = snapshots[1];\n const latestCols: Array<{ name: string; type: string; cardinality: number; nullPct: number }> = JSON.parse(latest.columns_json);\n const prevCols: Array<{ name: string; type: string; cardinality: number; nullPct: number }> = JSON.parse(previous.columns_json);\n\n const latestMap = new Map(latestCols.map(c => [c.name, c]));\n const prevMap = new Map(prevCols.map(c => [c.name, c]));\n\n const addedColumns: string[] = [];\n const removedColumns: string[] = [];\n const changedColumns: Array<{ name: string; changes: string[] }> = [];\n\n // Find added columns\n for (const col of latestCols) {\n if (!prevMap.has(col.name)) addedColumns.push(col.name);\n }\n\n // Find removed columns\n for (const col of prevCols) {\n if (!latestMap.has(col.name)) removedColumns.push(col.name);\n }\n\n // Find changed columns\n for (const col of latestCols) {\n const prev = prevMap.get(col.name);\n if (!prev) continue;\n const changes: string[] = [];\n if (col.type !== prev.type) changes.push(`type: ${prev.type} \u2192 ${col.type}`);\n if (col.cardinality !== prev.cardinality) changes.push(`cardinality: ${prev.cardinality} \u2192 ${col.cardinality}`);\n if (Math.abs(col.nullPct - prev.nullPct) > 0.01) changes.push(`nullPct: ${prev.nullPct.toFixed(1)}% \u2192 ${col.nullPct.toFixed(1)}%`);\n if (changes.length > 0) changedColumns.push({ name: col.name, changes });\n }\n\n const rowCountDelta = latest.row_count - previous.row_count;\n const hasDrift = addedColumns.length > 0 || removedColumns.length > 0 || changedColumns.length > 0;\n\n return { hasDrift, addedColumns, removedColumns, changedColumns, rowCountDelta };\n }\n\n // \u2500\u2500 Quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n quality(datasetId: string): ColumnQuality[] | null {\n const ds = this.db.get('SELECT * FROM data_datasets WHERE id = ?', [datasetId]);\n if (!ds) return null;\n\n const cols = this.db.all(\n 'SELECT * FROM data_columns WHERE dataset_id = ? ORDER BY position',\n [datasetId],\n ) as any[];\n\n if (cols.length === 0) return [];\n\n const results: ColumnQuality[] = [];\n\n // PDF-specific quality checks\n if (ds.format === 'pdf') {\n const tableName = `data_rows_${datasetId.replace(/[^a-zA-Z0-9_]/g, '_')}`;\n try {\n const ocrRow = this.db.get(`SELECT COUNT(*) as cnt FROM \"${tableName}\" WHERE needs_ocr = 'true'`);\n const ocrCount = ocrRow?.cnt ?? 0;\n if (ocrCount > 0) {\n const totalPages = ds.row_count ?? 0;\n results.push({\n column: 'needs_ocr',\n riskScore: Math.min(ocrCount / Math.max(totalPages, 1), 1.0),\n nullRisk: 0,\n cardinalityRisk: 0,\n typeRisk: 0,\n issues: [`${ocrCount} of ${totalPages} pages need OCR (scanned/image-only content)`],\n });\n }\n } catch {\n // table may not exist yet \u2014 non-critical\n }\n\n if (ds.metadata_json) {\n try {\n const meta = JSON.parse(ds.metadata_json);\n if (meta.hasEncodingIssues) {\n results.push({\n column: 'content',\n riskScore: 0.3,\n nullRisk: 0,\n cardinalityRisk: 0,\n typeRisk: 0.3,\n issues: ['PDF has encoding issues \u2014 text extraction may be garbled; consider OCR pre-processing'],\n });\n }\n } catch {\n // malformed metadata \u2014 skip\n }\n }\n }\n\n for (const col of cols) {\n const issues: string[] = [];\n let nullRisk = 0;\n let cardinalityRisk = 0;\n let typeRisk = 0;\n\n // Null rate risk\n const nullPct = col.null_pct ?? 0;\n if (nullPct > 50) {\n issues.push(`High null rate (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.4;\n } else if (nullPct > 20) {\n issues.push(`Moderate null rate (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.2;\n } else if (nullPct > 5) {\n issues.push(`Some nulls (${nullPct.toFixed(1)}%)`);\n nullRisk = 0.1;\n }\n\n // Cardinality anomalies\n const cardinality = col.cardinality ?? 0;\n const rowCount = ds.row_count ?? 1;\n\n if (cardinality === 1 && rowCount > 10) {\n issues.push('Constant value (cardinality=1)');\n cardinalityRisk = 0.3;\n } else if (cardinality / rowCount > 0.95 && rowCount > 100) {\n issues.push('Near-unique (>95% distinct)');\n cardinalityRisk = 0.1;\n }\n\n // Type issues\n if (col.inferred_type === 'string' && col.cardinality < 20 && rowCount > 100) {\n issues.push('Low-cardinality string (consider enum/category)');\n typeRisk = 0.05;\n }\n\n const riskScore = Math.min(nullRisk + cardinalityRisk + typeRisk, 1.0);\n\n if (issues.length > 0) {\n results.push({ column: col.name, riskScore, nullRisk, cardinalityRisk, typeRisk, issues });\n }\n }\n\n // Sort by risk score descending\n results.sort((a, b) => b.riskScore - a.riskScore);\n return results;\n }\n\n // \u2500\u2500 Validation Rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Infer validation rules from column profiles.\n * Returns rules the code should apply when reading/writing this dataset.\n */\n validationRules(datasetId: string): ValidationRule[] | null {\n const info = this.describeDataset(datasetId);\n if (!info) return null;\n\n const results: ValidationRule[] = [];\n\n for (const col of info.columns) {\n const rules: string[] = [];\n\n // Required/nullable\n if (!col.nullable) {\n rules.push('required (never null)');\n } else if (col.nullPct < 0.01) {\n rules.push('rarely null (<1%) \u2014 consider required');\n }\n\n // Type constraint\n rules.push(`type: ${col.inferredType}`);\n\n // Range constraints for numeric\n if ((col.inferredType === 'integer' || col.inferredType === 'float') && col.minValue != null && col.maxValue != null) {\n rules.push(`range: ${col.minValue} to ${col.maxValue}`);\n }\n\n // Enum constraint for low-cardinality strings\n if (col.inferredType === 'string' && col.cardinality <= 20 && info.dataset.rowCount > 50) {\n rules.push(`enum (${col.cardinality} distinct values): ${col.sampleValues.join(', ')}`);\n }\n\n // Uniqueness\n if (col.cardinality === info.dataset.rowCount && info.dataset.rowCount > 10) {\n rules.push('unique (all values distinct)');\n }\n\n if (rules.length > 1) { // always has at least the type rule\n results.push({ column: col.name, rules });\n }\n }\n\n return results;\n }\n\n // \u2500\u2500 Sample Data Generation Hints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * From column profiles, provide hints for generating realistic test data.\n */\n sampleHints(datasetId: string): SampleHint[] | null {\n const info = this.describeDataset(datasetId);\n if (!info) return null;\n\n const results: SampleHint[] = [];\n\n for (const col of info.columns) {\n let hint: string;\n\n switch (col.inferredType) {\n case 'integer':\n if (col.minValue != null && col.maxValue != null) {\n hint = `Random integer between ${col.minValue} and ${col.maxValue}`;\n } else {\n hint = 'Random integer';\n }\n if (col.cardinality === info.dataset.rowCount) hint += ' (must be unique)';\n break;\n\n case 'float':\n if (col.minValue != null && col.maxValue != null) {\n hint = `Random float between ${col.minValue} and ${col.maxValue}`;\n if (col.meanValue != null) hint += ` (mean \u2248 ${col.meanValue.toFixed(2)})`;\n } else {\n hint = 'Random float';\n }\n break;\n\n case 'boolean':\n hint = 'Random boolean (true/false)';\n break;\n\n case 'date':\n if (col.minValue && col.maxValue) {\n hint = `Random date between ${col.minValue} and ${col.maxValue}`;\n } else {\n hint = 'Random ISO date string';\n }\n break;\n\n case 'string':\n if (col.cardinality <= 10 && info.dataset.rowCount > 20) {\n hint = `Pick from: ${col.sampleValues.join(', ')}`;\n } else if (col.cardinality === info.dataset.rowCount) {\n // Looks like an ID or email\n const name = col.name.toLowerCase();\n if (name.includes('email')) hint = 'Generate unique email (e.g. user{n}@example.com)';\n else if (name.includes('id') || name.includes('uuid')) hint = 'Generate unique ID/UUID';\n else if (name.includes('name')) hint = 'Generate unique name string';\n else if (name.includes('url') || name.includes('link')) hint = 'Generate unique URL';\n else hint = `Generate unique string (${col.cardinality} distinct in source)`;\n } else {\n hint = `Random string (${col.cardinality} distinct values in source)`;\n if (col.sampleValues.length > 0) hint += `. Examples: ${col.sampleValues.slice(0, 3).join(', ')}`;\n }\n break;\n\n default:\n hint = `Generate ${col.inferredType} value`;\n }\n\n if (col.nullable && col.nullPct > 0.05) {\n hint += ` \u2014 ${(col.nullPct * 100).toFixed(0)}% chance of null`;\n }\n\n results.push({ column: col.name, hint });\n }\n\n return results;\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,qBAAiC;AAY1B,MAAM,YAAY;AAAA,EAGvB,YAAY,IAAS;AACnB,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA,EAIA,eAA0B;AACxB,UAAM,OAAO,KAAK,GAAG,IAAI,gDAAgD;AACzE,WAAO,KAAK,IAAI,QAAM;AAAA,MACpB,IAAI,EAAE;AAAA,MACN,UAAU,EAAE;AAAA,MACZ,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE;AAAA,MACZ,aAAa,EAAE;AAAA,MACf,UAAU,EAAE;AAAA,MACZ,aAAa,EAAE;AAAA,MACf,SAAS,EAAE;AAAA,MACX,WAAW,EAAE;AAAA,MACb,cAAc,EAAE,iBAAiB;AAAA,IACnC,EAAE;AAAA,EACJ;AAAA;AAAA,EAIA,gBAAgB,WAAuE;AACrF,UAAM,KAAK,KAAK,GAAG,IAAI,4CAA4C,CAAC,SAAS,CAAC;AAC9E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,QACP,IAAI,GAAG;AAAA,QAAI,UAAU,GAAG;AAAA,QAAW,QAAQ,GAAG;AAAA,QAC9C,UAAU,GAAG;AAAA,QAAW,aAAa,GAAG;AAAA,QACxC,UAAU,GAAG;AAAA,QAAW,aAAa,GAAG;AAAA,QACxC,SAAS,GAAG;AAAA,QAAS,WAAW,GAAG;AAAA,QACnC,cAAc,GAAG,iBAAiB;AAAA,MACpC;AAAA,MACA,SAAS,KAAK,IAAI,QAAM;AAAA,QACtB,IAAI,EAAE;AAAA,QAAI,WAAW,EAAE;AAAA,QAAY,MAAM,EAAE;AAAA,QAAM,UAAU,EAAE;AAAA,QAC7D,cAAc,EAAE;AAAA,QAAe,UAAU,CAAC,CAAC,EAAE;AAAA,QAC7C,WAAW,EAAE;AAAA,QAAY,SAAS,EAAE;AAAA,QAAU,aAAa,EAAE;AAAA,QAC7D,UAAU,EAAE;AAAA,QAAW,UAAU,EAAE;AAAA,QAAW,WAAW,EAAE;AAAA,QAC3D,cAAc,EAAE,gBAAgB,KAAK,MAAM,EAAE,aAAa,IAAI,CAAC;AAAA,QAC/D,SAAS,EAAE;AAAA,QAAS,WAAW,EAAE;AAAA,MACnC,EAAE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,eAAe,WAAmB,YAAuC;AACvE,UAAM,MAAM,KAAK,GAAG;AAAA,MAClB;AAAA,MACA,CAAC,WAAW,UAAU;AAAA,IACxB;AACA,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MAAI,WAAW,IAAI;AAAA,MAAY,MAAM,IAAI;AAAA,MAAM,UAAU,IAAI;AAAA,MACrE,cAAc,IAAI;AAAA,MAAe,UAAU,CAAC,CAAC,IAAI;AAAA,MACjD,WAAW,IAAI;AAAA,MAAY,SAAS,IAAI;AAAA,MAAU,aAAa,IAAI;AAAA,MACnE,UAAU,IAAI;AAAA,MAAW,UAAU,IAAI;AAAA,MAAW,WAAW,IAAI;AAAA,MACjE,cAAc,IAAI,gBAAgB,KAAK,MAAM,IAAI,aAAa,IAAI,CAAC;AAAA,MACnE,SAAS,IAAI;AAAA,MAAS,WAAW,IAAI;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAIA,UACE,WACA,MAC+C;AAC/C,UAAM,KAAK,KAAK,GAAG,IAAI,4CAA4C,CAAC,SAAS,CAAC;AAC9E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,YAAY,aAAa,UAAU,QAAQ,kBAAkB,GAAG,CAAC;AAGvE,UAAM,YAAY,IAAI;AAAA,MACnB,KAAK,GAAG,IAAI,sDAAsD,CAAC,SAAS,CAAC,EAAY,IAAI,OAAK,EAAE,IAAI;AAAA,IAC3G;AAGA,UAAM,EAAE,OAAO,OAAO,QAAI,iCAAiB,MAAM,WAAW,CAAC,GAAG,SAAS;AAGzE,QAAI,aAAa;AACjB,QAAI,MAAM,WAAW,KAAK,QAAQ,SAAS,GAAG;AAC5C,iBAAW,KAAK,KAAK,SAAS;AAC5B,YAAI,CAAC,UAAU,IAAI,CAAC,EAAG,OAAM,IAAI,MAAM,oBAAoB,CAAC,GAAG;AAAA,MACjE;AACA,mBAAa,KAAK,QAAQ,IAAI,OAAK,IAAI,EAAE,QAAQ,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI;AAAA,IAC5E;AAEA,UAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,KAAK,GAAG;AAC9C,UAAM,SAAS,MAAM,UAAU;AAG/B,UAAM,WAAW,KAAK,GAAG,IAAI,gCAAgC,SAAS,KAAK,KAAK,IAAI,MAAM;AAC1F,UAAM,gBAAgB,UAAU,OAAO;AAGvC,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB,UAAU,UAAU,UAAU,SAAS,KAAK,KAAK;AAAA,MACjD,CAAC,GAAG,QAAQ,OAAO,MAAM;AAAA,IAC3B;AAEA,WAAO,EAAE,MAAM,cAAc;AAAA,EAC/B;AAAA;AAAA,EAIA,UACE,WACA,MACwB;AACxB,UAAM,KAAK,KAAK,GAAG,IAAI,4CAA4C,CAAC,SAAS,CAAC;AAC9E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,YAAY,aAAa,UAAU,QAAQ,kBAAkB,GAAG,CAAC;AAGvE,UAAM,YAAY,IAAI;AAAA,MACnB,KAAK,GAAG,IAAI,sDAAsD,CAAC,SAAS,CAAC,EAAY,IAAI,OAAK,EAAE,IAAI;AAAA,IAC3G;AAGA,eAAW,OAAO,KAAK,SAAS;AAC9B,UAAI,CAAC,UAAU,IAAI,GAAG,EAAG,OAAM,IAAI,MAAM,6BAA6B,GAAG,GAAG;AAAA,IAC9E;AAGA,UAAM,cAAwB,CAAC;AAC/B,eAAW,OAAO,KAAK,SAAS;AAC9B,kBAAY,KAAK,IAAI,IAAI,QAAQ,MAAM,IAAI,CAAC,GAAG;AAAA,IACjD;AACA,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,CAAC,UAAU,IAAI,OAAO,MAAM,KAAK,OAAO,OAAO,SAAS;AAC1D,cAAM,IAAI,MAAM,2BAA2B,OAAO,MAAM,GAAG;AAAA,MAC7D;AACA,YAAM,SAAS,IAAI,OAAO,OAAO,QAAQ,MAAM,IAAI,CAAC;AACpD,YAAM,QAAQ,OAAO,SAAS,GAAG,OAAO,EAAE,IAAI,OAAO,MAAM;AAC3D,cAAQ,OAAO,IAAI;AAAA,QACjB,KAAK;AAAS,sBAAY,KAAK,SAAS,MAAM,SAAS,KAAK,GAAG;AAAG;AAAA,QAClE,KAAK;AAAkB,sBAAY,KAAK,kBAAkB,MAAM,SAAS,KAAK,GAAG;AAAG;AAAA,QACpF,KAAK;AAAO,sBAAY,KAAK,YAAY,MAAM,kBAAkB,KAAK,GAAG;AAAG;AAAA,QAC5E,KAAK;AAAO,sBAAY,KAAK,YAAY,MAAM,kBAAkB,KAAK,GAAG;AAAG;AAAA,QAC5E,KAAK;AAAO,sBAAY,KAAK,OAAO,MAAM,SAAS,KAAK,GAAG;AAAG;AAAA,QAC9D,KAAK;AAAO,sBAAY,KAAK,OAAO,MAAM,SAAS,KAAK,GAAG;AAAG;AAAA,MAChE;AAAA,IACF;AAGA,UAAM,EAAE,OAAO,OAAO,QAAI,iCAAiB,KAAK,WAAW,CAAC,GAAG,SAAS;AAGxE,UAAM,gBAAgB,KAAK,QAAQ,SAAS,IACxC,YAAY,KAAK,QAAQ,IAAI,OAAK,IAAI,EAAE,QAAQ,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,KAC1E;AAEJ,UAAM,MAAM,UAAU,YAAY,KAAK,IAAI,CAAC,UAAU,SAAS,KAAK,KAAK,IAAI,aAAa;AAC1F,UAAM,OAAO,KAAK,GAAG,IAAI,KAAK,MAAM;AAEpC,WAAO,EAAE,KAAK;AAAA,EAChB;AAAA;AAAA,EAIA,cAAc,WAAmB,OAA6B;AAC5D,UAAM,UAAU,IAAI,MAAM,YAAY,CAAC;AACvC,UAAM,OAAO,KAAK,GAAG,IAAI;AAAA;AAAA;AAAA;AAAA,OAItB,CAAC,WAAW,SAAS,SAAS,OAAO,CAAC;AAEzC,WAAO,KAAK,IAAI,QAAM;AAAA,MACpB,IAAI,EAAE;AAAA,MAAI,WAAW,EAAE;AAAA,MAAY,MAAM,EAAE;AAAA,MAAM,UAAU,EAAE;AAAA,MAC7D,cAAc,EAAE;AAAA,MAAe,UAAU,CAAC,CAAC,EAAE;AAAA,MAC7C,WAAW,EAAE;AAAA,MAAY,SAAS,EAAE;AAAA,MAAU,aAAa,EAAE;AAAA,MAC7D,UAAU,EAAE;AAAA,MAAW,UAAU,EAAE;AAAA,MAAW,WAAW,EAAE;AAAA,MAC3D,cAAc,EAAE,gBAAgB,KAAK,MAAM,EAAE,aAAa,IAAI,CAAC;AAAA,MAC/D,SAAS,EAAE;AAAA,MAAS,WAAW,EAAE;AAAA,IACnC,EAAE;AAAA,EACJ;AAAA;AAAA,EAIA,WAA0E;AACxE,UAAM,WAAW,KAAK,GAAG,IAAI,2CAA2C,GAAG,OAAO;AAClF,UAAM,YAAY,KAAK,GAAG,IAAI,mDAAmD,GAAG,SAAS;AAC7F,UAAM,eAAe,KAAK,GAAG,IAAI,0CAA0C,GAAG,OAAO;AACrF,WAAO,EAAE,UAAU,WAAW,aAAa;AAAA,EAC7C;AAAA;AAAA,EAIA,KAAK,MAQ6C;AAEhD,UAAM,SAAS,KAAK,GAAG,IAAI,4CAA4C,CAAC,KAAK,IAAI,CAAC;AAClF,UAAM,UAAU,KAAK,GAAG,IAAI,4CAA4C,CAAC,KAAK,KAAK,CAAC;AACpF,QAAI,CAAC,OAAQ,QAAO;AACpB,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,YAAY,aAAa,KAAK,KAAK,QAAQ,kBAAkB,GAAG,CAAC;AACvE,UAAM,aAAa,aAAa,KAAK,MAAM,QAAQ,kBAAkB,GAAG,CAAC;AAGzE,UAAM,WAAW,IAAI;AAAA,MAClB,KAAK,GAAG,IAAI,sDAAsD,CAAC,KAAK,IAAI,CAAC,EAAY,IAAI,OAAK,EAAE,IAAI;AAAA,IAC3G;AACA,UAAM,YAAY,IAAI;AAAA,MACnB,KAAK,GAAG,IAAI,sDAAsD,CAAC,KAAK,KAAK,CAAC,EAAY,IAAI,OAAK,EAAE,IAAI;AAAA,IAC5G;AAEA,QAAI,CAAC,SAAS,IAAI,KAAK,UAAU,EAAG,OAAM,IAAI,MAAM,WAAW,KAAK,UAAU,gCAAgC,KAAK,IAAI,GAAG;AAC1H,QAAI,CAAC,UAAU,IAAI,KAAK,WAAW,EAAG,OAAM,IAAI,MAAM,WAAW,KAAK,WAAW,iCAAiC,KAAK,KAAK,GAAG;AAG/H,UAAM,WAAW,KAAK,QAAQ;AAC9B,UAAM,cAAc,aAAa,SAAS,cAAc,aAAa,UAAU,eAAe;AAG9F,QAAI;AACJ,QAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GAAG;AAC3C,mBAAa,KAAK,QAAQ,IAAI,OAAK;AACjC,YAAI,EAAE,WAAW,GAAG,KAAK,IAAI,GAAG,GAAG;AACjC,gBAAM,MAAM,EAAE,MAAM,KAAK,KAAK,SAAS,CAAC;AACxC,cAAI,CAAC,SAAS,IAAI,GAAG,EAAG,OAAM,IAAI,MAAM,oBAAoB,CAAC,GAAG;AAChE,iBAAO,MAAM,IAAI,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,GAAG;AAAA,QAC/D,WAAW,EAAE,WAAW,GAAG,KAAK,KAAK,GAAG,GAAG;AACzC,gBAAM,MAAM,EAAE,MAAM,KAAK,MAAM,SAAS,CAAC;AACzC,cAAI,CAAC,UAAU,IAAI,GAAG,EAAG,OAAM,IAAI,MAAM,oBAAoB,CAAC,GAAG;AACjE,iBAAO,MAAM,IAAI,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,KAAK,IAAI,GAAG;AAAA,QAChE;AAEA,YAAI,SAAS,IAAI,CAAC,EAAG,QAAO,MAAM,EAAE,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC;AAC9E,YAAI,UAAU,IAAI,CAAC,EAAG,QAAO,MAAM,EAAE,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,KAAK,IAAI,CAAC;AAChF,cAAM,IAAI,MAAM,WAAW,CAAC,+BAA+B;AAAA,MAC7D,CAAC,EAAE,KAAK,IAAI;AAAA,IACd,OAAO;AAEL,YAAM,aAAa,MAAM,KAAK,QAAQ,EAAE,IAAI,OAAK,MAAM,EAAE,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC,GAAG;AACtG,YAAM,cAAc,MAAM,KAAK,SAAS,EAAE,IAAI,OAAK,MAAM,EAAE,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,KAAK,IAAI,CAAC,GAAG;AACzG,mBAAa,CAAC,GAAG,YAAY,GAAG,WAAW,EAAE,KAAK,IAAI;AAAA,IACxD;AAEA,UAAM,QAAQ,KAAK,IAAI,KAAK,SAAS,KAAK,GAAG;AAC7C,UAAM,UAAU,MAAM,KAAK,WAAW,QAAQ,MAAM,IAAI,CAAC;AACzD,UAAM,WAAW,MAAM,KAAK,YAAY,QAAQ,MAAM,IAAI,CAAC;AAG3D,UAAM,WAAW,gCAAgC,SAAS,OAAO,WAAW,KAAK,UAAU,UAAU,OAAO,MAAM,QAAQ;AAC1H,UAAM,WAAW,KAAK,GAAG,IAAI,QAAQ;AACrC,UAAM,gBAAgB,UAAU,OAAO;AAGvC,UAAM,MAAM,UAAU,UAAU,UAAU,SAAS,OAAO,WAAW,KAAK,UAAU,UAAU,OAAO,MAAM,QAAQ;AACnH,UAAM,OAAO,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC;AAErC,WAAO,EAAE,MAAM,cAAc;AAAA,EAC/B;AAAA;AAAA,EAIA,aAAa,WAAmB,YAAY,KAA+B;AACzE,UAAM,KAAK,KAAK,GAAG,IAAI,4CAA4C,CAAC,SAAS,CAAC;AAC9E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,YAAY,aAAa,UAAU,QAAQ,kBAAkB,GAAG,CAAC;AAGvE,UAAM,cAAe,KAAK,GAAG;AAAA,MAC3B;AAAA,MACA,CAAC,SAAS;AAAA,IACZ,EAAY,IAAI,OAAK,EAAE,IAAI;AAE3B,QAAI,YAAY,SAAS,EAAG,QAAO,CAAC;AAGpC,UAAM,UAA6B,CAAC;AAEpC,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,eAAS,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC/C,cAAM,OAAO,YAAY,CAAC;AAC1B,cAAM,OAAO,YAAY,CAAC;AAC1B,cAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC;AACvC,cAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC;AAIvC,cAAM,MAAM,KAAK,GAAG,IAAI;AAAA;AAAA;AAAA,uBAGT,EAAE,oBAAoB,EAAE;AAAA,uBACxB,EAAE;AAAA,uBACF,EAAE;AAAA,uBACF,EAAE,oBAAoB,EAAE;AAAA,uBACxB,EAAE,oBAAoB,EAAE;AAAA,kBAC7B,SAAS;AAAA,kBACT,EAAE,oBAAoB,EAAE;AAAA,yBACjB,EAAE,0BAA0B,EAAE;AAAA,SAC9C;AAED,YAAI,CAAC,OAAO,IAAI,IAAI,EAAG;AAEvB,cAAM,IAAI,IAAI;AACd,cAAM,YAAY,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI;AACnD,cAAM,SAAS,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI;AAChD,cAAM,SAAS,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI;AAChD,cAAM,QAAQ,KAAK,KAAK,SAAS,MAAM;AAEvC,YAAI,UAAU,EAAG;AAEjB,cAAM,cAAc,YAAY;AAChC,cAAM,UAAU,KAAK,IAAI,WAAW;AAEpC,YAAI,UAAU,UAAW;AAEzB,cAAM,WACJ,WAAW,MAAM,WACjB,WAAW,MAAM,aACjB,WAAW,MAAM,SAAS;AAE5B,gBAAQ,KAAK,EAAE,SAAS,MAAM,SAAS,MAAM,aAAa,KAAK,MAAM,cAAc,GAAK,IAAI,KAAO,SAAS,CAAC;AAAA,MAC/G;AAAA,IACF;AAGA,YAAQ,KAAK,CAAC,GAAG,MAAM,KAAK,IAAI,EAAE,WAAW,IAAI,KAAK,IAAI,EAAE,WAAW,CAAC;AACxE,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,WAAW,WAAmB,QAAQ,IAQ5B;AACR,UAAM,KAAK,KAAK,GAAG,IAAI,6CAA6C,CAAC,SAAS,CAAC;AAC/E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,MACA,CAAC,WAAW,KAAK;AAAA,IACnB;AAEA,WAAO,KAAK,IAAI,QAAM;AAAA,MACpB,IAAI,EAAE;AAAA,MACN,WAAW,EAAE;AAAA,MACb,YAAY,EAAE;AAAA,MACd,UAAU,EAAE;AAAA,MACZ,aAAa,EAAE;AAAA,MACf,SAAS,KAAK,MAAM,EAAE,YAAY;AAAA,MAClC,aAAa,EAAE;AAAA,IACjB,EAAE;AAAA,EACJ;AAAA;AAAA,EAIA,YAAY,WAMH;AACP,UAAM,KAAK,KAAK,GAAG,IAAI,6CAA6C,CAAC,SAAS,CAAC;AAC/E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,YAAY,KAAK,GAAG;AAAA,MACxB;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAEA,QAAI,UAAU,SAAS,GAAG;AACxB,aAAO,EAAE,UAAU,OAAO,cAAc,CAAC,GAAG,gBAAgB,CAAC,GAAG,gBAAgB,CAAC,GAAG,eAAe,EAAE;AAAA,IACvG;AAEA,UAAM,SAAS,UAAU,CAAC;AAC1B,UAAM,WAAW,UAAU,CAAC;AAC5B,UAAM,aAA0F,KAAK,MAAM,OAAO,YAAY;AAC9H,UAAM,WAAwF,KAAK,MAAM,SAAS,YAAY;AAE9H,UAAM,YAAY,IAAI,IAAI,WAAW,IAAI,OAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1D,UAAM,UAAU,IAAI,IAAI,SAAS,IAAI,OAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtD,UAAM,eAAyB,CAAC;AAChC,UAAM,iBAA2B,CAAC;AAClC,UAAM,iBAA6D,CAAC;AAGpE,eAAW,OAAO,YAAY;AAC5B,UAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,EAAG,cAAa,KAAK,IAAI,IAAI;AAAA,IACxD;AAGA,eAAW,OAAO,UAAU;AAC1B,UAAI,CAAC,UAAU,IAAI,IAAI,IAAI,EAAG,gBAAe,KAAK,IAAI,IAAI;AAAA,IAC5D;AAGA,eAAW,OAAO,YAAY;AAC5B,YAAM,OAAO,QAAQ,IAAI,IAAI,IAAI;AACjC,UAAI,CAAC,KAAM;AACX,YAAM,UAAoB,CAAC;AAC3B,UAAI,IAAI,SAAS,KAAK,KAAM,SAAQ,KAAK,SAAS,KAAK,IAAI,WAAM,IAAI,IAAI,EAAE;AAC3E,UAAI,IAAI,gBAAgB,KAAK,YAAa,SAAQ,KAAK,gBAAgB,KAAK,WAAW,WAAM,IAAI,WAAW,EAAE;AAC9G,UAAI,KAAK,IAAI,IAAI,UAAU,KAAK,OAAO,IAAI,KAAM,SAAQ,KAAK,YAAY,KAAK,QAAQ,QAAQ,CAAC,CAAC,YAAO,IAAI,QAAQ,QAAQ,CAAC,CAAC,GAAG;AACjI,UAAI,QAAQ,SAAS,EAAG,gBAAe,KAAK,EAAE,MAAM,IAAI,MAAM,QAAQ,CAAC;AAAA,IACzE;AAEA,UAAM,gBAAgB,OAAO,YAAY,SAAS;AAClD,UAAM,WAAW,aAAa,SAAS,KAAK,eAAe,SAAS,KAAK,eAAe,SAAS;AAEjG,WAAO,EAAE,UAAU,cAAc,gBAAgB,gBAAgB,cAAc;AAAA,EACjF;AAAA;AAAA,EAIA,QAAQ,WAA2C;AACjD,UAAM,KAAK,KAAK,GAAG,IAAI,4CAA4C,CAAC,SAAS,CAAC;AAC9E,QAAI,CAAC,GAAI,QAAO;AAEhB,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAEA,QAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAE/B,UAAM,UAA2B,CAAC;AAGlC,QAAI,GAAG,WAAW,OAAO;AACvB,YAAM,YAAY,aAAa,UAAU,QAAQ,kBAAkB,GAAG,CAAC;AACvE,UAAI;AACF,cAAM,SAAS,KAAK,GAAG,IAAI,gCAAgC,SAAS,4BAA4B;AAChG,cAAM,WAAW,QAAQ,OAAO;AAChC,YAAI,WAAW,GAAG;AAChB,gBAAM,aAAa,GAAG,aAAa;AACnC,kBAAQ,KAAK;AAAA,YACX,QAAQ;AAAA,YACR,WAAW,KAAK,IAAI,WAAW,KAAK,IAAI,YAAY,CAAC,GAAG,CAAG;AAAA,YAC3D,UAAU;AAAA,YACV,iBAAiB;AAAA,YACjB,UAAU;AAAA,YACV,QAAQ,CAAC,GAAG,QAAQ,OAAO,UAAU,8CAA8C;AAAA,UACrF,CAAC;AAAA,QACH;AAAA,MACF,QAAQ;AAAA,MAER;AAEA,UAAI,GAAG,eAAe;AACpB,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,GAAG,aAAa;AACxC,cAAI,KAAK,mBAAmB;AAC1B,oBAAQ,KAAK;AAAA,cACX,QAAQ;AAAA,cACR,WAAW;AAAA,cACX,UAAU;AAAA,cACV,iBAAiB;AAAA,cACjB,UAAU;AAAA,cACV,QAAQ,CAAC,4FAAuF;AAAA,YAClG,CAAC;AAAA,UACH;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,eAAW,OAAO,MAAM;AACtB,YAAM,SAAmB,CAAC;AAC1B,UAAI,WAAW;AACf,UAAI,kBAAkB;AACtB,UAAI,WAAW;AAGf,YAAM,UAAU,IAAI,YAAY;AAChC,UAAI,UAAU,IAAI;AAChB,eAAO,KAAK,mBAAmB,QAAQ,QAAQ,CAAC,CAAC,IAAI;AACrD,mBAAW;AAAA,MACb,WAAW,UAAU,IAAI;AACvB,eAAO,KAAK,uBAAuB,QAAQ,QAAQ,CAAC,CAAC,IAAI;AACzD,mBAAW;AAAA,MACb,WAAW,UAAU,GAAG;AACtB,eAAO,KAAK,eAAe,QAAQ,QAAQ,CAAC,CAAC,IAAI;AACjD,mBAAW;AAAA,MACb;AAGA,YAAM,cAAc,IAAI,eAAe;AACvC,YAAM,WAAW,GAAG,aAAa;AAEjC,UAAI,gBAAgB,KAAK,WAAW,IAAI;AACtC,eAAO,KAAK,gCAAgC;AAC5C,0BAAkB;AAAA,MACpB,WAAW,cAAc,WAAW,QAAQ,WAAW,KAAK;AAC1D,eAAO,KAAK,6BAA6B;AACzC,0BAAkB;AAAA,MACpB;AAGA,UAAI,IAAI,kBAAkB,YAAY,IAAI,cAAc,MAAM,WAAW,KAAK;AAC5E,eAAO,KAAK,iDAAiD;AAC7D,mBAAW;AAAA,MACb;AAEA,YAAM,YAAY,KAAK,IAAI,WAAW,kBAAkB,UAAU,CAAG;AAErE,UAAI,OAAO,SAAS,GAAG;AACrB,gBAAQ,KAAK,EAAE,QAAQ,IAAI,MAAM,WAAW,UAAU,iBAAiB,UAAU,OAAO,CAAC;AAAA,MAC3F;AAAA,IACF;AAGA,YAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;AAChD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,WAA4C;AAC1D,UAAM,OAAO,KAAK,gBAAgB,SAAS;AAC3C,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,UAA4B,CAAC;AAEnC,eAAW,OAAO,KAAK,SAAS;AAC9B,YAAM,QAAkB,CAAC;AAGzB,UAAI,CAAC,IAAI,UAAU;AACjB,cAAM,KAAK,uBAAuB;AAAA,MACpC,WAAW,IAAI,UAAU,MAAM;AAC7B,cAAM,KAAK,4CAAuC;AAAA,MACpD;AAGA,YAAM,KAAK,SAAS,IAAI,YAAY,EAAE;AAGtC,WAAK,IAAI,iBAAiB,aAAa,IAAI,iBAAiB,YAAY,IAAI,YAAY,QAAQ,IAAI,YAAY,MAAM;AACpH,cAAM,KAAK,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,EAAE;AAAA,MACxD;AAGA,UAAI,IAAI,iBAAiB,YAAY,IAAI,eAAe,MAAM,KAAK,QAAQ,WAAW,IAAI;AACxF,cAAM,KAAK,SAAS,IAAI,WAAW,sBAAsB,IAAI,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,MACxF;AAGA,UAAI,IAAI,gBAAgB,KAAK,QAAQ,YAAY,KAAK,QAAQ,WAAW,IAAI;AAC3E,cAAM,KAAK,8BAA8B;AAAA,MAC3C;AAEA,UAAI,MAAM,SAAS,GAAG;AACpB,gBAAQ,KAAK,EAAE,QAAQ,IAAI,MAAM,MAAM,CAAC;AAAA,MAC1C;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,WAAwC;AAClD,UAAM,OAAO,KAAK,gBAAgB,SAAS;AAC3C,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,UAAwB,CAAC;AAE/B,eAAW,OAAO,KAAK,SAAS;AAC9B,UAAI;AAEJ,cAAQ,IAAI,cAAc;AAAA,QACxB,KAAK;AACH,cAAI,IAAI,YAAY,QAAQ,IAAI,YAAY,MAAM;AAChD,mBAAO,0BAA0B,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAAA,UACnE,OAAO;AACL,mBAAO;AAAA,UACT;AACA,cAAI,IAAI,gBAAgB,KAAK,QAAQ,SAAU,SAAQ;AACvD;AAAA,QAEF,KAAK;AACH,cAAI,IAAI,YAAY,QAAQ,IAAI,YAAY,MAAM;AAChD,mBAAO,wBAAwB,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAC/D,gBAAI,IAAI,aAAa,KAAM,SAAQ,iBAAY,IAAI,UAAU,QAAQ,CAAC,CAAC;AAAA,UACzE,OAAO;AACL,mBAAO;AAAA,UACT;AACA;AAAA,QAEF,KAAK;AACH,iBAAO;AACP;AAAA,QAEF,KAAK;AACH,cAAI,IAAI,YAAY,IAAI,UAAU;AAChC,mBAAO,uBAAuB,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AAAA,UAChE,OAAO;AACL,mBAAO;AAAA,UACT;AACA;AAAA,QAEF,KAAK;AACH,cAAI,IAAI,eAAe,MAAM,KAAK,QAAQ,WAAW,IAAI;AACvD,mBAAO,cAAc,IAAI,aAAa,KAAK,IAAI,CAAC;AAAA,UAClD,WAAW,IAAI,gBAAgB,KAAK,QAAQ,UAAU;AAEpD,kBAAM,OAAO,IAAI,KAAK,YAAY;AAClC,gBAAI,KAAK,SAAS,OAAO,EAAG,QAAO;AAAA,qBAC1B,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,MAAM,EAAG,QAAO;AAAA,qBACrD,KAAK,SAAS,MAAM,EAAG,QAAO;AAAA,qBAC9B,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,EAAG,QAAO;AAAA,gBAC1D,QAAO,2BAA2B,IAAI,WAAW;AAAA,UACxD,OAAO;AACL,mBAAO,kBAAkB,IAAI,WAAW;AACxC,gBAAI,IAAI,aAAa,SAAS,EAAG,SAAQ,eAAe,IAAI,aAAa,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,UACjG;AACA;AAAA,QAEF;AACE,iBAAO,YAAY,IAAI,YAAY;AAAA,MACvC;AAEA,UAAI,IAAI,YAAY,IAAI,UAAU,MAAM;AACtC,gBAAQ,YAAO,IAAI,UAAU,KAAK,QAAQ,CAAC,CAAC;AAAA,MAC9C;AAEA,cAAQ,KAAK,EAAE,QAAQ,IAAI,MAAM,KAAK,CAAC;AAAA,IACzC;AAEA,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/data/types.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph Data Module \u2014 Type Definitions\n *\n * Types for tabular data indexing, profiling, querying, and aggregation.\n * All populated only when enableData=true in config.\n */\n\n// \u2500\u2500 Dataset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataSet {\n id: string; // derived from file path (slugified)\n filePath: string; // relative path to data file\n format: 'csv' | 'tsv' | 'jsonl' | 'json' | 'xlsx' | 'parquet';\n rowCount: number;\n columnCount: number;\n fileSize: number; // bytes\n contentHash: string; // SHA-256 for incremental detection\n summary: string | null; // auto-generated NL summary\n indexedAt: number;\n}\n\n// \u2500\u2500 Column Profile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type InferredType = 'string' | 'integer' | 'float' | 'boolean' | 'date' | 'null' | 'mixed';\n\nexport interface DataColumn {\n id: string; // {dataset}::{column_name}#column\n datasetId: string;\n name: string;\n position: number; // 0-based column order\n inferredType: InferredType;\n nullable: boolean;\n nullCount: number;\n nullPct: number; // 0.0\u20131.0\n cardinality: number; // distinct value count\n minValue: string | null;\n maxValue: string | null;\n meanValue: number | null;\n sampleValues: string[]; // up to 5 sample values\n summary: string | null; // NL description\n updatedAt: number;\n}\n\n// \u2500\u2500 Query Filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface QueryFilter {\n column: string;\n op: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in' | 'is_null' | 'between';\n value: any;\n}\n\n// \u2500\u2500 Aggregate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type AggregateOp = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct';\n\nexport interface AggregateMetric {\n column: string;\n op: AggregateOp;\n alias?: string; // output column name\n}\n\n// \u2500\u2500 Join \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface JoinCondition {\n leftColumn: string;\n rightColumn: string;\n}\n\nexport type JoinType = 'inner' | 'left' | 'right';\n\n// \u2500\u2500 Correlation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CorrelationPair {\n column1: string;\n column2: string;\n correlation: number; // -1.0 to 1.0\n strength: 'strong' | 'moderate' | 'weak' | 'negligible';\n}\n\n// \u2500\u2500 Code Reference \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type DataRefType = 'reads' | 'writes' | 'imports' | 'configures';\n\nexport interface DataCodeRef {\n datasetId: string;\n qualifiedName: string; // stable across reindex\n refType: DataRefType;\n confidence: number; // 0.0\u20131.0\n}\n\n// \u2500\u2500 Parser Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedRow {\n [column: string]: string | number | boolean | null;\n}\n\nexport interface ParseResult {\n columns: string[]; // column names in order\n rows: ParsedRow[]; // parsed rows (may be partial if streaming)\n format: string;\n totalRows: number;\n}\n\n// \u2500\u2500 Parser Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataFormatParser {\n name: string;\n extensions: string[];\n /** Check if the optional dependency is available */\n isAvailable(): boolean;\n /** Parse a file, yielding rows in batches via callback */\n parse(filePath: string, opts: { maxRows: number; onBatch: (rows: ParsedRow[], columns: string[]) => void }): Promise<{ columns: string[]; totalRows: number }>;\n}\n\n// \u2500\u2500 Index Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataIndexResult {\n datasetsIndexed: number;\n rowsIndexed: number;\n columnsProfiled: number;\n errors: string[];\n duration: number;\n}\n\n// \u2500\u2500 Quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ColumnQuality {\n column: string;\n riskScore: number; // 0.0\u20131.0 composite risk\n nullRisk: number; // contribution from null rate\n cardinalityRisk: number; // contribution from cardinality anomalies\n typeRisk: number; // contribution from mixed types\n issues: string[]; // human-readable issue descriptions\n}\n"],
4
+ "sourcesContent": ["/**\n * KiroGraph Data Module \u2014 Type Definitions\n *\n * Types for tabular data indexing, profiling, querying, and aggregation.\n * All populated only when enableData=true in config.\n */\n\n// \u2500\u2500 Dataset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataSet {\n id: string; // derived from file path (slugified)\n filePath: string; // relative path to data file\n format: 'csv' | 'tsv' | 'jsonl' | 'json' | 'xlsx' | 'parquet' | 'pdf';\n rowCount: number;\n columnCount: number;\n fileSize: number; // bytes\n contentHash: string; // SHA-256 for incremental detection\n summary: string | null; // auto-generated NL summary\n indexedAt: number;\n metadataJson?: string | null; // PDF-specific metadata blob (null for non-PDF formats)\n}\n\n// \u2500\u2500 Column Profile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type InferredType = 'string' | 'integer' | 'float' | 'boolean' | 'date' | 'null' | 'mixed';\n\nexport interface DataColumn {\n id: string; // {dataset}::{column_name}#column\n datasetId: string;\n name: string;\n position: number; // 0-based column order\n inferredType: InferredType;\n nullable: boolean;\n nullCount: number;\n nullPct: number; // 0.0\u20131.0\n cardinality: number; // distinct value count\n minValue: string | null;\n maxValue: string | null;\n meanValue: number | null;\n sampleValues: string[]; // up to 5 sample values\n summary: string | null; // NL description\n updatedAt: number;\n}\n\n// \u2500\u2500 Query Filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface QueryFilter {\n column: string;\n op: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in' | 'is_null' | 'between';\n value: any;\n}\n\n// \u2500\u2500 Aggregate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type AggregateOp = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct';\n\nexport interface AggregateMetric {\n column: string;\n op: AggregateOp;\n alias?: string; // output column name\n}\n\n// \u2500\u2500 Join \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface JoinCondition {\n leftColumn: string;\n rightColumn: string;\n}\n\nexport type JoinType = 'inner' | 'left' | 'right';\n\n// \u2500\u2500 Correlation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CorrelationPair {\n column1: string;\n column2: string;\n correlation: number; // -1.0 to 1.0\n strength: 'strong' | 'moderate' | 'weak' | 'negligible';\n}\n\n// \u2500\u2500 Code Reference \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type DataRefType = 'reads' | 'writes' | 'imports' | 'configures';\n\nexport interface DataCodeRef {\n datasetId: string;\n qualifiedName: string; // stable across reindex\n refType: DataRefType;\n confidence: number; // 0.0\u20131.0\n}\n\n// \u2500\u2500 Parser Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedRow {\n [column: string]: string | number | boolean | null;\n}\n\nexport interface ParseResult {\n columns: string[]; // column names in order\n rows: ParsedRow[]; // parsed rows (may be partial if streaming)\n format: string;\n totalRows: number;\n}\n\n// \u2500\u2500 Parser Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataFormatParser {\n name: string;\n extensions: string[];\n /** Check if the optional dependency is available */\n isAvailable(): boolean;\n /** Parse a file, yielding rows in batches via callback */\n parse(filePath: string, opts: { maxRows: number; onBatch: (rows: ParsedRow[], columns: string[]) => void }): Promise<{ columns: string[]; totalRows: number; metadataJson?: string | null }>;\n}\n\n// \u2500\u2500 Index Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataIndexResult {\n datasetsIndexed: number;\n rowsIndexed: number;\n columnsProfiled: number;\n errors: string[];\n duration: number;\n}\n\n// \u2500\u2500 Quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ColumnQuality {\n column: string;\n riskScore: number; // 0.0\u20131.0 composite risk\n nullRisk: number; // contribution from null rate\n cardinalityRisk: number; // contribution from cardinality anomalies\n typeRisk: number; // contribution from mixed types\n issues: string[]; // human-readable issue descriptions\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -10,7 +10,8 @@ CREATE TABLE IF NOT EXISTS data_datasets (
10
10
  file_size INTEGER NOT NULL,
11
11
  content_hash TEXT NOT NULL,
12
12
  summary TEXT,
13
- indexed_at INTEGER NOT NULL
13
+ indexed_at INTEGER NOT NULL,
14
+ metadata_json TEXT
14
15
  );
15
16
 
16
17
  CREATE INDEX IF NOT EXISTS idx_data_datasets_file ON data_datasets(file_path);
@@ -99,6 +99,13 @@ Or delete the lock manually: ${lockPath}`
99
99
  const sql = fs.readFileSync(schemaPath, "utf8");
100
100
  this.db.exec(sql);
101
101
  }
102
+ const tryAlter = (stmt) => {
103
+ try {
104
+ this.db.run(stmt);
105
+ } catch {
106
+ }
107
+ };
108
+ tryAlter("ALTER TABLE data_datasets ADD COLUMN metadata_json TEXT");
102
109
  }
103
110
  /**
104
111
  * Apply patterns schema tables. Called when enablePatterns is true.