Skip to content

Commit 378d370

Browse files
committed
Machine: 통합 진입점 Builder·Fluent·10프리셋·JIT Cascade·Auto-Discovery·레시피 생태계
pyproc root에 Machine 클래스를 추가해 제로 설정, Builder 체인, Fluent API, 10개 내장 프리셋, 사용자 레시피 등록, JIT auto-discovery, asServer() 통합, Blueprint 파일 로딩(Machine.fromFile), auto-optimize, 액션 가능한 오류 메시지를 하나의 import로 제공한다. 모든 기존 API(open, boot, createWebComputer)는 하위 호환을 유지한다. unMachine.js: Machine + MachineBuilder + MachineHandle + JIT Cascade (460줄) index.js/d.ts: Machine export, 완전한 타입 선언과 @param/@throws JSDoc tests/northStar.mjs: 진입점 축 UX 중심 재정의 (10.0→9.5), evidence 승격 docs/reference/api.md: Machine 언급 반영 package.json: unMachine.js 배포 포함 북극성: 9.5 (104.7/120) 검증: npm test 3188 passed, 구조게이트 표면·계약·README·링크 통과
1 parent 125f225 commit 378d370

6 files changed

Lines changed: 836 additions & 13 deletions

File tree

docs/reference/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# pyproc API reference
22

3-
The root surface is exactly six exports and gathers the complete product choice: `open` for the
4-
durable Python Machine and source-specific revival, `boot` for an explicit transient Machine,
3+
The root surface gathers the complete product choice through `Machine` (unified Builder/Fluent/Preset),
4+
`open` for the durable Python Machine and source-specific revival, `boot` for an explicit transient Machine,
55
`createWebComputer` for the multi-guest host, `checkEnvironment` for preflight, and the shared error
66
contract (`PyProcError`, `PYPROC_ERROR_CODES`). Everything else is a verb on a returned handle, an
77
advanced escape hatch, or a plumbing subpath. Signatures are

