@alisio/core 0.1.0-alpha.21 → 0.1.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +41 -0
- package/dist/application.js +197 -0
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/commands/builtins.js +13 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.js +186 -8
- package/dist/core/contracts.d.ts +69 -1
- package/dist/core/runner.d.ts +53 -4
- package/dist/core/runner.js +549 -161
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/index.d.ts +23 -4
- package/dist/index.js +22 -3
- package/dist/mcp/rich.js +4 -0
- package/dist/plugins/host.js +14 -6
- package/dist/runtime/blobs.d.ts +22 -0
- package/dist/runtime/blobs.js +84 -1
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +5 -0
- package/dist/runtime/paths.js +6 -0
- package/dist/runtime/process.d.ts +12 -0
- package/dist/runtime/process.js +64 -4
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +5 -0
- package/dist/runtime/store.js +75 -0
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +4 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +572 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.js +6 -2
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted capability decisions (`capability_grants`). A `session` allow of the ROOT session is
|
|
3
|
+
* the only state that grants anything; `once` allows and every `deny` are audit rows only (a deny
|
|
4
|
+
* is never sticky: the next call asks again). Revoking stamps `revoked_at`; nothing is deleted.
|
|
5
|
+
*/
|
|
6
|
+
import type { AnalysisCapability, CapabilityGrantWire, SqlDatabase } from "@alisio/sdk";
|
|
7
|
+
export type GrantSource = CapabilityGrantWire["source"];
|
|
8
|
+
export interface GrantRecord extends CapabilityGrantWire {
|
|
9
|
+
workspace: string;
|
|
10
|
+
callId?: string;
|
|
11
|
+
runId?: string;
|
|
12
|
+
correlationId?: string;
|
|
13
|
+
revokedBy?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Wire shape (no workspace path or call ids). */
|
|
16
|
+
export declare const toGrantWire: (record: GrantRecord) => CapabilityGrantWire;
|
|
17
|
+
export declare class CapabilityGrants {
|
|
18
|
+
private options;
|
|
19
|
+
constructor(options: {
|
|
20
|
+
db: SqlDatabase;
|
|
21
|
+
/** Root of a session (grants always belong to the root). */
|
|
22
|
+
rootOf: (sessionId: string) => string;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
});
|
|
25
|
+
rootOf(sessionId: string): string;
|
|
26
|
+
/** The live `session` allow of `capability` for the root of `sessionId`, if any. */
|
|
27
|
+
active(capability: AnalysisCapability, sessionId: string): GrantRecord | undefined;
|
|
28
|
+
granted(capability: AnalysisCapability, sessionId: string): boolean;
|
|
29
|
+
/** Records one decision (audit row; a `session` allow is also the live grant). */
|
|
30
|
+
record(input: {
|
|
31
|
+
capability: AnalysisCapability;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
workspace: string;
|
|
34
|
+
scope: "once" | "session";
|
|
35
|
+
decision: "allow" | "deny";
|
|
36
|
+
source: GrantSource;
|
|
37
|
+
callId?: string;
|
|
38
|
+
runId?: string;
|
|
39
|
+
correlationId?: string;
|
|
40
|
+
}): GrantRecord;
|
|
41
|
+
get(id: string): GrantRecord | undefined;
|
|
42
|
+
/** Live session grants first, then the latest `limit` audit rows of the root session. */
|
|
43
|
+
list(sessionId: string, limit?: number): GrantRecord[];
|
|
44
|
+
/** Live grants of the root session only. */
|
|
45
|
+
live(sessionId: string): GrantRecord[];
|
|
46
|
+
/** Revokes a live grant of `sessionId`'s root. False when unknown, foreign or already revoked. */
|
|
47
|
+
revoke(sessionId: string, grantId: string, by: string): boolean;
|
|
48
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { newId } from "../runtime/ids.js";
|
|
2
|
+
const toRecord = (r) => ({
|
|
3
|
+
id: String(r.id),
|
|
4
|
+
capability: String(r.capability),
|
|
5
|
+
sessionId: String(r.session),
|
|
6
|
+
workspace: String(r.workspace),
|
|
7
|
+
scope: String(r.scope),
|
|
8
|
+
decision: String(r.decision),
|
|
9
|
+
source: String(r.source),
|
|
10
|
+
createdAt: Number(r.created_at),
|
|
11
|
+
...(r.revoked_at != null ? { revokedAt: Number(r.revoked_at) } : {}),
|
|
12
|
+
...(r.revoked_by != null ? { revokedBy: String(r.revoked_by) } : {}),
|
|
13
|
+
...(r.call_id != null ? { callId: String(r.call_id) } : {}),
|
|
14
|
+
...(r.run_id != null ? { runId: String(r.run_id) } : {}),
|
|
15
|
+
...(r.correlation_id != null ? { correlationId: String(r.correlation_id) } : {}),
|
|
16
|
+
});
|
|
17
|
+
/** Wire shape (no workspace path or call ids). */
|
|
18
|
+
export const toGrantWire = (record) => ({
|
|
19
|
+
id: record.id,
|
|
20
|
+
capability: record.capability,
|
|
21
|
+
sessionId: record.sessionId,
|
|
22
|
+
scope: record.scope,
|
|
23
|
+
decision: record.decision,
|
|
24
|
+
source: record.source,
|
|
25
|
+
createdAt: record.createdAt,
|
|
26
|
+
...(record.revokedAt !== undefined ? { revokedAt: record.revokedAt } : {}),
|
|
27
|
+
});
|
|
28
|
+
export class CapabilityGrants {
|
|
29
|
+
options;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
rootOf(sessionId) {
|
|
34
|
+
return this.options.rootOf(sessionId);
|
|
35
|
+
}
|
|
36
|
+
/** The live `session` allow of `capability` for the root of `sessionId`, if any. */
|
|
37
|
+
active(capability, sessionId) {
|
|
38
|
+
const row = this.options.db
|
|
39
|
+
.prepare(`SELECT * FROM capability_grants WHERE capability=? AND session=? AND scope='session'
|
|
40
|
+
AND decision='allow' AND revoked_at IS NULL ORDER BY created_at DESC LIMIT 1`)
|
|
41
|
+
.get(capability, this.rootOf(sessionId));
|
|
42
|
+
return row ? toRecord(row) : undefined;
|
|
43
|
+
}
|
|
44
|
+
granted(capability, sessionId) {
|
|
45
|
+
return !!this.active(capability, sessionId);
|
|
46
|
+
}
|
|
47
|
+
/** Records one decision (audit row; a `session` allow is also the live grant). */
|
|
48
|
+
record(input) {
|
|
49
|
+
const id = newId("grant");
|
|
50
|
+
const createdAt = (this.options.now ?? Date.now)();
|
|
51
|
+
this.options.db
|
|
52
|
+
.prepare(`INSERT INTO capability_grants(id,capability,session,workspace,scope,decision,source,call_id,
|
|
53
|
+
run_id,correlation_id,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`)
|
|
54
|
+
.run(id, input.capability, this.rootOf(input.sessionId), input.workspace, input.scope, input.decision, input.source, input.callId ?? null, input.runId ?? null, input.correlationId ?? null, createdAt);
|
|
55
|
+
return this.get(id);
|
|
56
|
+
}
|
|
57
|
+
get(id) {
|
|
58
|
+
const row = this.options.db.prepare("SELECT * FROM capability_grants WHERE id=?").get(id);
|
|
59
|
+
return row ? toRecord(row) : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** Live session grants first, then the latest `limit` audit rows of the root session. */
|
|
62
|
+
list(sessionId, limit = 50) {
|
|
63
|
+
const root = this.rootOf(sessionId);
|
|
64
|
+
const live = this.options.db
|
|
65
|
+
.prepare(`SELECT * FROM capability_grants WHERE session=? AND scope='session' AND decision='allow'
|
|
66
|
+
AND revoked_at IS NULL ORDER BY created_at DESC`)
|
|
67
|
+
.all(root).map(toRecord);
|
|
68
|
+
const ids = new Set(live.map((g) => g.id));
|
|
69
|
+
const audit = this.options.db
|
|
70
|
+
.prepare("SELECT * FROM capability_grants WHERE session=? ORDER BY created_at DESC LIMIT ?")
|
|
71
|
+
.all(root, limit)
|
|
72
|
+
.map(toRecord)
|
|
73
|
+
.filter((g) => !ids.has(g.id));
|
|
74
|
+
return [...live, ...audit];
|
|
75
|
+
}
|
|
76
|
+
/** Live grants of the root session only. */
|
|
77
|
+
live(sessionId) {
|
|
78
|
+
return this.list(sessionId, 0).filter((g) => g.revokedAt === undefined && g.scope === "session");
|
|
79
|
+
}
|
|
80
|
+
/** Revokes a live grant of `sessionId`'s root. False when unknown, foreign or already revoked. */
|
|
81
|
+
revoke(sessionId, grantId, by) {
|
|
82
|
+
const grant = this.get(grantId);
|
|
83
|
+
if (!grant || grant.sessionId !== this.rootOf(sessionId) || grant.revokedAt !== undefined)
|
|
84
|
+
return false;
|
|
85
|
+
this.options.db
|
|
86
|
+
.prepare("UPDATE capability_grants SET revoked_at=?, revoked_by=? WHERE id=?")
|
|
87
|
+
.run((this.options.now ?? Date.now)(), by, grantId);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming CSV/TSV parser (RFC 4180, spec §17.2): quoted fields, doubled quotes, line breaks
|
|
3
|
+
* inside quotes, CRLF/LF/CR rows. No dependencies and no I/O: callers push decoded text chunks
|
|
4
|
+
* (any split, even inside a quote or between CR and LF) and receive rows through a callback.
|
|
5
|
+
* Blank lines are skipped. Lenient on purpose: a stray quote inside an unquoted field is literal
|
|
6
|
+
* and text after a closing quote is appended.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CsvError extends Error {
|
|
9
|
+
readonly code: "cell_too_large";
|
|
10
|
+
constructor(message: string, code: "cell_too_large");
|
|
11
|
+
}
|
|
12
|
+
export declare class CsvParser {
|
|
13
|
+
private readonly onRow;
|
|
14
|
+
private readonly maxCellChars;
|
|
15
|
+
private state;
|
|
16
|
+
private field;
|
|
17
|
+
private fields;
|
|
18
|
+
private skipLineFeed;
|
|
19
|
+
/** The current field was opened with a quote (so `""` is a value, not a blank line). */
|
|
20
|
+
private quotedField;
|
|
21
|
+
private readonly delimiter;
|
|
22
|
+
/** Rows delivered so far (blank lines excluded). */
|
|
23
|
+
rows: number;
|
|
24
|
+
constructor(delimiter: string, onRow: (fields: string[]) => void, maxCellChars?: number);
|
|
25
|
+
private add;
|
|
26
|
+
private endField;
|
|
27
|
+
private endRow;
|
|
28
|
+
write(chunk: string): void;
|
|
29
|
+
/** Flushes the last row when the input has no final line break. */
|
|
30
|
+
end(): void;
|
|
31
|
+
}
|
|
32
|
+
/** Parses a whole string (tests and small inputs). */
|
|
33
|
+
export declare function parseCsv(text: string, delimiter?: string): string[][];
|
|
34
|
+
/**
|
|
35
|
+
* Detects the delimiter from a text sample (the first 64 KiB): the candidate that splits most
|
|
36
|
+
* rows into the same number (> 1) of fields wins; ties keep the order `,` `;` TAB `|`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function detectDelimiter(sample: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Decodes a byte sample as the ingestion does: UTF-8 (BOM removed), UTF-16 by BOM, or
|
|
41
|
+
* windows-1252 when the bytes are not valid UTF-8. A sample cut inside a character is tolerated.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decodeTextBytes(bytes: Uint8Array, truncated?: boolean): {
|
|
44
|
+
text: string;
|
|
45
|
+
encoding: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming CSV/TSV parser (RFC 4180, spec §17.2): quoted fields, doubled quotes, line breaks
|
|
3
|
+
* inside quotes, CRLF/LF/CR rows. No dependencies and no I/O: callers push decoded text chunks
|
|
4
|
+
* (any split, even inside a quote or between CR and LF) and receive rows through a callback.
|
|
5
|
+
* Blank lines are skipped. Lenient on purpose: a stray quote inside an unquoted field is literal
|
|
6
|
+
* and text after a closing quote is appended.
|
|
7
|
+
*/
|
|
8
|
+
const QUOTE = 34;
|
|
9
|
+
const CR = 13;
|
|
10
|
+
const LF = 10;
|
|
11
|
+
var State;
|
|
12
|
+
(function (State) {
|
|
13
|
+
/** Before the first character of a field. */
|
|
14
|
+
State[State["Start"] = 0] = "Start";
|
|
15
|
+
State[State["Unquoted"] = 1] = "Unquoted";
|
|
16
|
+
State[State["Quoted"] = 2] = "Quoted";
|
|
17
|
+
/** Right after a `"` inside a quoted field: either an escaped quote or the closing one. */
|
|
18
|
+
State[State["QuoteInQuoted"] = 3] = "QuoteInQuoted";
|
|
19
|
+
})(State || (State = {}));
|
|
20
|
+
export class CsvError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(message, code) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class CsvParser {
|
|
28
|
+
onRow;
|
|
29
|
+
maxCellChars;
|
|
30
|
+
state = State.Start;
|
|
31
|
+
field = "";
|
|
32
|
+
fields = [];
|
|
33
|
+
skipLineFeed = false;
|
|
34
|
+
/** The current field was opened with a quote (so `""` is a value, not a blank line). */
|
|
35
|
+
quotedField = false;
|
|
36
|
+
delimiter;
|
|
37
|
+
/** Rows delivered so far (blank lines excluded). */
|
|
38
|
+
rows = 0;
|
|
39
|
+
constructor(delimiter, onRow, maxCellChars = 1024 * 1024) {
|
|
40
|
+
this.onRow = onRow;
|
|
41
|
+
this.maxCellChars = maxCellChars;
|
|
42
|
+
if (delimiter.length !== 1)
|
|
43
|
+
throw new Error("The CSV delimiter must be one character");
|
|
44
|
+
this.delimiter = delimiter.charCodeAt(0);
|
|
45
|
+
}
|
|
46
|
+
add(text) {
|
|
47
|
+
this.field += text;
|
|
48
|
+
if (this.field.length > this.maxCellChars)
|
|
49
|
+
throw new CsvError(`A cell in row ${this.rows + 1} exceeds ${this.maxCellChars} characters`, "cell_too_large");
|
|
50
|
+
}
|
|
51
|
+
endField() {
|
|
52
|
+
this.fields.push(this.field);
|
|
53
|
+
this.field = "";
|
|
54
|
+
this.quotedField = false;
|
|
55
|
+
}
|
|
56
|
+
endRow() {
|
|
57
|
+
const blank = this.fields.length === 0 && this.field === "" && !this.quotedField;
|
|
58
|
+
if (!blank) {
|
|
59
|
+
this.endField();
|
|
60
|
+
this.rows++;
|
|
61
|
+
const row = this.fields;
|
|
62
|
+
this.fields = [];
|
|
63
|
+
this.state = State.Start;
|
|
64
|
+
this.onRow(row);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.state = State.Start;
|
|
68
|
+
this.quotedField = false;
|
|
69
|
+
}
|
|
70
|
+
write(chunk) {
|
|
71
|
+
const n = chunk.length;
|
|
72
|
+
let i = 0;
|
|
73
|
+
if (this.skipLineFeed && n > 0) {
|
|
74
|
+
if (chunk.charCodeAt(0) === LF)
|
|
75
|
+
i = 1;
|
|
76
|
+
this.skipLineFeed = false;
|
|
77
|
+
}
|
|
78
|
+
while (i < n) {
|
|
79
|
+
switch (this.state) {
|
|
80
|
+
case State.Start: {
|
|
81
|
+
if (chunk.charCodeAt(i) === QUOTE) {
|
|
82
|
+
this.state = State.Quoted;
|
|
83
|
+
this.quotedField = true;
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
this.state = State.Unquoted;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case State.Unquoted: {
|
|
91
|
+
let j = i;
|
|
92
|
+
let c = 0;
|
|
93
|
+
while (j < n) {
|
|
94
|
+
c = chunk.charCodeAt(j);
|
|
95
|
+
if (c === this.delimiter || c === LF || c === CR)
|
|
96
|
+
break;
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
if (j > i)
|
|
100
|
+
this.add(chunk.slice(i, j));
|
|
101
|
+
i = j;
|
|
102
|
+
if (j >= n)
|
|
103
|
+
break;
|
|
104
|
+
if (c === this.delimiter) {
|
|
105
|
+
this.endField();
|
|
106
|
+
this.state = State.Start;
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
i++;
|
|
111
|
+
if (c === CR) {
|
|
112
|
+
if (i < n && chunk.charCodeAt(i) === LF)
|
|
113
|
+
i++;
|
|
114
|
+
else if (i >= n)
|
|
115
|
+
this.skipLineFeed = true;
|
|
116
|
+
}
|
|
117
|
+
this.endRow();
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case State.Quoted: {
|
|
122
|
+
const quote = chunk.indexOf('"', i);
|
|
123
|
+
if (quote < 0) {
|
|
124
|
+
this.add(chunk.slice(i));
|
|
125
|
+
i = n;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (quote > i)
|
|
129
|
+
this.add(chunk.slice(i, quote));
|
|
130
|
+
i = quote + 1;
|
|
131
|
+
this.state = State.QuoteInQuoted;
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case State.QuoteInQuoted: {
|
|
136
|
+
const c = chunk.charCodeAt(i);
|
|
137
|
+
if (c === QUOTE) {
|
|
138
|
+
this.add('"');
|
|
139
|
+
this.state = State.Quoted;
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
else if (c === this.delimiter) {
|
|
143
|
+
this.endField();
|
|
144
|
+
this.state = State.Start;
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
else if (c === LF || c === CR) {
|
|
148
|
+
i++;
|
|
149
|
+
if (c === CR) {
|
|
150
|
+
if (i < n && chunk.charCodeAt(i) === LF)
|
|
151
|
+
i++;
|
|
152
|
+
else if (i >= n)
|
|
153
|
+
this.skipLineFeed = true;
|
|
154
|
+
}
|
|
155
|
+
// A quoted-empty field ("") is a real, empty value: the row is not blank.
|
|
156
|
+
this.state = State.Unquoted;
|
|
157
|
+
this.endRow();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Text after the closing quote: keep it (lenient).
|
|
161
|
+
this.state = State.Unquoted;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Flushes the last row when the input has no final line break. */
|
|
169
|
+
end() {
|
|
170
|
+
if (this.fields.length === 0 && this.field === "" && !this.quotedField)
|
|
171
|
+
return;
|
|
172
|
+
this.endRow();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Parses a whole string (tests and small inputs). */
|
|
176
|
+
export function parseCsv(text, delimiter = ",") {
|
|
177
|
+
const rows = [];
|
|
178
|
+
const parser = new CsvParser(delimiter, (row) => rows.push(row));
|
|
179
|
+
parser.write(text);
|
|
180
|
+
parser.end();
|
|
181
|
+
return rows;
|
|
182
|
+
}
|
|
183
|
+
const CANDIDATES = [",", ";", "\t", "|"];
|
|
184
|
+
/**
|
|
185
|
+
* Detects the delimiter from a text sample (the first 64 KiB): the candidate that splits most
|
|
186
|
+
* rows into the same number (> 1) of fields wins; ties keep the order `,` `;` TAB `|`.
|
|
187
|
+
*/
|
|
188
|
+
export function detectDelimiter(sample) {
|
|
189
|
+
let best = ",";
|
|
190
|
+
let bestScore = 0;
|
|
191
|
+
for (const candidate of CANDIDATES) {
|
|
192
|
+
const counts = new Map();
|
|
193
|
+
let rows = 0;
|
|
194
|
+
const parser = new CsvParser(candidate, (row) => {
|
|
195
|
+
rows++;
|
|
196
|
+
counts.set(row.length, (counts.get(row.length) ?? 0) + 1);
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
parser.write(sample);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// The last row may be cut by the sample boundary: it is never flushed (no end()).
|
|
205
|
+
if (rows < 1)
|
|
206
|
+
continue;
|
|
207
|
+
let width = 0;
|
|
208
|
+
let frequency = 0;
|
|
209
|
+
for (const [w, count] of counts)
|
|
210
|
+
if (w > 1 && (count > frequency || (count === frequency && w > width))) {
|
|
211
|
+
width = w;
|
|
212
|
+
frequency = count;
|
|
213
|
+
}
|
|
214
|
+
const score = frequency * 1000 + width;
|
|
215
|
+
if (score > bestScore) {
|
|
216
|
+
best = candidate;
|
|
217
|
+
bestScore = score;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return best;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Decodes a byte sample as the ingestion does: UTF-8 (BOM removed), UTF-16 by BOM, or
|
|
224
|
+
* windows-1252 when the bytes are not valid UTF-8. A sample cut inside a character is tolerated.
|
|
225
|
+
*/
|
|
226
|
+
export function decodeTextBytes(bytes, truncated = false) {
|
|
227
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe)
|
|
228
|
+
return { text: new TextDecoder("utf-16le").decode(bytes), encoding: "utf-16le" };
|
|
229
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff)
|
|
230
|
+
return { text: new TextDecoder("utf-16be").decode(bytes), encoding: "utf-16be" };
|
|
231
|
+
try {
|
|
232
|
+
return {
|
|
233
|
+
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: truncated }),
|
|
234
|
+
encoding: "utf-8",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return { text: new TextDecoder("windows-1252").decode(bytes), encoding: "windows-1252" };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { DatasetDetailWire, DatasetFormat, DatasetRef, DatasetRowsPage, SqlDatabase } from "@alisio/sdk";
|
|
2
|
+
import type { PythonResolution } from "../runtime-manager.ts";
|
|
3
|
+
import { type SampleRows } from "./describe.ts";
|
|
4
|
+
import { DataEngine, DataError } from "./engine-client.ts";
|
|
5
|
+
import type { SheetSummary } from "./engine-types.ts";
|
|
6
|
+
import { PageParamError } from "./rows.ts";
|
|
7
|
+
export { DataError, PageParamError };
|
|
8
|
+
export interface DatasetLimits {
|
|
9
|
+
maxUploadBytes: number;
|
|
10
|
+
maxRows: number;
|
|
11
|
+
queryTimeoutMs: number;
|
|
12
|
+
maxInteractiveRows: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_DATASET_LIMITS: DatasetLimits;
|
|
15
|
+
export declare const XLSX_REMEDY = "Export the sheet as CSV, or install Python 3.10+.";
|
|
16
|
+
/** The format of a file name, or undefined when it is not a supported dataset. */
|
|
17
|
+
export declare function datasetFormat(name: string): DatasetFormat | undefined;
|
|
18
|
+
export interface DatasetRecord {
|
|
19
|
+
id: string;
|
|
20
|
+
/** The session that created it. */
|
|
21
|
+
sessionId: string;
|
|
22
|
+
rootSessionId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
format: DatasetFormat;
|
|
25
|
+
sha256: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
blobHash?: string;
|
|
28
|
+
sourcePath?: string;
|
|
29
|
+
dbRelPath: string;
|
|
30
|
+
ingestVersion: number;
|
|
31
|
+
sheets: SheetSummary[];
|
|
32
|
+
createdAt: number;
|
|
33
|
+
}
|
|
34
|
+
export declare const toDatasetRef: (record: DatasetRecord) => DatasetRef;
|
|
35
|
+
export interface IngestInput {
|
|
36
|
+
rootSessionId: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
workspace: string;
|
|
39
|
+
/** Absolute path of the file to read (resolved and authorised by the caller). */
|
|
40
|
+
path: string;
|
|
41
|
+
/** Display name; defaults to the file name. */
|
|
42
|
+
name?: string;
|
|
43
|
+
blobHash?: string;
|
|
44
|
+
/** Recorded as `source_path` (a workspace or artifact file); never sent to clients. */
|
|
45
|
+
sourcePath?: string;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
onProgress?: (rows: number) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface DatasetServiceOptions {
|
|
50
|
+
/** State folder (`stateHome()` or the folder of `--db`). */
|
|
51
|
+
root: string;
|
|
52
|
+
db: SqlDatabase;
|
|
53
|
+
limits?: Partial<DatasetLimits>;
|
|
54
|
+
/** The Python interpreter (XLSX only); absent means XLSX is unsupported. */
|
|
55
|
+
python?: {
|
|
56
|
+
interpreter(signal: AbortSignal): Promise<PythonResolution>;
|
|
57
|
+
};
|
|
58
|
+
engine?: DataEngine;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
export declare class DatasetService {
|
|
62
|
+
private readonly options;
|
|
63
|
+
readonly limits: DatasetLimits;
|
|
64
|
+
readonly engine: DataEngine;
|
|
65
|
+
private inflight;
|
|
66
|
+
constructor(options: DatasetServiceOptions);
|
|
67
|
+
private now;
|
|
68
|
+
/** Absolute path of a dataset file. */
|
|
69
|
+
file(record: DatasetRecord): string;
|
|
70
|
+
get(id: string): DatasetRecord | undefined;
|
|
71
|
+
private touched;
|
|
72
|
+
/**
|
|
73
|
+
* Records that a dataset was used, for the retention sweep ("unused for `jobsDays`"): the
|
|
74
|
+
* modification time of its SQLite file (nothing else changes it after the ingestion), at most
|
|
75
|
+
* once every ten minutes per dataset. Best effort: a failure never affects the caller.
|
|
76
|
+
*/
|
|
77
|
+
private touch;
|
|
78
|
+
/** Every dataset (all sessions), for the retention sweep; does not count as a use. */
|
|
79
|
+
all(): DatasetRecord[];
|
|
80
|
+
/** A dataset of this root session; another session's id is `not_found`. */
|
|
81
|
+
getFor(id: string, rootSessionId: string): DatasetRecord;
|
|
82
|
+
list(rootSessionId: string): DatasetRecord[];
|
|
83
|
+
private bySha;
|
|
84
|
+
/**
|
|
85
|
+
* Ingests a file into a dataset, or returns the session's existing dataset with the same
|
|
86
|
+
* content (`reused`). Concurrent calls for the same content share one ingestion. A failure
|
|
87
|
+
* leaves no row and no file.
|
|
88
|
+
*/
|
|
89
|
+
ingest(input: IngestInput): Promise<{
|
|
90
|
+
record: DatasetRecord;
|
|
91
|
+
reused: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
private build;
|
|
94
|
+
/** The XLSX route (spec §17.6, option B): the Python standard-library helper, then statistics. */
|
|
95
|
+
private ingestXlsx;
|
|
96
|
+
/** The `alisio_runtime` package on disk for `python -m` (content-addressed, written once). */
|
|
97
|
+
private helperDir;
|
|
98
|
+
/** Schema and statistics of every sheet (`GET /api/datasets/:did`). */
|
|
99
|
+
detail(record: DatasetRecord): DatasetDetailWire;
|
|
100
|
+
/** First rows of a table, for `data_inspect` and the prompt summary. */
|
|
101
|
+
sample(record: DatasetRecord, table: string, rows: number, signal?: AbortSignal): Promise<SampleRows>;
|
|
102
|
+
/** The description for the model: `data_inspect` text. */
|
|
103
|
+
describe(record: DatasetRecord, sampleRows: number, maxChars: number, signal?: AbortSignal): Promise<{
|
|
104
|
+
detail: DatasetDetailWire;
|
|
105
|
+
samples: Map<string, SampleRows>;
|
|
106
|
+
text: string;
|
|
107
|
+
}>;
|
|
108
|
+
/** The ≤ 4 KB summary appended to a prompt that attaches the dataset. */
|
|
109
|
+
summary(record: DatasetRecord, signal?: AbortSignal): Promise<string>;
|
|
110
|
+
/**
|
|
111
|
+
* Runs a model-written read-only query (`data_query`): lexical guard, then a read-only
|
|
112
|
+
* connection in the engine process, with a row limit and a time limit.
|
|
113
|
+
*/
|
|
114
|
+
query(record: DatasetRecord, sql: string, options?: {
|
|
115
|
+
maxRows?: number;
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
}): Promise<import("./engine-types.ts").QueryResult>;
|
|
118
|
+
/** One page of a sheet for `SpreadsheetView` (spec §17.4). */
|
|
119
|
+
page(record: DatasetRecord, request: {
|
|
120
|
+
sheet?: string;
|
|
121
|
+
after?: string;
|
|
122
|
+
offset?: number;
|
|
123
|
+
limit?: number;
|
|
124
|
+
sort?: string;
|
|
125
|
+
dir?: "asc" | "desc";
|
|
126
|
+
filter?: string;
|
|
127
|
+
column?: string;
|
|
128
|
+
}, signal?: AbortSignal): Promise<DatasetRowsPage>;
|
|
129
|
+
/** Deletes a dataset file and row (connections are closed first, for Windows). */
|
|
130
|
+
remove(record: DatasetRecord): Promise<void>;
|
|
131
|
+
close(): void;
|
|
132
|
+
}
|