@alisio/core 0.1.0-alpha.9 → 0.1.1

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 (187) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +105 -3
  65. package/dist/application.js +446 -18
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +306 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +206 -6
  95. package/dist/config.js +426 -20
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +134 -4
  105. package/dist/core/runner.js +780 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +50 -5
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +34 -1
  142. package/dist/plugins/host.js +143 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.js +6 -1
  148. package/dist/runtime/access.d.ts +62 -0
  149. package/dist/runtime/access.js +115 -0
  150. package/dist/runtime/blobs.d.ts +51 -0
  151. package/dist/runtime/blobs.js +177 -0
  152. package/dist/runtime/diff.d.ts +23 -0
  153. package/dist/runtime/diff.js +154 -0
  154. package/dist/runtime/ids.d.ts +8 -0
  155. package/dist/runtime/ids.js +32 -0
  156. package/dist/runtime/paths.d.ts +11 -0
  157. package/dist/runtime/paths.js +17 -1
  158. package/dist/runtime/process.d.ts +22 -0
  159. package/dist/runtime/process.js +100 -15
  160. package/dist/runtime/sqlite.d.ts +2 -0
  161. package/dist/runtime/sqlite.js +18 -0
  162. package/dist/runtime/store.d.ts +91 -3
  163. package/dist/runtime/store.js +465 -11
  164. package/dist/runtime/zip.d.ts +24 -0
  165. package/dist/runtime/zip.js +126 -0
  166. package/dist/sessions/children.js +17 -1
  167. package/dist/sessions/side-questions.d.ts +57 -0
  168. package/dist/sessions/side-questions.js +161 -0
  169. package/dist/tools/analysis.d.ts +37 -0
  170. package/dist/tools/analysis.js +573 -0
  171. package/dist/tools/artifacts.d.ts +14 -0
  172. package/dist/tools/artifacts.js +143 -0
  173. package/dist/tools/background.d.ts +7 -0
  174. package/dist/tools/background.js +165 -0
  175. package/dist/tools/data.d.ts +7 -0
  176. package/dist/tools/data.js +160 -0
  177. package/dist/tools/execute.d.ts +5 -0
  178. package/dist/tools/execute.js +15 -3
  179. package/dist/tools/goal.d.ts +19 -0
  180. package/dist/tools/goal.js +126 -0
  181. package/dist/tools/search.d.ts +17 -3
  182. package/dist/tools/search.js +153 -0
  183. package/dist/tools/standard.d.ts +4 -1
  184. package/dist/tools/standard.js +99 -18
  185. package/dist/version.d.ts +5 -3
  186. package/dist/version.js +35 -6
  187. package/package.json +2 -2