index.d.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,202 @@ declare class PyProc {
11981198

11991199
export { createWebComputer, type WebComputer } from "./src/machine/index.js";
12001200

1201+
// ── Unified Machine entrance (new) ──────────────────────────────────
1202+
// One import that gathers every product door through Builder + Fluent API + JIT capability cascade.
1203+
1204+
/** Machine lifecycle mode. */
1205+
export type MachineMode = "durable" | "transient" | "portable";
1206+
1207+
/** Network policy: fail-closed (default) or fail-open. */
1208+
export type NetworkPolicy = "fail-closed" | "fail-open";
1209+
1210+
/** Declarative machine blueprint - the single source of truth for machine configuration. */
1211+
export interface MachineBlueprint {
1212+
/** Durable machine name (OPFS key). Defaults to "default". */
1213+
name?: string;
1214+
/** Machine lifecycle mode. Defaults to "durable". */
1215+
mode?: MachineMode;
1216+
/** Python guest configuration. */
1217+
python?: {
1218+
/** Pyodide distribution URL. */
1219+
indexURL?: string;
1220+
/** Packages to preload at boot. */
1221+
packages?: string[];
1222+
/** Environment variables. */
1223+
env?: Record<string, string>;
1224+
/** Python code run after boot. */
1225+
setup?: string;
1226+
/** Opt into deterministic replay boot. */
1227+
deterministic?: boolean;
1228+
/** Engine script SRI. */
1229+
engineScriptIntegrity?: string | false;
1230+
/** Core asset SRI policy. */
1231+
coreIntegrity?: CoreIntegrityMap | CoreIntegrityPolicy | false;
1232+
/** Core asset cache directory. */
1233+
coreCacheDir?: FileSystemDirectoryHandle;
1234+
/** Wheel cache directory. */
1235+
wheelDir?: FileSystemDirectoryHandle;
1236+
};
1237+
/** Process pool configuration. */
1238+
processes?: {
1239+
/** Number of worker lanes (cores). Default 2. */
1240+
lanes?: number;
1241+
/** Use snapshot fork. Default true. */
1242+
useSnapshot?: boolean;
1243+
/** Packages to load per worker. */
1244+
packages?: string[];
1245+
};
1246+
/** Network policy configuration. */
1247+
network?: {
1248+
/** Connection policy. Default "fail-closed". */
1249+
policy?: NetworkPolicy;
1250+
/** Allowed hosts when policy is fail-closed. */
1251+
allowHosts?: string[];
1252+
};
1253+
/** History configuration. */
1254+
history?: {
1255+
/** Enable journal. */
1256+
journal?: boolean;
1257+
/** Journal directory in OPFS. */
1258+
journalDir?: FileSystemDirectoryHandle;
1259+
/** Idle commit interval in ms. */
1260+
idleMs?: number;
1261+
/** Auto-pack after commit. */
1262+
autoPack?: boolean;
1263+
};
1264+
/** Machine image configuration. */
1265+
image?: {
1266+
/** Enable signing. */
1267+
sign?: boolean;
1268+
/** Signing key. */
1269+
signingKey?: CryptoKey | CryptoKeyPair;
1270+
};
1271+
/** Filesystem configuration. */
1272+
filesystem?: {
1273+
/** Persist the mount. */
1274+
persist?: boolean;
1275+
/** Directory to mount. */
1276+
mountDir?: FileSystemDirectoryHandle;
1277+
/** Home path. Default "/home/web". */
1278+
homePath?: string;
1279+
};
1280+
/** Terminal configuration. */
1281+
terminal?: {
1282+
/** Enable time travel (%undo). */
1283+
timeTravel?: boolean;
1284+
};
1285+
/** Enable auto-optimization (core detection, etc). */
1286+
autoOptimize?: boolean;
1287+
}
1288+
1289+
/** Fluent builder returned by Machine.create(). Chains .with*() calls, then .launch(). */
1290+
declare class MachineBuilder {
1291+
/** Configures the Python guest. */
1292+
withPython(options?: MachineBlueprint["python"]): MachineBuilder;
1293+
/** Shorthand for withPython({ packages }). */
1294+
withPackages(...packages: string[]): MachineBuilder;
1295+
/** Configures the process pool. */
1296+
withProcesses(options?: MachineBlueprint["processes"]): MachineBuilder;
1297+
/** Configures network policy. */
1298+
withNetwork(options?: MachineBlueprint["network"]): MachineBuilder;
1299+
/** Configures history (journal / checkpoint). */
1300+
withHistory(options?: MachineBlueprint["history"]): MachineBuilder;
1301+
/** Configures machine image (export / sign). */
1302+
withImage(options?: MachineBlueprint["image"]): MachineBuilder;
1303+
/** Configures the filesystem. */
1304+
withFilesystem(options?: MachineBlueprint["filesystem"]): MachineBuilder;
1305+
/** Configures the terminal. */
1306+
withTerminal(options?: MachineBlueprint["terminal"]): MachineBuilder;
1307+
/** Enables auto-optimization. */
1308+
withAutoOptimize(options?: boolean | Record<string, unknown>): MachineBuilder;
1309+
/** Enables JIT auto-discovery. Python code patterns (import, network, subprocess) are detected and capabilities auto-activated. */
1310+
withAutoDiscover(enabled?: boolean): MachineBuilder;
1311+
/** Returns the current blueprint (for debugging/saving). */
1312+
toBlueprint(): MachineBlueprint;
1313+
/** Boots the machine and returns a handle. */
1314+
launch(): Promise<MachineHandle>;
1315+
}
1316+
1317+
/** Handle returned by Machine.create().launch(). All capabilities are JIT-activated. */
1318+
declare class MachineHandle {
1319+
/** Execute Python code. With autoDiscover, detects import/network patterns and auto-activates capabilities. @throws {PyProcError} PYPROC_KERNEL_EXECUTION_ERROR */
1320+
run(code: string): unknown;
1321+
/** Execute Python asynchronously (JSPI path). With autoDiscover, detects patterns automatically. @throws {PyProcError} PYPROC_KERNEL_EXECUTION_ERROR */
1322+
runAsync(code: string): Promise<unknown>;
1323+
/** Execute Python code with full auto-discovery even if autoDiscover is off. Detects imports, network, subprocess. */
1324+
runWithDiscovery(code: string): Promise<unknown>;
1325+
/** Load packages into the machine. @throws {PyProcError} PYPROC_BOOT_FAILED if engine not ready */
1326+
loadPackages(packages: string[]): Promise<unknown>;
1327+
/** Filesystem (JIT-initialized on first access). @throws {PyProcError} PYPROC_INPUT_INVALID if engine lacks FS support */
1328+
readonly fs: FileSystem;
1329+
/** Process pool for parallel execution (JIT-initialized on first call). @param options.lanes Number of workers (default 2). @param options.useSnapshot Use snapshot-fork. @throws {PyProcError} PYPROC_INPUT_INVALID if not available (use transient mode) */
1330+
proc(options?: { lanes?: number; useSnapshot?: boolean; packages?: string[] }): Promise<PyProc>;
1331+
/** Terminal REPL (JIT-activated). @param options.timeTravel Enable %undo checkpoint. */
1332+
term(options?: { timeTravel?: boolean }): Promise<Terminal>;
1333+
/** Network syscall bridge (JIT-activated). Enables urllib, input(), subprocess. @param options.proxyUrl Relay URL. @param options.requests Wire up requests library. */
1334+
enableNetwork(options?: { proxyUrl?: string; requests?: boolean }): Promise<SyscallBridge>;
1335+
/** In-kernel ASGI server (JIT-activated). Dispatch FastAPI/Starlette with zero sockets. */
1336+
enableAsgi(options?: { app?: string }): Promise<AsgiServer>;
1337+
/** Virtual origin for the ASGI server (JIT-activated). Call after enableAsgi(). */
1338+
enableVirtualOrigin(options?: { app?: string }): Promise<VirtualOrigin>;
1339+
/** Permission jail (JIT-activated). @param permissions.net false | true | string[]. @param permissions.clipboard boolean. */
1340+
enableJail(permissions?: JailPermissions): Promise<{ jail: MachineJail; permissions: JailPermissions; connectSrc: string }>;
1341+
/** Device filesystem (JIT-activated). Expose /proc/meminfo, /dev/clipboard, /dev/random, /dev/fb0. */
1342+
enableDeviceFs(options?: DeviceFsConfig): Promise<DeviceFs>;
1343+
/** Wheel package cache (JIT-activated). @param options.dir FileSystemDirectoryHandle. */
1344+
enableWheelCache(options: { dir: FileSystemDirectoryHandle }): Promise<WheelCache>;
1345+
/** OS init (JIT-activated). Runs boot.py on boot, resume.py after revival, cron.py periodically. */
1346+
enableInit(options?: InitConfig): Promise<Init>;
1347+
/** Complete web server setup: ASGI + VirtualOrigin + Network in one call. @returns { asgi, virtualOrigin, syscall } */
1348+
asServer(options?: { app?: string; proxyUrl?: string }): Promise<{ asgi: AsgiServer; virtualOrigin: VirtualOrigin; syscall: SyscallBridge | null }>;
1349+
/** History handle. Checkpoint/restore (volatile), commit/export (durable). */
1350+
readonly history: PyprocHistory;
1351+
/** Whether this machine was booted with deterministic replay. */
1352+
readonly deterministic: boolean;
1353+
/** Internal Runtime (advanced use). */
1354+
readonly runtime: Runtime;
1355+
/** Summarizes machine status. */
1356+
status(): { mode: string; deterministic: boolean; packages: string[]; processes: number | null; network: string; jail: string | null; asgi: string | null; autoDiscover: boolean };
1357+
/** Export this machine as a signed .pymachine blob. Deterministic machines only. @throws {PyProcError} PYPROC_INPUT_INVALID if not deterministic */
1358+
exportImage(options?: { signingKey?: CryptoKey | CryptoKeyPair; includeHome?: boolean; allowHostProxies?: boolean }): Promise<Blob>;
1359+
/** Disposes all resources: process pool, job control, containers, reactive tree. */
1360+
dispose(): Promise<void>;
1361+
/** Declares an external heap mutation (e.g. through a live PyProxy). */
1362+
markDirty(): unknown;
1363+
/** Shell job control (JIT). `expr &` forks to another core. @param options.workers Pool size (default 3). */
1364+
jobs(options?: { workers?: number; replay?: Record<string, unknown> }): Promise<JobControl>;
1365+
/** Machine-in-machine containers (JIT). @param options.indexURL Engine distribution. */
1366+
containers(options?: { indexURL?: string }): Promise<MachineContainer>;
1367+
}
1368+
1369+
/** The unified product entrance. Single import, zero config, maximum capability discovery. */
1370+
declare class Machine {
1371+
/** Starts the builder. Pass nothing for default durable, a blueprint object for declarative config, or use Machine.fromPreset("name"). @throws {PyProcError} PYPROC_INPUT_INVALID if blueprint is invalid */
1372+
static create(blueprint?: MachineBlueprint): MachineBuilder;
1373+
/** Creates a machine from a built-in preset or registered recipe. 10 built-in: data-science, ai-sandbox, repl, max-performance, web-server, ml-training, etl-pipeline, education, dev-server, default. */
1374+
static fromPreset(name: string): MachineBuilder;
1375+
/** Loads a blueprint JSON file from a URL and returns a builder. @throws {PyProcError} PYPROC_INPUT_INVALID if fetch fails or JSON is invalid */
1376+
static fromFile(url: string): Promise<MachineBuilder>;
1377+
/** Registers a custom recipe blueprint. After registration, Machine.fromPreset(name) resolves it. @throws {PyProcError} PYPROC_INPUT_INVALID if name is already a built-in preset */
1378+
static registerRecipe(name: string, blueprint: MachineBlueprint): void;
1379+
/** Removes a registered recipe. Built-in presets cannot be removed. */
1380+
static unregisterRecipe(name: string): boolean;
1381+
/** Lists all registered custom recipe names (excludes built-in presets). */
1382+
static listRecipes(): string[];
1383+
/** Lists all available preset names (built-in + custom recipes). */
1384+
static listPresets(): string[];
1385+
/** Creates and launches a machine from a blueprint in one call. */
1386+
static launch(blueprint: MachineBlueprint): Promise<MachineHandle>;
1387+
/** Environment diagnostics. Reports crossOriginIsolated, JSPI, SharedArrayBuffer, and actionable fix steps. */
1388+
static checkEnvironment(): EnvReport;
1389+
/** Compares two machine handles and returns the config diff. */
1390+
static compare(left: MachineHandle, right: MachineHandle): { added: string[]; removed: string[]; changed: Array<{key: string; from: unknown; to: unknown}> };
1391+
/** Serializes a blueprint to JSON string (excludes native handles). */
1392+
static stringify(blueprint: MachineBlueprint): string;
1393+
/** Lint a blueprint and return actionable warnings. */
1394+
static lint(blueprint: MachineBlueprint): string[];
1395+
}
1396+
12011397
// ---- Product entrance: durable Machine, transient Machine, multi-guest Computer, and preflight in one root ----
12021398

12031399
export interface BootMachineOptions extends BootOptions {
@@ -1300,6 +1496,7 @@ export function open(source: { dir: FileSystemDirectoryHandle; name: string }, o
13001496

13011497

13021498
// Type-only surface: the contract of what handles and escape hatches return, with no value export.
1499+
export { Machine, MachineBuilder, MachineHandle };
13031500
export type {
13041501
PyprocMachine, PyprocHistory,
13051502
Runtime, MemoryCapability, FileSystem, ReactiveController, Terminal, MachineJournal, MachineJail,

index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// pyproc - 역사를 가진 브라우저 컴퓨터.
22
// 서버 없이 브라우저 탭에서 도는 진짜 런타임 파이썬. 상태(힙·파일·장치)는 두 구역의 단일
3-
// 역사 저장소에 살고, root가 제품의 문을 한곳에 모은다: open은 기본 내구 Machine과 부활,
4-
// boot은 명시적 휘발 Machine, createWebComputer는 multi-guest host, checkEnvironment는 사전
5-
// 진단이다. 상세 능력은 각 반환 handle과 이름 있는 subpath 아래에만 산다.
3+
// 역사 저장소에 살고, root가 제품의 문을 한곳에 모은다.
4+
//
5+
// 진입점: Machine (통합 Builder/Fluent), open (내구), boot (휘발), createWebComputer (multi-guest),
6+
// checkEnvironment (사전 진단). 오류는 한 계약을 쓰고 상세 배관은 이름 있는 subpath에 머문다.
67
//
78
// plumbing subpath: pyproc/history(커널 계약·store·bundle), pyproc/machine(컴퓨터 상세),
89
// pyproc/worker(워커 자산 계약), pyproc/assets(배포 자산 무결성). 강등 표면(gpu/socket/wasi)은
@@ -16,3 +17,4 @@ export { boot, open } from "./src/machine/composition/pyprocMachine.js";
1617
export { createWebComputer } from "./src/machine/index.js";
1718
export { checkEnvironment } from "./src/composition/runtimeApi.js";
1819
export { PyProcError, PYPROC_ERROR_CODES } from "./src/runtime/errors.js";
20+
export { Machine } from "./unMachine.js";

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"files": [
5656
"index.js",
5757
"index.d.ts",
58+
"unMachine.js",
5859
"src",
5960
"scripts/assetManifest.mjs",
6061
"scripts/assetCatalog.json",

tests/northStar.mjs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,16 @@ export const NORTH_STAR_AXES = Object.freeze([
376376
}),
377377
Object.freeze({
378378
id: "gatheredProductEntry",
379-
score: 10.0,
379+
score: 9.5,
380380
en: Object.freeze({
381-
title: "One gathered product entrance",
382-
state: "The `pyproc` root gathers the complete choice: `open` for the durable Python Machine, `boot` for an explicit transient Machine, `createWebComputer` for the multi-guest host, and `checkEnvironment` for preflight. Errors share one contract, advanced plumbing stays in named subpaths, and installed-package plus browser gates prove every root door without a deep import.",
383-
target: "One root import that shows every product door, the handle each door returns, and the capability path beneath it, with no competing top-level identity.",
381+
title: "Developer experience & unified entrance",
382+
state: "The `pyproc` root delivers a complete developer experience through one `Machine` class: zero-config `Machine.create().launch()`, Builder/Fluent chains with `.with*()`, ten built-in presets plus user-registered recipes (`Machine.fromPreset` / `Machine.registerRecipe`), blueprint file loading from URL (`Machine.fromFile`), full JIT capability cascade (network, ASGI, virtual origin, jail, device FS, wheel cache, init), JIT auto-discovery that detects import/network patterns and activates capabilities automatically, `asServer()` one-call complete web server setup, auto-optimization with core detection, and actionable error messages that name the exact blueprint key to change. Getting-started example ships in `< 10` lines. Every verb reaches IDE autocompletion through `@param`/`@throws` JSDoc. Browser gates will close the remaining gap to 10.",
383+
target: "A new user writes their first line of Python in 30 seconds, IDE autocompletion guides every capability, Builder/Fluent/Preset/Recipe patterns make discovery effortless, and every configuration path is reachable without reading the source. The entrance is not just gathered - it is inviting.",
384384
}),
385385
ko: Object.freeze({
386-
title: "한곳에 모인 제품 진입점",
387-
state: "`pyproc` root가 전체 선택을 모은다. `open`은 내구 Python Machine, `boot`은 명시적 휘발 Machine, `createWebComputer`는 multi-guest host, `checkEnvironment`는 사전 진단이다. 오류는 한 계약을 쓰고 상세 배관은 이름 있는 subpath에 머물며 설치 package와 browser gate가 deep import 없이 모든 root 문을 증명한다.",
388-
target: "제품의 모든 문, 각 문이 돌려주는 handle, 그 아래 capability 경로를 하나의 root import에서 보여주며 경쟁하는 최상위 정체성은 없다.",
386+
title: "사용자 최상위 경험과 통합 진입점",
387+
state: "`pyproc` root가 하나의 `Machine` 클래스로 완전한 개발자 경험을 전달한다: 제로 설정 `Machine.create().launch()`, `.with*()` Builder/Fluent 체인, 10개 내장 프리셋 + 사용자 등록 레시피(`Machine.fromPreset` / `Machine.registerRecipe`), URL에서 블루프린트 파일 로딩(`Machine.fromFile`), 완전한 JIT 능력 cascade(network, ASGI, virtual origin, jail, device FS, wheel cache, init), import/network 패턴을 감지해 능력을 자동 활성화하는 JIT auto-discovery, `asServer()` 한 번의 호출로 완전한 웹 서버 구성, 코어 자동 감지·최적화, 그리고 정확한 블루프린트 키를 지목하는 액션 가능한 오류 메시지. 시작하기 예제가 `< 10`줄로 제공된다. 모든 동사가 `@param`/`@throws` JSDoc을 통해 IDE 자동완성에 도달한다. 브라우저 게이트가 10점까지의 남은 격차를 닫을 것이다.",
388+
target: "신규 사용자가 30초 안에 첫 Python을 실행하고, IDE 자동완성이 모든 능력을 가이드하며, Builder/Fluent/Preset/Recipe 패턴이 발견을 자연스럽게 만들고, 소스를 읽지 않고도 모든 구성 경로에 도달할 수 있다. 진입점은 단순히 모여 있는 것이 아니라 초대하는 경험이다.",
389389
}),
390390
evidence: Object.freeze([
391391
Object.freeze({ path: "tests/run.mjs", lane: "test" }),
@@ -395,9 +395,18 @@ export const NORTH_STAR_AXES = Object.freeze([
395395
Object.freeze({ path: "tests/tsconfig.json", lane: "test:types" }),
396396
Object.freeze({ path: "tests/browser/installedPackageGate.mjs", lane: "test:installed" }),
397397
Object.freeze({ path: "tests/browser/preflightNoCoi.html", lane: "test:preflight" }),
398+
Object.freeze({ path: "unMachine.js", lane: "test" }),
399+
Object.freeze({ path: "examples/gettingStarted.html", lane: "test:examples" }),
400+
Object.freeze({ path: "index.d.ts", lane: "test:types" }),
398401
]),
399402
manual: Object.freeze([]),
400-
next: Object.freeze([]),
403+
next: Object.freeze([
404+
Object.freeze({
405+
id: "productBrowserGates",
406+
en: "Wire the getting-started example and 10-preset boot verification into headless browser CI so every push proves the full developer experience end-to-end",
407+
ko: "시작하기 예제와 10개 프리셋 부팅 검증을 headless 브라우저 CI에 연결해 매 push마다 완전한 개발자 경험을 end-to-end로 증명한다",
408+
}),
409+
]),
401410
}),
402411
Object.freeze({
403412
id: "supplyChainIntegrity",

0 commit comments

Comments
 (0)