@dcyfr/ai-notebooks 1.0.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 (51) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.env.example +21 -0
  4. package/.github/workflows/ci.yml +33 -0
  5. package/.github/workflows/release.yml +82 -0
  6. package/AGENTS.md +38 -0
  7. package/CHANGELOG.md +58 -0
  8. package/CONTRIBUTING.md +34 -0
  9. package/LICENSE +21 -0
  10. package/README.md +134 -0
  11. package/SECURITY.md +924 -0
  12. package/docs/API.md +1775 -0
  13. package/docs/ARCHITECTURE.md +70 -0
  14. package/docs/DEVELOPMENT.md +70 -0
  15. package/docs/plans/PROMOTION_CHECKLIST_DCYFR_AI_NOTEBOOKS_2026-02-08.md +293 -0
  16. package/eslint.config.mjs +23 -0
  17. package/examples/data-exploration/index.ts +95 -0
  18. package/examples/data-pipeline/index.ts +111 -0
  19. package/examples/model-analysis/index.ts +118 -0
  20. package/package.json +57 -0
  21. package/src/index.ts +208 -0
  22. package/src/notebook/cell.ts +149 -0
  23. package/src/notebook/index.ts +50 -0
  24. package/src/notebook/notebook.ts +232 -0
  25. package/src/notebook/runner.ts +141 -0
  26. package/src/pipeline/dataset.ts +220 -0
  27. package/src/pipeline/index.ts +60 -0
  28. package/src/pipeline/runner.ts +195 -0
  29. package/src/pipeline/statistics.ts +182 -0
  30. package/src/pipeline/transform.ts +187 -0
  31. package/src/types/index.ts +301 -0
  32. package/src/utils/csv.ts +106 -0
  33. package/src/utils/format.ts +78 -0
  34. package/src/utils/index.ts +37 -0
  35. package/src/utils/validation.ts +142 -0
  36. package/src/visualization/chart.ts +149 -0
  37. package/src/visualization/formatter.ts +140 -0
  38. package/src/visualization/index.ts +34 -0
  39. package/src/visualization/themes.ts +60 -0
  40. package/tests/cell.test.ts +158 -0
  41. package/tests/dataset.test.ts +159 -0
  42. package/tests/notebook.test.ts +168 -0
  43. package/tests/pipeline.test.ts +158 -0
  44. package/tests/runner.test.ts +168 -0
  45. package/tests/statistics.test.ts +162 -0
  46. package/tests/transform.test.ts +165 -0
  47. package/tests/types.test.ts +258 -0
  48. package/tests/utils.test.ts +257 -0
  49. package/tests/visualization.test.ts +224 -0
  50. package/tsconfig.json +19 -0
  51. package/vitest.config.ts +19 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Chart Builder - Chart specification creation
