런타임 계약
Lute는 완전하고 부작용이 없는 컴파일러입니다. lute compile <file>은
.lute 문서를 검사하고 JSON 아티팩트로 낮춘 다음 멈춥니다. CEL을 실행하지 않고,
Datalog 고정점을 계산하지 않으며, 팩트 저장소를 유지하지 않고, 브리지를 호출하지도 않습니다. 모든
동작은 아티팩트의 반대편에 있는 엔진에 있습니다. 이 페이지는 축약한 런타임 계약이며, 전체적인
소스 기반 규격은
docs/runtime/에 있습니다.
lute.engine.yaml 매트릭스 형식은
0.33.0.md §4에
schemas/lute-ir-0.53.schema.json
(JSON Schema draft 2020-12)입니다.
런타임 이벤트 계약과 lute-runtime
섹션 제목: “런타임 이벤트 계약과 lute-runtime”재개 가능한 런타임 API와 JSON 와이어 형식은
런타임 이벤트 계약을
참고하세요. lute-runtime 크레이트는 순수한 통합 표면을 노출합니다. Runtime::load로
compile --project 번들을 로드하고, begin으로 세션을 만들고, step으로 진행하며,
snapshot/restore와 읽기 전용 질의를 사용해 호스트가 검사할 수 있습니다. 렌더링, 페이싱,
브리지 호출, 지급 확정은 호스트가 소유하고, 런타임은 그 효과를 데이터로 반환합니다.
TypeScript 런타임
섹션 제목: “TypeScript 런타임”TypeScript 호스트는 wasm 런타임 위의 Effect 4 API인
@lute-lang/runtime을 사용할 수 있습니다. Bun 및 브라우저 레이어는 lute compile --project 번들을 로드하고, 범위가 지정된 세션과 이벤트 스트림을 노출하며, 타입이 지정된 거부, 스냅숏, 리플레이를 제공합니다. 설치, 완전한 Bun 세션, 브라우저 제공, 프로그램 질의, 오류, 생성된 계약 스키마, Replay에 대해서는 전체 TypeScript 런타임 가이드를 참고하세요.
0.41에서 Program에는 gate(state, occasion, target?)(Effect<Gate, InvalidInput>)와
def(state, name, args?)가 추가됩니다. args는 선언된 매개변수마다 JSON 스칼라 하나입니다. 닫힌 관문의
발생은 Errors.OccasionGateClosed(E-OCCASION-GATE)로 실패하며 상태를 바꾸지 않습니다.
Session.Session(서비스 타입 Session.SessionShape)은 자신이 로드한 Program을 program으로 노출하므로,
호스트는 번들을 두 번 로드하지 않고 current의 상태를 질의합니다. RuntimeState는 같은 번들
fingerprint의 모든 Program에서 유효하며, 다른 fingerprint의 프로그램에 넘기면 결함(defect)입니다.
Bundle.index의 타입은 Ir.projectIndex입니다.
Bun 호스트는 선택적 플랫폼 peer도 같은 Effect 릴리스에 정확히 고정해 런타임과 함께 설치해야 합니다:
bun add @lute-lang/runtime effect@4.0.2 @effect/platform-bun@4.0.2 @effect/platform-node-shared@4.0.2로컬 tarball을 사용한다면 scripts/pack-local.sh가 출력한
file:<runtime-tarball>로 @lute-lang/runtime을 바꾸고, 출력된
dependencies와 overrides 항목을 붙여 넣으세요. 그 뒤에도
effect@4.0.2, @effect/platform-bun@4.0.2,
@effect/platform-node-shared@4.0.2를 함께 지정합니다.
릴리스되지 않은 체크아웃을 호스트에서 시험하려면 Lute 저장소에서 bash scripts/pack-local.sh [--web] [OUT_DIR]를 실행하세요. 런타임과 player(--web이면 player-web도)를 bun pm pack으로 묶고,
호스트의 package.json에 붙여 넣을 dependencies와 overrides 항목(file:<tarball>)을 출력합니다.
호스트는 자신의 effect 4.0.2를 유지합니다. file:../lute/packages/* 디렉터리 링크는 쓰지 마세요.
Lute 저장소 자신의 effect로 해석되어 호스트에 effect 인스턴스가 두 개 생깁니다.
로컬 도그푸딩을
참고하세요.
시계 값과 HUD 라벨
섹션 제목: “시계 값과 HUD 라벨”Program.clock(state)은 숫자 값입니다. day와 0부터 시작하는 slot
인덱스를 반환합니다. 슬롯 이름과 요일 라벨은
program.view(state).clock에서 가져오며, ClockView는
{ day, slot, weekday, weekdayLabel, ended, last } 필드를 가집니다.
Effect 스타일 HUD는 다음처럼 작성할 수 있습니다:
import { Effect, Option } from "effect"
const hud = Effect.gen(function* () { const position = yield* program.clock(state) const world = yield* program.view(state) if (Option.isNone(position) || world.clock == null) return "No clock" const clock = world.clock return `${clock.weekdayLabel ?? `Day ${clock.day}`} — ${clock.slot ?? `slot ${position.value.slot}`}`})정렬에는 숫자 위치를 사용하고, 표시 문자열에는 ClockView.slot과
ClockView.weekdayLabel을 사용하세요.
0.52 추가 사항
섹션 제목: “0.52 추가 사항”0.52의 IR 변경은 모두 추가형입니다. 새 구성을 쓰지 않는 프로젝트는 capabilitySnapshot을 포함해 바이트 단위로 같은 아티팩트로 컴파일됩니다. 계약 스키마의 이름은 -0.52 계열로 바뀝니다. 이벤트 형태는 하나 바뀝니다. awaitChoice 메뉴가 렌더링된 텍스트를 담습니다.
허브를 떠나는 jump. jump 명령은 선택적 leaves(1 이상의 정수)를 새로 가집니다. jump와 대상 본문 사이에 있는 허브 옵션·<return> 본문의 수이며, 허브를 하나 이상 떠날 때만 기록됩니다(허브 떠나기). 한도 있는 jump에는 붙지 않습니다.
- 새 semantic id
lute.core.hubLeave/1(모듈core, 동작 “jumps that leave a hub”, provenancehub leave).leaves가 있는jump를 가진 아티팩트만 이 id를 수집합니다. 0.51 동작을 따르는 엔진은 허브를 다시 제시하게 되므로, semantic 협상에서 그런 아티팩트를 거부합니다. 레지스트리는 17개에서 18개 id가 됩니다. - 런타임. jump는 자기 range를 즉시 끝내고, 그것을 감싼 허브 방문도 끝납니다(
<return>본문은 실행되지 않고 메뉴도 다시 제시되지 않습니다).leaves = n이면 가장 안쪽n개의 허브 방문이 끝나고 제어는 대상으로 이어집니다. 퀘스트 세그먼트와 최상위 range는 leave를 받지 않습니다. - 로드 검사. 레퍼런스 런타임은 대상에 정확히 이르지 않는
leaves를 로드 때 거부합니다. jump를 감싼 가장 안쪽n개의 range 본문을 제거하면 대상의 본문이 가장 안쪽에 남아야 하고, 제거한 각 본문은 허브 옵션이나<return>range여야 합니다(부족한 수, 넘치는 수, 형제 본문을 거부합니다).max와leaves를 함께 가진jump도, 바깥에서 허브 옵션·<return>본문 안으로 들어가는 전이도 거부합니다(그런 본문은hub레코드를 통해서만 들어갑니다). 컴파일된 아티팩트에는 이런 것이 없습니다. 안쪽 허브가<return>없는 바깥 허브의 마지막으로 배치된 비-exit 옵션이면leaves: 1과leaves: 2가 같은 주소에 이르지만 다르게 실행됩니다(바깥 허브를 다시 제시하거나 하지 않거나).leaves가 기준입니다. - 스냅숏 변경 없음. leave는 보행 지역적입니다. 제시마다의
hub레코드,scene.choices.*,scene.visited.*,once, 재실행은 그대로입니다.
렌더링된 메뉴 텍스트. 런타임은 제시할 때마다 줄 렌더러로 각 옵션 라벨과 프롬프트를 렌더링합니다({{…}} 마커는 값으로, \{{와 백슬래시는 lute 0.47.1 읽기로, 번역은 줄의 texts처럼 마커별로). awaitChoice 메뉴는 그 결과를 담습니다. prompt(branch나 hub에 있으면 이제 설정됨)와 promptTexts(로케일 → 텍스트, 비어 있으면 생략), 각 옵션의 text(항상)와 texts(비어 있으면 생략)입니다. 호스트 토큰과 경로가 설정되지 않은 마커는 작성한 텍스트 그대로 남고, 메뉴 텍스트는 보행을 멈추지 않습니다. hub 제시 레코드의 prompt는 렌더링된 프롬프트입니다. 대기 중인 Await가 Snapshot에 들어가므로 스냅숏 스키마도 바뀝니다. 호스트는 IR의 문자열을 렌더링하는 대신 메뉴의 텍스트를 보여 줍니다(사용자 정의 호스트 참고).
promptPlaceholders. 마커가 있는 프롬프트를 가진choice·hub명령은 옵션의placeholders처럼 만든promptPlaceholders를 담습니다. semantic id를 더하지 않습니다. 그 식은 옵션 라벨의 식과 같은 식 기반 요구 사항을 더하며(시간형 def를 읽는 프롬프트는lute.knowledge.temporal/1을 요구),path플레이스홀더는 콘텐츠 읽기입니다(프롬프트만 보여 주는 브리지 결과도awaitBridge.results에서 요청됩니다).
보상 kind 속성. RewardEntry는 attrs를 새로 가집니다. 작성한 각 보상 kind 속성과 작성하지 않은 각 default:를 plugin 레코드의 fields처럼 타입 적용해 맨 뒤에, 비어 있지 않을 때만 기록합니다. 이로부터 바인딩된 효과는 기존 retracts/asserts/raises에 들어갑니다. grant 레코드의 reward는 attrs를 복사합니다(비어 있지 않을 때만). 설명용이며 semantic id는 없습니다. 호스트는 capabilitySnapshot이 고정하는 보상 kind 계약에 따라 attrs를 읽습니다.
{"index":0,"instance":1,"kind":"grant","objective":"pay","quest":"first","reward":{"amount":1,"attrs":{"rarity":"gold"},"kind":"BADGE","target":"mira"}}32비트 대상. 목록 인덱스, countDistinct 열, 요일 레이블은 모든 대상에서 int 피연산자를 잘림 없이 변환하며(레퍼런스 player-web의 wasm32 런타임이 네이티브와 같습니다), lute lint의 목록 인덱스도 마찬가지입니다(#198).
엔진이나 호스트가 할 일. 버전 게이트를 0.52로 넓힙니다. leave하는 jump가 있는 아티팩트를 받으려면 supportedIds에 lute.core.hubLeave/1을 넣고, 그 jump가 떠나는 허브 방문을 끝냅니다. 메뉴 옵션 텍스트와 프롬프트는 awaitChoice 메뉴에서 보여 줍니다. promptPlaceholders와 보상 attrs는 선택적으로 읽습니다.
0.51 추가 사항
섹션 제목: “0.51 추가 사항”0.51의 IR 변경은 모두 추가형입니다. 새 키를 하나도 쓰지 않는 프로젝트는 바이트 단위로 같은 아티팩트로 컴파일되며, 예외가 하나 있습니다. 코어 ::jump가 이제 max를 선언하므로 capabilitySnapshot은 모든 아티팩트에서 한 번 바뀝니다. 그 밖의 새 키(effects.raises, raisedBy, type: text)는 이를 선언한 스냅숏의 해시에만 접힙니다. 계약 스키마의 이름은 -0.51 계열로 바뀝니다.
제한된 역방향 jump. jump 명령은 선택적 max(1~99의 정수)를 새로 가지며, jump에 없으면 생략됩니다. max가 없는 jump는 0.50과 바이트 단위로 같고 여전히 앞으로만 갑니다. max가 있으면 jump는 앞선 라벨을 대상으로 삼을 수 있고 최대 max번 실행됩니다. 그 뒤에는 실행되지 않고 제어가 다음 명령으로 넘어갑니다.
- 새 semantic id
lute.core.boundedJump/1(모듈core, 동작 “bounded backward jumps”). 아티팩트는 어떤jump명령에max가 있을 때만 이를requiredSemantics(출처bounded jump)에 나열합니다.max를 무시하는 엔진은 한없이 반복하거나 전혀 반복하지 않으므로 이 id는 의미를 바꾸는 id입니다.supportedIds에 이를 나열하지 않은 엔진은 이런 아티팩트를 재생 전 semantic 협상에서 거부합니다. 레지스트리는 16개에서 17개 id가 됩니다. - 카운터. 각 제한된 jump는 한 번의 walk(프레젠테이션 하나, 퀘스트 핸들러나 목표 세그먼트 하나, 엔트리나 비트 본문 하나)에 국한된 카운터를 0에서 시작해 가집니다. 제어가 jump에 도달했을 때(
when이 있으면 그것이 성립하고) 카운터가max미만이면 카운터를 올리고 대상으로 이동하며, 아니면 다음 명령으로 넘어갑니다. 제어가 jump의 영역 바깥에서 jump의 대상에 도달할 때마다 카운터는 0으로 돌아갑니다. 영역은 명령 스트림에서 대상부터 jump까지의 구간이며, 라벨로 흘러 들어오거나, 앞으로 가는 jump로 오거나, 영역 안에 있지 않은 제한된 jump로 올 때가 이에 해당합니다. jump 자신의 실행이나 그 영역 안의 다른 제한된 jump의 실행은 카운터를 리셋하지 않습니다. 같은 머리를 공유하는 두 제한된 jump는 서로를 리셋하지 않습니다. 루프는<on>이나<objective>경계에 걸치지 않으므로 영역은 항상 하나의 명령 범위 안에 있습니다. - 종료. 두 번의 실행 사이에서 제어는 앞으로만 움직이므로 walk는 루프의 중첩으로 한정됩니다. 중첩 깊이 d의 영역은 둘러싼 jump들에 대해 최대 Π(max + 1)번 실행됩니다. 참조 런타임은 이를 컴파일러에 맡기지 않고, 적재 시 모든 레코드의 모든 제어 이동(
jump의target, choice·hub 옵션의target, hub의return,converge, match 갈래의target,otherwise,converge, 목표·핸들러·엔트리·비트의body)을 다시 검사합니다. 레코드 자신이나 앞선 레코드로의 이동은 잘 중첩된 영역 안의max있는jump가 아니면 거부되며, 중첩 없이 겹치는 두 제한된 루프, 1..=99 밖의max, 빠졌거나 위치가 아닌 필수 대상도 거부됩니다.Runtime::load는 그 레코드를 알려 주고(malformed `jump` record at 001-0200: its `target` goes back to 001-0100, a cycle that never ends (only a `jump` with `max` may go back)),lute run/lute trace도 같은 오류를 출력합니다. 폭주 단계 가드는 여전히 범위가 시작된 뒤 또는 마지막으로 실행된 제한된 jump 뒤의 단계를 셉니다. - 컴포넌트 라벨. 컴포넌트 전개 안의
::label은 컴파일된 스트림에서::use인스턴스마다 따로 이름이 붙고, 그 인스턴스의 jump는 자기 라벨을 대상으로 합니다. 따라서 두 번 쓰인 컴포넌트도 각 인스턴스 안에서 루프를 돌거나 앞으로 jump합니다(#207). - 스냅숏 변경 없음. 카운터는 walk 안에서만 쓰이며 hub의 presentation을 재구성하는 것과 같은 재생으로 다시 만들어집니다. 런타임
Snapshot과 이벤트 스키마의 모양은 바뀌지 않고 버전 문자열만0.51로 바뀝니다. 이후 반복에서 다시 도달한<branch>는 새request를 가진 새awaitChoice이며presentation은0으로 유지됩니다. 루프 안의<hub>는 매 방문을 presentation 0에서 시작합니다. - 트레이스. 드라이버의
jump관측은 실행 횟수와max를 얻고, jump가 실행되지 않으면 소진 관측을 얻습니다.lute trace는 이를 정적 주장이 아니라 설명으로 출력합니다.
선언된 raise. plugin 명령은 선택적 해석된 raises: [{ occasion, target?, dynamic? }]를 새로 가지며, RewardEntry도 같은 raises를 얻습니다. 각각 비어 있으면 생략되고 마지막에 쓰입니다(RewardEntry.raises는 credits, retracts, asserts, accepts 뒤). @attr은 속성의 컴파일된 값(없으면 선언된 default:)으로 바인딩되고 occasion의 도메인 접두사가 적용됩니다(scene.chapter2). 바인딩되지 않은 대상은 항목을 쓰지 않습니다. occasion.target을 읽는 대상은 도메인 접두사를 붙인 참조(scene.occasion.target)로 쓰이고 "dynamic": true로 표시됩니다. 런타임은 표시된 raise만 바인딩하며, 대상이 occasion의 선언된 접두사를 붙인 occasion.target이 아니거나, 대상이 없거나, 대상 없는 occasion에 붙은 표시된 raise는 적재 시 거부합니다. 보상의 raise는 보상 종류의 effects.raises로 찍히고, @target은 accepts처럼 바인딩되며, 동적이지 않습니다.
- 내보내는
plugin레코드(중단된 변형 포함)와grant레코드는occasion.target을 바인딩하고dynamic표시를 뺀raises를 담으며, 비어 있으면 생략됩니다.record페이로드는 이벤트 스키마에서 타입이 없으므로 레코드 카탈로그만 바뀝니다. 지급의 효과 레코드(grant키가 붙은retract,assert,accept)는 바뀌지 않으며, 런타임이 아무것도 발생시키지 않으므로 raise에는 자체 레코드가 없습니다.lute run의 적합성 레코드는raises를 유지합니다. - 서술용이며 semantic id 없음. 런타임은
raises로 동작하지 않으며 플러그인 어휘는capabilitySnapshot으로 버전이 관리됩니다. 보상 종류가raises만 선언해도lute.quest.rewardEffects/1은requiredSemantics에 들어가지 않습니다. 그 id는 여전히retracts,asserts,accepts에서만 수집됩니다. - 호스트의 책무. raise를 수행하는 호스트는 호출을 실행한 프레젠테이션이 끝나고(
presentationEnd) 런타임이 idle이 된 뒤에, 또는grant레코드를 담은 출력이 적용된 뒤에 각 raise를raiseOccasion입력으로 보냅니다. 런타임은 출력의 아직 전달되지 않은 뒷부분만 재생하므로, 호스트는 저장을 거쳐 자체 대기 큐를 유지합니다(커스텀 호스트 참고).
현지화된 지시문 텍스트. plugin 명령은 text로 타입이 지정된 각 속성에 대해 선택적 localized 맵({ "<attr>": { "lineId", "placeholders"?, "texts"? } }, 속성 이름이 키)을 새로 가집니다. lineId는 {prefix}~{tag}~{code}~{attr}이고 placeholders와 texts는 비어 있으면 생략됩니다. fields.<attr>는 작성된 그대로의 {{…}} 마커를 가진 원본 언어 문자열을 유지하므로 현지화를 무시하는 호스트는 전과 같은 값을 읽으며, 호출의 0.50 스탬프는 stamps에 남습니다. texts(locale 태그 → 번역 텍스트)는 대사 줄처럼 lute compile --locale-bundle이 채웁니다. text 속성이 없는 지시문은 0.50과 바이트 단위로 같습니다.
- 런타임.
localized가 있는plugin명령에 대해 런타임은 각text속성의 마커를 한 번 렌더링해 렌더링된 원문을 레코드의attrs.<attr>에, 렌더링된 번역을 레코드의localized: { <attr>: { lineId, texts } }에 씁니다. 대사 줄과 같은 방식입니다. 호스트 토큰 마커는 그대로 남습니다. bridge await의attrs도 같은 렌더링된 값을 담습니다.lute run의 적합성 레코드는 대사 줄의 번역을 빼는 것처럼localized를 뺍니다. - 서술용이며 semantic id 없음. 엔진은
raises와localized를 읽어도 됩니다(MAY).
엔진이나 호스트가 해야 할 일. 버전 게이트를 0.51로 넓히고, 제한된 루프가 있는 아티팩트를 받으려면 supportedIds에 lute.core.boundedJump/1을 나열하며, 선택적으로 선언된 raise를 수행하고 localized를 읽습니다. 모든 아티팩트의 capabilitySnapshot이 한 번 바뀌므로 스냅숏 해시를 고정해 둔 엔진은 이를 갱신합니다. texts 모델은 현지화 텍스트를 참고하세요.
0.50 추가 사항
섹션 제목: “0.50 추가 사항”0.50의 IR 변경은 모두 추가형입니다. 새 키를 하나도 쓰지 않는 프로젝트는 capabilitySnapshot을 포함해 바이트 단위로 같은 아티팩트로 컴파일됩니다. 계약 스키마의 이름은 -0.50 계열로 바뀝니다.
branch, hub, choice의 스탬프. on:이 branch, hub, choice를 지명하는 플러그인 stampAttrs 항목은 해당 요소에 작성할 수 있습니다. choice/hub 명령 레코드의 기존 stamps 객체는 이제 <branch>/<hub> 요소의 스탬프를 담습니다. 각 choice/hub 옵션은 선택적 stamps 객체({ "<attr>": <value> })를 새로 가지며, <choice> 요소의 스탬프로 채워지고 마지막에 직렬화되며 비어 있으면 생략됩니다. 따라서 스탬프 없는 옵션은 0.49와 바이트 단위로 같습니다. 작성하지 않은 스탬프는 주입되지 않으며, 스탬프는 자기 표면의 선언으로 타입이 정해집니다(한 이름이 두 표면에서 서로 다른 타입으로 직렬화될 수 있음). 스탬프는 서술용입니다. 런타임은 스탬프를 읽지 않고, awaitChoice 메뉴(주소 document/position, verdict, once/exit)는 바뀌지 않으며, semantic id도 없습니다. 참조 player를 쓰지 않는 호스트는 옵션 텍스트를 읽듯이 메뉴 주소의 IR 레코드에서 요소·옵션 스탬프를 읽습니다. 참조 player의 ChoiceView와 각 옵션은 그 레코드에서 읽은 선택적 stamps를 가지며, 없으면 생략됩니다.
호스트 토큰은 그대로 남습니다. 플러그인이 hostTokens export로 선언한 bare {{token}}은 userName이 늘 그랬듯 reserved placeholder, 즉 { "kind": "reserved", "token": "<name>", "format"? }입니다(lute.core가 userName을 선언합니다). 새 environment 항목은 없습니다. 런타임은 모든 호스트 토큰 마커를 출력 텍스트에 그대로 렌더링하고, 호스트 또는 presenter가 렌더링 시점에 치환합니다. 이는 userName만이 아니라 선언된 모든 토큰에 적용됩니다. player 동작은 바뀌지 않습니다.
보상 효과. RewardEntry는 선택적으로 해석된 retracts와 asserts(각각 plugin 명령의 팩트가 쓰는 { relation, args } 팩트 레코드 목록), accepts(quest id 목록)를 새로 가지며, 각각 비어 있으면 생략되고 credits 뒤에 쓰입니다. @target은 이미 바인딩되어 있고, 바인딩되지 않은 @target이 남은 패턴은 쓰이지 않습니다. 보상 kind는 rewardKinds.<K>.effects로 이를 선언하며, effects가 없는 kind의 항목은 바이트 단위로 같습니다.
보상이 지급되는 시점(outcome 필터가 전환과 맞고 when=이 성립하는 경우)에는 선언 순서대로 보상마다: 먼저 grant 레코드와 그 credits 쓰기(이전과 동일), 그다음 보상의 retracts, 이어서 asserts를 런타임의 일반 assert/retract 경로(E-FACT-EXCLUSIVE 런타임 검사 포함)로, 마지막으로 accepts를 ::accept의 경로로 처리합니다. 아티팩트의 accept 구동 quest는 다음 lifecycle 라운드에 활성화되고, 이미 unset을 지난 quest는 그대로 두며 레코드가 그 사실을 알려 줍니다. objective 지급은 quest 지급보다 앞서고, 한 전환의 모든 지급은 그 lifecycle 핸들러 본문보다 앞서므로 questComplete 핸들러는 assert된 팩트를 읽습니다. 효과는 지급마다 정확히 한 번 발생하며, 리플레이와 복원은 지급의 기존 인스턴스 identity를 따릅니다.
효과 레코드의 grant 키. 각 효과는 그 명령이 냈을 레코드에 더해 "grant": { "quest", "instance", "index", "objective"?, "kind" }, 즉 뒤따르는 grant 레코드의 identity를 내보냅니다. position도 effectOf도 없습니다. 호스트는 assert, retract, accept 레코드의 grant 키를 목록에 올립니다:
{"kind":"grant","quest":"first","instance":1,"index":0,"reward":{"kind":"BADGE","target":"mira","amount":1}}{"kind":"assert","fact":"met(mira)","grant":{"quest":"first","instance":1,"index":0,"kind":"BADGE"}}{"kind":"accept","quest":"second","grant":{"quest":"first","instance":1,"index":1,"kind":"OFFER_QUEST"}}이벤트와 스냅숏의 형태는 바뀌지 않습니다. 이벤트 스키마에서 record payload는 타입이 없으므로 레코드 목록만 늘고, 팩트와 수락된 quest는 이미 스냅숏에 저장됩니다. player는 assert/retract/accept를 bookkeeping cue로 다룹니다.
새 semantic id lute.quest.rewardEffects/1(모듈 quest, 동작 “reward fact and accept effects”). 어떤 RewardEntry에 비어 있지 않은 retracts, asserts, accepts가 있을 때만 아티팩트의 requiredSemantics에 이 id가 들어갑니다(provenance reward effects). 이 필드를 무시하는 엔진은 팩트와 quest 활성화를 잃으므로 이 id는 의미를 바꾸는 id입니다. supportedIds에 이 id가 없는 엔진은 그런 아티팩트를 플레이백 전 semantic 협상에서 거부합니다. 레지스트리는 15개에서 16개로 늘어나며, 보상 효과를 쓰지 않는 아티팩트는 이전과 똑같이 협상됩니다.
0.49 런타임 추가 사항
섹션 제목: “0.49 런타임 추가 사항”플러그인 스탬프는 중첩된 stamps 객체 하나입니다. 작성자가 디렉티브나 콘텐츠 줄에 쓴 횡단 플러그인
stampAttrs 값은 더 이상 레코드에 평탄화되지 않습니다.
스탬프가 붙는 모든 명령(jump와 barrier를 제외한 전부)은 timing, provenance, source 뒤에
"stamps": { "<attr>": <value>, … }를 가집니다. 작성된 스탬프가 없으면 객체는 생략되므로 스탬프가 없는
레코드는 0.48.1과 바이트 단위로 같습니다. 따라서 레코드 키와 같은 이름의 스탬프(tag, speaker, text,
label 등)도 더 이상 충돌하지 않습니다. 코어 스탬프 멤버 timing, provenance, source는 그대로
평탄화되며, 예약된 여덟 이름도 바뀌지 않습니다.
같은 객체가 모든 플러그인 경로에서 호스트에 전달되며, attrs와 분리되고 절대 attrs에 병합되지 않습니다.
정상 및 중단된 plugin 이벤트 레코드, awaitBridge, 브리지 요청, 이를 담는 중단된 스냅숏이 각각
선택적 stamps를 가지며 비어 있으면 생략됩니다. 스테이징 레코드는 코어 필드 attrs를 유지하며 스탬프는
섞이지 않습니다.
콘텐츠 줄의 timing.wait. 줄은 wait(불리언)를 받습니다. 이는 줄 레코드의 timing.wait로
컴파일되며 작성하지 않으면 생략됩니다. 줄은 항상 자신의 진행을 기다립니다. wait는 그 줄 뒤부터 다음
줄 앞까지의 스테이징 큐가 그 줄을 기다리는지를 말합니다. true이거나 없으면 기다립니다(기존과 같음).
false이면 줄이 표시되는 동안 시작합니다. 다음 줄은 여전히 이전 줄을 기다립니다. 레퍼런스 플레이어는
이를 따릅니다. timing.wait === false인 줄 큐는 포크되며, 스케줄러는 다음 line 큐 앞, presentation 큐 앞(대화창을 비우므로 줄과 겹치면 안 됨), 배치가
정착하기 전에 이를 조인합니다. 줄의 delay와 duration은 여전히 E-UNKNOWN-ATTR입니다.
타입이 지정된 브리지를 통한 자유 텍스트 입력
섹션 제목: “타입이 지정된 브리지를 통한 자유 텍스트 입력”Lute에는 자유 텍스트 구성물이 없습니다. “플레이어에게 무언가를 입력하게 한다”의 호스트 쪽 절반은
결과가 string이고 상태 슬롯에 쓰이는 브리지입니다:
bridgeCapabilities: - service: prompt operation: ask replay: recorded result: - { name: answer, type: string }directives: - name: ask layer: bridge attrs: - { name: slot, required: true, type: { slotId: { namespace: scene.ask } } } semantics: [ "writes.sceneState", "bridgeCall" ] state: declares: - { scope: scene, path: [ask, { fromAttr: { name: slot, slotType: localId } }], shape: textAnswer } effects: writes: - { scope: scene, path: [ask, { fromAttr: { name: slot } }, answer], value: { fromBridgeResult: answer } } bridge: { service: prompt, operation: ask }런타임은 awaitBridge로 중단됩니다. 호스트는 호출의 attrs(여기서는 slot)로 자체 텍스트 프롬프트를
열고 bridgeResult { request, fields: { "answer": "<플레이어가 입력한 값>" } }로 답합니다. 런타임은
string을 검증하고 선언된 쓰기를 적용하므로, 호출 뒤의 줄은 일반 상태를 통해 답을 읽습니다. Lute는
입력 채널을 보지 못하며, 답은 다른 브리지 결과와 같은 데이터입니다. .test.yaml을 포함한 전체 프로젝트는
docs/examples/free-text-input/입니다.
0.44 런타임 추가 사항
섹션 제목: “0.44 런타임 추가 사항”엔딩에 속한 쓰기. 런타임이 ended로 응답한 뒤에는 newRun이나 outsideRun 계기만 받습니다. 그래서 끝난 런에 속한 호스트 쓰기(엔딩 메타데이터, 최종 점수)는 종료를 일으키는 입력이 쓰기를 실을 수 있다면 그 입력의 writes에 실어야 합니다. raiseOccasion, advanceClock, bridgeResult, hostWrite가 그런 입력이며, 이들의 쓰기는 종료 전환보다 먼저 적용됩니다({"type": "raiseOccasion", "occasion": "dawn", "writes": {"state": [{"path": "run.endedBy", "value": "dawnSeven"}]}}). choose, timeout, worldEvent는 쓰기를 싣지 않으므로, 이들이 일으키는 엔딩은 그 상태를 스토리(끝나는 갈래의 ::set)나 그 전에 보낸 hostWrite에 기록합니다. newRun의 쓰기는 새로 시작하는 런의 초기값이며, 그 런에서 끝난 런의 run.* 값은 prev.run.*로 읽힙니다. 엔딩 뒤의 hostWrite는 여전히 상태를 바꾸지 않고 E-RUNTIME-BUSY로 거절되며, 이제 메시지가 그 쓰기가 어디에 실려야 하는지 알려 줍니다.
0.45 런타임 추가 사항
섹션 제목: “0.45 런타임 추가 사항”읽기 전용 질의(candidates, evaluate, def, gate, view)는 모든 await
(idle, awaitChoice, awaitBridge, ended)에서 상태를 바꾸지 않고 답합니다. TypeScript
Session에서 입력이 실행 중일 때 질의는 마지막으로 커밋된 상태를 읽습니다. choice나
bridge가 대기 중일 때는 해당 응답만 받습니다. 호스트는 candidates(occasion, target?)를
질의해 eligible인 entry를 고르고, artifact에서 본문을 찾아 모든 레코드가 placeholder 없는
line일 때에만 프레젠테이션 밖에서 background bark를 표시할 수 있습니다. 이 static bark는
읽음 표시, once 소비, writes를 기록하지 않습니다. 기록이 필요하거나 동적인 bark는 이야기가
idle로 돌아온 뒤 raise해야 합니다. 동시 프레젠테이션은 계획하지 않습니다. 자세한 내용은
0.45 background-bark 호스트 패턴과
전체 이벤트 계약을 참고하세요.
0.42 런타임 추가 사항
섹션 제목: “0.42 런타임 추가 사항”모든 스테이징 레코드 — bg, music, sfx, vfx, actor, camera, cg, video, sequence — 는 plugin 레코드의 attrs처럼 명령의 해석된 필드를 attrs로 담습니다. 작성한 값과 lowering이 채운 기본값이 들어가고(cg의 display: "show", actor의 선언된 기본 anchor), 없는 선택 필드는 생략되며 attrs는 항상 객체입니다. 레코드 엔벨로프(kind, position), timing, provenance, 중첩된 플러그인 stamps는 원래 자리에 있으며(스탬프는 attrs에 섞이지 않습니다), 호스트는 아티팩트에서 명령을 찾지 않고 스테이징 레코드를 읽습니다. volume은 여전히 프로젝트 volume 도메인의 멤버이며, 플레이어가 volumes로 gain으로 바꿉니다.
| kind | attrs |
|---|---|
bg | location?, time?, assetId? |
music | playback?, stop?, mood?, volume?, assetId? |
sfx | sound?, assetId? |
vfx | type, label?, transition? |
actor | character, anchor?, action?, exit?, emotion?, costume?, posReset?, preload? |
camera | focus?, framing?, move?, transition? |
cg | assetId, display, layout? |
video | assetId, display |
sequence | name |
0.41 런타임 추가 사항
섹션 제목: “0.41 런타임 추가 사항”- 관문 거절은 거부입니다.
raisedWhen이 거짓인raiseOccasion, 또는terminal이 성립한 뒤outsideRun이 아닌 occasion의 발생은 상태를 바꾸지 않고(입력 자신의 쓰기와 payload 포함)E-OCCASION-GATE로 거부되며, 메시지는lute play의 거절 문구입니다. 판정할 수 없는 관문은 여전히incomplete로 정지합니다. 읽기 전용 질의gate(occasion, target?)가 미리{ open, unresolved? }를 답합니다. def(name, args)는 매개변수가 있는 인덱스 def를 평가합니다: 선언된 매개변수마다 JSON 스칼라 하나가param노드에 바인딩되며, 개수·타입 불일치는E-RUNTIME-INPUT입니다.evaluate는param노드를 받지 않습니다.awaitBridge는attrs(호출의 속성,occasion.target바인딩)와results(콘텐츠가 읽는 결과 필드와 선언 타입)를 가지며, 답은 그 타입으로 검증됩니다.bridgeResult는hostWrite와 같은writes를 가질 수 있고, 호출의 선언된 효과 뒤에 적용되어 같은 프레젠테이션의 이후 줄이 읽습니다. 모든plugin레코드는attrs를 가지며, 차단하지 않는 plugin 레코드에는 쓰기가 없습니다.advanceClock { writes }는 시계가 움직이기 전에 쓰기를 적용하고 정착합니다(그때terminal이 성립하면 시계는 머물고 await는ended). 이동의 발생들이 그 쓰기를 읽고,clock이벤트는 마지막에 옵니다.
0.40 런타임 추가 사항
섹션 제목: “0.40 런타임 추가 사항”0.40 계약은 persistent presentation stage(background, music, actor, CG,
video)를 world.stage 스냅숏에 담고 읽기 전용 stage() 질의를 제공합니다.
중단된 choice나 bridge도 await 시점의 stage를 continuation.stage에 담습니다.
복원할 때 레코드를 다시 재생하지 말고 이 값으로 presenter를 재구성합니다.
시간 제한 branch는 awaitChoice.menu.timeout과 menu.default를 채웁니다.
호스트가 벽시계를 측정해 {\"type\":\"timeout\",\"request\":N}을 보냅니다.
런타임은 일치하는 시간 제한 choice에서만 이를 받아 선언된 default option을
적용합니다. 다른 await에서 보내면 E-RUNTIME-TIMEOUT으로 거부되며, 잘못된
request는 계속 E-RUNTIME-REQUEST입니다. 시간 제한 branch는 timeout과
default를 함께 선언해야 합니다.
Lute가 하는 일과 엔진이 하는 일
섹션 제목: “Lute가 하는 일과 엔진이 하는 일”| Lute(컴파일 시점) | 엔진(런타임) |
|---|---|
| 문서를 정적으로 검사하고, 오류가 있으면 출력을 거부합니다. | 아티팩트를 신뢰합니다 — 깨끗하게 컴파일되었습니다. |
| 상태 스키마를 초기화/타입 테이블로 접습니다. | 그 테이블에서 상태를 초기화하고 등급별 수명을 소유합니다. |
닫힌 §8.4 프로필 안에 텍스트가 있는 모든 CEL 가드를 이식 가능한 expr AST로 낮추고, 나머지는 CEL 텍스트로 cel에 남깁니다. | 실제 상태에 대해 가드를 평가합니다. |
팩트, assert/retract 델타, Datalog 규칙을 데이터로 내보내고, 규칙이 층화되고 안전한지 증명합니다. | 팩트 저장소에서 최소 모델(최소 고정점)을 계산합니다. |
퀘스트, 목표, <on> 핸들러를 선언으로 내보냅니다. | start/fail/목표 완료로부터 퀘스트 생명주기를 도출합니다. |
| 플러그인 브리지 호출과 그 상태 쓰기 바인딩을 해석합니다. | 호출하고 효과를 적용합니다. |
| 타임라인 클립을 스케줄링하고 쓰기 경합이 없는지 증명합니다. | 스케줄을 재생하거나(또는 트랙을 동시에 실행하고) 배리어를 준수합니다. |
holds()와 count()는 저자가 작성할 수 있는 §8.4 CEL 프로필 안에 있지만
expr AST에는 의도적으로 없습니다. 따라서 팩트를 질의하는 가드는 expr 형제 없이
cel 텍스트만으로 엔진에 도달합니다(option.when, arm.test,
set.value). 그러므로 엔진은 단순한 AST 순회기가 아니라 CEL 평가기를 반드시 갖춰야
합니다. lute run의 모듈 문서도 같은 내용을 말합니다. 즉, 구조화된 expr AST가 의도적으로
생략한 holds/count 팩트 질의 함수를 포함해 모든 슬롯을 cel 텍스트에서 해석합니다.
visited('<scene id>')(dsl 0.21.0)도 마찬가지입니다. 모든 조건 슬롯에서 사용할 수 있으며, 해당
씬이 이 세이브에서 제시되면 참이 됩니다 — 엔진이 이미 after:를 위해 유지하는 visited 집합입니다 —
그리고 cel 텍스트만으로 전달됩니다.
identity 마이그레이션 (0.36.6)
섹션 제목: “identity 마이그레이션 (0.36.6)”컴포넌트 스코프는 저작된 instance 키를 사용합니다:
::use{component="hearthFire" instance="opening"}은 순번이 아닌
hearthFire#opening을 생성합니다. 조인 또는 세이브를 게시하는 프로젝트는
identity.requireStable: true를 선택합니다. 이렇게 하면 컴포넌트 인스턴스 누락 및 줄 코드에
대한 진단이 활성화됩니다. lute tag는 기존 저작 키를 보존하면서 두 항목을 모두 보충하고,
use-001, use-002, …를 할당합니다.
실행 IR과 프로젝트 인덱스의 선택적 identityRenames 배열은 엔진이 사용하는 유일한 이름 변경
마이그레이션 입력입니다. 각 항목은 정규 NodeKey 문자열을 사용하는 {from, to}입니다.
lute diff는 원장 일치 시 renamed를 보고하고, 저작된 매핑이 없으면
unmappedIdentity를 보고합니다. 텍스트나 위치를 근거로 추측하지는 않습니다.
---kind: sceneid: runtime-identity---## Opening@narrator{code="intro"}: Ready.핵심은 다음과 같습니다. Lute는 형태와 구조를 증명하고, 엔진은 평가와 효과를 제공합니다.
Lute의 정적 분석도 한계를 정직하게 드러냅니다 — 도달 가능성은 선언된 after: 경로 아래에서
보수적으로 계산되고, 관계형 팩트 게이트는 사람 검토 경계 뒤에서 Unknown 판정을 내리며,
lute trace는 결정적인 목업 기반 경로 하나를 순회할 뿐 모든 경로의 증명이 아닙니다.
엔벨로프
섹션 제목: “엔벨로프”모든 아티팩트는 고정된 엔벨로프로 시작합니다(crates/lute-compile/src/ir.rs의
ExecutionIr 구조체).
| 필드 | 의미 |
|---|---|
kind | "scene" | "quest" — 먼저 읽으며 meta의 형태를 선택합니다. |
lute | 언어 버전 고정값(런타임에 정보를 제공하기 위한 값). |
irVersion | 가장 먼저 게이트할 IR 스키마 버전입니다. |
capabilitySnapshot | 플러그인 캐퍼빌리티 스냅숏 해시입니다. 불일치하면 거부합니다. |
requiredSemantics | 컴파일러가 도출한, 아티팩트가 사용하는 정렬된 의미 ID입니다. 저자는 이 목록을 편집할 수 없습니다. |
meta | 씬 메타데이터 또는 퀘스트 메타데이터입니다. |
state | 접힌 초기화/타입 테이블입니다. |
entities / enums / relations / seedFacts / rules | 선언된 어휘입니다(비어 있으면 생략). |
commands | 평탄하고 순서가 있는 명령 스트림입니다. 모든 레코드에 kind, family, position이 있습니다. |
prereqEdges | 참고용 원시 그래프 간선입니다(비어 있으면 생략). |
sections | 문서의 ## 섹션이며, 각각 { section, heading, id? }입니다(heading 또는 id가 하나도 없으면 생략). |
seasons / gates / terminal / terminalPersists | 선택적 시계 및 계기 제어입니다. |
requiredSemantics는 하나의 아티팩트에서 낮춰진 기능들의 정렬된 합집합입니다. 프로젝트 인덱스에도 문서 아티팩트의 정렬된 합집합으로 같은 필드가 들어 있으므로, 프로젝트 전체 check, play, 엔진은 모든 문서를 열기 전에 협상할 수 있습니다. 완전한 레지스트리와 트리거 매핑은 0.33.0 제안서 §2–§3에 있으며, 이 페이지에서는 의도적으로 그 레지스트리를 반복하지 않습니다.
명령 레코드
섹션 제목: “명령 레코드”모든 명령 레코드에는 필수 필드 세 개와 선택적 형제 필드 네 개가 있습니다.
| 필드 | 의미 |
|---|---|
kind | 레코드 종류입니다. 알 수 없는 kind는 치명적 오류입니다. |
family | kind의 규범적 그룹화입니다(아래 참조). kind보다 먼저 디스패치에서 읽습니다. |
position | 빌드에 국한된 레코드 실행 위치입니다(위치 참고). |
timing | 선택적 { wait?, duration?, delay?, at?, timeline? }입니다. 모든 멤버가 없으면 생략됩니다. line은 wait를 가질 수 있습니다(0.49 참고). |
provenance | 컴파일러가 합성한 레코드에 붙는 선택적 { by, explanation }입니다. |
source | 선택적 소스 위치입니다. |
stamps | 스탬프가 붙는 모든 명령에 있는 플러그인 stampAttrs 값의 선택적 객체({ "<attr>": <value> })입니다. 비어 있으면 생략되며, jump와 barrier에는 없습니다. 0.50부터 choice/hub 레코드의 stamps는 <branch>/<hub> 요소의 스탬프를 담고, 각 옵션도 자기 스탬프를 가집니다(0.50 참고). |
family | 종류 |
|---|---|
content | line |
staging | bg, music, sfx, vfx, actor, camera, cg, video, sequence |
state | set, assert, retract |
control | choice, match, hub, jump, end, barrier |
declaration | quest, on, entry, accept, beat |
plugin | plugin |
timing.wait는 불리언이고, duration, delay, at은 유한한 음이 아닌 초 단위 값입니다. timeline은 레코드가 방출된 <timeline>의 0부터 시작하는 순번이며, 초가 아닌 순번입니다. barrier에는 timing이 없습니다. 해당 레코드의 timeline 순번과 at 초는 직접 필드입니다. 타이밍은 timing에서만 읽으세요.
스테이징 값은 프로젝트가 선언한 도메인의 불투명한 멤버입니다:
actor.anchor/action/emotion/costume, camera.framing/move/
transition(focus도 포함하며, 이는 cast 참조입니다), cg.layout, music.playback,
sequence.name입니다. Lute는 숫자 카메라 변환을 합성하지 않습니다. cg와 video에는 항상
해석된 display가 들어갑니다(기본값은 show). sequence 레코드는 외부 시네마틱을
이름으로 가리키며, timing.wait: true이면 엔진은 진행하기 전에 해당 시네마틱의 제시를
완료합니다. 액터 퇴장은 파생된 actor.exit: true입니다(action 도메인의 exits:에서
유래). false일 때는 생략됩니다.
line에는 모든 역할에 대해 role(dialogue, narration, mono, os, vo),
speaker, text, lineId, voiceKey가 들어갑니다. voiceKey는 녹음이 존재한다는 약속이
아니라 결합 키입니다. text는 저작된 텍스트에서 파생된 평문입니다. 인라인 수정자는 제거하고,
이스케이프는 디코딩하며, {{…}} 마커는 유지하고 마커마다 순서대로 placeholders 항목이 하나씩 있습니다.
다만(lute 0.47.1) 리터럴 {{(저작 시 \{{)는 \{{로 쓰고, {{ 바로 앞의 리터럴 백슬래시는 모두
두 번 씁니다. segment 실행, texts, localeSegments도 같은 인코딩을 씁니다. 왼쪽에서 오른쪽으로
읽으며, {{ 바로 앞에 백슬래시가 n개 이어지면 ⌊n/2⌋개의 백슬래시를 뜻하고, n이 홀수이면 그
{{는 리터럴 텍스트(한 단위로 소비)이며 짝수이면 다음 마커를 엽니다. run.a = 3일 때
Lit \{{run.a}} and {{ run.a }}.는 Lit {{run.a}} and 3.로 렌더링됩니다. placeholder를 직접
치환하는 엔진은 이 읽기 규칙을 적용해야 합니다. \{{가 있는 줄은 loc sourceHash가 새로 바뀌므로
다시 내보내세요(0.47.0에서 내보낸 것은 W-L10N-STALE입니다). 인라인 수정자를 사용하는 줄에는 제시
실행 단위인 segments도 들어갑니다: { text, styles?, rate? } 텍스트 실행과
{ pause } 리프(초)입니다. 수정자가 없는 줄에는 segments가 없습니다. 로캘 형식은
지역화된 텍스트를 참고하세요.
{ "kind": "music", "family": "staging", "position": "001-0400", "playback": "play", "assetId": "theme", "timing": { "duration": 1.5 }}활성화와 평가
섹션 제목: “활성화와 평가”엔진은 라이브 상태로 만든 중첩 맵을 대상으로 각 CEL 슬롯을 평가합니다.
- 선언된 슬롯으로 가는 경로의 선언된 모든 루트와 중간 맵이 존재합니다.
- 예약되지 않은 슬롯은 유효한 값(쓰기, 시드 또는 기본값)이 있을 때만 존재하며, 값은 선언된 CEL 타입을 유지합니다.
- 예약된 엔진 경로는 항상 예약된 기본값으로 존재합니다. IR이 선언하는 모든 퀘스트에 대해
quest.<id>.state와quest.<id>.failedBy는"unset"이고, 각 목표의quest.<id>.objectives.<oid>.done과.failed는false이며, 모든 로어 엔트리의entry.<id>.read와.everRead도false입니다.
존재 여부에는 has(path)를 사용하세요. is가 있는 <match> 갈래는 유효한 값이 없는
단순 경로에 대해 그 값이 정확히 "unset"일 때만 충족됩니다. 다른 is 값은 확실히
거짓입니다. 다른 갈래는 정상적으로 평가하며, 평가 오류가 나면 해당 갈래는 충족되지 않습니다.
문서마다 아티팩트 하나가 생성됩니다. 프로젝트의 엔진은 명령 스트림을 연결하는 것과 정확히
같은 방식으로 로드하는 모든 아티팩트에서 relations / rules / seedFacts /
entities / enums / prereqEdges를 합집합합니다.
스키마가 그대로인 동안 언어 0.9.0에서 내용이 이동한 필드는 enums가 유일합니다. 이 필드는 항상 저자가 enums: 블록에서 선언하는 도메인을 담아 왔습니다. 콘텐츠 어휘 멤버가 프로젝트가 선언할 대상이 된 이후에는 emotion, action, anchor, mood, volume, musicPlayback, vfxType 도메인도 같은 배열을 통해 전달됩니다. 프로젝트가 이를 인라인으로 선언하거나 uses:/extends:를 통해 선언하면 이를 내보냅니다. 반면 멤버가 플러그인의 enums export에서 오면 프로젝트는 enums를 전혀 내보내지 않습니다. 플러그인 어휘는 문서별 데이터가 아니라 캐퍼빌리티 표면(capabilitySnapshot에 접힘)이기 때문입니다. 어느 경우든 엔진이 이미 합집합하는 데이터이므로 엔진에 새로운 요구 사항은 없습니다 — enums 이동은 필드를 추가하거나 이름을 바꾸거나 옮기지 않았습니다.
이 이동은 IR 형태를 변경하지 않았습니다. (관련 없는 meta.plugin 키는 같은 릴리스에서 추가되었습니다. 변경 이력의 IR 0.10.2에서 변경된 내용을 참고하세요.) 컴파일러 의미를 담은 멤버(action의 exits:, anchor의 default:)는 컴파일 시점에 해석되어 사라지고 직렬화되지 않습니다. 엔진은 런타임에 멤버 의미가 필요하지 않습니다.
버전 협상과 엔진 통합
섹션 제목: “버전 협상과 엔진 통합”모든 호스트는 플레이백 세션을 열기 전에 다음 순서로 협상합니다.
- 정확한 MAJOR.MINOR IR 게이트. 1.0 이전에는 모든 마이너 버전이 실행 IR 형태를 깨뜨릴 수 있으므로, 엔진은 정확한
irVersionmajor.minor 계열을 고정합니다.0.53엔진은0.53.*아티팩트만 받아들이고0.52.*및0.54.*는 거부합니다. 패치 처리는 엔진 정책입니다. 1.0부터는 릴리스된 메이저 버전의 정책을 적용합니다. - 의미 캐퍼빌리티 게이트. 변경할 수 없는
lute.engine.yaml매트릭스를 로드하고 모든 아티팩트의requiredSemanticsID를supportedIds와 비교합니다.engine,irVersion, 고유한supportedIds는 필수이며,version과description은 설명용입니다. 전체 레지스트리는 0.33.0 제안서 §2에 있습니다.
매트릭스 예시는 다음과 같습니다.
engine: chat-text-engineirVersion: "0.53.0"supportedIds: - lute.core/1 - lute.quest.lifecycle/1 - lute.quest.rewards/1 - lute.knowledge.facts/1 - lute.knowledge.rules/1 - lute.knowledge.temporal/1version: "1.4.0"description: "Text-only chat client: no staging, timeline, or clock"lute run --engine <file>과 lute play --engine <file>은 두 게이트를 모두 수행합니다.
ID가 누락되면 플레이백 전에 종료 코드 2와 함께
E-ENGINE-SEMANTICS로 거부합니다. 명령, 조건, 에셋 또는 브리지는 실행되지 않습니다.
형식이 잘못된 매트릭스에는 E-ENGINE-MATRIX를 사용합니다. 매트릭스의 정확한
major.minor 계열을 벗어난 아티팩트(예를 들어 위 0.53.0 매트릭스에 대한
0.41.* 아티팩트)에는 E-ENGINE-IR-VERSION을 사용합니다. 알 수 없는 아티팩트 또는
레지스트리 ID에는 E-SEMANTICS-UNKNOWN을 사용합니다. lute run은 제거된 0.36 필드를
여전히 담은 아티팩트를 E-IR-REMOVED-FIELD로 거부합니다.
lute check --engine <file>과 lute check --project <DIR> --engine <file>은 플레이백 없이
저작 시점의 협상을 수행합니다. 누락된 캐퍼빌리티는 낮춰진 구성의 소스 범위, 의미 ID,
엔진 이름과 함께 E-CHECK-ENGINE-SEMANTICS로 보고됩니다. 프로젝트 형식은 프로젝트 인덱스
합집합을 검사하고 기여한 각 문서/범위를 나열합니다. 플러그인 호환성은 여전히 정확한
capabilitySnapshot 검사로 유지되며 이 매트릭스에 포함되지 않습니다.
참조 실행기의 내장 매트릭스 이름은 reference이며, 현재의 모든 의미 ID를 지원합니다.
Match 갈래 축약형 의미론 (dsl 0.32.0 §7)
섹션 제목: “Match 갈래 축약형 의미론 (dsl 0.32.0 §7)”각 match 갈래에는 test와 target이 있습니다. <when is="…">로 저작된 갈래에는
축약형 텍스트를 담은 의미 IR 필드 is도 있습니다. test= 갈래는 is를 생략합니다.
슬롯의 authored 필드는 진단 전용이며 갈래가 축약형에서 왔는지 추론하는 데 사용해서는
안 됩니다.
match 주제가 유효한 값이 없는 단순 상태 경로일 때 "unset"이 아닌 is를 가진 갈래는
확실히 충족되지 않고, is: "unset"은 충족됩니다. 다른 갈래는 정상적으로 평가하며,
평가 오류가 나면 충족되지 않습니다.
0.41의 project.index.json
섹션 제목: “0.41의 project.index.json”인덱스 형태는 Rust 인덱스 타입에서 생성됩니다. IR 스키마의 projectIndex,
indexDocument, indexEntry, indexBeat, indexOccasion,
indexStateDomain, indexDef, fieldType 정의는 lute-contract-schema가
만들며, 아티팩트 형태(enumEntry, celSlot, exprNode, clockDecl, …)는
다시 정의하지 않고 참조합니다. 바뀐 키는 세 개입니다(spec 0.41.0 §4).
bridgeResults: 각 브리지 결과 필드가"number"/"string"대신occasions[*].payload와 같은 형태의 선언 타입을 가집니다 —"bool","int","double","string",{"enum": [...]},{"domain": "<name>"}, ….enums[].labels: { <member>: "<text>" }: 병합된enums:선언의 긴 형식labels:이며, 인덱스와 아티팩트 행에 똑같이 실립니다. 선언이 없으면 생략됩니다.defs: 플러그인 또는uses:스키마 def마다 이름을 키로 한 행{ cel, expr, authored?, params?, host? }입니다.params는 인자 순서대로[{ name, type }]을 나열하고, 각 파라미터는expr에서{"param": "<name>"}로 나타나며,host: true는 호스트가 읽는 def를 표시합니다. 스칼라가 아닌 파라미터가 있거나, 본문이 낮춰지지 않거나, 두 문서에서 본문이나 파라미터 목록이 다른 def는 빠집니다.param노드는 다른 곳에는 나타나지 않습니다.
0.47부터 인덱스에는 선택적인 contentLang도 들어갑니다. 매니페스트의
defaults.contentLang, 즉 프로젝트 텍스트의 원본 언어이며 매니페스트에 없으면 생략됩니다.
플레이어는 이를 로드한 번들의 원본 로케일로 쓰며, 문서의 meta.contentLang보다 먼저 봅니다.
IR 0.37.0에서 변경된 내용
섹션 제목: “IR 0.37.0에서 변경된 내용”깨끗한 표면 전환: 이름이 바뀐 종류와 필드, 새 종류 하나, 제거된 필드. 0.36 계열의 엔진은 0.37 아티팩트를 거부해야 하며, 이전 이름은 별칭으로도 허용되지 않습니다. 엔진을 0.37로 옮기려면(dsl 0.37.0 §2.2, §5) 다음을 적용하세요.
- 엔벨로프:
capabilityVersion은capabilitySnapshot입니다(프로젝트 인덱스와lute context --json도 같은 키를 사용합니다).shots는sections가 되며, 각 항목은{ section, heading, id? }입니다. 여기서id는 저작된## Heading {#id}접미사입니다. - 모든 레코드: 새 필수
family를 가집니다(명령 레코드 참고). 공통addr은 같은 형식의position이 되고, 평탄화된 타이밍 필드는 하나의timing객체로 이동하며,provenance.injected는 제거됩니다. - 종류:
background는bg,sprite는actor,cut은cg가 되며,sequence가 새로 추가됩니다.music.action은music.playback이 되고,cg.action과video.action은display가 됩니다.cg.full은 도메인 값인layout이 되고,vfx.vfxType은type이 됩니다.music.track과sfx.name은 제거됩니다. 카메라의 숫자 필드zoom/moveX/moveY/shake/reset/easing은 도메인 값인framing,move,transition으로 대체됩니다. - 줄:
monologue,offscreen,voiceover역할은 각각mono,os,vo가 됩니다. 모든 줄에는voiceKey가 들어가며, 인라인 수정자가 있는 줄에는segments가 추가되고 번역에는localeSegments가 추가됩니다. - 제어: choice 및 hub 옵션의
label은text가 되고(로캘 맵은texts),recordKey는selectionKey가 됩니다.
스키마 파일은 lute-ir-0.37.schema.json입니다.
IR 변경 이력
섹션 제목: “IR 변경 이력”이전 계열에서 엔진을 마이그레이션하는 사람을 위해 보존한 이력입니다. 아래 각 항목은 해당 릴리스 자체의 게이트 규칙을 설명합니다(일부는 정확한 major.minor 게이트보다 앞섭니다). 현재 엔진은 위의 버전 협상을 따릅니다.
IR 0.30.0에서 변경된 내용
섹션 제목: “IR 0.30.0에서 변경된 내용”필드가 추가·이름 변경·재타입되지 않았으며, 이제 이름에 -를 포함하거나 숫자로 시작할 수 있습니다. 0.29 아티팩트를 로드하는 엔진은 다음과 같이 경로와 조건을 읽는다면 0.30 아티팩트도 로드할 수 있습니다(dsl 0.30.0 §3).
- 경로 필드는 표준 점 표기 문자열입니다.
set명령의path,state항목의path,expr노드의path/isSet/has,{{…}}플레이스홀더의path는 이름을.으로 연결합니다:quest.zero-coke-001.state,run.visits.lab-b2,run.visits.001. 세그먼트에는-가 포함되거나 숫자로 시작할 수 있지만.은 포함하지 않으므로split('.')으로 세그먼트를 복원할 수 있습니다. 해당 문자열 또는 그 세그먼트에 대해 상태를 조회하고, 이를 CEL로 다시 파싱하지 마세요. - 조건은 작성된 형태로 전달됩니다. 조건의
raw는 작성자가 쓴 CEL이며, 식별자가 아닌 이름은 JavaScript처럼 따옴표 인덱스로 접근합니다:quest["zero-coke-001"].state,run.visits['lab-b2'] >= 2. 이는 중첩 맵에 대한 일반 CEL입니다 —m["k"]와m.k는 같은 항목을 읽으므로, 상태 맵에 대해raw를 평가하는 엔진에는 새로 필요한 것이 없습니다. 이러한 조건의 이식 가능한expr에는 표준 경로가 담깁니다({"path": "run.visits.lab-b2"}). - 팩트 인자는 이름입니다.
raw의holds('at', ["lab-b2"])는assert명령이{"relation": "at", "args": ["lab-b2"]}로 쓰는 팩트를 묻습니다. 따옴표는 조건의 표기일 뿐 이름의 일부가 아닙니다. 시드 팩트와 규칙 헤드·본문의 구조화된 항도 같은 방식으로 따옴표 없는 이름을 담습니다.
스키마 파일은 릴리스 계열마다 이름이 변경됩니다: lute-ir-0.29.schema.json은 이제 lute-ir-0.30.schema.json이며, 이름 패턴(season, occasion, share 키, series, 문서 id)은 -가 포함되거나 숫자로 시작하는 이름을 허용합니다.
IR 0.29.0에서 변경된 내용
섹션 제목: “IR 0.29.0에서 변경된 내용”선택적 필드 하나이며, 이름 변경은 없습니다. 새 명령 kind는 없고 필드가 이름 변경·재타입·삭제되지 않았으므로, MAJOR 전용 게이트 아래에서 0.28 아티팩트를 로드하는 엔진은 0.29 아티팩트도 변경 없이 로드합니다(dsl 0.29.0 §5).
- 아티팩트와
project.index.json의terminalPersists: true: 모든terminal:선언이persists: true를 포함한 긴 형식일 때terminal뒤에 직렬화됩니다. 종료 상태는 의도적으로 런을 넘어 유지됩니다(로그라이크의 영구 엔딩):terminal이 성립하면 이 런이나 이후 런에서 계기를 발생시키지 않습니다 — 새 런이 게임을 다시 열지 않습니다(지속될 수 있는 조건은 새 런에서도 유지되는 상태를 읽으므로 여전히 성립할 수 있습니다). 거짓이면 생략됩니다.
스키마 파일은 릴리스 계열마다 이름이 변경됩니다: lute-ir-0.28.schema.json은 이제 lute-ir-0.29.schema.json입니다.
IR 0.28.0에서 변경된 내용
섹션 제목: “IR 0.28.0에서 변경된 내용”퀘스트 계층 필드 세 개의 이름이 변경되었고 나머지는 모두 추가되었습니다. 새 명령 kind는 없고 필드가 재타입·삭제되지 않았으므로, MAJOR 전용 게이트 아래에서 0.27 아티팩트를 로드하는 엔진은 0.28 아티팩트도 로드합니다. — 단, 이름이 변경된 필드를 읽는 엔진은 새 이름으로 읽어야 합니다(dsl 0.28.0 §6).
ObjectiveEntry.visibleWhen(이전when): 가시성만 제어합니다 — 성립하는 동안 목표를 표시하며done을 제한하지 않습니다.RewardEntry.outcome(이전on): 퀘스트 수준 보상에서 새로운failed전환이 발생할 때만 지급되며, 값은 오직"failed"입니다.- 퀘스트의
prereqEdges행은follows(이전after)를 담습니다: 퀘스트 그래프 메타데이터이며 퀘스트를 제한하지 않습니다(start가 제한합니다). 스크립트 안의 accept도 제한하지 않으며,accept="external"퀘스트에서는 호스트가 부여하는 조건입니다(호스트가 적용하고 Lute는 강제하지 않습니다). 씬과 번들 비트 행은after를 유지합니다.
새 동작과 선택적 필드는 다음과 같으며, 작성되지 않았으면 각각 생략됩니다.
- 아티팩트와
project.index.json의outsideRun: [occasion](이름순):terminal이 성립한 뒤에도 이 계기들을 발생시킵니다(타이틀 화면, 런 사이의 갤러리). 게임이 끝나면 나머지 모든 계기는 닫힌 상태로 유지됩니다. HubCmd.return: 허브의<return>세그먼트 위치입니다. 종료가 아닌 옵션의 세그먼트가 끝날 때마다 허브를 다시 판정하고 제시하기 전에 실행합니다 — 첫 메뉴 전이나exit옵션 뒤에는 실행하지 않습니다. 옵션 대상과 마찬가지로 그 앞의 세그먼트 범위를 정합니다.clock.raiseAtStart: true: 런의 첫 위치에서 시계 슬롯 계기(문자열 형식이면 그 문자열, 맵이면slot)와 맵의dayStart를 직접 발생시킵니다. 이 위치에서는 advance가 중지되지 않습니다.false이면 생략됩니다.- 예약된 시계 읽기 값은
clock.day와clock.slot(시계의 day 및 slot 경로 별칭)이며, 유한 시계에서는clock.ended도 제공합니다: 시계를 끝내는 advance 전에는false, 해당 advance의 정착부터 새 런이 런 계층 시계를 다시 시작할 때까지true입니다. labelForms: { <member>: { start?, indefinite? } }는entities[]와state[]항목에 제공되며, 플레이스홀더 형식cardinalWord(one…twenty, 그 이상은 숫자),capitalize,start,indefinite도reserved와occasionTarget플레이스홀더에 제공됩니다.capitalize는 렌더링된 텍스트의 첫 글자를 대문자로 만듭니다.start는 멤버의start형식을 렌더링하고, 없으면 대문자로 시작합니다.indefinite는 멤버의indefinite형식을 렌더링하고, 없으면 첫 글자에 따라a/an, 공백, 텍스트를 사용합니다.plural형식에서는#word/#Word가 (대문자로 시작하는) 수사로서의 숫자입니다.- 관계
tier: "season:<name>": 해당 관계의 팩트는 그 시즌이 열릴 때마다 시드 팩트로 되돌아갑니다. authored:@def확장이 seam 조건(gates[].raisedWhen,terminal,seasons[].live)을 변경했을 때cel옆에 표시됩니다. 메시지용일 뿐이며cel을 평가합니다.spentBy래치: 이제spentBy비트의once는"none"이 아니라 해당 비트의 기간(작성되지 않으면run)입니다 — 이러한 비트는 제시 시 소진하지 않습니다(spentBy아래 참조).forKind비트는 멤버마다once를 소진합니다: 각 멤버의 제시가 별도로 계산되며,for항목의 첫 읽기 쓰기는 런에서 각 멤버의 첫 읽기에 적용됩니다.occasion.target을 통한 쓰기: 아래의occasion.target을 통한 쓰기를 참조하세요.- seam이 결정되는 시점: 계기가 발생할 때
terminal과 계기의raisedWhen을 판정합니다.terminal을 성립시키는judge: before판정은 해당 계기 자체의 비트를 닫지 않습니다.select: sequence발생은 앞선 비트가 재생된 뒤, 각 비트의 차례 직전에 다시 판정합니다.
IR 0.27.0에서 변경된 내용
섹션 제목: “IR 0.27.0에서 변경된 내용”필드가 추가되고 기존 필드에 새 값이 추가되었습니다. 필드의 이름 변경·재타입·삭제는 없고 새 명령 kind도 없으므로, MAJOR 전용 게이트 아래에서 0.26 아티팩트를 로드하는 엔진은 0.27 아티팩트도 로드합니다. 주기는 다음과 같습니다(dsl 0.27.0 §5).
once: "week":BeatIr(씬의meta.beat),BeatCmd,EntryCmd,project.index.json비트 행에 사용되며, 제시 시점부터 다음 시계 주가 시작될 때까지 소진됩니다. 주 n은(day - 1) div week.length가 n인 날을 포함하므로clock.weekday가week.first로 돌아올 때 주가 시작됩니다. 시계가week:를 선언한 경우에만 출력됩니다.once: "season:<name>": 같은 레코드에 사용되며, 제시 시점부터<name>시즌이 다시 열릴 때까지 소진됩니다.spentBy: {cel, expr, authored?}:BeatIr,BeatCmd,EntryCmd에 제공됩니다(index 비트 행에는raw만 제공). 비트가 제시되어서가 아니라 이 조건으로 소진됩니다. 모든 퀘스트 정착 시(종류 또는for비트의 멤버별,occasion.target바인딩 시)와 비트 판정 시 관찰합니다. 한 번 성립하면once기간 동안 비트가 소진됩니다 —once가run이거나 없으면run(새 런이 초기화),user이면 절대 소진되지 않고,day/slot/week이면 처음 성립한 기간에 시계가 머무는 동안,season:<name>이면 해당 시즌이 다시 열릴 때까지 유지됩니다 — 조건이 거짓으로 바뀌어도 마찬가지입니다.spentBy비트를 제시해도 아무것도 소진되지 않습니다. 작성되지 않았으면 생략됩니다.QuestCmd.rearm: {cel, expr, authored?}: 플레이스루의 모든 퀘스트 정착 시 관찰하며, 첫 관찰은 기준선입니다. false→true가 될 때마다 퀘스트를unset으로 되돌립니다(완료되지 않은 목표, 삭제된failedBy, 잊힌 기한). 성립하는start는 같은 정착에서 퀘스트를 활성화합니다. 작성되지 않았으면 생략됩니다.QuestCmd.tier: "season:<name>": 시즌이 다시 열릴 때 퀘스트를unset으로 되돌리며, 새 런에서는"run"퀘스트가 됩니다.- 아티팩트(
clock,gates,terminal뒤)와project.index.json의seasons: [{ name, live: {cel, expr, authored?} }]:live는@def확장 이후의 값입니다. 시즌의live가 false→true가 되면 모든season.<name>.*값을prev.season.<name>.*에 복사하고,season.<name>.*을 선언된 기본값으로 초기화하며,once: "season:<name>"비트의 제시 기록을 지우고, 해당 시즌 계층 퀘스트를unset으로 되돌립니다. 시즌은 서로 독립적이며 겹칠 수 있습니다.prev.season.*은prev.run.*처럼 읽기 전용 미러입니다.
텍스트(dsl 0.27.0 §7):
entities[].labels: { <member>: "<text>" }: 종류 멤버의 표시 텍스트이며, 공유 멤버에는 하위 종류와 조상의 레이블을 사용합니다.entityKindK의occasionTarget플레이스홀더는 K의labels[member]로 렌더링하고, 멤버가 cast id이면 cast의name:을, 그 외에는 id를 사용합니다.{ domain: K }또는{ entity: K }상태 경로는 해당state[].labels와 같은 맵을 전달하며,path플레이스홀더는 이미 이를 렌더링합니다. 없으면 생략됩니다.forms: [one, other]를 사용하는 플레이스홀더format: "plural": 수가 1이면one, 그 외에는other를 렌더링하며, 각 형식의#는 숫자로 치환됩니다({{n:plural(# lamp|# lamps)}}→3 lamps). 현지화 엔진은 두 영어 형식에서 자체 복수 범주를 선택할 수 있습니다.
유한 시계(dsl 0.27.0 §4):
clock.last: { day, slot? }또는clock.days: N은 선언된 그대로입니다(days: N은last: { day: N }이며slot이 없으면 해당 날의 마지막 슬롯). 시계는 그 위치에서 멈춥니다. 목적지가 이를 지나가는 advance는 마지막 위치로만 이동하고(그 과정에서 통과하는 매 자정마다dayEnd/dayStart를 발생시킴), 마지막 날의dayEnd를 한 번 발생시키며slot계기는 발생시키지 않고 시계를 종료합니다 — 새 런이 day 및 slot 경로를 초기화할 때까지 더 advance하지 않습니다. 절대 종료되지 않는 시계에서는 둘 다 생략됩니다.
엔진 seam(dsl 0.27.0 §4):
gates: [{ occasion, raisedWhen: { cel, expr, authored? } }](최상위, 계기순,@def확장): 게이트가 성립하는 동안에만occasion을 발생시키며,occasion.target을 발생 대상 멤버로 읽습니다(room.office→office). 시계가 만드는 발생(raise.slot,dayStart,dayEnd)의 게이트가 거짓이면 발생하지 않습니다. 시계는 계속 이동합니다. 검사기는 게이트 아래에서 해당 계기의 모든 비트를 판정했으므로, 절대 제시하지 않을 비트도 작성자에게 보고합니다. 어떤 계기도 게이트를 선언하지 않으면 생략됩니다.terminal: { cel, expr, authored? }(최상위): 성립하는 순간 게임이 종료되며 새 런이 시작될 때까지 어떤 계기도 발생시키지 않고 시계도 advance하지 않습니다. 여러 스키마의 선언은||로 결합됩니다. 선언이 없으면 생략됩니다.- 디렉티브 팩트 효과:
kind: "plugin"레코드에는retracts와asserts가 각각[{ relation, args }]형식으로 포함될 수 있으며, 호출의 속성은 이미 치환되어 있습니다(_는 무엇이든 일치). 레코드 자체의 쓰기 뒤에 이를 적용하고, retract를 먼저 실행하며, 일반 assert/retract 경로를 사용합니다(reserved관계도 포함하며, 쓰기는 엔진 자체의 쓰기입니다). 비어 있으면 생략됩니다. 로어 항목의 효과 전용 디렉티브는 항목의set레코드처럼 항목을 처음 읽을 때만 적용됩니다.
계기에 의해 바인딩되는 멤버(dsl 0.27.0 §3):
- 패턴 인자 및 패밀리 인덱스로서의
occasion.target: 종류 비트(및forKind비트)의 CEL에서 팩트 질의 패턴은occasion.target을 인자로 받을 수 있고(holds('owned', [occasion.target])),per:패밀리는 이를 인덱스로 사용할 수 있습니다(user.bond[occasion.target]). 바인딩된 멤버로 치환합니다. 패턴 인자는 해당 멤버 id가 되고,F[occasion.target]은F.<member>를 읽습니다. 검사기는 모든 멤버 인스턴스가 올바른 타입임을 입증했습니다. occasion.target을 통한 쓰기(dsl 0.28.0 §3): 같은 비트에서set레코드의path는[occasion.target]으로 끝날 수 있고(run.count[occasion.target]:run.count.<member>에 쓰기),assert/retract인자와plugin레코드의 필드 값, 효과 경로 인덱스(effects[].path), 팩트 인자(asserts/retracts)도occasion.target이 될 수 있습니다. 읽기와 동일하게 쓰기를 적용하기 전에 바인딩된 멤버로 치환합니다. 컴포넌트 인자는 이런 방식으로 전달되지 않습니다.occasion.target의 멤버마다 하나의matcharm으로 컴파일됩니다.forKind: { kind, members }:BeatIr(씬의meta.beat),BeatCmd,EntryCmd,project.index.json비트 행에 제공됩니다. 대상 없는select: sequence계기에 목록 순서대로 멤버마다 한 번 응답합니다. 발생 시 각 멤버의 자격을occasion.target에 해당 멤버를 바인딩한 채 판정하고, 자격 있는 각 멤버마다 한 번씩 제시하며 실행 중에도occasion.target을 바인딩합니다.once는 멤버가 아니라 비트를 소진합니다. 작성되지 않았으면 생략됩니다.- 계기 페이로드:
payload: { <field>: <type> }로 선언한 계기는 각 발생에 타입이 지정된 값을 전달합니다. 비트를 판정하기 전에occasion.payload.<field>를 발생 값에 바인딩하고, 비트가 제시된 뒤 바인딩을 해제합니다. 페이로드는 한 번의 발생 동안만 유지됩니다. 아티팩트의 CEL과path플레이스홀더는 상태처럼 이를 읽습니다(occasion.payload.copies).
IR 0.22.0에서 변경된 내용
섹션 제목: “IR 0.22.0에서 변경된 내용”추가 필드 두 개, 새 예약 플래그 하나, 확장된 계기 선언, 새 identity 값이 추가되었습니다. 필드의 이름 변경·재타입·삭제는 없고 새 명령 kind도 없으므로, MAJOR 전용 게이트 아래에서 0.21 아티팩트를 로드하는 엔진은 0.22 아티팩트도 로드합니다. 새 필드를 무시하는 엔진은 0.21 의미론으로 실행합니다. 즉 모든 퀘스트가 런을 넘어 유지되고 모든 항목 비트가 반복됩니다. 작성된 대로 실행하려면 다음을 구현하세요.
QuestCmd.tier는<quest tier="run">에서"run"이고 기본user계층에서는 생략됩니다. 하나의 퀘스트 문서가 여러 퀘스트를 선언할 수 있으므로meta가 아니라 각quest레코드에 포함됩니다. 런이 시작되면 모든 런 계층 퀘스트를 완료되지 않은 목표와 함께unset으로 되돌린 뒤, 어떤 평가 시점에서든 생명주기를 정착합니다. 사용자 계층 퀘스트는 상태를 유지합니다. quest-lifecycle.md를 참조하세요.EntryCmd.once는 이를 작성한 항목 비트에서"run"또는"user"이며, 그 외에는 생략됩니다(0.21처럼 반복 가능).ProjectIndex.beats항목 행에도 같은 값이 포함되며, 씬 행에는 항상once가 있었습니다."run"항목 비트는entry.<id>.read가 true인 동안 자격이 없고,"user"항목 비트는entry.<id>.everRead가 true인 뒤 자격이 없습니다.entry.<id>.everRead는 항목마다 새로 추가되는 예약 사용자 계층bool이며 기본값은false입니다. 첫 읽기 후entry.<id>.read를 설정할 때마다 함께 설정하고, 런 시작 시 재설정하지 않습니다. 아티팩트의state표에는 나열되지 않지만(entry.<id>.read는entry:<id>provenance와 함께 나열됨), 콘텐츠는 raw CEL과exprAST({ "path": "entry.<id>.everRead" })에서 어떤 경로처럼 읽습니다. 로드하는 각 항목에 대해 하나를 유지하세요.- 계기 대상 도메인: capability snapshot에서 계기의
target은false,true, 또는{ "prefix", "entity" }입니다. 도메인의 경우 검사기는 해당 계기의 모든 비트 대상이 프로젝트 엔티티 종류entity의<prefix>.<member>임을 보장하므로, 그 형태의 대상으로 계기를 발생시키세요. 계기 선언은 아티팩트에 포함되지 않습니다. 도메인은capabilitySnapshot을 변경하며true와false는 0.21 스탬프를 유지합니다. owner: engine:state:선언은 해당 경로의 작성자로 엔진을 지정할 수 있습니다. 검사기는 그 경로 또는 그 아래 필드에 대한 콘텐츠::set을 거부하므로(E-ENGINE-OWNED-WRITE), 정상 아티팩트의set레코드는 이를 대상으로 하지 않습니다. 엔진이 씁니다. 해당 경로의 아티팩트state항목은 변하지 않습니다(path, type, default).lute context --json은 소유자를"owner": "engine"으로 보고합니다.- Identity 값이 이동합니다. 기본
voiceKey템플릿은{prefix}.{speaker}-{code}입니다(이전에는{speaker}-{code}). 컴포넌트에서 확장된 줄은{prefix}.{component}#{n}아래에서 생성되며,n은 호스트가 해당 컴포넌트에::use한 횟수를 1부터 셉니다. 따라서 기본 템플릿에서lineId는{prefix}.{component}#{n}.{speaker}_{code},voiceKey는{prefix}.{component}#{n}.{speaker}-{code}이고 중첩된::use는 세그먼트를 하나 더 추가합니다. 필드 형태는 변하지 않았지만 값이 달라지므로 0.21 id를 키로 한 음성 및 현지화 표를 다시 만드세요. 이전 키로 녹음한 오디오가 있는 프로젝트는identity: { voiceKey: "{speaker}-{code}" }를 고정합니다(Frontmatter & profiles 참조).
IR 0.10.2에서 변경된 내용
섹션 제목: “IR 0.10.2에서 변경된 내용”추가 필드 하나이며 필드의 이름 변경·재타입·삭제는 없습니다. meta(sceneMeta와 questMeta 모두)에 plugin이 추가됩니다. 이는 플러그인이 소유하고 검사기가 검증한 최상위 frontmatter 키이며 { "<key>": <json value> }로 접습니다. 문서에서 플러그인 소유 키를 작성하지 않았으면 완전히 생략됩니다.
{ "meta": { "character": "marina", "season": 1, "episode": 2, "episodeId": "s01ep02", "plugin": { "cast": { "ana": { "costume": "school" } } } } }키는 활성 플러그인이 선언했고 작성된 값이 해당 플러그인의 선언된 스키마를 독립적으로 통과할 때만 존재합니다 — 이는 검사기가 이미 E-FRONTMATTER-SCHEMA 적용 여부를 판단하는 데 사용하는 것과 같은 조건이므로 검사기가 거부할 값은 meta.plugin에 도달할 수 없습니다. 규범적 소속 규칙은 plugin-system 0.0.4를 참조하세요.
알 수 없는 필드 무시 규칙에 따라 게이트는 넓어지지 않습니다. 0.10.2는 0.10.0/0.10.1과 major.minor 0.10을 공유하고 plugin은 진정으로 새로 추가된 객체 키이므로, 이미 0.10.x 아티팩트를 받아들이고 읽지 않는 키를 무시하는 엔진은 변경 없이 계속 작동합니다. 단순히 파싱하는 것이 아니라 공개 JSON Schema를 엄격히 검증한다면, lute-ir-0.10.schema.json의 sceneMeta/questMeta는 이제 plugin을 타입이 지정된 속성으로 허용합니다(그 외에는 여전히 additionalProperties: false). 게이트된 major.minor가 이동하지 않았으므로 파일 이름은 유지됩니다.
IR 0.10.1에서 변경된 내용
섹션 제목: “IR 0.10.1에서 변경된 내용”아직 0.10.1 계열을 사용하는 사람을 위해 보존한 이력입니다. 형태에는 아무것도 없습니다. IR 0.10.1은 IR 0.10.0과 형태가 동일했습니다 — 필드의 추가·이름 변경·이동·재타입이 없고 새 명령 kind도 없습니다. 릴리스가 보이는 모든 축을 다시 정렬했기 때문에 숫자만 이동했으며, 0.10.1을 받아들이도록 게이트를 넓히는 데는 0.10.0이 이미 요구한 것 외에는 비용이 없었습니다. 둘 다 major.minor 0.10을 공유하기 때문입니다.
IR 0.10.0에서 변경된 내용
섹션 제목: “IR 0.10.0에서 변경된 내용”아직 0.10.0 계열을 사용하는 사람을 위해 보존한 이력입니다. 필드 이름 하나가 변경되었으며, 0.8.0 이후 첫 형태 변경입니다. 주입 provenance 스탬프의 reason이 explanation이 됩니다.
{ "by": "auto-pose-reset", "explanation": "pre-loading `vesna`'s first emotion `level` seen ahead of the entrance" }이전 이름은 동의어가 아니라 충돌이었습니다. end.reason은 작성자가 쓰고 그 값으로 분기하는 불투명한 작성자 토큰입니다. 반면 이 필드는 컴파일러가 작성한 사람이 읽을 수 있는 영어로, 합성한 레코드가 존재하는 이유를 설명하며 이 값을 기준으로 디스패치하지 않습니다. 이름만 같고 나머지는 전혀 공통점이 없는 두 키가 바로 이름 변경으로 제거할 함정이며, explanation은 by 옆에서 올바르게 읽힙니다.
형태에서 달라지는 다른 것은 없습니다. 필드 추가·재타입, 새 명령 kind, 제약 변경은 없습니다.
위 게이트가 규범적이며, 이번 버전 상승에는 편집이 하나가 아니라 두 개 필요합니다. IR 0.9를 구현하는 엔진은 0.10이 더 새로운 major.minor이므로 0.10.0으로 표시된 아티팩트를 거부해야 합니다. 변경 사항은 다음과 같습니다.
게이트를 넓혀
0.10을 허용하고, 주입 provenance 스탬프를 읽는다면reason을explanation으로 이름 변경하세요. 그 외에는 없습니다 — 디스패치할 새kind도, 동작 차이도 없습니다.
JSON Schema에 대해 검증한다면 lute-ir-0.10.schema.json을 가리키세요. 0.9 파일은 함께 보존됩니다.
아티팩트 내용도 비용 없이 변경됩니다. clip의 at, duration, delay, barrier의 at은 여전히 초 단위 JSON 숫자이지만 이제 정수 밀리초에서 계산되므로, 커서에서 파생된 1.2가 1.2000000000000002로 직렬화되지 않습니다. capability snapshot도 변경됩니다 — W-INJECT-CONFLICT가 코드 집합에서 빠지고 11개 코드가 추가되었습니다.
IR 0.9.0에서 변경된 내용
섹션 제목: “IR 0.9.0에서 변경된 내용”아직 0.9 계열을 사용하는 사람을 위해 보존한 이력입니다. 형태에는 아무것도 없습니다. IR 0.9.0은 IR 0.8.0과 형태가 동일했습니다 — 필드의 추가·이름 변경·이동·재타입이 없고 새 명령 kind도 없습니다. Lute의 버전 정책이 모든 릴리스에서 보이는 모든 축의 번호를 다시 정렬하기 때문에 숫자만 이동했으며, 게이트를 넓혀 0.9를 허용하는 것이 전체 마이그레이션이었습니다. 실제로 이동한 것은 아티팩트 내용입니다: enums가 프로젝트 콘텐츠 어휘 도메인을 담기 시작했고(엔벨로프 참조), core의 어휘가 비면서 capability snapshot이 변경되었습니다. 둘 다 이미 존재하던 필드의 새 값입니다.
IR 0.8.0에서 변경된 내용
섹션 제목: “IR 0.8.0에서 변경된 내용”아직 0.8 계열을 사용하는 사람을 위해 보존한 이력입니다. 스키마 파일은 minor 상승과 함께 schemas/lute-ir-0.7.schema.json → schemas/lute-ir-0.8.schema.json으로 이름이 변경되었습니다. 소비자에게 중요한 변경은 세 가지입니다.
end는 새 명령kind입니다. 알 수 없는 kind 규칙에 따라 IR 0.7만 구현한 엔진은 이를 포함한 아티팩트를 거부해야 합니다 — 레코드를 통과시킬 수 없습니다.end는 순회를 종료하는데 통과시키면 작성자가 도달 불가로 표시한 콘텐츠를 실행하게 되기 때문입니다. 이것이 종료가 플러그인 디렉티브가 아니라 core kind인 이유입니다. 플러그인 디렉티브는kind: "plugin"으로 낮춰지므로 이전 엔진이 아무 문제 없이 건너뛸 수 있습니다.- 섹션 제목 표와 로케일 맵은 추가 전용 선택 필드이므로, 알 수 없는 필드 무시 규칙에 따라
end레코드가 없는 0.8 아티팩트는 0.7 엔진도 여전히 로드합니다. - 위치 폭 불변식이 새로 생겼으며, 이미 소비 중인 아티팩트의 바이트를 바꿀 수 있는 유일한 변경입니다. 아래를 참조하세요.
모든 명령 레코드는 position을 가지며, 그 값은 "{section}-{(index + 1) * 100}"(예: "001-0300")입니다. 앞부분은 1부터 세는 ## 섹션 번호이고, 뒷부분은 해당 섹션 안에서 레코드의 순서입니다. 이 값은 컴파일할 때마다 다시 생성되며, 빌드에 한정된 실행 위치이지 식별자, 현지화 키 또는 세이브 키가 아닙니다. 안정적인 콘텐츠 결합에는 lineId / voiceKey를 사용하고, 섹션의 안정적인 식별자는 sections에 있는 선택적 id입니다.
섹션은 표시와 식별을 위한 것이며 런타임 경계가 아닙니다. 런타임은 씬의 섹션들을 하나의 range로 실행하므로, 섹션 재개 지점도, 호스트가 지정할 수 있는 섹션 진입점도, 섹션 점프 대상도 없으며 한정된 루프는 섹션에 걸칠 수 있습니다. 호스트가 라우팅하는 콘텐츠는 섹션으로 들어가는 것이 아니라 스크립트가 <match>하는 브리지 결과입니다.
두 부분 모두 문서에서 계산한 너비로 0을 채웁니다. 섹션은 최소 3, 인덱스는 최소 4이며, 문서에 더 큰 값이 필요하면 너비가 늘어납니다. 이 너비는 아티팩트 전체에서 균일합니다. 따라서 다음 보장을 신뢰할 수 있습니다.
하나의 아티팩트 안에서 모든
position의 길이는 같습니다. 따라서position의 사전식 순서는 실행 순서와 같습니다.
위치를 전달하는 필드는 정확히 다음과 같습니다. 모든 명령의 position; choice.converge와 choice.options[].target; hub.converge, hub.return, hub.options[].target; match.converge, match.otherwise, match.arms[].target; jump.target; quest.objectives[].body(빈 본문이면 null); 그리고 on.body, entry.body, beat.body입니다.
스트리밍 스냅숏 교체
섹션 제목: “스트리밍 스냅숏 교체”lute compile --stream은 승인된 연속 입력 단위마다 완전한 일반 아티팩트 스냅숏을 내보냅니다. 두 번째 IR이나 패치 언어를 정의하지 않습니다. appendFrom은 이전 스냅숏의 명령 수이며, 새로 추가된 배열 영역만 식별합니다. 런타임 PC가 아니며 그 영역의 모든 레코드를 실행하라고 요청하는 값도 아닙니다.
스냅숏이 도착하면 불변 프로그램 이미지를 교체하고 position -> command index 맵을 다시 만듭니다. 숫자 명령 커서, 현재 상태 값, 팩트, 선택된 제어 흐름 스택, 호스트가 소유한 효과/멱등성 기록은 유지합니다. 새로 선언된 상태 슬롯만 초기화합니다. 아티팩트 객체가 교체되었다는 이유만으로 기존 기본값이나 시드 팩트를 다시 적용해서는 안 됩니다.
명령을 추가하면 아티팩트 전체에서 균일한 위치 padding이 넓어질 수 있으므로 위치 조회를 다시 만드는 것이 필수입니다. 컴파일러는 타입이 있는 위치와 제어 대상을 숫자 (section, index) 의미로 비교하므로 이 서식만의 변경은 허용합니다. 이미 내보낸 명령이나 상태 엔트리에 의미상 변경이 있으면 E-STREAM-PREFIX-CHANGED로 거부합니다.
일반 디스패처는 여전히 branch, match, hub, jump 경로를 선택합니다. 현재 스트림 프런티어에 도달하면 장면이 완료되었다고 표시하지 말고 다른 스냅숏을 기다립니다. 완료는 컴파일러 EOF finish의 성공 또는 저자가 작성한 일반 end 명령에 뒤따릅니다. EOF와 end는 서로 다릅니다. end는 stdin이 닫히기 전에 런타임 실행을 멈출 수 있지만, EOF는 end를 합성하지 않습니다.
디스패처
섹션 제목: “디스패처”commands 배열은 이미 실행 순서입니다. 제어 흐름 필드인 jump.target, choice/hub 옵션의 target과 converge, match arm의 target/otherwise/converge는 모두 위치입니다. position → index 맵을 통해 프로그램 카운터로 순회하면서 family와 kind에 따라 디스패치합니다.
// Every CEL slot carries its verbatim source under its own key — `option.when`,// `arm.test`, `set.value` — and the lowered `expr` AST ONLY when that CEL is// inside the closed §8.4 profile. A relational fact query or a `visited()` read// carries raw text alone.const evalSlot = (raw, expr, state, facts) => expr !== undefined ? evalExpr(expr, state) : evalCel(raw, state, facts);
const index = new Map(artifact.commands.map((c, i) => [c.position, i]));let pc = 0;while (pc < artifact.commands.length) { const cmd = artifact.commands[pc]; let next: string | null = null; // null ⇒ fall through to pc + 1
switch (cmd.kind) { // family "content" case "line": present(cmd, state); break; // placeholders; segments when present // family "staging": opaque domain members; honor cmd.timing case "bg": case "music": case "sfx": case "vfx": case "actor": case "camera": case "cg": case "video": case "sequence": stage(cmd, cmd.timing); break;
// family "state" case "set": writeState(state, cmd.path, cmd.op, evalSlot(cmd.value, cmd.expr, state, facts)); break; case "assert": facts.assert(cmd.relation, cmd.args); break; case "retract": facts.retract(cmd.relation, cmd.args); break;
// family "control" case "choice": case "hub": { const opt = pickOption(cmd, state); // per option: evalSlot(o.when, o.expr, …) next = opt ? opt.target : cmd.converge; break; } case "match": { const arm = cmd.arms.find(a => truthy(evalSlot(a.test, a.expr, state, facts))); next = arm ? arm.target : (cmd.otherwise ?? cmd.converge); break; } case "jump": next = cmd.target; break; case "end": finish(cmd.reason); return; // terminates the walk case "barrier": joinTimeline(cmd.timeline, cmd.at); break;
// family "declaration" and family "plugin" case "quest": registerQuest(cmd); break; case "on": registerHandler(cmd); break; case "accept": acceptQuest(cmd.quest); break; // activates it iff still `unset` case "plugin": callBridgeAndApplyEffects(cmd, state); break;
default: throw new UnknownCommandKind(cmd.kind); // version gate: hard error }
pc = next === null ? pc + 1 : index.get(next)!;}0.51부터 위의 jump case는 제한 없는 형태입니다. max가 있는 jump는 walk별 카운터를 유지하며 카운터가 max 미만일 때만 jump하고 아니면 다음으로 넘어갑니다(next = null). 0.51 추가 사항을 참고하세요.
전체 장면 및 퀘스트 명령 집합은 스물세 가지 종류입니다: line, bg, music, sfx, vfx, actor, camera, cg, video, sequence, set, assert, retract, choice, match, hub, jump, end, barrier, quest, on, accept, plugin. 로어 아티팩트에는 entry와 beat 선언 헤드도 추가되며, 엔진은 이를 재생하지 않고 조회합니다(로어 엔트리).
accept (dsl 0.21.0)는 씬 쪽의 ::accept{quest}입니다. 엔진은 해당 accept 방식 퀘스트의 상태가 여전히 unset이면 활성화하고, 그렇지 않으면 레코드를 무시합니다(퀘스트와 씬).
end에는 선택적인 자유 형식 reason이 있습니다. 저자가 지정하는 문자열("completed", 결말 id)이며, Lute는 여기에 의미를 부여하지 않고 호스트가 표시할 수 있습니다. 이를 만나 종료하는 동작은 commands의 끝까지 실행한 것과 같지만, reason을 사용할 수 있습니다.
현지화 텍스트
섹션 제목: “현지화 텍스트”line 레코드의 text, choice/hub 옵션의 text, branch/hub의 prompt는 항상 원본 언어(contentLang)의 일반 텍스트입니다. 아티팩트를 lute compile --locale-bundle로 빌드하면 레코드에 locale 태그 → 번역된 일반 텍스트인 texts 맵도 들어가며, 키는 레코드의 lineId입니다. prompt가 있는 choice/hub 레코드에는 promptLineId({prefix}.{branchOrHubId}.prompt, prompt가 있으면 항상 존재, dsl 0.47.0)와, 번들이 번역했다면 promptTexts(locale 태그 → 번역된 프롬프트)도 들어갑니다. 인라인 수정자가 있는 line에는 원본용 segments와 각 번역 locale용 localeSegments가 일반 texts 항목 옆에 들어갑니다. 모든 번역 locale에는 항상 texts 항목이 있으므로 backlog, TTS, 검색에서 세그먼트를 평탄화할 필요가 없습니다.
{ "kind": "line", "family": "content", "position": "001-0200", "role": "narration", "speaker": "narrator", "text": "Wait for it here it comes.", "lineId": "opening.narrator_0020", "voiceKey": "opening.narrator-0020", "segments": [ { "text": "Wait for it" }, { "pause": 0.5 }, { "text": " " }, { "text": "here it comes", "rate": 1.25 }, { "text": "." } ], "texts": { "ja-JP": "待って 来るよ。" }, "localeSegments": { "ja-JP": [ { "text": "待って" }, { "pause": 0.5 }, { "text": " " }, { "text": "来るよ", "rate": 1.25 }, { "text": "。" } ] }}locale 맵은 비어 있으면 생략됩니다. 따라서 --locale-bundle 없이 컴파일한 아티팩트는 이전과 바이트 단위로 동일하고, 이를 무시하는 소비자는 계속 원본 언어를 렌더링합니다. texts(수정된 line이면 localeSegments도, 프롬프트는 promptTexts)에서 locale을 조회해 제시하고 text(prompt)로 대체하십시오. 컴파일러는 빌드 시 정확히 그런 누락에 대해 경고(W-L10N-MISSING)하므로 완전한 번들에는 대체할 부분이 남지 않습니다.
런타임의 line 레코드(--locale-bundle로 컴파일한 번들에 대한 lute-runtime / @lute-lang/runtime 이벤트)는 이 맵을 렌더링해 줍니다(dsl 0.47.0). {{…}} 마커를 값으로 바꾼 text와 segments에 더해, line 명령에 texts와 localeSegments가 있으면 이를 함께 싣는데, 각 번역의 마커는 원본에서 참조 대상이 같은 마커({{ }} 안의 텍스트를 앞뒤 공백을 빼고 비교하므로 {{ user.voyages }}와 {{user.voyages}}는 같음)에 대해 렌더링한 값으로 바뀝니다. 번역자는 마커 순서를 바꿔도 되고, 원본에 없는 마커는 쓴 그대로 남습니다. 레코드는 디코딩된 텍스트를 담으며(lute 0.47.1: 인코딩 백슬래시 없음), 번역에도 \{{를 쓸 수 있고 원본과 마찬가지로 리터럴 {{로 남습니다. 런타임 line은 아티팩트의 템플릿이 아니라 레코드로 보여 주십시오. 컨포먼스 레코드인 lute run --json은 다른 delivery 필드처럼 이를 생략합니다. 0.52부터 선택지 라벨과 프롬프트도 같은 방식으로 렌더링됩니다. awaitChoice 메뉴가 각 옵션의 text와 texts, 메뉴의 prompt와 promptTexts를 담습니다(0.52 추가 사항 참고).
런타임 문서
섹션 제목: “런타임 문서”각 표면에는 docs/runtime/ 아래에 자체 계약 문서가 있습니다.
- incremental-continuations.md — 검사된 연속 컴파일, 전체 스냅숏 교체, 커서/상태 유지, 스트림 최종화 및 접두 안정성을 설명합니다.
- execution-model.md — 아티팩트 형태, 버전 게이트, 위치 및 디스패처 루프를 설명합니다.
- state-lifecycle.md —
scene/run/user/app/quest.<id>계층, 초기화 및 리셋 경계를 설명합니다. - cel-and-facts.md —
exprAST 평가, 팩트 저장소의 assert/retract 델타 및 엔진이 계산하는 계층화된 최소 고정점을 설명합니다. - quest-lifecycle.md —
start/fail우선순위, 필수 및 선택 목표, 단조 증가하는 완료, run 계층 퀘스트 및 수명 주기 이벤트를 설명합니다. - beats-and-occasions.md — 계기와 대상 도메인, 비트 후보, 자격 및
once소진, 선택 순서를 설명합니다. - lore-entries.md — 엔트리 자격, 제시, 첫 읽기 효과 및
read/everRead플래그를 설명합니다. - timeline-semantics.md — 로컬 시계, 트랙별 커서, 장벽 및 검사기가 보장하는 대상별 단일 writer 불변식을 설명합니다.
- bridge-protocol.md — 타입이 지정된 브리지 호출, 반환 형태,
wait및 확정된 상태 효과를 설명합니다.