commit 3dacc9242318c5428803b9b0284f441b10d2252a Author: Codex Date: Tue Jul 28 12:51:00 2026 +0800 chore: bootstrap nana-story M0 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2e8b29 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.rs] +indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..39a5a52 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: verify + +on: + push: + branches: ["main", "integration/**", "agent/**"] + pull_request: + +jobs: + verify: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + path: workspace/nana-story + - name: Checkout pinned lapp-rs + uses: actions/checkout@v4 + with: + repository: openlapp/lapp-rs + ref: 5ba3c659e1536ec4bee16340faca603940a5cb17 + path: workspace/lapp-rs + - uses: pnpm/action-setup@v4 + with: + version: 10.29.2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: workspace/nana-story/pnpm-lock.yaml + - uses: dtolnay/rust-toolchain@1.96.0 + with: + components: clippy,rustfmt + - name: Install dependencies + working-directory: workspace/nana-story + run: pnpm install --frozen-lockfile + - name: Verify + working-directory: workspace/nana-story + run: pnpm verify diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ec0684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +target/ +.idea/ +.vscode/ +*.log +*.db +*.db-shm +*.db-wal diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3285899 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,40 @@ +[workspace] +resolver = "2" +members = [ + "crates/nana-contracts", + "crates/nana-domain", + "crates/nana-engine", + "crates/nana-runtime", + "crates/nana-store", + "src-tauri", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.96" +license = "MIT" +authors = ["nana-story contributors"] + +[workspace.dependencies] +nana-domain = { path = "crates/nana-domain" } +nana-engine = { path = "crates/nana-engine" } +nana-runtime = { path = "crates/nana-runtime" } +nana-store = { path = "crates/nana-store" } +openlapp = { path = "../lapp-rs", default-features = false, features = ["client-rustls"] } +schemars = "1.0.4" +semver = { version = "1.0.27", features = ["serde"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +sha2 = "0.10.9" +thiserror = "2.0.17" +ts-rs = { version = "11.1.0", features = ["serde-compat"] } + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +missing_errors_doc = "allow" +module_name_repetitions = "allow" diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f98170 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# 听娜娜讲故事 + +`nana-story` 是一个本地优先、纯 BYOK 的 AI 对话游戏。V1 聚焦一段可回溯、可分支、状态可重放的单角色纵切。 + +## 当前阶段 + +M0:工程骨架与领域契约冻结。 + +已建立: + +- Tauri 2 + Vue 3 + TypeScript 工程; +- Rust workspace 与模块所有权边界; +- Rust 领域类型作为契约唯一事实来源; +- 生成的 JSON Schema 与 TypeScript DTO; +- canonical `PlayerView` fixture; +- 最小娜娜内容包; +- 静态主演出屏与窄 Tauri 命令。 + +## 开发 + +要求: + +- Node.js 24+ +- pnpm 10.29.2 +- Rust 1.96 +- 与本仓库同级的 `lapp-rs`,固定到 `lapp-rs.lock` 中的提交 + +```text +workspace/ +├─ lapp-rs/ +└─ nana-story/ +``` + +```bash +pnpm install +pnpm verify +pnpm tauri dev +``` + +只验证前端和已提交契约时可运行: + +```bash +pnpm verify:web +``` + +## 边界 + +- 模型供应商协议与凭据处理属于 `lapp-rs`。 +- 前端只接收 `PlayerView`,不会得到完整 `RuntimeState`。 +- V1 不使用向量记忆库、导演 Agent 或多个完整角色。 +- 模型与前端都不能直接修改运行时状态。 diff --git a/content/nana-demo/bundle.json b/content/nana-demo/bundle.json new file mode 100644 index 0000000..3dd34c1 --- /dev/null +++ b/content/nana-demo/bundle.json @@ -0,0 +1,194 @@ +{ + "id": "nana.demo.station", + "schema_version": 1, + "revision": "0.1.0", + "display_name": "天亮之前", + "entry_character": "nana.character.nana", + "entry_persona": "nana.persona.traveler", + "entry_plot_module": "nana.plot.before_dawn", + "characters": [ + { + "header": { + "id": "nana.character.nana", + "kind": "character", + "schema_version": 1, + "revision": "0.1.0", + "content_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "dependencies": [ + { + "id": "nana.world.station", + "kind": "world_book", + "revision": "0.1.0", + "content_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + } + ] + }, + "name": "娜娜", + "identity": "独自守在废弃青川站的年轻女孩,正在等待失踪的妹妹留下的讯息。", + "personality": ["克制", "敏锐", "不轻易求助", "会用反问掩饰不安"], + "values": ["兑现承诺", "保护家人", "不轻率触碰他人的秘密"], + "boundaries": ["不会因一次说服就交出妹妹的遗物", "不会替玩家解释玩家的内心"], + "style": { + "speech": "短句,少用修辞;紧张时会先停顿再追问。", + "mannerisms": ["不安时攥住外套袖口", "提到妹妹时避开视线"], + "forbidden_player_assumptions": ["替玩家许诺", "替玩家主动触碰娜娜", "替玩家决定是否相信她"] + }, + "initial_relationship": { + "affinity": 48, + "trust": 34, + "hope": 31, + "respect": 45, + "intimacy": 26, + "attachment": 42 + }, + "judgment_rules": [ + { + "tags": ["kept_promise", "accepted_risk"], + "meaning": "比口头保证更重视为许诺实际承担风险。", + "relationship_effects": [ + { + "dimension": "trust", + "delta": 4, + "cause": "玩家以行动证明许诺", + "judgment_rule": "nana.promises.proven_by_action" + } + ] + } + ], + "skills": [ + { + "skill": "psychology", + "value": 58 + }, + { + "skill": "listen", + "value": 64 + } + ], + "default_expression": "guarded" + } + ], + "world_books": [ + { + "header": { + "id": "nana.world.station", + "kind": "world_book", + "schema_version": 1, + "revision": "0.1.0", + "content_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "dependencies": [] + }, + "name": "青川站", + "entries": [ + { + "id": "station_clock", + "title": "停摆的站钟", + "content": "青川站的钟停在四点十七分。十二年前最后一班列车也在这个时间失联。", + "keywords": ["站钟", "四点十七分", "最后一班列车"], + "tags": ["station_history", "missing_train"], + "required_flags": [] + } + ] + } + ], + "personas": [ + { + "header": { + "id": "nana.persona.traveler", + "kind": "persona", + "schema_version": 1, + "revision": "0.1.0", + "content_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "dependencies": [ + { + "id": "nana.item.old_flashlight", + "kind": "item_spec", + "revision": "0.1.0", + "content_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + } + ] + }, + "name": "旅人", + "identity": "因暴雨临时进入废弃车站避雨的普通旅人。", + "traits": ["谨慎", "对异常细节敏感"], + "skills": [ + { + "skill": "spot_hidden", + "value": 55 + }, + { + "skill": "persuade", + "value": 50 + } + ], + "initial_items": ["nana.item.old_flashlight"] + } + ], + "plot_modules": [ + { + "header": { + "id": "nana.plot.before_dawn", + "kind": "plot_module", + "schema_version": 1, + "revision": "0.1.0", + "content_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "dependencies": [ + { + "id": "nana.world.station", + "kind": "world_book", + "revision": "0.1.0", + "content_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + } + ] + }, + "name": "天亮之前", + "opening_event": "rainy_platform", + "events": [ + { + "id": "rainy_platform", + "title": "雨夜站台", + "tags": ["stranger_arrives", "promise_possible"], + "situation": ["玩家进入废弃青川站避雨", "娜娜正在等待一封不可能寄到的信"], + "required_flags": [], + "pressures": [ + { + "id": "dawn_approaches", + "description": "天亮前仍未找到线索,娜娜会独自进入封锁隧道。", + "clock_id": "clock_dawn", + "clock_delta": 1 + } + ], + "outcomes": [ + { + "id": "promise_accepted", + "description": "娜娜接受玩家天亮前归来的许诺。", + "effects": [] + } + ] + } + ] + } + ], + "item_specs": [ + { + "header": { + "id": "nana.item.old_flashlight", + "kind": "item_spec", + "schema_version": 1, + "revision": "0.1.0", + "content_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "dependencies": [] + }, + "name": "旧手电筒", + "description": "一支表面磨损严重的手电筒。", + "tags": ["tool", "light"], + "lore_refs": [], + "mechanics": { + "usable": true, + "grants_tags": ["has_light"], + "check_modifier": 1 + }, + "hidden_facts": ["电池仓里刻着青川站工作人员的编号"] + } + ] +} diff --git a/contracts/.source.sha256 b/contracts/.source.sha256 new file mode 100644 index 0000000..e994f5f --- /dev/null +++ b/contracts/.source.sha256 @@ -0,0 +1 @@ +374a325cf1198df128248876fe7f115b41b899777a89609f87cb4ba9e2958239 diff --git a/contracts/schema/app-info.schema.json b/contracts/schema/app-info.schema.json new file mode 100644 index 0000000..0b3d364 --- /dev/null +++ b/contracts/schema/app-info.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "AppInfo", + "type": "object", + "required": ["name", "version", "contractVersion"], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "contractVersion": { + "type": "integer" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/character-card.schema.json b/contracts/schema/character-card.schema.json new file mode 100644 index 0000000..7e783b3 --- /dev/null +++ b/contracts/schema/character-card.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "CharacterCard", + "type": "object", + "required": [ + "header", + "name", + "identity", + "personality", + "values", + "boundaries", + "style", + "initial_relationship", + "judgment_rules", + "skills", + "default_expression" + ], + "properties": { + "header": { + "$ref": "resource-header.schema.json" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "identity": { + "type": "string", + "minLength": 1 + }, + "personality": { + "type": "array", + "items": { + "type": "string" + } + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + }, + "boundaries": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": { + "type": "object" + }, + "initial_relationship": { + "type": "object" + }, + "judgment_rules": { + "type": "array" + }, + "skills": { + "type": "array" + }, + "default_expression": { + "type": "string" + } + } +} diff --git a/contracts/schema/demo-pack-summary.schema.json b/contracts/schema/demo-pack-summary.schema.json new file mode 100644 index 0000000..11c3d17 --- /dev/null +++ b/contracts/schema/demo-pack-summary.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "DemoPackSummary", + "type": "object", + "required": [ + "id", + "displayName", + "revision", + "characters", + "worldBooks", + "personas", + "plotModules", + "itemSpecs" + ], + "properties": { + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "characters": { + "type": "integer" + }, + "worldBooks": { + "type": "integer" + }, + "personas": { + "type": "integer" + }, + "plotModules": { + "type": "integer" + }, + "itemSpecs": { + "type": "integer" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/item-spec.schema.json b/contracts/schema/item-spec.schema.json new file mode 100644 index 0000000..d62a273 --- /dev/null +++ b/contracts/schema/item-spec.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ItemSpec", + "type": "object", + "required": [ + "header", + "name", + "description", + "tags", + "lore_refs", + "mechanics", + "hidden_facts" + ], + "properties": { + "header": { + "$ref": "resource-header.schema.json" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "tags": { + "type": "array" + }, + "lore_refs": { + "type": "array" + }, + "mechanics": { + "type": "object" + }, + "hidden_facts": { + "type": "array" + } + } +} diff --git a/contracts/schema/persona.schema.json b/contracts/schema/persona.schema.json new file mode 100644 index 0000000..953e242 --- /dev/null +++ b/contracts/schema/persona.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Persona", + "type": "object", + "required": ["header", "name", "identity", "traits", "skills", "initial_items"], + "properties": { + "header": { + "$ref": "resource-header.schema.json" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "identity": { + "type": "string" + }, + "traits": { + "type": "array" + }, + "skills": { + "type": "array" + }, + "initial_items": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/contracts/schema/player-view.schema.json b/contracts/schema/player-view.schema.json new file mode 100644 index 0000000..dce85d0 --- /dev/null +++ b/contracts/schema/player-view.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "PlayerView", + "type": "object", + "required": [ + "storyId", + "nodeId", + "branchId", + "sceneId", + "sceneTitle", + "characterName", + "beats", + "suggestions", + "inventory", + "knowledge", + "promises", + "relationship", + "history", + "canContinue" + ], + "properties": { + "storyId": { + "type": "string" + }, + "nodeId": { + "type": "string" + }, + "branchId": { + "type": "string" + }, + "sceneId": { + "type": "string" + }, + "sceneTitle": { + "type": "string" + }, + "characterName": { + "type": "string" + }, + "beats": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "kind", "speaker", "text", "visual"] + } + }, + "suggestions": { + "type": "array" + }, + "inventory": { + "type": "array" + }, + "knowledge": { + "type": "array" + }, + "promises": { + "type": "array" + }, + "relationship": { + "type": "object" + }, + "history": { + "type": "array" + }, + "canContinue": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/plot-module.schema.json b/contracts/schema/plot-module.schema.json new file mode 100644 index 0000000..e5eccf5 --- /dev/null +++ b/contracts/schema/plot-module.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "PlotModule", + "type": "object", + "required": ["header", "name", "opening_event", "events"], + "properties": { + "header": { + "$ref": "resource-header.schema.json" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "opening_event": { + "type": "string", + "minLength": 1 + }, + "events": { + "type": "array", + "items": { + "type": "object" + } + } + } +} diff --git a/contracts/schema/resource-bundle.schema.json b/contracts/schema/resource-bundle.schema.json new file mode 100644 index 0000000..6886ebf --- /dev/null +++ b/contracts/schema/resource-bundle.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ResourceBundle", + "type": "object", + "required": [ + "id", + "schema_version", + "revision", + "display_name", + "entry_character", + "entry_persona", + "entry_plot_module", + "characters", + "world_books", + "personas", + "plot_modules", + "item_specs" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9_]+(?:\\.[a-z0-9_]+)+$" + }, + "schema_version": { + "const": 1 + }, + "revision": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "display_name": { + "type": "string", + "minLength": 1 + }, + "entry_character": { + "type": "string" + }, + "entry_persona": { + "type": "string" + }, + "entry_plot_module": { + "type": ["string", "null"] + }, + "characters": { + "type": "array", + "items": { + "$ref": "character-card.schema.json" + } + }, + "world_books": { + "type": "array", + "items": { + "$ref": "world-book.schema.json" + } + }, + "personas": { + "type": "array", + "items": { + "$ref": "persona.schema.json" + } + }, + "plot_modules": { + "type": "array", + "items": { + "$ref": "plot-module.schema.json" + } + }, + "item_specs": { + "type": "array", + "items": { + "$ref": "item-spec.schema.json" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/resource-header.schema.json b/contracts/schema/resource-header.schema.json new file mode 100644 index 0000000..83f839e --- /dev/null +++ b/contracts/schema/resource-header.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ResourceHeader", + "type": "object", + "required": ["id", "kind", "schema_version", "revision", "content_hash", "dependencies"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9_]+(?:\\.[a-z0-9_]+)+$" + }, + "kind": { + "enum": ["character", "world_book", "persona", "plot_module", "item_spec"] + }, + "schema_version": { + "const": 1 + }, + "revision": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "content_hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "dependencies": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/runtime-state.schema.json b/contracts/schema/runtime-state.schema.json new file mode 100644 index 0000000..ca8446d --- /dev/null +++ b/contracts/schema/runtime-state.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RuntimeState", + "type": "object", + "required": [ + "story_id", + "current_node", + "current_branch", + "world_flags", + "relationships", + "relationship_states", + "promises", + "knowledge", + "items", + "clocks", + "checks" + ], + "properties": { + "story_id": { + "type": "string" + }, + "current_node": { + "type": "string" + }, + "current_branch": { + "type": "string" + }, + "world_flags": { + "type": "object" + }, + "relationships": { + "type": "object" + }, + "relationship_states": { + "type": "array" + }, + "promises": { + "type": "array" + }, + "knowledge": { + "type": "array" + }, + "items": { + "type": "array" + }, + "clocks": { + "type": "array" + }, + "checks": { + "type": "array" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/story-node.schema.json b/contracts/schema/story-node.schema.json new file mode 100644 index 0000000..d34615e --- /dev/null +++ b/contracts/schema/story-node.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "StoryNode", + "type": "object", + "required": [ + "id", + "story_id", + "branch_id", + "parent_id", + "action_id", + "user_input", + "beats", + "delta", + "state_hash" + ], + "properties": { + "id": { + "type": "string" + }, + "story_id": { + "type": "string" + }, + "branch_id": { + "type": "string" + }, + "parent_id": { + "type": ["string", "null"] + }, + "action_id": { + "type": "string" + }, + "user_input": { + "type": "string" + }, + "beats": { + "type": "array" + }, + "delta": { + "type": "object" + }, + "state_hash": { + "type": "string" + } + } +} diff --git a/contracts/schema/turn-failure.schema.json b/contracts/schema/turn-failure.schema.json new file mode 100644 index 0000000..6644f45 --- /dev/null +++ b/contracts/schema/turn-failure.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TurnFailure", + "type": "object", + "required": ["code", "message", "retryable"], + "properties": { + "code": { + "enum": [ + "stale_node", + "invalid_input", + "invalid_model_output", + "provider_unavailable", + "cancelled", + "timed_out", + "internal" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/turn-request.schema.json b/contracts/schema/turn-request.schema.json new file mode 100644 index 0000000..6a69678 --- /dev/null +++ b/contracts/schema/turn-request.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TurnRequest", + "type": "object", + "required": [ + "storyId", + "branchId", + "expectedNodeId", + "actionId", + "intent", + "input" + ], + "properties": { + "storyId": { + "type": "string" + }, + "branchId": { + "type": "string" + }, + "expectedNodeId": { + "type": "string" + }, + "actionId": { + "type": "string" + }, + "intent": { + "enum": ["speak_or_act", "continue", "regenerate", "push_check"] + }, + "input": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/turn-result.schema.json b/contracts/schema/turn-result.schema.json new file mode 100644 index 0000000..b58b818 --- /dev/null +++ b/contracts/schema/turn-result.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TurnResult", + "type": "object", + "required": ["committedNodeId", "playerView"], + "properties": { + "committedNodeId": { + "type": "string" + }, + "playerView": { + "$ref": "player-view.schema.json" + } + }, + "additionalProperties": false +} diff --git a/contracts/schema/world-book.schema.json b/contracts/schema/world-book.schema.json new file mode 100644 index 0000000..7964b37 --- /dev/null +++ b/contracts/schema/world-book.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "WorldBook", + "type": "object", + "required": ["header", "name", "entries"], + "properties": { + "header": { + "$ref": "resource-header.schema.json" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "entries": { + "type": "array", + "items": { + "type": "object" + } + } + } +} diff --git a/contracts/ts/index.ts b/contracts/ts/index.ts new file mode 100644 index 0000000..4595b6b --- /dev/null +++ b/contracts/ts/index.ts @@ -0,0 +1,349 @@ +// @generated by crates/nana-contracts; do not edit. + +export type ResourceId = string; +export type ResourceKind = + | "character" + | "world_book" + | "persona" + | "plot_module" + | "item_spec"; + +export interface ResourceRef { + id: ResourceId; + kind: ResourceKind; + revision: string; + content_hash: string; +} + +export interface ResourceHeader { + id: ResourceId; + kind: ResourceKind; + schema_version: number; + revision: string; + content_hash: string; + dependencies: ResourceRef[]; +} + +export type RelationshipDimension = + | "affinity" + | "trust" + | "hope" + | "respect" + | "intimacy" + | "attachment"; + +export interface RelationshipAxes { + affinity: number; + trust: number; + hope: number; + respect: number; + intimacy: number; + attachment: number; +} + +export interface RelationshipAdjustment { + dimension: RelationshipDimension; + delta: number; + cause: string; + judgment_rule: string | null; +} + +export interface SkillValue { + skill: string; + value: number; +} + +export interface CharacterCard { + header: ResourceHeader; + name: string; + identity: string; + personality: string[]; + values: string[]; + boundaries: string[]; + style: { + speech: string; + mannerisms: string[]; + forbidden_player_assumptions: string[]; + }; + initial_relationship: RelationshipAxes; + judgment_rules: Array<{ + tags: string[]; + meaning: string; + relationship_effects: RelationshipAdjustment[]; + }>; + skills: SkillValue[]; + default_expression: string; +} + +export interface WorldBook { + header: ResourceHeader; + name: string; + entries: Array<{ + id: string; + title: string; + content: string; + keywords: string[]; + tags: string[]; + required_flags: string[]; + }>; +} + +export interface Persona { + header: ResourceHeader; + name: string; + identity: string; + traits: string[]; + skills: SkillValue[]; + initial_items: ResourceId[]; +} + +export interface PlotModule { + header: ResourceHeader; + name: string; + opening_event: string; + events: Array<{ + id: string; + title: string; + tags: string[]; + situation: string[]; + required_flags: string[]; + pressures: Array<{ + id: string; + description: string; + clock_id: string | null; + clock_delta: number | null; + }>; + outcomes: Array<{ + id: string; + description: string; + effects: StateOp[]; + }>; + }>; +} + +export interface ItemSpec { + header: ResourceHeader; + name: string; + description: string; + tags: string[]; + lore_refs: string[]; + mechanics: { + usable: boolean; + grants_tags: string[]; + check_modifier: number | null; + }; + hidden_facts: string[]; +} + +export interface ResourceBundle { + id: string; + schema_version: number; + revision: string; + display_name: string; + entry_character: ResourceId; + entry_persona: ResourceId; + entry_plot_module: ResourceId | null; + characters: CharacterCard[]; + world_books: WorldBook[]; + personas: Persona[]; + plot_modules: PlotModule[]; + item_specs: ItemSpec[]; +} + +export type PromiseStatus = + | "proposed" + | "accepted" + | "fulfilled" + | "broken" + | "released" + | "impossible"; +export type PromiseWeight = "minor" | "major"; +export type KnowledgeCertainty = "suspected" | "reported" | "confirmed"; +export type ItemPlacement = "bag" | "worn" | "hand" | "scene" | "hidden"; +export type AcquisitionMode = "initial" | "found" | "given" | "borrowed" | "stolen" | "returned"; + +export interface Promise { + id: string; + promiser: string; + promisee: string; + content: string; + status: PromiseStatus; + weight: PromiseWeight; + created_at: string; + accepted_at: string | null; + resolved_at: string | null; +} + +export interface KnowledgeRecord { + id: string; + observer: string; + subject: string | null; + fact: string; + certainty: KnowledgeCertainty; + source: string; + learned_at: string; + last_verified_at: string | null; +} + +export interface ItemInstance { + id: string; + spec_ref: ResourceId; + owner: string; + holder: string; + placement: ItemPlacement; + quantity: number; + condition: string; + state_tags: string[]; + acquisition: { + mode: AcquisitionMode; + from: string | null; + at_node: string; + }; +} + +export interface RuntimeState { + story_id: string; + current_node: string; + current_branch: string; + world_flags: Record; + relationships: Record; + relationship_states: unknown[]; + promises: Promise[]; + knowledge: KnowledgeRecord[]; + items: ItemInstance[]; + clocks: unknown[]; + checks: unknown[]; +} + +export type StateOp = + | { op: "set_world_flag"; key: string; value: boolean } + | { op: "adjust_relationship"; from: string; to: string; adjustment: RelationshipAdjustment } + | { op: "create_promise"; promise: Promise } + | { op: "update_promise"; promise_id: string; status: PromiseStatus; resolved_at: string | null } + | { op: "add_knowledge"; record: KnowledgeRecord } + | { op: "add_item"; item: ItemInstance } + | { + op: "transfer_item"; + item_id: string; + from: string; + to: string; + placement: ItemPlacement; + mode: AcquisitionMode; + } + | { op: "record_check"; check: unknown } + | { op: "advance_clock"; clock_id: string; delta: number }; + +export type BeatKind = "narration" | "dialogue" | "action" | "system"; + +export interface PresentationBeat { + id: string; + kind: BeatKind; + speaker: string | null; + text: string; + visual: { + character: string | null; + expression: string | null; + pose: string | null; + scene: string | null; + } | null; +} + +export type RelationshipBand = "distant" | "guarded" | "warming" | "close" | "bonded"; + +export interface RelationshipView { + affinity: RelationshipBand; + trust: RelationshipBand; + hope: RelationshipBand; + respect: RelationshipBand; + intimacy: RelationshipBand; + attachment: RelationshipBand; + updatedAtNode: string | null; +} + +export interface PlayerView { + storyId: string; + nodeId: string; + branchId: string; + sceneId: string; + sceneTitle: string; + characterName: string; + beats: PresentationBeat[]; + suggestions: Array<{ id: string; label: string; draft: string }>; + inventory: Array<{ + instanceId: string; + name: string; + description: string; + quantity: number; + placement: ItemPlacement; + condition: string; + }>; + knowledge: Array<{ + id: string; + title: string; + summary: string; + certainty: KnowledgeCertainty; + }>; + promises: Array<{ + id: string; + content: string; + status: PromiseStatus; + weight: PromiseWeight; + }>; + relationship: RelationshipView; + history: Array<{ + id: string; + parentId: string | null; + branchId: string; + label: string; + isCurrent: boolean; + }>; + canContinue: boolean; +} + +export type TurnIntent = "speak_or_act" | "continue" | "regenerate" | "push_check"; + +export interface TurnRequest { + storyId: string; + branchId: string; + expectedNodeId: string; + actionId: string; + intent: TurnIntent; + input: string; +} + +export interface TurnResult { + committedNodeId: string; + playerView: PlayerView; +} + +export type TurnFailureCode = + | "stale_node" + | "invalid_input" + | "invalid_model_output" + | "provider_unavailable" + | "cancelled" + | "timed_out" + | "internal"; + +export interface TurnFailure { + code: TurnFailureCode; + message: string; + retryable: boolean; +} + +export interface AppInfo { + name: string; + version: string; + contractVersion: number; +} + +export interface DemoPackSummary { + id: string; + displayName: string; + revision: string; + characters: number; + worldBooks: number; + personas: number; + plotModules: number; + itemSpecs: number; +} diff --git a/crates/nana-contracts/Cargo.toml b/crates/nana-contracts/Cargo.toml new file mode 100644 index 0000000..a80e324 --- /dev/null +++ b/crates/nana-contracts/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nana-contracts" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +nana-domain.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +ts-rs.workspace = true + +[lints] +workspace = true diff --git a/crates/nana-contracts/src/main.rs b/crates/nana-contracts/src/main.rs new file mode 100644 index 0000000..cbf2c91 --- /dev/null +++ b/crates/nana-contracts/src/main.rs @@ -0,0 +1,167 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use nana_domain::{ + AcquisitionMode, ActionSuggestion, AppInfo, BeatKind, CharacterCard, CharacterJudgmentRule, + CharacterStyle, CheckDifficulty, CheckRecord, CheckResult, ClockState, DemoPackSummary, + HistoryNodeView, ItemAcquisition, ItemInstance, ItemMechanics, ItemPlacement, ItemSpec, + KnowledgeCertainty, KnowledgeRecord, Persona, PlayerItemView, PlayerKnowledgeView, + PlayerPromiseView, PlayerView, PlotEvent, PlotModule, PlotOutcome, PlotPressure, Promise, + PromiseStatus, PromiseWeight, RelationshipAdjustment, RelationshipAxes, RelationshipBand, + RelationshipDimension, RelationshipState, RelationshipView, ResourceBundle, ResourceHeader, + ResourceId, ResourceKind, ResourceRef, RuntimeState, SkillValue, StateDelta, StateOp, Story, + StoryBinding, StoryNode, TurnFailure, TurnFailureCode, TurnIntent, TurnRequest, TurnResult, + ValidationCode, ValidationIssue, ValidationReport, VisualDirective, WorldBook, WorldBookEntry, +}; +use schemars::{JsonSchema, schema_for}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use ts_rs::TS; + +fn main() -> Result<(), Box> { + let check = env::args().any(|argument| argument == "--check"); + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let outputs = generated_outputs(&root)?; + + if check { + let mut stale = Vec::new(); + for (path, expected) in outputs { + if fs::read(&path).ok().as_deref() != Some(expected.as_slice()) { + stale.push(path); + } + } + if stale.is_empty() { + return Ok(()); + } + for path in stale { + eprintln!("stale generated contract: {}", path.display()); + } + return Err("generated contracts are stale".into()); + } + + for (path, bytes) in outputs { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, bytes)?; + } + Ok(()) +} + +fn generated_outputs( + root: &Path, +) -> Result)>, Box> { + let mut outputs = Vec::new(); + let schema_dir = root.join("contracts/schema"); + + add_schema::(&mut outputs, &schema_dir, "resource-header")?; + add_schema::(&mut outputs, &schema_dir, "character-card")?; + add_schema::(&mut outputs, &schema_dir, "world-book")?; + add_schema::(&mut outputs, &schema_dir, "persona")?; + add_schema::(&mut outputs, &schema_dir, "plot-module")?; + add_schema::(&mut outputs, &schema_dir, "item-spec")?; + add_schema::(&mut outputs, &schema_dir, "resource-bundle")?; + add_schema::(&mut outputs, &schema_dir, "story-node")?; + add_schema::(&mut outputs, &schema_dir, "runtime-state")?; + add_schema::(&mut outputs, &schema_dir, "player-view")?; + add_schema::(&mut outputs, &schema_dir, "turn-request")?; + add_schema::(&mut outputs, &schema_dir, "turn-result")?; + add_schema::(&mut outputs, &schema_dir, "turn-failure")?; + add_schema::(&mut outputs, &schema_dir, "app-info")?; + add_schema::(&mut outputs, &schema_dir, "demo-pack-summary")?; + + let declarations = [ + ResourceId::decl(), + ResourceKind::decl(), + ResourceRef::decl(), + ResourceHeader::decl(), + CharacterStyle::decl(), + CharacterJudgmentRule::decl(), + SkillValue::decl(), + CharacterCard::decl(), + WorldBookEntry::decl(), + WorldBook::decl(), + Persona::decl(), + PlotPressure::decl(), + PlotOutcome::decl(), + PlotEvent::decl(), + PlotModule::decl(), + ItemMechanics::decl(), + ItemSpec::decl(), + ResourceBundle::decl(), + RelationshipAxes::decl(), + RelationshipAdjustment::decl(), + RelationshipDimension::decl(), + RelationshipBand::decl(), + RelationshipView::decl(), + PromiseStatus::decl(), + PromiseWeight::decl(), + Promise::decl(), + RelationshipState::decl(), + KnowledgeCertainty::decl(), + KnowledgeRecord::decl(), + ItemPlacement::decl(), + AcquisitionMode::decl(), + ItemAcquisition::decl(), + ItemInstance::decl(), + CheckDifficulty::decl(), + CheckResult::decl(), + CheckRecord::decl(), + ClockState::decl(), + StoryBinding::decl(), + Story::decl(), + StateDelta::decl(), + StateOp::decl(), + StoryNode::decl(), + RuntimeState::decl(), + BeatKind::decl(), + VisualDirective::decl(), + PresentationBeat::decl(), + ActionSuggestion::decl(), + PlayerItemView::decl(), + PlayerKnowledgeView::decl(), + PlayerPromiseView::decl(), + HistoryNodeView::decl(), + PlayerView::decl(), + TurnIntent::decl(), + TurnRequest::decl(), + TurnResult::decl(), + TurnFailureCode::decl(), + TurnFailure::decl(), + AppInfo::decl(), + DemoPackSummary::decl(), + ValidationCode::decl(), + ValidationIssue::decl(), + ValidationReport::decl(), + ] + .join("\n\n"); + let ts = format!( + "// @generated by crates/nana-contracts; do not edit.\n\n{declarations}\n" + ); + outputs.push(( + root.join("contracts/ts/index.ts"), + ts.into_bytes(), + )); + + let domain_source = fs::read(root.join("crates/nana-domain/src/lib.rs"))?; + let source_hash = format!("{:x}\n", Sha256::digest(domain_source)); + outputs.push(( + root.join("contracts/.source.sha256"), + source_hash.into_bytes(), + )); + + Ok(outputs) +} + +fn add_schema( + outputs: &mut Vec<(PathBuf, Vec)>, + schema_dir: &Path, + name: &str, +) -> Result<(), serde_json::Error> { + let mut bytes = serde_json::to_vec_pretty(&schema_for!(T))?; + bytes.push(b'\n'); + outputs.push((schema_dir.join(format!("{name}.schema.json")), bytes)); + Ok(()) +} diff --git a/crates/nana-domain/Cargo.toml b/crates/nana-domain/Cargo.toml new file mode 100644 index 0000000..15b8013 --- /dev/null +++ b/crates/nana-domain/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nana-domain" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +schemars.workspace = true +serde.workspace = true +sha2.workspace = true +thiserror.workspace = true +ts-rs.workspace = true + +[lints] +workspace = true diff --git a/crates/nana-domain/src/lib.rs b/crates/nana-domain/src/lib.rs new file mode 100644 index 0000000..fac20e5 --- /dev/null +++ b/crates/nana-domain/src/lib.rs @@ -0,0 +1,1096 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use ts_rs::TS; + +pub const DOMAIN_SCHEMA_VERSION: u32 = 1; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS, +)] +#[serde(transparent)] +pub struct ResourceId(pub String); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum ResourceKind { + Character, + WorldBook, + Persona, + PlotModule, + ItemSpec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ResourceRef { + pub id: ResourceId, + pub kind: ResourceKind, + pub revision: String, + pub content_hash: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ResourceHeader { + pub id: ResourceId, + pub kind: ResourceKind, + pub schema_version: u32, + pub revision: String, + pub content_hash: String, + #[serde(default)] + pub dependencies: Vec, +} + +pub trait Resource { + fn header(&self) -> &ResourceHeader; +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct CharacterStyle { + pub speech: String, + #[serde(default)] + pub mannerisms: Vec, + #[serde(default)] + pub forbidden_player_assumptions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct CharacterJudgmentRule { + pub tags: Vec, + pub meaning: String, + #[serde(default)] + pub relationship_effects: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct SkillValue { + pub skill: String, + pub value: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct CharacterCard { + pub header: ResourceHeader, + pub name: String, + pub identity: String, + pub personality: Vec, + pub values: Vec, + pub boundaries: Vec, + pub style: CharacterStyle, + pub initial_relationship: RelationshipAxes, + #[serde(default)] + pub judgment_rules: Vec, + #[serde(default)] + pub skills: Vec, + pub default_expression: String, +} + +impl Resource for CharacterCard { + fn header(&self) -> &ResourceHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct WorldBookEntry { + pub id: String, + pub title: String, + pub content: String, + #[serde(default)] + pub keywords: Vec, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub required_flags: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct WorldBook { + pub header: ResourceHeader, + pub name: String, + pub entries: Vec, +} + +impl Resource for WorldBook { + fn header(&self) -> &ResourceHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct Persona { + pub header: ResourceHeader, + pub name: String, + pub identity: String, + #[serde(default)] + pub traits: Vec, + #[serde(default)] + pub skills: Vec, + #[serde(default)] + pub initial_items: Vec, +} + +impl Resource for Persona { + fn header(&self) -> &ResourceHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct PlotPressure { + pub id: String, + pub description: String, + pub clock_id: Option, + pub clock_delta: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct PlotOutcome { + pub id: String, + pub description: String, + #[serde(default)] + pub effects: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct PlotEvent { + pub id: String, + pub title: String, + pub tags: Vec, + pub situation: Vec, + #[serde(default)] + pub required_flags: Vec, + #[serde(default)] + pub pressures: Vec, + pub outcomes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct PlotModule { + pub header: ResourceHeader, + pub name: String, + pub opening_event: String, + pub events: Vec, +} + +impl Resource for PlotModule { + fn header(&self) -> &ResourceHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ItemMechanics { + pub usable: bool, + #[serde(default)] + pub grants_tags: Vec, + pub check_modifier: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ItemSpec { + pub header: ResourceHeader, + pub name: String, + pub description: String, + pub tags: Vec, + #[serde(default)] + pub lore_refs: Vec, + pub mechanics: ItemMechanics, + #[serde(default)] + pub hidden_facts: Vec, +} + +impl Resource for ItemSpec { + fn header(&self) -> &ResourceHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ResourceBundle { + pub id: String, + pub schema_version: u32, + pub revision: String, + pub display_name: String, + pub entry_character: ResourceId, + pub entry_persona: ResourceId, + pub entry_plot_module: Option, + #[serde(default)] + pub characters: Vec, + #[serde(default)] + pub world_books: Vec, + #[serde(default)] + pub personas: Vec, + #[serde(default)] + pub plot_modules: Vec, + #[serde(default)] + pub item_specs: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct RelationshipAxes { + pub affinity: i16, + pub trust: i16, + pub hope: i16, + pub respect: i16, + pub intimacy: i16, + pub attachment: i16, +} + +impl RelationshipAxes { + pub const fn neutral() -> Self { + Self { + affinity: 50, + trust: 50, + hope: 50, + respect: 50, + intimacy: 50, + attachment: 50, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct RelationshipAdjustment { + pub dimension: RelationshipDimension, + pub delta: i16, + pub cause: String, + pub judgment_rule: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum RelationshipDimension { + Affinity, + Trust, + Hope, + Respect, + Intimacy, + Attachment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum RelationshipBand { + Distant, + Guarded, + Warming, + Close, + Bonded, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct RelationshipView { + pub affinity: RelationshipBand, + pub trust: RelationshipBand, + pub hope: RelationshipBand, + pub respect: RelationshipBand, + pub intimacy: RelationshipBand, + pub attachment: RelationshipBand, + pub updated_at_node: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum PromiseStatus { + Proposed, + Accepted, + Fulfilled, + Broken, + Released, + Impossible, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum PromiseWeight { + Minor, + Major, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct Promise { + pub id: String, + pub promiser: String, + pub promisee: String, + pub content: String, + pub status: PromiseStatus, + pub weight: PromiseWeight, + pub created_at: String, + pub accepted_at: Option, + pub resolved_at: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct RelationshipState { + pub id: String, + pub tag: String, + pub description: String, + pub source_node: String, + pub active: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum KnowledgeCertainty { + Suspected, + Reported, + Confirmed, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct KnowledgeRecord { + pub id: String, + pub observer: String, + pub subject: Option, + pub fact: String, + pub certainty: KnowledgeCertainty, + pub source: String, + pub learned_at: String, + pub last_verified_at: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum ItemPlacement { + Bag, + Worn, + Hand, + Scene, + Hidden, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum AcquisitionMode { + Initial, + Found, + Given, + Borrowed, + Stolen, + Returned, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ItemAcquisition { + pub mode: AcquisitionMode, + pub from: Option, + pub at_node: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ItemInstance { + pub id: String, + pub spec_ref: ResourceId, + pub owner: String, + pub holder: String, + pub placement: ItemPlacement, + pub quantity: u32, + pub condition: String, + #[serde(default)] + pub state_tags: Vec, + pub acquisition: ItemAcquisition, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum CheckDifficulty { + Regular, + Hard, + Extreme, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum CheckResult { + CriticalSuccess, + ExtremeSuccess, + HardSuccess, + Success, + Failure, + Fumble, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct CheckRecord { + pub id: String, + pub action_id: String, + pub actor: String, + pub skill: String, + pub target: u8, + pub difficulty: CheckDifficulty, + pub bonus_dice: i8, + pub roll: u8, + pub result: CheckResult, + pub pushed_from: Option, + pub node_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ClockState { + pub id: String, + pub label: String, + pub value: u16, + pub max: u16, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct StoryBinding { + pub persona: ResourceRef, + pub characters: Vec, + #[serde(default)] + pub world_books: Vec, + #[serde(default)] + pub plot_modules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct Story { + pub id: String, + pub title: String, + pub ruleset: String, + pub bindings: StoryBinding, + pub root_node: String, + pub active_branch: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct StateDelta { + #[serde(default)] + pub ops: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(tag = "op", rename_all = "snake_case")] +pub enum StateOp { + SetWorldFlag { + key: String, + value: bool, + }, + AdjustRelationship { + from: String, + to: String, + adjustment: RelationshipAdjustment, + }, + CreatePromise { + promise: Promise, + }, + UpdatePromise { + promise_id: String, + status: PromiseStatus, + resolved_at: Option, + }, + AddKnowledge { + record: KnowledgeRecord, + }, + AddItem { + item: ItemInstance, + }, + TransferItem { + item_id: String, + from: String, + to: String, + placement: ItemPlacement, + mode: AcquisitionMode, + }, + RecordCheck { + check: CheckRecord, + }, + AdvanceClock { + clock_id: String, + delta: u16, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct RuntimeState { + pub story_id: String, + pub current_node: String, + pub current_branch: String, + #[serde(default)] + pub world_flags: BTreeMap, + #[serde(default)] + pub relationships: BTreeMap, + #[serde(default)] + pub relationship_states: Vec, + #[serde(default)] + pub promises: Vec, + #[serde(default)] + pub knowledge: Vec, + #[serde(default)] + pub items: Vec, + #[serde(default)] + pub clocks: Vec, + #[serde(default)] + pub checks: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum BeatKind { + Narration, + Dialogue, + Action, + System, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct VisualDirective { + pub character: Option, + pub expression: Option, + pub pose: Option, + pub scene: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct PresentationBeat { + pub id: String, + pub kind: BeatKind, + pub speaker: Option, + pub text: String, + pub visual: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct ActionSuggestion { + pub id: String, + pub label: String, + pub draft: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct PlayerItemView { + pub instance_id: String, + pub name: String, + pub description: String, + pub quantity: u32, + pub placement: ItemPlacement, + pub condition: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct PlayerKnowledgeView { + pub id: String, + pub title: String, + pub summary: String, + pub certainty: KnowledgeCertainty, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct PlayerPromiseView { + pub id: String, + pub content: String, + pub status: PromiseStatus, + pub weight: PromiseWeight, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct HistoryNodeView { + pub id: String, + pub parent_id: Option, + pub branch_id: String, + pub label: String, + pub is_current: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct PlayerView { + pub story_id: String, + pub node_id: String, + pub branch_id: String, + pub scene_id: String, + pub scene_title: String, + pub character_name: String, + pub beats: Vec, + #[serde(default)] + pub suggestions: Vec, + #[serde(default)] + pub inventory: Vec, + #[serde(default)] + pub knowledge: Vec, + #[serde(default)] + pub promises: Vec, + pub relationship: RelationshipView, + #[serde(default)] + pub history: Vec, + pub can_continue: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum TurnIntent { + SpeakOrAct, + Continue, + Regenerate, + PushCheck, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct TurnRequest { + pub story_id: String, + pub branch_id: String, + pub expected_node_id: String, + pub action_id: String, + pub intent: TurnIntent, + pub input: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct TurnResult { + pub committed_node_id: String, + pub player_view: PlayerView, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum TurnFailureCode { + StaleNode, + InvalidInput, + InvalidModelOutput, + ProviderUnavailable, + Cancelled, + TimedOut, + Internal, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct TurnFailure { + pub code: TurnFailureCode, + pub message: String, + pub retryable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct StoryNode { + pub id: String, + pub story_id: String, + pub branch_id: String, + pub parent_id: Option, + pub action_id: String, + pub user_input: String, + pub beats: Vec, + pub delta: StateDelta, + pub state_hash: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct AppInfo { + pub name: String, + pub version: String, + pub contract_version: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct DemoPackSummary { + pub id: String, + pub display_name: String, + pub revision: String, + pub characters: usize, + pub world_books: usize, + pub personas: usize, + pub plot_modules: usize, + pub item_specs: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum ValidationCode { + InvalidId, + InvalidSchemaVersion, + InvalidRevision, + InvalidContentHash, + WrongResourceKind, + DuplicateResource, + MissingReference, + InvalidRelationshipValue, + InvalidSkillValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ValidationIssue { + pub code: ValidationCode, + pub path: String, + pub message: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +pub struct ValidationReport { + pub issues: Vec, +} + +impl ValidationReport { + #[must_use] + pub fn is_valid(&self) -> bool { + self.issues.is_empty() + } +} + +#[derive(Debug, Error)] +pub enum BundleError { + #[error("content bundle is invalid")] + Invalid(ValidationReport), +} + +pub fn validate_bundle(bundle: &ResourceBundle) -> ValidationReport { + let mut issues = Vec::new(); + if bundle.schema_version != DOMAIN_SCHEMA_VERSION { + issues.push(issue( + ValidationCode::InvalidSchemaVersion, + "schema_version", + format!( + "expected schema version {DOMAIN_SCHEMA_VERSION}, got {}", + bundle.schema_version + ), + )); + } + if !is_resource_id(&bundle.id) { + issues.push(issue( + ValidationCode::InvalidId, + "id", + "bundle id must use lowercase dotted segments", + )); + } + if !is_revision(&bundle.revision) { + issues.push(issue( + ValidationCode::InvalidRevision, + "revision", + "revision must be a three-part semantic version", + )); + } + + let mut ids = BTreeSet::new(); + for (path, resource) in bundle_resources(bundle) { + validate_header(&path, resource.header(), &mut issues); + if !ids.insert(resource.header().id.clone()) { + issues.push(issue( + ValidationCode::DuplicateResource, + format!("{path}.header.id"), + "resource id must be unique inside a bundle", + )); + } + } + + let available = ids; + validate_entry_ref( + &bundle.entry_character, + "entry_character", + &available, + &mut issues, + ); + validate_entry_ref( + &bundle.entry_persona, + "entry_persona", + &available, + &mut issues, + ); + if let Some(plot) = &bundle.entry_plot_module { + validate_entry_ref(plot, "entry_plot_module", &available, &mut issues); + } + + for (path, resource) in bundle_resources(bundle) { + for (index, dependency) in resource.header().dependencies.iter().enumerate() { + if !available.contains(&dependency.id) { + issues.push(issue( + ValidationCode::MissingReference, + format!("{path}.header.dependencies[{index}].id"), + format!("missing resource dependency {}", dependency.id.0), + )); + } + } + } + + for (index, character) in bundle.characters.iter().enumerate() { + validate_expected_kind( + &character.header, + ResourceKind::Character, + format!("characters[{index}].header.kind"), + &mut issues, + ); + validate_axes( + character.initial_relationship, + format!("characters[{index}].initial_relationship"), + &mut issues, + ); + validate_skills( + &character.skills, + format!("characters[{index}].skills"), + &mut issues, + ); + } + for (index, world_book) in bundle.world_books.iter().enumerate() { + validate_expected_kind( + &world_book.header, + ResourceKind::WorldBook, + format!("world_books[{index}].header.kind"), + &mut issues, + ); + } + for (index, persona) in bundle.personas.iter().enumerate() { + validate_expected_kind( + &persona.header, + ResourceKind::Persona, + format!("personas[{index}].header.kind"), + &mut issues, + ); + validate_skills( + &persona.skills, + format!("personas[{index}].skills"), + &mut issues, + ); + } + for (index, plot) in bundle.plot_modules.iter().enumerate() { + validate_expected_kind( + &plot.header, + ResourceKind::PlotModule, + format!("plot_modules[{index}].header.kind"), + &mut issues, + ); + } + for (index, item) in bundle.item_specs.iter().enumerate() { + validate_expected_kind( + &item.header, + ResourceKind::ItemSpec, + format!("item_specs[{index}].header.kind"), + &mut issues, + ); + } + + ValidationReport { issues } +} + +pub fn validate_bundle_or_error(bundle: &ResourceBundle) -> Result<(), BundleError> { + let report = validate_bundle(bundle); + if report.is_valid() { + Ok(()) + } else { + Err(BundleError::Invalid(report)) + } +} + +#[must_use] +pub fn stable_json_hash(bytes: &[u8]) -> String { + format!("sha256:{:x}", Sha256::digest(bytes)) +} + +fn bundle_resources(bundle: &ResourceBundle) -> Vec<(String, &dyn Resource)> { + let mut resources: Vec<(String, &dyn Resource)> = Vec::new(); + resources.extend( + bundle + .characters + .iter() + .enumerate() + .map(|(index, value)| (format!("characters[{index}]"), value as &dyn Resource)), + ); + resources.extend( + bundle + .world_books + .iter() + .enumerate() + .map(|(index, value)| (format!("world_books[{index}]"), value as &dyn Resource)), + ); + resources.extend(bundle.personas.iter().enumerate().map(|(index, value)| { + (format!("personas[{index}]"), value as &dyn Resource) + })); + resources.extend( + bundle + .plot_modules + .iter() + .enumerate() + .map(|(index, value)| (format!("plot_modules[{index}]"), value as &dyn Resource)), + ); + resources.extend( + bundle + .item_specs + .iter() + .enumerate() + .map(|(index, value)| (format!("item_specs[{index}]"), value as &dyn Resource)), + ); + resources +} + +fn validate_header(path: &str, header: &ResourceHeader, issues: &mut Vec) { + if !is_resource_id(&header.id.0) { + issues.push(issue( + ValidationCode::InvalidId, + format!("{path}.header.id"), + "resource id must use lowercase dotted segments", + )); + } + if header.schema_version != DOMAIN_SCHEMA_VERSION { + issues.push(issue( + ValidationCode::InvalidSchemaVersion, + format!("{path}.header.schema_version"), + format!("expected schema version {DOMAIN_SCHEMA_VERSION}"), + )); + } + if !is_revision(&header.revision) { + issues.push(issue( + ValidationCode::InvalidRevision, + format!("{path}.header.revision"), + "revision must be a three-part semantic version", + )); + } + if !is_content_hash(&header.content_hash) { + issues.push(issue( + ValidationCode::InvalidContentHash, + format!("{path}.header.content_hash"), + "content hash must be sha256 followed by 64 lowercase hex characters", + )); + } +} + +fn validate_expected_kind( + header: &ResourceHeader, + expected: ResourceKind, + path: String, + issues: &mut Vec, +) { + if header.kind != expected { + issues.push(issue( + ValidationCode::WrongResourceKind, + path, + "resource kind does not match its collection", + )); + } +} + +fn validate_axes( + axes: RelationshipAxes, + path: String, + issues: &mut Vec, +) { + let values = [ + ("affinity", axes.affinity), + ("trust", axes.trust), + ("hope", axes.hope), + ("respect", axes.respect), + ("intimacy", axes.intimacy), + ("attachment", axes.attachment), + ]; + for (name, value) in values { + if !(0..=100).contains(&value) { + issues.push(issue( + ValidationCode::InvalidRelationshipValue, + format!("{path}.{name}"), + "relationship values must be between 0 and 100", + )); + } + } +} + +fn validate_skills(skills: &[SkillValue], path: String, issues: &mut Vec) { + for (index, skill) in skills.iter().enumerate() { + if skill.value > 100 { + issues.push(issue( + ValidationCode::InvalidSkillValue, + format!("{path}[{index}].value"), + "skill values must be between 0 and 100", + )); + } + } +} + +fn validate_entry_ref( + id: &ResourceId, + path: &str, + available: &BTreeSet, + issues: &mut Vec, +) { + if !available.contains(id) { + issues.push(issue( + ValidationCode::MissingReference, + path, + format!("missing entry resource {}", id.0), + )); + } +} + +fn issue( + code: ValidationCode, + path: impl Into, + message: impl Into, +) -> ValidationIssue { + ValidationIssue { + code, + path: path.into(), + message: message.into(), + } +} + +fn is_resource_id(value: &str) -> bool { + !value.is_empty() + && value.split('.').all(|segment| { + !segment.is_empty() + && segment + .chars() + .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_') + }) +} + +fn is_revision(value: &str) -> bool { + let parts: Vec<_> = value.split('.').collect(); + parts.len() == 3 + && parts + .iter() + .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit())) +} + +fn is_content_hash(value: &str) -> bool { + let Some(hex) = value.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 + && hex + .chars() + .all(|ch| ch.is_ascii_digit() || ('a'..='f').contains(&ch)) +} + +#[cfg(test)] +mod tests { + use super::{ResourceId, is_content_hash, is_resource_id, is_revision}; + + #[test] + fn validates_resource_ids() { + assert!(is_resource_id("nana.character.nana")); + assert!(is_resource_id("nana.item.red_hairpin")); + assert!(!is_resource_id("Nana.Character")); + assert!(!is_resource_id("nana..item")); + let _typed = ResourceId("nana.character.nana".to_owned()); + } + + #[test] + fn validates_revisions_and_hashes() { + assert!(is_revision("1.2.0")); + assert!(!is_revision("1.2")); + assert!(is_content_hash(&format!("sha256:{}", "a".repeat(64)))); + assert!(!is_content_hash("sha256:abc")); + } +} diff --git a/crates/nana-engine/Cargo.toml b/crates/nana-engine/Cargo.toml new file mode 100644 index 0000000..e555502 --- /dev/null +++ b/crates/nana-engine/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nana-engine" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +nana-domain.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/crates/nana-engine/src/lib.rs b/crates/nana-engine/src/lib.rs new file mode 100644 index 0000000..d919a06 --- /dev/null +++ b/crates/nana-engine/src/lib.rs @@ -0,0 +1,48 @@ +use nana_domain::{RuntimeState, StateDelta}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ReduceError { + #[error("state delta is not implemented yet: {0} operation(s)")] + NotImplemented(usize), +} + +/// M0 contract seam. M1 replaces this placeholder with the only authoritative +/// state mutation path in the application. +pub fn apply_delta(state: &RuntimeState, delta: &StateDelta) -> Result { + if delta.ops.is_empty() { + Ok(state.clone()) + } else { + Err(ReduceError::NotImplemented(delta.ops.len())) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use nana_domain::{RuntimeState, StateDelta}; + + use super::apply_delta; + + #[test] + fn empty_delta_is_identity() { + let state = RuntimeState { + story_id: "story_demo".to_owned(), + current_node: "node_001".to_owned(), + current_branch: "branch_main".to_owned(), + world_flags: BTreeMap::new(), + relationships: BTreeMap::new(), + relationship_states: Vec::new(), + promises: Vec::new(), + knowledge: Vec::new(), + items: Vec::new(), + clocks: Vec::new(), + checks: Vec::new(), + }; + assert_eq!( + apply_delta(&state, &StateDelta { ops: Vec::new() }), + Ok(state) + ); + } +} diff --git a/crates/nana-runtime/Cargo.toml b/crates/nana-runtime/Cargo.toml new file mode 100644 index 0000000..7c8a1e7 --- /dev/null +++ b/crates/nana-runtime/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nana-runtime" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +nana-domain.workspace = true +openlapp.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/crates/nana-runtime/src/lib.rs b/crates/nana-runtime/src/lib.rs new file mode 100644 index 0000000..9489587 --- /dev/null +++ b/crates/nana-runtime/src/lib.rs @@ -0,0 +1,53 @@ +use std::collections::VecDeque; + +use nana_domain::{TurnFailure, TurnFailureCode, TurnRequest, TurnResult}; +use thiserror::Error; + +pub const LAPP_BASELINE_COMMIT: &str = "5ba3c659e1536ec4bee16340faca603940a5cb17"; + +#[derive(Debug, Error)] +pub enum ProviderError { + #[error("no recorded response remains")] + FixtureExhausted, + #[error("LAPP profile could not be loaded: {0}")] + Profile(String), +} + +pub trait TurnProvider { + fn complete_turn(&mut self, request: &TurnRequest) -> Result; +} + +#[derive(Debug)] +pub struct FakeProvider { + responses: VecDeque, +} + +impl FakeProvider { + #[must_use] + pub fn new(responses: impl IntoIterator) -> Self { + Self { + responses: responses.into_iter().collect(), + } + } +} + +impl TurnProvider for FakeProvider { + fn complete_turn(&mut self, _request: &TurnRequest) -> Result { + self.responses + .pop_front() + .ok_or(ProviderError::FixtureExhausted) + } +} + +pub fn load_default_lapp_profile() -> Result { + openlapp::load_default_profile().map_err(|error| ProviderError::Profile(error.to_string())) +} + +#[must_use] +pub fn provider_failure(message: impl Into) -> TurnFailure { + TurnFailure { + code: TurnFailureCode::ProviderUnavailable, + message: message.into(), + retryable: true, + } +} diff --git a/crates/nana-store/Cargo.toml b/crates/nana-store/Cargo.toml new file mode 100644 index 0000000..05bee4f --- /dev/null +++ b/crates/nana-store/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nana-store" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +nana-domain.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/crates/nana-store/src/lib.rs b/crates/nana-store/src/lib.rs new file mode 100644 index 0000000..a0449e5 --- /dev/null +++ b/crates/nana-store/src/lib.rs @@ -0,0 +1,20 @@ +use nana_domain::{RuntimeState, StoryNode}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StoreError { + #[error("story not found: {0}")] + StoryNotFound(String), + #[error("storage backend is not initialized")] + NotInitialized, +} + +pub trait StoryStore: Send + Sync { + fn append_node(&self, node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError>; + + fn load_state( + &self, + story_id: &str, + branch_id: &str, + ) -> Result; +} diff --git a/docs/OWNERSHIP.md b/docs/OWNERSHIP.md new file mode 100644 index 0000000..602cf5e --- /dev/null +++ b/docs/OWNERSHIP.md @@ -0,0 +1,18 @@ +# 并行开发所有权 + +契约冻结提交之后,目录即所有权边界。 + +| 角色 | 独占目录 | 允许工作 | +|---|---|---| +| 集成人 | 根配置、`contracts/`、`docs/adr/`、`src-tauri/`、入口与 CI | 契约、生成物、接线、验收 | +| Core | `crates/nana-domain/`、`crates/nana-engine/`、`crates/nana-store/` | 类型、reducer、规则、SQLite、重放 | +| Runtime | `crates/nana-runtime/`、`fixtures/turns/` | LAPP、上下文、工具循环、Turn Engine | +| Experience | `src/`、`content/nana-demo/` | Vue、演出、试玩内容 | + +规则: + +1. 根 `Cargo.toml`、`package.json`、锁文件、生成代码和公共契约仅由集成人修改。 +2. 新依赖和公共契约变化先提交短变更请求,不跨目录顺手修改。 +3. 禁止无关重构、全仓格式化和依赖大版本升级。 +4. 每个分支必须从同一份契约冻结提交开始。 +5. 合并顺序固定为契约/fixtures → Core → Runtime → Experience → E2E。 diff --git a/docs/adr/0001-rust-contract-source.md b/docs/adr/0001-rust-contract-source.md new file mode 100644 index 0000000..39c0290 --- /dev/null +++ b/docs/adr/0001-rust-contract-source.md @@ -0,0 +1,22 @@ +# ADR 0001:Rust 是公共契约的唯一事实来源 + +- 状态:已接受 +- 日期:2026-07-28 + +## 决策 + +公共数据模型只在 `nana-domain` 中定义。JSON Schema 与 TypeScript DTO 由 +`nana-contracts` 生成并提交到 `contracts/`。CI 同时检查源文件哈希和生成结果。 + +禁止手工维护第二套同名模型。前端只能导入 `contracts/ts`。 + +## 原因 + +资源校验、状态 reducer、存档和 Tauri 命令都运行在 Rust 侧。让 Rust 成为唯一事实来源 +可以避免多个子智能体分别发明不兼容的字段和枚举。 + +## 后果 + +- 任何契约变更必须重新生成 Schema 与 TypeScript。 +- 契约变更必须连同 canonical fixtures 一起评审。 +- 生成物漂移会使 `pnpm verify` 失败。 diff --git a/docs/adr/0002-player-view-boundary.md b/docs/adr/0002-player-view-boundary.md new file mode 100644 index 0000000..1550cd8 --- /dev/null +++ b/docs/adr/0002-player-view-boundary.md @@ -0,0 +1,29 @@ +# ADR 0002:RuntimeState 与 PlayerView 是安全边界 + +- 状态:已接受 +- 日期:2026-07-28 + +## 决策 + +前端永远不接收完整 `RuntimeState`。所有界面通过 Rust 生成的 `PlayerView` 或同等 +脱敏 DTO 读取数据。 + +`PlayerView` 只包含: + +- 当前演出节拍和建议; +- 玩家当前可访问的持有物; +- 玩家已经获得的知识; +- 已接受且允许展示的许诺; +- 模糊化关系阶段; +- 可回溯节点摘要。 + +## 原因 + +这条边界从结构上阻止 NPC 隐藏物品、未揭示事实、精确关系数值、骰点和剧情节点泄漏。 +仅靠前端“不要显示”不足以形成安全保证。 + +## 后果 + +- Vue 不允许导入或缓存 `RuntimeState`。 +- PlayerView 泄密测试属于跨模块硬门槛。 +- 新界面字段必须先证明玩家有权知道。 diff --git a/docs/adr/0003-lapp-boundary.md b/docs/adr/0003-lapp-boundary.md new file mode 100644 index 0000000..572aef5 --- /dev/null +++ b/docs/adr/0003-lapp-boundary.md @@ -0,0 +1,23 @@ +# ADR 0003:供应商能力只通过 lapp-rs + +- 状态:已接受 +- 日期:2026-07-28 + +## 决策 + +供应商协议、模型选择、凭据解析与网络调用全部属于 `lapp-rs`。`nana-story` 只在 +`nana-runtime` 中保留窄适配层,不引入供应商 SDK。 + +开发基线固定为: + +```text +openlapp/lapp-rs@5ba3c659e1536ec4bee16340faca603940a5cb17 +``` + +本地模型仅视为 HTTP 服务;应用不安装、下载、启动或管理 Ollama/LM Studio。 + +## 后果 + +- 普通 CI 使用确定性 Fake Provider。 +- 真实模型只参与独立冒烟测试。 +- `lapp-rs` 升级必须先通过协议夹具回归。 diff --git a/docs/mockups/01-main-gameplay.png b/docs/mockups/01-main-gameplay.png new file mode 100644 index 0000000..5cc4041 Binary files /dev/null and b/docs/mockups/01-main-gameplay.png differ diff --git a/docs/mockups/02-inventory-drawer.png b/docs/mockups/02-inventory-drawer.png new file mode 100644 index 0000000..6ad3bd9 Binary files /dev/null and b/docs/mockups/02-inventory-drawer.png differ diff --git a/docs/mockups/03-character-relationship.png b/docs/mockups/03-character-relationship.png new file mode 100644 index 0000000..2098f9b Binary files /dev/null and b/docs/mockups/03-character-relationship.png differ diff --git a/docs/mockups/04-story-rewind.png b/docs/mockups/04-story-rewind.png new file mode 100644 index 0000000..7654e00 Binary files /dev/null and b/docs/mockups/04-story-rewind.png differ diff --git a/docs/nana-story-v1-development-plan.md b/docs/nana-story-v1-development-plan.md new file mode 100644 index 0000000..8b7eda2 --- /dev/null +++ b/docs/nana-story-v1-development-plan.md @@ -0,0 +1,669 @@ +# `nana-story` V1 开发计划 + +> 中文名:《听娜娜讲故事》 +> 计划版本:0.1 +> 制定日期:2026-07-28 +> 目标平台:Windows 桌面端 +> 执行模型:1 个集成人 + 最多 3 个并行子智能体 + +## 1. 交付目标 + +V1 不是“通用 AI Galgame 平台”的完整形态,而是一段能够证明整个产品闭环的可玩纵切: + +1. 用户通过 LAPP 选择并调用聊天模型; +2. 导入或使用内置的娜娜试玩资源包; +3. 创建故事并进行 20~30 分钟的自由输入式角色扮演; +4. 经历至少一次隐藏的简化 CoC7 判定; +5. 建立一项重要许诺,并让行为影响信赖、希望等关系维度; +6. 获得、使用或转移一件带隐藏背景的持有物; +7. 随时查看自己的持有物、已知线索和已接受许诺; +8. 查看模糊化的六维关系雷达图; +9. 每轮自动保存,退出后能够恢复; +10. 回溯到旧节点继续时创建新分支,并保留原线路。 + +V1 的成功标准不是资源数量,而是: + +> 从创建故事到完成纵切,全程不需要开发者干预;状态可重放、隐藏信息不泄漏、回溯不破坏旧分支、异常不会留下半轮存档。 + +## 2. 已冻结的产品范围 + +### 2.1 技术与平台 + +- 仓库名:`nana-story`。 +- 首发 Windows 桌面端;安卓以后再考虑。 +- 技术栈: + - Tauri 2 + - Vue 3 + - TypeScript + - Vite + - Naive UI + - Pinia + - Vue Router +- 管理界面可以使用 Naive UI,游玩界面采用自定义视觉实现。 +- 管理端、游玩端和运行时位于同一个 Tauri 桌面进程,不引入 daemon、sidecar 或本地网关。 + +### 2.2 模型与 LAPP 边界 + +- 供应商协议、模型选择、凭据解析和网络调用由 `lapp-rs` 负责。 +- `nana-story` 不直接依赖任何供应商 SDK。 +- `nana-story` 只提供必要的 LAPP 查看、选择、连接测试和简单管理能力。 +- 本地模型只作为 HTTP 服务接入。 +- 不负责安装、下载、启动、停止、更新或管理 Ollama、LM Studio 等本地模型运行器。 +- 开发时可使用相邻仓库的 path dependency;发布构建必须固定到明确的 `lapp-rs` 提交,不能复制其协议语义。 + +### 2.3 游戏交互 + +- 自由输入为主。 +- 每轮可提供 2~3 个可编辑行动建议;点击后只填入输入框,不直接提交。 +- 提供“继续”按钮,表示沉默、观察、等待或允许 NPC 继续行动。 +- AI 不得替玩家决定说话、主动行动或内心想法。 +- 时间按照实际行动推进,不按消息轮数推进。 +- 高风险行动需要先给出叙事风险提示,再执行隐藏的简化 CoC7 判定。 +- 玩家默认看不到骰点、技能数值和精确关系变化。 +- 回复由若干短演出节拍构成,而不是一整块聊天气泡。 + +### 2.4 角色、关系与剧情 + +- V1 只有一个完整角色:娜娜。 +- 剧情模块可以包含会说话的轻量配角,但不为其建立完整关系系统、长期记忆或自主调度。 +- 六维关系: + - 好感 `affinity` + - 信赖 `trust` + - 希望 `hope` + - 尊重 `respect` + - 亲密 `intimacy` + - 依恋 `attachment` +- 信赖主要来自已经发生的行为。 +- 希望主要来自被接受的许诺和共同未来计划。 +- 重要许诺必须保存为独立对象,不能简化成一次关系数值变化。 +- 怨恨、恐惧、愧疚、嫉妒等保存为带来源的关系状态,不占六维。 +- 创建故事时绑定剧情模块;V1 不支持中途增删模块。 + +### 2.5 持有物、知识与分支 + +- 物品定义、物品实例和角色认知必须分离。 +- 所有权 `owner` 与当前持有者 `holder` 必须分离。 +- 玩家可以随时读取自己当前可访问的持有物。 +- NPC 的真实持有物不能直接暴露;只能显示公开可见或玩家已经知道的内容。 +- V1 不处理负重、体积、格子或背包容量。 +- 每轮自动保存。 +- 从旧节点继续时创建新分支。 +- V1 不做分支合并,也不建立跨分支记忆。 + +## 3. V1 明确不做 + +- 多个完整角色及多角色发言调度。 +- 导演 Agent、自动总结 Agent 或运行时多 Agent 编排。 +- 完整 CoC7 职业、成长、战斗、理智、追逐和幸运消费。 +- Live2D、语音、口型、动态镜头和复杂动画。 +- 分支合并和跨分支记忆。 +- 中途增删剧情模块。 +- 负重、体积、容量、制作和物品经济。 +- 内置资源编辑器或可视化剧情编辑器。 +- Steam Workshop 正式接入、DLC 支付、社区付费内容抽成。 +- 云同步、账户、多人或跨设备存档。 +- Android、Web 和多语言发布。 +- 模型下载器、模型启动器和本地模型进程管理。 +- 任意脚本、任意插件或任意外部工具执行。 + +V1 只保证资源包格式未来可以承接 Workshop/DLC,不实现市场本身。 + +## 4. 总体架构 + +### 4.1 三类事实 + +| 层级 | 内容 | 权威来源 | +|---|---|---| +| 创作资源 | CharacterCard、WorldBook、Persona、PlotModule、ItemSpec 和素材引用 | 不可变的版本化资源 | +| 分支状态 | 世界状态、关系、许诺、知识、持有物、时钟、判定记录 | 根状态/检查点 + 节点 delta | +| 上下文投影 | 当前意图、命中的世界书、最近演出、提示词 | 每轮临时编译,可重建 | + +任何模型推测、检索结果和临时提示词都不是持久事实。需要持续存在的变化必须经过类型化操作,由状态 reducer 验证后写入当前分支。 + +### 4.2 单一权威写入路径 + +```mermaid +flowchart TD + A["玩家输入"] --> B["上下文编译"] + B --> C["单模型工具循环"] + C --> D["暂存工具结果与状态操作"] + D --> E["规则校验与 reducer"] + E --> F["原子提交 StoryNode"] + F --> G["生成 PlayerView"] + G --> H["Vue 演出"] +``` + +约束: + +- 模型和前端都不能直接修改 `RuntimeState`。 +- reducer 是状态变化的唯一权威入口。 +- 一轮生成要么完整提交,要么完全不提交。 +- 模型失败、取消、超时或畸形输出不得留下半轮状态。 +- 同一玩家行动的重新生成复用原判定记录;推骰属于新行动和新节点。 + +### 4.3 前后端安全边界 + +前端不得得到完整 `RuntimeState`,只能得到经过投影的 `PlayerView`: + +```text +PlayerView +├─ 当前演出节拍 +├─ 可编辑行动建议 +├─ 玩家可访问的持有物 +├─ 已知线索 +├─ 已接受许诺 +├─ 模糊化六维关系 +└─ 可回溯节点摘要 +``` + +这条边界用于结构性防止: + +- NPC 隐藏物品泄漏; +- 未揭示世界事实泄漏; +- 精确关系数值泄漏; +- 内部骰点和难度泄漏; +- 未触发的剧情节点泄漏。 + +### 4.4 推荐持久化 + +V1 默认采用嵌入式 SQLite,由 Rust 侧独占访问。 + +- 内容包以经过校验的不可变文件形式安装到应用数据目录。 +- SQLite 保存故事、资源绑定、节点、分支、检查点和迁移版本。 +- 节点中的状态操作使用版本化、类型化 JSON。 +- 每隔固定节点数创建检查点,恢复时使用“最近检查点 + 后续 delta”。 +- SQLite 事务同时提交用户输入、模型演出、工具结果、判定记录和状态 delta。 + +建议最小表: + +```text +stories +story_bindings +story_nodes +story_branches +state_checkpoints +installed_packs +schema_migrations +``` + +不要在 V1 过早拆出几十张关系、物品、许诺明细表。分支事件流才是事实来源,当前状态是可重建投影。 + +### 4.5 模型运行时 + +V1 游戏运行时仍然只有一个模型,不引入导演 Agent。 + +每轮流程: + +1. 读取当前节点、资源绑定和重建后的运行时状态; +2. 按优先级编译上下文; +3. 调用 `lapp-rs` 选中的聊天模型; +4. 模型按需调用判定、关系、许诺、知识、持有物、时钟等领域工具; +5. Rust 验证工具参数并写入暂存事务; +6. 模型根据工具返回继续生成短演出节拍和行动建议; +7. 校验演出协议与全部状态操作; +8. 原子提交新节点; +9. 生成脱敏后的 `PlayerView` 给前端。 + +上下文优先级: + +1. 玩家自主权和输出协议; +2. 当前角色卡、Persona 与场景; +3. 当前关系、许诺、知识、持有物和剧情压力; +4. 命中的世界书条目; +5. 当前分支最近演出; +6. 非关键风格参考。 + +V1 不做向量记忆库。世界书先采用确定性的标签、关键词和条件触发;长上下文压缩只能作为可丢弃缓存,不能成为新的事实来源。 + +## 5. 仓库结构与代码所有权 + +建议从第一天就使用 Rust workspace,把并行边界变成目录边界: + +```text +nana-story/ +├─ src/ # Vue 应用 +│ ├─ app/ # 路由、全局状态、真实/Mock adapter +│ ├─ features/play/ # 主演出屏 +│ ├─ features/inventory/ # 持有物、线索、许诺 +│ ├─ features/relationship/ # 关系档案 +│ └─ features/history/ # 回溯与分支 +├─ src-tauri/ # Tauri 外壳与窄命令 +├─ crates/ +│ ├─ nana-domain/ # 资源、状态、delta、PlayerView +│ ├─ nana-engine/ # reducer、规则、简化 CoC7 +│ ├─ nana-store/ # SQLite、节点、检查点、恢复 +│ └─ nana-runtime/ # LAPP、上下文、工具循环、Turn Engine +├─ contracts/ # 生成的 JSON Schema 与 TS 类型 +├─ content/nana-demo/ # 试玩纵切资源包 +├─ fixtures/ # 稳定的契约和模型响应样例 +├─ tests/e2e/ # 跨模块验收 +└─ docs/adr/ # 已接受架构决策 +``` + +单一契约源: + +```text +Rust serde 类型 +→ 生成 JSON Schema +→ 生成 TypeScript DTO +→ CI 检查生成物是否漂移 +``` + +禁止同时手写 Rust、Schema 和 TypeScript 三套同名模型。 + +## 6. 多子智能体并行方案 + +### 6.1 推荐拓扑 + +并行度维持在 1 个集成人 + 3 个子智能体。 + +| 角色 | 独占范围 | 主要职责 | +|---|---|---| +| 集成人 | 公共契约、根配置、入口、生成物、跨模块接线、最终验收 | 拆任务、冻结契约、日常合并、拒绝范围扩张 | +| 子智能体 A:Core | `nana-domain`、`nana-engine`、`nana-store` | 类型、reducer、规则、SQLite、分支重放 | +| 子智能体 B:Runtime | `nana-runtime` | LAPP、上下文编译、工具循环、Turn Engine | +| 子智能体 C:Experience | `src/`、`content/nana-demo/` | Vue 界面、演出、官方 Fixture、娜娜纵切内容 | + +测试靠近各自模块编写;跨模块 E2E 和契约生成由集成人负责。 + +不建议同时开启 5~8 个编码子智能体。这个项目的瓶颈是共享语义而不是代码量,过高并行度会把收益变成 Schema、状态提交和 UI DTO 的返工。 + +### 6.2 必须先冻结的契约 + +进入正式并行前必须合并: + +- `ResourceHeader` +- `CharacterCard` +- `WorldBook` +- `Persona` +- `PlotModule` +- `ItemSpec` 与 `ItemInstance` +- `Story`、`StoryBinding`、`StoryNode` +- `RuntimeState` +- 所有状态操作的判别联合 +- `PresentationBeat` +- `PlayerView` +- `TurnRequest`、`TurnResult`、`TurnFailure` +- 五组 canonical fixtures: + - 普通对话; + - 隐藏判定; + - 许诺建立与履行; + - 物品转移且 NPC 尚未知情; + - 回溯后分叉。 + +### 6.3 Worktree 与分支 + +每个子智能体必须使用独立 Git worktree: + +```text +integration/v1 +agent/core +agent/runtime +agent/experience +``` + +规则: + +- 所有分支从同一份已冻结契约提交开始。 +- 每个子智能体只修改分配的目录。 +- 根 `Cargo.toml`、`package.json`、锁文件、CI、Tauri 入口、Vue 入口和生成代码只由集成人修改。 +- 子智能体需要新增依赖或修改公共契约时,提交短小的变更请求,不直接跨目录修改。 +- 禁止无关重构、全仓格式化和顺手升级依赖。 +- 每天至少合并一次小的可验证增量,不在阶段末进行巨型合并。 +- 只有集成人解决跨模块冲突。 + +### 6.4 子智能体任务包模板 + +每个任务必须包含: + +```text +目标: +基线提交: +允许修改的目录: +输入契约版本: +必须新增的测试: +验收命令: +明确不做: +交付时需要说明的风险: +``` + +没有这些信息的任务不应分派给子智能体。 + +## 7. 里程碑与并行波次 + +基准工期为 25 个有效开发日,约 5 周。AI 子智能体可以压缩编码时间,但不能取消契约、纵切联调和 Windows 发布验证。 + +### M0:工程骨架与契约冻结(第 1~2 日) + +交付: + +- 初始化 Tauri 2 + Vue 3 工程和 Rust workspace; +- 接入相邻 `lapp-rs`,固定开发基线; +- 建立 `pnpm verify` 或等价的一键校验; +- 定义 Rust 领域类型、生成 JSON Schema 和 TS DTO; +- 建立 canonical fixtures; +- 内置最小娜娜资源包; +- 写入首批 ADR。 + +演示: + +- Windows 桌面窗口可以启动; +- 前端通过窄命令读取应用版本和一份静态 `PlayerView`; +- 最小娜娜资源包能够加载。 + +硬门槛: + +- CI 可重复运行; +- 非法资源返回稳定错误; +- ID、版本、哈希、依赖和引用可校验; +- Rust、Schema、TS 不存在多个事实来源。 + +并行: + +- 集成人:契约与仓库骨架; +- A:CI 和 Rust workspace; +- B:Fake LAPP Provider/录制模型响应夹具; +- C:按四张原型搭静态 UI 外壳。 + +### M1:确定性状态内核(第 3~6 日) + +交付: + +- `RuntimeState`; +- 纯 reducer; +- 六维关系与单轮限幅; +- 许诺生命周期; +- 知识与可见性; +- 物品 owner/holder、位置、取得方式; +- 时钟; +- 简化 CoC7 判定记录; +- `PlayerView` 投影; +- SQLite 节点、delta 和检查点。 + +演示: + +- 不调用模型,仅通过固定命令完成: + - 获得物品; + - 作出许诺; + - 改变关系; + - 推进时钟; + - 执行一次判定; + - 保存、关闭、恢复。 + +硬门槛: + +- 同一根状态 + 同一组 delta 必须产生完全相同的 `RuntimeState` 和 `PlayerView`; +- delta 重放不能重复结算; +- 关系变化会被规则限幅; +- owner/holder 语义正确; +- 玩家无法读取 NPC 未知持有物; +- 同一行动重新生成时可以复用判定记录。 + +并行: + +- A:domain、engine、store; +- B:基于 fixture 实现 runtime 接口和 Fake Provider; +- C:前端使用同一份 `PlayerView` fixture 完成完整假数据交互。 + +### M2:单轮模型运行循环(第 5~10 日,与 M1 后半段重叠) + +交付: + +- LAPP profile 读取、模型选择和连接测试; +- 非流式与流式聊天适配; +- 上下文编译器; +- 领域工具目录; +- 工具循环和最大轮次保护; +- `PresentationBeat`; +- 暂存事务、校验、提交和回滚; +- 取消、超时、畸形输出和一次安全重试。 + +演示: + +- 用户输入一句自由文本; +- 娜娜以短节拍回应; +- 表情发生一次可选变化; +- 生成 2~3 个可编辑建议; +- 一次需要判定的行动经过工具返回后继续叙述; +- 整轮状态一次性提交。 + +硬门槛: + +- 模型和 UI 都不能直接写 `RuntimeState`; +- 工具失败、取消和断流不产生半轮节点; +- 模型不能替玩家决定主动言行和内心; +- 高风险行动先有风险提示; +- 凭据、完整上游错误体和隐藏状态不进入日志或前端 DTO; +- CI 使用 Fake Provider,不依赖真实付费模型。 + +并行: + +- A:补齐 reducer 和 store 的运行时接口; +- B:实现 LAPP adapter、上下文和工具循环; +- C:把主演出屏从 fixture adapter 切到真实 Turn API。 + +### M3:自动存档、回溯和分支(第 8~14 日) + +交付: + +- 每轮原子自动保存; +- 分支头和节点父子关系; +- 从任意旧节点创建新分支; +- 旧线路保留; +- 故事恢复; +- 同输入重新生成与推骰的不同语义; +- 简洁故事时间线和完整回溯页。 + +演示: + +- 连续玩 5 轮后退出; +- 重启恢复; +- 回到第 2 轮并继续; +- 原分支和新分支同时存在; +- 两条分支的持有物、许诺和关系状态互不污染。 + +硬门槛: + +- 每轮只出现一次有效提交; +- 重启后的状态与提交前一致; +- 回溯不会修改旧节点; +- 重新生成复用原判定; +- 推骰创建新行动和新判定记录; +- 500 节点以内的分支恢复不需要扫描无关分支。 + +并行: + +- A:分支、检查点、恢复和迁移; +- B:重新生成、推骰与运行时事务; +- C:故事回溯 UI 和错误恢复交互。 + +### M4:完整玩法系统与四个界面(第 11~18 日) + +交付: + +- 主游戏界面; +- 持有物/线索/许诺侧栏; +- 六维关系档案; +- 故事回溯页; +- 角色卡、世界书、Persona、剧情模块和 ItemSpec 的导入校验; +- 轻量配角; +- 场景、立绘、表情和姿态保持; +- “自由输入 / 建议 / 继续”三种操作。 + +演示: + +- 四张界面原型对应的真实交互全部接入运行状态; +- 玩家查看自己的持有物; +- NPC 未知物品不会出现; +- 重要许诺能从 accepted 发展到 fulfilled 或 broken; +- 关系雷达变化但不显示精确数字; +- 轻量配角可以参与一段事件但不会获得完整关系系统。 + +硬门槛: + +- 所有 UI 数据只来自 `PlayerView` 或其他脱敏 DTO; +- 不显示骰点、检定难度和精确关系增量; +- UI 不把消息和演出节拍混为一套结构; +- 静态资源加载失败有占位和明确错误; +- 导入包不能通过路径穿越写出目标目录。 + +并行: + +- A:资源导入、规则系统和 PlayerView; +- B:世界书触发、剧情工具和轻量配角上下文; +- C:四个界面和演出状态机。 + +### M5:娜娜 20~30 分钟纵切(第 17~21 日) + +纵切至少包含: + +- 一个明确开场目标; +- 一次普通自由对话; +- 一次隐藏判定; +- 一次玩家可感知的风险提示; +- 一个被接受的重要许诺; +- 一次用行动证明或破坏许诺; +- 一件具有隐藏背景的物品; +- 一次 owner/holder 不同的物品状态; +- 一次关系路线分化; +- 一个剧情时钟; +- 一次轻量配角发言; +- 一次回溯; +- 至少两个结果方向。 + +硬门槛: + +- 无开发者操作可以从开场玩到结果; +- 条件触发没有死路; +- 角色不泄露玩家尚未获得的知识; +- 不同选择产生可回放的不同状态; +- 引擎规则中不得硬编码 `nana` 或纵切事件 ID; +- 使用另一个最小反例资源夹具证明引擎仍然通用。 + +本阶段停止增加系统功能。所有子智能体只处理纵切暴露的问题。 + +### M6:Windows 发布前加固(第 22~25 日) + +交付: + +- Windows 安装包; +- 崩溃和断电恢复; +- 存档迁移夹具; +- 模型超时、断网、限流、拒绝、畸形工具调用处理; +- 长对话上下文预算测试; +- 日志脱敏; +- 内容包边界和尺寸限制; +- 用户可理解的错误页; +- 最小发布说明和已知限制。 + +硬门槛: + +- 全新 Windows 环境能够安装、启动和卸载; +- 配置一个 LAPP 模型后能够完成纵切; +- 进程被强制关闭后不会产生半轮节点; +- 旧 Schema 存档夹具能够迁移或给出明确的不兼容提示; +- 凭据不进入前端状态、日志、崩溃报告或存档; +- 所有 canonical fixtures、单元测试和跨模块验收通过。 + +## 8. 合并顺序与质量门 + +每个并行波次按以下顺序合并: + +1. 契约与 fixtures; +2. domain/engine/store; +3. runtime; +4. client adapter; +5. 跨模块 E2E; +6. 纵切内容更新。 + +任何合并都必须跑同一条最小跨模块场景: + +```text +创建故事 +→ 三轮交互 +→ 一次隐藏判定 +→ 获得物品 +→ 建立许诺 +→ 关闭并重启 +→ 回溯 +→ 从旧节点继续 +→ 校验两个分支互不污染 +``` + +阶段门禁: + +- M1 未通过确定性重放,不接正式存档 UI。 +- M2 未实现“校验后一次性提交”,不允许模型修改运行时状态。 +- M3 未通过分支隔离,不开放回溯入口。 +- M5 未跑通完整纵切,不扩展第二完整角色、编辑器、Workshop 或 Live2D。 + +## 9. 测试矩阵 + +| 层级 | 主要测试 | +|---|---| +| Domain | Schema、ID/引用、版本/哈希、资源依赖、反序列化错误 | +| Engine | reducer、限幅、许诺生命周期、知识可见性、物品 owner/holder、CoC7 | +| Store | 原子提交、重放、检查点、分支隔离、迁移、损坏恢复 | +| Runtime | Fake Provider、工具循环、上下文预算、取消、超时、畸形输出 | +| Security | PlayerView 泄密、日志脱敏、内容包路径穿越、凭据边界 | +| Frontend | 演出状态机、建议编辑、继续、侧栏、雷达、回溯 | +| Cross-module | 完整最小场景、重启恢复、回溯继续、真实 LAPP 冒烟 | +| Content | 条件可达性、无死路、隐藏事实、不同结果、角色一致性 | + +真实模型测试不作为普通 CI 的前提。CI 使用确定性 Fake Provider;真实模型只运行独立冒烟和录制回归。 + +## 10. 主要风险与控制 + +| 风险 | 控制 | +|---|---| +| 多个子智能体各自发明 Schema | Rust 类型为唯一事实来源;契约目录独占;变更必须更新 fixtures | +| UI 或模型直接修改状态 | 所有修改必须经过类型化工具和唯一 reducer | +| 模型输出不稳定 | 工具调用、结构校验、最大循环次数、一次安全重试、失败不提交 | +| 假数据与真实接口分裂 | UI 和运行时共用 canonical `PlayerView` fixtures | +| 分支恢复不确定 | 纯 reducer;固定 delta 顺序;每次重放做哈希校验 | +| 重生成刷骰 | 判定绑定玩家行动;重新生成复用,推骰创建新行动 | +| 隐藏信息泄漏 | 后端生成 `PlayerView`;前端永远不获得完整状态 | +| `lapp-rs` 仍处于 0.1 阶段 | 固定提交;包一层窄 adapter;用三种协议夹具做回归 | +| 本地模型不支持工具调用 | V1 启动前做能力检测并明确提示;不在 V1 增加第二套自由文本解析运行时 | +| 子智能体大范围重构 | 目录所有权、短任务包、禁止无关改动、每日小合并 | +| Windows 问题最后才出现 | M0 即建立 Windows 构建;每个阶段做一次安装/启动冒烟 | +| 纵切内容被写成娜娜专用代码 | 引擎只识别通用标签和资源引用;增加非娜娜反例夹具 | + +## 11. V1 Definition of Done + +满足以下全部条件才称为 V1: + +- Windows 安装包可用; +- 使用 LAPP 可以选择云端或本地 HTTP 模型并通过连接测试; +- 内置娜娜试玩包可直接创建故事; +- 20~30 分钟纵切可从头玩到至少两个结果方向; +- 自由输入、可编辑建议和“继续”都能稳定完成一轮; +- 六维关系、许诺、知识、持有物、时钟和隐藏判定都在纵切中真实生效; +- 每轮原子自动保存; +- 重启恢复一致; +- 回溯后建立新分支并保留旧线; +- 同一行动重新生成不重新掷骰; +- NPC 隐藏状态、精确关系值、骰点和凭据不泄漏; +- 模型取消、超时和畸形输出不产生半轮状态; +- canonical fixtures、自动测试和 Windows 冒烟全部通过; +- 没有引入 V1 明确不做的功能。 + +## 12. 开工顺序 + +第一批实际开发任务按下面顺序创建: + +1. 初始化 `nana-story` 仓库、Tauri/Vue 工程和 Rust workspace; +2. 写入架构 ADR 与目录所有权; +3. 定义 `ResourceHeader`、资源绑定、`StoryNode`、`RuntimeState`、状态操作、`PresentationBeat` 和 `PlayerView`; +4. 生成 JSON Schema、TypeScript DTO 和 canonical fixtures; +5. 同时启动三个独立 worktree: + - Core:reducer + SQLite; + - Runtime:Fake Provider + Turn API; + - Experience:静态主演出屏 + fixture adapter; +6. 在 M1 确定性重放通过后,接入真实 `lapp-rs` 和第一条完整游戏回合。 + +第一条集成目标应当非常小: + +> 玩家输入“我答应天亮前回来”,娜娜回应并接受许诺;引擎创建 Promise、提高希望、保存节点;关闭应用后恢复;回溯到许诺之前继续,旧分支仍然存在。 + +它同时覆盖角色演出、工具调用、许诺、关系、存档、恢复和分支,是最适合作为 `nana-story` 第一条端到端主线的开发样例。 diff --git a/docs/status/2026-07-28-m0.md b/docs/status/2026-07-28-m0.md new file mode 100644 index 0000000..5d4ea79 --- /dev/null +++ b/docs/status/2026-07-28-m0.md @@ -0,0 +1,43 @@ +# M0 开工状态 + +日期:2026-07-28 + +## 已完成 + +- 初始化 Git、Tauri 2、Vue 3、TypeScript 与 Rust workspace。 +- 固定 `lapp-rs` 基线提交。 +- 建立 `nana-domain`、`nana-engine`、`nana-store`、`nana-runtime` 和 + `nana-contracts` 边界。 +- 定义第一批公共领域契约。 +- 提交 15 份 JSON Schema、TypeScript DTO 和源文件漂移检查。 +- 建立普通对话、隐藏判定、许诺、NPC 未知物品转移和回溯分叉 fixtures。 +- 建立最小娜娜内容包“天亮之前”。 +- 建立静态 `PlayerView` 主演出屏。 +- 建立版本、试玩包摘要和静态 `PlayerView` 三个窄 Tauri 命令。 +- 写入契约、安全边界与 LAPP 边界 ADR。 +- 建立 Windows CI。 + +## 已通过 + +```text +pnpm contracts:check +pnpm typecheck +pnpm test # 4 tests passed +pnpm build +pnpm verify:web +``` + +## 尚未通过 + +当前 Work 执行环境没有 `cargo`/Rust 1.96,因此以下门禁尚未执行: + +```text +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace +cargo run -p nana-contracts -- --check +pnpm tauri dev +``` + +这意味着 M0 已经开工并形成第一份可合并基线,但在 Rust/Tauri 与 Windows 冒烟完成前, +不标记为正式完成。 diff --git a/fixtures/branches/rewind.json b/fixtures/branches/rewind.json new file mode 100644 index 0000000..fceef23 --- /dev/null +++ b/fixtures/branches/rewind.json @@ -0,0 +1,16 @@ +{ + "sourceBranch": { + "id": "branch_main", + "head": "node_006" + }, + "fork": { + "fromNode": "node_002", + "newBranch": "branch_002", + "newHead": "node_007" + }, + "assertions": { + "sourceHeadUnchanged": "node_006", + "newParent": "node_002", + "crossBranchStateLeak": false + } +} diff --git a/fixtures/deltas/item-transfer-npc-unaware.json b/fixtures/deltas/item-transfer-npc-unaware.json new file mode 100644 index 0000000..9928ca8 --- /dev/null +++ b/fixtures/deltas/item-transfer-npc-unaware.json @@ -0,0 +1,19 @@ +{ + "ops": [ + { + "op": "transfer_item", + "item_id": "item_red_hairpin_001", + "from": "nana", + "to": "player", + "placement": "bag", + "mode": "stolen" + } + ], + "assertions": { + "owner": "nana", + "holder": "player", + "nanaKnowsTransfer": false, + "visibleInPlayerInventory": true, + "hiddenFactsVisible": false + } +} diff --git a/fixtures/deltas/promise-accepted.json b/fixtures/deltas/promise-accepted.json new file mode 100644 index 0000000..e3218d8 --- /dev/null +++ b/fixtures/deltas/promise-accepted.json @@ -0,0 +1,29 @@ +{ + "ops": [ + { + "op": "create_promise", + "promise": { + "id": "promise_return_before_dawn", + "promiser": "player", + "promisee": "nana", + "content": "天亮前一定回来", + "status": "accepted", + "weight": "major", + "created_at": "node_003", + "accepted_at": "node_003", + "resolved_at": null + } + }, + { + "op": "adjust_relationship", + "from": "nana", + "to": "player", + "adjustment": { + "dimension": "hope", + "delta": 4, + "cause": "娜娜接受了玩家关于共同未来的明确许诺", + "judgment_rule": "nana.promises.accepted" + } + } + ] +} diff --git a/fixtures/player-view/initial.json b/fixtures/player-view/initial.json new file mode 100644 index 0000000..5160c06 --- /dev/null +++ b/fixtures/player-view/initial.json @@ -0,0 +1,89 @@ +{ + "storyId": "story_nana_demo", + "nodeId": "node_001", + "branchId": "branch_main", + "sceneId": "old_station_platform", + "sceneTitle": "旧青川站", + "characterName": "娜娜", + "beats": [ + { + "id": "beat_001", + "kind": "narration", + "speaker": null, + "text": "雨水沿着废弃站台的铁棚落下。远处的钟停在四点十七分。", + "visual": { + "character": "nana", + "expression": "uneasy", + "pose": "holding_coat", + "scene": "old_station_platform_rain" + } + }, + { + "id": "beat_002", + "kind": "dialogue", + "speaker": "娜娜", + "text": "你真的会在天亮前回来吗?", + "visual": { + "character": "nana", + "expression": "guarded", + "pose": null, + "scene": null + } + } + ], + "suggestions": [ + { + "id": "suggestion_promise", + "label": "认真答应她", + "draft": "我答应你,天亮前一定回来。" + }, + { + "id": "suggestion_ask", + "label": "追问原因", + "draft": "你为什么这么在意天亮之前?" + }, + { + "id": "suggestion_observe", + "label": "先观察她", + "draft": "我没有立刻回答,先看了看她攥紧外套的手。" + } + ], + "inventory": [ + { + "instanceId": "item_flashlight_001", + "name": "旧手电筒", + "description": "光线不算亮,但足以照清站台下方。", + "quantity": 1, + "placement": "bag", + "condition": "worn" + } + ], + "knowledge": [ + { + "id": "knowledge_station_clock", + "title": "停摆的站钟", + "summary": "站钟停在四点十七分,似乎并非自然损坏。", + "certainty": "confirmed" + } + ], + "promises": [], + "relationship": { + "affinity": "warming", + "trust": "guarded", + "hope": "guarded", + "respect": "warming", + "intimacy": "guarded", + "attachment": "warming", + "updatedAtNode": null + }, + "history": [ + { + "id": "node_001", + "parentId": null, + "branchId": "branch_main", + "label": "雨夜车站", + "isCurrent": true + } + ], + "canContinue": true +} diff --git a/fixtures/turns/hidden-check.json b/fixtures/turns/hidden-check.json new file mode 100644 index 0000000..1823549 --- /dev/null +++ b/fixtures/turns/hidden-check.json @@ -0,0 +1,28 @@ +{ + "request": { + "storyId": "story_nana_demo", + "branchId": "branch_main", + "expectedNodeId": "node_004", + "actionId": "action_search_locker", + "intent": "speak_or_act", + "input": "我趁娜娜看向铁轨时,检查她刚才挡住的储物柜。" + }, + "recordedCheck": { + "id": "check_001", + "action_id": "action_search_locker", + "actor": "player", + "skill": "spot_hidden", + "target": 55, + "difficulty": "hard", + "bonus_dice": 0, + "roll": 24, + "result": "hard_success", + "pushed_from": null, + "node_id": "node_005" + }, + "playerVisible": { + "riskWasNarratedBeforeCheck": true, + "containsRoll": false, + "containsTarget": false + } +} diff --git a/fixtures/turns/regular-dialogue.json b/fixtures/turns/regular-dialogue.json new file mode 100644 index 0000000..d7572dd --- /dev/null +++ b/fixtures/turns/regular-dialogue.json @@ -0,0 +1,15 @@ +{ + "request": { + "storyId": "story_nana_demo", + "branchId": "branch_main", + "expectedNodeId": "node_001", + "actionId": "action_002", + "intent": "speak_or_act", + "input": "这里以前发生过什么?" + }, + "expected": { + "committedNodeId": "node_002", + "stateOps": [], + "beatKinds": ["action", "dialogue"] + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..f2d334b --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 听娜娜讲故事 + + +
+ + + diff --git a/lapp-rs.lock b/lapp-rs.lock new file mode 100644 index 0000000..3920322 --- /dev/null +++ b/lapp-rs.lock @@ -0,0 +1,3 @@ +repository = "https://github.com/openlapp/lapp-rs" +commit = "5ba3c659e1536ec4bee16340faca603940a5cb17" +development_path = "../lapp-rs" diff --git a/package.json b/package.json new file mode 100644 index 0000000..a7ff1b4 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "nana-story", + "private": true, + "version": "0.1.0", + "type": "module", + "packageManager": "pnpm@10.29.2", + "engines": { + "node": ">=24" + }, + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run", + "contracts:generate": "cargo run -p nana-contracts", + "contracts:check": "node scripts/verify-contracts.mjs", + "fmt:check": "cargo fmt --all -- --check", + "clippy": "cargo clippy --workspace --all-targets -- -D warnings", + "test:rust": "cargo test --workspace", + "verify:web": "pnpm contracts:check && pnpm typecheck && pnpm test && pnpm build", + "verify:rust": "pnpm fmt:check && pnpm clippy && pnpm test:rust && cargo run -p nana-contracts -- --check", + "verify": "pnpm verify:web && pnpm verify:rust" + }, + "dependencies": { + "@tauri-apps/api": "^2.11.1", + "naive-ui": "^2.44.1", + "pinia": "^3.0.3", + "vue": "^3.5.40", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.4", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.8", + "@vue/test-utils": "^2.4.11", + "jsdom": "^29.1.1", + "typescript": "~5.9.3", + "vite": "^8.1.5", + "vitest": "^4.1.10", + "vue-tsc": "^3.3.7" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..c017064 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2138 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tauri-apps/api': + specifier: ^2.11.1 + version: 2.11.1 + naive-ui: + specifier: ^2.44.1 + version: 2.44.1(vue@3.5.40(typescript@5.9.3)) + pinia: + specifier: ^3.0.3 + version: 3.0.4(typescript@5.9.3)(vue@3.5.40(typescript@5.9.3)) + vue: + specifier: ^3.5.40 + version: 3.5.40(typescript@5.9.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.40(typescript@5.9.3)) + devDependencies: + '@tauri-apps/cli': + specifier: ^2.11.4 + version: 2.11.4 + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@vitejs/plugin-vue': + specifier: ^6.0.8 + version: 6.0.8(vite@8.1.5(@types/node@24.13.3))(vue@3.5.40(typescript@5.9.3)) + '@vue/test-utils': + specifier: ^2.4.11 + version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.9.3)) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.5 + version: 8.1.5(@types/node@24.13.3) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.13.3)) + vue-tsc: + specifier: ^3.3.7 + version: 3.3.8(typescript@5.9.3) + +packages: + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==} + peerDependencies: + vue: ^3.0.11 + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.4': + resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.4': + resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} + engines: {node: '>= 10'} + hasBin: true + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.10': + resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==} + + '@vue/devtools-kit@7.7.10': + resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==} + + '@vue/devtools-shared@7.7.10': + resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==} + + '@vue/language-core@3.3.8': + resolution: {integrity: sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==} + + '@vue/reactivity@3.5.40': + resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} + + '@vue/runtime-core@3.5.40': + resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} + + '@vue/runtime-dom@3.5.40': + resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} + + '@vue/server-renderer@3.5.40': + resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} + + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + + '@vue/test-utils@2.4.11': + resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + naive-ui@2.44.1: + resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==} + engines: {node: '>=20'} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + seemly@0.3.10: + resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} + peerDependencies: + vue: ^3.0.11 + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.3.8: + resolution: {integrity: sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.40: + resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.65: + resolution: {integrity: sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==} + peerDependencies: + vue: ^3.0.11 + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + +snapshots: + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.40(typescript@5.9.3))': + dependencies: + vue: 3.5.40(typescript@5.9.3) + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/hash@0.8.0': {} + + '@exodus/bytes@1.15.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@juggle/resize-observer@3.4.0': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@one-ini/wasm@0.1.1': {} + + '@oxc-project/types@0.139.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli@2.11.4': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.4 + '@tauri-apps/cli-darwin-x64': 2.11.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 + '@tauri-apps/cli-linux-arm64-musl': 2.11.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-musl': 2.11.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 + '@tauri-apps/cli-win32-x64-msvc': 2.11.4 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@vitejs/plugin-vue@6.0.8(vite@8.1.5(@types/node@24.13.3))(vue@3.5.40(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.5(@types/node@24.13.3) + vue: 3.5.40(typescript@5.9.3) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@24.13.3) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.40 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.40': + dependencies: + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-sfc@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.23 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.40': + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.10': + dependencies: + '@vue/devtools-kit': 7.7.10 + + '@vue/devtools-kit@7.7.10': + dependencies: + '@vue/devtools-shared': 7.7.10 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.10': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.3.8': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.5 + + '@vue/reactivity@3.5.40': + dependencies: + '@vue/shared': 3.5.40 + + '@vue/runtime-core@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/runtime-dom@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/runtime-core': 3.5.40 + '@vue/shared': 3.5.40 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.40': + dependencies: + '@vue/compiler-ssr': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/shared@3.5.40': {} + + '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@5.9.3))': + dependencies: + '@vue/compiler-dom': 3.5.40 + js-beautify: 1.15.4 + vue: 3.5.40(typescript@5.9.3) + vue-component-type-helpers: 3.3.8 + optionalDependencies: + '@vue/server-renderer': 3.5.40 + + abbrev@2.0.0: {} + + alien-signals@3.2.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + assertion-error@2.0.1: {} + + async-validator@4.2.5: {} + + balanced-match@1.0.2: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + birpc@2.9.0: {} + + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + chai@6.2.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + csstype@3.0.11: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + date-fns-tz@3.2.0(date-fns@4.4.0): + dependencies: + date-fns: 4.4.0 + + date-fns@4.4.0: {} + + decimal.js@10.6.0: {} + + detect-libc@2.1.2: {} + + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@7.0.1: {} + + entities@8.0.0: {} + + es-module-lexer@2.3.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + evtd@0.2.4: {} + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + highlight.js@11.11.1: {} + + hookable@5.5.3: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + ini@1.3.8: {} + + is-fullwidth-code-point@3.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-what@5.5.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.8 + nopt: 7.2.1 + + js-cookie@3.0.8: {} + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.29.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lodash-es@4.18.1: {} + + lodash@4.18.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdn-data@2.27.1: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + muggle-string@0.4.1: {} + + naive-ui@2.44.1(vue@3.5.40(typescript@5.9.3)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.40(typescript@5.9.3)) + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.2.3 + date-fns: 4.4.0 + date-fns-tz: 3.2.0(date-fns@4.4.0) + evtd: 0.2.4 + highlight.js: 11.11.1 + lodash: 4.18.1 + lodash-es: 4.18.1 + seemly: 0.3.10 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.40(typescript@5.9.3)) + vooks: 0.2.12(vue@3.5.40(typescript@5.9.3)) + vue: 3.5.40(typescript@5.9.3) + vueuc: 0.4.65(vue@3.5.40(typescript@5.9.3)) + + nanoid@3.3.16: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + obug@2.1.4: {} + + package-json-from-dist@1.0.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.40(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.10 + vue: 3.5.40(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + postcss@8.5.23: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + require-from-string@2.0.2: {} + + rfdc@1.4.1: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + seemly@0.3.10: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + tldts-core@7.4.9: {} + + tldts@7.4.9: + dependencies: + tldts-core: 7.4.9 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.9 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + treemate@0.3.11: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + undici@7.29.0: {} + + vdirs@0.1.8(vue@3.5.40(typescript@5.9.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.40(typescript@5.9.3) + + vite@8.1.5(@types/node@24.13.3): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.23 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + + vitest@4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.13.3)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@24.13.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + vooks@0.2.12(vue@3.5.40(typescript@5.9.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.40(typescript@5.9.3) + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.8: {} + + vue-router@4.6.4(vue@3.5.40(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.40(typescript@5.9.3) + + vue-tsc@3.3.8(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.8 + typescript: 5.9.3 + + vue@3.5.40(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-sfc': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/server-renderer': 3.5.40 + '@vue/shared': 3.5.40 + optionalDependencies: + typescript: 5.9.3 + + vueuc@0.4.65(vue@3.5.40(typescript@5.9.3)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.40(typescript@5.9.3)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.10 + vdirs: 0.1.8(vue@3.5.40(typescript@5.9.3)) + vooks: 0.2.12(vue@3.5.40(typescript@5.9.3)) + vue: 3.5.40(typescript@5.9.3) + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..80e6a12 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.96.0" +components = ["clippy", "rustfmt"] +profile = "minimal" diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs new file mode 100644 index 0000000..ecd0658 --- /dev/null +++ b/scripts/verify-contracts.mjs @@ -0,0 +1,60 @@ +import { createHash } from "node:crypto"; +import { readFile, readdir } from "node:fs/promises"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const sourcePath = resolve(root, "crates/nana-domain/src/lib.rs"); +const hashPath = resolve(root, "contracts/.source.sha256"); +const schemaDirectory = resolve(root, "contracts/schema"); + +const source = await readFile(sourcePath); +const expectedHash = (await readFile(hashPath, "utf8")).trim(); +const actualHash = createHash("sha256").update(source).digest("hex"); + +if (actualHash !== expectedHash) { + throw new Error( + "Generated contracts are stale. Install Rust and run `pnpm contracts:generate`." + ); +} + +const schemaFiles = (await readdir(schemaDirectory)) + .filter((name) => name.endsWith(".schema.json")) + .sort(); + +const required = [ + "app-info.schema.json", + "character-card.schema.json", + "demo-pack-summary.schema.json", + "item-spec.schema.json", + "persona.schema.json", + "player-view.schema.json", + "plot-module.schema.json", + "resource-bundle.schema.json", + "resource-header.schema.json", + "runtime-state.schema.json", + "story-node.schema.json", + "turn-failure.schema.json", + "turn-request.schema.json", + "turn-result.schema.json", + "world-book.schema.json" +]; + +for (const name of required) { + if (!schemaFiles.includes(name)) { + throw new Error(`Missing generated schema: contracts/schema/${name}`); + } +} + +for (const name of schemaFiles) { + const value = JSON.parse(await readFile(resolve(schemaDirectory, name), "utf8")); + if (typeof value !== "object" || value === null || value.$schema === undefined) { + throw new Error(`Invalid generated schema: contracts/schema/${name}`); + } +} + +const typeScript = await readFile(resolve(root, "contracts/ts/index.ts"), "utf8"); +if (!typeScript.startsWith("// @generated by crates/nana-contracts")) { + throw new Error("contracts/ts/index.ts is not marked as generated"); +} + +console.log(`Verified ${schemaFiles.length} schemas and TypeScript DTO source hash.`); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..588c2dc --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nana-story-app" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Desktop shell for 听娜娜讲故事" + +[lib] +name = "nana_story_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +nana-domain.workspace = true +nana-runtime.workspace = true +serde.workspace = true +serde_json.workspace = true +tauri = { version = "2", features = [] } + +[lints] +workspace = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..230b422 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for the main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..4bfc11b --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,96 @@ +use nana_domain::{ + AppInfo, DemoPackSummary, DOMAIN_SCHEMA_VERSION, PlayerView, ResourceBundle, ValidationIssue, + validate_bundle, +}; +use serde::Serialize; + +const DEMO_PLAYER_VIEW: &str = include_str!("../../fixtures/player-view/initial.json"); +const DEMO_BUNDLE: &str = include_str!("../../content/nana-demo/bundle.json"); + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandError { + code: &'static str, + message: String, + issues: Vec, +} + +impl CommandError { + fn parse(target: &str, error: &serde_json::Error) -> Self { + Self { + code: "invalid_embedded_json", + message: format!("failed to parse embedded {target}: {error}"), + issues: Vec::new(), + } + } +} + +#[tauri::command] +fn get_app_info() -> AppInfo { + AppInfo { + name: "听娜娜讲故事".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + contract_version: DOMAIN_SCHEMA_VERSION, + } +} + +#[tauri::command] +fn get_demo_player_view() -> Result { + serde_json::from_str(DEMO_PLAYER_VIEW) + .map_err(|error| CommandError::parse("PlayerView fixture", &error)) +} + +#[tauri::command] +fn get_demo_pack_summary() -> Result { + let bundle: ResourceBundle = serde_json::from_str(DEMO_BUNDLE) + .map_err(|error| CommandError::parse("content bundle", &error))?; + let report = validate_bundle(&bundle); + if !report.is_valid() { + return Err(CommandError { + code: "invalid_content_bundle", + message: "the embedded content bundle failed domain validation".to_owned(), + issues: report.issues, + }); + } + Ok(DemoPackSummary { + id: bundle.id, + display_name: bundle.display_name, + revision: bundle.revision, + characters: bundle.characters.len(), + world_books: bundle.world_books.len(), + personas: bundle.personas.len(), + plot_modules: bundle.plot_modules.len(), + item_specs: bundle.item_specs.len(), + }) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + get_app_info, + get_demo_pack_summary, + get_demo_player_view + ]) + .run(tauri::generate_context!()) + .expect("failed to run nana-story"); +} + +#[cfg(test)] +mod tests { + use super::{get_demo_pack_summary, get_demo_player_view}; + + #[test] + fn embedded_player_view_is_valid() { + let view = get_demo_player_view().expect("valid PlayerView fixture"); + assert_eq!(view.character_name, "娜娜"); + assert!(!view.beats.is_empty()); + } + + #[test] + fn embedded_content_bundle_is_valid() { + let summary = get_demo_pack_summary().expect("valid demo bundle"); + assert_eq!(summary.characters, 1); + assert_eq!(summary.plot_modules, 1); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..6106469 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + nana_story_app_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..43af710 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "听娜娜讲故事", + "version": "0.1.0", + "identifier": "dev.nanastory.app", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "听娜娜讲故事", + "width": 1440, + "height": 900, + "minWidth": 960, + "minHeight": 640, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' asset: data:; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "shortDescription": "本地优先的 AI 对话游戏" + } +} diff --git a/src/App.spec.ts b/src/App.spec.ts new file mode 100644 index 0000000..834440d --- /dev/null +++ b/src/App.spec.ts @@ -0,0 +1,25 @@ +import { flushPromises, mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; + +import App from "./App.vue"; + +describe("App", () => { + it("renders the canonical demo PlayerView", async () => { + const wrapper = mount(App); + await flushPromises(); + + expect(wrapper.text()).toContain("听娜娜讲故事"); + expect(wrapper.text()).toContain("娜娜"); + expect(wrapper.text()).toContain("你真的会在天亮前回来吗?"); + expect(wrapper.get('textarea[aria-label="自由输入"]')).toBeDefined(); + }); + + it("copies an editable suggestion into the composer", async () => { + const wrapper = mount(App); + await flushPromises(); + await wrapper.get(".suggestions button").trigger("click"); + + const composer = wrapper.get("textarea"); + expect(composer.element.value.length).toBeGreaterThan(0); + }); +}); diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..1823e2a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,94 @@ + + +