@@ -0,0 +1,39 @@
1
+ export declare const MAX_PAGE_ROWS = 500;
2
+ export declare const MAX_OFFSET = 100000;
3
+ /** Filtering every column at once (no `column`) is only allowed up to this many rows. */
4
+ export declare const MAX_ALL_COLUMNS_FILTER_ROWS = 100000;
5
+ export interface PageParams {
6
+ table: string;
7
+ columns: string[];
8
+ limit: number;
9
+ sort?: {
10
+ column: string;
11
+ dir: "asc" | "desc";
12
+ };
13
+ /** Substring filter on one column (or on every column when `column` is absent). */
14
+ filter?: {
15
+ column?: string;
16
+ text: string;
17
+ };
18
+ /** Opaque cursor returned by the previous page. */
19
+ after?: string;
20
+ /** Rows to skip (jump). */
21
+ offset?: number;
22
+ }
23
+ export declare class PageParamError extends Error {
24
+ }
25
+ /** The cursor of a row: base64 of `[rowid]`. */
26
+ export declare const encodeCursor: (rowid: number) => string;
27
+ export declare function decodeCursor(cursor: string): number;
28
+ type Param = string | number | null;
29
+ /** Statement and parameters of one page (`limit + 1` rows, to know whether a next page exists). */
30
+ export declare function buildPageQuery(params: PageParams): {
31
+ sql: string;
32
+ params: Param[];
33
+ };
34
+ /** `SELECT count(*)` of the filter (shown on the first page of a filtered view). */
35
+ export declare function buildCountQuery(params: Pick<PageParams, "table" | "columns" | "filter">): {
36
+ sql: string;
37
+ params: Param[];
38
+ };
39
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SQL of `SpreadsheetView` pages (spec §17.4). Pure: builds trusted statements for the engine.
3
+ * Paging is keyset-based on `rowid` (dense, in file order), so a page never scans from the start
4
+ * for the unsorted view and never duplicates or skips rows while sorting. `LIMIT/OFFSET` only
5
+ * serves jumps to a position when a sort or filter is active (≤ 100 000 rows in).
6
+ */
7
+ import { quoteIdent } from "./infer.js";
8
+ export const MAX_PAGE_ROWS = 500;
9
+ export const MAX_OFFSET = 100_000;
10
+ /** Filtering every column at once (no `column`) is only allowed up to this many rows. */
11
+ export const MAX_ALL_COLUMNS_FILTER_ROWS = 100_000;
12
+ export class PageParamError extends Error {
13
+ }
14
+ /** The cursor of a row: base64 of `[rowid]`. */
15
+ export const encodeCursor = (rowid) => Buffer.from(JSON.stringify([rowid])).toString("base64url");
16
+ export function decodeCursor(cursor) {
17
+ try {
18
+ const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
19
+ if (Array.isArray(value) && Number.isSafeInteger(value[0]) && value[0] >= 0)
20
+ return value[0];
21
+ }
22
+ catch {
23
+ /* falls through */
24
+ }
25
+ throw new PageParamError("Invalid cursor");
26
+ }
27
+ const escapeLike = (text) => text.replace(/[\\%_]/g, (c) => `\\${c}`);
28
+ /** Statement and parameters of one page (`limit + 1` rows, to know whether a next page exists). */
29
+ export function buildPageQuery(params) {
30
+ const table = quoteIdent(params.table);
31
+ const known = new Set(params.columns);
32
+ const values = [];
33
+ const where = [];
34
+ const sortColumn = params.sort?.column;
35
+ if (sortColumn !== undefined && !known.has(sortColumn))
36
+ throw new PageParamError(`Unknown column: ${sortColumn}`);
37
+ if (params.filter?.text) {
38
+ const like = `%${escapeLike(params.filter.text)}%`;
39
+ if (params.filter.column !== undefined) {
40
+ if (!known.has(params.filter.column))
41
+ throw new PageParamError(`Unknown column: ${params.filter.column}`);
42
+ where.push(`${quoteIdent(params.filter.column)} LIKE ? ESCAPE '\\'`);
43
+ values.push(like);
44
+ }
45
+ else {
46
+ where.push(`(${params.columns.map((c) => `${quoteIdent(c)} LIKE ? ESCAPE '\\'`).join(" OR ")})`);
47
+ for (let i = 0; i < params.columns.length; i++)
48
+ values.push(like);
49
+ }
50
+ }
51
+ const filtered = where.length > 0;
52
+ let order = "rowid";
53
+ let offset = 0;
54
+ if (sortColumn === undefined) {
55
+ if (params.after) {
56
+ where.push("rowid > ?");
57
+ values.push(decodeCursor(params.after));
58
+ }
59
+ else if (params.offset) {
60
+ if (!filtered) {
61
+ // rowid is dense (1..N in file order): jumping needs no OFFSET at all.
62
+ where.push("rowid > ?");
63
+ values.push(params.offset);
64
+ }
65
+ else
66
+ offset = params.offset;
67
+ }
68
+ }
69
+ else {
70
+ const k = quoteIdent(sortColumn);
71
+ const asc = params.sort?.dir !== "desc";
72
+ order = `${k} ${asc ? "ASC" : "DESC"}, rowid ASC`;
73
+ if (params.after) {
74
+ const r = decodeCursor(params.after);
75
+ const v = `(SELECT ${k} FROM ${table} WHERE rowid = ?)`;
76
+ // NULLs sort first ascending and last descending; ties break on rowid ascending.
77
+ const next = asc
78
+ ? `((${v} IS NULL AND (${k} IS NOT NULL OR rowid > ?)) OR ` +
79
+ `(${v} IS NOT NULL AND (${k} > ${v} OR (${k} = ${v} AND rowid > ?))))`
80
+ : `((${v} IS NULL AND ${k} IS NULL AND rowid > ?) OR ` +
81
+ `(${v} IS NOT NULL AND (${k} < ${v} OR (${k} = ${v} AND rowid > ?) OR ${k} IS NULL)))`;
82
+ where.push(next);
83
+ for (let i = 0; i < 6; i++)
84
+ values.push(r);
85
+ }
86
+ else if (params.offset)
87
+ offset = params.offset;
88
+ }
89
+ if (offset > MAX_OFFSET)
90
+ throw new PageParamError(`Jumping is limited to the first ${MAX_OFFSET} rows when sorting or filtering`);
91
+ const select = ["rowid", ...params.columns.map(quoteIdent)].join(", ");
92
+ values.push(Math.min(params.limit, MAX_PAGE_ROWS) + 1);
93
+ const sql = `SELECT ${select} FROM ${table}` +
94
+ (where.length ? ` WHERE ${where.join(" AND ")}` : "") +
95
+ ` ORDER BY ${order} LIMIT ?` +
96
+ (offset ? ` OFFSET ${Math.floor(offset)}` : "");
97
+ return { sql, params: values };
98
+ }
99
+ /** `SELECT count(*)` of the filter (shown on the first page of a filtered view). */
100
+ export function buildCountQuery(params) {
101
+ const table = quoteIdent(params.table);
102
+ const text = params.filter?.text;
103
+ if (!text)
104
+ return { sql: `SELECT count(*) FROM ${table}`, params: [] };
105
+ const like = `%${escapeLike(text)}%`;
106
+ if (params.filter?.column !== undefined) {
107
+ if (!params.columns.includes(params.filter.column))
108
+ throw new PageParamError(`Unknown column: ${params.filter.column}`);
109
+ return {
110
+ sql: `SELECT count(*) FROM ${table} WHERE ${quoteIdent(params.filter.column)} LIKE ? ESCAPE '\\'`,
111
+ params: [like],
112
+ };
113
+ }
114
+ return {
115
+ sql: `SELECT count(*) FROM ${table} WHERE (${params.columns
116
+ .map((c) => `${quoteIdent(c)} LIKE ? ESCAPE '\\'`)
117
+ .join(" OR ")})`,
118
+ params: params.columns.map(() => like),
119
+ };
120
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Lexical guard of `data_query` (spec §17.3). `node:sqlite` has no authorizer or progress
3
+ * handler and `DatabaseSync.prepare()` silently ignores text after the first statement, so the
4
+ * guard checks the SQL itself: exactly one statement, starting with SELECT or WITH, without
5
+ * write, schema, transaction or extension keywords outside literals and comments. The read-only
6
+ * connection (`readOnly`, `query_only`, extensions off) is the second line of defence.
7
+ */
8
+ export type GuardResult = {
9
+ ok: true;
10
+ sql: string;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ /**
16
+ * Validates a model-written query. Accepts one `SELECT`/`WITH` statement (a single trailing `;`
17
+ * is allowed and removed). Keywords inside literals, comments and quoted identifiers are fine;
18
+ * `replace(…)` as a function is fine (REPLACE INTO is not).
19
+ */
20
+ export declare function guardSql(sql: string): GuardResult;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Lexical guard of `data_query` (spec §17.3). `node:sqlite` has no authorizer or progress
3
+ * handler and `DatabaseSync.prepare()` silently ignores text after the first statement, so the
4
+ * guard checks the SQL itself: exactly one statement, starting with SELECT or WITH, without
5
+ * write, schema, transaction or extension keywords outside literals and comments. The read-only
6
+ * connection (`readOnly`, `query_only`, extensions off) is the second line of defence.
7
+ */
8
+ const FORBIDDEN = new Set([
9
+ "ATTACH",
10
+ "DETACH",
11
+ "PRAGMA",
12
+ "INSERT",
13
+ "UPDATE",
14
+ "DELETE",
15
+ "REPLACE",
16
+ "CREATE",
17
+ "DROP",
18
+ "ALTER",
19
+ "VACUUM",
20
+ "REINDEX",
21
+ "ANALYZE",
22
+ "BEGIN",
23
+ "COMMIT",
24
+ "ROLLBACK",
25
+ "SAVEPOINT",
26
+ "RELEASE",
27
+ "LOAD_EXTENSION",
28
+ ]);
29
+ const isWordStart = (c) => /[A-Za-z_]/.test(c);
30
+ const isWordChar = (c) => /[A-Za-z0-9_$]/.test(c);
31
+ /**
32
+ * Splits SQL into significant tokens, skipping whitespace, comments and the contents of string
33
+ * literals and quoted identifiers. Returns an error text for an unterminated literal or comment.
34
+ */
35
+ function tokenize(sql) {
36
+ const tokens = [];
37
+ const n = sql.length;
38
+ let i = 0;
39
+ while (i < n) {
40
+ const c = sql[i];
41
+ if (c === "'" || c === '"' || c === "`") {
42
+ let j = i + 1;
43
+ for (;;) {
44
+ if (j >= n)
45
+ return { error: `Unterminated ${c === "'" ? "string" : "identifier"}` };
46
+ if (sql[j] === c) {
47
+ if (sql[j + 1] === c) {
48
+ j += 2;
49
+ continue;
50
+ }
51
+ break;
52
+ }
53
+ j++;
54
+ }
55
+ // A quoted identifier or string is a value, not a keyword; it still counts as a token so
56
+ // that "SELECT" 1 is not mistaken for an empty statement.
57
+ tokens.push({ type: "punct", text: c });
58
+ i = j + 1;
59
+ }
60
+ else if (c === "[") {
61
+ const end = sql.indexOf("]", i + 1);
62
+ if (end < 0)
63
+ return { error: "Unterminated identifier" };
64
+ tokens.push({ type: "punct", text: "[" });
65
+ i = end + 1;
66
+ }
67
+ else if (c === "-" && sql[i + 1] === "-") {
68
+ const end = sql.indexOf("\n", i + 2);
69
+ i = end < 0 ? n : end + 1;
70
+ }
71
+ else if (c === "/" && sql[i + 1] === "*") {
72
+ const end = sql.indexOf("*/", i + 2);
73
+ if (end < 0)
74
+ return { error: "Unterminated comment" };
75
+ i = end + 2;
76
+ }
77
+ else if (/\s/.test(c)) {
78
+ i++;
79
+ }
80
+ else if (isWordStart(c)) {
81
+ let j = i + 1;
82
+ while (j < n && isWordChar(sql[j]))
83
+ j++;
84
+ tokens.push({ type: "word", text: sql.slice(i, j) });
85
+ i = j;
86
+ }
87
+ else {
88
+ tokens.push({ type: "punct", text: c });
89
+ i++;
90
+ }
91
+ }
92
+ return { tokens };
93
+ }
94
+ /**
95
+ * Validates a model-written query. Accepts one `SELECT`/`WITH` statement (a single trailing `;`
96
+ * is allowed and removed). Keywords inside literals, comments and quoted identifiers are fine;
97
+ * `replace(…)` as a function is fine (REPLACE INTO is not).
98
+ */
99
+ export function guardSql(sql) {
100
+ const scanned = tokenize(sql);
101
+ if ("error" in scanned)
102
+ return { ok: false, reason: scanned.error };
103
+ const { tokens } = scanned;
104
+ // Drop one trailing semicolon; any other one separates statements.
105
+ if (tokens.at(-1)?.text === ";")
106
+ tokens.pop();
107
+ if (!tokens.length)
108
+ return { ok: false, reason: "The query is empty" };
109
+ if (tokens.some((t) => t.type === "punct" && t.text === ";"))
110
+ return { ok: false, reason: "Only one statement is allowed (no `;` between statements)" };
111
+ const first = tokens[0];
112
+ if (first.type !== "word" || !["SELECT", "WITH"].includes(first.text.toUpperCase()))
113
+ return { ok: false, reason: "Only SELECT and WITH queries are allowed" };
114
+ for (let i = 0; i < tokens.length; i++) {
115
+ const token = tokens[i];
116
+ if (token.type !== "word")
117
+ continue;
118
+ const word = token.text.toUpperCase();
119
+ if (!FORBIDDEN.has(word))
120
+ continue;
121
+ if (word === "REPLACE" && tokens[i + 1]?.text === "(")
122
+ continue;
123
+ return {
124
+ ok: false,
125
+ reason: `\`${token.text}\` is not allowed in a read-only query (quote column names that look like keywords, e.g. "${token.text}")`,
126
+ };
127
+ }
128
+ return { ok: true, sql };
129
+ }
@@ -0,0 +1,18 @@
1
+ /** Minimal view of `node:sqlite`'s `DatabaseSync` (the engine runs outside the type roots). */
2
+ export interface Db {
3
+ exec(sql: string): void;
4
+ prepare(sql: string): {
5
+ run(...params: unknown[]): unknown;
6
+ get(...params: unknown[]): unknown;
7
+ all(...params: unknown[]): unknown[];
8
+ };
9
+ }
10
+ export declare const INGEST_VERSION = 1;
11
+ /** Distinct counts and top values are exact up to this many rows; above, a sample is used. */
12
+ export declare const EXACT_DISTINCT_ROWS = 1000000;
13
+ export declare const META_SCHEMA = "\nCREATE TABLE IF NOT EXISTS _alisio_meta(key TEXT PRIMARY KEY, value TEXT);\nCREATE TABLE IF NOT EXISTS _alisio_sheets(ordinal INTEGER PRIMARY KEY, name TEXT NOT NULL,\n table_name TEXT NOT NULL, rows INTEGER NOT NULL, columns INTEGER NOT NULL);\nCREATE TABLE IF NOT EXISTS _alisio_columns(table_name TEXT NOT NULL, ordinal INTEGER NOT NULL,\n name TEXT NOT NULL, label TEXT NOT NULL, inferred_type TEXT NOT NULL,\n nulls INTEGER, distinct_count INTEGER, distinct_exact INTEGER,\n min TEXT, max TEXT, mean REAL, text_fallbacks INTEGER, top_values TEXT,\n PRIMARY KEY(table_name, ordinal));\n";
14
+ /**
15
+ * Computes `_alisio_columns` statistics for every registered column and refreshes the row and
16
+ * column counts of `_alisio_sheets`. Column rows (table, ordinal, name, label) must exist.
17
+ */
18
+ export declare function finalizeDatabase(db: Db): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Metadata tables of a dataset file and the per-column statistics (spec §17.2). The same
3
+ * `finalizeDatabase` runs after Node parses a CSV/TSV/JSON/JSONL file and after the Python helper
4
+ * converts an XLSX workbook, so both routes produce identical types and statistics.
5
+ */
6
+ import { inferType, quoteIdent } from "./infer.js";
7
+ export const INGEST_VERSION = 1;
8
+ /** Distinct counts and top values are exact up to this many rows; above, a sample is used. */
9
+ export const EXACT_DISTINCT_ROWS = 1_000_000;
10
+ const SAMPLE_ROWS = 200_000;
11
+ const TYPE_SAMPLE = 1000;
12
+ export const META_SCHEMA = `
13
+ CREATE TABLE IF NOT EXISTS _alisio_meta(key TEXT PRIMARY KEY, value TEXT);
14
+ CREATE TABLE IF NOT EXISTS _alisio_sheets(ordinal INTEGER PRIMARY KEY, name TEXT NOT NULL,
15
+ table_name TEXT NOT NULL, rows INTEGER NOT NULL, columns INTEGER NOT NULL);
16
+ CREATE TABLE IF NOT EXISTS _alisio_columns(table_name TEXT NOT NULL, ordinal INTEGER NOT NULL,
17
+ name TEXT NOT NULL, label TEXT NOT NULL, inferred_type TEXT NOT NULL,
18
+ nulls INTEGER, distinct_count INTEGER, distinct_exact INTEGER,
19
+ min TEXT, max TEXT, mean REAL, text_fallbacks INTEGER, top_values TEXT,
20
+ PRIMARY KEY(table_name, ordinal));
21
+ `;
22
+ /**
23
+ * Computes `_alisio_columns` statistics for every registered column and refreshes the row and
24
+ * column counts of `_alisio_sheets`. Column rows (table, ordinal, name, label) must exist.
25
+ */
26
+ export function finalizeDatabase(db) {
27
+ const sheets = db
28
+ .prepare("SELECT ordinal, table_name FROM _alisio_sheets ORDER BY ordinal")
29
+ .all();
30
+ db.exec("BEGIN");
31
+ try {
32
+ for (const sheet of sheets) {
33
+ const table = quoteIdent(sheet.table_name);
34
+ const rows = db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n;
35
+ const columns = db
36
+ .prepare("SELECT table_name, ordinal, name FROM _alisio_columns WHERE table_name=? ORDER BY ordinal")
37
+ .all(sheet.table_name);
38
+ db.prepare("UPDATE _alisio_sheets SET rows=?, columns=? WHERE ordinal=?").run(rows, columns.length, sheet.ordinal);
39
+ const exact = rows <= EXACT_DISTINCT_ROWS;
40
+ const stride = exact ? 1 : Math.ceil(rows / SAMPLE_ROWS);
41
+ const andWhere = exact ? "" : `AND rowid % ${stride} = 0`;
42
+ for (const column of columns) {
43
+ const col = quoteIdent(column.name);
44
+ const sample = db
45
+ .prepare(`SELECT typeof(${col}) AS kind, ${col} AS value FROM ${table} LIMIT ${TYPE_SAMPLE}`)
46
+ .all();
47
+ const type = inferType(sample);
48
+ const agg = db
49
+ .prepare(`SELECT sum(${col} IS NULL) AS nulls, min(${col}) AS mn, max(${col}) AS mx,
50
+ sum(typeof(${col}) IN ('integer','real')) AS nums,
51
+ sum(typeof(${col})='text') AS texts,
52
+ sum(typeof(${col})='text' AND ${col} GLOB '*[0-9]*' AND NOT ${col} GLOB '*[A-Za-z]*') AS numlike,
53
+ avg(CASE WHEN typeof(${col}) IN ('integer','real') THEN ${col} END) AS mean
54
+ FROM ${table}`)
55
+ .get();
56
+ const groups = db
57
+ .prepare(`WITH g AS MATERIALIZED (
58
+ SELECT ${col} AS v, count(*) AS n FROM ${table}
59
+ WHERE ${col} IS NOT NULL ${andWhere} GROUP BY ${col})
60
+ SELECT (SELECT count(*) FROM g) AS d, CAST(v AS TEXT) AS v, n FROM g
61
+ ORDER BY n DESC, v LIMIT 5`)
62
+ .all();
63
+ const nums = Number(agg.nums ?? 0);
64
+ const texts = Number(agg.texts ?? 0);
65
+ const fallbacks = nums > 0 ? texts : type === "text" ? Number(agg.numlike ?? 0) : 0;
66
+ const numeric = type === "integer" || type === "real";
67
+ db.prepare(`UPDATE _alisio_columns SET inferred_type=?, nulls=?, distinct_count=?, distinct_exact=?,
68
+ min=?, max=?, mean=?, text_fallbacks=?, top_values=? WHERE table_name=? AND ordinal=?`).run(type, Number(agg.nulls ?? 0), Number(groups[0]?.d ?? 0), exact ? 1 : 0, agg.mn === null || agg.mn === undefined ? null : String(agg.mn), agg.mx === null || agg.mx === undefined ? null : String(agg.mx), numeric && agg.mean !== null ? Number(agg.mean) : null, fallbacks, JSON.stringify(groups.map((g) => ({ value: g.v, count: Number(g.n) }))), sheet.table_name, column.ordinal);
69
+ }
70
+ }
71
+ db.exec("COMMIT");
72
+ }
73
+ catch (error) {
74
+ db.exec("ROLLBACK");
75
+ throw error;
76
+ }
77
+ }
@@ -0,0 +1,33 @@
1
+ export interface HintHost {
2
+ platform: NodeJS.Platform;
3
+ arch: string;
4
+ /** Linux `/etc/os-release` fields (`ID`, `ID_LIKE`, `PRETTY_NAME`, `VERSION_ID`). */
5
+ osRelease?: Record<string, string>;
6
+ hasUv?: boolean;
7
+ hasWinget?: boolean;
8
+ hasBrew?: boolean;
9
+ /** A Python that was found but is too old (or a `--python` that failed). */
10
+ found?: {
11
+ version?: string;
12
+ path: string;
13
+ reason?: string;
14
+ };
15
+ }
16
+ export interface InstallHints {
17
+ /** `Linux · Ubuntu 22.04 · x86_64` */
18
+ system: string;
19
+ /** Optional heading line (found Python too old, `--python` not usable). */
20
+ heading?: string;
21
+ primary: string;
22
+ alternatives: string[];
23
+ notes: string[];
24
+ }
25
+ /** Parses `KEY=value` / `KEY="value"` lines of an os-release file. */
26
+ export declare function parseOsRelease(text: string): Record<string, string>;
27
+ export declare function pythonInstallHints(host: HintHost): InstallHints;
28
+ /** One paragraph for the model and terminals (English, like every tool message). */
29
+ export declare function installHintsText(hints: InstallHints): string;
30
+ /** The same guidance as Markdown (web and TUI render it as a `markdown` block). */
31
+ export declare function installHintsMarkdown(hints: InstallHints): string;
32
+ /** Detects the host for `pythonInstallHints` (reads files and PATH only; launches nothing). */
33
+ export declare function detectHintHost(found?: HintHost["found"]): Promise<HintHost>;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Install instructions for Python ≥ 3.10 on the machine Alisio runs on. A pure function of the
3
+ * detected host (injected in tests); the host detection reads `/etc/os-release` and looks for
4
+ * `uv`, `winget` and `brew` on PATH without launching any process. Commands are shown, never run.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { which } from "../runtime/fs.js";
8
+ const UV = "uv python install 3.12";
9
+ const PYTHON_ORG = "Download the installer from https://www.python.org/downloads/";
10
+ /** Parses `KEY=value` / `KEY="value"` lines of an os-release file. */
11
+ export function parseOsRelease(text) {
12
+ const out = {};
13
+ for (const line of text.split(/\r?\n/)) {
14
+ const match = /^([A-Z_]+)=(.*)$/.exec(line.trim());
15
+ if (match?.[1])
16
+ out[match[1]] = (match[2] ?? "").replace(/^["']|["']$/g, "");
17
+ }
18
+ return out;
19
+ }
20
+ function systemLabel(host) {
21
+ const os = host.platform === "win32"
22
+ ? "Windows"
23
+ : host.platform === "darwin"
24
+ ? "macOS"
25
+ : host.platform === "linux"
26
+ ? "Linux"
27
+ : host.platform;
28
+ const distro = host.osRelease?.PRETTY_NAME;
29
+ return [os, distro, host.arch].filter(Boolean).join(" · ");
30
+ }
31
+ export function pythonInstallHints(host) {
32
+ const system = systemLabel(host);
33
+ const heading = host.found
34
+ ? host.found.version
35
+ ? `Found Python ${host.found.version} at ${host.found.path}; Alisio needs 3.10 or newer.`
36
+ : `The Python at ${host.found.path} is not usable${host.found.reason ? ` (${host.found.reason})` : ""}.`
37
+ : undefined;
38
+ const notes = [];
39
+ if (host.found)
40
+ notes.push("If another Python 3.10+ is installed, start Alisio with --python <path>.");
41
+ let primary;
42
+ let alternatives = [];
43
+ if (host.platform === "win32") {
44
+ primary = "winget install Python.Python.3.12";
45
+ alternatives = [`${PYTHON_ORG} (check "Add python.exe to PATH")`];
46
+ if (host.hasUv)
47
+ alternatives.push(UV);
48
+ notes.push("The Microsoft Store alias (WindowsApps\\python.exe) does not count as an installed Python.", "Open a new terminal after installing so the updated PATH is visible.");
49
+ }
50
+ else if (host.platform === "darwin") {
51
+ if (host.hasBrew) {
52
+ primary = "brew install python@3.12";
53
+ alternatives = [
54
+ "Download the macOS installer from https://www.python.org/downloads/macos/",
55
+ UV,
56
+ ];
57
+ }
58
+ else {
59
+ primary = "Download the macOS installer from https://www.python.org/downloads/macos/";
60
+ alternatives = [
61
+ UV,
62
+ "xcode-select --install (Command Line Tools Python; it may be older than 3.10)",
63
+ ];
64
+ }
65
+ }
66
+ else if (host.platform === "linux") {
67
+ const ids = `${host.osRelease?.ID ?? ""} ${host.osRelease?.ID_LIKE ?? ""}`.toLowerCase();
68
+ const has = (id) => ids.split(/\s+/).includes(id);
69
+ if (has("debian") || has("ubuntu")) {
70
+ primary = "sudo apt install python3";
71
+ alternatives = [`If your distribution ships a version older than 3.10: ${UV}`];
72
+ }
73
+ else if (has("fedora") || has("rhel") || has("centos")) {
74
+ primary = "sudo dnf install python3";
75
+ alternatives = [UV];
76
+ }
77
+ else if (has("arch")) {
78
+ primary = "sudo pacman -S python";
79
+ }
80
+ else if (ids.includes("suse")) {
81
+ primary = "sudo zypper install python3";
82
+ }
83
+ else if (has("alpine")) {
84
+ primary = "sudo apk add python3";
85
+ }
86
+ else {
87
+ primary = "Install python3 with your system package manager";
88
+ alternatives = [UV];
89
+ }
90
+ }
91
+ else {
92
+ primary = "Install python3 with your system package manager";
93
+ alternatives = [UV];
94
+ }
95
+ return { system, ...(heading ? { heading } : {}), primary, alternatives, notes };
96
+ }
97
+ /** One paragraph for the model and terminals (English, like every tool message). */
98
+ export function installHintsText(hints) {
99
+ const lines = [
100
+ ...(hints.heading ? [hints.heading] : []),
101
+ `Python 3.10+ was not found on this machine (${hints.system}). To install it: ${hints.primary}`,
102
+ ...hints.alternatives.map((alternative) => `Alternatively: ${alternative}`),
103
+ ...hints.notes,
104
+ "After installing, ask again; no Alisio restart is needed. To use a specific interpreter, start Alisio with --python <path>.",
105
+ ];
106
+ return lines.join("\n");
107
+ }
108
+ /** The same guidance as Markdown (web and TUI render it as a `markdown` block). */
109
+ export function installHintsMarkdown(hints) {
110
+ return [
111
+ "**Python 3.10+ is not available**",
112
+ "",
113
+ ...(hints.heading ? [hints.heading, ""] : []),
114
+ `Detected system: ${hints.system}`,
115
+ "",
116
+ "```sh",
117
+ hints.primary,
118
+ "```",
119
+ ...(hints.alternatives.length
120
+ ? ["", "Alternatives:", ...hints.alternatives.map((a) => `- ${a}`)]
121
+ : []),
122
+ ...(hints.notes.length ? ["", ...hints.notes.map((n) => `- ${n}`)] : []),
123
+ "",
124
+ "After installing, ask again; no restart is needed. Start Alisio with `--python <path>` to pin an interpreter.",
125
+ ].join("\n");
126
+ }
127
+ /** Detects the host for `pythonInstallHints` (reads files and PATH only; launches nothing). */
128
+ export async function detectHintHost(found) {
129
+ let osRelease;
130
+ if (process.platform === "linux")
131
+ osRelease = await readFile("/etc/os-release", "utf8")
132
+ .then(parseOsRelease)
133
+ .catch(() => undefined);
134
+ const [uv, winget, brew] = await Promise.all([which("uv"), which("winget"), which("brew")]);
135
+ return {
136
+ platform: process.platform,
137
+ arch: process.arch,
138
+ ...(osRelease ? { osRelease } : {}),
139
+ hasUv: !!uv,
140
+ hasWinget: !!winget,
141
+ hasBrew: !!brew,
142
+ ...(found ? { found } : {}),
143
+ };
144
+ }
@@ -0,0 +1,60 @@
1
+ import type { SqlDatabase } from "@alisio/sdk";
2
+ import type { ArtifactStore } from "../artifacts/store.ts";
3
+ import type { BlobStore } from "../runtime/blobs.ts";
4
+ import type { DatasetService } from "./data/datasets.ts";
5
+ export declare const SWEEP_INTERVAL_MS = 86400000;
6
+ export interface RetentionPolicy {
7
+ jobsDays: number;
8
+ intermediateDays: number;
9
+ artifactsDays: number;
10
+ }
11
+ export interface SweepReport {
12
+ /** Why nothing ran: swept less than 24 h ago, or another process is sweeping. */
13
+ skipped?: "recent" | "locked";
14
+ workDeleted: number;
15
+ jobsTrimmed: number;
16
+ jobsDeleted: number;
17
+ artifactsExpired: number;
18
+ datasetsDeleted: number;
19
+ uploadsDeleted: number;
20
+ bytesFreed: number;
21
+ errors: number;
22
+ }
23
+ export declare class AnalysisJanitor {
24
+ private options;
25
+ private timer?;
26
+ private stopped;
27
+ constructor(options: {
28
+ /** State root (`stateHome()` or the folder of `--db`). */
29
+ root: string;
30
+ db: SqlDatabase;
31
+ artifacts: ArtifactStore;
32
+ datasets?: DatasetService;
33
+ blobs?: BlobStore;
34
+ /** Read at every sweep, so a changed setting applies without a restart. */
35
+ retention: () => RetentionPolicy;
36
+ /** The clock (injected in tests). */
37
+ now?: () => number;
38
+ /** Delay before the first background sweep after `start()` (default 30 s). */
39
+ initialDelayMs?: number;
40
+ });
41
+ private now;
42
+ private get dir();
43
+ /** When the last sweep started (epoch ms), if there was one. */
44
+ lastSweep(): Promise<number | undefined>;
45
+ /** Launches the sweep in the background; never delays the caller and never keeps Node alive. */
46
+ start(): void;
47
+ stop(): void;
48
+ private arm;
49
+ /** One sweep (see the module comment). `force` ignores the 24 h mark. */
50
+ sweep(options?: {
51
+ force?: boolean;
52
+ }): Promise<SweepReport>;
53
+ /** Exclusive lock between processes sharing the state folder; undefined when someone holds it. */
54
+ private lock;
55
+ private expireArtifacts;
56
+ private trimJobs;
57
+ /** Removes the empty session and workspace folders a deleted job leaves behind. */
58
+ private pruneEmptyParents;
59
+ private dropDatasets;
60
+ }