@alisio/core 0.5.2 → 0.6.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.
- package/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GuideProvider` (spec §8.3, D12, Lazy Load): layer 2 of the incremental context. Each topic is
|
|
3
|
+
* generated on request from the live state of the engines, so a guide never describes a limit, a
|
|
4
|
+
* package or a guarantee the engine does not have right now. Nothing is built at construction.
|
|
5
|
+
* Every topic is at most 4 KiB and carries an `etag` (truncated SHA-256 of its content); a caller
|
|
6
|
+
* that already holds the same etag gets `{ unchanged: true }`.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { jsApiGuide, jsPatternsGuide, jsToolsGuide, } from "./javascript.js";
|
|
10
|
+
import { pythonDashboardsGuide, pythonDataGuide, pythonEnvironmentGuide, pythonMlGuide, pythonRuntimeGuide, } from "./python.js";
|
|
11
|
+
export const JS_GUIDE_TOPICS = ["js.api", "js.patterns", "js.tools"];
|
|
12
|
+
export const PYTHON_GUIDE_TOPICS = [
|
|
13
|
+
"python.runtime",
|
|
14
|
+
"python.data",
|
|
15
|
+
"python.ml",
|
|
16
|
+
"python.dashboards",
|
|
17
|
+
"python.environment",
|
|
18
|
+
];
|
|
19
|
+
export const GUIDE_TOPICS = [...JS_GUIDE_TOPICS, ...PYTHON_GUIDE_TOPICS];
|
|
20
|
+
/** Hard bound of one topic (spec §8.3). */
|
|
21
|
+
export const GUIDE_MAX_BYTES = 4096;
|
|
22
|
+
/** Characters of the SHA-256 kept as the etag. */
|
|
23
|
+
export const ETAG_LENGTH = 16;
|
|
24
|
+
export class UnknownGuideTopicError extends Error {
|
|
25
|
+
constructor(topic, available) {
|
|
26
|
+
super(`Unknown guide topic "${topic}". Available: ${available.join(", ")}`);
|
|
27
|
+
this.name = "UnknownGuideTopicError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const etagOf = (content) => createHash("sha256").update(content).digest("hex").slice(0, ETAG_LENGTH);
|
|
31
|
+
/** Cuts to the byte bound on a character boundary, saying so. */
|
|
32
|
+
function bounded(content) {
|
|
33
|
+
if (Buffer.byteLength(content) <= GUIDE_MAX_BYTES)
|
|
34
|
+
return content;
|
|
35
|
+
const note = "\n[guide cut at 4 KiB]";
|
|
36
|
+
let out = content;
|
|
37
|
+
while (Buffer.byteLength(out) + Buffer.byteLength(note) > GUIDE_MAX_BYTES)
|
|
38
|
+
out = out.slice(0, -64);
|
|
39
|
+
return out + note;
|
|
40
|
+
}
|
|
41
|
+
export class GuideProvider {
|
|
42
|
+
#sources;
|
|
43
|
+
constructor(sources) {
|
|
44
|
+
this.#sources = sources;
|
|
45
|
+
}
|
|
46
|
+
/** The topics that exist now: the JavaScript ones with a ready engine, the Python ones with `python_run`. */
|
|
47
|
+
topics() {
|
|
48
|
+
return [
|
|
49
|
+
...(this.#sources.javascript ? JS_GUIDE_TOPICS : []),
|
|
50
|
+
...(this.#sources.python ? PYTHON_GUIDE_TOPICS : []),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
async get(request) {
|
|
54
|
+
const available = this.topics();
|
|
55
|
+
const topic = available.find((candidate) => candidate === request.topic);
|
|
56
|
+
if (!topic)
|
|
57
|
+
throw new UnknownGuideTopicError(request.topic, available);
|
|
58
|
+
const content = bounded(await this.#render(topic, request));
|
|
59
|
+
const etag = etagOf(content);
|
|
60
|
+
if (request.ifNoneMatch !== undefined && request.ifNoneMatch === etag)
|
|
61
|
+
return { unchanged: true, etag };
|
|
62
|
+
return { unchanged: false, topic, etag, content };
|
|
63
|
+
}
|
|
64
|
+
async #render(topic, request) {
|
|
65
|
+
switch (topic) {
|
|
66
|
+
case "js.api":
|
|
67
|
+
return jsApiGuide(await this.#js());
|
|
68
|
+
case "js.patterns":
|
|
69
|
+
return jsPatternsGuide(await this.#js());
|
|
70
|
+
case "js.tools":
|
|
71
|
+
return jsToolsGuide(request.catalog);
|
|
72
|
+
case "python.runtime":
|
|
73
|
+
return pythonRuntimeGuide(await this.#py());
|
|
74
|
+
case "python.data":
|
|
75
|
+
return pythonDataGuide(await this.#py());
|
|
76
|
+
case "python.ml":
|
|
77
|
+
return pythonMlGuide(await this.#py());
|
|
78
|
+
case "python.dashboards":
|
|
79
|
+
return pythonDashboardsGuide(await this.#py());
|
|
80
|
+
case "python.environment":
|
|
81
|
+
return pythonEnvironmentGuide(await this.#py());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#js() {
|
|
85
|
+
const source = this.#sources.javascript;
|
|
86
|
+
if (!source)
|
|
87
|
+
throw new Error("the JavaScript engine is not available");
|
|
88
|
+
return source();
|
|
89
|
+
}
|
|
90
|
+
#py() {
|
|
91
|
+
const source = this.#sources.python;
|
|
92
|
+
if (!source)
|
|
93
|
+
throw new Error("python_run is not available");
|
|
94
|
+
return source();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PythonBackend, PythonBackendId } from "../python/backend.ts";
|
|
2
|
+
import { type IsolationPolicy, type PythonRuntimeSetting } from "../python/backend-selector.ts";
|
|
3
|
+
import type { PythonEnvironment } from "./python.ts";
|
|
4
|
+
export interface PythonEnvironmentDeps {
|
|
5
|
+
backends: Partial<Record<PythonBackendId, PythonBackend>>;
|
|
6
|
+
/** `analysis.runtime`, read live. */
|
|
7
|
+
runtime(): PythonRuntimeSetting;
|
|
8
|
+
/** `analysis.isolation`, read live. */
|
|
9
|
+
isolation(): IsolationPolicy;
|
|
10
|
+
/** The extras installed in the managed virtual environment (`analysis`, `science`). */
|
|
11
|
+
managedExtras(): Promise<readonly string[]>;
|
|
12
|
+
/** `analysis.smartDashboard`. */
|
|
13
|
+
smartDashboard(): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function probePythonEnvironment(deps: PythonEnvironmentDeps, signal?: AbortSignal): Promise<PythonEnvironment>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gathers the `PythonEnvironment` snapshot the Python guides are written from: the backend the
|
|
3
|
+
* selector would choose RIGHT NOW (same chain, same settings as `python_run`, no script known) and
|
|
4
|
+
* the packages that backend really has. It probes readiness like a call would and never starts a
|
|
5
|
+
* script or installs anything.
|
|
6
|
+
*/
|
|
7
|
+
import { EXTRAS_DISTRIBUTIONS } from "../../analysis/requirements.js";
|
|
8
|
+
import { isForgeError } from "../errors.js";
|
|
9
|
+
import { createPythonBackendSelector, neededBackends, } from "../python/backend-selector.js";
|
|
10
|
+
import { PYODIDE_PROFILE_ORDER, PYODIDE_PROFILES } from "../python/pyodide-manifest.js";
|
|
11
|
+
const selector = createPythonBackendSelector();
|
|
12
|
+
async function packagesOf(backend, deps) {
|
|
13
|
+
if (backend.id === "wasm") {
|
|
14
|
+
const installed = (await backend.describe()).profiles ?? [];
|
|
15
|
+
const top = PYODIDE_PROFILE_ORDER.filter((profile) => installed.includes(profile)).at(-1);
|
|
16
|
+
return top
|
|
17
|
+
? {
|
|
18
|
+
known: true,
|
|
19
|
+
packages: [...PYODIDE_PROFILES[top]],
|
|
20
|
+
detail: `Pyodide profile "${top}" is installed (profiles: ${installed.join(", ")}).`,
|
|
21
|
+
}
|
|
22
|
+
: { known: true, packages: [], detail: "No Pyodide package profile is installed (base)." };
|
|
23
|
+
}
|
|
24
|
+
if (backend.id === "managed") {
|
|
25
|
+
const extras = await deps.managedExtras();
|
|
26
|
+
const top = extras.includes("science")
|
|
27
|
+
? "science"
|
|
28
|
+
: extras.includes("analysis")
|
|
29
|
+
? "analysis"
|
|
30
|
+
: undefined;
|
|
31
|
+
return top
|
|
32
|
+
? {
|
|
33
|
+
known: true,
|
|
34
|
+
packages: [...EXTRAS_DISTRIBUTIONS[top]],
|
|
35
|
+
detail: `The "${top}" extras are installed in the managed environment.`,
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
known: true,
|
|
39
|
+
packages: [],
|
|
40
|
+
detail: "No extras are installed; pass `extras` to ask the user to install them.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
known: false,
|
|
45
|
+
detail: "A container has the packages of its image; scripts cannot install anything (no network).",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function probePythonEnvironment(deps, signal) {
|
|
49
|
+
const runtime = deps.runtime();
|
|
50
|
+
const isolation = deps.isolation();
|
|
51
|
+
const smartDashboard = deps.smartDashboard();
|
|
52
|
+
const wanted = neededBackends({ runtime, isolation });
|
|
53
|
+
const readiness = {};
|
|
54
|
+
const networkDenied = {};
|
|
55
|
+
await Promise.all(wanted.map(async (id) => {
|
|
56
|
+
const backend = deps.backends[id];
|
|
57
|
+
readiness[id] = backend
|
|
58
|
+
? await backend.check(signal)
|
|
59
|
+
: { ready: false, reason: `${id} is not available here` };
|
|
60
|
+
if (backend && readiness[id]?.ready && isolation === "strict")
|
|
61
|
+
networkDenied[id] = (await backend.describe()).capabilities.networkDeniedByOS;
|
|
62
|
+
}));
|
|
63
|
+
try {
|
|
64
|
+
const chosen = selector.select({ runtime, isolation, readiness, networkDenied });
|
|
65
|
+
const backend = deps.backends[chosen.engine];
|
|
66
|
+
if (!backend)
|
|
67
|
+
return { available: false, reason: `${chosen.engine} is not available here`, smartDashboard };
|
|
68
|
+
return {
|
|
69
|
+
available: true,
|
|
70
|
+
backend: chosen.engine,
|
|
71
|
+
reason: chosen.reason,
|
|
72
|
+
packages: await packagesOf(backend, deps),
|
|
73
|
+
smartDashboard,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isForgeError(error))
|
|
78
|
+
throw error;
|
|
79
|
+
return { available: false, reason: error.message, smartDashboard };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Python topics of `forge_guide` (spec §8.3; Python spec §4.3, §6, §7, §8): `python.runtime`,
|
|
3
|
+
* `python.data`, `python.ml`, `python.dashboards` and `python.environment`. Pure functions of a
|
|
4
|
+
* `PythonEnvironment` snapshot: the backend the selector would choose right now and what that
|
|
5
|
+
* backend really provides. No guide ever claims a guarantee the chosen backend does not give.
|
|
6
|
+
*/
|
|
7
|
+
import type { PythonBackendId } from "../python/backend.ts";
|
|
8
|
+
export type PackageKnowledge =
|
|
9
|
+
/** The packages this backend provides right now (a distribution name each). */
|
|
10
|
+
{
|
|
11
|
+
known: true;
|
|
12
|
+
packages: string[];
|
|
13
|
+
detail: string;
|
|
14
|
+
}
|
|
15
|
+
/** The backend's packages cannot be listed from here (a container image). */
|
|
16
|
+
| {
|
|
17
|
+
known: false;
|
|
18
|
+
detail: string;
|
|
19
|
+
};
|
|
20
|
+
export type PythonEnvironment = {
|
|
21
|
+
available: true;
|
|
22
|
+
backend: PythonBackendId;
|
|
23
|
+
/** Why the selector chose it (its own `reason`). */
|
|
24
|
+
reason: string;
|
|
25
|
+
packages: PackageKnowledge;
|
|
26
|
+
/** `analysis.smartDashboard`: `dashboard_generate` exists. */
|
|
27
|
+
smartDashboard: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
available: false;
|
|
30
|
+
/** The selector's message: every backend's reason and the shortest fix. */
|
|
31
|
+
reason: string;
|
|
32
|
+
smartDashboard: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare function pythonRuntimeGuide(env: PythonEnvironment): string;
|
|
35
|
+
export declare function pythonDataGuide(env: PythonEnvironment): string;
|
|
36
|
+
export declare function pythonMlGuide(env: PythonEnvironment): string;
|
|
37
|
+
export declare function pythonDashboardsGuide(env: PythonEnvironment): string;
|
|
38
|
+
export declare function pythonEnvironmentGuide(env: PythonEnvironment): string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Python spec §4.3, one row per backend; the guide never states more than this. */
|
|
2
|
+
const GUARANTEES = {
|
|
3
|
+
managed: [
|
|
4
|
+
"Isolation: none. The script runs with your user permissions: it can read your files and use the network.",
|
|
5
|
+
"Termination: the process is killed on timeout. Memory: no hard limit.",
|
|
6
|
+
],
|
|
7
|
+
oci: [
|
|
8
|
+
"Isolation: os-container. Network off (`--network=none`); only the job folders are mounted (the scratchpad read-only).",
|
|
9
|
+
"Termination: the container is killed on timeout. Memory: hard limit (cgroup).",
|
|
10
|
+
],
|
|
11
|
+
wasm: [
|
|
12
|
+
"Isolation: wasm-process (Pyodide in a restricted Node process). It reads only the Pyodide distribution and the job, and writes only its work/ and out/ folders.",
|
|
13
|
+
"Network: NOT blocked by the OS on Node 22/24; do not rely on it being off. Memory: soft limit (RSS watchdog). Termination: the process is killed on timeout.",
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
const SETUP_NOTE = "Setup commands are run by the user, never by you: `alisio analysis setup --wasm [--profile analysis|science]` or `alisio analysis setup --oci`.";
|
|
17
|
+
const has = (env, pkg) => {
|
|
18
|
+
if (!env.available)
|
|
19
|
+
return false;
|
|
20
|
+
if (!env.packages.known)
|
|
21
|
+
return undefined;
|
|
22
|
+
return env.packages.packages.includes(pkg);
|
|
23
|
+
};
|
|
24
|
+
/** What the model should know about one package group right now. */
|
|
25
|
+
function availability(env, pkgs) {
|
|
26
|
+
const states = pkgs.map((pkg) => has(env, pkg));
|
|
27
|
+
if (states.every((s) => s === true))
|
|
28
|
+
return "available";
|
|
29
|
+
if (states.every((s) => s === undefined))
|
|
30
|
+
return "depends on the container image (not listed here)";
|
|
31
|
+
const missing = pkgs.filter((_, i) => states[i] === false);
|
|
32
|
+
return `NOT available (${missing.join(", ")} missing)`;
|
|
33
|
+
}
|
|
34
|
+
function installAdvice(env, profile) {
|
|
35
|
+
if (env.backend === "wasm")
|
|
36
|
+
return `ask the user to run \`alisio analysis setup --wasm --profile ${profile}\``;
|
|
37
|
+
if (env.backend === "managed")
|
|
38
|
+
return `pass \`extras: ["${profile}"]\` (the user approves the download once)`;
|
|
39
|
+
return "use an image that includes them (`analysis.oci.image`)";
|
|
40
|
+
}
|
|
41
|
+
export function pythonRuntimeGuide(env) {
|
|
42
|
+
return [
|
|
43
|
+
"# python_run runtime",
|
|
44
|
+
"",
|
|
45
|
+
"Your script runs once in a job folder outside the repository, with `main.py` as the entry. The helper package `alisio_runtime` (standard library only) is importable.",
|
|
46
|
+
"",
|
|
47
|
+
"## Folders (environment variables)",
|
|
48
|
+
"- `ALISIO_OUTPUT_DIR`: everything written here is published as a downloadable artifact only if the script exits 0 (`publishOnError` keeps partial output). Publication is validated by the host: paths, sizes and types. An `outputs.json` limits publishing to the files it declares.",
|
|
49
|
+
"- `ALISIO_INPUT_DIR`: read-only copies of `inputs`. `ALISIO_WORK_DIR`: scratch, also the current directory.",
|
|
50
|
+
"- `ALISIO_SCRATCH_DIR`: the session scratchpad, READ-ONLY (write scripts there with the file tools, run them with `codePath`).",
|
|
51
|
+
"- `ALISIO_EXECUTION_ID` identifies the run. `ALISIO_FORGE_*` belong to the protocol: never print them or fake their lines.",
|
|
52
|
+
"",
|
|
53
|
+
"## alisio_runtime",
|
|
54
|
+
"- `output_dir()`, `input_dir()`, `work_dir()`, `execution_id()`; `outputs.path(name)` makes a new output path, `outputs.declare(name, title)` registers it.",
|
|
55
|
+
"- `forge.progress(message, completed=, total=)`, `forge.text(value)` (explicit output for you), `forge.result(value)` (the final JSON value), `forge.state.get/set/delete` (small JSON shared with js_run; kept only if the run succeeds).",
|
|
56
|
+
"- `datasets`, `html`, `charts`, `svg`: see `python.data` and `python.dashboards`.",
|
|
57
|
+
"",
|
|
58
|
+
"## Rules",
|
|
59
|
+
"- Print little: stdout and stderr return only their last 4 KB. Use `forge.text`/`forge.result` for what you need to read.",
|
|
60
|
+
"- No nested tool calls and no JavaScript from Python. Chain tools yourself, or compose with `js_run` first.",
|
|
61
|
+
"- Keep each script short; save long ones to the scratchpad. Rerun an earlier analysis unchanged with `rerunOf`.",
|
|
62
|
+
`- Packages depend on the backend: see \`python.environment\` (now: ${env.available ? env.backend : "no backend can run"}).`,
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
export function pythonDataGuide(env) {
|
|
66
|
+
const pandas = availability(env, ["pandas"]);
|
|
67
|
+
return [
|
|
68
|
+
"# Data in python_run",
|
|
69
|
+
"",
|
|
70
|
+
"## Which tool",
|
|
71
|
+
"- A SQL question on a loaded dataset: `data_query` (no code, read-only). Use `data_inspect` first for tables and columns.",
|
|
72
|
+
"- Joins across files, statistics, models, exports, or anything SQL cannot do: `python_run`.",
|
|
73
|
+
"",
|
|
74
|
+
"## Inputs",
|
|
75
|
+
"- `inputs: [{ datasetId }]` copies a dataset to `$ALISIO_INPUT_DIR/<name>.sqlite`; `inputs: [{ path }]` copies a workspace file.",
|
|
76
|
+
"- `from alisio_runtime import datasets`: `datasets.inputs()` lists what was placed; `datasets.open(name)` returns a read-only `sqlite3` connection (table `data`, or `s_<sheet>` for workbooks); `datasets.tables(name)` and `datasets.columns(name, table)` describe them.",
|
|
77
|
+
"- XLSX files are converted to SQLite by `alisio_runtime/xlsx_to_sqlite.py` (standard library `sqlite3`); you rarely need to call it yourself.",
|
|
78
|
+
"",
|
|
79
|
+
"## pandas",
|
|
80
|
+
`- pandas is ${pandas} on the backend that would run now.`,
|
|
81
|
+
"- Without pandas use `sqlite3`, `csv`, `statistics` and `json` from the standard library.",
|
|
82
|
+
'- `pandas.read_sql_query("SELECT * FROM data", datasets.open(name))` loads a dataset.',
|
|
83
|
+
...(env.available && env.backend === "wasm"
|
|
84
|
+
? ["- The wasm backend limits the total input size (`analysis.wasm.maxInputBytes`)."]
|
|
85
|
+
: []),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
export function pythonMlGuide(env) {
|
|
89
|
+
const science = availability(env, ["scipy", "statsmodels", "scikit-learn"]);
|
|
90
|
+
const where = env.available
|
|
91
|
+
? `Backend that would run now: ${env.backend}.`
|
|
92
|
+
: "No Python backend can run right now (see `python.environment`).";
|
|
93
|
+
return [
|
|
94
|
+
"# Statistics and machine learning",
|
|
95
|
+
"",
|
|
96
|
+
where,
|
|
97
|
+
`- scikit-learn, statsmodels and scipy are ${science}.`,
|
|
98
|
+
...(env.available && has(env, "scikit-learn") === false
|
|
99
|
+
? [`- To get them: ${installAdvice(env, "science")}.`]
|
|
100
|
+
: []),
|
|
101
|
+
"",
|
|
102
|
+
"## Patterns",
|
|
103
|
+
"- Split first: `train_test_split(X, y, test_size=0.2, random_state=0)`; fix `random_state` so a rerun gives the same numbers.",
|
|
104
|
+
"- Tabular baseline: `sklearn.ensemble.RandomForestClassifier` or `sklearn.linear_model.LogisticRegression`; report accuracy AND a baseline (majority class) with `forge.result({...})`.",
|
|
105
|
+
"- Inference and tests: `scipy.stats.ttest_ind`, `chi2_contingency`; regression with coefficients and p-values: `statsmodels.api.OLS(y, sm.add_constant(X)).fit().summary()`.",
|
|
106
|
+
'- Return numbers, not objects: `forge.result({"r2": float(r2), "n": int(n)})`. JSON cannot hold NaN or Infinity; convert them first.',
|
|
107
|
+
"- Keep a time budget in mind: heavy training can exceed the call's timeout. Subsample first, then scale up.",
|
|
108
|
+
"- Pandas first, then models; without the science packages use the standard library `statistics` for descriptive work.",
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
export function pythonDashboardsGuide(env) {
|
|
112
|
+
const plotly = availability(env, ["plotly"]);
|
|
113
|
+
const matplotlib = availability(env, ["matplotlib"]);
|
|
114
|
+
return [
|
|
115
|
+
"# Charts and dashboards",
|
|
116
|
+
"",
|
|
117
|
+
...(env.smartDashboard
|
|
118
|
+
? [
|
|
119
|
+
"- A standard dashboard from a dataset: `dashboard_generate` (no code). Use `python_run` only for visuals its catalog does not cover.",
|
|
120
|
+
]
|
|
121
|
+
: ["- `dashboard_generate` is not enabled here: build dashboards with `alisio_runtime`."]),
|
|
122
|
+
"",
|
|
123
|
+
"## alisio_runtime (no extra packages)",
|
|
124
|
+
"- `charts`: Chart.js is bundled. `charts.pie/donut/bar/hbar/line/area/scatter(labels, values, title=, fmt='currency:USD', locale=)` return HTML; compose with `charts.card(title, chart)`, `charts.grid(*cards)`, `charts.kpis((label, value), ...)`, then `charts.write('dashboard.html', 'Title', body)`.",
|
|
125
|
+
"- `html`: `html.page/table/write` for plain reports. `svg`: static SVG, no script.",
|
|
126
|
+
"- Never hand-write SVG arcs, never load a CDN: HTML is shown offline, so inline every script and style. One chart per card; pie only for 2 to 5 parts.",
|
|
127
|
+
"",
|
|
128
|
+
"## Static images",
|
|
129
|
+
`- matplotlib is ${matplotlib}. It runs with the Agg backend: save a PNG with \`fig.savefig(output_dir() / 'chart.png', dpi=150)\`; \`plt.show()\` does nothing.`,
|
|
130
|
+
`- plotly is ${plotly}. Write a self-contained file with \`fig.write_html(path, include_plotlyjs=True)\` (several MB); prefer \`charts\` when it is enough.`,
|
|
131
|
+
"",
|
|
132
|
+
"A folder under the output directory with an `index.html` is published as one multi-file dashboard.",
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
export function pythonEnvironmentGuide(env) {
|
|
136
|
+
if (!env.available)
|
|
137
|
+
return [
|
|
138
|
+
"# Python environment",
|
|
139
|
+
"",
|
|
140
|
+
"No Python backend can run a script right now.",
|
|
141
|
+
"",
|
|
142
|
+
env.reason,
|
|
143
|
+
"",
|
|
144
|
+
SETUP_NOTE,
|
|
145
|
+
"Until one is ready, use `js_run` or the dataset tools (`data_inspect`, `data_query`).",
|
|
146
|
+
].join("\n");
|
|
147
|
+
const packages = env.packages.known
|
|
148
|
+
? env.packages.packages.length
|
|
149
|
+
? env.packages.packages.join(", ")
|
|
150
|
+
: "none beyond the standard library"
|
|
151
|
+
: "those baked into the container image (not listed here)";
|
|
152
|
+
return [
|
|
153
|
+
"# Python environment",
|
|
154
|
+
"",
|
|
155
|
+
`Backend that runs your next script: ${env.backend} (${env.reason}).`,
|
|
156
|
+
"",
|
|
157
|
+
"## Guarantees of this backend",
|
|
158
|
+
...GUARANTEES[env.backend].map((line) => `- ${line}`),
|
|
159
|
+
"",
|
|
160
|
+
"## Packages here",
|
|
161
|
+
`- Standard library and \`alisio_runtime\` always. ${env.packages.detail}`,
|
|
162
|
+
`- Installed: ${packages}.`,
|
|
163
|
+
"- Anything else fails with `PACKAGE_UNAVAILABLE`/`ModuleNotFoundError` and is never retried on another backend.",
|
|
164
|
+
...(env.packages.known && !env.packages.packages.includes("scikit-learn")
|
|
165
|
+
? ["", `For scikit-learn, statsmodels and scipy: ${installAdvice(env, "science")}.`]
|
|
166
|
+
: []),
|
|
167
|
+
"",
|
|
168
|
+
SETUP_NOTE,
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EngineDescriptor } from "@alisio/sdk";
|
|
2
|
+
import type { EngineOutcome, ExecutionEngine, ExecutionRequest, IsolateHost, IsolateSession, KernelChannel, PreparedExecution, TerminationReason, TerminationReport, TrustedExecutionContext } from "../ports.ts";
|
|
3
|
+
import type { IsolateHostFactory } from "./hosts/factory.ts";
|
|
4
|
+
export interface PreparedJavaScript extends PreparedExecution {
|
|
5
|
+
readonly source: string;
|
|
6
|
+
/** The state snapshot the script starts with. */
|
|
7
|
+
readonly state: Record<string, unknown>;
|
|
8
|
+
readonly catalogSize: number;
|
|
9
|
+
readonly host: IsolateHost;
|
|
10
|
+
session?: IsolateSession;
|
|
11
|
+
/** Ends the run with a host verdict; set by `run`, used when a stop cannot be confirmed. */
|
|
12
|
+
forceEnd?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface JavaScriptEngineOptions {
|
|
15
|
+
hosts: IsolateHostFactory;
|
|
16
|
+
/** The verified module; defaults to the process-wide cache. */
|
|
17
|
+
loadModule?: () => Promise<WebAssembly.Module>;
|
|
18
|
+
}
|
|
19
|
+
export declare class JavaScriptEngine implements ExecutionEngine {
|
|
20
|
+
#private;
|
|
21
|
+
readonly id: "javascript-quickjs";
|
|
22
|
+
constructor(options: JavaScriptEngineOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Cheap self-check (runs at startup): the WASM matches its pinned hash and compiles, and the
|
|
25
|
+
* files the chosen host starts exist. The full spin-up of an isolate is `probe()`.
|
|
26
|
+
*/
|
|
27
|
+
describe(): Promise<EngineDescriptor>;
|
|
28
|
+
/** The kind of host that runs the isolates (`forge status`). */
|
|
29
|
+
get hostKind(): "worker" | "process";
|
|
30
|
+
prepare(request: ExecutionRequest, ctx: TrustedExecutionContext): Promise<PreparedJavaScript>;
|
|
31
|
+
run(prepared: PreparedExecution, channel: KernelChannel): Promise<EngineOutcome>;
|
|
32
|
+
terminate(prepared: PreparedExecution, reason: TerminationReason): Promise<TerminationReport>;
|
|
33
|
+
dispose(prepared: PreparedExecution): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Full self-test (`alisio forge status`): starts a real isolate, runs `return 1` and reports how
|
|
36
|
+
* long it took. Nothing here is on the hot path of a normal execution.
|
|
37
|
+
*/
|
|
38
|
+
probe(): Promise<{
|
|
39
|
+
ok: boolean;
|
|
40
|
+
reason?: string;
|
|
41
|
+
bootMs?: number;
|
|
42
|
+
host: "worker" | "process";
|
|
43
|
+
}>;
|
|
44
|
+
}
|