코딩 LLM이 원하는 프로그램을 spec.json(순수 JSON, 호스트 코드 0)으로 작성하기 위한 규격.
플랫폼 빌트인 op만으로 완결 — 샌드박스·커스텀 JS 없이 상태머신을 선언한다.
본 가이드는 현재 런타임(
platform/engine.mjs,ui/components.mjs)의 실제 동작에 맞춤. 예시는 그대로spec.json으로 저장 가능(JSON 유효).
변형 Moore 머신. 프로그램 = 상태들의 그래프.
- 상태(state) =
{ui?, enter?, on}.ui: 이 상태에서 렌더할 컴포넌트. 생략 시 transient(렌더 안 함 → 직전 화면 유지, no-flicker).enter: 입장 액션 = op(빌트인 함수 1회 호출).→ {event, envops}반환.on: 전이 테이블{이벤트: 도착상태}. 이 상태에서 처리할 이벤트.
- op 계약:
enter호출 →{event:문자열, envops:[[op,...],...]}.envops= context 에 대한 선언적 쓰기(set/del/incr/decr/not).event= 다음 전이 구동(on으로 분기). event 없으면 정지(정류 상태).
- 체인: 하나의 외부 이벤트가
enter → event → 전이 → enter → …자동 연쇄(정류 상태까지). - context:
{키:값}객체.- persistent(기본): 체인 후에도 유지, 스냅샷에 포함.
- request(
#접두 키): 체인 종료 후 자동 제거, 스냅샷 제외. 이벤트 payload 주입용.
- 제어 = 전이 테이블: 분기(
$ifeq등의 결과 event 로 갈래), 시퀀스/루프(상태 그래프). op 는 부수효과(envops)만.
핵심: 로직은 op(순수 계산 + envops) + 전이 테이블(제어) 의 조합. 호스트 코드 불필요.
{
"id": "unique-id",
"version": 1,
"label": "표시 이름",
"initial": "IDLE",
"context": { "count": 0 },
"states": { "IDLE": { ... }, ... }
}| 필드 | 필수 | 설명 |
|---|---|---|
id |
✓ | 스펙 고유 식별자(영속성 키). |
version |
✓ | 스펙 버전(정수). 바뀌면 스냅샷 무효(재초기화). |
label |
드롭다운 표시용. | |
initial |
✓ | 진입 상태(states 의 키). |
context |
✓ | 초기 context. |
states |
✓ | 상태 맵(키=상태명). |
"STATE_NAME": {
"ui": { "columns": 12, "gap": "12px", "components": [ ... ] },
"enter": { "fn": "$add", "params": { "a": "{count}", "b": "1", "to": "count" } },
"on": { "INC": "INC_OP", "DEC": "DEC_OP" }
}ui생략 → transient(계산만 하는 중간 상태).enter생략 → 정류(입장 액션 없음, 보통 결과 표시 상태).on에 없는 이벤트 수신 → drop(무시). 린터(Rule A)가 반환 이벤트는 미리 검증.
ui.components 배열. 각 원소: { "component": "종류", "grid": "1 / span 12", "props": {...}, "event": {...} }.
grid: CSSgrid-column값(예:"1 / span 12"전체 너비,"1 / span 4"1/3).
| component | props | event | 비고 |
|---|---|---|---|
Card |
title, desc |
— | 정적 카드. |
Metric |
label, key, unit? |
— | ctx[key] 큰 글씨 표시 + unit. |
ProgressBar |
progressKey, color? |
— | ctx[progressKey] % 막대. |
Button |
label, variant:"primary"|"secondary" |
{type, payload?} |
클릭 시 이벤트 발생. |
TextInput |
label, placeholder, bindingKey? |
{type} |
bindingKey ctx 양방향 바인딩; Enter 시 이벤트. |
Button.event.payload: 객체. 각 키가 context 의
#키로 주입(체인 후 제거). 예:{"type":"KEY","payload":{"kind":"digit","v":"7"}}→#kind,#v.
| 발생원 | 형태 | 비고 |
|---|---|---|
| 사용자 | Button 클릭 / TextInput Enter | send({type, payload?}). payload → #키. |
| 타이머 | on 에 TICK_1000MS / TICK_5S 등 |
해당 주기마다 자동. 최소 간격 1개만 구독(demand clock). |
| BOOT | 호스트가 start() 직후 1회 send({type:'BOOT'}) |
부팅 작업 필요 시 on:{BOOT:...}. 미사용 시 drop(비침투적). |
| 반환 | enter op 의 결과 event |
전이 테이블로 분기. |
"enter": { "fn": "$빌트인명", "params": { ... } }params값은 문자열.expand치환:{key}→ctx[key](없으면"").{#key}→ request 키.- 정규식 수량자
{n,m}/{n}치환 안 함(리터럴 보존) —$regex패턴 안전.
- 반환 =
{event, envops}. event 는on에 있어야(없으면 정지). envops 로 context 갱신.
모두
{event, envops}반환.event는 전이 구동,envops는 context 쓰기.events열 = 린터가 검증하는 반환 가능 이벤트(Rule A).
| fn | params | events | 동작 |
|---|---|---|---|
$pass |
{event} |
동적(event 값) |
주어진 event 그대로 반환(분기/시퀀스용). |
$ifeq |
{a,b} |
true,false |
a===b(문자열 비교). |
$ifne |
{a,b} |
true,false |
a!==b. |
$iflt $ifgt $iflte $ifgte |
{a,b} |
true,false,error |
수치 비교(비숫자→error). |
$set |
{key,value} |
ok |
ctx[key]=value. |
$log |
{...} |
ok |
console.log(디버그). |
$add $sub $mul |
{a,b,to} |
ok,error |
ctx[to]=a±*/b(비숫자→error). |
$div |
{a,b,to} |
ok,error |
ctx[to]=a/b; b=0→error(div_zero). |
$concat |
{a,b,to} |
ok |
ctx[to]=String(a)+String(b). |
$http_get |
{url,to} |
ok,error |
async fetch; ok→ctx[to]=body, error→ctx.__error(HTTP/네트워크). |
$regex |
{from,pattern,to,group?,flags?} |
ok,error |
from에서 정규식 매칭→캡처(group 기본 1, 0=전체 매치)→ctx[to]; no match/잘못된 정규식→error. |
$http_get 은 async(엔진이 thenable await). CORS 허용 엔드포인트 필요(브라우저 fetch).
$regex 의 pattern 은 JSON 문자열 — 역슬래시는 \\ 로(예: "(\\d+\\.\\d+\\.\\d+\\.\\d+)").
빌트인이 알아서 구성. (커스텀/원격 fn 에서 직접 쓸 때 참고.)
| 형태 | 동작 |
|---|---|
["set", key, val] |
ctx[key]=val |
["del", key] |
delete ctx[key] |
["incr", key] |
ctx[key] += 1 |
["decr", key] |
ctx[key] -= 1 |
["not", key] |
ctx[key] = !ctx[key] |
spec.json 도 최상위 functions 필드로 fn 선언 가능(엔진이 spec.functions 를 읽는다):
"functions": {
"$dbl": { "code": "function (p) { return { event: 'ok', envops: [['set', p.to, Number(p.v) * 2]] }; }", "spec": { "events": ["ok"] } },
"$myip": { "remote": "https://host/fn", "spec": { "events": ["ok", "error"] } }
}| 형태 | 필드 | 동작 |
|---|---|---|
| 커스텀 | {code, spec} |
code = 전체 함수 소스(문자열). 엔진이 new Function('return ('+code+')')() 로 평가 → (params) => {event,envops}. 호스트 스레드(신뢰 spec). |
| 원격 | {remote, spec} |
엔진이 POST params(JSON) → 응답 {event,envops} JSON 수신(로컬 fn 과 동일 계약). |
| (.mjs 전용) | {fn, spec} |
실제 함수 객체. JSON 불가. |
spec.events선언 필수(린터 Rule A 검증용).code는 신뢰할 수 있는 spec(작성자=사용자/LLM)에서만 — 호스트에서 평가(eval). 신뢰 불가하면 원격 fn(서버 측) 사용.- 빌트인 op 체인으로 풀 수 있으면
functions없이 순수 빌트인이 가장 깔끔(정적 분석 용이). - 엔진은
spec.functions(JSON) + 호스트opts.functions(.mjs) 병합. 순수 JSON 스펙은spec.functions만으로 자급식. - JSON 스펙으로 위 기능이 필요하면 → 빌트인 op 체인으로 풀 수 있는지 먼저 검토(대부분 가능).
- Rule A: op 상태(
enter있는 상태)의 fn 이 반환 가능한 모든 event 가on에 있어야.- 빌트인 events = 고정(BUILTIN_SPECS). 커스텀/원격 =
spec.events선언. - 예:
$ifgte→on에true,false,error세 갈래 모두 필요(미처리 event → 에러).
- 빌트인 events = 고정(BUILTIN_SPECS). 커스텀/원격 =
- Rule B: 모든 상태는
initial에서 도달 가능해야(dead state 금지).
- 체인 끝(최종 정류 상태) 에서 스냅샷 1회 저장. transient 상태(중간)는 스냅샷 미포함 → 빈 화면/하이드레이션 이슈 없음.
#request 키는 스냅샷 제외(재부팅 시 transient 안 남음).- 리셋: 현재 spec 스냅샷 + KV 삭제 → 재부팅(
initial+ 초기context).
{
"id": "mini-counter",
"version": 1,
"label": "🔢 Counter",
"initial": "IDLE",
"context": { "count": 0 },
"states": {
"IDLE": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Metric", "grid": "1 / span 12", "props": { "label": "COUNT", "key": "count" } },
{ "component": "Button", "grid": "1 / span 6", "props": { "label": "+", "variant": "primary" }, "event": { "type": "INC" } },
{ "component": "Button", "grid": "7 / span 6", "props": { "label": "−", "variant": "secondary" }, "event": { "type": "DEC" } }
] },
"on": { "INC": "INC_OP", "DEC": "DEC_OP" }
},
"INC_OP": { "enter": { "fn": "$add", "params": { "a": "{count}", "b": "1", "to": "count" } }, "on": { "ok": "IDLE", "error": "IDLE" } },
"DEC_OP": { "enter": { "fn": "$sub", "params": { "a": "{count}", "b": "1", "to": "count" } }, "on": { "ok": "IDLE", "error": "IDLE" } }
}
}
INC_OP/DEC_OP는 ui 없음(transient) → 계산 후IDLE재렌더(숫자 갱신).$add/$sub는ok/error반환 →error도on에 있어야(Rule A). 여기선 계산 실패 시IDLE복귀(카운트 유지).
{
"id": "myip",
"version": 1,
"label": "🌐 My IP",
"initial": "IDLE",
"context": { "ip": "…", "__error": "", "raw": "" },
"states": {
"IDLE": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Card", "grid": "1 / span 12", "props": { "title": "My Public IPv4", "desc": "버튼 클릭" } },
{ "component": "Button", "grid": "1 / span 12", "props": { "label": "Get IPv4", "variant": "primary" }, "event": { "type": "FETCH" } }
] },
"on": { "FETCH": "FETCH" }
},
"FETCH": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Card", "grid": "1 / span 12", "props": { "title": "조회 중…", "desc": "" } }
] },
"enter": { "fn": "$http_get", "params": { "url": "https://ipv4.icanhazip.com", "to": "raw" } },
"on": { "ok": "PARSE", "error": "ERROR" }
},
"PARSE": {
"enter": { "fn": "$regex", "params": { "from": "{raw}", "pattern": "(\\d+\\.\\d+\\.\\d+\\.\\d+)", "to": "ip" } },
"on": { "ok": "SHOW", "error": "ERROR" }
},
"SHOW": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Metric", "grid": "1 / span 12", "props": { "label": "IPv4", "key": "ip" } },
{ "component": "Button", "grid": "1 / span 12", "props": { "label": "다시 조회", "variant": "secondary" }, "event": { "type": "FETCH" } }
] },
"on": { "FETCH": "FETCH" }
},
"ERROR": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Metric", "grid": "1 / span 12", "props": { "label": "error", "key": "__error" } },
{ "component": "Button", "grid": "1 / span 12", "props": { "label": "재시도", "variant": "primary" }, "event": { "type": "FETCH" } }
] },
"on": { "FETCH": "FETCH" }
}
}
}
FETCH(async) →PARSE(정규식 추출/검증) →SHOW. CORS 허용 + IPv4-only 엔드포인트 사용.
{
"id": "greet",
"version": 1,
"label": "👋 Greet",
"initial": "IDLE",
"context": { "name": "", "greeting": "" },
"states": {
"IDLE": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "TextInput", "grid": "1 / span 12", "props": { "label": "이름", "placeholder": "이름 입력 후 Enter", "bindingKey": "name" }, "event": { "type": "SAY" } },
{ "component": "Card", "grid": "1 / span 12", "props": { "title": "Greet", "desc": "이름을 넣고 Enter" } }
] },
"on": { "SAY": "CHECK" }
},
"CHECK": { "enter": { "fn": "$ifeq", "params": { "a": "{name}", "b": "" } }, "on": { "true": "EMPTY", "false": "BUILD" } },
"EMPTY": {
"enter": { "fn": "$set", "params": { "key": "greeting", "value": "이름을 입력하세요" } },
"on": { "ok": "IDLE" }
},
"BUILD": {
"enter": { "fn": "$concat", "params": { "a": "Hello, ", "b": "{name}", "to": "greeting" } },
"on": { "ok": "SHOW" }
},
"SHOW": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Metric", "grid": "1 / span 12", "props": { "label": "Greeting", "key": "greeting" } },
{ "component": "Button", "grid": "1 / span 12", "props": { "label": "다시", "variant": "secondary" }, "event": { "type": "RESET" } }
] },
"on": { "RESET": "IDLE" }
}
}
}
CHECK($ifeq) → 빈 이름이면EMPTY($set경고문 → IDLE), 아니면BUILD($concat→ SHOW). 분기 = 전이 테이블.
{
"id": "<unique-id>",
"version": 1,
"label": "<표시명>",
"initial": "<초기상태>",
"context": { "<키>": "<초기값>" },
"states": {
"<초기상태>": {
"ui": { "columns": 12, "gap": "12px", "components": [
{ "component": "Button", "grid": "1 / span 12", "props": { "label": "Go", "variant": "primary" }, "event": { "type": "DO" } }
] },
"on": { "DO": "<NEXT>" }
},
"<NEXT>": {
"enter": { "fn": "<$빌트인>", "params": { "<p>": "<값 또는 {키}>" } },
"on": { "<event>": "<TARGET>" }
}
}
}-
id고유,initial은states의 키. - Rule A: 모든
enterfn 의 반환 event 가on에 존재.$ifgt계열 →true/false/error3개 모두on에.$http_get/$regex→ok/error2개.$add/$set/$concat→ok.
- Rule B: 모든 상태가
initial에서 도달 가능(사용자/타이머/BOOT/반환 이벤트 경로). -
params값은 문자열;{key}/{#key}치환.$regex수량자{n,m}안전. - 계산 상태는
ui생략(transient); 결과 표시 상태는ui+enter생략(정류). - Button 이벤트:
{"type":"...","payload":{...}}. payload →#키. - 타이머:
on에TICK_<n>MS/TICK_<n>S. 부팅 작업:on:{BOOT:...}. -
$http_get은 CORS 허용 URL; IPv4 강제 필요 시 IPv4-only 호스트(ipv4.icanhazip.com). - 커스텀/원격 fn 필요 시
spec.functions선언(code/remote, JSON 가능). 빌트인 체인으로 풀리면 가급적 순수 빌트인. - 작성 후 린트 통과 확인(Rule A/B).
- op = 순수: fn 은
(params) → {event, envops}. context 직접 안 쓰고 envops 로. - 제어 = 전이 테이블: 분기/시퀀스/루프는 상태 그래프로.
- 부수효과 = envops: 선언적 쓰기(set/del/incr/decr/not).
- 읽기 = param expand:
{key}치환. - 외부 I/O = 빌트인:
$http_get(fetch),$regex(추출). 커스텀/원격은 .mjs. - 호스트 코드 0: spec.json 은 순수 선언 → 샌드박스 불필요, 정적 검증 가능.