aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/results/2026-09-23-native-connections
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:14:54 +0100
committereunhhu <[email protected]>2026-09-24 17:14:54 +0100
commitb1ce1927022d374198441afafeb27199fc8a052a (patch)
tree6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/connections/results/2026-09-23-native-connections
parentd34f9426d5317a1948f12f229439946728bc342a (diff)
downloadpure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz
pure-b1ce1927022d374198441afafeb27199fc8a052a.zip
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/connections/results/2026-09-23-native-connections')
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/README.md26
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/native.json98
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sanitized.json98
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile22
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py306
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c41
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h25
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c346
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c132
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h25
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c110
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs127
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs121
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure47
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs89
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs496
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasmbin0 -> 61072 bytes
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs47
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs350
-rw-r--r--prototypes/connections/results/2026-09-23-native-connections/verification.json29
20 files changed, 2535 insertions, 0 deletions
diff --git a/prototypes/connections/results/2026-09-23-native-connections/README.md b/prototypes/connections/results/2026-09-23-native-connections/README.md
new file mode 100644
index 0000000..0e4f385
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/README.md
@@ -0,0 +1,26 @@
+# 018 검증 자료
+
+> 이 SQLite 구현은 [019](../2026-09-23-byte-storage/README.md)의 SQL 없는 경로로 교체했다.
+> 이 디렉터리의 원자료와 소스 사본은 당시 결과를 보존한다. 현재 작업 트리에서 실행하면
+> 새 구현을 검사하므로, 당시 구현의 재현에는 `sources/`의 파일들을 별도 작업 트리에 사용한다.
+
+이 디렉터리는 실제 루프백 TCP·SQLite·기존 Workbench CLI를 연결한 기능·수명 검사 자료다.
+속도·처리량·전체 RSS 비교는 수행하지 않았다.
+
+- [native.json](native.json): 일반 빌드의 15개 통합 시나리오, 종료 시 자원 수, 환경과 소스·실행 파일 해시.
+- [sanitized.json](sanitized.json): ASan·UBSan 빌드에서 같은 시나리오. 누수 검사를 비활성화하지 않았다.
+- [verification.json](verification.json): 빌드·실행 명령, 코어·CLI 결과와 파일 일치 확인.
+
+서버가 만든 임시 DB에 실제 작성물 배치를 저장하고 새 CLI에서 복원했다. 객체 버전의
+충돌과 64비트 정밀도, 저장 오류, DB 잠금, 읽는 중 다른 커밋, 부분 입력·출력, 연결 종료,
+한도와 기한을 검사한다. 큰 본문은 부분 전송·한도 거절을 발생시키기 위한 정확성 fixture다.
+작업량을 크게 만들어 성능을 주장하는 벤치마크가 아니다.
+
+`retained_payload_bytes`는 수신 할당과 빌린 DB 결과의 길이 합이다. 실제 SQLite·OS·전체
+프로세스 메모리는 포함하지 않는다. `ingress_prefix_copy_bytes`는 서버가 직접 복사한
+수신 접두부만 센다. 전송 배치에 따라 값이 바뀔 수 있어 두 빌드의 시간·복사 효율 비교에 쓰지 않는다.
+
+재현은 [구현 README](../../README.md)의 `make check` 명령을 따른다. `EVIDENCE=/새/경로.json`을
+추가하면 새 증거 파일을 저장한다. 이미 있는 파일을 덮어쓰지 않는다. 실제 소켓·자식 프로세스·
+LeakSanitizer가 제한된 환경에서는 권한이 필요하다. 이 실행에서는 샌드박스가 해당 기능을
+차단해 승인된 샌드박스 밖의 로컬 검사로 확인했다. 외부 네트워크 서비스는 사용하지 않았다.
diff --git a/prototypes/connections/results/2026-09-23-native-connections/native.json b/prototypes/connections/results/2026-09-23-native-connections/native.json
new file mode 100644
index 0000000..5f7e53f
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/native.json
@@ -0,0 +1,98 @@
+{
+ "kind": "native-path-correctness-not-performance",
+ "checks": [
+ "fragmented request and binary bytes round trip",
+ "conditional create and stale revision preserve committed bytes",
+ "busy and injected DB failure do not acknowledge or change data",
+ "incomplete request never commits and releases its allocation",
+ "borrowed read remains the same snapshot during another commit",
+ "receiver reset aborts an outstanding borrowed transfer",
+ "unsupported framing and oversized payload are rejected before effects",
+ "existing workbench manipulation restores in a fresh runtime and reports conflict",
+ "invalid or non-idle restore preserves local state and does not adopt remote revision",
+ "revision above JavaScript safe integer round trips without numeric coercion",
+ "unread acknowledgment does not undo commit or permit a stale retry",
+ "empty body has a valid lifetime and stored revision",
+ "committed authoring state survives process restart",
+ "slow consumer retains its lease; budget rejects another; deadline releases it",
+ "connection slots stay bounded and recover after header deadlines"
+ ],
+ "server_runs": [
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 1048583,
+ "payload_budget": 4194304,
+ "peak_connections": 2,
+ "accepted": 38,
+ "commits": 11,
+ "gets": 13,
+ "conflicts": 4,
+ "budget_rejections": 0,
+ "expired": 0,
+ "releases": 31,
+ "ingress_prefix_copy_bytes": 11674,
+ "borrowed_output_bytes": 2115411,
+ "server_structure_bytes": 39112
+ },
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 2553,
+ "payload_budget": 4194304,
+ "peak_connections": 1,
+ "accepted": 2,
+ "commits": 0,
+ "gets": 2,
+ "conflicts": 0,
+ "budget_rejections": 0,
+ "expired": 0,
+ "releases": 2,
+ "ingress_prefix_copy_bytes": 0,
+ "borrowed_output_bytes": 2556,
+ "server_structure_bytes": 39112
+ },
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 1048576,
+ "payload_budget": 1048576,
+ "peak_connections": 8,
+ "accepted": 12,
+ "commits": 0,
+ "gets": 3,
+ "conflicts": 0,
+ "budget_rejections": 1,
+ "expired": 9,
+ "releases": 3,
+ "ingress_prefix_copy_bytes": 0,
+ "borrowed_output_bytes": 1048582,
+ "server_structure_bytes": 39112
+ }
+ ],
+ "sqlite_runtime": "3.46.1",
+ "binary_sha256": "ff1c2d09d940d9f9a9170e6fea894aee9926e98f04567d389a0c00d5ddf4c7f7",
+ "environment": {
+ "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41",
+ "machine": "aarch64",
+ "python": "3.14.7",
+ "node": "v24.20.0"
+ },
+ "source_sha256": {
+ "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749",
+ "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421",
+ "prototypes/connections/store.c": "b0b50b0d85900b26257aac68177951e8319fd5482a1fcff4c91722f7ba30f74c",
+ "prototypes/connections/store.h": "07d347ae872a3d618dda1697b01bb32abee44b3451472a1b2225963085c11cf2",
+ "prototypes/connections/server.c": "3ff9d9497ad2a8ece657b50af9f8e6e564a974e6bbe3542f0ebe32279883d219",
+ "prototypes/connections/check.py": "be75db13ab2f11d3dd0e064aaa271bf28145aef11423e8ed536c755dcf96eaac",
+ "prototypes/connections/test-flow.c": "17472b90ebd3067b43a0cd06fb571111206b2025a2bd552b1602c3189f17ce0b",
+ "prototypes/connections/Makefile": "6c05917782adf733d31d9bfd42ee8f41b05fe94d965c362577189d6849b61e3e",
+ "prototypes/workbench/remote.mjs": "6f5aa2bc5a3c88749b2b688c5631eed9e4a5ab41d72eca168bfb67941a8cff2f",
+ "prototypes/workbench/cli.mjs": "55a5ead56fc8ad4c3f2391505fc3dbfa477ccff5b68fc208f700da24d5e194da",
+ "prototypes/workbench/web/core.wasm": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479",
+ "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54",
+ "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1",
+ "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35",
+ "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a"
+ }
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sanitized.json b/prototypes/connections/results/2026-09-23-native-connections/sanitized.json
new file mode 100644
index 0000000..c391070
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sanitized.json
@@ -0,0 +1,98 @@
+{
+ "kind": "native-path-correctness-not-performance",
+ "checks": [
+ "fragmented request and binary bytes round trip",
+ "conditional create and stale revision preserve committed bytes",
+ "busy and injected DB failure do not acknowledge or change data",
+ "incomplete request never commits and releases its allocation",
+ "borrowed read remains the same snapshot during another commit",
+ "receiver reset aborts an outstanding borrowed transfer",
+ "unsupported framing and oversized payload are rejected before effects",
+ "existing workbench manipulation restores in a fresh runtime and reports conflict",
+ "invalid or non-idle restore preserves local state and does not adopt remote revision",
+ "revision above JavaScript safe integer round trips without numeric coercion",
+ "unread acknowledgment does not undo commit or permit a stale retry",
+ "empty body has a valid lifetime and stored revision",
+ "committed authoring state survives process restart",
+ "slow consumer retains its lease; budget rejects another; deadline releases it",
+ "connection slots stay bounded and recover after header deadlines"
+ ],
+ "server_runs": [
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 1048583,
+ "payload_budget": 4194304,
+ "peak_connections": 2,
+ "accepted": 38,
+ "commits": 11,
+ "gets": 13,
+ "conflicts": 4,
+ "budget_rejections": 0,
+ "expired": 0,
+ "releases": 31,
+ "ingress_prefix_copy_bytes": 11674,
+ "borrowed_output_bytes": 2115411,
+ "server_structure_bytes": 39112
+ },
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 2553,
+ "payload_budget": 4194304,
+ "peak_connections": 1,
+ "accepted": 2,
+ "commits": 0,
+ "gets": 2,
+ "conflicts": 0,
+ "budget_rejections": 0,
+ "expired": 0,
+ "releases": 2,
+ "ingress_prefix_copy_bytes": 0,
+ "borrowed_output_bytes": 2556,
+ "server_structure_bytes": 39112
+ },
+ {
+ "active": 0,
+ "retained_payload_bytes": 0,
+ "peak_retained_payload_bytes": 1048576,
+ "payload_budget": 1048576,
+ "peak_connections": 8,
+ "accepted": 12,
+ "commits": 0,
+ "gets": 3,
+ "conflicts": 0,
+ "budget_rejections": 1,
+ "expired": 9,
+ "releases": 3,
+ "ingress_prefix_copy_bytes": 0,
+ "borrowed_output_bytes": 1048582,
+ "server_structure_bytes": 39112
+ }
+ ],
+ "sqlite_runtime": "3.46.1",
+ "binary_sha256": "2e3bb0802082637989041063c2bc34ad6c9a072abc257d7adefdc65cba315298",
+ "environment": {
+ "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41",
+ "machine": "aarch64",
+ "python": "3.14.7",
+ "node": "v24.20.0"
+ },
+ "source_sha256": {
+ "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749",
+ "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421",
+ "prototypes/connections/store.c": "b0b50b0d85900b26257aac68177951e8319fd5482a1fcff4c91722f7ba30f74c",
+ "prototypes/connections/store.h": "07d347ae872a3d618dda1697b01bb32abee44b3451472a1b2225963085c11cf2",
+ "prototypes/connections/server.c": "3ff9d9497ad2a8ece657b50af9f8e6e564a974e6bbe3542f0ebe32279883d219",
+ "prototypes/connections/check.py": "be75db13ab2f11d3dd0e064aaa271bf28145aef11423e8ed536c755dcf96eaac",
+ "prototypes/connections/test-flow.c": "17472b90ebd3067b43a0cd06fb571111206b2025a2bd552b1602c3189f17ce0b",
+ "prototypes/connections/Makefile": "6c05917782adf733d31d9bfd42ee8f41b05fe94d965c362577189d6849b61e3e",
+ "prototypes/workbench/remote.mjs": "6f5aa2bc5a3c88749b2b688c5631eed9e4a5ab41d72eca168bfb67941a8cff2f",
+ "prototypes/workbench/cli.mjs": "55a5ead56fc8ad4c3f2391505fc3dbfa477ccff5b68fc208f700da24d5e194da",
+ "prototypes/workbench/web/core.wasm": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479",
+ "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54",
+ "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1",
+ "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35",
+ "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a"
+ }
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile
new file mode 100644
index 0000000..c520aa5
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/Makefile
@@ -0,0 +1,22 @@
+CC ?= cc
+CPPFLAGS += $(shell pkg-config --cflags sqlite3)
+CFLAGS ?= -O2 -g
+CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror
+LDLIBS += $(shell pkg-config --libs sqlite3)
+BUILD ?= build
+
+.PHONY: all check
+all: $(BUILD)/pure-connections
+
+$(BUILD):
+ mkdir -p $@
+
+$(BUILD)/pure-connections: server.c store.c flow.c store.h flow.h | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) server.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@
+
+$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.h | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) test-flow.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@
+
+check: $(BUILD)/test-flow $(BUILD)/pure-connections
+ $(BUILD)/test-flow
+ python3 check.py --server $(BUILD)/pure-connections $(if $(EVIDENCE),--output "$(EVIDENCE)")
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py
new file mode 100644
index 0000000..c19fcaa
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/check.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+"""Exercise the real local TCP/SQLite path and the existing workbench CLI."""
+import argparse
+from contextlib import contextmanager
+import hashlib
+import http.client
+import json
+import platform
+from pathlib import Path
+import select
+import socket
+import sqlite3
+import struct
+import subprocess
+import tempfile
+import time
+
+ROOT = Path(__file__).resolve().parent
+WORKBENCH = ROOT.parent / 'workbench'
+OBJECT_LIMIT = (2 * 6 + 32) * 32768 + 8192
+
+
+class Server:
+ def __init__(self, binary, database, budget=4 * 1024 * 1024, timeout=5000):
+ self.process = subprocess.Popen([str(binary), '--db', str(database), '--port', '0',
+ '--budget', str(budget), '--timeout-ms', str(timeout), '--send-buffer', '4096'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ if not select.select([self.process.stdout], [], [], 15)[0]:
+ self.process.kill()
+ raise AssertionError('server readiness timed out')
+ line = self.process.stdout.readline()
+ if not line:
+ raise AssertionError(self.process.communicate(timeout=5))
+ self.ready = json.loads(line)
+ self.port = self.ready['port']
+ self.stats = None
+
+ def close(self):
+ if self.process.poll() is None:
+ self.process.terminate()
+ try:
+ stdout, stderr = self.process.communicate(timeout=10)
+ except subprocess.TimeoutExpired:
+ self.process.kill()
+ self.process.communicate()
+ raise
+ assert self.process.returncode == 0, (stdout, stderr)
+ self.stats = json.loads(stderr)
+ assert self.stats['active'] == self.stats['retained_payload_bytes'] == 0
+ assert self.stats['peak_retained_payload_bytes'] <= self.stats['payload_budget']
+ assert self.stats['peak_connections'] <= 8
+
+
+@contextmanager
+def serving(*args, **kwargs):
+ server = Server(*args, **kwargs)
+ try:
+ yield server
+ finally:
+ server.close()
+
+
+def rpc(server, method, key, body=b'', expected=None):
+ headers = {'Content-Length': str(len(body))}
+ if expected is not None:
+ headers['If-None-Match' if expected == 0 else 'If-Match'] = '*' if expected == 0 else f'"{expected}"'
+ connection = http.client.HTTPConnection('127.0.0.1', server.port, timeout=8)
+ try:
+ connection.request(method, f'/objects/{key}', body, headers)
+ response = connection.getresponse()
+ return response.status, dict(response.getheaders()), response.read()
+ finally:
+ connection.close()
+
+
+def wire(server, request, chunks=None):
+ with socket.create_connection(('127.0.0.1', server.port), timeout=8) as sock:
+ if chunks:
+ for chunk in chunks:
+ sock.sendall(chunk)
+ else:
+ sock.sendall(request)
+ response = http.client.HTTPResponse(sock)
+ response.begin()
+ return response.status, dict(response.getheaders()), response.read()
+
+
+def header(method, key, size=0, condition=b''):
+ return (f'{method} /objects/{key} HTTP/1.1\r\nHost: localhost\r\n'
+ f'Content-Length: {size}\r\n'.encode() + condition + b'\r\n')
+
+
+def cli(server, commands, source=None, expected_code=0):
+ result = subprocess.run(['node', str(WORKBENCH / 'cli.mjs'), *([str(source)] if source else [])],
+ input=commands.replace('$URL', f'http://127.0.0.1:{server.port}'), text=True,
+ capture_output=True, timeout=30)
+ assert result.returncode == expected_code, (result.stdout, result.stderr)
+ return [json.loads(line) for line in result.stdout.splitlines()], result.stderr
+
+
+def check(binary, directory):
+ database = directory / 'objects.db'
+ checks, records = [], []
+ with serving(binary, database) as server:
+ assert rpc(server, 'GET', 'absent')[0] == 404
+ payload = b'\x00\xffbinary\r\n\r\n' + bytes(range(256)) * 41
+ request = header('PUT', 'binary', len(payload), b'If-None-Match: *\r\n') + payload
+ status, headers, _ = wire(server, request, [request[:7], request[7:51], request[51:57], request[57:]])
+ assert status == 200 and headers['ETag'] == '"1"'
+ assert rpc(server, 'GET', 'binary')[2] == payload
+ checks.append('fragmented request and binary bytes round trip')
+ assert rpc(server, 'PUT', 'binary', b'new', 0)[0] == 412
+ assert rpc(server, 'PUT', 'binary', b'new', 1)[0] == 200
+ assert rpc(server, 'PUT', 'binary', b'stale', 1)[0] == 412
+ assert rpc(server, 'GET', 'binary')[2] == b'new'
+ checks.append('conditional create and stale revision preserve committed bytes')
+
+ lock = sqlite3.connect(database, timeout=0)
+ lock.execute('BEGIN IMMEDIATE')
+ assert rpc(server, 'PUT', 'binary', b'blocked', 2)[0] == 503
+ lock.rollback()
+ lock.execute("CREATE TRIGGER reject_write BEFORE UPDATE ON pure_objects BEGIN SELECT RAISE(ABORT,'test failure'); END")
+ lock.commit()
+ assert rpc(server, 'PUT', 'binary', b'failure', 2)[0] == 500
+ assert rpc(server, 'GET', 'binary')[2] == b'new'
+ lock.execute('DROP TRIGGER reject_write')
+ lock.commit()
+ lock.close()
+ checks.append('busy and injected DB failure do not acknowledge or change data')
+
+ sock = socket.create_connection(('127.0.0.1', server.port), timeout=8)
+ sock.sendall(header('PUT', 'incomplete', 100, b'If-None-Match: *\r\n') + b'partial')
+ sock.shutdown(socket.SHUT_WR)
+ assert sock.recv(1) == b''
+ sock.close()
+ assert rpc(server, 'GET', 'incomplete')[0] == 404
+ checks.append('incomplete request never commits and releases its allocation')
+
+ # Force genuine partial socket sends. A write on another connection must
+ # not alter the SQLite row whose bytes are still being transmitted.
+ large = bytes(range(256)) * 4096
+ assert rpc(server, 'PUT', 'large', large, 0)[0] == 200
+ sock = socket.create_connection(('127.0.0.1', server.port), timeout=8)
+ sock.sendall(header('GET', 'large'))
+ response = http.client.HTTPResponse(sock)
+ response.begin()
+ assert response.status == 200 and response.getheader('ETag') == '"1"'
+ assert response.read(17) == large[:17]
+ assert rpc(server, 'PUT', 'large', b'changed', 1)[0] == 200
+ assert response.read() == large[17:]
+ sock.close()
+ assert rpc(server, 'GET', 'large')[2] == b'changed'
+ checks.append('borrowed read remains the same snapshot during another commit')
+
+ assert rpc(server, 'PUT', 'large', large, 2)[0] == 200
+ sock = socket.create_connection(('127.0.0.1', server.port), timeout=8)
+ sock.sendall(header('GET', 'large'))
+ assert sock.recv(1)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
+ sock.close()
+ assert rpc(server, 'GET', 'binary')[2] == b'new'
+ checks.append('receiver reset aborts an outstanding borrowed transfer')
+
+ assert wire(server, header('PUT', 'too_large', OBJECT_LIMIT + 1, b'If-None-Match: *\r\n'))[0] == 413
+ malformed = [
+ b'PUT /objects/bad HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nContent-Length: 1\r\nIf-None-Match: *\r\n\r\nx',
+ b'PUT /objects/bad HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n',
+ b'GET /objects/bad HTTP/1.1\r\nHost: x\x00hidden\r\n\r\n',
+ ]
+ for request in malformed:
+ assert wire(server, request)[0] == 400
+ assert rpc(server, 'GET', 'bad')[0] == 404
+ checks.append('unsupported framing and oversized payload are rejected before effects')
+
+ # Existing authoring path: define, manipulate, save remotely, reconstruct
+ # in a fresh runtime. No fabricated project fixture or mock database.
+ source = WORKBENCH / 'examples' / 'direct.pure'
+ saved = directory / 'restored.json'
+ lines, _ = cli(server, 'point drag down 80 180\npoint drag move 210 210\npoint drag up 210 210\n'
+ 'connect $URL authored\npush 0\nquit\n', source)
+ snapshots = [line for line in lines if 'states' in line]
+ assert snapshots
+ expected_states = snapshots[-1]['states']
+ assert expected_states['x'] == 210 and expected_states['y'] == 190 and expected_states['dragging'] == 0
+ lines, _ = cli(server, f'connect $URL authored\npull\nsave-project {saved}\nquit\n')
+ assert [line for line in lines if 'states' in line][-1]['states'] == expected_states
+ document = json.loads(saved.read_text())
+ assert document['version'] == 2 and document['source'] == source.read_text()
+ lines, error = cli(server, 'connect $URL authored\npush 0\nquit\n', source, 1)
+ assert '412' in error
+ checks.append('existing workbench manipulation restores in a fresh runtime and reports conflict')
+
+ assert rpc(server, 'PUT', 'invalid_project', b'{"format":"pure-workbench","version":2,"source":"state x=1","states":[]}', 0)[0] == 200
+ lines, error = cli(server, 'show\nconnect $URL invalid_project\npull\nshow\nquit\n', source, 1)
+ states = [line['states'] for line in lines if 'states' in line]
+ assert states[0] == states[-1] and error
+ lines, error = cli(server, 'point drag down 90 170\nconnect $URL authored\npull\npush\nshow\nquit\n', source, 1)
+ states = [line['states'] for line in lines if 'states' in line]
+ assert states[0] == states[-1] and states[-1]['dragging'] == 1
+ assert len(error.strip().splitlines()) == 2
+ checks.append('invalid or non-idle restore preserves local state and does not adopt remote revision')
+
+ assert rpc(server, 'PUT', 'wide_revision', rpc_bytes(saved), 0)[0] == 200
+ with sqlite3.connect(database) as database_connection:
+ database_connection.execute('UPDATE pure_objects SET revision=? WHERE key=?', (9007199254740993, 'wide_revision'))
+ lines, _ = cli(server, 'connect $URL wide_revision\npull\npush\nquit\n')
+ revisions = [line['revision'] for line in lines if 'remote' in line and 'revision' in line]
+ assert revisions == ['9007199254740993', '9007199254740994']
+ checks.append('revision above JavaScript safe integer round trips without numeric coercion')
+
+ sock = socket.create_connection(('127.0.0.1', server.port), timeout=8)
+ sock.sendall(header('PUT', 'unread_ack', 4, b'If-None-Match: *\r\n') + b'done')
+ try:
+ deadline = time.monotonic() + 3
+ while True:
+ with sqlite3.connect(database) as database_connection:
+ committed = database_connection.execute('SELECT body FROM pure_objects WHERE key=?', ('unread_ack',)).fetchone()
+ if committed:
+ assert committed[0] == b'done'
+ break
+ assert time.monotonic() < deadline
+ time.sleep(0.01)
+ finally:
+ sock.close() # Client never consumes the successful response.
+ assert rpc(server, 'PUT', 'unread_ack', b'again', 0)[0] == 412
+ assert rpc(server, 'GET', 'unread_ack')[2] == b'done'
+ checks.append('unread acknowledgment does not undo commit or permit a stale retry')
+
+ # A disconnect after commit can hide the ACK; retries remain conditional.
+ assert rpc(server, 'PUT', 'empty', b'', 0)[0] == 200
+ assert rpc(server, 'GET', 'empty')[2] == b''
+ checks.append('empty body has a valid lifetime and stored revision')
+ records.append(server.stats)
+
+ with serving(binary, database) as restarted:
+ assert rpc(restarted, 'GET', 'binary')[2] == b'new'
+ assert rpc(restarted, 'GET', 'authored')[2] == rpc_bytes(saved)
+ checks.append('committed authoring state survives process restart')
+ records.append(restarted.stats)
+
+ # Fixed small payload budget is an admission test, not a throughput workload.
+ with serving(binary, database, budget=1024 * 1024, timeout=1200) as limited:
+ sock = socket.create_connection(('127.0.0.1', limited.port), timeout=8)
+ sock.sendall(header('GET', 'large'))
+ response = http.client.HTTPResponse(sock)
+ response.begin()
+ assert response.status == 200
+ assert response.read(1) == large[:1]
+ status, headers, _ = rpc(limited, 'GET', 'large')
+ assert status == 503 and headers['X-Pure-Error'] == 'payload_budget'
+ # Leave the first receiver stalled until the absolute connection deadline.
+ time.sleep(1.4)
+ assert rpc(limited, 'GET', 'binary')[2] == b'new'
+ response.close()
+ sock.close()
+ checks.append('slow consumer retains its lease; budget rejects another; deadline releases it')
+
+ idle = [socket.create_connection(('127.0.0.1', limited.port), timeout=8) for _ in range(8)]
+ try:
+ for s in idle:
+ s.sendall(b'G')
+ for s in idle:
+ assert s.recv(1) == b''
+ finally:
+ for s in idle:
+ s.close()
+ assert rpc(limited, 'GET', 'binary')[0] == 200
+ checks.append('connection slots stay bounded and recover after header deadlines')
+ assert limited.stats['budget_rejections'] >= 1 and limited.stats['expired'] >= 9
+ records.append(limited.stats)
+ return {'kind': 'native-path-correctness-not-performance', 'checks': checks,
+ 'server_runs': records, 'sqlite_runtime': server.ready['sqlite']}
+
+
+def rpc_bytes(path):
+ # CLI export is pretty JSON; wire export is compact JSON with the same model.
+ return json.dumps(json.loads(path.read_text()), ensure_ascii=False, separators=(',', ':')).encode()
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--server', type=Path, default=ROOT / 'build/pure-connections')
+ parser.add_argument('--output', type=Path)
+ args = parser.parse_args()
+ if args.output and args.output.exists():
+ parser.error('evidence file already exists')
+ binary = args.server.resolve()
+ with tempfile.TemporaryDirectory(prefix='pure-connections-check-') as temporary:
+ result = check(binary, Path(temporary))
+ result['binary_sha256'] = hashlib.sha256(binary.read_bytes()).hexdigest()
+ result['environment'] = {'platform': platform.platform(), 'machine': platform.machine(),
+ 'python': platform.python_version(), 'node': subprocess.check_output(['node', '--version'], text=True).strip()}
+ result['source_sha256'] = {str(path.relative_to(ROOT.parent.parent)): hashlib.sha256(path.read_bytes()).hexdigest()
+ for path in [*(ROOT / name for name in ('flow.c', 'flow.h', 'store.c', 'store.h', 'server.c', 'check.py', 'test-flow.c', 'Makefile')),
+ *(WORKBENCH / name for name in ('remote.mjs', 'cli.mjs', 'web/core.wasm', 'web/runtime.mjs',
+ 'web/compiler.mjs', 'web/project.mjs', 'examples/direct.pure'))]}
+ output = json.dumps(result, ensure_ascii=False, indent=2) + '\n'
+ if args.output:
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ with args.output.open('x') as destination:
+ destination.write(output)
+ print(output, end='')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c
new file mode 100644
index 0000000..1070c70
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.c
@@ -0,0 +1,41 @@
+#include "flow.h"
+#include <assert.h>
+
+void pure_transfer_abort(PureTransfer *t) {
+ if (!t->active) return;
+ PureRelease release = t->release;
+ void *owner = t->owner;
+ *t = (PureTransfer){0};
+ if (release) release(owner);
+}
+
+static void advance(PureTransfer *t) {
+ while (t->active && t->part < 2 && t->offset == t->spans[t->part].size) {
+ t->part++;
+ t->offset = 0;
+ }
+ if (t->active && t->part == 2) pure_transfer_abort(t);
+}
+
+void pure_transfer_init(PureTransfer *t, PureSpan header, PureSpan body,
+ void *owner, PureRelease release) {
+ assert(!t->active);
+ assert((!header.size || header.data) && (!body.size || body.data));
+ *t = (PureTransfer){.spans = {header, body}, .owner = owner,
+ .release = release, .active = true};
+ advance(t);
+}
+
+PureSpan pure_transfer_next(const PureTransfer *t, size_t limit) {
+ if (!t->active) return (PureSpan){0};
+ PureSpan span = t->spans[t->part];
+ size_t remaining = span.size - t->offset;
+ return (PureSpan){span.data + t->offset, remaining < limit ? remaining : limit};
+}
+
+bool pure_transfer_consume(PureTransfer *t, size_t bytes) {
+ if (!t->active || bytes > t->spans[t->part].size - t->offset) return false;
+ t->offset += bytes;
+ advance(t);
+ return true;
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h
new file mode 100644
index 0000000..6e27c58
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/flow.h
@@ -0,0 +1,25 @@
+#ifndef PURE_FLOW_H
+#define PURE_FLOW_H
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef struct { const unsigned char *data; size_t size; } PureSpan;
+typedef void (*PureRelease)(void *owner);
+
+/* A borrowed body stays alive until every byte is consumed or the transfer is
+ * aborted. A successful send() consumes bytes; peer receipt is a separate fact.
+ * Single owner thread; callbacks must not reenter or reuse this transfer. */
+typedef struct {
+ PureSpan spans[2];
+ size_t part, offset;
+ void *owner;
+ PureRelease release;
+ bool active;
+} PureTransfer;
+
+void pure_transfer_init(PureTransfer *, PureSpan header, PureSpan body,
+ void *owner, PureRelease);
+PureSpan pure_transfer_next(const PureTransfer *, size_t limit);
+bool pure_transfer_consume(PureTransfer *, size_t bytes);
+void pure_transfer_abort(PureTransfer *);
+#endif
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c
new file mode 100644
index 0000000..8f7b1ed
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/server.c
@@ -0,0 +1,346 @@
+#define _POSIX_C_SOURCE 200809L
+#include "store.h"
+#include <arpa/inet.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+enum { SLOTS = 8, HEADER_LIMIT = 4096, CHUNK = 65536 };
+typedef enum { READING_HEADER, READING_BODY, WRITING } Phase;
+typedef struct Server Server;
+typedef struct {
+ int fd;
+ Phase phase;
+ uint64_t deadline;
+ char request[HEADER_LIMIT + 1], response[512], key[65];
+ size_t header_size, body_size, received, charged;
+ unsigned char *body;
+ bool put;
+ int64_t expected;
+ PureStoredView stored;
+ PureTransfer transfer;
+ Server *server;
+} Connection;
+struct Server {
+ const char *path;
+ size_t budget, retained, peak_retained, active, peak_active;
+ uint64_t accepted, commits, gets, conflicts, budget_rejections, expired;
+ uint64_t releases, prefix_copy_bytes, borrowed_output_bytes;
+ uint64_t timeout_ms;
+ int send_buffer;
+ Connection connections[SLOTS];
+};
+static volatile sig_atomic_t stopping;
+static void stop_server(int sig) { (void)sig; stopping = 1; }
+
+static uint64_t now_ms(void) {
+ struct timespec t;
+ if (clock_gettime(CLOCK_MONOTONIC, &t)) abort();
+ return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000;
+}
+
+static bool number(const char *text, uint64_t maximum, uint64_t *value) {
+ if (!*text) return false;
+ uint64_t n = 0;
+ for (; *text; ++text) {
+ if (*text < '0' || *text > '9') return false;
+ unsigned digit = (unsigned)(*text - '0');
+ if (digit > maximum || n > (maximum - digit) / 10) return false;
+ n = n * 10 + digit;
+ }
+ *value = n;
+ return true;
+}
+
+static void release_payload(void *owner) {
+ Connection *c = owner;
+ bool owned = c->body || c->stored.db || c->charged;
+ pure_store_release(&c->stored);
+ free(c->body);
+ c->body = NULL;
+ assert(c->charged <= c->server->retained);
+ c->server->retained -= c->charged;
+ c->charged = 0;
+ if (owned) ++c->server->releases;
+}
+
+static void close_connection(Connection *c) {
+ if (c->fd < 0) return;
+ if (c->transfer.active) pure_transfer_abort(&c->transfer);
+ else if (c->body || c->stored.db || c->charged) release_payload(c);
+ close(c->fd);
+ --c->server->active;
+ *c = (Connection){.fd = -1};
+}
+
+static bool charge(Connection *c, size_t size) {
+ Server *s = c->server;
+ if (size > s->budget - s->retained) { ++s->budget_rejections; return false; }
+ s->retained += size;
+ c->charged += size;
+ if (s->retained > s->peak_retained) s->peak_retained = s->retained;
+ return true;
+}
+
+static void respond(Connection *c, int status, const char *reason, const char *error,
+ int64_t revision, PureSpan body) {
+ char tag[80] = "";
+ if (revision > 0) snprintf(tag, sizeof(tag), "ETag: \"%" PRId64 "\"\r\n", revision);
+ int length = snprintf(c->response, sizeof(c->response),
+ "HTTP/1.1 %d %s\r\nContent-Length: %zu\r\nConnection: close\r\n"
+ "Content-Type: application/octet-stream\r\nCache-Control: no-store\r\n"
+ "X-Pure-Error: %s\r\n%s\r\n", status, reason, body.size, error, tag);
+ assert(length > 0 && (size_t)length < sizeof(c->response));
+ c->phase = WRITING;
+ pure_transfer_init(&c->transfer,
+ (PureSpan){(const unsigned char *)c->response, (size_t)length}, body, c, release_payload);
+}
+
+static void fail(Connection *c, int status, const char *reason, const char *error) {
+ /* No outstanding backend access on the owner thread at this point. */
+ if (c->body || c->stored.db || c->charged) release_payload(c);
+ respond(c, status, reason, error, 0, (PureSpan){0});
+}
+
+static void run_request(Connection *c) {
+ PureResult result;
+ int64_t revision = 0;
+ if (c->put) {
+ result = pure_store_put(c->server->path, c->key, c->expected,
+ (PureSpan){c->body, c->body_size}, &revision);
+ /* Binding has been finalized, including the failure path. */
+ release_payload(c);
+ if (result == PURE_OK) ++c->server->commits;
+ } else {
+ result = pure_store_get(c->server->path, c->key,
+ c->server->budget - c->server->retained, &c->stored);
+ if (result == PURE_OK) {
+ if (!charge(c, c->stored.body.size)) abort(); /* Same owner thread. */
+ revision = c->stored.revision;
+ ++c->server->gets;
+ c->server->borrowed_output_bytes += c->stored.body.size;
+ } else if (result == PURE_BUDGET) ++c->server->budget_rejections;
+ }
+ switch (result) {
+ case PURE_OK: respond(c, 200, "OK", "none", revision, c->stored.body); break;
+ case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break;
+ case PURE_CONFLICT:
+ ++c->server->conflicts;
+ respond(c, 412, "Precondition Failed", "conflict", revision, (PureSpan){0}); break;
+ case PURE_BUSY: fail(c, 503, "Service Unavailable", "db_busy"); break;
+ case PURE_BUDGET: fail(c, 503, "Service Unavailable", "payload_budget"); break;
+ case PURE_LIMIT: fail(c, 413, "Content Too Large", "limit"); break;
+ default: fail(c, 500, "Internal Server Error", "storage"); break;
+ }
+}
+
+/* Deliberately bounded HTTP/1.1 subset: one GET/PUT, Content-Length, close.
+ * No transfer coding, pipelining, trailers, Expect, URL decoding, or proxying. */
+static bool parse_header(Connection *c, size_t end) {
+ for (size_t i = 0; i < end; ++i) {
+ unsigned char b = (unsigned char)c->request[i];
+ if ((b < 32 && b != '\r' && b != '\n' && b != '\t') || b == 127) return false;
+ }
+ char *line = c->request, *next = strstr(line, "\r\n");
+ if (!next) return false;
+ *next = 0;
+ char *path = strchr(line, ' ');
+ if (!path) return false;
+ *path++ = 0;
+ if (strcmp(line, "PUT") == 0) c->put = true;
+ else if (strcmp(line, "GET") != 0) return false;
+ char *version = strchr(path, ' ');
+ if (!version) return false;
+ *version++ = 0;
+ if (strcmp(version, "HTTP/1.1") || strncmp(path, "/objects/", 9)) return false;
+ const char *key = path + 9;
+ size_t key_size = strlen(key);
+ if (!key_size || key_size > 64) return false;
+ for (size_t i = 0; i < key_size; ++i) {
+ char b = key[i];
+ if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') ||
+ (b >= '0' && b <= '9') || b == '-' || b == '_')) return false;
+ }
+ memcpy(c->key, key, key_size + 1);
+ bool length_seen = false, condition_seen = false, host_seen = false;
+ line = next + 2;
+ while (*line != '\r') {
+ next = strstr(line, "\r\n");
+ if (!next) return false;
+ *next = 0;
+ char *value = strchr(line, ':');
+ if (!value || value == line) return false;
+ *value++ = 0;
+ for (char *p = line; *p; ++p)
+ if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
+ (*p >= '0' && *p <= '9') || *p == '-')) return false;
+ while (*value == ' ' || *value == '\t') ++value;
+ char *tail = value + strlen(value);
+ while (tail > value && (tail[-1] == ' ' || tail[-1] == '\t')) *--tail = 0;
+ if (strchr(value, '\r') || strchr(value, '\n')) return false;
+ uint64_t n;
+ if (!strcasecmp(line, "Content-Length")) {
+ if (length_seen || !number(value, SIZE_MAX, &n)) return false;
+ length_seen = true;
+ c->body_size = (size_t)n;
+ } else if (!strcasecmp(line, "Host")) {
+ if (host_seen || !*value) return false;
+ host_seen = true;
+ } else if (!strcasecmp(line, "If-Match")) {
+ if (condition_seen || tail - value < 3 || *value != '"' || tail[-1] != '"') return false;
+ tail[-1] = 0;
+ if (!number(value + 1, INT64_MAX, &n) || !n) return false;
+ c->expected = (int64_t)n;
+ condition_seen = true;
+ } else if (!strcasecmp(line, "If-None-Match")) {
+ if (condition_seen || strcmp(value, "*")) return false;
+ c->expected = 0;
+ condition_seen = true;
+ } else if (!strcasecmp(line, "Transfer-Encoding") || !strcasecmp(line, "Expect")) return false;
+ line = next + 2;
+ }
+ return host_seen && (c->put ? length_seen && condition_seen : !condition_seen && c->body_size == 0);
+}
+
+static void read_connection(Connection *c) {
+ if (c->phase == READING_HEADER) {
+ ssize_t n = recv(c->fd, c->request + c->header_size, HEADER_LIMIT - c->header_size, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ c->header_size += (size_t)n;
+ c->request[c->header_size] = 0;
+ /* Scan by byte count, so embedded NUL cannot hide a terminator. */
+ size_t end = 0;
+ for (size_t i = 3; i < c->header_size; ++i)
+ if (!memcmp(c->request + i - 3, "\r\n\r\n", 4)) { end = i + 1; break; }
+ if (!end) {
+ if (c->header_size == HEADER_LIMIT) fail(c, 431, "Request Header Fields Too Large", "headers");
+ return;
+ }
+ size_t prefix = c->header_size - end;
+ if (!parse_header(c, end)) { fail(c, 400, "Bad Request", "request"); return; }
+ if (c->body_size > PURE_OBJECT_LIMIT) { fail(c, 413, "Content Too Large", "limit"); return; }
+ if (prefix > c->body_size) { fail(c, 400, "Bad Request", "extra_body"); return; }
+ if (!c->put) { run_request(c); return; }
+ if (!charge(c, c->body_size)) { fail(c, 503, "Service Unavailable", "payload_budget"); return; }
+ c->body = malloc(c->body_size ? c->body_size : 1);
+ if (!c->body) { fail(c, 503, "Service Unavailable", "allocation"); return; }
+ if (prefix) memcpy(c->body, c->request + end, prefix);
+ c->server->prefix_copy_bytes += prefix;
+ c->received = prefix;
+ c->phase = READING_BODY;
+ if (c->received == c->body_size) run_request(c);
+ } else {
+ size_t remaining = c->body_size - c->received;
+ ssize_t n = recv(c->fd, c->body + c->received, remaining < CHUNK ? remaining : CHUNK, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ c->received += (size_t)n;
+ if (c->received == c->body_size) run_request(c);
+ }
+}
+
+static void write_connection(Connection *c) {
+ PureSpan span = pure_transfer_next(&c->transfer, CHUNK);
+ ssize_t n = send(c->fd, span.data, span.size, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ if (!pure_transfer_consume(&c->transfer, (size_t)n)) abort();
+ if (!c->transfer.active) close_connection(c);
+}
+
+static bool nonblocking(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
+}
+
+int main(int argc, char **argv) {
+ Server server = {.budget = 4u * 1024u * 1024u, .timeout_ms = 10000};
+ unsigned port = 8767;
+ for (int i = 1; i < argc; i += 2) {
+ if (i + 1 == argc) goto usage;
+ uint64_t value;
+ if (!strcmp(argv[i], "--db")) server.path = argv[i + 1];
+ else if (!strcmp(argv[i], "--port") && number(argv[i + 1], 65535, &value)) port = (unsigned)value;
+ else if (!strcmp(argv[i], "--budget") && number(argv[i + 1], SIZE_MAX, &value) && value) server.budget = (size_t)value;
+ else if (!strcmp(argv[i], "--timeout-ms") && number(argv[i + 1], 3600000, &value) && value) server.timeout_ms = value;
+ else if (!strcmp(argv[i], "--send-buffer") && number(argv[i + 1], INT32_MAX, &value)) server.send_buffer = (int)value;
+ else goto usage;
+ }
+ if (!server.path) goto usage;
+ if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize SQLite store\n", stderr); return 1; }
+ signal(SIGPIPE, SIG_IGN);
+ signal(SIGINT, stop_server);
+ signal(SIGTERM, stop_server);
+ int listener = socket(AF_INET, SOCK_STREAM, 0);
+ if (listener < 0) { perror("socket"); return 1; }
+ int one = 1;
+ setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+ struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
+ if (bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, SLOTS) || !nonblocking(listener)) {
+ perror("listen"); close(listener); return 1;
+ }
+ socklen_t address_size = sizeof(address);
+ if (getsockname(listener, (struct sockaddr *)&address, &address_size)) { close(listener); return 1; }
+ for (size_t i = 0; i < SLOTS; ++i) server.connections[i].fd = -1;
+ printf("{\"port\":%u,\"sqlite\":\"%s\"}\n", ntohs(address.sin_port), sqlite3_libversion());
+ fflush(stdout);
+ while (!stopping) {
+ struct pollfd polls[SLOTS + 1] = {{.fd = server.active < SLOTS ? listener : -1, .events = POLLIN}};
+ for (size_t i = 0; i < SLOTS; ++i) {
+ Connection *c = &server.connections[i];
+ polls[i + 1] = (struct pollfd){.fd = c->fd, .events = c->phase == WRITING ? POLLOUT : POLLIN};
+ }
+ int ready = poll(polls, SLOTS + 1, 50);
+ if (ready < 0 && errno != EINTR) { perror("poll"); stopping = 1; }
+ uint64_t now = now_ms();
+ for (size_t i = 0; i < SLOTS; ++i) {
+ Connection *c = &server.connections[i];
+ if (c->fd < 0) continue;
+ if (now >= c->deadline) { ++server.expired; close_connection(c); continue; }
+ short events = polls[i + 1].revents;
+ if (events & (POLLERR | POLLNVAL)) { close_connection(c); continue; }
+ if (events & (POLLIN | POLLOUT | POLLHUP)) {
+ if (c->phase == WRITING) write_connection(c); else read_connection(c);
+ }
+ }
+ if (!stopping && (polls[0].revents & POLLIN)) {
+ int fd = accept(listener, NULL, NULL);
+ if (fd < 0) continue;
+ if (!nonblocking(fd)) { close(fd); continue; }
+ if (server.send_buffer) setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &server.send_buffer, sizeof(server.send_buffer));
+ for (size_t i = 0; i < SLOTS; ++i) if (server.connections[i].fd < 0) {
+ server.connections[i] = (Connection){.fd = fd, .server = &server, .deadline = now_ms() + server.timeout_ms};
+ ++server.active;
+ ++server.accepted;
+ if (server.active > server.peak_active) server.peak_active = server.active;
+ break;
+ }
+ }
+ }
+ close(listener);
+ for (size_t i = 0; i < SLOTS; ++i) close_connection(&server.connections[i]);
+ fprintf(stderr, "{\"active\":%zu,\"retained_payload_bytes\":%zu,\"peak_retained_payload_bytes\":%zu,"
+ "\"payload_budget\":%zu,\"peak_connections\":%zu,\"accepted\":%" PRIu64 ",\"commits\":%" PRIu64 ","
+ "\"gets\":%" PRIu64 ",\"conflicts\":%" PRIu64 ",\"budget_rejections\":%" PRIu64 ","
+ "\"expired\":%" PRIu64 ",\"releases\":%" PRIu64 ",\"ingress_prefix_copy_bytes\":%" PRIu64 ","
+ "\"borrowed_output_bytes\":%" PRIu64 ",\"server_structure_bytes\":%zu}\n",
+ server.active, server.retained, server.peak_retained, server.budget, server.peak_active,
+ server.accepted, server.commits, server.gets, server.conflicts, server.budget_rejections,
+ server.expired, server.releases, server.prefix_copy_bytes, server.borrowed_output_bytes, sizeof(server));
+ return 0;
+usage:
+ fputs("pure-connections --db PATH [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n", stderr);
+ return 2;
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c
new file mode 100644
index 0000000..a805f72
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.c
@@ -0,0 +1,132 @@
+#include "store.h"
+#include <limits.h>
+#include <string.h>
+
+static PureResult error_result(int rc) {
+ int code = rc & 255;
+ return code == SQLITE_BUSY || code == SQLITE_LOCKED ? PURE_BUSY : PURE_STORE_ERROR;
+}
+
+static int execute(sqlite3 *db, const char *sql) {
+ return sqlite3_exec(db, sql, NULL, NULL, NULL);
+}
+
+static int open_db(const char *path, sqlite3 **db, int flags) {
+ int rc = sqlite3_open_v2(path, db, flags | SQLITE_OPEN_NOMUTEX, NULL);
+ if (rc != SQLITE_OK) return rc;
+ sqlite3_busy_timeout(*db, 0); /* No hidden wait/retry on this owner thread. */
+ sqlite3_limit(*db, SQLITE_LIMIT_LENGTH, (int)PURE_OBJECT_LIMIT + 4096);
+ return execute(*db, "PRAGMA cache_size=-64; PRAGMA synchronous=FULL;");
+}
+
+PureResult pure_store_init(const char *path) {
+ sqlite3 *db = NULL;
+ int rc = open_db(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
+ sqlite3_stmt *mode = NULL;
+ if (rc == SQLITE_OK) rc = sqlite3_prepare_v2(db, "PRAGMA journal_mode=WAL", -1, &mode, NULL);
+ if (rc == SQLITE_OK) {
+ rc = sqlite3_step(mode);
+ if (rc == SQLITE_ROW) {
+ const unsigned char *name = sqlite3_column_text(mode, 0);
+ rc = name && strcmp((const char *)name, "wal") == 0 ? SQLITE_OK : SQLITE_ERROR;
+ }
+ }
+ sqlite3_finalize(mode);
+ if (rc == SQLITE_OK) rc = execute(db,
+ "CREATE TABLE IF NOT EXISTS pure_objects("
+ "key TEXT PRIMARY KEY, revision INTEGER NOT NULL CHECK(revision>0),"
+ "body BLOB NOT NULL CHECK(typeof(body)='blob'));");
+ if (db) sqlite3_close(db);
+ return rc == SQLITE_OK ? PURE_OK : error_result(rc);
+}
+
+void pure_store_release(PureStoredView *view) {
+ sqlite3_finalize(view->statement);
+ if (view->db) {
+ execute(view->db, "ROLLBACK");
+ sqlite3_close(view->db);
+ }
+ *view = (PureStoredView){0};
+}
+
+PureResult pure_store_get(const char *path, const char *key, size_t available,
+ PureStoredView *out) {
+ *out = (PureStoredView){0};
+ int rc = open_db(path, &out->db, SQLITE_OPEN_READONLY);
+ if (rc == SQLITE_OK) rc = execute(out->db, "BEGIN");
+ if (rc == SQLITE_OK) rc = sqlite3_prepare_v2(out->db,
+ "SELECT revision,length(body) FROM pure_objects WHERE key=?", -1, &out->statement, NULL);
+ if (rc == SQLITE_OK) rc = sqlite3_bind_text(out->statement, 1, key, -1, SQLITE_STATIC);
+ if (rc == SQLITE_OK) rc = sqlite3_step(out->statement);
+ PureResult result = PURE_STORE_ERROR;
+ if (rc == SQLITE_DONE) { result = PURE_NOT_FOUND; goto fail; }
+ if (rc != SQLITE_ROW) { result = error_result(rc); goto fail; }
+ out->revision = sqlite3_column_int64(out->statement, 0);
+ sqlite3_int64 size = sqlite3_column_int64(out->statement, 1);
+ if (out->revision <= 0 || size < 0) goto fail;
+ if (size > PURE_OBJECT_LIMIT) { result = PURE_LIMIT; goto fail; }
+ if ((uint64_t)size > available) { result = PURE_BUDGET; goto fail; }
+ sqlite3_finalize(out->statement);
+ out->statement = NULL;
+ /* Keep the read transaction so admission and body observe the same version.
+ * The result remains borrowed until socket completion/abort finalizes it. */
+ rc = sqlite3_prepare_v2(out->db, "SELECT body FROM pure_objects WHERE key=?", -1, &out->statement, NULL);
+ if (rc == SQLITE_OK) rc = sqlite3_bind_text(out->statement, 1, key, -1, SQLITE_STATIC);
+ if (rc == SQLITE_OK) rc = sqlite3_step(out->statement);
+ if (rc != SQLITE_ROW) { result = error_result(rc); goto fail; }
+ if (sqlite3_column_type(out->statement, 0) != SQLITE_BLOB) goto fail;
+ const unsigned char *data = sqlite3_column_blob(out->statement, 0);
+ int bytes = sqlite3_column_bytes(out->statement, 0);
+ if (bytes != size || (bytes && !data)) goto fail;
+ out->body = (PureSpan){data, (size_t)bytes};
+ return PURE_OK;
+fail:
+ pure_store_release(out);
+ return result;
+}
+
+PureResult pure_store_put(const char *path, const char *key, int64_t expected,
+ PureSpan body, int64_t *revision) {
+ *revision = 0;
+ if (body.size > PURE_OBJECT_LIMIT || expected < 0) return PURE_LIMIT;
+ sqlite3 *db = NULL;
+ sqlite3_stmt *statement = NULL;
+ PureResult result = PURE_STORE_ERROR;
+ int rc = open_db(path, &db, SQLITE_OPEN_READWRITE);
+ if (rc == SQLITE_OK) rc = execute(db, "BEGIN IMMEDIATE");
+ if (rc != SQLITE_OK) { result = error_result(rc); goto finish; }
+ rc = sqlite3_prepare_v2(db, "SELECT revision FROM pure_objects WHERE key=?", -1, &statement, NULL);
+ if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 1, key, -1, SQLITE_STATIC);
+ if (rc == SQLITE_OK) rc = sqlite3_step(statement);
+ if (rc == SQLITE_ROW) *revision = sqlite3_column_int64(statement, 0);
+ else if (rc != SQLITE_DONE) { result = error_result(rc); goto finish; }
+ sqlite3_finalize(statement);
+ statement = NULL;
+ if (*revision != expected) { result = PURE_CONFLICT; goto finish; }
+ if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; }
+ const char *sql = *revision ?
+ "UPDATE pure_objects SET body=?1,revision=?2 WHERE key=?3" :
+ "INSERT INTO pure_objects(body,revision,key) VALUES(?1,?2,?3)";
+ rc = sqlite3_prepare_v2(db, sql, -1, &statement, NULL);
+ static const unsigned char empty = 0;
+ /* Do not copy at the binding boundary. Finalize before returning the input. */
+ if (rc == SQLITE_OK) rc = sqlite3_bind_blob(statement, 1, body.size ? body.data : &empty,
+ (int)body.size, SQLITE_STATIC);
+ if (rc == SQLITE_OK) rc = sqlite3_bind_int64(statement, 2, *revision + 1);
+ if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 3, key, -1, SQLITE_STATIC);
+ if (rc == SQLITE_OK) rc = sqlite3_step(statement);
+ if (rc != SQLITE_DONE) { result = error_result(rc); goto finish; }
+ rc = sqlite3_finalize(statement);
+ statement = NULL;
+ if (rc == SQLITE_OK) rc = execute(db, "COMMIT");
+ if (rc != SQLITE_OK) { result = error_result(rc); goto finish; }
+ ++*revision;
+ result = PURE_OK; /* Acknowledgment is constructed only after COMMIT. */
+finish:
+ sqlite3_finalize(statement);
+ if (db) {
+ if (!sqlite3_get_autocommit(db)) execute(db, "ROLLBACK");
+ sqlite3_close(db);
+ }
+ return result;
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h
new file mode 100644
index 0000000..c0e9f96
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/store.h
@@ -0,0 +1,25 @@
+#ifndef PURE_STORE_H
+#define PURE_STORE_H
+#include "flow.h"
+#include <sqlite3.h>
+#include <stdint.h>
+
+#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u)
+typedef enum {
+ PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET,
+ PURE_LIMIT, PURE_STORE_ERROR
+} PureResult;
+typedef struct {
+ sqlite3 *db;
+ sqlite3_stmt *statement;
+ PureSpan body;
+ int64_t revision;
+} PureStoredView;
+
+PureResult pure_store_init(const char *path);
+PureResult pure_store_get(const char *path, const char *key, size_t available,
+ PureStoredView *out);
+void pure_store_release(PureStoredView *);
+PureResult pure_store_put(const char *path, const char *key, int64_t expected,
+ PureSpan body, int64_t *revision);
+#endif
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c
new file mode 100644
index 0000000..997e7df
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/connections/test-flow.c
@@ -0,0 +1,110 @@
+#define _POSIX_C_SOURCE 200809L
+#include "store.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define CHECK(expr) do { if (!(expr)) { fprintf(stderr, "line %d: %s\n", __LINE__, #expr); exit(1); } } while (0)
+typedef struct { unsigned char *bytes; unsigned releases; } Owner;
+static void release(void *raw) {
+ Owner *owner = raw;
+ free(owner->bytes);
+ owner->bytes = NULL;
+ ++owner->releases;
+}
+
+static void transfers(void) {
+ Owner owner = {.bytes = malloc(5)};
+ CHECK(owner.bytes);
+ memcpy(owner.bytes, "a\0bcd", 5);
+ PureTransfer transfer = {0};
+ pure_transfer_init(&transfer, (PureSpan){(const unsigned char *)"head", 4},
+ (PureSpan){owner.bytes, 5}, &owner, release);
+ CHECK(!pure_transfer_consume(&transfer, 5));
+ CHECK(transfer.active && !owner.releases);
+ CHECK(pure_transfer_consume(&transfer, 0));
+ unsigned char received[9];
+ size_t used = 0;
+ while (transfer.active) {
+ PureSpan span = pure_transfer_next(&transfer, 2);
+ CHECK(span.size && span.size <= 2);
+ memcpy(received + used, span.data, span.size);
+ used += span.size;
+ CHECK(pure_transfer_consume(&transfer, span.size));
+ CHECK((used == 9) == (owner.releases == 1));
+ }
+ CHECK(used == 9 && !memcmp(received, "heada\0bcd", 9));
+ pure_transfer_abort(&transfer);
+ CHECK(owner.releases == 1 && !pure_transfer_consume(&transfer, 0));
+ owner.bytes = malloc(3);
+ CHECK(owner.bytes);
+ pure_transfer_init(&transfer, (PureSpan){0}, (PureSpan){owner.bytes, 3}, &owner, release);
+ CHECK(pure_transfer_consume(&transfer, 1));
+ pure_transfer_abort(&transfer);
+ pure_transfer_abort(&transfer);
+ CHECK(owner.releases == 2 && !transfer.active);
+ pure_transfer_init(&transfer, (PureSpan){0}, (PureSpan){0}, &owner, release);
+ CHECK(owner.releases == 3 && !transfer.active);
+}
+
+static void storage(const char *path) {
+ CHECK(pure_store_init(path) == PURE_OK);
+ PureStoredView first, second;
+ CHECK(pure_store_get(path, "missing", 100, &first) == PURE_NOT_FOUND);
+ CHECK(!first.db && !first.statement);
+ static const unsigned char original[] = {0, 1, 2, 255, 17};
+ static const unsigned char updated[] = {9, 8, 7};
+ int64_t revision;
+ CHECK(pure_store_put(path, "work", 0, (PureSpan){original, sizeof(original)}, &revision) == PURE_OK);
+ CHECK(revision == 1);
+ CHECK(pure_store_get(path, "work", sizeof(original) - 1, &first) == PURE_BUDGET);
+ CHECK(!first.db && !first.statement);
+ CHECK(pure_store_get(path, "work", sizeof(original), &first) == PURE_OK);
+ CHECK(first.body.size == sizeof(original) && first.revision == 1);
+ CHECK(pure_store_put(path, "work", 1, (PureSpan){updated, sizeof(updated)}, &revision) == PURE_OK);
+ CHECK(revision == 2);
+ CHECK(!memcmp(first.body.data, original, sizeof(original)));
+ CHECK(pure_store_get(path, "work", 100, &second) == PURE_OK);
+ CHECK(second.revision == 2 && second.body.size == sizeof(updated));
+ CHECK(!memcmp(second.body.data, updated, sizeof(updated)));
+ pure_store_release(&first);
+ pure_store_release(&first);
+ pure_store_release(&second);
+ CHECK(pure_store_put(path, "work", 1, (PureSpan){original, sizeof(original)}, &revision) == PURE_CONFLICT);
+ CHECK(revision == 2);
+
+ sqlite3 *other = NULL;
+ CHECK(sqlite3_open(path, &other) == SQLITE_OK);
+ CHECK(sqlite3_exec(other, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK);
+ CHECK(pure_store_put(path, "work", 2, (PureSpan){original, sizeof(original)}, &revision) == PURE_BUSY);
+ CHECK(sqlite3_exec(other, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK);
+ CHECK(sqlite3_exec(other, "CREATE TRIGGER deny BEFORE UPDATE ON pure_objects "
+ "BEGIN SELECT RAISE(ABORT,'injected failure'); END", NULL, NULL, NULL) == SQLITE_OK);
+ CHECK(pure_store_put(path, "work", 2, (PureSpan){original, sizeof(original)}, &revision) == PURE_STORE_ERROR);
+ CHECK(pure_store_get(path, "work", 100, &first) == PURE_OK);
+ CHECK(first.revision == 2 && !memcmp(first.body.data, updated, sizeof(updated)));
+ pure_store_release(&first);
+ CHECK(sqlite3_exec(other, "DROP TRIGGER deny; UPDATE pure_objects SET revision=9223372036854775807", NULL, NULL, NULL) == SQLITE_OK);
+ CHECK(pure_store_put(path, "work", INT64_MAX, (PureSpan){original, sizeof(original)}, &revision) == PURE_LIMIT);
+ CHECK(sqlite3_close(other) == SQLITE_OK);
+ CHECK(pure_store_put(path, "empty", 0, (PureSpan){0}, &revision) == PURE_OK);
+ CHECK(pure_store_get(path, "empty", 0, &first) == PURE_OK && first.body.size == 0);
+ pure_store_release(&first);
+}
+
+int main(void) {
+ transfers();
+ char directory[] = "/tmp/pure-connection-unit-XXXXXX";
+ CHECK(mkdtemp(directory));
+ char path[256], sidecar[280];
+ snprintf(path, sizeof(path), "%s/store.db", directory);
+ storage(path);
+ CHECK(unlink(path) == 0);
+ snprintf(sidecar, sizeof(sidecar), "%s-wal", path); unlink(sidecar);
+ snprintf(sidecar, sizeof(sidecar), "%s-shm", path); unlink(sidecar);
+ CHECK(rmdir(directory) == 0);
+ puts("{\"transfer\":\"partial-consumption-and-single-release\","
+ "\"storage\":\"snapshot-budget-conflict-busy-rollback-overflow-empty\"}");
+ return 0;
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs
new file mode 100644
index 0000000..b67f4d3
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.mjs
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+import { readFile, writeFile } from 'node:fs/promises';
+import { createInterface } from 'node:readline';
+import { createRuntime } from './web/runtime.mjs';
+import { parseProject } from './web/project.mjs';
+import { remoteTarget, pushProject, pullProject } from './remote.mjs';
+
+const help = `Pure authoring workbench · 같은 정의와 Zig 실행기
+ load PATH .pure 파일을 적용 (같은 이름의 상태 보존)
+ down NAME 정의한 입력 누르기
+ up NAME 정의한 입력 놓기
+ point NAME PHASE X Y 좌표 입력 (down / move / up / cancel)
+ point-target NAME VIEW PHASE X Y 그룹의 대상에 좌표 입력
+ change NAME VALUE 숫자 입력 전달
+ frame [1..600] 시간 진행, 1프레임 = 1/60초
+ show 상태와 도형 출력
+ limits C H W 표시 캐시(0..32)·변경 기록(0..8)·작업량(0..4294967295) 설정
+ forget [PART] 표시 기록 삭제: all, values, history (상태는 유지)
+ costs 마지막 표시 계산의 작업량·명령 수·캐시 적중 출력
+ reset 현재 정의의 초기 상태로
+ save PATH 적용한 정의를 저장
+ save-project PATH 적용 코드와 현재 배치를 JSON으로 저장
+ restore PATH 저장한 배치를 복원 (초안이 있으면 파일에 보존)
+ connect URL KEY 로컬 네이티브 저장 서버의 작업물에 연결
+ push [REVISION] 현재 배치를 저장 (첫 생성은 push 0, 충돌 시 기존 값 유지)
+ pull 원격 배치를 검증·복원하고 revision 기억
+ help / quit
+브라우저와 별도 인스턴스입니다. 임의 JavaScript는 실행하지 않습니다.`;
+
+async function main() {
+ const args = process.argv.slice(2);
+ if (args.includes('--help')) { console.log(help); return; }
+ if (args.length > 1) throw new Error('사용법: node cli.mjs [project.pure]');
+ const runtime = await createRuntime(await readFile(new URL('./web/core.wasm', import.meta.url)));
+ let savedDraft = null;
+ let remote = null, remoteRevision = null;
+ async function loadFile(path, restoreOnly = false) {
+ const content = await readFile(path, 'utf8');
+ if (restoreOnly || path.toLowerCase().endsWith('.json')) {
+ const project = parseProject(content);
+ if (project.version === 2) {
+ savedDraft = runtime.restoreDocument(project).draft;
+ return;
+ }
+ if (restoreOnly) throw new Error('현재 배치가 포함된 version 2 파일이 필요합니다.');
+ runtime.apply(project.source);
+ } else runtime.apply(content);
+ savedDraft = null;
+ }
+ if (args[0]) await loadFile(args[0]); else runtime.apply('');
+ const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
+ if (process.stdin.isTTY) console.log(help);
+ terminal.setPrompt('pure> ');
+ if (process.stdin.isTTY) terminal.prompt();
+ for await (const line of terminal) {
+ try {
+ const match = /^(\S+)(?:\s+(.+))?$/.exec(line.trim());
+ const name = match?.[1] || 'show';
+ const arg = match?.[2];
+ if (name === 'quit') { terminal.close(); break; }
+ if (name === 'help') console.log(help);
+ else if (name === 'costs' && !arg) console.log(JSON.stringify({ evaluation: runtime.evaluationStats() }));
+ else {
+ if (name === 'load' && arg) await loadFile(arg);
+ else if (name === 'restore' && arg) await loadFile(arg, true);
+ else if (name === 'connect' && arg) {
+ const parts = arg.split(/\s+/);
+ if (parts.length !== 2) throw new Error('connect URL KEY 형식을 사용하세요.');
+ remote = remoteTarget(parts[0], parts[1]);
+ remoteRevision = null;
+ console.log(JSON.stringify({ remote: remote.href }));
+ } else if (name === 'push') {
+ if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.');
+ const expected = arg ?? remoteRevision;
+ if (expected === null) throw new Error('먼저 pull로 버전을 읽거나 push 0으로 새 작업물을 만드세요.');
+ remoteRevision = await pushProject(remote, expected, runtime.exportDocument(savedDraft ?? undefined));
+ console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, stored: true }));
+ } else if (name === 'pull' && !arg) {
+ if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.');
+ const loaded = await pullProject(remote);
+ savedDraft = runtime.restoreDocument(loaded.project).draft;
+ remoteRevision = loaded.revision;
+ console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, restored: true }));
+ }
+ else if (name === 'save-project' && arg) await writeFile(arg, JSON.stringify(runtime.exportDocument(savedDraft ?? undefined), null, 2) + '\n', {flag:'wx'});
+ else if (name === 'save' && arg) await writeFile(arg, runtime.exportProject().source, { flag: 'wx' });
+ else if (name === 'down' && arg) runtime.down(arg, 'terminal');
+ else if (name === 'up' && arg) runtime.up(arg, 'terminal');
+ else if (name === 'point' && arg) {
+ const parts = arg.split(/\s+/);
+ if (parts.length !== 4) throw new Error('point NAME PHASE X Y 형식을 사용하세요.');
+ runtime.point(parts[0], parts[1], Number(parts[2]), Number(parts[3]), 'terminal');
+ } else if (name === 'point-target' && arg) {
+ const parts = arg.split(/\s+/);
+ if (parts.length !== 5) throw new Error('point-target NAME VIEW PHASE X Y 형식을 사용하세요.');
+ runtime.point(parts[0], parts[2], Number(parts[3]), Number(parts[4]), 'terminal', parts[1]);
+ } else if (name === 'change' && arg) {
+ const parts = arg.split(/\s+/);
+ if (parts.length !== 2) throw new Error('change NAME VALUE 형식을 사용하세요.');
+ runtime.change(parts[0], Number(parts[1]), 'terminal');
+ } else if (name === 'frame') {
+ if (arg !== undefined && !/^\d+$/.test(arg)) throw new Error('frame 1..600 정수를 입력하세요.');
+ const count = arg === undefined ? 1 : Number(arg);
+ if (count < 1 || count > 600) throw new Error('frame 1..600 정수를 입력하세요.');
+ runtime.frame(count);
+ } else if (name === 'reset' && !arg) runtime.reset();
+ else if (name === 'limits' && arg) {
+ const parts = arg.split(/\s+/);
+ if (parts.length !== 3 || parts.some(part => !/^\d+$/.test(part)))
+ throw new Error('limits C H W 정수 세 개를 입력하세요.');
+ const [cacheEntries, historyEntries, workLimit] = parts.map(Number);
+ runtime.configureEvaluation({ cacheEntries, historyEntries, workLimit });
+ } else if (name === 'forget') runtime.forgetEvaluation(arg || 'all');
+ else if (name !== 'show' || arg) throw new Error('명령 형식을 확인하세요. help로 안내를 볼 수 있습니다.');
+ const result = runtime.snapshot();
+ console.log(JSON.stringify(result));
+ if (result.lastError && !process.stdin.isTTY) process.exitCode = 1;
+ }
+ } catch (error) {
+ console.error(`${error.line ? `${error.line}:${error.column || 1} ` : ''}${error.message}`);
+ if (!process.stdin.isTTY) process.exitCode = 1;
+ }
+ if (process.stdin.isTTY) terminal.prompt();
+ }
+}
+
+main().catch((error) => { console.error(error.message); process.exitCode = 1; });
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs
new file mode 100644
index 0000000..f355d2d
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/cli.test.mjs
@@ -0,0 +1,121 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createRuntime } from './web/runtime.mjs';
+
+const root = fileURLToPath(new URL('.', import.meta.url));
+function run(input, file) {
+ const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], {
+ input, encoding: 'utf8', cwd: root,
+ });
+ if (result.error) throw result.error;
+ return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) };
+}
+
+test('terminal starts empty and produces the same charge trace as the shared runtime', async () => {
+ assert.deepEqual(run('show\nquit\n').snapshots[0].states, {});
+ const file = join(root, 'examples/charge.pure');
+ const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file);
+ assert.equal(result.status, 0, result.stderr);
+ const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm')));
+ runtime.apply(await readFile(file, 'utf8'));
+ runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame();
+ // JSON has no undefined-valued optional fields; compare the same wire form.
+ assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot())));
+ assert(result.snapshots.at(-1).states.y < 240);
+});
+
+test('invalid terminal commands do not alter state and fail piped execution', () => {
+ const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure'));
+ assert.equal(result.status, 1);
+ assert.deepEqual(result.snapshots[0], result.snapshots[1]);
+ assert.equal(result.stderr.trim().split('\n').length, 7);
+});
+
+test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => {
+ const file = join(root, 'examples/direct.pure');
+ const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file);
+ assert.equal(result.status, 0, result.stderr);
+ const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm')));
+ runtime.apply(await readFile(file, 'utf8'));
+ for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]])
+ runtime.point('drag', phase, x, y, 'terminal');
+ assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot())));
+ assert.equal(result.snapshots.at(-1).states.x, 350);
+ assert.equal(result.snapshots.at(-1).states.y, 210);
+ assert.deepEqual(result.snapshots.at(-1).held, []);
+});
+
+test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => {
+ const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-'));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure');
+ const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure');
+ await writeFile(first, 'state n = 7');
+ await writeFile(second, 'state n = 100\nstate added = 9');
+ await writeFile(invalid, 'state n = broken');
+ const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first);
+ assert.equal(result.status, 1);
+ assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 });
+ assert.deepEqual(result.snapshots[1], result.snapshots[0]);
+ assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 });
+ assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8'));
+ assert.match(result.stderr, /EEXIST/);
+});
+
+test('terminal exposes arithmetic rollback and retains failure through matching release', async t => {
+ const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-'));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ const file = join(directory, 'error.pure');
+ await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }');
+ const result = run('down tap\nup tap\nshow\nreset\nquit\n', file);
+ assert.equal(result.status, 1);
+ for (const snapshot of result.snapshots.slice(0, 3)) {
+ assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4);
+ assert.equal(snapshot.errorLine, 3);
+ }
+ assert.equal(result.snapshots[3].lastError, 0);
+});
+
+test('terminal configures and discards evaluation records and recovers from a display limit', () => {
+ const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure'));
+ assert.equal(result.status, 1); // The explicit zero-budget display failed.
+ assert.equal(result.stderr, '');
+ const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots;
+ assert.deepEqual(warm.views, configured.views);
+ assert(costs.evaluation.cacheHits > 0);
+ assert.deepEqual(forgotten.states, configured.states);
+ assert.deepEqual(limited.views, []);
+ assert.equal(limited.lastError, 101);
+ assert.deepEqual(recovered, final);
+ assert.deepEqual(recovered.views, configured.views);
+ assert.deepEqual(recovered.states, configured.states);
+});
+
+test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => {
+ const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-'));
+ t.after(() => rm(directory, {recursive:true,force:true}));
+ const saved=join(directory,'layout.json'), copied=join(directory,'copy.json');
+ const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure'));
+ assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350);
+ const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete =';
+ await writeFile(saved,JSON.stringify(document));
+ const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved);
+ assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80);
+ const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states);
+ const rejected=run(`save-project ${saved}\nquit\n`,saved);
+ assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/);
+});
+
+test('invalid restoration leaves terminal code and state unchanged', async t => {
+ const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-'));
+ t.after(()=>rm(directory,{recursive:true,force:true}));
+ const file=join(directory,'bad.json');
+ await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]}));
+ const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure'));
+ assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]);
+});
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure
new file mode 100644
index 0000000..b29721c
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/examples/direct.pure
@@ -0,0 +1,47 @@
+# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다.
+state x = 80
+state y = 160
+state grab_x = 0
+state grab_y = 0
+state start_x = 80
+state start_y = 160
+state dragging = 0
+
+input drag = pointer(tile)
+input left = ArrowLeft
+input right = ArrowRight
+
+box left_area(20, 100, 280, 220, "#233a31")
+box right_area(340, 100, 280, 220, "#343b25")
+box tile(x, y, 56, 44, "#c9f08b")
+text horizontal(30, 45, x, "x: ")
+text vertical(180, 45, y, "y: ")
+text region(350, 70, x >= 320, "오른쪽 영역: ")
+text active(30, 75, dragging, "잡고 있음: ")
+
+on drag.down {
+ start_x = x
+ start_y = y
+ grab_x = event_x - x
+ grab_y = event_y - y
+ dragging = 1
+}
+on drag.move {
+ x = clamp(event_x - grab_x, 20, 564)
+ y = clamp(event_y - grab_y, 100, 276)
+}
+on drag.up {
+ # 놓기는 마지막으로 표시한 위치를 확정합니다.
+ dragging = 0
+}
+on drag.cancel {
+ x = start_x
+ y = start_y
+ dragging = 0
+}
+on left.down {
+ if dragging == 0 { x = max(20, x - 10) }
+}
+on right.down {
+ if dragging == 0 { x = min(564, x + 10) }
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs
new file mode 100644
index 0000000..6ee6a59
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/remote.mjs
@@ -0,0 +1,89 @@
+import http from 'node:http';
+import { MAX_PROJECT_BYTES, parseProject } from './web/project.mjs';
+
+const maxRevision = 9223372036854775807n;
+function revisionText(value, allowZero = false) {
+ if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value) ||
+ value.length > 19 || BigInt(value) > maxRevision || (!allowZero && value === '0'))
+ throw new Error('revision은 범위 안의 정수 문자열이어야 합니다.');
+ return value;
+}
+
+export function remoteTarget(base, key) {
+ const url = new URL(base);
+ if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost'].includes(url.hostname) ||
+ url.username || url.password || url.pathname !== '/' || url.search || url.hash)
+ throw new Error('현재 연결 호스트는 로컬 http://127.0.0.1:PORT 형식을 사용합니다.');
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(key)) throw new Error('작업물 이름은 영문·숫자·_·- 1~64자입니다.');
+ url.hostname = '127.0.0.1';
+ url.pathname = `/objects/${key}`;
+ return url;
+}
+
+function request(url, method, body, headers = {}) {
+ return new Promise((resolve, reject) => {
+ let done = false;
+ const finish = (error, value) => {
+ if (done) return;
+ done = true;
+ clearTimeout(deadline);
+ if (error) reject(error); else resolve(value);
+ };
+ const req = http.request(url, { method, agent: false, headers: { ...headers,
+ 'Content-Length': body?.byteLength ?? 0, Connection: 'close' } }, (res) => {
+ const chunks = [];
+ let size = 0;
+ res.on('data', (chunk) => {
+ size += chunk.length;
+ if (size > MAX_PROJECT_BYTES) {
+ const error = new Error('서버 작업물이 허용 크기를 넘었습니다.');
+ finish(error);
+ res.destroy(error);
+ req.destroy(error);
+ } else chunks.push(chunk);
+ });
+ res.on('error', (error) => finish(error));
+ res.on('aborted', () => finish(new Error('응답이 완료되기 전에 연결이 끊겼습니다.')));
+ res.on('end', () => {
+ if (res.statusCode !== 200) {
+ const error = new Error(`원격 작업 실패: ${res.statusCode} ${res.headers['x-pure-error'] ?? ''}`.trim());
+ error.status = res.statusCode;
+ error.etag = res.headers.etag;
+ finish(error);
+ return;
+ }
+ try {
+ const match = /^"([1-9]\d*)"$/.exec(res.headers.etag ?? '');
+ if (!match) throw new Error('서버의 revision 응답이 올바르지 않습니다.');
+ const revision = revisionText(match[1]);
+ finish(null, { revision, bytes: Buffer.concat(chunks, size) });
+ } catch (error) { finish(error); }
+ });
+ });
+ const deadline = setTimeout(() => {
+ const error = new Error('원격 작업 시간 초과: 저장 완료 여부는 다시 확인해야 합니다.');
+ finish(error);
+ req.destroy(error);
+ }, 12000);
+ req.on('error', (error) => finish(error));
+ req.end(body);
+ });
+}
+
+export async function pushProject(target, expected, project) {
+ revisionText(expected, true);
+ const text = JSON.stringify(project);
+ parseProject(text);
+ const body = Buffer.from(text, 'utf8');
+ const headers = expected === '0' ? { 'If-None-Match': '*' } : { 'If-Match': `"${expected}"` };
+ const result = await request(target, 'PUT', body, headers);
+ return result.revision;
+}
+
+export async function pullProject(target) {
+ const result = await request(target, 'GET');
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(result.bytes);
+ const project = parseProject(text);
+ if (project.version !== 2) throw new Error('현재 배치를 포함한 version 2 작업물이 필요합니다.');
+ return { project, revision: result.revision };
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs
new file mode 100644
index 0000000..6aec335
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/compiler.mjs
@@ -0,0 +1,496 @@
+// Pure's experimental scalar interaction language. No host code is evaluated.
+const LIMITS = Object.freeze({ sourceBytes: 32768, states: 32, inputs: 8,
+ views: 24, handlers: 64, instructions: 512, depth: 64, stack: 64 });
+const RESERVED = new Set(['frame', 'dt', 'event_x', 'event_y', 'event_value', 'min', 'max', 'clamp']);
+const BINARY = new Map([
+ ['||', [1, 15]], ['&&', [2, 14]], ['==', [3, 12]], ['!=', [3, 13]],
+ ['<', [4, 8]], ['<=', [4, 9]], ['>', [4, 10]], ['>=', [4, 11]],
+ ['+', [5, 3]], ['-', [5, 4]], ['*', [6, 5]], ['/', [6, 6]],
+]);
+const FUNCTIONS = { min: [2, 17], max: [2, 18], clamp: [3, 19] };
+const VIEW_FIELDS = { box: ['x', 'y', 'width', 'height'],
+ circle: ['x', 'y', 'radius'], text: ['x', 'y', 'value'] };
+
+function fail(token, message) {
+ const error = new Error(`${message} (${token.line}:${token.column})`);
+ error.line = token.line;
+ error.column = token.column;
+ throw error;
+}
+
+function lex(source) {
+ const tokens = [];
+ let offset = 0, line = 1, column = 1;
+ const advance = () => {
+ const char = source[offset++];
+ if (char === '\n') { line++; column = 1; } else column++;
+ return char;
+ };
+ while (offset < source.length) {
+ const char = source[offset];
+ if (char === ' ' || char === '\t' || char === '\r') { advance(); continue; }
+ if (char === '#') { while (offset < source.length && source[offset] !== '\n') advance(); continue; }
+ const token = { line, column };
+ if (char === '\n') { advance(); tokens.push({ ...token, kind: 'nl' }); continue; }
+ if (char === '"') {
+ advance();
+ let value = '', closed = false;
+ while (offset < source.length) {
+ const next = advance();
+ if (next === '"') { closed = true; break; }
+ if (next === '\n' || next === '\r') fail(token, 'Strings must fit on one line');
+ if (next === '\\') {
+ const escaped = advance();
+ const escapes = { n: '\n', r: '\r', t: '\t', '"': '"', '\\': '\\' };
+ if (!Object.hasOwn(escapes, escaped)) fail(token, 'Unsupported string escape');
+ value += escapes[escaped];
+ } else value += next;
+ }
+ if (!closed) fail(token, 'Unterminated string');
+ tokens.push({ ...token, kind: 'string', value }); continue;
+ }
+ const remaining = source.slice(offset);
+ const number = remaining.match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/);
+ if (number) {
+ const value = Number(number[0]);
+ if (!Number.isFinite(value)) fail(token, 'Number must be finite');
+ for (let i = 0; i < number[0].length; i++) advance();
+ tokens.push({ ...token, kind: 'number', value }); continue;
+ }
+ const name = remaining.match(/^[A-Za-z_][A-Za-z_0-9]*/);
+ if (name) {
+ for (let i = 0; i < name[0].length; i++) advance();
+ tokens.push({ ...token, kind: 'name', value: name[0] }); continue;
+ }
+ const pair = source.slice(offset, offset + 2);
+ if (['<=', '>=', '==', '!=', '&&', '||'].includes(pair)) {
+ advance(); advance(); tokens.push({ ...token, kind: pair }); continue;
+ }
+ if ('=+-*/!<>(){},.;:'.includes(char)) {
+ advance(); tokens.push({ ...token, kind: char }); continue;
+ }
+ fail(token, `Unexpected character ${JSON.stringify(char)}`);
+ }
+ tokens.push({ kind: 'eof', line, column });
+ return tokens;
+}
+
+class Parser {
+ constructor(tokens) { this.tokens = tokens; this.position = 0; this.depth = 0; }
+ peek() { return this.tokens[this.position]; }
+ take() { return this.tokens[this.position++]; }
+ is(kind, value) { return this.peek().kind === kind && (value === undefined || this.peek().value === value); }
+ expect(kind, description = kind) {
+ if (!this.is(kind)) fail(this.peek(), `Expected ${description}`);
+ return this.take();
+ }
+ newlines() { while (this.is('nl')) this.take(); }
+ separators() { while (this.is('nl') || this.is(';')) this.take(); }
+ end() {
+ if (!['nl', ';', '}', 'eof'].includes(this.peek().kind)) fail(this.peek(), 'Expected newline or semicolon');
+ this.separators();
+ }
+ reference(token) {
+ if (!this.is('.')) return token.value;
+ this.take();
+ return `${token.value}.${this.expect('name', 'field name').value}`;
+ }
+ initial() {
+ let sign = 1;
+ if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1;
+ return sign * this.expect('number', 'numeric state initializer').value;
+ }
+ node(token, properties, children = []) {
+ const depth = 1 + Math.max(0, ...children.map(child => child.depth));
+ if (depth > LIMITS.depth) fail(token, 'Expression nesting exceeds 64');
+ return { token, depth, ...properties };
+ }
+ expression(minimum = 0, multiline = false) {
+ if (++this.depth > LIMITS.depth) fail(this.peek(), 'Expression nesting exceeds 64');
+ if (multiline) this.newlines();
+ const token = this.take();
+ let left;
+ if (token.kind === 'number') left = this.node(token, { kind: 'constant', value: token.value });
+ else if (token.kind === '-' || token.kind === '!') {
+ const child = this.expression(7, multiline);
+ left = this.node(token, { kind: 'unary', op: token.kind === '-' ? 7 : 16, child }, [child]);
+ } else if (token.kind === '(') {
+ left = this.expression(0, true); this.newlines(); this.expect(')');
+ } else if (token.kind === 'name') {
+ if (Object.hasOwn(FUNCTIONS, token.value) && this.is('(')) {
+ this.take();
+ const args = [];
+ this.newlines();
+ if (!this.is(')')) {
+ for (;;) {
+ args.push(this.expression(0, true)); this.newlines();
+ if (!this.is(',')) break;
+ this.take();
+ }
+ }
+ this.expect(')');
+ if (args.length !== FUNCTIONS[token.value][0]) fail(token, `${token.value} expects ${FUNCTIONS[token.value][0]} arguments`);
+ left = this.node(token, { kind: 'call', op: FUNCTIONS[token.value][1], args }, args);
+ } else left = this.node(token, { kind: 'reference', name: this.reference(token) });
+ } else fail(token, 'Expected a number, state, or expression');
+ for (;;) {
+ if (multiline) this.newlines();
+ const entry = BINARY.get(this.peek().kind);
+ if (!entry || entry[0] < minimum) break;
+ const operator = this.take();
+ const right = this.expression(entry[0] + 1, multiline);
+ left = this.node(operator, { kind: 'binary', op: entry[1], left, right }, [left, right]);
+ }
+ this.depth--;
+ return left;
+ }
+ block(depth = 0) {
+ if (depth >= LIMITS.depth) fail(this.peek(), 'Handler nesting exceeds 64');
+ this.newlines(); this.expect('{'); this.separators();
+ const statements = [];
+ while (!this.is('}')) {
+ const token = this.expect('name', 'assignment, if, or closing brace');
+ if (token.value === 'if' && !this.is('=')) {
+ const condition = this.expression();
+ const body = this.block(depth + 1);
+ statements.push({ kind: 'if', token, condition, body });
+ } else {
+ const name = this.reference(token);
+ this.expect('=');
+ statements.push({ kind: 'assign', token, name, expression: this.expression() });
+ }
+ this.end();
+ }
+ this.take();
+ return statements;
+ }
+ parse() {
+ const declarations = [];
+ this.separators();
+ while (!this.is('eof')) {
+ const token = this.expect('name', 'declaration');
+ const kind = token.value;
+ if (kind === 'state') {
+ const name = this.expect('name', 'state name'); this.expect('=');
+ declarations.push({ kind, token, name, initial: this.initial() });
+ } else if (kind === 'record') {
+ const name = this.expect('name', 'record type'); this.newlines(); this.expect('{'); this.separators();
+ const fields = [];
+ while (!this.is('}')) {
+ const field = this.expect('name', 'field name'); this.expect('=');
+ fields.push({name:field, initial:this.initial()}); this.end();
+ }
+ this.take(); declarations.push({kind,token,name,fields});
+ } else if (kind === 'item') {
+ const name = this.expect('name', 'item name'); this.expect('=');
+ const type = this.expect('name', 'record type'); this.expect('('); this.newlines();
+ const fields = [];
+ while (!this.is(')')) {
+ const field = this.expect('name', 'field name'); this.expect('=');
+ fields.push({name:field, initial:this.initial()}); this.newlines();
+ if (!this.is(',')) break;
+ this.take(); this.newlines();
+ }
+ this.expect(')'); declarations.push({kind,token,name,type,fields});
+ } else if (kind === 'rule') {
+ const name = this.expect('name', 'rule name'); this.expect('(');
+ const parameter = this.expect('name', 'record parameter'); this.expect(':');
+ const type = this.expect('name', 'record type'); this.expect(')');
+ declarations.push({kind,token,name,parameter,type,body:this.block()});
+ } else if (kind === 'input') {
+ const name = this.expect('name', 'input name'); this.expect('=');
+ const key = this.expect('name', 'keyboard code or pointer(view)');
+ if (key.value === 'value') {
+ declarations.push({kind,token,name,valueInput:true}); this.end(); continue;
+ }
+ if (key.value === 'pointer') {
+ if (this.is('{')) {
+ this.take(); this.separators(); const routes = [];
+ while (!this.is('}')) {
+ const target = this.expect('name', 'view ID'); this.expect(':');
+ const item = this.expect('name', 'item name'); routes.push({target,item}); this.end();
+ }
+ this.take(); declarations.push({kind,token,name,routes}); this.end(); continue;
+ }
+ this.expect('(');
+ const target = this.expect('name', 'view ID'); this.expect(')');
+ declarations.push({ kind, token, name, target });
+ this.end(); continue;
+ }
+ if (!/^(?:Space|Enter|Arrow(?:Left|Right|Up|Down)|Key[A-Z]|Digit[0-9])$/.test(key.value)) fail(key, 'Unsupported keyboard code');
+ declarations.push({ kind, token, name, key });
+ } else if (Object.hasOwn(VIEW_FIELDS, kind)) {
+ const name = this.expect('name', 'view ID'); this.expect('(');
+ const expressions = [];
+ for (let i = 0; i < VIEW_FIELDS[kind].length; i++) {
+ if (i) { this.newlines(); this.expect(','); }
+ expressions.push(this.expression(0, true));
+ }
+ this.newlines();
+ let color, prefix = '';
+ if (kind !== 'text') {
+ this.expect(','); this.newlines();
+ const value = this.expect('string', 'literal hex color');
+ if (!/^#[0-9a-fA-F]{6}$/.test(value.value)) fail(value, 'Color must use six hexadecimal digits, such as "#356548"');
+ color = value.value;
+ } else if (this.is(',')) { this.take(); this.newlines(); prefix = this.expect('string', 'literal text prefix').value; }
+ this.newlines(); this.expect(')');
+ declarations.push({ kind: 'view', type: kind, token, name, expressions, color, prefix });
+ } else if (kind === 'on') {
+ const event = this.expect('name', 'input name or frame');
+ let edge;
+ if (event.value !== 'frame') {
+ this.expect('.'); edge = this.expect('name', 'down, up, move or cancel');
+ if (!['down', 'up', 'move', 'cancel', 'change'].includes(edge.value)) fail(edge, 'Expected down or up, move, cancel or change');
+ }
+ if (this.is('name', 'use')) {
+ this.take(); const rule = this.expect('name', 'rule name'); this.expect('(');
+ const item = this.expect('name', 'item name or input.target');
+ const argument = this.reference(item); this.expect(')');
+ declarations.push({kind:'handler',token,event,edge,rule,item,argument});
+ } else declarations.push({ kind: 'handler', token, event, edge, body: this.block() });
+ } else fail(token, `Unknown declaration ${kind}`);
+ this.end();
+ }
+ return declarations;
+ }
+}
+
+export function compile(source) {
+ if (typeof source !== 'string') fail({ line: 1, column: 1 }, 'Source must be a string');
+ const sourceBytes = new TextEncoder().encode(source).length;
+ if (sourceBytes > LIMITS.sourceBytes) fail({ line: 1, column: 1 }, 'Source exceeds 32768 UTF-8 bytes');
+ const declarations = new Parser(lex(source)).parse();
+ const program = { states: [], inputs: [], instructions: [], handlers: [], views: [], pointerTargets: [], metrics: {} };
+ const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(), targets = new Set();
+ const records = new Map(), items = new Map(), rules = new Map();
+ const scalarNames = new Set(declarations.filter(d => d.kind === 'state').map(d => d.name.value));
+ for (const declaration of declarations) {
+ if (!['record','rule'].includes(declaration.kind)) continue;
+ const {name,fields,kind} = declaration, symbols = kind === 'record' ? records : rules;
+ if (RESERVED.has(name.value)) fail(name, `Reserved name ${name.value}`);
+ if (symbols.has(name.value)) fail(name, `Duplicate ${kind} ${name.value}`);
+ if (kind === 'record') {
+ if (!fields.length || fields.length > 8) fail(name, 'A record needs 1..8 fields');
+ const used = new Set();
+ for (const field of fields) {
+ if (used.has(field.name.value)) fail(field.name, `Duplicate field ${field.name.value}`);
+ if (RESERVED.has(field.name.value)) fail(field.name, `Reserved name ${field.name.value}`);
+ used.add(field.name.value);
+ }
+ }
+ symbols.set(name.value, declaration);
+ }
+ let handlerCount = 0;
+ for (const declaration of declarations) {
+ const { kind, name, token } = declaration;
+ if (name && RESERVED.has(name.value)) fail(name, `Reserved name ${name.value}`);
+ if (kind === 'record' || kind === 'rule') continue;
+ if (kind === 'item') {
+ if (items.has(name.value) || scalarNames.has(name.value)) fail(name, `Duplicate state/item ${name.value}`);
+ const record = records.get(declaration.type.value);
+ if (!record) fail(declaration.type, `Unknown record ${declaration.type.value}`);
+ const overrides = new Map();
+ for (const field of declaration.fields) {
+ if (!record.fields.some(f => f.name.value === field.name.value)) fail(field.name, `Unknown field ${field.name.value}`);
+ if (overrides.has(field.name.value)) fail(field.name, `Duplicate field ${field.name.value}`);
+ overrides.set(field.name.value, field.initial);
+ }
+ const base = states.size;
+ for (const field of record.fields) {
+ if (states.size >= LIMITS.states) fail(name, 'At most 32 states are allowed, including item fields');
+ const qualified = `${name.value}.${field.name.value}`, index = states.size;
+ states.set(qualified,index);
+ program.states.push({name:qualified,initial:overrides.has(field.name.value) ? overrides.get(field.name.value) : field.initial,line:token.line});
+ }
+ items.set(name.value,{type:declaration.type.value,base,count:record.fields.length});
+ } else if (kind === 'state' || kind === 'input') {
+ const symbols = kind === 'state' ? states : inputs;
+ if (symbols.has(name.value)) fail(name, `Duplicate ${kind} ${name.value}`);
+ if (symbols.size >= LIMITS[`${kind}s`]) fail(name, `At most ${LIMITS[`${kind}s`]} ${kind}s are allowed`);
+ symbols.set(name.value, symbols.size);
+ if (kind === 'state') program.states.push({ name: name.value, initial: declaration.initial, line: token.line });
+ else if (declaration.valueInput) {
+ program.inputs.push({name:name.value,kind:'value',line:token.line});
+ } else if (declaration.routes) {
+ if (!declaration.routes.length) fail(name, 'A pointer group needs at least one target');
+ program.inputs.push({name:name.value, routes:[], line:token.line});
+ } else if (declaration.target) {
+ const target = declaration.target;
+ if (targets.has(target.value)) fail(target, `Duplicate pointer target ${target.value}`);
+ targets.add(target.value);
+ program.inputs.push({ name: name.value, target: target.value, line: token.line });
+ } else {
+ if (keys.has(declaration.key.value)) fail(declaration.key, `Duplicate keyboard code ${declaration.key.value}`);
+ keys.add(declaration.key.value);
+ program.inputs.push({ name: name.value, key: declaration.key.value, line: token.line });
+ }
+ } else if (kind === 'view') {
+ if (views.has(name.value)) fail(name, `Duplicate view ID ${name.value}`);
+ if (views.size >= LIMITS.views) fail(name, 'At most 24 views are allowed');
+ views.add(name.value);
+ } else if (++handlerCount > LIMITS.handlers) fail(token, 'At most 64 handlers are allowed');
+ }
+ for (const declaration of declarations) {
+ if (declaration.target && !views.has(declaration.target.value))
+ fail(declaration.target, `Unknown pointer target ${declaration.target.value}`);
+ }
+ const routedItems = new Set();
+ for (const declaration of declarations) {
+ if (!declaration.routes) continue;
+ const input = inputs.get(declaration.name.value), resolved = program.inputs[input];
+ let type;
+ for (const {target,item:token} of declaration.routes) {
+ if (!views.has(target.value)) fail(target, `Unknown pointer target ${target.value}`);
+ if (targets.has(target.value)) fail(target, `Duplicate pointer target ${target.value}`);
+ const item = items.get(token.value);
+ if (!item) fail(token, `Unknown item ${token.value}`);
+ if (type && type !== item.type) fail(token, 'Pointer group targets must share one record type');
+ if (routedItems.has(token.value)) fail(token, `Item ${token.value} already has a pointer route`);
+ targets.add(target.value); routedItems.add(token.value); type = item.type;
+ const route = program.pointerTargets.length;
+ if (route >= 24) fail(target, 'At most 24 pointer targets are allowed');
+ program.pointerTargets.push({input,base:item.base,count:item.count});
+ resolved.routes.push({target:target.value, item:token.value, route});
+ }
+ resolved.type = type;
+ }
+ for (const rule of rules.values()) {
+ if (!records.has(rule.type.value)) fail(rule.type, `Unknown record ${rule.type.value}`);
+ if (RESERVED.has(rule.parameter.value)) fail(rule.parameter, `Reserved name ${rule.parameter.value}`);
+ }
+ let pointHandler = false, valueHandler = false;
+ let parameters = null;
+ let parameterPrefix = null;
+ let stack = 0, maxStack = 0;
+ const emit = (op, token, arg = 0, value = 0, delta = 0) => {
+ if (program.instructions.length >= LIMITS.instructions) fail(token, 'At most 512 instructions are allowed');
+ stack += delta;
+ if (stack < 0 || stack > LIMITS.stack) fail(token, 'Expression stack exceeds 64');
+ maxStack = Math.max(maxStack, stack);
+ program.instructions.push({ op, arg, value, line: token.line });
+ };
+ const expression = (node, binding = false) => {
+ const { token } = node;
+ if (node.kind === 'constant') emit(0, token, 0, node.value, 1);
+ else if (node.kind === 'reference') {
+ if (node.name === 'event_value') {
+ if (binding || !valueHandler) fail(token, 'event_value is only available in value change handlers');
+ emit(26, token, 0, 0, 1);
+ } else if (node.name === 'event_x' || node.name === 'event_y') {
+ if (binding || !pointHandler) fail(token, 'event_x and event_y are only available in pointer handlers');
+ emit(node.name === 'event_x' ? 22 : 23, token, 0, 0, 1);
+ } else if (node.name === 'dt') {
+ if (binding) fail(token, 'dt is only available in handlers');
+ emit(2, token, 0, 0, 1);
+ } else {
+ if (parameterPrefix && node.name.startsWith(parameterPrefix) && !parameters.has(node.name))
+ fail(token, `Unknown parameter field ${node.name}`);
+ if (parameters?.has(node.name)) {
+ emit(24, token, parameters.get(node.name), 0, 1);
+ return;
+ }
+ if (!states.has(node.name)) fail(token, `Unknown state ${node.name}`);
+ emit(1, token, states.get(node.name), 0, 1);
+ }
+ } else if (node.kind === 'unary') { expression(node.child, binding); emit(node.op, token); }
+ else if (node.kind === 'binary') {
+ expression(node.left, binding); expression(node.right, binding); emit(node.op, token, 0, 0, -1);
+ } else {
+ for (const arg of node.args) expression(arg, binding);
+ emit(node.op, token, 0, 0, 1 - node.args.length);
+ }
+ };
+ const statements = body => {
+ for (const statement of body) {
+ if (statement.kind === 'assign') {
+ if (parameterPrefix && statement.name.startsWith(parameterPrefix) && !parameters.has(statement.name))
+ fail(statement.token, `Unknown parameter field ${statement.name}`);
+ const parameter = parameters?.get(statement.name);
+ if (parameter === undefined && !states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`);
+ expression(statement.expression);
+ emit(parameter === undefined ? 20 : 25, statement.token, parameter === undefined ? states.get(statement.name) : parameter, 0, -1);
+ } else {
+ expression(statement.condition);
+ const branch = program.instructions.length;
+ emit(21, statement.token, 0, 0, -1);
+ statements(statement.body);
+ program.instructions[branch].arg = program.instructions.length;
+ }
+ }
+ };
+ const bodies = new Map();
+ function ruleBody(rule) {
+ if (bodies.has(rule.name.value)) return bodies.get(rule.name.value);
+ parameterPrefix = `${rule.parameter.value}.`;
+ parameters = new Map(records.get(rule.type.value).fields.map((field,index) => [`${rule.parameter.value}.${field.name.value}`,index]));
+ // A shared body may read coordinates; every binding is checked below.
+ pointHandler = true; valueHandler = true; stack = 0;
+ const start = program.instructions.length;
+ statements(rule.body);
+ const length = program.instructions.length - start;
+ const point = program.instructions.slice(start).some(op => op.op === 22 || op.op === 23);
+ parameters = null; parameterPrefix = null;
+ const value = program.instructions.slice(start).some(op => op.op === 26);
+ const body = {start,length,point,value}; bodies.set(rule.name.value,body); return body;
+ }
+ for (const declaration of declarations) {
+ if (declaration.kind === 'handler') {
+ let event = 0;
+ pointHandler = false; valueHandler = false;
+ if (declaration.event.value !== 'frame') {
+ if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`);
+ const index = inputs.get(declaration.event.value), edge = declaration.edge.value;
+ pointHandler = Boolean(program.inputs[index].target || program.inputs[index].routes);
+ valueHandler = program.inputs[index].kind === 'value';
+ if (valueHandler !== (edge === 'change')) fail(declaration.edge, 'Value inputs require change; change requires a value input');
+ if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input');
+ event = edge === 'change' ? 33 + index : edge === 'move' ? 17 + index : edge === 'cancel' ? 25 + index : 1 + index * 2 + (edge === 'up' ? 1 : 0);
+ }
+ if (declaration.rule) {
+ const rule = rules.get(declaration.rule.value);
+ const routed = declaration.argument.includes('.');
+ const input = program.inputs[inputs.get(declaration.event.value)];
+ if (routed && (!input?.routes || declaration.argument !== `${declaration.event.value}.target`))
+ fail(declaration.item, 'Use the current pointer group target');
+ const item = routed ? {type:input.type, count:records.get(input.type).fields.length} : items.get(declaration.item.value);
+ if (!rule) fail(declaration.rule, `Unknown rule ${declaration.rule.value}`);
+ if (!item) fail(declaration.item, `Unknown item ${declaration.item.value}`);
+ if (item.type !== rule.type.value) fail(declaration.item, `Rule ${rule.name.value} requires ${rule.type.value}`);
+ const pointer = pointHandler, value = valueHandler;
+ const body = ruleBody(rule);
+ if (body.value && !value) fail(declaration.rule, 'Value-reading rules require a value input');
+ if (body.point && !pointer) fail(declaration.rule, 'Coordinate-reading rules require a pointer input');
+ program.handlers.push({event,start:body.start,length:body.length,line:declaration.token.line,parameterBase:routed ? 255 : item.base,parameterCount:item.count});
+ } else {
+ const start = program.instructions.length;
+ stack = 0; statements(declaration.body);
+ program.handlers.push({ event, start, length: program.instructions.length - start, line: declaration.token.line });
+ }
+ } else if (declaration.kind === 'view') {
+ const view = { id: declaration.name.value, type: declaration.type, line: declaration.token.line, bindings: {} };
+ if (view.type === 'text') view.prefix = declaration.prefix;
+ else view.color = declaration.color;
+ declaration.expressions.forEach((node, index) => {
+ const start = program.instructions.length;
+ stack = 0; expression(node, true);
+ view.bindings[VIEW_FIELDS[view.type][index]] = { start, length: program.instructions.length - start };
+ });
+ program.views.push(view);
+ }
+ }
+ // Unused definitions still receive name/type checking, but reserve no VM body.
+ for (const rule of rules.values()) {
+ if (bodies.has(rule.name.value)) continue;
+ const instructions = program.instructions, previousMax = maxStack;
+ program.instructions = [];
+ ruleBody(rule);
+ program.instructions = instructions; maxStack = previousMax;
+ }
+ program.metrics = { sourceBytes, sourceLines: source === '' ? 0 : source.split('\n').length,
+ states: states.size, inputs: inputs.size, views: views.size,
+ handlers: program.handlers.length, instructions: program.instructions.length, maxStack };
+ return program;
+}
+
+export { LIMITS };
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm
new file mode 100644
index 0000000..79c50a8
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/core.wasm
Binary files differ
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs
new file mode 100644
index 0000000..861e0fc
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/project.mjs
@@ -0,0 +1,47 @@
+import { compile } from './compiler.mjs';
+
+// Two escaped source strings plus up to 32 generated item.field names. A long
+// schema field can appear in every item without repeating in the source text.
+export const MAX_PROJECT_BYTES = (2 * 6 + 32) * 32768 + 8192;
+const encoder = new TextEncoder();
+function sourceText(value, label) {
+ if (typeof value !== 'string' || encoder.encode(value).length > 32768)
+ throw new Error(`${label}: UTF-8 32 KiB 이하의 코드가 필요합니다.`);
+ return value;
+}
+
+// No runtime mutation. Validate every saved value before opening core staging.
+export function validateDocument(document) {
+ if (!document || typeof document !== 'object' || Array.isArray(document) ||
+ document.format !== 'pure-workbench' || document.version !== 2)
+ throw new Error('배치 파일 형식이 맞지 않습니다. pure-workbench version 2가 필요합니다.');
+ const source = sourceText(document.source, '적용된 코드');
+ const draft = sourceText(Object.hasOwn(document, 'draft') ? document.draft : source, '편집 중인 코드');
+ const program = compile(source);
+ if (!Array.isArray(document.states) || document.states.length !== program.states.length)
+ throw new Error('저장된 상태 목록이 적용된 코드와 일치하지 않습니다.');
+ const values = new Map();
+ for (const entry of document.states) {
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.name !== 'string' || values.has(entry.name))
+ throw new Error('상태 이름이 잘못됐거나 중복됐습니다.');
+ const value = entry.value === '-0' ? -0 : entry.value;
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('상태 값은 유한한 숫자여야 합니다.');
+ values.set(entry.name, value);
+ }
+ for (const state of program.states)
+ if (!values.has(state.name)) throw new Error(`저장된 상태가 없습니다: ${state.name}`);
+ return { source, draft, program, values: program.states.map(state => values.get(state.name)) };
+}
+
+export function parseProject(text) {
+ if (typeof text !== 'string' || encoder.encode(text).length > MAX_PROJECT_BYTES)
+ throw new Error('작업 파일이 너무 큽니다.');
+ let project;
+ try { project = JSON.parse(text); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); }
+ if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench')
+ throw new Error('Pure 작업물 형식이 아닙니다.');
+ if (project.version === 1) sourceText(project.source, '코드');
+ else if (project.version === 2) validateDocument(project);
+ else throw new Error('지원하지 않는 작업 파일 버전입니다.');
+ return project;
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs
new file mode 100644
index 0000000..d8d6618
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/sources/prototypes/workbench/web/runtime.mjs
@@ -0,0 +1,350 @@
+import { compile } from './compiler.mjs';
+import { validateDocument } from './project.mjs';
+
+const messages = [
+ '', '실행기의 용량 한도를 넘었습니다.', '실행 정의가 유효하지 않습니다.',
+ '계산 스택의 범위를 벗어났습니다.', '0으로 나눌 수 없습니다.',
+ '계산 결과가 유한한 수의 범위를 벗어났습니다.', '조건 분기 범위가 유효하지 않습니다.',
+ '존재하지 않는 상태를 참조했습니다.', '시간 간격이 유효하지 않습니다.',
+ '정의되지 않은 입력 사건입니다.',
+];
+const sourceLimit = 32768;
+
+function problem(message, line = null, column = null) {
+ return Object.assign(new Error(message), { line, column });
+}
+
+function freezeProgram(value) {
+ if (value && typeof value === 'object') {
+ for (const child of Object.values(value)) freezeProgram(child);
+ Object.freeze(value);
+ }
+ return value;
+}
+
+export async function createRuntime(wasmBytes, evaluationOptions = {}) {
+ if (wasmBytes === undefined) {
+ const response = await fetch(new URL('./core.wasm', import.meta.url), { cache: 'no-store' });
+ if (!response.ok) throw new Error(`실행기를 불러오지 못했습니다 (HTTP ${response.status}).`);
+ wasmBytes = await response.arrayBuffer();
+ }
+ const module = await WebAssembly.compile(wasmBytes);
+ if (WebAssembly.Module.imports(module).length) throw new Error('이 실행기는 호스트 함수 없이 동작해야 합니다.');
+ const { exports: core } = await WebAssembly.instantiate(module, {});
+ if (typeof core.eval_bounded !== 'function' || typeof core.abort_program !== 'function' || typeof core.dispatch_point !== 'function' || typeof core.put_restored_value !== 'function' || typeof core.commit_restored_program !== 'function' || typeof core.bind_handler !== 'function' || typeof core.put_pointer_target !== 'function' || typeof core.bind_target_handler !== 'function' || typeof core.dispatch_target !== 'function' || typeof core.dispatch_value !== 'function')
+ throw new Error('실행기 버전이 맞지 않습니다. 새로고침하거나 core.wasm을 다시 빌드하세요.');
+ core.reset_all();
+ let program = null;
+ let appliedSource = '';
+ let lastEvent = null;
+ // Keep a failed event visible through its release/focus-loss cleanup.
+ // The active program can clear this diagnostic by apply or explicit reset.
+ let lastFailure = null;
+ const held = new Map();
+ const points = new Map();
+ let presenting = false;
+ let bindings = [];
+ let pointRoutes = new Map();
+ // One bounded publication buffer, never exposed to callers. No state mirror.
+ let displayValues = null, displayValid = null;
+ function available() {
+ if (presenting) throw new Error('출력 전달 중에는 실행기를 다시 호출할 수 없습니다.');
+ }
+ let evaluationPolicy = { cacheEntries: 0, historyEntries: 8, workLimit: 2048 };
+
+ function checked(code, line = null) {
+ if (code !== 0) throw problem(messages[code] || `실행기 오류 ${code}`, line);
+ }
+ function inputIndex(name) {
+ const index = program?.inputs.findIndex((input) => input.name === name) ?? -1;
+ if (index < 0) throw new Error(`정의되지 않은 입력: ${name}`);
+ return index;
+ }
+ function dispatch(event, name, phase, source, point = null, value = undefined) {
+ const status = value !== undefined ? core.dispatch_value(event, value) : point ? (point.route === undefined ? core.dispatch_point(event, point.x, point.y)
+ : core.dispatch_target(event, point.route, point.x, point.y)) : core.dispatch(event, event === 0 ? 1 / 60 : 0);
+ const pc = core.info(2) >>> 0;
+ lastEvent = { name, phase, source, status,
+ line: program?.instructions[pc]?.line ?? null, ops: core.info(3) >>> 0 };
+ if (point) { lastEvent.x = point.x; lastEvent.y = point.y; if (point.target) lastEvent.target = point.target; }
+ if (value !== undefined) lastEvent.value = value;
+ if (status) lastFailure = { ...lastEvent };
+ return status;
+ }
+
+ function evaluateDisplay() {
+ core.begin_evaluation(evaluationPolicy.workLimit);
+ if (!bindings.length) return null;
+ displayValues ??= new Float64Array(96);
+ displayValid ??= new Uint8Array(24);
+ let error = null, slot = 0;
+ for (let i = 0; i < bindings.length; i++) {
+ const view = program.views[i];
+ displayValid[i] = 1;
+ for (const [name, binding] of bindings[i]) {
+ const value = core.eval_bounded(binding.start, binding.length);
+ if (core.evaluation_info(0) === 10) {
+ displayValid.fill(0);
+ return { code: 101, line: view.line,
+ message: '표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.' };
+ }
+ if (!Number.isFinite(value) || ((name === 'width' || name === 'height' || name === 'radius') && value < 0)) {
+ error ??= { line: view.line, message: `${view.id}.${name}: 표시할 수 없는 값입니다. 0 나눗셈이나 음수 크기를 확인하세요.` };
+ displayValid[i] = 0;
+ }
+ displayValues[slot++] = value;
+ }
+ }
+ return error;
+ }
+
+ function status(viewError) {
+ const executionError = lastFailure?.status || 0;
+ return {
+ tick: core.info(0) >>> 0, revision: core.info(4) >>> 0,
+ displayError: viewError ? { code: viewError.code || 100, ...viewError } : null,
+ lastError: executionError || (viewError ? viewError.code || 100 : 0),
+ errorMessage: executionError ? messages[executionError] || `실행 오류 ${executionError}` : viewError?.message || '',
+ errorLine: executionError ? lastFailure.line : viewError?.line ?? null,
+ lastOps: core.info(3) >>> 0, successfulEvents: core.info(9) >>> 0,
+ held: [...held.keys()], lastEvent: lastEvent ? { ...lastEvent } : null,
+ lastFailure: lastFailure ? { ...lastFailure } : null,
+ memory: {
+ linearBytes: core.memory.buffer.byteLength,
+ reservedEngineBytes: core.layout(0), programReservedBytes: core.layout(1),
+ instructionBytes: core.layout(2), handlerBytes: core.layout(3),
+ activeIRBytes: core.layout(4), activeStateBytes: core.layout(5),
+ stagingReservedBytes: core.layout(6), evalStackBytes: core.layout(7),
+ pointerTargetBytes: core.layout(8), pointerTargets: core.layout(9),
+ },
+ };
+ }
+
+ function load(next, source, restored = null) {
+ const oldIds = new Map(restored ? [] : program?.states.map((state, index) => [state.name, index]) ?? []);
+ checked(core.begin_program(next.states.length, next.inputs.length));
+ try {
+ for (let index = 0; index < next.states.length; index++) {
+ const state = next.states[index];
+ checked(core.put_state(index, state.initial, oldIds.get(state.name) ?? -1), state.line);
+ if (restored) checked(core.put_restored_value(index, restored[index]), state.line);
+ }
+ for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line);
+ for (const target of next.pointerTargets) checked(core.put_pointer_target(target.input, target.base, target.count));
+ for (const [index, handler] of next.handlers.entries()) {
+ checked(core.put_handler(handler.event, handler.start, handler.length), handler.line);
+ if (handler.parameterCount)
+ checked(handler.parameterBase === 255 ? core.bind_target_handler(index, handler.parameterCount)
+ : core.bind_handler(index, handler.parameterBase, handler.parameterCount), handler.line);
+ }
+ checked(restored ? core.commit_restored_program() : core.commit_program());
+ } catch (error) {
+ core.abort_program();
+ throw error;
+ }
+ pointRoutes = new Map(next.inputs.flatMap(input => (input.routes ?? []).map(route => [route.target, route])));
+ program = next;
+ bindings = next.views.map(view => Object.entries(view.bindings));
+ appliedSource = source;
+ lastEvent = null;
+ lastFailure = null;
+ }
+
+ const api = {
+ get currentProgram() { return program; },
+ configureEvaluation({ cacheEntries = evaluationPolicy.cacheEntries,
+ historyEntries = evaluationPolicy.historyEntries, workLimit = evaluationPolicy.workLimit } = {}) {
+ available();
+ for (const [name, value, maximum] of [['cacheEntries', cacheEntries, 32],
+ ['historyEntries', historyEntries, 8], ['workLimit', workLimit, 0xffffffff]]) {
+ if (!Number.isInteger(value) || value < 0 || value > maximum)
+ throw new Error(`${name}: 0~${maximum} 사이의 정수여야 합니다.`);
+ }
+ checked(core.configure_evaluation(cacheEntries, historyEntries));
+ evaluationPolicy = { cacheEntries, historyEntries, workLimit };
+ return { ...evaluationPolicy };
+ },
+ forgetEvaluation(part = 'all') {
+ available();
+ const flags = part === 'all' ? 3 : part === 'values' ? 1 : part === 'history' ? 2 : undefined;
+ if (flags === undefined) throw new Error('삭제할 기록은 values, history, all 중 하나여야 합니다.');
+ core.forget_evaluation(flags);
+ },
+ evaluationStats() {
+ return { ...evaluationPolicy, work: core.evaluation_info(1),
+ instructions: core.evaluation_info(2), cacheHits: core.evaluation_info(3),
+ reservedBytes: core.evaluation_info(6), dedicatedBytes: core.evaluation_info(7),
+ sharedBytes: core.evaluation_info(8), reuseAvailable: Boolean(core.evaluation_info(9)),
+ workspaceTagBytes: core.evaluation_info(10) };
+ },
+ apply(source) {
+ available();
+ if (typeof source !== 'string' || new TextEncoder().encode(source).length > sourceLimit) {
+ throw problem('정의는 UTF-8 기준 32,768바이트 이하의 텍스트여야 합니다.');
+ }
+ if (held.size) throw problem('누르고 있는 입력을 놓은 뒤 적용하세요. 현재 작업은 그대로 유지합니다.');
+ const next = freezeProgram(compile(source));
+ load(next, source);
+ return next;
+ },
+ restoreDocument(document) {
+ available();
+ if (held.size) throw problem('누르고 있는 입력을 놓은 뒤 복원하세요.');
+ const next = validateDocument(document);
+ load(freezeProgram(next.program), next.source, next.values);
+ return { source: next.source, draft: next.draft };
+ },
+ exportDocument(draft = appliedSource) {
+ available();
+ if (!program) throw problem('먼저 코드를 적용하세요.');
+ if (held.size) throw problem('조작을 끝낸 뒤 배치를 저장하세요.');
+ if (lastFailure) throw problem('실행 오류를 해결한 뒤 배치를 저장하세요.');
+ if (typeof draft !== 'string' || new TextEncoder().encode(draft).length > sourceLimit)
+ throw problem('편집 중인 코드는 UTF-8 32 KiB 이하여야 합니다.');
+ return { format: 'pure-workbench', version: 2, source: appliedSource, draft,
+ states: program.states.map((state, i) => {
+ const value = core.get_state(i);
+ return { name: state.name, value: Object.is(value, -0) ? '-0' : value };
+ }) };
+ },
+ reset() {
+ available();
+ held.clear();
+ points.clear();
+ core.reset_state();
+ lastEvent = null;
+ lastFailure = null;
+ },
+ frame(count = 1) {
+ available();
+ if (!Number.isInteger(count) || count < 0 || count > 600) throw new Error('프레임 수는 0~600 사이의 정수여야 합니다.');
+ if (!program) return 0;
+ for (let i = 0; i < count; i++) {
+ const code = dispatch(0, 'frame', 'frame', 'clock');
+ if (code !== 0) return code;
+ }
+ return 0;
+ },
+ down(name, source = 'external') {
+ available();
+ const index = inputIndex(name);
+ if (program.inputs[index].kind === 'value') throw new Error('숫자 입력은 change(name, value)를 사용하세요.');
+ if (program.inputs[index].target || program.inputs[index].routes) throw new Error('좌표 입력은 point(name, phase, x, y)를 사용하세요.');
+ const sources = held.get(name) || new Set();
+ if (sources.has(source)) return 0;
+ const wasDown = sources.size > 0;
+ sources.add(source);
+ held.set(name, sources);
+ return wasDown ? 0 : dispatch(1 + index * 2, name, 'down', source);
+ },
+ up(name, source = 'external') {
+ available();
+ const index = inputIndex(name);
+ if (program.inputs[index].kind === 'value') throw new Error('숫자 입력은 change(name, value)를 사용하세요.');
+ if (program.inputs[index].target || program.inputs[index].routes) throw new Error('좌표 입력은 point(name, phase, x, y)를 사용하세요.');
+ const sources = held.get(name);
+ if (!sources?.delete(source)) return 0;
+ if (sources.size) return 0;
+ held.delete(name);
+ return dispatch(2 + index * 2, name, 'up', source);
+ },
+ change(name, value, source = 'external') {
+ available();
+ const index = inputIndex(name);
+ if (program.inputs[index].kind !== 'value') throw new Error('숫자 값에 연결된 입력이 아닙니다.');
+ if (!Number.isFinite(value)) throw new Error('입력 값은 유한한 숫자여야 합니다.');
+ return dispatch(33 + index, name, 'change', source, null, value);
+ },
+ point(name, phase, x, y, source = 'external', target = undefined) {
+ available();
+ const index = inputIndex(name);
+ const input = program.inputs[index];
+ if (!input.target && !input.routes) throw new Error('포인터에 연결된 입력이 아닙니다.');
+ const route = input.routes ? pointRoutes.get(target) : undefined;
+ if (input.routes && (!route || program.pointerTargets[route.route].input !== index)) throw new Error('포인터 그룹에 연결된 도형 이름이 필요합니다.');
+ if (!input.routes && target !== undefined) throw new Error('단일 포인터 입력에는 대상 인자를 지정하지 않습니다.');
+ const owner = route ? `${name}:${target}` : name;
+ if (!['down', 'move', 'up', 'cancel'].includes(phase)) throw new Error('포인터 단계는 down, move, up, cancel 중 하나여야 합니다.');
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error('포인터 좌표는 유한한 숫자여야 합니다.');
+ if (typeof source !== 'string' || source.length === 0) throw new Error('포인터 출처 이름이 필요합니다.');
+ let point = points.get(owner);
+ if (phase === 'down') {
+ if (point?.source === source) return 0;
+ if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.');
+ point = {name, source, x, y, target, route:route?.route};
+ points.set(owner, point);
+ held.set(owner, new Set([source]));
+ } else {
+ if (!point || point.source !== source) return 0;
+ point.x = x; point.y = y;
+ if (phase === 'up' || phase === 'cancel') { points.delete(owner); held.delete(owner); }
+ }
+ const event = phase === 'move' ? 17 + index : phase === 'cancel' ? 25 + index : 1 + index * 2 + (phase === 'up' ? 1 : 0);
+ return dispatch(event, name, phase, source, point);
+ },
+ releaseAll() {
+ available();
+ let firstError = 0;
+ for (const point of [...points.values()]) {
+ const status = api.point(point.name, 'cancel', point.x, point.y, point.source, point.target);
+ if (!firstError) firstError = status;
+ }
+ for (const [name, sources] of [...held]) {
+ for (const source of [...sources]) {
+ const status = api.up(name, source);
+ if (!firstError) firstError = status;
+ }
+ }
+ return firstError;
+ },
+ exportProject() {
+ return { format: 'pure-workbench', version: 1, source: appliedSource };
+ },
+ // Synchronous trusted output adapter. Callbacks run only after the whole
+ // display has been evaluated. Runtime mutation/reentrant reads are refused.
+ // Exceptions from an adapter propagate; the next call republishes all values.
+ present(target) {
+ available();
+ presenting = true;
+ try {
+ const { begin, state, view, value, end } = target;
+ for (const callback of [begin, state, view, value, end])
+ if (typeof callback !== 'function') throw new TypeError('출력 어댑터 함수가 필요합니다.');
+ const error = evaluateDisplay();
+ begin.call(target, program);
+ for (let i = 0; i < (program?.states.length ?? 0); i++) state.call(target, i, core.get_state(i));
+ let slot = 0, visible = 0;
+ for (let i = 0; i < bindings.length; i++) {
+ const valid = Boolean(displayValid[i]);
+ view.call(target, i, valid);
+ for (const [name] of bindings[i]) {
+ if (valid) value.call(target, i, name, displayValues[slot]);
+ slot++;
+ }
+ if (valid) visible++;
+ }
+ end.call(target, visible);
+ return status(error);
+ } finally { presenting = false; }
+ },
+ snapshot() {
+ available();
+ const error = evaluateDisplay();
+ const states = Object.fromEntries((program?.states ?? []).map((state, index) => [state.name, core.get_state(index)]));
+ const views = [];
+ let slot = 0;
+ for (let i = 0; i < bindings.length; i++) {
+ const view = program.views[i];
+ const rendered = displayValid[i] ? { id: view.id, type: view.type, color: view.color, prefix: view.prefix } : null;
+ for (const [name] of bindings[i]) {
+ if (rendered) rendered[name] = displayValues[slot];
+ slot++;
+ }
+ if (rendered) views.push(rendered);
+ }
+ return { states, views, ...status(error) };
+ },
+ };
+ api.configureEvaluation(evaluationOptions);
+ return Object.freeze(api);
+}
diff --git a/prototypes/connections/results/2026-09-23-native-connections/verification.json b/prototypes/connections/results/2026-09-23-native-connections/verification.json
new file mode 100644
index 0000000..3a8b2cc
--- /dev/null
+++ b/prototypes/connections/results/2026-09-23-native-connections/verification.json
@@ -0,0 +1,29 @@
+{
+ "scope": "Correctness, lifetimes, admission bounds; no throughput, latency, or RSS comparison",
+ "compiler": "cc (Debian 14.2.0-19) 14.2.0",
+ "native_command": "make check EVIDENCE=results/2026-09-23-native-connections/native.json",
+ "sanitized_command": "make check BUILD=build/sanitized CFLAGS='-std=c17 -O1 -g -Wall -Wextra -Wpedantic -Werror -fsanitize=address,undefined -fno-omit-frame-pointer' EVIDENCE=results/2026-09-23-native-connections/sanitized.json",
+ "core_checks": {
+ "native": "passed",
+ "asan_ubsan": "passed",
+ "leak_detection_disabled": false
+ },
+ "integration_scenarios_each": 15,
+ "cli_regression": {
+ "command": "node --test cli.test.mjs",
+ "cwd": "prototypes/workbench",
+ "passed": 8,
+ "failed": 0,
+ "test_sha256": "7e4688824e26dc7e56396f9d4d62726de0ccb1490e1a388bb64690ae2aae52f4"
+ },
+ "source_hashes_equal": true,
+ "source_snapshots": "sources/",
+ "final_binaries_match_records": true,
+ "limitations": [
+ "SQLite shared library was not rebuilt with sanitizers",
+ "Linux AArch64 only; no macOS execution",
+ "Bounded payload retention excludes SQLite internals, OS socket buffers and client allocations",
+ "SQLite operations remain synchronous; absolute deadlines are enforced between those operations",
+ "Native connection path is fixed in C; Pure-authored external effects are not implemented"
+ ]
+}