WASM 런타임 특성상 진짜 검증은 브라우저에서만 가능하다. 그래서 검증은 5단이다: Node 구조 게이트(커밋마다), 브라우저 런타임 게이트(공개 표면의 실동작), 브라우저 제품 소비자 게이트(설치 패키지 소비), 예제 실행 게이트(데모 페이지 완주), 수동 실측(사람 눈 확인·벤치). 자동 게이트는 CI에서 매 푸시마다 돈다.
npm test # = node tests/run.mjs, 의존성 0커밋 전 반드시 green. 검사 항목:
- 공개 표면:
index.js가 계약한 export(이름·타입)를 전부 내는가. - 능력 매트릭스: 공개 export가 제품 가치, 상태, 필수 조건, 실행 표면, 검증, 경계로 묶이고 각 행이 예제·probe·브라우저 게이트·벤치 산출물 중 하나를 가리키는가.
- 능력 계약 형태:
Runtime/PyProc/ReactiveController프로토타입 메서드 존재. - 타입 커버리지:
index.d.ts가 공개 표면을 전부 선언하고package.json이 이를 배선하는가. - 공개 import 경계:
package.json exports가 승인된 안정 subpath만 노출하고,examples/는src/내부 파일을 모듈 import로 직접 소비하지 않는가. - src graph 구조:
src/파일이 승인된 레이어 폴더에 있고, 내부 상대 module 참조가.js확장자를 가진 실제src/파일을 가리키며, ESM import graph에 cycle이 없고, cross-layer edge가 전부 아래로 향하는가. 레이어 순위는runtime(0) <capabilities(1) <composition(2) <session/processOs(3)이고 같은 순위끼리도 교차 금지다.runtime.jscore는 자기 레이어 밖을 import하지 않고, 합성 루트runtimeApi.js는 개별 capability class를 모르며(registry만 안다) 아래층이 합성 루트를 import하지 않는다.capabilities -> runtimeedge는 coupling budget이라 파일 쌍 단위로 심사하고,syscallBridge -newURL-> worker.js는 Worker 자산 URL이라 별도 승인이다. - 문서 위생: 전체
*.md/*.js에 em dash(U+2014) 0. - 상대 링크: 모든
*.md의 상대 링크가 실존 파일을 가리키는가(죽은 링크 차단). - attempts 구조:
tests/attempts/각 카테고리에 README(+ 졸업 게이트 절)가 있는가. - mainPlan 구조: 각 이니셔티브 폴더에 README가 있는가.
- worker 계약:
src/processOs/worker.js가 boot/task 프로토콜을 처리하는가(텍스트 검사. Node에서 import 불가). - 실행 자산 manifest:
getPyProcAssetManifest()와pyproc-assetsCLI가 Worker/SW graph + SRI manifest를 만들고,--copy-to로 필요한 파일을 복사하며, 브라우저 게이트 서버가 그 CLI 산출물을/pyproc-assets.json으로 제공하고verifyPyProcAssetIntegrity()가 잘못된 SRI를 spawn 전 거부하는가. - 패키지 소비자 계약:
npm packtarball을 임시 앱에 설치한 뒤pyproc,pyproc/assets, 설치된pyproc-assetsbin만으로 public import, graph SRI manifest,--copy-to복사가 성립하는가(npm run test:package로 단독 실행 가능).
새 규칙을 만들면 가능한 한 여기(또는 .githooks)에 기계 가드를 짝지어 추가한다.
통과를 확인하는 것만으로는 게이트가 아니다. 고의로 위반을 주입해 RED가 되는 것까지 보고, 그 사실을 커밋 메시지에 남긴다. 안 무는 게이트는 없는 게이트보다 나쁘다: 검증됐다는 착각을 판다.
근거는 실제 사고다. 타입 게이트를 --skipLibCheck로 처음 썼는데 그 플래그가 바로 .d.ts 검사를 건너뛰어 조용히 통과했다(2026-07-17). 자산 role 선택에도 같은 모양이 있었다: 경로 대조가 실패해도 폴백이 늘 같은 답을 내서 영원히 안 터지는 죽은 검사였다.
각 게이트의 한계도 함께 안다:
| 게이트 | 못 보는 것 |
|---|---|
npm test |
미정의 식별자(텍스트/그래프 검사다). 코드블록 산문 속 경로(마크다운 링크만 본다) |
npm run test:browser |
WebGPU 표면(헤드리스에 어댑터가 없어 SKIP한다. 실 GPU 머신 몫) |
npm run test:types |
런타임 동작(선언만 본다) |
게이트 없는 경로를 리팩터하면 게이트를 함께 낸다. 힙 성장과 저널 pack이 수동 probe로만 검증돼 온 채 수렴 대상이 됐고, 그래서 같은 커밋에 게이트를 냈다. 검증할 수 없는 경로는 검증했다고 쓰지 않고, 가능한 가장 강한 대조를 하고 한계를 명시한다(WGSL 이동은 셰이더 바이트 동일성 대조가 상한이었다).
npm run test:types # = tsc -p tests/tsconfig.json (typescript는 CI 도구, package 의존성 아님)손으로 유지하는 1,200줄 index.d.ts와 강등 subpath의 형제 d.ts에 컴파일러를 붙인다. npm test의 [타입] 절이 "선언이 있는가"를 문자열로 보는 반면 이 게이트는 "타입이 성립하는가"를 본다. 구조 게이트는 미정의 식별자도 못 보므로 둘은 겹치지 않는다.
skipLibCheck는 절대 켜지 않는다. 그 플래그가 바로.d.ts검사를 건너뛰므로 게이트가 조용히 통과한다(음성 시험으로 확인함).- 강등 표면(
pyproc/gpu,pyproc/socket,pyproc/wasi)의 타입은 자기.js옆의 d.ts로만 성립한다.index.d.ts안의declare module "pyproc/gpu"블록은 그 자리를 대신하지 못한다: 모듈이 untyped.js로 해석되면 TypeScript가 증강을 거부한다(TS2665). 구조 게이트가 이 위치를 계약으로 고정한다.
npm run test:browser # = node tests/browser/run.mjs, 의존성 0COOP/COEP 서버를 임시 포트로 띄우고, 로컬 Chromium 계열 브라우저(Edge/Chrome 자동 탐색, PYPROC_BROWSER=<경로>로 지정 가능)를 headless로 실행해 tests/browser/gate.html의 실측 결과를 POST 백채널로 회수한다. 공개 표면이 진짜 브라우저에서 도는지를 커밋 단위로 검증하는 게이트다:
- crossOriginIsolated 전제,
boot()+ 파이썬 실행. pyproc-assetsCLI 산출 manifest를 같은 오리진에서 fetch하고,assetIntegrity로PyProcworker graph와Runtime -> SyscallBridgechild worker 상속 경로를 spawn 전 검증.- 복원 리액티브의 실행 경계 계약(경계를 닫은
restoreLive, 안전 기준선restore). - 스냅샷-fork spawn,
map병렬 결과 정확성, 직렬 exec 기준선 일치(벤치 계약 S2의 mapSerialMs 산출 경로),ps/terminate.
런타임 동작을 바꾸는 커밋은 이 게이트 green이 조건이다. 실측 수치(부팅/복원/fork/map ms)가 함께 출력되므로, 의미 있는 변화는 활성 이니셔티브의 진행 원장에 기록한다(활성이 없으면 다음 이니셔티브 개설과 함께 시작. 직전 원장: mainPlan/_done/web-python-runtime/03-progress-ledger.md). CI에서도 같은 게이트가 돈다(.github/workflows/ci.yml).
npm run test:consumer
PYPROC_INDEX_URL=/vendor/pyodide/ npm run test:consumer # 자가 호스팅 엔진으로 설치 패키지 소비 검사npm pack으로 만든 tarball을 임시 앱에 설치한 뒤, 브라우저 import map에서 pyproc와 pyproc/assets public specifier만 노출한다. 그 앱이 설치된 pyproc-assets로 /node_modules/pyproc/ 기준 SRI manifest를 만들고, headless Chromium에서 다음을 검증한다:
- 설치된 패키지의 public specifier import가 동작한다.
- 설치된 worker graph의 SRI manifest가 실제
node_modules/pyproc/src/...바이트와 일치한다. - 설치된
pyprocSw.js를 SRI 검증 후asgi=/pyproc/로 등록하고,VirtualOriginfetch가 Python ASGI까지 도달한다. - 잘못된 worker SRI는
PyProcworker spawn 전에 거부된다. - 설치된
Runtime과PyProcworker가 같은 브라우저 앱 안에서 실제로 돈다. - 설치된
MachineJail이 제품 권한 manifest(net=false,clipboard=false,home=true,workers=false)를 정책 객체와 Python 초크포인트로 집행한다. - 설치된 패키지만 import한 앱이 signed
.pymachine을/home/web포함으로 export하고, signer fingerprint를 표시 가능한sha256:<hex>로 계산하며, 신뢰 공개키 없는 파일과 다른 공개키 파일을 거부한다. - trusted public key로 연
.pymachine이resume.py를 실행해 SQLite connection 같은 프로세스 자원을 다시 열고/home/web상태를 유지한다.
이 게이트는 "repo에서는 되는데 소비 앱에서는 깨지는" 구조 결함을 막는다. 특히 asgi=/pyproc/가 /node_modules/pyproc/... 패키지 자산을 오인해 가로채는 충돌을 막기 위해, VirtualOrigin fetch 뒤에도 PyProc worker graph 검증과 실행이 통과해야 한다. CI에서도 돈다.
public export별 coverage 정본은 소비 계약의 설치 패키지 consumer gate coverage 표다.
npm run test:examples # = node tests/browser/examples.mjs, 의존성 0
PYPROC_INDEX_URL=/vendor/pyodide/ npm run test:examples # 자가 호스팅 엔진으로 같은 예제 검사데모 페이지(examples/*.html)를 사람이 여는 그대로 headless로 열어 완주 여부를 회수한다.
각 예제는 ?gate 쿼리에서만 POST 백채널로 보고하고, 사람이 열면 아무것도 안 한다.
생긴 이유(2026-07-12): 라이브러리 게이트는 라이브러리만 검증해서, 예제 코드의 실결함
(BigInt 직렬화)이 라이브 데모까지 나갔다. 데모는 공개 진열장이므로 이 게이트가 회귀를 막는다.
PYPROC_INDEX_URL을 주면 런타임 게이트와 같은 방식으로 예제 전체가 자가 호스팅 엔진 배포판을 쓴다.
machine.html의 gate는 코드 실행뿐 아니라 signed .pymachine cast, trusted public key open, signer fingerprint/permission policy 표시, /home/web 복원, resume.py의 SQLite connection 재개설을 함께 검증한다.
speedLab.html의 gate는 단일 worker numpy matmul과 4-worker sharded matmul을 워밍업 뒤 3회 반복하고, 결과 일치와 median speedup >= 2.0, shard p95 latency < single-worker median latency를 함께 검증한다.
crossOriginIsolated(COOP/COEP 헤더) 페이지에서만 SharedArrayBuffer가 열리므로, 동봉된 서버로 띄운다:
npm run serve # = node scripts/staticServer.mjs (COOP/COEP 헤더 포함, 의존성 0)Chromium/Edge에서 확인:
| 페이지 | 확인하는 것 | green 기준 |
|---|---|---|
http://localhost:8788/examples/basic.html |
단일 런타임 부팅 + 파이썬 실행 + numpy 로드 | sum=4950, numpy sum=45 출력 |
http://localhost:8788/examples/serverDev.html |
FastAPI/SQLite + VirtualOrigin 서버 개발 루프 | iframe preview, POST todo, app.py v2 reload |
http://localhost:8788/examples/speedLab.html |
단일 worker numpy matmul vs 4-worker sharded matmul | 결과 일치, 3회 warmed sample median speedup >= 2.0, shard p95 < single median |
http://localhost:8788/examples/processOs.html |
스냅샷-fork spawn + map 병렬 vs 직렬 | forked=true, speedup > 1, 결과 일치 true |
Speed Lab의 S1 raw JSON은 별도 러너로 남긴다:
npm run bench:speed -- --out .tmp/speed-s1.json기본 S1 조건은 workers=4, size=1024, samples=3이다. 다른 조건은 --workers, --size, --samples로 명시하고 raw artifact의 command 필드에 남긴다.
bench:artifact가 만드는 raw JSON은 schema v2다. 새 측정은 가능하면 --source, --raw-output, --profile, --warmup-count, --browser-name, --browser-headless를 같이 남긴다. --raw-output은 --out이 있을 때 raw/<artifact-name>.txt sidecar로 저장된다. 이미 저장된 raw file을 쓰려면 --raw-output-file <path>를 쓴다. README에 올릴 수치는 embedded:report 또는 tracked file:raw/<artifact-name>.txt rawOutput reference가 있는 artifact만 승격한다.
S0 Python ready latency는 페이지 또는 런타임 시작부터 첫 Python 명령 성공까지의 시간이다:
npm run bench:artifact -- --scenario S0 --candidate pyproc --command "<command>" --sample 2500,0 --sample 2400,0 --sample 2450,0 --out .tmp/pyproc-s0.json
npm run bench:compare -- .tmp/pyproc-s0.json --out .tmp/s0-compare.mdS0C Python cold ready latency는 cold profile/cache-clear 조건만 받는다. warm S0와 한 표에 섞지 않는다:
npm run bench:artifact -- --scenario S0C --candidate pyproc --command "<command>" --sample 3600,0 --sample 3500,0 --sample 3700,0 --out .tmp/pyproc-s0c.json
npm run bench:compare -- .tmp/pyproc-s0c.json --out .tmp/s0c-compare.mdS2 process map은 같은 Python 함수를 같은 입력 묶음으로 직렬 실행한 wall time과 PyProc.map process pool wall time을 paired sample로 남긴다:
npm run bench:artifact -- --scenario S2 --candidate pyproc --command "npm run test:browser timings.mapSerialMs/mapParallelMs" --sample 2000,1000,0 --sample 1900,950,0 --sample 2100,1050,0 --out .tmp/pyproc-s2.json
npm run bench:compare -- .tmp/pyproc-s2.json --out .tmp/s2-compare.mdS3 browser server는 설치 패키지 consumer gate의 VirtualOrigin POST 왕복 시간을 latency sample로 남긴다:
npm run bench:artifact -- --scenario S3 --candidate pyproc --command "npm run test:consumer timings.virtualOriginMs" --sample 15,0 --sample 18,0 --sample 14,0 --out .tmp/pyproc-s3.json
npm run bench:compare -- .tmp/pyproc-s3.json --out .tmp/s3-compare.mdS4 machine resume는 설치 패키지 consumer gate의 signed .pymachine export/open/resume 값을 sample로 남긴다:
npm run bench:artifact -- --scenario S4 --candidate pyproc --command "npm run test:consumer timings.machineExportMs/machineOpenMs/machineMB/machineResumeRows" --sample 76,2264,10.8,2,0 --sample 75,2346,10.8,2,0 --sample 80,2136,10.8,2,0 --out .tmp/pyproc-s4.json
npm run bench:compare -- .tmp/pyproc-s4.json --out .tmp/s4-compare.mdS1L single-kernel NumPy latency는 S1을 대체하지 않는 별도 보조 축이다:
npm run bench:artifact -- --scenario S1L --candidate pyproc --command "<command>" --sample 10067,0 --sample 9633,0 --sample 10073,0 --out .tmp/pyproc-s1l.json
npm run bench:artifact -- --scenario S1L --candidate webvm --command "WebVM terminal S1L" --sample 12825,0 --sample 11406,0 --sample 11349,0 --out .tmp/webvm-s1l.json
npm run bench:compare -- .tmp/pyproc-s1l.json .tmp/webvm-s1l.json --out .tmp/s1l-compare.md체크리스트:
- 콘솔에
crossOriginIsolated가 true인지(false면 헤더 문제). - 공개 표면·런타임 동작을 바꾼 커밋은 실측 결과(수치)를 활성 이니셔티브의 진행 원장에 남긴다(활성 0이면 다음 이니셔티브에서). README의 실측 수치는 그 원장에서만 가져온다. 과거 수치의 출처: mainPlan/_done/web-python-runtime/03-progress-ledger.md.
- 속도 실측은 benchmarking.md의 canonical scenario, sample, raw output 규칙을 따르고, 결과는 진행 원장과 artifact에만 남긴다(공개 표면 게시 금지).
신규 능력의 실측은 examples가 아니라 tests/attempts/<카테고리>/의 probe에서 한다. probe도 같은 서버로 띄운다(http://localhost:8788/tests/attempts/...). 결과 기록 형식은 tests/attempts/README.md 참조.
runtimeParity/virtualOriginBoundaryProbe.html처럼 "지원하지 않는 벽"을 제품 계약으로 고정하는 probe도 여기에 둔다. 이런 probe는 기능 확장이 아니라 소비자가 쿠키 세션, WebSocket upgrade, 청크 스트리밍 같은 플랫폼 벽에 의존하지 않도록 막는 compatibility lab이다.