3
+ */
4
+
5
+ import type { ChartConfig, ChartSpec, ChartType, DataSeries } from '../types/index.js';
6
+
7
+ /**
8
+ * Create a chart spec
9
+ */
10
+ export function createChart(
11
+ type: ChartType,
12
+ title: string,
13
+ options?: Partial<ChartConfig>
14
+ ): ChartSpec {
15
+ return {
16
+ config: {
17
+ type,
18
+ title,
19
+ xLabel: options?.xLabel ?? '',
20
+ yLabel: options?.yLabel ?? '',
21
+ width: options?.width ?? 800,
22
+ height: options?.height ?? 400,
23
+ colors: options?.colors,
24
+ theme: options?.theme ?? 'dcyfr',
25
+ showLegend: options?.showLegend ?? true,
26
+ showGrid: options?.showGrid ?? true,
27
+ },
28
+ series: [],
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Add a data series to a chart
34
+ */
35
+ export function addSeries(chart: ChartSpec, series: DataSeries): ChartSpec {
36
+ return {
37
+ ...chart,
38
+ series: [...chart.series, series],
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Create a data series
44
+ */
45
+ export function createSeries(
46
+ name: string,
47
+ data: Array<{ x: number | string; y: number }>,
48
+ color?: string
49
+ ): DataSeries {
50
+ return { name, data, color };
51
+ }
52
+
53
+ /**
54
+ * Shortcut: create a bar chart
55
+ */
56
+ export function barChart(
57
+ title: string,
58
+ labels: string[],
59
+ values: number[],
60
+ options?: Partial<ChartConfig>
61
+ ): ChartSpec {
62
+ const chart = createChart('bar', title, options);
63
+ const data = labels.map((label, i) => ({ x: label, y: values[i] ?? 0 }));
64
+ return addSeries(chart, createSeries(title, data));
65
+ }
66
+
67
+ /**
68
+ * Shortcut: create a line chart
69
+ */
70
+ export function lineChart(
71
+ title: string,
72
+ xValues: number[],
73
+ yValues: number[],
74
+ options?: Partial<ChartConfig>
75
+ ): ChartSpec {
76
+ const chart = createChart('line', title, options);
77
+ const data = xValues.map((x, i) => ({ x, y: yValues[i] ?? 0 }));
78
+ return addSeries(chart, createSeries(title, data));
79
+ }
80
+
81
+ /**
82
+ * Shortcut: create a scatter plot
83
+ */
84
+ export function scatterPlot(
85
+ title: string,
86
+ points: Array<{ x: number; y: number }>,
87
+ options?: Partial<ChartConfig>
88
+ ): ChartSpec {
89
+ const chart = createChart('scatter', title, options);
90
+ return addSeries(chart, createSeries(title, points));
91
+ }
92
+
93
+ /**
94
+ * Shortcut: create a pie chart
95
+ */
96
+ export function pieChart(
97
+ title: string,
98
+ labels: string[],
99
+ values: number[],
100
+ options?: Partial<ChartConfig>
101
+ ): ChartSpec {
102
+ const chart = createChart('pie', title, options);
103
+ const data = labels.map((label, i) => ({ x: label, y: values[i] ?? 0 }));
104
+ return addSeries(chart, createSeries(title, data));
105
+ }
106
+
107
+ /**
108
+ * Shortcut: create a histogram
109
+ */
110
+ export function histogram(
111
+ title: string,
112
+ values: number[],
113
+ bins = 10,
114
+ options?: Partial<ChartConfig>
115
+ ): ChartSpec {
116
+ const chart = createChart('histogram', title, options);
117
+
118
+ if (values.length === 0) return chart;
119
+
120
+ const min = Math.min(...values);
121
+ const max = Math.max(...values);
122
+ const binWidth = (max - min) / bins;
123
+
124
+ const counts = new Array(bins).fill(0) as number[];
125
+ for (const v of values) {
126
+ const idx = Math.min(Math.floor((v - min) / binWidth), bins - 1);
127
+ counts[idx]++;
128
+ }
129
+
130
+ const data = counts.map((count, i) => ({
131
+ x: Number((min + i * binWidth + binWidth / 2).toFixed(2)),
132
+ y: count,
133
+ }));
134
+
135
+ return addSeries(chart, createSeries(title, data));
136
+ }
137
+
138
+ /**
139
+ * Update chart configuration
140
+ */
141
+ export function updateChartConfig(
142
+ chart: ChartSpec,
143
+ updates: Partial<ChartConfig>
144
+ ): ChartSpec {
145
+ return {
146
+ ...chart,
147
+ config: { ...chart.config, ...updates },
148
+ };
149
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Formatter - Text-based rendering of charts and tables
3
+ */
4
+
5
+ import type { ChartSpec, Dataset, DescriptiveStats } from '../types/index.js';
6
+
7
+ /**
8
+ * Render a text-based horizontal bar chart
9
+ */
10
+ export function renderBarChart(chart: ChartSpec, maxWidth = 50): string {
11
+ const lines: string[] = [];
12
+ lines.push(` ${chart.config.title}`);
13
+ lines.push(' ' + '─'.repeat(maxWidth + 15));
14
+
15
+ for (const series of chart.series) {
16
+ for (const point of series.data) {
17
+ const maxVal = Math.max(...series.data.map((d) => d.y));
18
+ const barLen = maxVal > 0 ? Math.round((point.y / maxVal) * maxWidth) : 0;
19
+ const label = String(point.x).padEnd(12);
20
+ const bar = '█'.repeat(barLen);
21
+ lines.push(` ${label} │${bar} ${point.y}`);
22
+ }
23
+ }
24
+
25
+ lines.push(' ' + '─'.repeat(maxWidth + 15));
26
+ return lines.join('\n');
27
+ }
28
+
29
+ /**
30
+ * Render data as a text table
31
+ */
32
+ export function renderTable(
33
+ headers: string[],
34
+ rows: (string | number | null | undefined)[][],
35
+ options?: { maxColWidth?: number; align?: 'left' | 'right' | 'center' }
36
+ ): string {
37
+ const maxCol = options?.maxColWidth ?? 20;
38
+ const align = options?.align ?? 'left';
39
+
40
+ // Calculate column widths
41
+ const widths = headers.map((h) => Math.min(h.length, maxCol));
42
+ for (const row of rows) {
43
+ for (let i = 0; i < row.length; i++) {
44
+ const len = String(row[i] ?? '').length;
45
+ widths[i] = Math.min(Math.max(widths[i] ?? 0, len), maxCol);
46
+ }
47
+ }
48
+
49
+ const pad = (val: string, width: number): string => {
50
+ const s = val.slice(0, width);
51
+ if (align === 'right') return s.padStart(width);
52
+ if (align === 'center') {
53
+ const left = Math.floor((width - s.length) / 2);
54
+ return ' '.repeat(left) + s + ' '.repeat(width - s.length - left);
55
+ }
56
+ return s.padEnd(width);
57
+ };
58
+
59
+ const headerLine = headers.map((h, i) => pad(h, widths[i])).join(' │ ');
60
+ const separator = widths.map((w) => '─'.repeat(w)).join('─┼─');
61
+
62
+ const dataLines = rows.map((row) =>
63
+ row.map((val, i) => pad(String(val ?? ''), widths[i])).join(' │ ')
64
+ );
65
+
66
+ return [headerLine, separator, ...dataLines].join('\n');
67
+ }
68
+
69
+ /**
70
+ * Render a dataset as a text table
71
+ */
72
+ export function renderDatasetTable(dataset: Dataset, maxRows = 20): string {
73
+ const headers = dataset.metadata.columns.map((c) => c.name);
74
+ const displayRows = dataset.rows.slice(0, maxRows);
75
+ const tableRows = displayRows.map((row) =>
76
+ headers.map((h) => row[h] as string | number | null | undefined)
77
+ );
78
+
79
+ let result = renderTable(headers, tableRows);
80
+
81
+ if (dataset.rows.length > maxRows) {
82
+ result += `\n... ${dataset.rows.length - maxRows} more rows`;
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Render descriptive statistics as a table
90
+ */
91
+ export function renderStatsTable(stats: DescriptiveStats[]): string {
92
+ const headers = ['Column', 'Count', 'Mean', 'Median', 'StdDev', 'Min', 'Max', 'Nulls'];
93
+ const rows = stats.map((s) => [
94
+ s.column ?? '',
95
+ s.count,
96
+ Number(s.mean.toFixed(2)),
97
+ Number(s.median.toFixed(2)),
98
+ Number(s.stddev.toFixed(2)),
99
+ s.min,
100
+ s.max,
101
+ s.nullCount,
102
+ ]);
103
+ return renderTable(headers, rows, { align: 'right' });
104
+ }
105
+
106
+ /**
107
+ * Render a sparkline from values
108
+ */
109
+ export function sparkline(values: number[]): string {
110
+ if (values.length === 0) return '';
111
+ const chars = '▁▂▃▄▅▆▇█';
112
+ const min = Math.min(...values);
113
+ const max = Math.max(...values);
114
+ const range = max - min || 1;
115
+
116
+ return values
117
+ .map((v) => {
118
+ const idx = Math.min(Math.floor(((v - min) / range) * (chars.length - 1)), chars.length - 1);
119
+ return chars[idx];
120
+ })
121
+ .join('');
122
+ }
123
+
124
+ /**
125
+ * Format a number with locale-aware formatting
126
+ */
127
+ export function formatNumber(value: number, decimals = 2): string {
128
+ return value.toFixed(decimals);
129
+ }
130
+
131
+ /**
132
+ * Render a simple text-based summary
133
+ */
134
+ export function renderSummary(title: string, items: Record<string, unknown>): string {
135
+ const lines = [` ${title}`, ' ' + '─'.repeat(40)];
136
+ for (const [key, value] of Object.entries(items)) {
137
+ lines.push(` ${key.padEnd(20)} ${value}`);
138
+ }
139
+ return lines.join('\n');
140
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Visualization module barrel exports
3
+ */
4
+
5
+ export {
6
+ createChart,
7
+ addSeries,
8
+ createSeries,
9
+ barChart,
10
+ lineChart,
11
+ scatterPlot,
12
+ pieChart,
13
+ histogram,
14
+ updateChartConfig,
15
+ } from './chart.js';
16
+
17
+ export {
18
+ renderBarChart,
19
+ renderTable,
20
+ renderDatasetTable,
21
+ renderStatsTable,
22
+ sparkline,
23
+ formatNumber,
24
+ renderSummary,
25
+ } from './formatter.js';
26
+
27
+ export {
28
+ themes,
29
+ getTheme,
30
+ getSeriesColor,
31
+ registerTheme,
32
+ } from './themes.js';
33
+
34
+ export type { ChartTheme } from './themes.js';
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Themes - Chart theme definitions
3
+ */
4
+
5
+ export interface ChartTheme {
6
+ name: string;
7
+ background: string;
8
+ foreground: string;
9
+ gridColor: string;
10
+ colors: string[];
11
+ fontFamily: string;
12
+ }
13
+
14
+ export const themes: Record<string, ChartTheme> = {
15
+ dcyfr: {
16
+ name: 'dcyfr',
17
+ background: '#0a0a0a',
18
+ foreground: '#fafafa',
19
+ gridColor: '#262626',
20
+ colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'],
21
+ fontFamily: 'Inter, system-ui, sans-serif',
22
+ },
23
+ light: {
24
+ name: 'light',
25
+ background: '#ffffff',
26
+ foreground: '#171717',
27
+ gridColor: '#e5e5e5',
28
+ colors: ['#2563eb', '#059669', '#d97706', '#dc2626', '#7c3aed', '#db2777'],
29
+ fontFamily: 'Inter, system-ui, sans-serif',
30
+ },
31
+ dark: {
32
+ name: 'dark',
33
+ background: '#171717',
34
+ foreground: '#f5f5f5',
35
+ gridColor: '#404040',
36
+ colors: ['#60a5fa', '#34d399', '#fbbf24', '#f87171', '#a78bfa', '#f472b6'],
37
+ fontFamily: 'Inter, system-ui, sans-serif',
38
+ },
39
+ };
40
+
41
+ /**
42
+ * Get a theme by name
43
+ */
44
+ export function getTheme(name: string): ChartTheme {
45
+ return themes[name] ?? themes.dcyfr;
46
+ }
47
+
48
+ /**
49
+ * Get color for a series index from a theme
50
+ */
51
+ export function getSeriesColor(theme: ChartTheme, index: number): string {
52
+ return theme.colors[index % theme.colors.length];
53
+ }
54
+
55
+ /**
56
+ * Register a custom theme
57
+ */
58
+ export function registerTheme(theme: ChartTheme): void {
59
+ themes[theme.name] = theme;
60
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Tests for notebook/cell.ts
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ createCell,
8
+ codeCell,
9
+ markdownCell,
10
+ rawCell,
11
+ createOutput,
12
+ addOutput,
13
+ clearOutputs,
14
+ markRunning,
15
+ markCompleted,
16
+ markError,
17
+ hasOutputs,
18
+ filterCodeCells,
19
+ filterByTag,
20
+ getSourceLines,
21
+ countLines,
22
+ } from '../src/notebook/index.js';
23
+
24
+ describe('Cell Creation', () => {
25
+ it('creates a cell with defaults', () => {
26
+ const cell = createCell('code', 'console.log("hi")');
27
+ expect(cell.type).toBe('code');
28
+ expect(cell.source).toBe('console.log("hi")');
29
+ expect(cell.status).toBe('idle');
30
+ expect(cell.outputs).toEqual([]);
31
+ expect(cell.executionCount).toBeUndefined();
32
+ expect(cell.id).toBeTruthy();
33
+ });
34
+
35
+ it('creates a code cell shortcut', () => {
36
+ const cell = codeCell('const x = 1;');
37
+ expect(cell.type).toBe('code');
38
+ expect(cell.source).toBe('const x = 1;');
39
+ });
40
+
41
+ it('creates a markdown cell shortcut', () => {
42
+ const cell = markdownCell('# Title');
43
+ expect(cell.type).toBe('markdown');
44
+ expect(cell.source).toBe('# Title');
45
+ });
46
+
47
+ it('creates a raw cell shortcut', () => {
48
+ const cell = rawCell('raw content');
49
+ expect(cell.type).toBe('raw');
50
+ });
51
+
52
+ it('creates a cell with tags', () => {
53
+ const cell = createCell('code', 'x', { tags: ['important', 'setup'] });
54
+ expect(cell.tags).toEqual(['important', 'setup']);
55
+ });
56
+
57
+ it('creates a cell with metadata', () => {
58
+ const cell = createCell('code', 'x', { metadata: { custom: 'value' } });
59
+ expect(cell.metadata).toEqual({ custom: 'value' });
60
+ });
61
+ });
62
+
63
+ describe('Cell Outputs', () => {
64
+ it('creates an output', () => {
65
+ const output = createOutput('text', 'Hello');
66
+ expect(output.type).toBe('text');
67
+ expect(output.data).toBe('Hello');
68
+ });
69
+
70
+ it('creates an output with metadata', () => {
71
+ const output = createOutput('chart', '{}', 'application/json');
72
+ expect(output.mimeType).toBe('application/json');
73
+ });
74
+
75
+ it('adds output to a cell', () => {
76
+ const cell = codeCell('x');
77
+ const updated = addOutput(cell, createOutput('text', 'result'));
78
+ expect(updated.outputs).toHaveLength(1);
79
+ expect(cell.outputs).toHaveLength(0); // immutable
80
+ });
81
+
82
+ it('clears outputs', () => {
83
+ let cell = codeCell('x');
84
+ cell = addOutput(cell, createOutput('text', 'a'));
85
+ cell = addOutput(cell, createOutput('text', 'b'));
86
+ expect(cell.outputs).toHaveLength(2);
87
+ const cleared = clearOutputs(cell);
88
+ expect(cleared.outputs).toHaveLength(0);
89
+ });
90
+
91
+ it('hasOutputs returns correct value', () => {
92
+ const empty = codeCell('x');
93
+ expect(hasOutputs(empty)).toBe(false);
94
+ const withOutput = addOutput(empty, createOutput('text', 'hi'));
95
+ expect(hasOutputs(withOutput)).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe('Cell Status', () => {
100
+ it('marks cell as running', () => {
101
+ const cell = codeCell('x');
102
+ const running = markRunning(cell, 1);
103
+ expect(running.status).toBe('running');
104
+ });
105
+
106
+ it('marks cell as completed', () => {
107
+ const cell = codeCell('x');
108
+ const completed = markCompleted(cell, [createOutput('text', 'done')]);
109
+ expect(completed.status).toBe('completed');
110
+ expect(completed.outputs).toHaveLength(1);
111
+ });
112
+
113
+ it('marks cell as error', () => {
114
+ const cell = codeCell('x');
115
+ const errored = markError(cell, 'Something failed');
116
+ expect(errored.status).toBe('error');
117
+ expect(errored.outputs).toHaveLength(1);
118
+ expect(errored.outputs[0].type).toBe('error');
119
+ expect(errored.outputs[0].data).toBe('Something failed');
120
+ });
121
+ });
122
+
123
+ describe('Cell Filtering', () => {
124
+ it('filters code cells', () => {
125
+ const cells = [codeCell('a'), markdownCell('b'), codeCell('c'), rawCell('d')];
126
+ const codeCells = filterCodeCells(cells);
127
+ expect(codeCells).toHaveLength(2);
128
+ expect(codeCells.every((c) => c.type === 'code')).toBe(true);
129
+ });
130
+
131
+ it('filters by tag', () => {
132
+ const cells = [
133
+ createCell('code', 'a', { tags: ['setup'] }),
134
+ createCell('code', 'b', { tags: ['cleanup'] }),
135
+ createCell('code', 'c', { tags: ['setup', 'important'] }),
136
+ ];
137
+ const setup = filterByTag(cells, 'setup');
138
+ expect(setup).toHaveLength(2);
139
+ });
140
+ });
141
+
142
+ describe('Cell Source', () => {
143
+ it('gets source lines', () => {
144
+ const cell = codeCell('line1\nline2\nline3');
145
+ const lines = getSourceLines(cell);
146
+ expect(lines).toEqual(['line1', 'line2', 'line3']);
147
+ });
148
+
149
+ it('counts lines', () => {
150
+ const cell = codeCell('a\nb\nc');
151
+ expect(countLines(cell)).toBe(3);
152
+ });
153
+
154
+ it('counts single line', () => {
155
+ const cell = codeCell('single');
156
+ expect(countLines(cell)).toBe(1);
157
+ });
158
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Tests for pipeline/dataset.ts
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ inferColumnType,
8
+ inferSchema,
9
+ createDataset,
10
+ selectColumns,
11
+ filterRows,
12
+ sortBy,
13
+ uniqueValues,
14
+ groupBy,
15
+ getColumn,
16
+ getNumericColumn,
17
+ head,
18
+ tail,
19
+ addColumn,
20
+ renameColumn,
21
+ dropNulls,
22
+ } from '../src/pipeline/index.js';
23
+
24
+ describe('Type Inference', () => {
25
+ it('infers string type', () => expect(inferColumnType('hello')).toBe('string'));
26
+ it('infers number type', () => expect(inferColumnType(42)).toBe('number'));
27
+ it('infers boolean type', () => expect(inferColumnType(true)).toBe('boolean'));
28
+ it('infers null type', () => expect(inferColumnType(null)).toBe('null'));
29
+ it('infers undefined as null', () => expect(inferColumnType(undefined)).toBe('null'));
30
+ it('infers date type', () => expect(inferColumnType(new Date())).toBe('date'));
31
+ it('infers array type', () => expect(inferColumnType([1, 2])).toBe('array'));
32
+ it('infers object type', () => expect(inferColumnType({ a: 1 })).toBe('object'));
33
+ });
34
+
35
+ describe('Schema Inference', () => {
36
+ it('infers schema from rows', () => {
37
+ const rows = [
38
+ { name: 'Alice', age: 25 },
39
+ { name: 'Bob', age: 30 },
40
+ ];
41
+ const schema = inferSchema(rows, 'people');
42
+ expect(schema.name).toBe('people');
43
+ expect(schema.rows).toBe(2);
44
+ expect(schema.columns).toHaveLength(2);
45
+ expect(schema.columns.find((c) => c.name === 'name')?.type).toBe('string');
46
+ expect(schema.columns.find((c) => c.name === 'age')?.type).toBe('number');
47
+ });
48
+
49
+ it('handles empty rows', () => {
50
+ const schema = inferSchema([]);
51
+ expect(schema.rows).toBe(0);
52
+ expect(schema.columns).toHaveLength(0);
53
+ });
54
+
55
+ it('detects nullable columns', () => {
56
+ const rows = [
57
+ { name: 'Alice', score: 90 },
58
+ { name: 'Bob', score: null },
59
+ ];
60
+ const schema = inferSchema(rows);
61
+ const scoreCol = schema.columns.find((c) => c.name === 'score');
62
+ expect(scoreCol?.nullable).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe('Dataset Creation', () => {
67
+ it('creates a dataset', () => {
68
+ const ds = createDataset([{ x: 1 }], 'test', 'A test dataset');
69
+ expect(ds.metadata.name).toBe('test');
70
+ expect(ds.metadata.description).toBe('A test dataset');
71
+ expect(ds.rows).toHaveLength(1);
72
+ });
73
+ });
74
+
75
+ describe('Dataset Operations', () => {
76
+ const data = createDataset([
77
+ { name: 'Alice', age: 25, city: 'NYC' },
78
+ { name: 'Bob', age: 30, city: 'LA' },
79
+ { name: 'Charlie', age: 22, city: 'NYC' },
80
+ { name: 'Diana', age: 35, city: 'SF' },
81
+ ]);
82
+
83
+ it('selects columns', () => {
84
+ const selected = selectColumns(data, ['name', 'age']);
85
+ expect(Object.keys(selected.rows[0])).toEqual(['name', 'age']);
86
+ });
87
+
88
+ it('filters rows', () => {
89
+ const filtered = filterRows(data, (r) => (r.age as number) > 25);
90
+ expect(filtered.rows).toHaveLength(2);
91
+ });
92
+
93
+ it('sorts ascending', () => {
94
+ const sorted = sortBy(data, 'age');
95
+ expect(sorted.rows[0].name).toBe('Charlie');
96
+ expect(sorted.rows[3].name).toBe('Diana');
97
+ });
98
+
99
+ it('sorts descending', () => {
100
+ const sorted = sortBy(data, 'age', false);
101
+ expect(sorted.rows[0].name).toBe('Diana');
102
+ });
103
+
104
+ it('gets unique values', () => {
105
+ const cities = uniqueValues(data, 'city');
106
+ expect(cities).toContain('NYC');
107
+ expect(cities).toContain('LA');
108
+ expect(cities).toContain('SF');
109
+ });
110
+
111
+ it('groups by column', () => {
112
+ const groups = groupBy(data, 'city');
113
+ expect(groups.get('NYC')).toHaveLength(2);
114
+ expect(groups.get('LA')).toHaveLength(1);
115
+ });
116
+
117
+ it('gets column values', () => {
118
+ const ages = getColumn(data, 'age');
119
+ expect(ages).toEqual([25, 30, 22, 35]);
120
+ });
121
+
122
+ it('gets numeric column values', () => {
123
+ const ages = getNumericColumn(data, 'age');
124
+ expect(ages).toEqual([25, 30, 22, 35]);
125
+ });
126
+
127
+ it('gets head', () => {
128
+ const top = head(data, 2);
129
+ expect(top.rows).toHaveLength(2);
130
+ expect(top.rows[0].name).toBe('Alice');
131
+ });
132
+
133
+ it('gets tail', () => {
134
+ const bottom = tail(data, 2);
135
+ expect(bottom.rows).toHaveLength(2);
136
+ expect(bottom.rows[1].name).toBe('Diana');
137
+ });
138
+
139
+ it('adds a computed column', () => {
140
+ const withLabel = addColumn(data, 'label', (r) => `${r.name}-${r.city}`);
141
+ expect(withLabel.rows[0].label).toBe('Alice-NYC');
142
+ });
143
+
144
+ it('renames a column', () => {
145
+ const renamed = renameColumn(data, 'name', 'fullName');
146
+ expect(renamed.rows[0].fullName).toBe('Alice');
147
+ expect(renamed.rows[0].name).toBeUndefined();
148
+ });
149
+
150
+ it('drops nulls', () => {
151
+ const withNulls = createDataset([
152
+ { a: 1, b: 'x' },
153
+ { a: null, b: 'y' },
154
+ { a: 3, b: null },
155
+ ]);
156
+ const cleaned = dropNulls(withNulls, ['a']);
157
+ expect(cleaned.rows).toHaveLength(2);
158
+ });
159
+ });