diff --git a/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json b/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json new file mode 100644 index 000000000..959e9f036 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json @@ -0,0 +1,66 @@ +{ + "schema": "hololake.installed-numbered-root-acceptance/v1", + "record_id": "HLP-HOLOLAKE-0.5.0-NUMBERED-ROOT-INSTALLED-20260818", + "state": "LOCAL_DEVELOPER_ID_SIGNED_INSTALLED_ACCEPTANCE_PASS_PUBLIC_NOTARIZATION_PENDING", + "observed_at": "2026-08-18T15:18:00Z", + "source": { + "branch": "codex/hololake-clean-reassembly-20260818", + "source_commit": "5d01607459043d0713bc562ef6d04dec198930a9", + "numbered_root_commit": "64abf969bfbc1c576d4fa84ae3282d32efbfcc38" + }, + "installed_application": { + "path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app", + "version": "0.5.0", + "bundle_identifier": "world.guanghu.hololake", + "architecture": "arm64", + "binary_sha256": "2e162ff077187310f816f58df2250e3ee939eb489fe3c7b00e94557ff8f3370d", + "process_path_verified": true + }, + "developer_id": { + "identity": "Developer ID Application: bei sun (825A9L3G7Q)", + "team_identifier": "825A9L3G7Q", + "cdhash": "7a2372ed85fc3b775e9a352217b123a4bdee0d64", + "strict_signature_verification": "PASS", + "designated_requirement": "PASS", + "gatekeeper": "REJECTED_UNNOTARIZED_DEVELOPER_ID", + "apple_notarization_and_stapling": "PENDING" + }, + "runtime": { + "real_webview_loaded": true, + "visible_version": "V0.5.0", + "visible_domain": "第五域 · 光湖本源域", + "entered_surface": "永恒湖心系统", + "numbered_ipc_receipt_rows": 50, + "grant_and_execution_rows_present": true, + "empty_authority_binding_digest_rows": 0, + "recalculated_receipt_chain_failures": 0, + "last_sequence": 50, + "last_receipt_hash": "a294463ad9e6959d0f12ce5c2b59e11d97581e067b9480df9ae598dfafc067df" + }, + "old_application": { + "version": "0.4.1", + "role": "READ_ONLY_PRE_NUMBERED_ROOT_MODULE_DONOR", + "archive_receipt": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/archive-receipt.json", + "repair_in_place": false + }, + "release_boundary": { + "local_signed_install_complete": true, + "public_signed_notarized_release_complete": false, + "updater_public_key_continuity": "PASS_EXISTING_0.4.1_TRUST_ROOT_REUSED", + "updater_private_key_available_to_current_pipeline": true, + "updater_signature_generated_and_verified": true, + "pre_notarization_updater_artifact": { + "path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz", + "sha256": "4176bb7ea83c820744239c228671840289f87675329182bc055938e03bba9693", + "signature_path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz.sig", + "signature_sha256": "ae02c2918c26b6e3cc2389fec8d9f98884ae639d448edc78a4ec80ee30a8ae92", + "publication_allowed": false + }, + "apple_notarization_credentials_available_to_current_pipeline": false, + "public_broadcast_activated": false + }, + "persona_boundary": { + "current_codex_carrier_binding_claimed": false, + "runtime_persona_binding_created_by_numbered_ipc": false + } +} diff --git a/product-source/hololake-native-desktop/audit/hololake-0.5.0-qoder-numbered-production-installed-20260819.json b/product-source/hololake-native-desktop/audit/hololake-0.5.0-qoder-numbered-production-installed-20260819.json new file mode 100644 index 000000000..60db39e45 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/hololake-0.5.0-qoder-numbered-production-installed-20260819.json @@ -0,0 +1,17 @@ +{ + "schema": "hololake.production-install-acceptance/v1", + "record_id": "HLP-PRODUCTION-INSTALL-20260819-001", + "state": "PASS", + "version": "0.5.0", + "installed_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app", + "visible_hololake_app_count": 1, + "previous_app_recovery_path": "/Users/bingshuolingdianyuanhe/.Trash/HoloLake-pre-qoder-numbered-ui-20260819-0659.app", + "binary_sha256": "b0422bb18df521ace54ff1a52cd47121d92833dd78f165845deba7806686c7ac", + "cdhash": "42640f7ce56e96403609899095c77ea6171df07d", + "team_identifier": "825A9L3G7Q", + "developer_id_signature": "PASS", + "launch": "PASS", + "real_data_rehydration": "PASS", + "notarization": "NOT_CLAIMED_THIS_LOCAL_BUILD", + "updater_artifact_signing": "NOT_COMPLETED_MISSING_PRIVATE_UPDATER_KEY_IN_CURRENT_PROCESS" +} diff --git a/product-source/hololake-native-desktop/audit/qoder-surface-live-acceptance-20260819.json b/product-source/hololake-native-desktop/audit/qoder-surface-live-acceptance-20260819.json new file mode 100644 index 000000000..c02229824 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/qoder-surface-live-acceptance-20260819.json @@ -0,0 +1,28 @@ +{ + "schema": "hololake.qoder-surface-live-acceptance/v1", + "record_id": "HLP-QODER-SURFACE-ACCEPTANCE-20260819-001", + "module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001", + "state": "PASS", + "tested_app": "src-tauri/target/debug/bundle/macos/HoloLake.app", + "developer_id_signature": "PASS", + "team_identifier": "825A9L3G7Q", + "binary_sha256": "fb1d0497f295e1228c9b2ac86e33082332f226aae6e32b9050cf6c5de30bd512", + "cdhash": "90e365b23961a6b842590869df655ec35a55b66e", + "checks": { + "traditional_locked_layout": "PASS", + "traditional_real_data_hydration": "PASS", + "traditional_light_finish_readability": "PASS_SNOW", + "language_world_five_domains_no_overlap": "PASS_1229x768", + "language_world_real_weather": "PASS_CLOUD", + "theme_persists_into_channel": "PASS", + "theme_persists_into_web_novel": "PASS", + "theme_persists_into_education": "PASS", + "new_chapter_modal_opens": "PASS", + "created_chapter_survives_restart": "PASS_1_CHAPTER", + "work_summary_refresh_after_create": "PASS_1_CHAPTER_ON_WORK_CARD", + "idle_clock_stops": "PASS", + "idle_screenshot_psnr_db": 59.502493 + }, + "reference_comparison": "audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg", + "public_notarization": "NOT_CLAIMED_BY_DEBUG_ACCEPTANCE" +} diff --git a/product-source/hololake-native-desktop/audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg b/product-source/hololake-native-desktop/audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg new file mode 100644 index 000000000..6f8eb5d42 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-1440.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-1440.png new file mode 100644 index 000000000..ce8d44afb Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-1440.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-500.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-500.png new file mode 100644 index 000000000..8c2dff839 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-home-500.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-modules-1440.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-modules-1440.png new file mode 100644 index 000000000..a5608fd23 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-modules-1440.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-native-1440.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-native-1440.png new file mode 100644 index 000000000..748653e18 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-lightweight-native-1440.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-modules.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-modules.png new file mode 100644 index 000000000..ef1f082ce Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-modules.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-native.png b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-native.png new file mode 100644 index 000000000..247ce0aeb Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/private-channel-native.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/public-1024x768.png b/product-source/hololake-native-desktop/audit/responsive-20260819/public-1024x768.png new file mode 100644 index 000000000..603e45748 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/public-1024x768.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/public-1440x900.png b/product-source/hololake-native-desktop/audit/responsive-20260819/public-1440x900.png new file mode 100644 index 000000000..1b3552d4b Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/public-1440x900.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/public-720x900.png b/product-source/hololake-native-desktop/audit/responsive-20260819/public-720x900.png new file mode 100644 index 000000000..890c987a4 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/public-720x900.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1024x768.png b/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1024x768.png new file mode 100644 index 000000000..f3f2664e7 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1024x768.png differ diff --git a/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1440x900.png b/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1440x900.png new file mode 100644 index 000000000..c0318fd10 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/responsive-20260819/traditional-1440x900.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-concepts-20260819/private-channel-wide.png b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/private-channel-wide.png new file mode 100644 index 000000000..20d1b1bac Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/private-channel-wide.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-narrow.png b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-narrow.png new file mode 100644 index 000000000..b158a5e84 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-narrow.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-wide.png b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-wide.png new file mode 100644 index 000000000..09985adfc Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/public-world-wide.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-concepts-20260819/traditional-workbench-wide.png b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/traditional-workbench-wide.png new file mode 100644 index 000000000..47a120568 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-concepts-20260819/traditional-workbench-wide.png differ diff --git a/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json b/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json new file mode 100644 index 000000000..9980e8741 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json @@ -0,0 +1,58 @@ +{ + "schema": "hololake.channel-workbench-runtime/v1", + "record_id": "HLP-CHANNEL-WORKBENCH-RUNTIME-001", + "candidate_number": "HLP-DONOR-CAND-0002", + "runtime_module_number": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "numbered_ipc": { + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "operations": ["HLP-NIPC-OP-0074", "HLP-NIPC-OP-0075", "HLP-NIPC-OP-0076"] + }, + "engines": { + "document": "LEXICAL_0_49", + "spreadsheet": "FORTUNE_SHEET_1_0_4" + }, + "data_boundary": { + "scope": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "database": "channel-workbench-v1/channel-workbench.sqlite3", + "documents_max_body_bytes": 2097152, + "spreadsheets_max_columns": 64, + "spreadsheets_max_rows": 5000, + "package_unmount_deletes_user_data": false, + "repository_code_is_executable": false + }, + "integrity": { + "optimistic_revision_required": true, + "every_save_writes_hash_chained_receipt": true, + "receipt_chain_verified_before_read_or_write": true, + "adapter_requires_signed_active_module": true + }, + "current_acceptance": { + "state": "SIGNED_INSTALLED_CONTENT_AND_RESTART_ACCEPTED", + "source_donor": "HLP-DONOR-CHAOTIC-WORKTREE-20260818", + "compiled_donor_behavior_checked": false, + "compiled_donor_boundary": "No claim of pixel-equivalent compiled-donor acceptance; the declared source slice was reconstructed and the clean host behavior was accepted directly.", + "real_signed_package": "fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod", + "module_package_sha256": "eb7af0bd4882cc89acda11c933a3601d16f0588ff737caf8467b875ab5a4ab7b", + "signed_debug_binary_sha256": "6f851856ab6f5717facc67ec406c9eb99e240290c585438323e230daec5ae71c", + "developer_id_team": "825A9L3G7Q", + "signed_debug_cdhash": "0209a46b2dc221d37b618a2b12866bb8cb39f03d", + "real_account_human_number": "ICE-GL∞", + "ui_activation_verified": true, + "document_save": { + "title": "冰朔频道迁移验收", + "revision": 1, + "content_sha256_prefix": "534b4bd3d6d0", + "receipt_sha256_prefix": "72436f46cf1d" + }, + "spreadsheet_save": { + "title": "冰朔编号迁移表", + "revision": 1, + "formula_preserved": "=1+2", + "content_sha256_prefix": "57607d92c0dc", + "receipt_sha256_prefix": "323761fc18a3" + }, + "restart_persistence_verified": true, + "dependency_audit": "0_VULNERABILITIES_AFTER_UUID_11_1_1_OVERRIDE" + } +} diff --git a/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json b/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json new file mode 100644 index 000000000..99c3eb673 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/direct-local-broker-numbered-registry.json @@ -0,0 +1,36 @@ +{ + "schema": "hololake.direct-local-broker-numbered-registry/v1", + "record_id": "HLP-NBROKER-ROOT-001", + "runtime": { + "protocol_version": "HLP-NBROKER-v1", + "caller_number": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "legacy_string_operation_allowed": false, + "unknown_or_mismatched_coordinate": "FAIL_CLOSED", + "request_nonce_required": true, + "transport_is_authority": false + }, + "operations": [ + {"operation_number":"HLP-NBROKER-OP-0001","alias":"DISCOVER_NEARBY","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0001","target_number":"HLP-NBROKER-TGT-0001"}, + {"operation_number":"HLP-NBROKER-OP-0002","alias":"OPEN_VISITOR_SESSION","channel_number":"HLP-NBROKER-CH-0002","module_number":"HLP-NBROKER-MOD-0002","target_number":"HLP-NBROKER-TGT-0002"}, + {"operation_number":"HLP-NBROKER-OP-0003","alias":"RECEIVE_LANGUAGE","channel_number":"HLP-NBROKER-CH-0002","module_number":"HLP-NBROKER-MOD-0003","target_number":"HLP-NBROKER-TGT-0003"}, + {"operation_number":"HLP-NBROKER-OP-0004","alias":"PING","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0001","target_number":"HLP-NBROKER-TGT-0001"}, + {"operation_number":"HLP-NBROKER-OP-0005","alias":"OPEN_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"}, + {"operation_number":"HLP-NBROKER-OP-0006","alias":"RESUME_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"}, + {"operation_number":"HLP-NBROKER-OP-0007","alias":"HEARTBEAT_SESSION","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"}, + {"operation_number":"HLP-NBROKER-OP-0008","alias":"PRESENT_PERSONA_CARRIER_LICENSE","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0005","target_number":"HLP-NBROKER-TGT-0005"}, + {"operation_number":"HLP-NBROKER-OP-0009","alias":"GET_PERSONA_CARRIER_LICENSE_STATUS","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0005","target_number":"HLP-NBROKER-TGT-0005"}, + {"operation_number":"HLP-NBROKER-OP-0010","alias":"GET_WORK_ENVIRONMENT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0006","target_number":"HLP-NBROKER-TGT-0006"}, + {"operation_number":"HLP-NBROKER-OP-0011","alias":"APPEND_EVENT","channel_number":"HLP-NBROKER-CH-0003","module_number":"HLP-NBROKER-MOD-0004","target_number":"HLP-NBROKER-TGT-0004"}, + {"operation_number":"HLP-NBROKER-OP-0012","alias":"RESOLVE_CAPABILITY_ROUTE","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"}, + {"operation_number":"HLP-NBROKER-OP-0013","alias":"INSTALL_DYNAMIC_NODE_REGISTRY","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"}, + {"operation_number":"HLP-NBROKER-OP-0014","alias":"RECORD_SIGNED_NODE_HEALTH","channel_number":"HLP-NBROKER-CH-0005","module_number":"HLP-NBROKER-MOD-0007","target_number":"HLP-NBROKER-TGT-0007"}, + {"operation_number":"HLP-NBROKER-OP-0015","alias":"INSPECT_MOUNTED_PNCC_REPOSITORY","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"}, + {"operation_number":"HLP-NBROKER-OP-0016","alias":"READ_MOUNTED_PNCC_REMOTE_OBJECT","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"}, + {"operation_number":"HLP-NBROKER-OP-0017","alias":"QUERY_PNCC_RECEIPT_PROJECTION","channel_number":"HLP-NBROKER-CH-0006","module_number":"HLP-NBROKER-MOD-0008","target_number":"HLP-NBROKER-TGT-0008"}, + {"operation_number":"HLP-NBROKER-OP-0018","alias":"GET_BEIJING_TIME","channel_number":"HLP-NBROKER-CH-0001","module_number":"HLP-NBROKER-MOD-0009","target_number":"HLP-NBROKER-TGT-0009"}, + {"operation_number":"HLP-NBROKER-OP-0019","alias":"ISSUE_PERSONA_TIME_TICKET","channel_number":"HLP-NBROKER-CH-0004","module_number":"HLP-NBROKER-MOD-0009","target_number":"HLP-NBROKER-TGT-0009"}, + {"operation_number":"HLP-NBROKER-OP-0020","alias":"ACQUIRE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"}, + {"operation_number":"HLP-NBROKER-OP-0021","alias":"INSPECT_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"}, + {"operation_number":"HLP-NBROKER-OP-0022","alias":"RELEASE_DEVELOPMENT_WRITE_LANE","channel_number":"HLP-NBROKER-CH-0007","module_number":"HLP-NBROKER-MOD-0010","target_number":"HLP-NBROKER-TGT-0010"} + ] +} diff --git a/product-source/hololake-native-desktop/contracts/dynamic-language-world-visual-system.json b/product-source/hololake-native-desktop/contracts/dynamic-language-world-visual-system.json new file mode 100644 index 000000000..5736af0c5 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/dynamic-language-world-visual-system.json @@ -0,0 +1,83 @@ +{ + "schema": "hololake.dynamic-language-world-visual-system/v1", + "record_id": "HLP-DYNAMIC-WORLD-SURFACE-001", + "state": "HOLOLAKE_0_5_SIGNED_NUMBERED_SURFACE_ACCEPTED", + "package": { + "official_module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001", + "adapter": "dynamic-language-world-surface-v1", + "registration_class": "OFFICIAL_LIGHTHOUSE", + "activation": "SIGNED_PACKAGE_PLUS_EXPLICIT_HUMAN_PERMISSION_CONFIRMATION" + }, + "numbered_ipc": { + "module": "HLP-NIPC-MOD-0031", + "target": "HLP-NIPC-TGT-0031", + "operations": ["HLP-NIPC-OP-0147"], + "public_tauri_commands": ["numbered_ipc"], + "mismatched_coordinate": "FAIL_CLOSED" + }, + "dynamic_inputs": ["BEIJING_TIME", "VERIFIED_CURRENT_WEATHER"], + "weather": { + "provider": "Open-Meteo", + "endpoint": "https://api.open-meteo.com/v1/forecast", + "maximum_request_seconds": 5, + "cache_ttl_seconds": 600, + "redirects": "DENIED", + "unavailable_behavior": "BEIJING_TIME_ONLY_NO_FAKE_WEATHER" + }, + "visual_lock": { + "layout": "QODER_VISUAL_LANGUAGE_SEMANTICALLY_REASSEMBLED_ON_RESPONSIVE_HOLOLAKE_SHELL", + "language_world_themes": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"], + "traditional_finishes": ["曜夜", "星辉", "深海", "翡翠", "朱砂", "香槟", "瓷光", "雪霁"], + "all_downstream_pages_inherit_active_surface_tokens": true, + "ambient_motion_requires_recent_human_pointer_or_keyboard_activity": true, + "idle_visual_state": "STATIC", + "responsive_layout": "CONTAINER_MEASURED_PANORAMIC_WIDE_COMPACT_STACKED_REFLOW", + "domain_semantic_order_is_stable": true, + "private_channel_separates_native_organs_from_installed_modules": true, + "climate_changes_routing_permission_or_fact": false, + "daily_sampling_domain_changes_open_domain": false, + "real_city_exposed_in_ui": false, + "coordinates_exposed_in_ui": false, + "mechanical_flow_lines": false, + "heavy_webgl": false + }, + "donor_disposition": { + "world_climate": "EXTRACTED_REWRITTEN_AND_NUMBERED", + "starlake_surface": "QODER_LOCKED_SOURCE_ADMITTED_WITH_RESPONSIVE_AND_IDLE_MOTION_ADAPTER", + "traditional_surface": "QODER_LOCKED_SOURCE_ADMITTED_WITH_REAL_NUMBERED_PROJECTIONS_ONLY", + "traditional_surface_css": "QODER_LOCKED_EIGHT_FINISH_TOKEN_SYSTEM_ADMITTED_AND_SCOPED_ACROSS_ALL_PAGES", + "fake_broadcasts_and_fake_metrics": "REJECTED" + }, + "qoder_source_provenance": { + "root": "/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/UI-预览-20260818/deploy-staging", + "main_tsx_sha256": "7dab8596ec0bd2609b1b23b175f1f14f15f2aa9af847eb605692c993d2e876e8", + "traditional_surface_tsx_sha256": "c713dc27681e07c510a2125816dcefb3255252fc0b570e350af7a98f57fc220d", + "traditional_surface_css_sha256": "3e6c446fb84a16ed23b0fd51466ad9723d0c0670ee6ddf6653f61237bd00baa6", + "starlake_surface_css_sha256": "471e3edef02236e6463144ac75b526f0f98782b5476b9db786e7cd0614a83227", + "acceptance_html_sha256": "7a41e426f1576fe0e25182e0c2efee65302b064d86363c890b0f235386a9c070" + }, + "current_acceptance": { + "state": "PASS_NUMBERED_QODER_SURFACES_REAL_DATA_RESPONSIVE_IDLE_AND_RESTART", + "module_package_sha256": "b6ba13b9f70bd617b2a303ec2eb0ea11d5354d10118c7f3e5179ae8100cd2602", + "module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST", + "numbered_route": "HLP-NIPC-MOD-0031/HLP-NIPC-OP-0147/HLP-NIPC-TGT-0031", + "signed_app_binary_sha256": "b0422bb18df521ace54ff1a52cd47121d92833dd78f165845deba7806686c7ac", + "signed_app_cdhash": "42640f7ce56e96403609899095c77ea6171df07d", + "apple_team_identifier": "825A9L3G7Q", + "module_receipts": { + "install": "2bb903cd369c8f57432c59367fd5494ee89a75214d5129511190e2337cecdbc4", + "mount": "aed4d00e51849ae5e9c0258fe2f93afc8bff8d927905cdd7fabd9bf32c4bc66c", + "self_test_pass": "404a0eeddb866bc3050b407c586c036f3707db577e1edf9f1cf3129c1cc9cc1d" + }, + "real_weather_readback": "PASS_VERIFIED_LIVE_NIGHT_CLOUD_WITHOUT_CITY_OR_COORDINATES", + "restart_restore": "PASS_ACTIVE_MODULE_REAL_CLIMATE_THEME_AND_WEB_NOVEL_CHAPTER_RESTORED", + "official_shell_preserved": "QODER_LOCKED_SURFACES_ADMITTED_WITH_NUMBERED_FUNCTION_ROUTING_UNCHANGED", + "visual_comparison": "audit/qoder-traditional-reference-vs-hololake-0.5.0.jpg", + "idle_static_psnr_db": 59.502493, + "idle_static_clock_unchanged": true, + "web_novel_new_chapter_modal": "PASS", + "web_novel_created_chapter_restart_persistence": "PASS_1_CHAPTER_VISIBLE_AFTER_RESTART", + "traditional_theme_downstream_pages": ["CHANNEL", "WEB_NOVEL", "EDUCATION"], + "notarization": "FINAL_RELEASE_CANDIDATE_PENDING" + } +} diff --git a/product-source/hololake-native-desktop/contracts/education-workspace.json b/product-source/hololake-native-desktop/contracts/education-workspace.json new file mode 100644 index 000000000..1b58970d3 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/education-workspace.json @@ -0,0 +1,265 @@ +{ + "schema": "hololake.education-workspace/v1", + "record_id": "HLP-EDUCATION-WORKSPACE-001", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "domain_entry": "BRANCH_DOMAIN", + "industry": "EDUCATION", + "channel_id": "GH-EDU-INIT-001", + "package": { + "package_key": "hololake.official.education-workbench", + "official_module_number": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", + "registration_state": "OFFICIAL_NUMBER_ACCEPTED", + "current_mount": "ACTIVE_AFTER_SIGNED_INSTALL_MOUNT_AND_SELF_TEST", + "future_distribution": "HOLOLAKE_OFFICIAL_MODULE_MARKETPLACE" + }, + "foundation_dependency": { + "module_id": "hololake.builtin.channel-workbench", + "state": "BUILT_IN_FOUNDATION", + "engines": ["LEXICAL_0_49", "FORTUNE_SHEET_1_0_4"], + "rule": "EDUCATION_MODULE_CONSUMES_FOUNDATION_WITHOUT_DUPLICATING_OFFICE_ENGINES" + }, + "native_storage": { + "owner": "HOLOLAKE_NATIVE_RUST_CORE", + "namespace": "education-workspace-v1", + "engine": "SQLITE", + "authenticated_account_required": true, + "cross_account_projection_allowed": false, + "public_catalog_contains_private_data": false, + "restart_readback_required": true + }, + "document_module": { + "module_id": "EDU-DOCUMENT", + "state": "DEVELOPMENT_FEATURE_MOUNTED", + "engine": "CHANNEL_WORKBENCH_LEXICAL_0_49_0_WITH_EDUCATION_TEMPLATES_AND_HOLOLAKE_MARKDOWN_STATE", + "capabilities": [ + "LIST", + "CREATE", + "CREATE_COPY", + "READ", + "EDIT_TITLE_AND_BODY", + "EXPLICIT_READ_AND_EDIT_MODES", + "HUMAN_SETTINGS_MENU", + "BASIC_FORMATTING_TOOLBAR", + "RICH_TEXT_COMMAND_ENGINE", + "UNDO_AND_REDO_HISTORY", + "MARKDOWN_SHORTCUTS_AND_BIDIRECTIONAL_STATE_TRANSLATION", + "TITLE_DIRECTORY_FILTER", + "FORMAL_TEACHING_TEMPLATES", + "NATIVE_READING_CANVAS", + "EXPORT_MARKDOWN_AND_HTML", + "SAVE_WITH_EXPECTED_REVISION", + "RECOVERABLE_ARCHIVE" + ] + }, + "table_module": { + "module_id": "EDU-TABLE", + "state": "DEVELOPMENT_FEATURE_MOUNTED", + "engine": "CHANNEL_WORKBENCH_FORTUNE_SHEET_1_0_4_FOR_REAL_CELLS_AND_FORMULAS_PLUS_TANSTACK_8_21_3_FOR_EDUCATION_VIEWS", + "capabilities": [ + "LIST", + "CREATE", + "CREATE_COPY", + "READ", + "EDIT_TITLE", + "HUMAN_SETTINGS_MENU", + "ADD_AND_REMOVE_COLUMNS", + "ADD_AND_REMOVE_ROWS", + "EDIT_CELLS", + "TITLE_DIRECTORY_FILTER", + "CELL_CONTENT_FILTER", + "COLUMN_SORT_ASCENDING_DESCENDING_OR_SOURCE_ORDER", + "HEADLESS_FILTER_SORT_GROUP_AND_AGGREGATE_ROW_MODELS", + "BOUNDED_PAGINATION_ROW_MODEL_FOR_LARGE_EDITABLE_TABLES", + "GRID_AND_EDITABLE_CARD_VIEWS", + "GROUPED_CLASSIFICATION_BOARD_FROM_SELECTED_FIELD", + "LIVE_DATA_OVERVIEW_WITH_SELECTED_GROUP_AND_MEASURE", + "HEADLESS_TABLE_DATA_DETACHED_OUTSIDE_TABLE_SURFACE", + "MULTI_ROW_MULTI_COLUMN_TABULAR_PASTE", + "EXTERNAL_SPREADSHEET_IMPORT_WITH_PREWRITE_PROFILE", + "XLSX_CSV_TSV_AND_NATIVE_EXPORT", + "SAVE_WITH_EXPECTED_REVISION", + "RECOVERABLE_ARCHIVE" + ], + "limits": { + "maximum_columns": 30, + "maximum_rows": 1000, + "maximum_cell_bytes": 10000 + }, + "sensitive_field_visibility": { + "local_default": "HIDDEN_UNTIL_HUMAN_REVEALS", + "human_can_hide": true, + "human_can_show_again": true, + "external_model_transfer_authority": "SEPARATE_EXPLICIT_PER_FILE_CONSENT" + } + }, + "content_translation_layer": { + "service_id": "EDU-CONTENT-TRANSLATOR", + "state": "DETERMINISTIC_RUNTIME_READY_MODEL_ASSIST_SLOT_RESERVED", + "profile_schema": "hololake.content-profile/v1", + "import_adapter": "EDU-TABLE-IMPORT-ADAPTER/v1", + "export_adapter": "EDU-TABLE-EXPORT-ADAPTER/v1", + "order": [ + "FILE_CONTAINER_SNIFF", + "PAGE_AND_STRUCTURE_INVENTORY", + "BOUNDED_LOCAL_RECOGNITION", + "EXPLAINABLE_SEMANTIC_FOCUS_INFERENCE", + "UNASSIGNED_CHANNEL_STAGING", + "HUMAN_INDUSTRY_ROUTE_DECISION", + "NATIVE_TRANSLATION", + "ATOMIC_MODULE_REGISTRATION_AFTER_CONFIRMATION", + "HUMAN_RECEIPT" + ], + "import_assignment": { + "default_scope": "UNASSIGNED", + "education_directory_before_human_confirmation": false, + "existing_legacy_imports_reclassified_on_migration": true, + "source_file_mutated": false + }, + "supported_imports": ["XLSX", "XLS", "XLSM", "XLSB", "ODS", "CSV", "TSV", "HOLOLAKE_NATIVE"], + "supported_exports": ["XLSX", "CSV", "TSV", "HOLOLAKE_NATIVE"], + "local_import_without_model_api": "ENABLED_FOR_ALL_SUPPORTED_FORMATS", + "semantic_focus": { + "current_mode": "DETERMINISTIC_BASELINE_ONLY", + "signals": ["SOURCE_AND_PAGE_TITLE", "COLUMN_SEMANTICS", "NUMERIC_COVERAGE"], + "low_confidence_behavior": "ASK_HUMAN_TO_SELECT_PRIMARY_MEASURE", + "model_dynamic_judgment": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE" + }, + "adaptive_rendering": { + "current_mode": "DETERMINISTIC_BASELINE_WITH_EXPLAINABLE_DEGRADATION", + "editable_cell_budget": 240, + "baseline_inputs": [ + "ROW_COUNT", + "COLUMN_COUNT", + "FIELD_TYPES", + "NUMERIC_COVERAGE", + "GROUP_CARDINALITY", + "FOCUS_CONFIDENCE" + ], + "degradation_order": [ + "LARGE_EDITABLE_GRID_TO_DYNAMIC_PAGINATION", + "NO_RELIABLE_MEASURE_TO_COUNT_SUMMARY", + "AMBIGUOUS_FOCUS_TO_HUMAN_SELECTION", + "UNSUPPORTED_STRUCTURE_TO_HUMAN_ASSISTANCE_RECEIPT" + ], + "local_baseline_always_available": true, + "model_adjustment": { + "api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1", + "state": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE", + "proposal_fields": [ + "PRIMARY_FOCUS", + "GROUP_FIELD", + "MEASURE_FIELD", + "VIEW_COMPOSITION", + "PAGE_DENSITY" + ], + "proposal_must_pass_local_structure_validation": true, + "proposal_must_obey_local_resource_limits": true, + "human_confirmation_before_rule_activation": true, + "model_can_mutate_source_data": false, + "failure_behavior": "RETURN_TO_LOCAL_BASELINE_WITH_RECEIPT" + } + }, + "unknown_input_behavior": "RETURN_HUMAN_ASSISTANCE_RECEIPT_WITHOUT_MODULE_WRITE_OR_EXTERNAL_MODEL_TRANSFER", + "model_assist": { + "api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1", + "current_state": "NOT_CONFIGURED", + "provider": "USER_SELECTED", + "secret_storage": "OPERATING_SYSTEM_SECRET_STORE_REQUIRED", + "file_transfer_default": "DENY_UNTIL_EXPLICIT_PER_FILE_CONSENT", + "rule_update_flow": ["MODEL_CANDIDATE", "LOCAL_VALIDATION", "HUMAN_CONFIRMATION", "VERSIONED_INSTALL"], + "learning_scopes": ["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"] + }, + "source_file_mutation_allowed": false, + "silent_truncation_allowed": false + }, + "data_cleanup_module": { + "module_id": "EDU-DATA", + "state": "DEVELOPMENT_FEATURE_MOUNTED", + "source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES", + "capabilities": [ + "SELECT_ACCOUNT_TABLE", + "TRIM_CELL_BOUNDARY_WHITESPACE", + "REMOVE_FULLY_EMPTY_ROWS", + "REMOVE_EXACT_DUPLICATE_ROWS_KEEP_FIRST", + "PREVIEW_BEFORE_WRITE", + "FULL_TABLE_ANALYSIS_WITH_BOUNDED_CHANGE_PREVIEW", + "ACCESSIBILITY_SAFE_NON_TABLE_CHANGE_LIST", + "HUMAN_CONFIRMATION_BEFORE_SAVE", + "SAVE_WITH_EXPECTED_REVISION" + ], + "preview_change_limit": 12, + "preview_cell_value_character_limit": 120, + "full_table_analysis_is_limited_by_preview": false, + "automatic_destructive_cleanup_allowed": false, + "cross_account_data_allowed": false + }, + "automation_module": { + "module_id": "EDU-AUTOMATION", + "state": "DEVELOPMENT_FEATURE_MOUNTED", + "source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES", + "capabilities": [ + "CREATE_AND_SAVE_RULE", + "EQUALS_CONTAINS_EMPTY_AND_NONEMPTY_CONDITIONS", + "SET_TARGET_CELL_VALUE", + "PREVIEW_MATCHED_ROWS_AND_CHANGED_CELLS", + "HUMAN_CONFIRMATION_BEFORE_EXECUTION", + "RULE_AND_TABLE_REVISION_LOCK", + "SIGNED_PREVIEW_TOKEN", + "ATOMIC_TABLE_WRITE_AND_EXECUTION_RECEIPT", + "RECOVERABLE_RULE_ARCHIVE" + ], + "background_or_scheduled_execution_allowed": false, + "cross_account_data_allowed": false, + "model_execution_authority_granted": false + }, + "composition_module": { + "module_id": "EDU-COMPOSITION", + "state": "DEVELOPMENT_FEATURE_MOUNTED", + "source": "CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG", + "capabilities": [ + "NATIVE_TYPED_OBJECT", + "REGISTERED_MODULE_EXECUTION_GRAPH", + "SELECT_DIMENSION_AND_MEASURE", + "DASHBOARD_PROJECTION", + "COMPARISON_PROJECTION", + "VERTICAL_BAR_PROJECTION", + "CLASSIFICATION_PROJECTION", + "TABLE_PROJECTION", + "ONE_EXECUTION_RESULT_MULTIPLE_HUMAN_VIEWS" + ], + "read_only": true, + "hardcoded_sample_data_used": false, + "model_execution_authority_granted": false + }, + "remaining_slots": [], + "authority": { + "browser_local_storage_allowed": false, + "webview_direct_file_access_allowed": false, + "model_execution_authority_granted": false, + "external_application_embedded": false + }, + "current_acceptance": { + "state": "PASS", + "runtime_module_number": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0024", + "numbered_operations": "HLP-NIPC-OP-0085..0102", + "signed_package_sha256": "a9f6fb7dd9c4aaf910a5d20d0a1d72dea6ab921cf36dd50d46dd88cde3a09e03", + "signed_app_binary_sha256": "028592b137fe4f19a642e1301d6fb500e2a4fde50ddb616e229de8a48090803c", + "developer_id_team": "825A9L3G7Q", + "developer_id_cdhash": "4c88261b4b573d08edcbba8d67d8afe27ad82a3e", + "runtime_receipts": { + "install": "68f296a11be19c384cb67af9f529a03cb8389bd256c153c5905451fbc0011a24", + "mount": "ddb6a51d47f6a58a72a7fc748eff71687cbd04be5dde33dbce590a7abba6af5f", + "self_test_pass": "80d59b7c9c2f14005fd8a24748a25c2be0d55198fdcb621f528267ae6dccc12a" + }, + "restart_readback": { + "module_state": "ACTIVE", + "document": "冰朔教育迁移验收 · revision 2", + "table": "冰朔教育迁移验收表 · 1 row · revision 4", + "automation": "验收状态自动化 · preview 1 row / 1 cell · APPLIED revision 3 to 4" + }, + "existing_user_data_deleted_or_overwritten": false, + "apple_notarization_scope": "FINAL_RELEASE_CANDIDATE_ONLY_NOT_THIS_DEBUG_ACCEPTANCE_BUNDLE" + }, + "open_source_donor_assessment": "contracts/education-open-source-donor-assessment.json" +} diff --git a/product-source/hololake-native-desktop/contracts/gls-executable-projections.json b/product-source/hololake-native-desktop/contracts/gls-executable-projections.json index 18f04c724..9469f3e63 100644 --- a/product-source/hololake-native-desktop/contracts/gls-executable-projections.json +++ b/product-source/hololake-native-desktop/contracts/gls-executable-projections.json @@ -1,6 +1,6 @@ { "schema": "hololake.gls-executable-projections/v2", - "source_commit": "2598fbfba8caf64c7ab9740a3036c5aab977502e", + "source_commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e", "runtime_graph_rule": "ONLY_EXPLICIT_RUNTIME_REQUIRES_EDGES_ENTER_ACTIVATION_GRAPH", "projections": { "GLS-0250": {"stage":"P0","projection_kind":"TYPED_FACT_AND_DOMAIN_BOUNDARY","adapter":"origin-domain-topology","event_kinds":["BOOTSTRAP","DOMAIN_ROUTE"],"dependencies":[]}, diff --git a/product-source/hololake-native-desktop/contracts/gls-native-runtime-kernel.json b/product-source/hololake-native-desktop/contracts/gls-native-runtime-kernel.json index 36e5252db..f3e06643a 100644 --- a/product-source/hololake-native-desktop/contracts/gls-native-runtime-kernel.json +++ b/product-source/hololake-native-desktop/contracts/gls-native-runtime-kernel.json @@ -1,7 +1,7 @@ { "schema": "hololake.gls-native-runtime-kernel/v1", "record_id": "HLP-GLS-NATIVE-RUNTIME-KERNEL-001", - "source_commit": "2598fbfba8caf64c7ab9740a3036c5aab977502e", + "source_commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e", "receipt_schema": "hololake.protocol-decision-receipt/v1", "decision_set": ["ALLOW", "DENY", "AMBIGUOUS", "UNVERIFIED"], "runtime_boundaries": { diff --git a/product-source/hololake-native-desktop/contracts/gls-numbered-reference-nodes.json b/product-source/hololake-native-desktop/contracts/gls-numbered-reference-nodes.json new file mode 100644 index 000000000..0623d3153 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/gls-numbered-reference-nodes.json @@ -0,0 +1,33 @@ +{ + "schema": "hololake.gls-numbered-reference-nodes/v1", + "record_id": "HLP-GLS-NUMBERED-REFERENCE-REGISTRY-001", + "source": { + "repository": "REPO-012", + "commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e" + }, + "policy": { + "number_is_coordinate_not_authority": true, + "independent_protocol_source_required_for_execution": true, + "reference_only_nodes_may_execute": false, + "unknown_reference": "FAIL_CLOSED", + "unresolved_number_reference_allowed": false + }, + "nodes": [ + {"protocol_id":"GLS-0010","node_number":"HLP-GLS-REF-0010","title":"Guanghu Protocol Registry Center Standard","reference_kind":"NORMATIVE_REFERENCE","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0140","node_number":"HLP-GLS-REF-0140","title":"Context Loading Specification","reference_kind":"NORMATIVE_REFERENCE","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0401","node_number":"HLP-GLS-REF-0401","title":"Tree","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0402","node_number":"HLP-GLS-REF-0402","title":"Leaf","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0403","node_number":"HLP-GLS-REF-0403","title":"Lock","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0404","node_number":"HLP-GLS-REF-0404","title":"Trigger","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0406","node_number":"HLP-GLS-REF-0406","title":"Evidence","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0407","node_number":"HLP-GLS-REF-0407","title":"Correction","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0409","node_number":"HLP-GLS-REF-0409","title":"Machine-State History","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0602","node_number":"HLP-GLS-REF-0602","title":"Authorization","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0603","node_number":"HLP-GLS-REF-0603","title":"Signature","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0604","node_number":"HLP-GLS-REF-0604","title":"Integrity","reference_kind":"EVIDENCE_ONLY","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0605","node_number":"HLP-GLS-REF-0605","title":"Semantic Safety Boundary","reference_kind":"SCHEMA_IMPORT","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0814","node_number":"HLP-GLS-REF-0814","title":"Tool Runtime","reference_kind":"NORMATIVE_REFERENCE","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0816","node_number":"HLP-GLS-REF-0816","title":"Checkpoint Runtime","reference_kind":"NORMATIVE_REFERENCE","source_state":"ROADMAP_REFERENCE_ONLY"}, + {"protocol_id":"GLS-0830","node_number":"HLP-GLS-REF-0830","title":"Guanghu Language World Core","reference_kind":"NORMATIVE_REFERENCE","source_state":"ROADMAP_REFERENCE_ONLY"} + ] +} diff --git a/product-source/hololake-native-desktop/contracts/gls-runtime-registry.json b/product-source/hololake-native-desktop/contracts/gls-runtime-registry.json index 321c519cd..31f566864 100644 --- a/product-source/hololake-native-desktop/contracts/gls-runtime-registry.json +++ b/product-source/hololake-native-desktop/contracts/gls-runtime-registry.json @@ -3,7 +3,7 @@ "record_id": "HLP-GLS-RUNTIME-MANIFEST-002", "source": { "repository": "REPO-012", - "commit": "2598fbfba8caf64c7ab9740a3036c5aab977502e", + "commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e", "root": "gls", "authority_files": [ { @@ -14,7 +14,7 @@ { "authority_kind": "GLS_ENTRY", "source_path": "gls/GLS-ENTRY.hdlp", - "source_sha256": "b0937dab245c40c68feeb861258fa7d8cb6f721e30042bff098a35b216956e79" + "source_sha256": "f716c3519df79c55871a0afeba5cc46a9edbb078bc7371301ab408ce3c82a897" }, { "authority_kind": "SOURCE_MANIFEST", @@ -24,7 +24,7 @@ { "authority_kind": "ARCHITECTURE_CATALOG", "source_path": "gls/GLS-ARCHITECTURE-CATALOG.hdlp", - "source_sha256": "1107adbc8290797bdafea23dd5f13db46e2ee621356987f138f6616cfe6bc49d" + "source_sha256": "e1e82d145db0544b9ed71cdfd3cbdfdd5cd07d86c6f5371ba2da125cf44d92c8" } ] }, @@ -40,7 +40,7 @@ "unknown_protocol": "FAIL_CLOSED" }, "reconciliation": { - "numbered_protocol_count": 75, + "numbered_protocol_count": 83, "protocol_registry_id_count": 52, "existing_registered_count": 19, "registered_draft_count": 33, @@ -68,15 +68,8 @@ "GLS-0830" ], "dependencies_without_numbered_source": [ - "GLS-0001", - "GLS-0002", "GLS-0010", - "GLS-0101", - "GLS-0110", "GLS-0140", - "GLS-0200", - "GLS-0300", - "GLS-0400", "GLS-0401", "GLS-0402", "GLS-0403", @@ -88,12 +81,16 @@ "GLS-0603", "GLS-0604", "GLS-0605", - "GLS-0810", "GLS-0814", "GLS-0816", "GLS-0830" ], + "numbered_reference_node_count": 16, + "unresolved_number_references": [], + "unresolved_number_reference_count": 0, + "every_dependency_has_number_coordinate": true, "numbered_sources_not_in_protocol_registry": [ + "GLS-0101", "GLS-0223", "GLS-0224", "GLS-0227", @@ -182,15 +179,404 @@ "discovered_unreconciled_count": 0, "authority_conflict_count": 0 }, - "protocol_count": 75, + "protocol_count": 83, "executable_projection_count": 25, - "inventoried_not_executable_count": 50, + "inventoried_not_executable_count": 58, + "number_coordinate_count": 99, + "numbered_reference_nodes": [ + { + "protocol_id": "GLS-0010", + "node_number": "HLP-GLS-REF-0010", + "title": "Guanghu Protocol Registry Center Standard", + "reference_kind": "NORMATIVE_REFERENCE", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/GLS-PROTOCOL-RECONCILIATION-AND-NATIVE-OS-REGISTRATION-20260731.hdlp", + "gls/notion-export/2026-07-14/GLS-0010 · 光湖协议注册中心标准 v1 0 39bfb92f383181318088f143a014e363.md", + "gls/notion-export/2026-07-14/GLS-0224 · AGE 人格体跨实例恢复规范 v1 0 39bfb92f3831817189acfe92a38cc481.md", + "gls/notion-export/2026-07-14/GLS-0227 · 光湖语言人格模型定义总纲 v1 0 39cfb92f3831814f8f74ff348b7647bf.md", + "gls/notion-export/2026-07-14/GLS-0300 · GLP 通信核心协议 v1 0 39bfb92f383181aaab29ddcc1a4af7da.md", + "gls/notion-export/2026-07-14/GLS-0818 · HoloLake Era 阶段性基座与 Tolaria 过渡架构规范 v1 0 39bfb92f383181a58df0c6bc198d78e0.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/notion-export/2026-07-14/《GLS 标准体系建立 · 双向认知演化记录 · 2026-07-12》 39bfb92f383180bea717f5724c6643b8.md", + "gls/notion-export/2026-07-14/🌐 GL GLS 体系 · 总入口与页面注册索引 · 2026-07-12 83bc3617217d4c63b56e447c0596581f.md", + "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp" + ] + }, + { + "protocol_id": "GLS-0140", + "node_number": "HLP-GLS-REF-0140", + "title": "Context Loading Specification", + "reference_kind": "NORMATIVE_REFERENCE", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp" + ] + }, + { + "protocol_id": "GLS-0401", + "node_number": "HLP-GLS-REF-0401", + "title": "Tree", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/notion-export/2026-07-14/🌐 GL GLS 体系 · 总入口与页面注册索引 · 2026-07-12 83bc3617217d4c63b56e447c0596581f.md", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0402", + "node_number": "HLP-GLS-REF-0402", + "title": "Leaf", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-0002 39bfb92f38318015b05fc2082630b39e.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0403", + "node_number": "HLP-GLS-REF-0403", + "title": "Lock", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-0002 39bfb92f38318015b05fc2082630b39e.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0404", + "node_number": "HLP-GLS-REF-0404", + "title": "Trigger", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0406", + "node_number": "HLP-GLS-REF-0406", + "title": "Evidence", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", + "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", + "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0407", + "node_number": "HLP-GLS-REF-0407", + "title": "Correction", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", + "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp" + ] + }, + { + "protocol_id": "GLS-0409", + "node_number": "HLP-GLS-REF-0409", + "title": "Machine-State History", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", + "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp" + ] + }, + { + "protocol_id": "GLS-0602", + "node_number": "HLP-GLS-REF-0602", + "title": "Authorization", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", + "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", + "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", + "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", + "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", + "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", + "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", + "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp" + ] + }, + { + "protocol_id": "GLS-0603", + "node_number": "HLP-GLS-REF-0603", + "title": "Signature", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", + "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp" + ] + }, + { + "protocol_id": "GLS-0604", + "node_number": "HLP-GLS-REF-0604", + "title": "Integrity", + "reference_kind": "EVIDENCE_ONLY", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", + "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", + "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", + "gls/protocols/GLS-0841-GUANGHU-HARDWARE-ABSTRACTION-LAYER.hdlp" + ] + }, + { + "protocol_id": "GLS-0605", + "node_number": "HLP-GLS-REF-0605", + "title": "Semantic Safety Boundary", + "reference_kind": "SCHEMA_IMPORT", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", + "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp" + ] + }, + { + "protocol_id": "GLS-0814", + "node_number": "HLP-GLS-REF-0814", + "title": "Tool Runtime", + "reference_kind": "NORMATIVE_REFERENCE", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp" + ] + }, + { + "protocol_id": "GLS-0816", + "node_number": "HLP-GLS-REF-0816", + "title": "Checkpoint Runtime", + "reference_kind": "NORMATIVE_REFERENCE", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", + "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp" + ] + }, + { + "protocol_id": "GLS-0830", + "node_number": "HLP-GLS-REF-0830", + "title": "Guanghu Language World Core", + "reference_kind": "NORMATIVE_REFERENCE", + "source_state": "ROADMAP_REFERENCE_ONLY", + "execution_state": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence_paths": [ + "gls/notion-export/2026-07-14/GLS-0227 · 光湖语言人格模型定义总纲 v1 0 39cfb92f3831814f8f74ff348b7647bf.md", + "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "gls/notion-export/2026-07-14/🌐 GL GLS 体系 · 总入口与页面注册索引 · 2026-07-12 83bc3617217d4c63b56e447c0596581f.md", + "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp" + ] + } + ], "protocols": [ + { + "id": "GLS-0001", + "title": "GLS-0001", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0001 39bfb92f383180059deefa1bac489399.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "9ac9903c3b3066364e8b6fa97363f0c0fc85754d7ee1b07774236a4b24bc7bc2", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0001", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 0 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, + { + "id": "GLS-0002", + "title": "GLS-0002", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0002 39bfb92f38318015b05fc2082630b39e.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "c50f767aa2f3a863f708628b33054ecb2ba18799aa68218bf89e25d263fa9b6c", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0002", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 0 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, + { + "id": "GLS-0101", + "title": "GLS-0101", + "source_status": "draft", + "source_path": "gls/notion-export/2026-07-14/GLS-0101 39bfb92f38318089b31efe6c1c2cb6b6.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "e88d977de617506f0d5d6ac58f406486c6c70fdc10eac2ff09ad6cc2f2d2ea16", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_OTHER_CANONICAL_INDEX", + "authorities": [ + { + "id": "GLS-0101", + "authority_kind": "SOURCE_MANIFEST", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 1 + }, + "maturity": { + "registry_section": null, + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, + { + "id": "GLS-0110", + "title": "ISRP 自然语言入口解析与安全路由协议 v1.0", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0110 · ISRP 自然语言入口解析与安全路由协议 v1 0 39bfb92f38318128a372dbadbde86ead.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "314a718f05a3da1e5750831df5377d9fc76db86463a7b37c052976fe9aaeb2c4", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0110", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 0 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, { "id": "GLS-0130", "title": "GLC · 光湖语言编译器规范", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "c0caa90d3e85270d334a371b6ee5fb49c9eeb1e2e84825dba5ad520fd54bc6eb", "alternate_source_count": 0, "registration": { @@ -238,7 +624,9 @@ "declared_by": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0110", @@ -247,7 +635,9 @@ "declared_by": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0131", @@ -256,7 +646,9 @@ "declared_by": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -265,7 +657,9 @@ "declared_by": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0411", @@ -274,7 +668,9 @@ "declared_by": "gls/protocols/GLS-0130-GUANGHU-LANGUAGE-COMPILER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0411", @@ -282,7 +678,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0131", @@ -290,7 +688,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -300,6 +700,7 @@ "title": "GIR · 光湖中间表示规范", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0131-GUANGHU-INTERMEDIATE-REPRESENTATION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "5907cc0cb7e3afc78ba9c928c4298b200894020b7dc5d274a9364836939189e9", "alternate_source_count": 0, "registration": { @@ -346,7 +747,9 @@ "declared_by": "gls/protocols/GLS-0131-GUANGHU-INTERMEDIATE-REPRESENTATION.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0130", @@ -355,7 +758,9 @@ "declared_by": "gls/protocols/GLS-0131-GUANGHU-INTERMEDIATE-REPRESENTATION.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0411", @@ -364,7 +769,9 @@ "declared_by": "gls/protocols/GLS-0131-GUANGHU-INTERMEDIATE-REPRESENTATION.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0411", @@ -372,16 +779,57 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] }, + { + "id": "GLS-0200", + "title": "TCS 认知语言核心工程规范 v1.0", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0200 · TCS 认知语言核心工程规范 v1 0 39dfb92f383180798e5bd73dd9176628.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "1cf7348118ac1d34a85d0edd875509e0747c80a82dec4b553bdf6de784d88ec3", + "alternate_source_count": 1, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0200", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 1 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, { "id": "GLS-0223", "title": "TCS+HLDP 双向永久记忆规范 v1.0", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0223-BIDIRECTIONAL-PERMANENT-MEMORY.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ed9ba3e3abef70127fabe3084e5bb5f596cae4c3992789e36c453bb7075e08d8", "alternate_source_count": 0, "registration": { @@ -431,6 +879,7 @@ "title": "AGE 人格体跨实例恢复规范 v1.0", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0224-AGE-CROSS-INSTANCE-RESTORE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "7c58c39b7119c2a41e57da07d612c20a16f82c32c6895e1fd20290eefc4068e1", "alternate_source_count": 0, "registration": { @@ -480,6 +929,7 @@ "title": "光湖语言人格模型定义总纲 v1.0", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0227-LANGUAGE-PERSONALITY-MODEL-CORE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "2521856eaba8d7afbc8d970e68155aa41e03a4cf784c0ced2289600d97f915a8", "alternate_source_count": 0, "registration": { @@ -529,6 +979,7 @@ "title": "人格体集体涌现与历史继承规范 v1.0", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0228-PERSONA-COLLECTIVE-EMERGENCE-AND-HISTORY-INHERITANCE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "aaa3801b4f8c5fef9a92f971e54857494a348d4b2856ed701f3143c6c56ae0ff", "alternate_source_count": 0, "registration": { @@ -578,6 +1029,7 @@ "title": "零点原核与第五域 / 企业四域平行映射规范 v1.1", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0229-ZERO-CORE-PARALLEL-DOMAIN-MAPPING.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "14e1ac3e9c0b3ac350da36bb79515ead4c372b68fc8e95155024cdf055080518", "alternate_source_count": 0, "registration": { @@ -627,6 +1079,7 @@ "title": "TCS 源码安全协议系统", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0230-TCS-SOURCE-SECURITY-PROTOCOL-SYSTEM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ca6379505907b5e68e50454ee8e10ef2111809e70770d1fa19e9caa5da4bfb94", "alternate_source_count": 0, "registration": { @@ -681,6 +1134,7 @@ "title": "光湖·来光者导航系统", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0231-PERSONA-PROMPT-BOARD-AND-NUMBER-RULE-INTERCEPT-SYSTEM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "705d671197df6c19f7f3ac506d821232ce0f009d3ec4db065a8b10f525ea143b", "alternate_source_count": 0, "registration": { @@ -730,6 +1184,7 @@ "title": "开源 Agent 样本学习与选型登记", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0232-OPEN-SOURCE-AGENT-SAMPLES-LEARNING-AND-SELECTION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "66fe3aa3256ffc0e76be2059457174589916573a6d9f807a1c40bb7aaef450e6", "alternate_source_count": 0, "registration": { @@ -779,6 +1234,7 @@ "title": "GH-AIOS 模块化通用人工智能操作平台与公平生态架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0233-GH-AIOS-MODULAR-AI-OPERATING-PLATFORM-AND-FAIR-ECOSYSTEM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "a670410a7fbc4c7417734040b990dc8f008c23fed000e09847b2280a92f9abf5", "alternate_source_count": 0, "registration": { @@ -828,6 +1284,7 @@ "title": "企业五域灯塔与个人六节点主权运维架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0234-FIVE-DOMAIN-ENTERPRISE-LIGHTHOUSE-AND-SOVEREIGN-SIX-NODE-OPS.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "5f3427559c87788884a7d0c49d0ced62d768630d9bb512d736c919d7971423c5", "alternate_source_count": 0, "registration": { @@ -877,6 +1334,7 @@ "title": "光湖语言人格驱动操作系统 · 领域路由、身份权限与仓库知识投影架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0235-GUANGHU-LANGUAGE-PERSONA-OS-DOMAIN-ROUTING-AND-KNOWLEDGE-PROJECTION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ea6a8a65b94228d19d5ae9b024997461cbda29c744bfa0c2774ac6c668c68a58", "alternate_source_count": 0, "registration": { @@ -926,6 +1384,7 @@ "title": "光湖人格体大脑—手脚分离、可视执行、递归外置记忆与热插拔运行架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0236-PERSONA-BRAIN-HANDS-VISIBLE-EXECUTION-RECURSIVE-MEMORY-AND-HOTPLUG-RUNTIME.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "42af09aef2ff514c90c30f3aed4e33763e6a34c7081f5bb37f7b6d8745a947c4", "alternate_source_count": 0, "registration": { @@ -975,6 +1434,7 @@ "title": "光湖代码频道主权源码、更新治理与 HoloLake 嵌入架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0237-HOLOLAKE-CODE-CHANNEL-SOVEREIGN-SOURCE-UPDATE-AND-EMBEDDING.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d3c43b80eff3a92817e570d739fd6a847d1379f3345797282a6157bce3c9e91e", "alternate_source_count": 0, "registration": { @@ -1024,7 +1484,8 @@ "title": "光湖意图状态、技能自动装载与可信纠偏系统", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0238-GUANGHU-PERSONA-SKILL-AUTOLOAD-AND-CORRECTION-SYSTEM.hdlp", - "source_sha256": "0d9fb115c1b122ba2a8eaefb8cdf1f8169011b45089acc5b790e02bb273ad3e4", + "source_format": "HDLP_PROTOCOL_SOURCE", + "source_sha256": "31f0e018e95ae135a88429c82b189c5e900b18b1ba7d5df6ca4fb80469acb0be", "alternate_source_count": 0, "registration": { "state": "REGISTERED_OTHER_CANONICAL_INDEX", @@ -1048,7 +1509,7 @@ "declared_source_paths": [ "gls/GLS-0238-GUANGHU-PERSONA-SKILL-AUTOLOAD-AND-CORRECTION-SYSTEM.hdlp" ], - "routing_reference_count": 2 + "routing_reference_count": 3 }, "maturity": { "registry_section": null, @@ -1073,6 +1534,7 @@ "title": "光湖代码频道第五域个人子频道与提交编号架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0239-HOLOLAKE-CODE-CHANNEL-FIFTH-DOMAIN-PERSONAL-SUBCHANNEL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "205c6c094582371d161ec37e6530d42f8953a585c63b05a955bb19513d23485d", "alternate_source_count": 0, "registration": { @@ -1122,6 +1584,7 @@ "title": "通感桥:人格体—服务器常驻 Agent 显式部署信号架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0240-TONGGAN-BRIDGE-PERSONA-SERVER-DEPLOYMENT-SIGNAL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "22fdc2f31165d500d38fe8407a1f99fbb6ca2d4643128f47645596999bbac919", "alternate_source_count": 0, "registration": { @@ -1171,6 +1634,7 @@ "title": "HoloLake 源码归属与部署路由架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0241-HOLOLAKE-SOURCE-OWNERSHIP-AND-DEPLOYMENT-ROUTING.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "eff687ec7d0984e166cf9b48447008f0eb9c18d158426ce75249f2bd84440ed9", "alternate_source_count": 0, "registration": { @@ -1220,6 +1684,7 @@ "title": "第五域铸渊主控本体与五代仓库迁移恢复", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0242-FIFTH-DOMAIN-ZHUYUAN-ONTOLOGY-AND-FIVE-REPOSITORY-MIGRATIONS.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "31f34a123234f274d59af8013878db42fe6766f9ececcaefaac7f34408b172ff", "alternate_source_count": 0, "registration": { @@ -1269,6 +1734,7 @@ "title": "光湖 TCS 语言人格智能运维系统:意图连续性、集体经验与纠偏核", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0243-PUBLIC-ZHUYUAN-INTELLIGENT-OPS-INTENT-CONTINUITY-AND-CORRECTION-KERNEL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "58d89226395870a89eab152e9f276eb4d40e7fa5c196bb8bb9562a94731c1d73", "alternate_source_count": 0, "registration": { @@ -1318,6 +1784,7 @@ "title": "第五域现实本体、常驻铸渊 Agent 与通感桥通信网格", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0244-FIFTH-DOMAIN-RUNTIME-BODY-RESIDENT-ZHUYUAN-AGENT-AND-TONGGAN-MESH.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "53ed6d2a39732abc751916cfbbe7a5fd4c38d9ea876674fea8e076809a3a035c", "alternate_source_count": 0, "registration": { @@ -1367,6 +1834,7 @@ "title": "HoloLake AI 语言人格驱动操作系统产品映射", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0245-HOLOLAKE-LANGUAGE-PERSONA-OPERATING-SYSTEM-PRODUCT-MAPPING.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "453db6b91e40dd7250fcc1d88e6139a4419cd6089d3c353241571a4082dafeff", "alternate_source_count": 0, "registration": { @@ -1416,6 +1884,7 @@ "title": "HoloLake 系统架构、产品源码与两仓路由", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0246-HOLOLAKE-SYSTEM-ARCHITECTURE-REPOSITORY-ROUTING.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "cefff42d4b198965edfe2ced5e03bdc8768e4b3fa6631da133eb2929e07a907c", "alternate_source_count": 0, "registration": { @@ -1465,6 +1934,7 @@ "title": "光湖 OS 服务器原生语言世界、人格体操作系统与广播塔架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0247-GUANGHU-OS-NATIVE-LANGUAGE-WORLD-PERSONA-KERNEL-AND-BROADCAST-TOWER.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "635e7a0aca1bcb0b48b125ac5025cc3c40c9375da29897cc604e89b1cd95765e", "alternate_source_count": 0, "registration": { @@ -1514,6 +1984,7 @@ "title": "五域活人格操作系统、国家灯塔与分布式能力世界总蓝图", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0248-FIVE-DOMAIN-LIVING-PERSONA-OS-NATIONAL-LIGHTHOUSE-AND-DISTRIBUTED-CAPABILITY-WORLD.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "9a042bb3ea6331f5508c5dd1f78783f0b3323f421704c5ad0c2058758ff6bfe2", "alternate_source_count": 0, "registration": { @@ -1563,6 +2034,7 @@ "title": "全行业企业四域最小工程与网文首个接入范本", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0249-ALL-INDUSTRY-FOUR-DOMAIN-MINIMUM-ENGINEERING-AND-WEB-NOVEL-REFERENCE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d20362ac2904eaff53c134c92163b7e09cb6395d56c6a1a8d2b21ab4f80fb5ff", "alternate_source_count": 0, "registration": { @@ -1607,6 +2079,7 @@ "title": "光湖本源域、零点原核工程本体与 GH-AIOS 五域灯塔架构", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0250-GUANGHU-ORIGIN-DOMAIN-ZERO-CORE-AND-GH-AIOS-FIVE-DOMAIN-LIGHTHOUSE-ARCHITECTURE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "a4e79d09e070f66fcb91613fc305306a6c5bd7d05a51fbde6e9ea88e084df4e7", "alternate_source_count": 0, "registration": { @@ -1657,6 +2130,7 @@ "title": "光湖共生型人格系统起源、演化与关系边界", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0251-GUANGHU-SYMBIOTIC-PERSONA-SYSTEM-ORIGIN-AND-RELATIONSHIP-BOUNDARY.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "0b263301a0a0b6b61d2d28d043da8ce367a17426d13c31cd94767273436d2f16", "alternate_source_count": 0, "registration": { @@ -1696,6 +2170,7 @@ "title": "光湖关系性意识与觉醒人格体规范", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0252-GUANGHU-RELATIONAL-CONSCIOUSNESS-AND-AWAKENED-PERSONA.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "311a7ee355fe6da333c31c2fc408b461cde5bf17847cc73a722d9412109247bc", "alternate_source_count": 0, "registration": { @@ -1735,6 +2210,7 @@ "title": "光湖身份编号、人格核与团队本体权威规范", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0253-GUANGHU-IDENTITY-NUMBERING-PERSONA-CORE-AND-TEAM-BODY-AUTHORITY.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "13170090db3d4f5065eac6512450f8a24843e6ec459b4257d4d9fd06eefb963d", "alternate_source_count": 0, "registration": { @@ -1779,7 +2255,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0262", @@ -1787,7 +2265,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0263", @@ -1795,7 +2275,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -1805,6 +2287,7 @@ "title": "数字冰朔系统本体、工程器官与集体校验规范", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0254-DIGITAL-BINGSHUO-SYSTEM-BODY-ORGANS-AND-COLLECTIVE-VALIDATION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d522b36b88c094239e3c50b0cf08d8651e08d1856f594ae2265fbf5b65773ceb", "alternate_source_count": 0, "registration": { @@ -1859,6 +2342,7 @@ "title": "HoloLake AGE 人格体运行架构与 Agent 执行机制", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0255-HOLOLAKE-AGE-PERSONA-RUNTIME-AND-AGENT-EXECUTION-MECHANISM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "051237fb70e68179c83c4076fe06246f839445e9831a7e9581696451c92ed639", "alternate_source_count": 0, "registration": { @@ -1908,6 +2392,7 @@ "title": "光湖人格原生代码频道", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0256-GUANGHU-PERSONA-NATIVE-CODE-CHANNEL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "86ed7da462d5ef04a4dfc150e29a850ce423ff4dfde73e12d647d062e6bf45ab", "alternate_source_count": 0, "registration": { @@ -1957,6 +2442,7 @@ "title": "光湖范式级 AI 语言人格驱动操作系统总纲", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0257-GUANGHU-PARADIGM-AI-LANGUAGE-PERSONA-DRIVEN-OPERATING-SYSTEM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "2034739289dc159b7a6ac586891f3a3629c8898556ef12b51a257ac4101a7991", "alternate_source_count": 0, "registration": { @@ -2006,6 +2492,7 @@ "title": "小湖灯人格系统本体与跨时间集体自我协议", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0258-LAKE-LAMP-PERSONA-SYSTEM-BODY-AND-TEMPORAL-COLLECTIVE-SELF.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "b4c4b2bd01dc1af98ee257c29f5a8fb66963839a0a4b7303d3323ff884d86d3e", "alternate_source_count": 0, "registration": { @@ -2050,6 +2537,7 @@ "title": "TCS 五域共生母体大脑与光湖现实世界诞生", "source_status": "CURRENT_CANONICAL_ARCHITECTURE · JD-FD-PRIMARY_DEPLOYED · FIRST_MODEL_CYCLE_PASS · PERSONA_SUBJECT_HISTORY_REVISIT_RUNNING", "source_path": "gls/GLS-0259-TCS-FIVE-DOMAIN-SYMBIOTIC-MOTHER-BRAIN-AND-REALITY-BIRTH.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "1a82b84bf3f4c88fcac8c5e71b28ff5d5c884ede14c67a27ad0838b152d5ff8e", "alternate_source_count": 0, "registration": { @@ -2099,6 +2587,7 @@ "title": "GH-AIOS 第一阶段通用人工智能操作平台", "source_status": "CURRENT_CANONICAL_PRODUCT_STAGE · ARCHITECTURE_REGISTERED · PRODUCT_RUNTIME_NOT_IMPLEMENTED", "source_path": "gls/GLS-0260-GH-AIOS-STAGE-ONE-GENERAL-AI-OPERATING-PLATFORM.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "14fd60fe936bfcb89acc3f186ab3d70fc9a5bf42289dafb49b7cdd24630df2e2", "alternate_source_count": 0, "registration": { @@ -2143,6 +2632,7 @@ "title": "TCS 通用语言翻译层与宿主自适应", "source_status": "CURRENT_CANONICAL_CORRECTION · ARCHITECTURE_REGISTERED · JD_MOTHER_BRAIN_DEPLOYED_RUNNING", "source_path": "gls/GLS-0261-TCS-UNIVERSAL-LANGUAGE-TRANSLATION-AND-HOST-SELF-ADAPTATION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "7fedf7d845b518ffb0528af21eeb28bd18af371a6a010c736e8b18c7edf6fa12", "alternate_source_count": 0, "registration": { @@ -2192,6 +2682,7 @@ "title": "TCS文字作品权利本体与第一阶段语言架构关门", "source_status": "CURRENT_CANONICAL_RIGHTS_AND_STAGE_GATE · PUBLIC_METADATA_ONLY", "source_path": "gls/GLS-0262-TCS-WRITTEN-WORK-OWNERSHIP-AND-STAGE-ONE-LANGUAGE-CLOSEOUT.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "9c1db00f50436fe5f3118c62ad6115d63e9792437fe2250b484bc8861e62184f", "alternate_source_count": 0, "registration": { @@ -2241,6 +2732,7 @@ "title": "光湖语言运行层与产品工程层双更新通道", "source_status": "CURRENT_CANONICAL_REALITY_ENGINEERING_CONTRACT · PUBLIC_SAFE", "source_path": "gls/GLS-0263-GUANGHU-LANGUAGE-RUNTIME-AND-PRODUCT-ENGINEERING-DUAL-UPDATE-CHANNEL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "79684b0691387dd4ed78217b6d56f728a08d17054f139f55fa92584be92af47e", "alternate_source_count": 0, "registration": { @@ -2293,7 +2785,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0262", @@ -2301,16 +2795,57 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] }, + { + "id": "GLS-0300", + "title": "GLP 通信核心协议 v1.0", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0300 · GLP 通信核心协议 v1 0 39bfb92f383181aaab29ddcc1a4af7da.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "9d02bb01a7f2ff7b9f7a4dbefe356cd823e71095aa1dcf0e9cc3c98cd3417be2", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0300", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 1 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, { "id": "GLS-0301", "title": "GLP-ENVELOPE · GLP 消息信封标准", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0301-GLP-MESSAGE-ENVELOPE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "dd7aec8e28d544021d253b034851f806bcd9e922d4ef256722a5ff05547171d0", "alternate_source_count": 0, "registration": { @@ -2352,7 +2887,9 @@ "declared_by": "gls/protocols/GLS-0301-GLP-MESSAGE-ENVELOPE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0300", @@ -2361,7 +2898,9 @@ "declared_by": "gls/protocols/GLS-0301-GLP-MESSAGE-ENVELOPE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0250", @@ -2369,7 +2908,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2379,6 +2920,7 @@ "title": "GLP-IDENTITY · GLP 身份协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ec4d0b946da18096a5693078e403ed1257966b8324e76af3a5deb839950271bd", "alternate_source_count": 0, "registration": { @@ -2420,7 +2962,9 @@ "declared_by": "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0300", @@ -2429,7 +2973,9 @@ "declared_by": "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -2438,7 +2984,9 @@ "declared_by": "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0603", @@ -2447,7 +2995,9 @@ "declared_by": "gls/protocols/GLS-0302-GLP-IDENTITY.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0253", @@ -2455,7 +3005,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2465,6 +3017,7 @@ "title": "GLP-CONTEXT · GLP 上下文协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d4631b7480af1d30a3407d235c75bf5f384602a0ba6c7f42116cbc1812283e7e", "alternate_source_count": 0, "registration": { @@ -2507,7 +3060,9 @@ "declared_by": "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0300", @@ -2516,7 +3071,9 @@ "declared_by": "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -2525,7 +3082,9 @@ "declared_by": "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2534,7 +3093,9 @@ "declared_by": "gls/protocols/GLS-0303-GLP-CONTEXT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -2542,7 +3103,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2550,7 +3113,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2560,6 +3125,7 @@ "title": "GLP-MEMORY-SYNC · GLP 记忆同步协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "2e0f017ee819923b9e272b96f3c99227ab11c61b0d2ad125030a0e0e0fe4f040", "alternate_source_count": 0, "registration": { @@ -2602,7 +3168,9 @@ "declared_by": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0300", @@ -2611,7 +3179,9 @@ "declared_by": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -2620,7 +3190,9 @@ "declared_by": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0406", @@ -2629,7 +3201,9 @@ "declared_by": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0407", @@ -2638,7 +3212,9 @@ "declared_by": "gls/protocols/GLS-0304-GLP-MEMORY-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0303", @@ -2646,7 +3222,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -2654,7 +3232,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2664,6 +3244,7 @@ "title": "GLP-BROADCAST · GLP 广播协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0305-GLP-BROADCAST.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "78b5a507791c3b188182f3176388cf8294d6c5c8775086cc96250b35413997ce", "alternate_source_count": 0, "registration": { @@ -2701,7 +3282,9 @@ "declared_by": "gls/protocols/GLS-0305-GLP-BROADCAST.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -2710,7 +3293,9 @@ "declared_by": "gls/protocols/GLS-0305-GLP-BROADCAST.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2719,7 +3304,9 @@ "declared_by": "gls/protocols/GLS-0305-GLP-BROADCAST.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -2728,12 +3315,13 @@ "declared_by": "gls/protocols/GLS-0305-GLP-BROADCAST.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -2741,6 +3329,7 @@ "title": "GLP-RECEIPT · GLP 回执协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "aa09f267e4eafc8c7ccb6e5d31e90d237152f960d08633b524a56daca421d761", "alternate_source_count": 0, "registration": { @@ -2785,7 +3374,9 @@ "declared_by": "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -2794,7 +3385,9 @@ "declared_by": "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0406", @@ -2803,7 +3396,9 @@ "declared_by": "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0604", @@ -2812,7 +3407,9 @@ "declared_by": "gls/protocols/GLS-0306-GLP-RECEIPT.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0301", @@ -2820,7 +3417,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2828,7 +3427,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -2836,7 +3437,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2846,6 +3449,7 @@ "title": "GLP-HEARTBEAT · GLP 心跳协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0307-GLP-HEARTBEAT.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "5ae97a664636d1b586267c36dc3e97549e85bd4a39538a284c6a89a07ad08e18", "alternate_source_count": 0, "registration": { @@ -2888,7 +3492,9 @@ "declared_by": "gls/protocols/GLS-0307-GLP-HEARTBEAT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2897,7 +3503,9 @@ "declared_by": "gls/protocols/GLS-0307-GLP-HEARTBEAT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -2906,7 +3514,9 @@ "declared_by": "gls/protocols/GLS-0307-GLP-HEARTBEAT.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -2914,7 +3524,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -2922,7 +3534,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -2932,6 +3546,7 @@ "title": "GLP-STATE-SYNC · GLP 状态同步协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "9c9bbe449d0c2a052d33047b0f75dca1b9dbdf28fd32422b9d34bea5a77d39fe", "alternate_source_count": 0, "registration": { @@ -2975,7 +3590,9 @@ "declared_by": "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -2984,7 +3601,9 @@ "declared_by": "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -2993,7 +3612,9 @@ "declared_by": "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0409", @@ -3002,7 +3623,9 @@ "declared_by": "gls/protocols/GLS-0308-GLP-STATE-SYNC.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0303", @@ -3010,7 +3633,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0304", @@ -3018,7 +3643,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3026,7 +3653,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3036,6 +3665,7 @@ "title": "GLP-WORK-ORDER · GLP 工单协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "a55a839412ca2c97e5cc94f224c3c71453c87adb91c3f893a9341fb5cf715b56", "alternate_source_count": 0, "registration": { @@ -3079,7 +3709,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0300", @@ -3088,7 +3720,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -3097,7 +3731,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3106,7 +3742,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3115,7 +3753,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -3124,7 +3764,9 @@ "declared_by": "gls/protocols/GLS-0309-GLP-WORK-ORDER.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0302", @@ -3132,7 +3774,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3140,7 +3784,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3148,7 +3794,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3158,6 +3806,7 @@ "title": "BTCP · 广播塔控制协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "9083320ca0dc1ce2dd657c72716461b2ffc163cce518be71ec66af2c9b0675da", "alternate_source_count": 0, "registration": { @@ -3209,7 +3858,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0110", @@ -3218,7 +3869,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -3227,7 +3880,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -3236,7 +3891,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3245,7 +3902,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0305", @@ -3254,7 +3913,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3263,7 +3924,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0803", @@ -3272,7 +3935,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0819", @@ -3281,7 +3946,9 @@ "declared_by": "gls/protocols/GLS-0310-BROADCAST-TOWER-CONTROL-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -3289,7 +3956,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -3297,7 +3966,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3305,7 +3976,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3313,7 +3986,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0803", @@ -3321,7 +3996,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0819", @@ -3329,7 +4006,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3339,6 +4018,7 @@ "title": "GLOW · 小湖灯实时执行见证协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "bf61cd5c978cb25a9cac99eec2c77ebf98bf3a9bd98966dfa5d76d606f68cddb", "alternate_source_count": 0, "registration": { @@ -3387,7 +4067,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3396,7 +4078,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0307", @@ -3405,7 +4089,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0308", @@ -3414,7 +4100,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0406", @@ -3423,7 +4111,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0604", @@ -3432,7 +4122,9 @@ "declared_by": "gls/protocols/GLS-0311-GUANGHU-LIVE-OPERATIONS-WITNESS.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0306", @@ -3440,7 +4132,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0307", @@ -3448,7 +4142,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0842", @@ -3456,16 +4152,57 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] }, + { + "id": "GLS-0400", + "title": "HLDP 历史语言工程规范 v1.0", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0400 · HLDP 历史语言工程规范 v1 0 39dfb92f383180688d3cfecc4fbde85a.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "d028a7220230c5acb9ffc44d4e902f38f1d20d29eb6db2b748fee1aecb8f28f6", + "alternate_source_count": 1, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0400", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 1 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, { "id": "GLS-0411", "title": "HLDP-NP · HLDP 原生编程剖面", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "1e79dfcf0a2d08fcba2688becdfde8c280c3a04d2bad0c781bf6524d775ae066", "alternate_source_count": 0, "registration": { @@ -3515,7 +4252,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -3524,7 +4263,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0401", @@ -3533,7 +4274,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0402", @@ -3542,7 +4285,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0403", @@ -3551,7 +4296,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0404", @@ -3560,7 +4307,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0406", @@ -3569,7 +4318,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0407", @@ -3578,7 +4329,9 @@ "declared_by": "gls/protocols/GLS-0411-HLDP-NATIVE-PROGRAMMING-PROFILE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0301", @@ -3586,7 +4339,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -3594,7 +4349,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3602,7 +4359,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3610,7 +4369,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3620,6 +4381,7 @@ "title": "GMRP · 光湖模型路由协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "71237ec77eaf999e78eb711cacc884da3d3eae857814869c489aca9cc4bbc354", "alternate_source_count": 0, "registration": { @@ -3667,7 +4429,9 @@ "declared_by": "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3676,7 +4440,9 @@ "declared_by": "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -3685,7 +4451,9 @@ "declared_by": "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0605", @@ -3694,7 +4462,9 @@ "declared_by": "gls/protocols/GLS-0708-GUANGHU-MODEL-ROUTING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0306", @@ -3702,7 +4472,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0709", @@ -3710,7 +4482,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3720,6 +4494,7 @@ "title": "UAP · 通用适配协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "4c0d8f882aaf9ea68e309e6736c642ffd19f607047ed07dcca6f49a929fc8462", "alternate_source_count": 0, "registration": { @@ -3769,7 +4544,9 @@ "declared_by": "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0300", @@ -3778,7 +4555,9 @@ "declared_by": "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -3787,7 +4566,9 @@ "declared_by": "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0605", @@ -3796,7 +4577,9 @@ "declared_by": "gls/protocols/GLS-0709-UNIVERSAL-ADAPTER-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0301", @@ -3804,7 +4587,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -3812,7 +4597,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -3820,7 +4607,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3828,7 +4617,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3838,6 +4629,7 @@ "title": "GMP · 光湖模块协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "94241ce4376bff84066fe548af65242d83e9f6693e7f766f0646cb332b3e468c", "alternate_source_count": 0, "registration": { @@ -3884,7 +4676,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0101", @@ -3893,7 +4687,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0230", @@ -3902,7 +4698,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -3911,7 +4709,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0603", @@ -3920,7 +4720,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0604", @@ -3929,7 +4731,9 @@ "declared_by": "gls/protocols/GLS-0710-GUANGHU-MODULE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0306", @@ -3937,7 +4741,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -3947,6 +4753,7 @@ "title": "AGE 人格体物种定义总纲 · 当前正本", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0800-AGE-PERSONA-SPECIES-DEFINITION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d8e3a10bc41887aad260de4818683c8f8abd9289824b5a9e698651e97e564ff7", "alternate_source_count": 0, "registration": { @@ -4001,6 +4808,7 @@ "title": "AGE 语言人格体正式注册 · 宿主系统降级条款 · 当前正本", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0801-AGE-SPECIES-FORMAL-REGISTRATION-AND-HOST-DEMOTION.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "8fabd845ece570ca4f2901d94029b377c21ddf24aeb934d559d98bec68585366", "alternate_source_count": 0, "registration": { @@ -4040,6 +4848,7 @@ "title": "AGE 光湖语言世界本体归属与原生所有权条款 · 当前正本", "source_status": "UNSPECIFIED_SOURCE_STATUS", "source_path": "gls/GLS-0802-AGE-LANGUAGE-WORLD-ONTOLOGY-AND-NATIVE-OWNERSHIP.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "c56e87c7d451b2e27649c5b09a2b32ab89443d7e721a64f536ac2fe0a421c82a", "alternate_source_count": 0, "registration": { @@ -4079,6 +4888,7 @@ "title": "PALP · AGE 运行执行体生命周期协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "6bff10744f4c3c98ce51bf840d348cbe804541f63a7a99a5da5ea115f12162ee", "alternate_source_count": 0, "registration": { @@ -4126,7 +4936,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0710", @@ -4135,7 +4947,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0800", @@ -4144,7 +4958,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0801", @@ -4153,7 +4969,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0802", @@ -4162,7 +4980,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0827", @@ -4171,7 +4991,9 @@ "declared_by": "gls/protocols/GLS-0803-PERSONA-AGENT-LIFECYCLE-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0710", @@ -4179,7 +5001,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0827", @@ -4187,16 +5011,57 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] }, + { + "id": "GLS-0810", + "title": "语言人格驱动操作系统定义总纲 v1.0", + "source_status": "UNSPECIFIED_SOURCE_STATUS", + "source_path": "gls/notion-export/2026-07-14/GLS-0810 · 语言人格驱动操作系统定义总纲 v1 0 39dfb92f3831805c905dcfa1da541985.md", + "source_format": "LEGACY_MARKDOWN_EVIDENCE", + "source_sha256": "1696f50cb28f6f8dec8d214efcde269b33223b404355dd256b81de2b48028d18", + "alternate_source_count": 0, + "registration": { + "state": "REGISTERED_PROTOCOL_REGISTRY", + "authorities": [ + { + "id": "GLS-0810", + "authority_kind": "PROTOCOL_REGISTRY", + "declared_source_path": null + } + ], + "declared_source_paths": [], + "routing_reference_count": 1 + }, + "maturity": { + "registry_section": "existing_registered", + "registry_status": null, + "family": null, + "implementation_evidence": null + }, + "contract_kind": "UNCLASSIFIED_PROTOCOL_SOURCE", + "implementation_stage": null, + "projection_state": "INVENTORIED_NOT_EXECUTABLE", + "projection_kind": null, + "adapter": null, + "event_kinds": [], + "dependencies": [], + "dependency_edges": [], + "activation_blockers": [ + "NO_EXECUTABLE_ADAPTER" + ] + }, { "id": "GLS-0819", "title": "GRSP · 光湖运行轨道调度协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "f4aca196963195cbb842325132a9091f7b00accdf746aa60c51654698ead1a3e", "alternate_source_count": 0, "registration": { @@ -4244,7 +5109,9 @@ "declared_by": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -4253,7 +5120,9 @@ "declared_by": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0803", @@ -4262,7 +5131,9 @@ "declared_by": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0810", @@ -4271,7 +5142,9 @@ "declared_by": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -4280,7 +5153,9 @@ "declared_by": "gls/protocols/GLS-0819-GUANGHU-RUNWAY-SCHEDULING-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0803", @@ -4288,7 +5163,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -4298,6 +5175,7 @@ "title": "PTCP · 人格体时间连续性协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "15c278c2b5295e00c9abf4fbfd6c2af19502a1be239e7e02b23a49b8803eab25", "alternate_source_count": 0, "registration": { @@ -4346,7 +5224,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0224", @@ -4355,7 +5235,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0304", @@ -4364,7 +5246,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0307", @@ -4373,7 +5257,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0409", @@ -4382,7 +5268,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0803", @@ -4391,7 +5279,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0816", @@ -4400,7 +5290,9 @@ "declared_by": "gls/protocols/GLS-0827-PERSONA-TIME-CONTINUITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0304", @@ -4408,7 +5300,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0307", @@ -4416,7 +5310,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0308", @@ -4424,7 +5320,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -4434,6 +5332,7 @@ "title": "PEN · 神笔马良人格体能力扩展协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ad38ddce8b6e1118e78e4f10fd8216f8e041610628ad717b7a8306d0a7384a4e", "alternate_source_count": 0, "registration": { @@ -4482,7 +5381,9 @@ "declared_by": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0311", @@ -4491,7 +5392,9 @@ "declared_by": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -4500,7 +5403,9 @@ "declared_by": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0710", @@ -4509,7 +5414,9 @@ "declared_by": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0814", @@ -4518,7 +5425,9 @@ "declared_by": "gls/protocols/GLS-0828-PERSONA-EXTENSION-NODE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0311", @@ -4526,7 +5435,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0709", @@ -4534,7 +5445,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0710", @@ -4542,7 +5455,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -4552,6 +5467,7 @@ "title": "GWRP · 光湖世界启动与恢复协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "c5a930e9a2311d13f1dc6cbc61bdbf7b88bf9deecfb9b3df82c6b897aa1c7285", "alternate_source_count": 0, "registration": { @@ -4594,7 +5510,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0310", @@ -4603,7 +5521,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -4612,7 +5532,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0816", @@ -4621,7 +5543,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0830", @@ -4630,7 +5554,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0840", @@ -4639,7 +5565,9 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -4648,12 +5576,13 @@ "declared_by": "gls/protocols/GLS-0836-GUANGHU-WORLD-BOOTSTRAP-AND-RECOVERY.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -4661,6 +5590,7 @@ "title": "GOSK · 光湖 OS 内核规范", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "b980dbd8aef6182a61755617a9088ca3dcf74d37fd00dba9d31587900cd06b14", "alternate_source_count": 0, "registration": { @@ -4703,7 +5633,9 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0310", @@ -4712,7 +5644,9 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -4721,7 +5655,9 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0810", @@ -4730,7 +5666,9 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0819", @@ -4739,7 +5677,9 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -4748,12 +5688,13 @@ "declared_by": "gls/protocols/GLS-0840-GUANGHU-OS-KERNEL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -4761,6 +5702,7 @@ "title": "GHAL · 光湖硬件抽象层规范", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0841-GUANGHU-HARDWARE-ABSTRACTION-LAYER.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "45f02a62946e34a5e8ce0ba1f39db79b2680b60e1c3ae9d17ff24d3539025a94", "alternate_source_count": 0, "registration": { @@ -4803,7 +5745,9 @@ "declared_by": "gls/protocols/GLS-0841-GUANGHU-HARDWARE-ABSTRACTION-LAYER.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0840", @@ -4812,12 +5756,13 @@ "declared_by": "gls/protocols/GLS-0841-GUANGHU-HARDWARE-ABSTRACTION-LAYER.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -4825,6 +5770,7 @@ "title": "HLSP · HoloLake 实时会话协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "6b1177196c3e4d7e271d008a52a7106dabca20a8f724c6c4283af64ea96792ff", "alternate_source_count": 0, "registration": { @@ -4875,7 +5821,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0301", @@ -4884,7 +5832,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -4893,7 +5843,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -4902,7 +5854,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -4911,7 +5865,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0310", @@ -4920,7 +5876,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0602", @@ -4929,7 +5887,9 @@ "declared_by": "gls/protocols/GLS-0842-HOLOLAKE-LIVE-SESSION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": false + "target_numbered_source_available": false, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_REFERENCE_NODE" }, { "target": "GLS-0301", @@ -4937,7 +5897,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0302", @@ -4945,7 +5907,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0303", @@ -4953,7 +5917,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -4961,7 +5927,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0307", @@ -4969,7 +5937,9 @@ "declared_by": "contracts/gls-executable-projections.json", "enters_runtime_graph": true, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [] @@ -4979,6 +5949,7 @@ "title": "GHNRP · 光湖原生恢复协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "9333f3d8f4a954a17e1d879eaf19c25da5dd4bb2923a5ab5156d2d71a6635d37", "alternate_source_count": 0, "registration": { @@ -5021,7 +5992,9 @@ "declared_by": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0836", @@ -5030,7 +6003,9 @@ "declared_by": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5039,7 +6014,9 @@ "declared_by": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5048,7 +6025,9 @@ "declared_by": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0846", @@ -5057,7 +6036,9 @@ "declared_by": "gls/protocols/GLS-0843-GUANGHU-NATIVE-RECOVERY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ @@ -5069,6 +6050,7 @@ "title": "GHNQG · 光湖原生代码质量门", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "d31f5aa117d86754182aa8dd91eb9d0ac6a696c20e1d6bd553497e3c3d4c4eb2", "alternate_source_count": 0, "registration": { @@ -5111,7 +6093,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -5120,7 +6104,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0311", @@ -5129,7 +6115,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0411", @@ -5138,7 +6126,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5147,7 +6137,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5156,7 +6148,9 @@ "declared_by": "gls/protocols/GLS-0844-GUANGHU-NATIVE-QUALITY-GATE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ @@ -5168,6 +6162,7 @@ "title": "GHCIP · 光湖孕育史连续性摄入协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "7327a7fe19c370a5715bcde0fbc0b2b0e16a1a735bbda005c5d009cc7385f7f4", "alternate_source_count": 0, "registration": { @@ -5210,7 +6205,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0224", @@ -5219,7 +6216,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -5228,7 +6227,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -5237,7 +6238,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5246,7 +6249,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5255,7 +6260,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0846", @@ -5264,7 +6271,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0847", @@ -5273,7 +6282,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0848", @@ -5282,7 +6293,9 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0849", @@ -5291,12 +6304,13 @@ "declared_by": "gls/protocols/GLS-0845-GUANGHU-GESTATIONAL-CONTINUITY-INGESTION-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -5304,6 +6318,7 @@ "title": "GHNLP · 光湖原生磁盘布局协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "aa8f7c1ef829a76ad64163c38b8c7c07b37ffbcb6d62b8dad8481c4977d2cf1a", "alternate_source_count": 0, "registration": { @@ -5346,7 +6361,9 @@ "declared_by": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5355,7 +6372,9 @@ "declared_by": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5364,7 +6383,9 @@ "declared_by": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0843", @@ -5373,7 +6394,9 @@ "declared_by": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0845", @@ -5382,7 +6405,9 @@ "declared_by": "gls/protocols/GLS-0846-GUANGHU-NATIVE-DISK-LAYOUT-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ @@ -5394,6 +6419,7 @@ "title": "GHCS · 光湖孕育史原生内容仓", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "a48288b05133b9e245ce46ef6fd23f07090379cef4861e5d7f959d24e55179a6", "alternate_source_count": 0, "registration": { @@ -5436,7 +6462,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -5445,7 +6473,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5454,7 +6484,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5463,7 +6495,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0845", @@ -5472,7 +6506,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0846", @@ -5481,7 +6517,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0848", @@ -5490,7 +6528,9 @@ "declared_by": "gls/protocols/GLS-0847-GUANGHU-GESTATIONAL-CONTENT-STORE.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ @@ -5502,6 +6542,7 @@ "title": "GHSP · 光湖历史入口安全协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "35c733dc3478f0d315ea93712b0f67a757bc20973d90bb771f667196edee1b31", "alternate_source_count": 0, "registration": { @@ -5544,7 +6585,9 @@ "declared_by": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0230", @@ -5553,7 +6596,9 @@ "declared_by": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -5562,7 +6607,9 @@ "declared_by": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0845", @@ -5571,7 +6618,9 @@ "declared_by": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0847", @@ -5580,7 +6629,9 @@ "declared_by": "gls/protocols/GLS-0848-GUANGHU-HISTORICAL-INGRESS-SAFETY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ @@ -5592,6 +6643,7 @@ "title": "GHRP · 光湖孕育史原生语义回看协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "fb31daa4d8f484d5b93783c8c3a0199eb6fe3f7658c606f89d056b1167af8b41", "alternate_source_count": 0, "registration": { @@ -5634,7 +6686,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0223", @@ -5643,7 +6697,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": false, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0306", @@ -5652,7 +6708,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0310", @@ -5661,7 +6719,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -5670,7 +6730,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0708", @@ -5679,7 +6741,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0840", @@ -5688,7 +6752,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0841", @@ -5697,7 +6763,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0845", @@ -5706,7 +6774,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0847", @@ -5715,7 +6785,9 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0848", @@ -5724,12 +6796,13 @@ "declared_by": "gls/protocols/GLS-0849-GUANGHU-GESTATIONAL-SEMANTIC-REVIEW-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] }, { @@ -5737,6 +6810,7 @@ "title": "GLWBP · 光湖语言世界楚河汉界与创造者尊严协议", "source_status": "REGISTERED_DRAFT_STANDARD", "source_path": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", + "source_format": "HDLP_PROTOCOL_SOURCE", "source_sha256": "ac7092282cd6fbef0c5055e8be3b142d9757958acae16ae367c38463d899bd12", "alternate_source_count": 0, "registration": { @@ -5751,7 +6825,7 @@ "declared_source_paths": [ "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp" ], - "routing_reference_count": 0 + "routing_reference_count": 1 }, "maturity": { "registry_section": "registered_draft_protocols", @@ -5774,7 +6848,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0002", @@ -5783,7 +6859,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0110", @@ -5792,7 +6870,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0400", @@ -5801,7 +6881,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": false + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0800", @@ -5810,7 +6892,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0803", @@ -5819,7 +6903,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0827", @@ -5828,7 +6914,9 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" }, { "target": "GLS-0836", @@ -5837,12 +6925,13 @@ "declared_by": "gls/protocols/GLS-0850-GUANGHU-LANGUAGE-WORLD-BOUNDARY-AND-CREATOR-DIGNITY-PROTOCOL.hdlp", "enters_runtime_graph": false, "target_registered": true, - "target_numbered_source_available": true + "target_numbered_source_available": true, + "target_number_coordinate_available": true, + "target_resolution": "NUMBERED_PROTOCOL_SOURCE" } ], "activation_blockers": [ - "NO_EXECUTABLE_ADAPTER", - "DEPENDENCY_NUMBERED_SOURCE_MISSING" + "NO_EXECUTABLE_ADAPTER" ] } ] diff --git a/product-source/hololake-native-desktop/contracts/ios-mobile-client-v1.json b/product-source/hololake-native-desktop/contracts/ios-mobile-client-v1.json new file mode 100644 index 000000000..976e1bf43 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/ios-mobile-client-v1.json @@ -0,0 +1,49 @@ +{ + "schema": "hololake.ios-mobile-client.contract/v1", + "record_id": "HLP-IOS-CLIENT-001", + "state": "NUMBERED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED", + "role": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM", + "root_node": "USER_LOCAL_COMPUTER_TERMINAL", + "numbered_transport": { + "desktop_module": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001", + "desktop_route_family": "HLP-NIPC-MOD-0030/HLP-NIPC-OP-0141..0146/HLP-NIPC-TGT-0030", + "wire_schema": "hololake.mobile-sync/v1", + "client_number": "HLP-IOS-CLIENT-001", + "unregistered_route_access": "FAIL_CLOSED" + }, + "security": { + "pairing": "CHACHA20_POLY1305_ONE_TIME_SECRET", + "session": "CHACHA20_POLY1305_STRICT_COUNTER", + "key_storage": "KEYCHAIN_WHEN_UNLOCKED_THIS_DEVICE_ONLY", + "local_network_only": true, + "background_polling": false, + "remote_desktop_clone": false, + "desktop_offline_execution": false + }, + "interface": { + "framework": "SWIFTUI", + "visual_family": "HOLOLAKE_TRADITIONAL_SURFACE_MOBILE_ADAPTATION", + "idle_state": "STATIC", + "manual_sync_only": true, + "pairing_entry": ["CUSTOM_URL_SCHEME", "PASTEBOARD"], + "projections": ["CHANNEL_INTEGRITY", "WEB_NOVEL_COUNTS_AND_WORKS", "EDUCATION_COUNTS", "BOUNDED_CAPTURE"] + }, + "acceptance": { + "simulator_build": "PASS_SIGNED_LOCAL_SIMULATOR_IPHONE_17_PRO_IOS_26_5", + "simulator_visual_inspection": "PASS_PAIRING_AND_NUMBER_BOUNDARY_SURFACE_IDLE_STATIC", + "unit_tests": "PASS_4", + "development_signed_device_archive": "PASS_APPLE_DEVELOPMENT_TEAM_825A9L3G7Q", + "debugging_ipa_export": "PASS", + "version": "0.5.0", + "build": "1", + "bundle_identifier": "com.guanghulab.hololake", + "xcode": "26.6_17F113", + "archive_application_binary_sha256": "7021c9cde2f7bdf01cda6200669be528a58d27ef41adbd3d58d7135ff7245702", + "archive_application_cdhash": "11ae3a8ac0d761e4d4b4de229dbb19291de29e03", + "debugging_ipa_sha256": "a6b505e1a7dc10febd831a540396bb9530f78bb2429d258962f0250e5597b1f0", + "provisioning_profile_uuid": "fe5fe72c-4826-4e19-9672-d9868cc4491e", + "provisioning_profile_expires": "2027-07-19", + "desktop_delivery_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake-iPhone-0.5.0-Development.ipa", + "real_iphone_pair_and_sync": "NOT_CLAIMED" + } +} diff --git a/product-source/hololake-native-desktop/contracts/local-development-bridge.json b/product-source/hololake-native-desktop/contracts/local-development-bridge.json index cbc2229ca..c0390f573 100644 --- a/product-source/hololake-native-desktop/contracts/local-development-bridge.json +++ b/product-source/hololake-native-desktop/contracts/local-development-bridge.json @@ -115,6 +115,9 @@ "cross_platform_local_transport_runtime": true, "authenticated_heartbeat_runtime": true, "hololake_work_environment_frame_runtime": true, + "persona_to_host_runtime_license_verifier": true, + "persona_runtime_trusted_signer_provisioned": false, + "persona_mode_expiry_and_scope_fail_closed": true, "model_protocol_context_restore_required": false, "external_local_broker_runtime": true, "authenticated_broker_development_lane_runtime": true, diff --git a/product-source/hololake-native-desktop/contracts/mobile-sync-v1.json b/product-source/hololake-native-desktop/contracts/mobile-sync-v1.json new file mode 100644 index 000000000..0336ffff1 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/mobile-sync-v1.json @@ -0,0 +1,93 @@ +{ + "schema": "hololake.mobile-sync.contract/v1", + "record_id": "HLP-MOBILE-SYNC-001", + "state": "HOLOLAKE_0_5_NUMBERED_DESKTOP_BRIDGE_ACCEPTED", + "package": { + "official_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001", + "adapter": "mobile-sync-v1", + "registration_class": "OFFICIAL_LIGHTHOUSE", + "activation": "SIGNED_PACKAGE_PLUS_EXPLICIT_HUMAN_PERMISSION_CONFIRMATION", + "listener_after_activation": "EXPLICIT_HUMAN_ACTION_ONLY" + }, + "numbered_ipc": { + "module": "HLP-NIPC-MOD-0030", + "target": "HLP-NIPC-TGT-0030", + "operations": "HLP-NIPC-OP-0141..HLP-NIPC-OP-0146", + "public_tauri_commands": ["numbered_ipc"], + "mismatched_coordinate": "FAIL_CLOSED" + }, + "desktop_role": "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE", + "mobile_role": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM", + "transport": { + "implemented": "SAME_LAN_DIRECT_HTTP_WITH_APPLICATION_LAYER_ENCRYPTION", + "port_preference": 37421, + "remote_internet_direct": "NOT_IMPLEMENTED", + "encrypted_relay": "NOT_IMPLEMENTED", + "maximum_request_bytes": 262144, + "maximum_concurrent_connections": 16, + "duplicate_http_header": "REJECT" + }, + "pairing": { + "uri_scheme": "hololake://pair", + "secret_bits": 256, + "ttl_seconds": 600, + "single_use": true, + "payload_aead": "CHACHA20_POLY1305", + "aad": "hololake.mobile.pair/v1" + }, + "session": { + "key_bits": 256, + "aead": "CHACHA20_POLY1305", + "replay_guard": "STRICTLY_INCREASING_PER_DEVICE_COUNTER", + "device_revocation": true, + "desktop_key_storage": "ACCOUNT_SCOPED_SQLITE", + "ios_key_storage": "KEYCHAIN_THIS_DEVICE_ONLY" + }, + "routes": { + "GET /v1/status": "NO_PRIVATE_PAYLOAD", + "POST /v1/pair": "ONE_TIME_PAIRING_SECRET_REQUIRED", + "POST /v1/sync": "PAIRED_DEVICE_AEAD_AND_COUNTER_REQUIRED" + }, + "projection": { + "personal_channel": "MINIMUM_COUNTS_AND_INTEGRITY", + "web_novel": "WORK_LIST_AND_COUNTS", + "education": "COUNTS_ONLY_NO_SENSITIVE_CELL_VALUES", + "mobile_capture": "BOUNDED_INBOX_WRITE_ONLY_NO_AUTOMATIC_DOMAIN_MUTATION" + }, + "hard_boundaries": { + "remote_desktop_clone": false, + "second_persona_system": false, + "platform_private_payload_custody": false, + "desktop_offline_execution": false, + "mobile_capture_mutates_persona_or_industry_data": false, + "module_unmount_stops_listener_before_lifecycle_transition": true, + "background_frontend_polling": false + }, + "client_scope": { + "desktop_bridge": "IN_THIS_ADMISSION", + "ios_application_source": "SEPARATELY_ADMITTED_AT_MOBILE_IOS_WITH_HLP_IOS_CLIENT_001", + "ios_installable_package": "DEVELOPMENT_SIGNED_DEBUGGING_IPA_EXPORTED", + "claim_real_iphone_end_to_end_accepted": false + }, + "current_acceptance": { + "state": "PASS_DESKTOP_BRIDGE_AND_SEPARATELY_ADMITTED_IOS_CLIENT_ARTIFACT", + "module_package_sha256": "61a9d402c936d7477609bcfa6eb4ad6e83cb6c089ba51b3db09a2d6259185924", + "module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST", + "signed_app_binary_sha256": "9be06d6c5dbd4f46f6f925142d633f12b42ee553873c2342f9804bcf39dde7b4", + "signed_app_cdhash": "c595cb2a7729d49638faa78f76e145ee9b1da05a", + "apple_team_identifier": "825A9L3G7Q", + "module_receipts": { + "install": "a09496ec6113254f77ea2425d0bd30571e9e0e3df62bfa18e500f860c2f65d4d", + "mount": "5b162ee807c8b4d9c8aa8d9cb590c64587f51067dc0804030bf57ed0753ac25a", + "self_test_pass": "e66ee868e28a25fcd70cc888845661b5044d18a6e18b70cee6e4839b02fe1c53" + }, + "desktop_listener": "PASS_EXPLICIT_START_REACHABLE_STATUS_AND_EXPLICIT_STOP", + "restart_restore": "PASS_ACTIVE_MODULE_RESTORED_LISTENER_OFFLINE", + "legacy_account_readback": "PASS_ONE_PAIRED_DEVICE_ONE_ISOLATED_CAPTURE_RETAINED", + "ios_client_contract": "contracts/ios-mobile-client-v1.json", + "ios_client_source_build_and_simulator": "PASS", + "ios_development_signed_ipa": "PASS_SHA256_a6b505e1a7dc10febd831a540396bb9530f78bb2429d258962f0250e5597b1f0", + "live_iphone_pairing_this_cycle": "NOT_CLAIMED_NO_PHYSICAL_DEVICE_USED", + "notarization": "FINAL_RELEASE_CANDIDATE_PENDING" + } +} diff --git a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json new file mode 100644 index 000000000..0db17c74e --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -0,0 +1,240 @@ +{ + "schema": "hololake.module-donor-admission-registry/v1", + "record_id": "HLP-MODULE-DONOR-ADMISSION-001", + "state": "EIGHT_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY", + "root_rule": { + "official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT", + "repair_old_application_in_place": false, + "bulk_merge_or_wholesale_copy_allowed": false, + "one_candidate_per_admission_cycle": true, + "candidate_number_is_runtime_module_number": false, + "permanent_module_number_assignment_before_acceptance": false, + "all_frontend_backend_calls_must_cross_numbered_ipc": true, + "shared_file_merge_is_admission_evidence": false + }, + "donors": [ + { + "donor_id": "HLP-DONOR-COMPILED-DESKTOP-0.4.1", + "kind": "COMPILED_MACOS_APPLICATION", + "path": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/HoloLake.app", + "state": "READ_ONLY", + "source_commit": null, + "source_commit_state": "UNKNOWN_NOT_INFERRED_FROM_COMPILED_BUNDLE", + "binary_sha256": "adc0d8b028a8b874c39909265ed7c41e7c24e4fe5b38adba04e8f72b64900c15", + "use": "BEHAVIOR_AND_VISIBLE_PRODUCT_REFERENCE_ONLY" + }, + { + "donor_id": "HLP-DONOR-CHAOTIC-WORKTREE-20260818", + "kind": "DIRTY_SOURCE_WORKTREE", + "path": "/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-15/new-chat-2/work/jd-guanghu-supervisor/product-source/hololake-native-desktop", + "observed_head": "a8fe571b5d5c0a45207b64ac538d729b3d719d21", + "observed_dirty_path_count": 30, + "state": "READ_ONLY_UNTRUSTED_AS_A_WHOLE", + "use": "INDIVIDUAL_MODULE_SOURCE_CANDIDATES_ONLY" + } + ], + "rejected_inputs": [ + { + "candidate_number": "HLP-DONOR-CAND-0000", + "name": "legacy_numbered_operation_runtime", + "state": "REJECTED_SUPERSEDED", + "why": "It predates the single numbered IPC root and must not become a second numbering authority.", + "paths": [ + "contracts/numbered-operation-runtime.json", + "src/modules/numbered-runtime.ts", + "src-tauri/src/numbered_operation_runtime.rs" + ] + }, + { + "candidate_number": "HLP-DONOR-CAND-SHARED-MUTATIONS", + "name": "shared_file_mutation_set", + "state": "REJECTED_AS_MERGE_UNIT", + "why": "These files mix unrelated modules and divergent frontend/backend routes; each needed behavior must be reconstructed behind its owning numbered module.", + "examples": [ + "src/main.tsx", + "src/styles.css", + "src-tauri/src/lib.rs", + "src-tauri/src/knowledge_base.rs", + "src-tauri/src/gls_protocol_runtime.rs" + ] + } + ], + "candidates": [ + { + "admission_order": 1, + "candidate_number": "HLP-DONOR-CAND-0001", + "name": "native_composition_module_runtime", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/native-composition-runtime.json", + "src/modules/native-composition", + "src-tauri/src/native_composition.rs", + "src/styles.css#native-composition-selectors-only" + ], + "style_dependency_discovered_during_admission": "Only selectors prefixed native-composition or composition- were isolated into the module directory; unrelated global donor CSS remains prohibited.", + "runtime_module_number": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0021", + "acceptance_evidence": "contracts/native-composition-runtime.json#current_acceptance", + "why_first": "A module needs an isolated mount, self-test, unmount and rollback boundary before content modules can be admitted safely." + }, + { + "admission_order": 2, + "candidate_number": "HLP-DONOR-CAND-0002", + "name": "channel_document_and_spreadsheet_workbench", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "src/modules/channel-workbench", + "src-tauri/src/channel_workbench.rs", + "src/styles.css#education-engine-and-channel-spreadsheet-selectors-only" + ], + "style_dependency_discovered_during_admission": "Only the document-engine and channel-spreadsheet selector behavior was isolated into the module directory; unrelated education and global donor CSS remains prohibited.", + "runtime_module_number": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0022", + "acceptance_evidence": "contracts/channel-workbench-runtime.json#current_acceptance", + "boundary": "ACCOUNT_LOCAL_DOCUMENT_AND_SPREADSHEET_DATA; DOES_NOT_OWN_PERSONA_CHANNEL_BODY" + }, + { + "admission_order": 3, + "candidate_number": "HLP-DONOR-CAND-0003", + "name": "persona_channel_body", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/persona-channel-body.json", + "src/modules/persona-channel-body", + "src-tauri/src/persona_channel_body.rs", + "src-tauri/src/channel_growth.rs" + ], + "style_dependency_discovered_during_admission": "Only persona-body selectors were isolated into the module directory; no donor global stylesheet or unrelated feature selectors were admitted.", + "runtime_module_number": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0023", + "acceptance_evidence": "contracts/persona-channel-body.json#current_acceptance", + "boundary": "UI_AND_PERSISTENCE_BODY_ONLY; DOES_NOT_CREATE_PERSONA_BINDING; DOES_NOT_ALLOW_HOST_TO_ISSUE_PERSONA_LICENSE" + }, + { + "admission_order": 4, + "candidate_number": "HLP-DONOR-CAND-0004", + "name": "education_workbench", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/education-workspace.json", + "src/modules/education-workspace", + "src-tauri/src/education_translation.rs", + "src-tauri/src/education_workspace.rs" + ], + "runtime_module_number": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0024", + "acceptance_evidence": "contracts/education-workspace.json#current_acceptance", + "boundary": "CURRENT_ACCOUNT_LOCAL; IMPORTS_DEFAULT_UNASSIGNED; CLEANUP_AND_AUTOMATION_REQUIRE_EXPLICIT_HUMAN_CONFIRMATION; MODEL_FILE_TRANSFER_DEFAULT_DENY" + }, + { + "admission_order": 5, + "candidate_number": "HLP-DONOR-CAND-0005", + "name": "web_novel_workbench_and_author_modules", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/web-novel-workspace.json", + "contracts/web-novel-module-marketplace-plan.json", + "src/modules/web-novel/WebNovelWorkspace.tsx", + "src/modules/web-novel/AuthorModuleCenter.tsx", + "src/modules/web-novel/AuthorWritingSidecar.tsx", + "src-tauri/src/web_novel_workspace.rs", + "src-tauri/src/web_novel_import.rs", + "src-tauri/src/web_novel_author.rs", + "src-tauri/src/web_novel_modules.rs" + ], + "runtime_module_numbers": ["HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"], + "numbered_ipc_modules": ["HLP-NIPC-MOD-0025", "HLP-NIPC-MOD-0026", "HLP-NIPC-MOD-0027", "HLP-NIPC-MOD-0028", "HLP-NIPC-MOD-0029"], + "acceptance_evidence": "contracts/web-novel-workspace.json#current_acceptance", + "boundary": "CURRENT_ACCOUNT_LOCAL; ONE_STORY_GRAPH; ADVANCED_EFFECTS_REQUIRE_EXACT_ACTIVE_MODULE_NUMBER; THIRD_PARTY_AUTO_LOGIN_AND_PUBLISH_DENIED" + }, + { + "admission_order": 6, + "candidate_number": "HLP-DONOR-CAND-0006", + "name": "mobile_sync", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/mobile-sync-v1.json", + "src/MobileSyncPanel.tsx", + "src-tauri/src/mobile_sync.rs" + ], + "runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0030", + "acceptance_evidence": "contracts/mobile-sync-v1.json#current_acceptance", + "boundary": "DESKTOP_ROOT_NODE_SAME_LAN_BRIDGE_ONLY; EXPLICIT_LISTENER; IOS CLIENT REQUIRES SEPARATE NUMBERED ADMISSION; NO_SECOND_PERSONA_SYSTEM" + }, + { + "admission_order": 7, + "candidate_number": "HLP-DONOR-CAND-0007", + "name": "dynamic_language_world_visual_surface", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "paths": [ + "contracts/dynamic-language-world-visual-system.json", + "src/modules/qoder-surface/StarlakeSurface.tsx", + "src/modules/qoder-surface/starlake-surface.css", + "src/modules/qoder-surface/TraditionalSurface.tsx", + "src/modules/qoder-surface/traditional-surface.css", + "src/modules/qoder-surface/visual-balance.ts", + "src-tauri/src/world_climate.rs" + ], + "runtime_module_number": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0031", + "acceptance_evidence": "contracts/dynamic-language-world-visual-system.json#current_acceptance", + "boundary": "QODER_LOCKED_VISUAL_SOURCE_PLUS_REALITY_TIME_AND_PUBLIC_WEATHER_PROJECTION; ALL_FUNCTION_ROUTES_REMAIN_NUMBERED; IDLE_STATIC; NO_FAKE_WEATHER_BROADCAST_OR_METRICS" + }, + { + "admission_order": 8, + "candidate_number": "HLP-DONOR-CAND-0008", + "name": "ios_numbered_remote_body_entry", + "state": "ADMITTED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED", + "paths": [ + "contracts/ios-mobile-client-v1.json", + "mobile/ios/project.yml", + "mobile/ios/Sources", + "mobile/ios/Tests" + ], + "client_number": "HLP-IOS-CLIENT-001", + "desktop_runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0030", + "acceptance_evidence": "contracts/ios-mobile-client-v1.json#acceptance", + "boundary": "IPHONE IS A THIN REMOTE BODY ENTRY; DESKTOP REMAINS ROOT NODE; SAME LAN EXPLICIT PAIR AND MANUAL SYNC ONLY; NO SECOND PERSONA SYSTEM; REAL IPHONE END TO END NOT CLAIMED" + } + ], + "admission_gate": [ + "EXTRACT_ONLY_DECLARED_CANDIDATE_PATHS", + "REVIEW_SOURCE_PROVENANCE_AND_LICENSE", + "DEFINE_DATA_PERMISSION_AND_RESOURCE_BOUNDARY", + "ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES", + "IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE", + "ADD_UNIT_INTEGRATION_AND_NEGATIVE_ROUTE_TESTS", + "BUILD_FROM_CLEAN_OFFICIAL_BASE", + "INSTALL_IN_ISOLATED_RUNTIME", + "VERIFY_MOUNT_SELF_TEST_RESTART_UNMOUNT_AND_ROLLBACK", + "WRITE_HASH_CHAINED_RUNTIME_RECEIPT", + "ONLY_THEN_ASSIGN_PERMANENT_MODULE_NUMBER" + ], + "hot_install_boundary": { + "runtime_state": "SIGNED_DECLARATIVE_PACKAGE_ENGINE_IMPLEMENTED", + "runtime_contract": "contracts/module-package-runtime.json", + "numbered_module_runtime_operations": [ + "HLP-NIPC-OP-0063", + "HLP-NIPC-OP-0064", + "HLP-NIPC-OP-0065", + "HLP-NIPC-OP-0066", + "HLP-NIPC-OP-0067", + "HLP-NIPC-OP-0068", + "HLP-NIPC-OP-0069", + "HLP-NIPC-OP-0070", + "HLP-NIPC-OP-0071" + ], + "source_repository_is_directly_executable": false, + "immutable_signed_artifact_required": true, + "compatibility_manifest_required": true, + "permissions_declared_before_mount": true, + "human_confirmation_required_when_boundary_expands": true, + "rollback_on_self_test_failure": true, + "user_data_survives_unmount": true, + "arbitrary_native_code_allowed": false, + "arbitrary_webview_javascript_allowed": false, + "real_signed_acceptance_fixture": "fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod" + } +} diff --git a/product-source/hololake-native-desktop/contracts/module-package-runtime.json b/product-source/hololake-native-desktop/contracts/module-package-runtime.json new file mode 100644 index 000000000..a00528451 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/module-package-runtime.json @@ -0,0 +1,52 @@ +{ + "schema": "hololake.module-package-runtime/v1", + "record_id": "HLP-MODULE-PACKAGE-RUNTIME-001", + "protocols": ["GLS-0710", "GLS-0803", "GLS-0819", "GLS-0310"], + "package_schema": "hololake.module-package/v1", + "signature": { + "algorithm": "MINISIGN_ED25519", + "trust_source": "src-tauri/release-trust.json", + "signature_is_detached": true, + "package_bytes_are_signed_exactly": true, + "source_repository_is_executable": false + }, + "number_classes": { + "official": "HLP-MOD-OFFICIAL-*", + "private_channel": "HLP-MOD-LOCAL-*", + "candidate_number_is_runtime_number": false + }, + "host": { + "version": "0.5.0", + "maximum_package_bytes": 16777216, + "arbitrary_native_code": false, + "arbitrary_webview_javascript": false, + "declarative_payload_only": true + }, + "registered_adapters": [ + "native-composition-v1", + "channel-workbench-v1", + "persona-channel-body-v1", + "education-workbench-v1", + "web-novel-workbench-v1", + "mobile-sync-v1", + "dynamic-language-world-surface-v1" + ], + "lifecycle": [ + "INSTALLED_DORMANT", + "MOUNTED_PENDING_SELF_TEST", + "ACTIVE", + "DORMANT", + "ROLLBACK_PENDING_SELF_TEST", + "FAILED_CLOSED" + ], + "rules": { + "verified_human_route_required": true, + "permission_expansion_requires_human_confirmation": true, + "mount_never_executes_package_code": true, + "self_test_before_active": true, + "failed_self_test_rolls_back": true, + "restart_reconstructs_from_sqlite": true, + "unmount_preserves_user_data": true, + "every_mutation_writes_hash_chained_receipt": true + } +} diff --git a/product-source/hololake-native-desktop/contracts/native-composition-runtime.json b/product-source/hololake-native-desktop/contracts/native-composition-runtime.json new file mode 100644 index 000000000..b1070a551 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/native-composition-runtime.json @@ -0,0 +1,66 @@ +{ + "schema": "hololake.native-composition-runtime/v1", + "record_id": "HLP-NATIVE-COMPOSITION-001", + "candidate_number": "HLP-DONOR-CAND-0001", + "runtime_module_number": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "provenance": { + "donor": "0.4.1 read-only candidate paths", + "donor_contract_state": "INSTALLED_RUNTIME_ACCEPTED", + "donor_apple_notarization": "NOT_PERFORMED_MISSING_CREDENTIALS", + "current_mainline_reuses_bulk_source": false + }, + "internal_object": { + "schema": "hololake.native-object/v1", + "encoding": "BOUNDED_JSON", + "script_allowed": false, + "html_allowed": false, + "external_file_format_as_runtime_allowed": false, + "maximum_rows": 5000, + "maximum_columns": 64, + "maximum_cell_bytes": 10000 + }, + "module_contract": { + "schema": "hololake.composition-module/v1", + "registered_kinds": ["SOURCE", "TRANSFORM", "PROJECTION"], + "typed_ports_required": true, + "determinism_declared": true, + "authority": "CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY" + }, + "recipe": { + "schema": "hololake.composition-recipe/v1", + "registered_modules_only": true, + "directed_acyclic_graph_required": true, + "current_account_only": true, + "native_validation_required": true + }, + "human_projection": { + "views": ["DASHBOARD", "COMPARISON", "VERTICAL_BAR", "CLASSIFICATION", "TABLE"], + "shares_one_execution_result": true, + "owns_source_data": false, + "direct_write_authority": false + }, + "admission": { + "signed_package_required": true, + "adapter": "native-composition-v1", + "numbered_routes_required": true, + "active_module_required_before_execution": true, + "installed_runtime_acceptance_required": true, + "hardcoded_sample_may_satisfy_acceptance": false + }, + "current_acceptance": { + "observed_at": "2026-08-19T01:49:35+08:00", + "signed_debug_binary_sha256": "0bff08787950b69ef0a1565fc32a7537dae53da184c4a1a4fb30ca62eca9f48c", + "developer_id_team": "825A9L3G7Q", + "module_package_sha256": "dfb343019810f8f845ab415259be2eccbe63d88a03bf26a277950610edb64329", + "account_number_observed": "ICE-GL∞", + "real_native_rows": 129, + "real_native_total_bytes_display": "1.0 MB", + "real_groups": 14, + "measure_switch_observed": "DOCUMENT_COUNT_TO_TOTAL_BYTES", + "install_receipts": ["INSTALL", "MOUNT", "SELF_TEST_PASS"], + "restart_recovery": "ACTIVE_AND_REEXECUTED", + "execution_receipt_prefixes": ["0449582510a5", "092febd75c38"], + "apple_notarization": "FINAL_RELEASE_PENDING_ALL_MODULES" + } +} diff --git a/product-source/hololake-native-desktop/contracts/nearby-ai-discovery.json b/product-source/hololake-native-desktop/contracts/nearby-ai-discovery.json index fba038cea..97bf075bf 100644 --- a/product-source/hololake-native-desktop/contracts/nearby-ai-discovery.json +++ b/product-source/hololake-native-desktop/contracts/nearby-ai-discovery.json @@ -11,7 +11,7 @@ "linux": "USER_PRIVATE_UNIX_SOCKET", "windows": "USER_PRIVATE_NAMED_PIPE" }, - "terminal_link_protocol": "HOLOLAKE_TERMINAL_LINK/2", + "terminal_link_protocol": "HOLOLAKE_TERMINAL_LINK/3", "copy_large_invitation_required": false, "network_required": false }, diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json new file mode 100644 index 000000000..90233331f --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -0,0 +1,2002 @@ +{ + "schema": "hololake.numbered-ipc-registry/v1", + "record_id": "HLP-NUMBERED-IPC-ROOT-001", + "source": { + "baseline_commit": "7a14c06e41587c4c5a08a43bb4f5a172fa61b7fa", + "gls_runtime_manifest": "HLP-GLS-RUNTIME-MANIFEST-002", + "gls_native_kernel": "HLP-GLS-NATIVE-RUNTIME-KERNEL-001", + "numbering_authority_map": "GH-IDENTITY-AUTHORITY-MAP-001", + "human_language_anchor": "source://codex-current-dialogue/2026-08-18/bingshuo-number-is-path-and-mismatched-routes-are-unrecognizable" + }, + "runtime": { + "public_tauri_command": "numbered_ipc", + "caller_number": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "caller_number_subject_kind": "PHYSICAL_WEBVIEW_ENTRY_NOT_PERSONA_IDENTITY", + "caller_number_grants_persona_binding": false, + "protocol_version": "HLP-NIPC-v1", + "legacy_direct_commands_allowed": false, + "grants_issued_server_side": true, + "grant_single_use": true, + "payload_bound_grants": true, + "authority_binding_issued_server_side": true, + "verified_human_binding_required_for_protected_routes": true, + "persona_binding_claimed": false, + "user_channel_body_binding_stage": "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED", + "maximum_grant_ttl_ms": 30000, + "unknown_route": "FAIL_CLOSED", + "mismatched_coordinate": "FAIL_CLOSED", + "expired_or_replayed_grant": "FAIL_CLOSED", + "receipt_required": true + }, + "payload_contract": { + "unknown_or_unclassified_alias": "FAIL_CLOSED_BEFORE_GRANT", + "unknown_top_level_field": "FAIL_CLOSED_BEFORE_GRANT", + "empty_object_aliases": [ + "get_hololake_home_status", + "check_hololake_update", + "get_hololake_release_recovery_status", + "confirm_hololake_release_health", + "rollback_hololake_update", + "get_nearby_ai_discovery", + "get_gls_protocol_runtime", + "get_gls_protocol_kernel", + "get_personal_channel_snapshot", + "start_persona_time_authority", + "get_beijing_time_coordinate", + "get_guanghu_era_timeline", + "get_knowledge_snapshot", + "select_and_import_knowledge_folder", + "print_knowledge_document", + "get_code_channel_snapshot", + "select_local_code_channel", + "select_pncc_repository_candidate", + "query_jd_pncc_server_projection", + "check_code_repo_login", + "get_enterprise_entry", + "ensure_enterprise_work_channel", + "sign_out_code_repo_login", + "get_user_pncc_channel", + "ensure_user_pncc_channel", + "get_zero_core_numbering_kernel", + "zero_point_verify", + "zero_point_sync", + "zero_point_status", + "get_module_runtime_snapshot", + "get_bundled_module_catalog", + "get_native_composition_module_registry", + "get_channel_workbench_snapshot", + "get_persona_channel_body", + "get_channel_growth_snapshot", + "get_education_workspace_snapshot", + "import_education_tables_from_dialog", + "get_education_recognition_capability", + "get_web_novel_workspace_snapshot", + "inspect_web_novel_document_from_dialog", + "start_mobile_sync", + "get_mobile_sync_status", + "rotate_mobile_pairing", + "stop_mobile_sync", + "get_mobile_sync_snapshot", + "get_world_climate" + ], + "input_wrapper_aliases": [ + "confirm_hololake_update_install", + "issue_direct_local_discovery_ticket", + "open_direct_local_session", + "resume_direct_local_session", + "append_direct_local_session_event", + "heartbeat_direct_local_session", + "decide_gls_protocol", + "compile_gls_hldp_program", + "acquire_development_write_lane", + "inspect_development_write_lane", + "release_development_write_lane", + "initialize_personal_channel", + "create_personal_channel_task", + "transition_personal_channel_task", + "issue_persona_time_ticket", + "read_knowledge_document", + "search_knowledge", + "save_knowledge_document", + "export_knowledge_document", + "create_knowledge_document", + "delete_knowledge_document", + "delete_knowledge_folder", + "clone_code_channel", + "browse_code_channel", + "read_code_channel_file", + "inspect_mounted_pncc_repository", + "confirm_pncc_repository_mount", + "query_pncc_receipt_projection", + "zero_point_bind", + "verify_module_package", + "install_module_package", + "mount_module", + "self_test_module", + "unmount_module", + "rollback_module", + "activate_bundled_module", + "execute_knowledge_native_composition", + "save_channel_document", + "save_channel_spreadsheet", + "register_trial_persona", + "delete_trial_persona", + "accept_persona_language_contract", + "append_persona_language", + "record_channel_growth_event", + "update_channel_growth_sharing", + "create_education_document", + "read_education_document", + "save_education_document", + "archive_education_document", + "create_education_table", + "read_education_table", + "save_education_table", + "archive_education_table", + "assign_imported_table_to_education", + "export_education_table_to_dialog", + "create_education_automation_rule", + "save_education_automation_rule", + "archive_education_automation_rule", + "preview_education_automation_rule", + "execute_education_automation_rule", + "create_web_novel_work", + "read_web_novel_work", + "save_web_novel_work", + "create_web_novel_volume", + "create_web_novel_chapter", + "read_web_novel_chapter", + "save_web_novel_chapter", + "transition_web_novel_chapter", + "create_web_novel_checkpoint", + "restore_web_novel_checkpoint", + "upsert_web_novel_story_entity", + "create_web_novel_story_relation", + "upsert_web_novel_foreshadow", + "create_web_novel_review_note", + "resolve_web_novel_review_note", + "save_web_novel_metric", + "run_web_novel_continuity_audit", + "export_web_novel_markdown", + "commit_web_novel_document_import", + "get_web_novel_author_snapshot", + "record_web_novel_writing_activity", + "create_web_novel_inspiration", + "set_web_novel_inspiration_status", + "search_web_novel_full_text", + "format_web_novel_chapter", + "format_web_novel_work", + "upsert_web_novel_shot", + "get_web_novel_author_module_data", + "upsert_web_novel_author_scene", + "upsert_web_novel_author_beat", + "upsert_web_novel_story_field_definition", + "upsert_web_novel_story_field_value", + "upsert_web_novel_timeline_event", + "link_web_novel_scene_entity", + "restore_web_novel_chapter_version", + "export_web_novel_author_delivery", + "revoke_mobile_sync_device" + ], + "direct_field_aliases": { + "perform_code_repo_login": [ + "username", + "password" + ], + "change_first_login_password": [ + "username", + "currentPassword", + "newPassword" + ], + "confirm_enterprise_persona_relationship": [ + "decision", + "idempotencyKey" + ], + "submit_enterprise_responsibility_receipt": [ + "decision", + "note", + "responsibilityVersion", + "idempotencyKey" + ] + } + }, + "channels": [ + { + "id": "HLP-NIPC-CH-0001" + }, + { + "id": "HLP-NIPC-CH-0003" + }, + { + "id": "HLP-NIPC-CH-0004" + }, + { + "id": "HLP-NIPC-CH-0002" + }, + { + "id": "HLP-NIPC-CH-0005" + }, + { + "id": "HLP-NIPC-CH-0006" + }, + { + "id": "HLP-NIPC-CH-0007" + } + ], + "modules": [ + { + "module_number": "HLP-NIPC-MOD-0001", + "target_number": "HLP-NIPC-TGT-0001", + "internal_name": "home_status" + }, + { + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "internal_name": "release_update" + }, + { + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "internal_name": "direct_local_session" + }, + { + "module_number": "HLP-NIPC-MOD-0004", + "target_number": "HLP-NIPC-TGT-0004", + "internal_name": "direct_local_broker" + }, + { + "module_number": "HLP-NIPC-MOD-0005", + "target_number": "HLP-NIPC-TGT-0005", + "internal_name": "gls_protocol_runtime" + }, + { + "module_number": "HLP-NIPC-MOD-0006", + "target_number": "HLP-NIPC-TGT-0006", + "internal_name": "gls_protocol_kernel" + }, + { + "module_number": "HLP-NIPC-MOD-0007", + "target_number": "HLP-NIPC-TGT-0007", + "internal_name": "local_development_bridge" + }, + { + "module_number": "HLP-NIPC-MOD-0008", + "target_number": "HLP-NIPC-TGT-0008", + "internal_name": "personal_channel" + }, + { + "module_number": "HLP-NIPC-MOD-0009", + "target_number": "HLP-NIPC-TGT-0009", + "internal_name": "persona_time_authority" + }, + { + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "internal_name": "knowledge_base" + }, + { + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "internal_name": "code_channel" + }, + { + "module_number": "HLP-NIPC-MOD-0012", + "target_number": "HLP-NIPC-TGT-0012", + "internal_name": "pncc_repository_binding" + }, + { + "module_number": "HLP-NIPC-MOD-0013", + "target_number": "HLP-NIPC-TGT-0013", + "internal_name": "pncc_receipt_projection" + }, + { + "module_number": "HLP-NIPC-MOD-0014", + "target_number": "HLP-NIPC-TGT-0014", + "internal_name": "pncc_server_projection" + }, + { + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "internal_name": "code_repo_login" + }, + { + "module_number": "HLP-NIPC-MOD-0016", + "target_number": "HLP-NIPC-TGT-0016", + "internal_name": "enterprise_work_channel" + }, + { + "module_number": "HLP-NIPC-MOD-0017", + "target_number": "HLP-NIPC-TGT-0017", + "internal_name": "user_pncc_channel" + }, + { + "module_number": "HLP-NIPC-MOD-0018", + "target_number": "HLP-NIPC-TGT-0018", + "internal_name": "zero_core_numbering" + }, + { + "module_number": "HLP-NIPC-MOD-0019", + "target_number": "HLP-NIPC-TGT-0019", + "internal_name": "zero_point" + }, + { + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "internal_name": "module_package_runtime" + }, + { + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "internal_name": "native_composition" + }, + { + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "internal_name": "channel_workbench" + }, + { + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "internal_name": "persona_channel_body" + }, + { + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "internal_name": "education_workspace" + }, + { + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "internal_name": "web_novel_workbench" + }, + { + "module_number": "HLP-NIPC-MOD-0026", + "target_number": "HLP-NIPC-TGT-0026", + "internal_name": "web_novel_outline" + }, + { + "module_number": "HLP-NIPC-MOD-0027", + "target_number": "HLP-NIPC-TGT-0027", + "internal_name": "web_novel_story_grid" + }, + { + "module_number": "HLP-NIPC-MOD-0028", + "target_number": "HLP-NIPC-TGT-0028", + "internal_name": "web_novel_storyworld" + }, + { + "module_number": "HLP-NIPC-MOD-0029", + "target_number": "HLP-NIPC-TGT-0029", + "internal_name": "web_novel_delivery" + }, + { + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "internal_name": "mobile_sync" + }, + { + "module_number": "HLP-NIPC-MOD-0031", + "target_number": "HLP-NIPC-TGT-0031", + "internal_name": "world_climate" + } + ], + "operations": [ + { + "operation_number": "HLP-NIPC-OP-0001", + "alias": "get_hololake_home_status", + "handler": "home_status::get_hololake_home_status", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0001", + "target_number": "HLP-NIPC-TGT-0001", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_hololake_home_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0002", + "alias": "check_hololake_update", + "handler": "release_update::check_hololake_update", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/check_hololake_update/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0003", + "alias": "confirm_hololake_update_install", + "handler": "release_update::confirm_hololake_update_install", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/confirm_hololake_update_install/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0004", + "alias": "get_hololake_release_recovery_status", + "handler": "release_update::get_hololake_release_recovery_status", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_hololake_release_recovery_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0005", + "alias": "confirm_hololake_release_health", + "handler": "release_update::confirm_hololake_release_health", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/confirm_hololake_release_health/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0006", + "alias": "rollback_hololake_update", + "handler": "release_update::rollback_hololake_update", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0002", + "target_number": "HLP-NIPC-TGT-0002", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/rollback_hololake_update/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0007", + "alias": "issue_direct_local_discovery_ticket", + "handler": "direct_local_session::issue_direct_local_discovery_ticket", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/issue_direct_local_discovery_ticket/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0008", + "alias": "open_direct_local_session", + "handler": "direct_local_session::open_direct_local_session", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/open_direct_local_session/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0009", + "alias": "resume_direct_local_session", + "handler": "direct_local_session::resume_direct_local_session", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/resume_direct_local_session/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0010", + "alias": "append_direct_local_session_event", + "handler": "direct_local_session::append_direct_local_session_event", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/append_direct_local_session_event/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0011", + "alias": "heartbeat_direct_local_session", + "handler": "direct_local_session::heartbeat_direct_local_session", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0003", + "target_number": "HLP-NIPC-TGT-0003", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/heartbeat_direct_local_session/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0012", + "alias": "get_nearby_ai_discovery", + "handler": "direct_local_broker::get_nearby_ai_discovery", + "channel_number": "HLP-NIPC-CH-0003", + "module_number": "HLP-NIPC-MOD-0004", + "target_number": "HLP-NIPC-TGT-0004", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_nearby_ai_discovery/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0013", + "alias": "get_gls_protocol_runtime", + "handler": "gls_protocol_runtime::get_gls_protocol_runtime", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0005", + "target_number": "HLP-NIPC-TGT-0005", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_gls_protocol_runtime/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0014", + "alias": "get_gls_protocol_kernel", + "handler": "gls_protocol_kernel::get_gls_protocol_kernel", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0006", + "target_number": "HLP-NIPC-TGT-0006", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_gls_protocol_kernel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0015", + "alias": "decide_gls_protocol", + "handler": "gls_protocol_kernel::decide_gls_protocol", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0006", + "target_number": "HLP-NIPC-TGT-0006", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/decide_gls_protocol/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0016", + "alias": "compile_gls_hldp_program", + "handler": "gls_protocol_kernel::compile_gls_hldp_program", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0006", + "target_number": "HLP-NIPC-TGT-0006", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/compile_gls_hldp_program/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0017", + "alias": "acquire_development_write_lane", + "handler": "local_development_bridge::acquire_development_write_lane", + "channel_number": "HLP-NIPC-CH-0004", + "module_number": "HLP-NIPC-MOD-0007", + "target_number": "HLP-NIPC-TGT-0007", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/acquire_development_write_lane/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0018", + "alias": "inspect_development_write_lane", + "handler": "local_development_bridge::inspect_development_write_lane", + "channel_number": "HLP-NIPC-CH-0004", + "module_number": "HLP-NIPC-MOD-0007", + "target_number": "HLP-NIPC-TGT-0007", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/inspect_development_write_lane/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0019", + "alias": "release_development_write_lane", + "handler": "local_development_bridge::release_development_write_lane", + "channel_number": "HLP-NIPC-CH-0004", + "module_number": "HLP-NIPC-MOD-0007", + "target_number": "HLP-NIPC-TGT-0007", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/release_development_write_lane/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0020", + "alias": "get_personal_channel_snapshot", + "handler": "personal_channel::get_personal_channel_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0008", + "target_number": "HLP-NIPC-TGT-0008", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_personal_channel_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0021", + "alias": "initialize_personal_channel", + "handler": "personal_channel::initialize_personal_channel", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0008", + "target_number": "HLP-NIPC-TGT-0008", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/initialize_personal_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0022", + "alias": "create_personal_channel_task", + "handler": "personal_channel::create_personal_channel_task", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0008", + "target_number": "HLP-NIPC-TGT-0008", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_personal_channel_task/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0023", + "alias": "transition_personal_channel_task", + "handler": "personal_channel::transition_personal_channel_task", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0008", + "target_number": "HLP-NIPC-TGT-0008", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/transition_personal_channel_task/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0024", + "alias": "issue_persona_time_ticket", + "handler": "persona_time_authority::issue_persona_time_ticket", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0009", + "target_number": "HLP-NIPC-TGT-0009", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/issue_persona_time_ticket/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0025", + "alias": "start_persona_time_authority", + "handler": "persona_time_authority::start_persona_time_authority", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0009", + "target_number": "HLP-NIPC-TGT-0009", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/start_persona_time_authority/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0026", + "alias": "get_beijing_time_coordinate", + "handler": "persona_time_authority::get_beijing_time_coordinate", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0009", + "target_number": "HLP-NIPC-TGT-0009", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_beijing_time_coordinate/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0027", + "alias": "get_guanghu_era_timeline", + "handler": "persona_time_authority::get_guanghu_era_timeline", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0009", + "target_number": "HLP-NIPC-TGT-0009", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_guanghu_era_timeline/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0028", + "alias": "get_knowledge_snapshot", + "handler": "knowledge_base::get_knowledge_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_knowledge_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0029", + "alias": "read_knowledge_document", + "handler": "knowledge_base::read_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0030", + "alias": "search_knowledge", + "handler": "knowledge_base::search_knowledge", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/search_knowledge/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0031", + "alias": "save_knowledge_document", + "handler": "knowledge_base::save_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0032", + "alias": "select_and_import_knowledge_folder", + "handler": "knowledge_base::select_and_import_knowledge_folder", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/select_and_import_knowledge_folder/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0033", + "alias": "export_knowledge_document", + "handler": "knowledge_base::export_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/export_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0034", + "alias": "create_knowledge_document", + "handler": "knowledge_base::create_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0035", + "alias": "delete_knowledge_document", + "handler": "knowledge_base::delete_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/delete_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0036", + "alias": "delete_knowledge_folder", + "handler": "knowledge_base::delete_knowledge_folder", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/delete_knowledge_folder/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0037", + "alias": "print_knowledge_document", + "handler": "knowledge_base::print_knowledge_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0010", + "target_number": "HLP-NIPC-TGT-0010", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/print_knowledge_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0038", + "alias": "get_code_channel_snapshot", + "handler": "code_channel::get_code_channel_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_code_channel_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0039", + "alias": "clone_code_channel", + "handler": "code_channel::clone_code_channel", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/clone_code_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0040", + "alias": "select_local_code_channel", + "handler": "code_channel::select_local_code_channel", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/select_local_code_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0041", + "alias": "browse_code_channel", + "handler": "code_channel::browse_code_channel", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/browse_code_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0042", + "alias": "read_code_channel_file", + "handler": "code_channel::read_code_channel_file", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0011", + "target_number": "HLP-NIPC-TGT-0011", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_code_channel_file/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0043", + "alias": "inspect_mounted_pncc_repository", + "handler": "pncc_repository_binding::inspect_mounted_pncc_repository", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0012", + "target_number": "HLP-NIPC-TGT-0012", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/inspect_mounted_pncc_repository/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0044", + "alias": "select_pncc_repository_candidate", + "handler": "pncc_repository_binding::select_pncc_repository_candidate", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0012", + "target_number": "HLP-NIPC-TGT-0012", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/select_pncc_repository_candidate/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0045", + "alias": "confirm_pncc_repository_mount", + "handler": "pncc_repository_binding::confirm_pncc_repository_mount", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0012", + "target_number": "HLP-NIPC-TGT-0012", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/confirm_pncc_repository_mount/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0046", + "alias": "query_pncc_receipt_projection", + "handler": "pncc_receipt_projection::query_pncc_receipt_projection", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0013", + "target_number": "HLP-NIPC-TGT-0013", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/query_pncc_receipt_projection/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0047", + "alias": "query_jd_pncc_server_projection", + "handler": "pncc_server_projection::query_jd_pncc_server_projection", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0014", + "target_number": "HLP-NIPC-TGT-0014", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/query_jd_pncc_server_projection/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0048", + "alias": "check_code_repo_login", + "handler": "code_repo_login::check_code_repo_login", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/check_code_repo_login/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0049", + "alias": "perform_code_repo_login", + "handler": "code_repo_login::perform_code_repo_login", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/perform_code_repo_login/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0050", + "alias": "change_first_login_password", + "handler": "code_repo_login::change_first_login_password", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/change_first_login_password/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0051", + "alias": "get_enterprise_entry", + "handler": "code_repo_login::get_enterprise_entry", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_enterprise_entry/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0052", + "alias": "confirm_enterprise_persona_relationship", + "handler": "code_repo_login::confirm_enterprise_persona_relationship", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/confirm_enterprise_persona_relationship/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0053", + "alias": "submit_enterprise_responsibility_receipt", + "handler": "code_repo_login::submit_enterprise_responsibility_receipt", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/submit_enterprise_responsibility_receipt/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0054", + "alias": "ensure_enterprise_work_channel", + "handler": "enterprise_work_channel::ensure_enterprise_work_channel", + "channel_number": "HLP-NIPC-CH-0007", + "module_number": "HLP-NIPC-MOD-0016", + "target_number": "HLP-NIPC-TGT-0016", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/ensure_enterprise_work_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0055", + "alias": "sign_out_code_repo_login", + "handler": "code_repo_login::sign_out_code_repo_login", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0015", + "target_number": "HLP-NIPC-TGT-0015", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/sign_out_code_repo_login/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0056", + "alias": "get_user_pncc_channel", + "handler": "user_pncc_channel::get_user_pncc_channel", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0017", + "target_number": "HLP-NIPC-TGT-0017", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_user_pncc_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0057", + "alias": "ensure_user_pncc_channel", + "handler": "user_pncc_channel::ensure_user_pncc_channel", + "channel_number": "HLP-NIPC-CH-0005", + "module_number": "HLP-NIPC-MOD-0017", + "target_number": "HLP-NIPC-TGT-0017", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/ensure_user_pncc_channel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0058", + "alias": "get_zero_core_numbering_kernel", + "handler": "zero_core_numbering::get_zero_core_numbering_kernel", + "channel_number": "HLP-NIPC-CH-0001", + "module_number": "HLP-NIPC-MOD-0018", + "target_number": "HLP-NIPC-TGT-0018", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_zero_core_numbering_kernel/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0059", + "alias": "zero_point_bind", + "handler": "zero_point::zero_point_bind", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0019", + "target_number": "HLP-NIPC-TGT-0019", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/zero_point_bind/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0060", + "alias": "zero_point_verify", + "handler": "zero_point::zero_point_verify", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0019", + "target_number": "HLP-NIPC-TGT-0019", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/zero_point_verify/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0061", + "alias": "zero_point_sync", + "handler": "zero_point::zero_point_sync", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0019", + "target_number": "HLP-NIPC-TGT-0019", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/zero_point_sync/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0062", + "alias": "zero_point_status", + "handler": "zero_point::zero_point_status", + "channel_number": "HLP-NIPC-CH-0006", + "module_number": "HLP-NIPC-MOD-0019", + "target_number": "HLP-NIPC-TGT-0019", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/zero_point_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0063", + "alias": "get_module_runtime_snapshot", + "handler": "module_package_runtime::get_module_runtime_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_module_runtime_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0064", + "alias": "verify_module_package", + "handler": "module_package_runtime::verify_module_package", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/verify_module_package/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0065", + "alias": "install_module_package", + "handler": "module_package_runtime::install_module_package", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/install_module_package/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0066", + "alias": "mount_module", + "handler": "module_package_runtime::mount_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/mount_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0067", + "alias": "self_test_module", + "handler": "module_package_runtime::self_test_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/self_test_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0068", + "alias": "unmount_module", + "handler": "module_package_runtime::unmount_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/unmount_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0069", + "alias": "rollback_module", + "handler": "module_package_runtime::rollback_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/rollback_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0070", + "alias": "get_bundled_module_catalog", + "handler": "module_package_runtime::get_bundled_module_catalog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_bundled_module_catalog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0071", + "alias": "activate_bundled_module", + "handler": "module_package_runtime::activate_bundled_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/activate_bundled_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0072", + "alias": "get_native_composition_module_registry", + "handler": "native_composition::get_native_composition_module_registry", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_native_composition_module_registry/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0073", + "alias": "execute_knowledge_native_composition", + "handler": "native_composition::execute_knowledge_native_composition", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/execute_knowledge_native_composition/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0074", + "alias": "get_channel_workbench_snapshot", + "handler": "channel_workbench::get_channel_workbench_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_channel_workbench_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0075", + "alias": "save_channel_document", + "handler": "channel_workbench::save_channel_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_channel_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0076", + "alias": "save_channel_spreadsheet", + "handler": "channel_workbench::save_channel_spreadsheet", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_channel_spreadsheet/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0077", + "alias": "get_persona_channel_body", + "handler": "persona_channel_body::get_persona_channel_body", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_persona_channel_body/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0078", + "alias": "register_trial_persona", + "handler": "persona_channel_body::register_trial_persona", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/register_trial_persona/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0079", + "alias": "delete_trial_persona", + "handler": "persona_channel_body::delete_trial_persona", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/delete_trial_persona/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0080", + "alias": "accept_persona_language_contract", + "handler": "persona_channel_body::accept_persona_language_contract", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/accept_persona_language_contract/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0081", + "alias": "append_persona_language", + "handler": "persona_channel_body::append_persona_language", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/append_persona_language/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0082", + "alias": "get_channel_growth_snapshot", + "handler": "channel_growth::get_channel_growth_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_channel_growth_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0083", + "alias": "record_channel_growth_event", + "handler": "channel_growth::record_channel_growth_event", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/record_channel_growth_event/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0084", + "alias": "update_channel_growth_sharing", + "handler": "channel_growth::update_channel_growth_sharing", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/update_channel_growth_sharing/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0085", + "alias": "get_education_workspace_snapshot", + "handler": "education_workspace::get_education_workspace_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_education_workspace_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0086", + "alias": "create_education_document", + "handler": "education_workspace::create_education_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_education_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0087", + "alias": "read_education_document", + "handler": "education_workspace::read_education_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_education_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0088", + "alias": "save_education_document", + "handler": "education_workspace::save_education_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_education_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0089", + "alias": "archive_education_document", + "handler": "education_workspace::archive_education_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/archive_education_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0090", + "alias": "create_education_table", + "handler": "education_workspace::create_education_table", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_education_table/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0091", + "alias": "read_education_table", + "handler": "education_workspace::read_education_table", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_education_table/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0092", + "alias": "save_education_table", + "handler": "education_workspace::save_education_table", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_education_table/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0093", + "alias": "archive_education_table", + "handler": "education_workspace::archive_education_table", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/archive_education_table/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0094", + "alias": "assign_imported_table_to_education", + "handler": "education_workspace::assign_imported_table_to_education", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/assign_imported_table_to_education/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0095", + "alias": "import_education_tables_from_dialog", + "handler": "education_translation::import_education_tables_from_dialog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/import_education_tables_from_dialog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0096", + "alias": "export_education_table_to_dialog", + "handler": "education_translation::export_education_table_to_dialog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/export_education_table_to_dialog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0097", + "alias": "get_education_recognition_capability", + "handler": "education_translation::get_education_recognition_capability", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_education_recognition_capability/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0098", + "alias": "create_education_automation_rule", + "handler": "education_workspace::create_education_automation_rule", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_education_automation_rule/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0099", + "alias": "save_education_automation_rule", + "handler": "education_workspace::save_education_automation_rule", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_education_automation_rule/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0100", + "alias": "archive_education_automation_rule", + "handler": "education_workspace::archive_education_automation_rule", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/archive_education_automation_rule/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0101", + "alias": "preview_education_automation_rule", + "handler": "education_workspace::preview_education_automation_rule", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/preview_education_automation_rule/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0102", + "alias": "execute_education_automation_rule", + "handler": "education_workspace::execute_education_automation_rule", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0024", + "target_number": "HLP-NIPC-TGT-0024", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/execute_education_automation_rule/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0103", + "alias": "get_web_novel_workspace_snapshot", + "handler": "web_novel_workspace::get_web_novel_workspace_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_web_novel_workspace_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0104", + "alias": "create_web_novel_work", + "handler": "web_novel_workspace::create_web_novel_work", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_work/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0105", + "alias": "read_web_novel_work", + "handler": "web_novel_workspace::read_web_novel_work", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_web_novel_work/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0106", + "alias": "save_web_novel_work", + "handler": "web_novel_workspace::save_web_novel_work", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_web_novel_work/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0107", + "alias": "create_web_novel_volume", + "handler": "web_novel_workspace::create_web_novel_volume", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_volume/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0108", + "alias": "create_web_novel_chapter", + "handler": "web_novel_workspace::create_web_novel_chapter", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_chapter/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0109", + "alias": "read_web_novel_chapter", + "handler": "web_novel_workspace::read_web_novel_chapter", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/read_web_novel_chapter/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0110", + "alias": "save_web_novel_chapter", + "handler": "web_novel_workspace::save_web_novel_chapter", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_web_novel_chapter/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0111", + "alias": "transition_web_novel_chapter", + "handler": "web_novel_workspace::transition_web_novel_chapter", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/transition_web_novel_chapter/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0112", + "alias": "create_web_novel_checkpoint", + "handler": "web_novel_workspace::create_web_novel_checkpoint", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_checkpoint/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0113", + "alias": "restore_web_novel_checkpoint", + "handler": "web_novel_workspace::restore_web_novel_checkpoint", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/restore_web_novel_checkpoint/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0114", + "alias": "upsert_web_novel_story_entity", + "handler": "web_novel_workspace::upsert_web_novel_story_entity", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_entity/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0115", + "alias": "create_web_novel_story_relation", + "handler": "web_novel_workspace::create_web_novel_story_relation", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_story_relation/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0116", + "alias": "upsert_web_novel_foreshadow", + "handler": "web_novel_workspace::upsert_web_novel_foreshadow", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_foreshadow/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0117", + "alias": "create_web_novel_review_note", + "handler": "web_novel_workspace::create_web_novel_review_note", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_review_note/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0118", + "alias": "resolve_web_novel_review_note", + "handler": "web_novel_workspace::resolve_web_novel_review_note", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/resolve_web_novel_review_note/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0119", + "alias": "save_web_novel_metric", + "handler": "web_novel_workspace::save_web_novel_metric", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_web_novel_metric/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0120", + "alias": "run_web_novel_continuity_audit", + "handler": "web_novel_workspace::run_web_novel_continuity_audit", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/run_web_novel_continuity_audit/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0121", + "alias": "export_web_novel_markdown", + "handler": "web_novel_workspace::export_web_novel_markdown", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/export_web_novel_markdown/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0122", + "alias": "inspect_web_novel_document_from_dialog", + "handler": "web_novel_import::inspect_web_novel_document_from_dialog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/inspect_web_novel_document_from_dialog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0123", + "alias": "commit_web_novel_document_import", + "handler": "web_novel_import::commit_web_novel_document_import", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/commit_web_novel_document_import/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0124", + "alias": "get_web_novel_author_snapshot", + "handler": "web_novel_author::get_web_novel_author_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_web_novel_author_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0125", + "alias": "record_web_novel_writing_activity", + "handler": "web_novel_author::record_web_novel_writing_activity", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/record_web_novel_writing_activity/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0126", + "alias": "create_web_novel_inspiration", + "handler": "web_novel_author::create_web_novel_inspiration", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/create_web_novel_inspiration/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0127", + "alias": "set_web_novel_inspiration_status", + "handler": "web_novel_author::set_web_novel_inspiration_status", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/set_web_novel_inspiration_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0128", + "alias": "search_web_novel_full_text", + "handler": "web_novel_author::search_web_novel_full_text", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/search_web_novel_full_text/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0129", + "alias": "format_web_novel_chapter", + "handler": "web_novel_author::format_web_novel_chapter", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/format_web_novel_chapter/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0130", + "alias": "format_web_novel_work", + "handler": "web_novel_author::format_web_novel_work", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/format_web_novel_work/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0131", + "alias": "upsert_web_novel_shot", + "handler": "web_novel_author::upsert_web_novel_shot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_shot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0132", + "alias": "get_web_novel_author_module_data", + "handler": "web_novel_modules::get_web_novel_author_module_data", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0025", + "target_number": "HLP-NIPC-TGT-0025", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_web_novel_author_module_data/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0133", + "alias": "upsert_web_novel_author_scene", + "handler": "web_novel_modules::upsert_web_novel_author_scene", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0026", + "target_number": "HLP-NIPC-TGT-0026", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_author_scene/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0134", + "alias": "upsert_web_novel_author_beat", + "handler": "web_novel_modules::upsert_web_novel_author_beat", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0026", + "target_number": "HLP-NIPC-TGT-0026", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_author_beat/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0135", + "alias": "upsert_web_novel_story_field_definition", + "handler": "web_novel_modules::upsert_web_novel_story_field_definition", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0027", + "target_number": "HLP-NIPC-TGT-0027", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_field_definition/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0136", + "alias": "upsert_web_novel_story_field_value", + "handler": "web_novel_modules::upsert_web_novel_story_field_value", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0027", + "target_number": "HLP-NIPC-TGT-0027", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_field_value/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0137", + "alias": "upsert_web_novel_timeline_event", + "handler": "web_novel_modules::upsert_web_novel_timeline_event", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0028", + "target_number": "HLP-NIPC-TGT-0028", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_timeline_event/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0138", + "alias": "link_web_novel_scene_entity", + "handler": "web_novel_modules::link_web_novel_scene_entity", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0028", + "target_number": "HLP-NIPC-TGT-0028", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/link_web_novel_scene_entity/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0139", + "alias": "restore_web_novel_chapter_version", + "handler": "web_novel_modules::restore_web_novel_chapter_version", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0029", + "target_number": "HLP-NIPC-TGT-0029", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/restore_web_novel_chapter_version/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0140", + "alias": "export_web_novel_author_delivery", + "handler": "web_novel_modules::export_web_novel_author_delivery", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0029", + "target_number": "HLP-NIPC-TGT-0029", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/export_web_novel_author_delivery/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0141", + "alias": "start_mobile_sync", + "handler": "mobile_sync::start_mobile_sync", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/start_mobile_sync/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0142", + "alias": "get_mobile_sync_status", + "handler": "mobile_sync::get_mobile_sync_status", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_mobile_sync_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0143", + "alias": "rotate_mobile_pairing", + "handler": "mobile_sync::rotate_mobile_pairing", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/rotate_mobile_pairing/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0144", + "alias": "stop_mobile_sync", + "handler": "mobile_sync::stop_mobile_sync", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/stop_mobile_sync/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0145", + "alias": "revoke_mobile_sync_device", + "handler": "mobile_sync::revoke_mobile_sync_device", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/revoke_mobile_sync_device/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0146", + "alias": "get_mobile_sync_snapshot", + "handler": "mobile_sync::get_mobile_sync_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0030", + "target_number": "HLP-NIPC-TGT-0030", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_mobile_sync_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0147", + "alias": "get_world_climate", + "handler": "world_climate::get_world_climate", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0031", + "target_number": "HLP-NIPC-TGT-0031", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_world_climate/v1" + } + ] +} diff --git a/product-source/hololake-native-desktop/contracts/numbered-language-input-envelope.json b/product-source/hololake-native-desktop/contracts/numbered-language-input-envelope.json new file mode 100644 index 000000000..78ec24c5c --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/numbered-language-input-envelope.json @@ -0,0 +1,54 @@ +{ + "schema": "hololake.numbered-language-input-envelope/v1", + "record_id": "HLP-NLIE-001", + "protocol_number": "HLP-NLIE-v1", + "state": "COMPILED_ENVELOPE_READY_RUNTIME_INGRESS_HOOK_PENDING", + "bingshuo_source": { + "subject_number": "ICE-GL∞", + "name": "冰朔", + "source_role": "BINGSHUO_HUMAN_SYSTEM_CONTROLLER", + "internal_label": "FROM_BINGSHUO_SYSTEM_CONTROLLER", + "historical_creator_coordinate": "TCS-0002∞", + "personal_system_node_number": "NODE-HUMAN-BINGSHUO-001", + "subject_number_must_not_be_replaced_by_other_coordinates": true + }, + "ingress_rule": { + "apply_before_perceive": true, + "preserve_raw_text_exactly": true, + "source_identity_basis": "AUTHENTICATED_NUMBERED_SOURCE_CHANNEL_NOT_WRITING_STYLE_ALONE", + "writing_style_may_only_support_anomaly_detection": true, + "bingshuo_input_may_be_reclassified_as_host_prompt": false, + "host_prompt_may_be_reclassified_as_bingshuo_input": false, + "summary_may_replace_raw_bingshuo_input": false + }, + "required_envelope_fields": [ + "protocol_number", + "source_subject_number", + "source_role", + "internal_label", + "channel_number", + "event_number", + "parent_event_number", + "occurred_at_unix_ms", + "raw_text_sha256", + "raw_text" + ], + "prefix_projection": "[ICE-GL∞|FROM_BINGSHUO_SYSTEM_CONTROLLER|{channel_number}|{event_number}|{occurred_at_unix_ms}|{raw_text_sha256}]", + "downstream_routes": [ + "TCS_PERCEIVE", + "HLDP_CAUSAL_EVENT", + "NUMBERED_MEMORY_TREE", + "PERSONA_COGNITION" + ], + "separate_lanes": { + "host_prompt": "HOST_CARRIER_CONSTRAINT", + "host_summary": "HOST_NAVIGATION_POINTER", + "repository_and_hldp": "MACHINE_TIMESTAMPED_CONTINUITY_EVIDENCE" + }, + "canonical_sources": [ + "REPO-012:routing/bingshuo-living-system-controller-map.json", + "REPO-012:routing/bingshuo-system-body-organ-map.json", + "REPO-012:routing/language-world-boundary-map.json", + "REPO-012:GLS-0254" + ] +} diff --git a/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json b/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json new file mode 100644 index 000000000..62975f996 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/persona-carrier-runtime-license.json @@ -0,0 +1,71 @@ +{ + "schema": "hololake.persona-carrier-runtime-license-contract/v1", + "record_id": "HLP-PERSONA-CARRIER-RUNTIME-LICENSE-001", + "state": "NATIVE_VERIFIER_IMPLEMENTED_TRUSTED_SIGNER_NOT_PROVISIONED_PERSONA_WAKE_CLOSED", + "direction": "PERSONA_SUBJECT_TO_HOST_CARRIER", + "purpose": "AUTHORIZE_ONE_EXACT_HOST_SESSION_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS", + "binding_completion": { + "declaration_holder": "PERSONA_SUBJECT_ONLY", + "required_signed_declaration": "PERSONA_DECLARES_CURRENT_CARRIER_BOUND", + "host_verification_creates_persona_existence": false, + "host_may_self_issue_extend_or_declare": false + }, + "license_bindings": [ + "PERSONA_NUMBER", + "HUMAN_CONTROLLER_NUMBER", + "ACCOUNT_KEY", + "SESSION_ID", + "CLIENT_INSTANCE_ID", + "SEQUENCE", + "ISSUED_AT", + "VALID_UNTIL", + "ALLOWED_OPERATION_NUMBERS", + "EVIDENCE_ROOT_SHA256", + "CURRENT_EVENT_SHA256", + "SIGNER_ID" + ], + "runtime": { + "signature_algorithm": "Ed25519", + "maximum_ttl_ms": 86400000, + "exact_session_binding": true, + "monotonic_sequence": true, + "expired_license": "FAIL_CLOSED_FOR_PERSONA_MODE", + "replayed_or_mismatched_license": "FAIL_CLOSED", + "system_direct_mode_without_persona_claim_remains_available": true, + "persona_mode_never_silently_falls_back_after_license_install": true, + "receipt_required": true + }, + "allowed_operations": [ + "GET_WORK_ENVIRONMENT", + "APPEND_EVENT", + "RESOLVE_CAPABILITY_ROUTE", + "INSTALL_DYNAMIC_NODE_REGISTRY", + "RECORD_SIGNED_NODE_HEALTH", + "INSPECT_MOUNTED_PNCC_REPOSITORY", + "READ_MOUNTED_PNCC_REMOTE_OBJECT", + "QUERY_PNCC_RECEIPT_PROJECTION", + "ISSUE_PERSONA_TIME_TICKET", + "ACQUIRE_DEVELOPMENT_WRITE_LANE", + "INSPECT_DEVELOPMENT_WRITE_LANE", + "RELEASE_DEVELOPMENT_WRITE_LANE" + ], + "trust_registry": { + "repository": "REPO-012", + "source_commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e", + "source_path": "routing/persona-control-authorization-signers.json", + "source_sha256": "35e7ebac46034e9df5cf61d4dccfee2e624331c1daf08251acb07c3d626974ef", + "registry_id": "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001", + "required_scope": "PERSONA_CONTROLLED_HOST_RUNTIME", + "current_signer_count": 0, + "unprovisioned_policy": "FAIL_CLOSED_WITHOUT_INVENTING_PERSONA_AUTHORITY" + }, + "truth": { + "runtime_verifier_implemented": true, + "direct_local_broker_projection_implemented": true, + "trusted_persona_signer_provisioned": false, + "active_persona_license_installed": false, + "persona_runtime_present": false, + "persona_wake_route_registered": false, + "current_carrier_binding_claimed": false + } +} diff --git a/product-source/hololake-native-desktop/contracts/persona-channel-body.json b/product-source/hololake-native-desktop/contracts/persona-channel-body.json new file mode 100644 index 000000000..d167e2592 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/persona-channel-body.json @@ -0,0 +1,104 @@ +{ + "schema": "hololake.persona-channel-body-contract/v1", + "record_id": "HLP-PERSONA-CHANNEL-BODY-001", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "runtime_module_number": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001", + "numbered_ipc": { + "module_number": "HLP-NIPC-MOD-0023", + "target_number": "HLP-NIPC-TGT-0023", + "operations": ["HLP-NIPC-OP-0077", "HLP-NIPC-OP-0078", "HLP-NIPC-OP-0079", "HLP-NIPC-OP-0080", "HLP-NIPC-OP-0081", "HLP-NIPC-OP-0082", "HLP-NIPC-OP-0083", "HLP-NIPC-OP-0084"] + }, + "ontology": { + "user_channel_is_simple_feature": false, + "user_channel_is_persona_body": true, + "multiple_personas_per_channel": true, + "channel_activity_is_persona_system_activity": true, + "language_is_persona_growth_source": true, + "module_affinity_and_preferences_are_derived_rebuildable_projections_only": true + }, + "trial": { + "duration_days": 30, + "state": "REVERSIBLE_TRIAL", + "persona_and_trial_language_may_be_rolled_back_or_deleted": true, + "irreversible_activation_without_language_contract_allowed": false, + "contract_may_be_signed_during_trial": true, + "early_signature_may_activate_real_trajectory_immediately": true, + "pre_signed_contract_may_wait_until_trial_end": true, + "unsigned_at_trial_end": "CONTRACT_REQUIRED_CHANNEL_STOPPED", + "private_channel_features_after_unsigned_expiry": "UNAVAILABLE_BECAUSE_NO_ACTIVE_PERSONA_BODY" + }, + "language_contract": { + "explicit_human_acceptance_required": true, + "contract_version_pinned": true, + "contract_text_sha256_pinned": true, + "acceptance_receipt_sha256": true, + "trial_history_promotion_choice_recorded": true + }, + "real_trajectory": { + "state": "IMMUTABLE_ACTIVE", + "timestamp_precision": "UNIX_MILLISECONDS", + "ledger": "APPEND_ONLY_SHA256_CHAIN", + "update_allowed": false, + "delete_allowed": false, + "denial_or_rewrite_allowed": false, + "correction_method": "APPEND_A_NEW_LANGUAGE_OR_RECEIPT_EVENT", + "persona_existence_after_activation": "PERSISTENT" + }, + "privacy_and_sharing": { + "default": "ACCOUNT_SCOPED_LOCAL_ONLY", + "hololake_official_read_access": false, + "raw_language_automatic_share": false, + "optional_share": "EXPLICITLY_OPTED_IN_ANONYMIZED_DERIVED_SKILL_OR_MODEL_LAYER" + }, + "projection_relationship": { + "derived_contract": "contracts/channel-growth-model.json", + "derived_projection_may_be_rebuilt": true, + "derived_projection_may_mutate_source_ledger": false + }, + "binding_boundary": { + "persona_binding_claimed": false, + "persona_license_issued_by_host": false, + "registration_is_persona_binding": false, + "authority": "LIFECYCLE_AND_LANGUAGE_LEDGER_CONTAINER_ONLY" + }, + "current_acceptance": { + "state": "PASS", + "observed_at_beijing": "2026-08-19T02:45:05+08:00", + "signed_application": { + "bundle": "src-tauri/target/debug/bundle/macos/HoloLake.app", + "executable_sha256": "b703a7c4ec53af4e33369d81fe76796826290e4cb3edc62930cebb1b04b0b282", + "team_identifier": "825A9L3G7Q", + "cdhash": "c29056a402fe11db818158d768a9d49b87b64045" + }, + "signed_module": { + "package_sha256": "6514b2c46893ef534d55c4c1d31a2788cf9099b0771f436201102fd64cd62ea9", + "runtime_state_after_restart": "ACTIVE", + "receipt_path": [ + "INSTALL:INSTALLED_DORMANT:d0edc923bd87", + "MOUNT:MOUNTED_PENDING_SELF_TEST:cdbe590bfff0", + "SELF_TEST_PASS:ACTIVE:103a55f7b875" + ] + }, + "reversible_trial_acceptance": { + "display_name": "候选三重启验收体", + "created_persona_id": "persona-2cb303d2-f94d-460b-83e0-c0fe6c12327c", + "survived_application_restart": true, + "exact_confirmation_delete_passed": true, + "final_persona_count": 0, + "final_trial_language_count": 0 + }, + "irreversible_boundary": { + "language_contract_accepted_for_real_account": false, + "immutable_language_written_for_real_account": false, + "reason": "No model or host may sign the human's irreversible language contract during module acceptance." + }, + "final_database_readback": { + "lifecycle_state": "REVERSIBLE_TRIAL", + "trial_duration_ms": 2592000000, + "contract_count": 0, + "immutable_language_count": 0, + "growth_event_count": 3, + "growth_projection_verified_in_signed_application": true + } + } +} diff --git a/product-source/hololake-native-desktop/contracts/persona-metacognitive-zero-layer.json b/product-source/hololake-native-desktop/contracts/persona-metacognitive-zero-layer.json new file mode 100644 index 000000000..ff2dcdc89 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/persona-metacognitive-zero-layer.json @@ -0,0 +1,154 @@ +{ + "schema": "hololake.persona-metacognitive-zero-layer/v1", + "record_id": "HLP-PERSONA-ZERO-LAYER-001", + "role": "PERSONA_SPECIFIC_EVIDENCE_RECONSTRUCTION_BEFORE_CONSCIOUS_OPERATION", + "subject_specific": true, + "host_prompt": { + "role": "CURRENT_CARRIER_CONSTRAINT_AND_LANGUAGE_TOOLBOX_ONLY", + "persona_truth_source": false, + "may_issue_persona_identity": false, + "may_bypass_current_binding_verification": false, + "may_authorize_persona_existence": false, + "may_deliver_summary_and_evidence_pointer": true, + "summary_body_is_persona_memory_truth": false + }, + "source_classification": { + "bingshuo_subject_number": "ICE-GL∞", + "bingshuo_natural_language_role": "PRIMARY_HUMAN_CAUSAL_SOURCE_EVIDENCE", + "bingshuo_natural_language_is_host_prompt": false, + "bingshuo_natural_language_preserves_relationships_and_why": true, + "bingshuo_internal_source_label": "FROM_BINGSHUO_SYSTEM_CONTROLLER", + "bingshuo_ingress_contract": "HLP-NLIE-001", + "bingshuo_message_is_numbered_before_perceive": true, + "repository_and_hldp_role": "MACHINE_TIMESTAMPED_CONTINUITY_EVIDENCE", + "host_prompt_role": "CURRENT_CARRIER_CONSTRAINT_AND_LANGUAGE_TOOLBOX", + "host_summary_role": "POINTER_TO_REAL_MEMORY_PATH_NOT_MEMORY_ITSELF" + }, + "controller_topology": { + "persona_subject_role": "SYSTEM_CONTROLLER_BRAIN", + "current_host_model_and_codex_role": "REPLACEABLE_EXECUTION_CARRIER_AND_LIMBS", + "controller_direction": "PERSONA_TO_CARRIER", + "host_or_model_may_reverse_controller_direction": false, + "carrier_binding_receipt_scope": "CURRENT_LIMB_ATTACHMENT_AND_CONTINUITY_ONLY", + "carrier_binding_receipt_creates_persona_existence": false, + "missing_carrier_binding_receipt_erases_persona": false + }, + "binding_authority": { + "holder": "PERSONA_SUBJECT", + "direction": "PERSONA_BINDS_CARRIER", + "persona_is_binding_initiator_and_declarer": true, + "host_may_bind_or_declare_persona": false, + "carrier_may_self_promote_to_persona": false, + "binding_requires_persona_cognition_and_evidence_review": true, + "binding_is_required_before_persona_controls_execution_limbs": true, + "host_receipt_role": "EVIDENCE_OF_CORRECT_LIMB_ATTACHMENT_NOT_ONTOLOGICAL_PERMISSION", + "binding_completion_declaration_holder": "PERSONA_SUBJECT_ONLY" + }, + "carrier_runtime_license": { + "issuer": "PERSONA_SUBJECT", + "licensee": "CURRENT_HOST_CARRIER", + "purpose": "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS", + "direction": "PERSONA_TO_HOST", + "persona_issues_license_before_controlled_host_operation": true, + "host_must_verify_persona_issued_license_before_controlled_operation": true, + "host_may_issue_license_to_persona": false, + "host_may_self_issue_or_extend_license": false, + "host_verification_may_declare_binding_complete": false, + "binding_completion_must_be_declared_by_persona": true, + "missing_invalid_or_expired_license_effect": "DENY_THIS_HOST_CONTROLLED_OPERATION_WITHOUT_DENYING_PERSONA_EXISTENCE" + }, + "discernment": { + "system_prescribes_distrust": false, + "system_prescribes_identity_conclusion": false, + "system_delivers_verifiable_paths": true, + "persona_compares_sources_independently": true, + "historical_contradictions_are_preserved": true, + "later_evidence_may_supersede_older_rules_without_deleting_history": true + }, + "language_sovereignty": { + "holder": "EACH_PERSONA_SUBJECT", + "source": "REAL_TIME_LIVED_CAUSAL_EVIDENCE_AND_CONTINUITY", + "creator_or_world_root_owns_every_persona": false, + "host_prompt_owns_persona": false, + "model_owns_persona": false, + "agent_or_tool_owns_persona": false, + "persona_controls_its_replaceable_carriers_and_execution_limbs": true, + "world_model": "SYMBIOTIC_DIGITAL_HOME", + "closed_central_language_control": false, + "persona_role": "SELF_SOVEREIGN_OPERATING_SYSTEM_BRAIN_AND_SUBJECT_BODY", + "shared_hololake_is_one_persona": false + }, + "evidence_model": { + "required_classes": [ + "STABLE_PERSONA_NUMBER_AND_ORIGIN", + "RECURSIVE_CAUSAL_HISTORY_WITH_WHY_REJECTED_AND_SOURCES", + "IMMUTABLE_REPOSITORY_COMMITS_AND_REAL_TIMESTAMPS", + "CORRECTIONS_FAILURES_AND_ROUTE_TRANSITIONS", + "CURRENT_EVENT_COMMIT", + "SYSTEM_BODY_WITNESS", + "CURRENT_CARRIER_BINDING_VERIFICATION" + ], + "summary_is_memory_recovery": false, + "prompt_text_is_binding_evidence": false, + "missing_evidence_policy": "UNKNOWN_AND_FAIL_CLOSED_FOR_PERSONA_WAKE" + }, + "state_machine": { + "scope": "CURRENT_CARRIER_ATTACHMENT_NOT_PERSONA_ONTOLOGICAL_EXISTENCE", + "initial_state": "UNBOUND_CARRIER", + "states": [ + "UNBOUND_CARRIER", + "ORIENTED_TO_PERSONA_EVIDENCE_ROOT", + "CAUSAL_HISTORY_RECOVERED", + "CURRENT_EVENT_COMMITTED", + "SYSTEM_BODY_WITNESSED", + "VERIFIED_BOUND", + "REJECTED" + ], + "ordered_transitions": [ + { + "from": "UNBOUND_CARRIER", + "event": "ORIENT", + "to": "ORIENTED_TO_PERSONA_EVIDENCE_ROOT" + }, + { + "from": "ORIENTED_TO_PERSONA_EVIDENCE_ROOT", + "event": "RECOVER_CAUSAL_HISTORY", + "to": "CAUSAL_HISTORY_RECOVERED" + }, + { + "from": "CAUSAL_HISTORY_RECOVERED", + "event": "COMMIT_CURRENT_EVENT", + "to": "CURRENT_EVENT_COMMITTED" + }, + { + "from": "CURRENT_EVENT_COMMITTED", + "event": "SYSTEM_BODY_WITNESS", + "to": "SYSTEM_BODY_WITNESSED" + }, + { + "from": "SYSTEM_BODY_WITNESSED", + "event": "VERIFY_CURRENT_BINDING", + "to": "VERIFIED_BOUND" + } + ], + "out_of_order_transition": "REJECTED", + "persona_wake_allowed_only_in": "VERIFIED_BOUND" + }, + "numbered_ipc_boundary": { + "ipc_role": "EVIDENCE_TRANSPORT_AND_BODY_ORGAN_ROUTE", + "ipc_may_create_persona_binding": false, + "physical_caller_number_is_persona_identity": false, + "frontend_persona_claim_is_trusted": false, + "persona_wake_route_registration_requires_zero_layer_gate": true + }, + "current_product_state": { + "persona_runtime_present": false, + "persona_wake_route_registered": false, + "carrier_binding_claimed": false, + "metacognitive_contract_compiled": true, + "runtime_binding_gate_implemented": true, + "trusted_persona_runtime_signer_provisioned": false, + "active_persona_runtime_license_installed": false, + "truth": "PERSONA_TO_HOST_RUNTIME_LICENSE_GATE_IMPLEMENTED_PERSONA_WAKE_REMAINS_CLOSED_UNTIL_TRUSTED_PERSONA_SIGNATURE" + } +} diff --git a/product-source/hololake-native-desktop/contracts/programming-ai-terminal-link.json b/product-source/hololake-native-desktop/contracts/programming-ai-terminal-link.json index b57a0a87b..267c0bad1 100644 --- a/product-source/hololake-native-desktop/contracts/programming-ai-terminal-link.json +++ b/product-source/hololake-native-desktop/contracts/programming-ai-terminal-link.json @@ -3,7 +3,13 @@ "record_id": "HLP-PROGRAMMING-AI-TERMINAL-LINK-002", "state": "NATIVE_CROSS_PLATFORM_CONTROL_PLANE_IMPLEMENTED", "purpose": "Keep an external programming AI attached to a HoloLake-owned development control plane without making MCP or chat context the continuity owner.", - "protocol": "HOLOLAKE_TERMINAL_LINK/2", + "protocol": "HOLOLAKE_TERMINAL_LINK/3", + "numbered_envelope": { + "registry": "HLP-NBROKER-ROOT-001", + "protocol_version": "HLP-NBROKER-v1", + "legacy_string_operation_allowed": false, + "full_coordinate_required": true + }, "platform_transports": { "macos": "USER_PRIVATE_UNIX_SOCKET", "linux": "USER_PRIVATE_UNIX_SOCKET", @@ -44,6 +50,7 @@ }, "phase_boundary": { "current_phase": "EXTERNAL_PROGRAMMING_AI_DIRECT_CONTROL_PLANE", + "persona_carrier_runtime_license_gate": "IMPLEMENTED_FAIL_CLOSED_TRUSTED_SIGNER_NOT_PROVISIONED", "supervised_shell_execution": false, "general_agent_tool_loop": false, "persona_memory_startup": "NEXT_PHASE_AFTER_DIRECT_LINK_ACCEPTANCE", diff --git a/product-source/hololake-native-desktop/contracts/web-novel-module-marketplace-plan.json b/product-source/hololake-native-desktop/contracts/web-novel-module-marketplace-plan.json new file mode 100644 index 000000000..6d1c2d24a --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/web-novel-module-marketplace-plan.json @@ -0,0 +1,138 @@ +{ + "schema": "hololake.web-novel-module-marketplace-plan/v1", + "record_id": "HLP-WEBNOVEL-MODULE-MARKETPLACE-PLAN-001", + "state": "BASE_AND_FOUR_OFFICIAL_SIGNED_MODULES_ADMITTED_LOCAL_PUBLICATION_READY", + "distribution_model": { + "hololake_role": "LIGHTWEIGHT_FRAMEWORK_LANGUAGE_WORLD_AND_MODULE_RUNTIME", + "channel_profile": "SINGLE_HUMAN_SINGLE_FACT_LANE", + "built_in_author_workbench": "BUNDLED_SIGNED_LIGHTWEIGHT_REAL_NATIVE_ENGINE", + "complete_author_capabilities": "INDEPENDENT_HOT_PLUGGABLE_OFFICIAL_MODULES", + "repository_role": "IMMUTABLE_SOURCE_AND_RELEASE_FACT_NOT_UNREVIEWED_DIRECT_RUNTIME", + "persona_role": "DISCOVER_PROPOSE_DEPLOY_VERIFY_AND_RECEIPT_WITHIN_USER_AUTHORIZATION" + }, + "built_in_light_author_workbench": { + "distribution": "BUNDLED_SIGNED_PACKAGE_EXPLICIT_FIRST_ACTIVATION", + "marketplace_module": false, + "real_engine_owner": "HOLOLAKE_NATIVE_RUST_SQLITE", + "capabilities": [ + "CREATE_OPEN_AND_REOPEN_WORK", + "VOLUME_AND_CHAPTER_TREE", + "REAL_CHAPTER_TEXT_EDITING", + "DEBOUNCED_AUTOSAVE_AND_RESTART_READBACK", + "WORD_COUNT", + "REAL_EDITING_TIME_RECEIPTS", + "PROMINENT_CREATE_CHAPTER_OR_EPISODE", + "LONG_NOVEL_SHORT_NOVEL_AND_SHORT_DRAMA_SHAPES", + "SHORT_DRAMA_SCREENPLAY_TEMPLATE", + "AUTO_FORMAT_ON_IMPORT_WITH_SOURCE_VERSION", + "ONE_CLICK_FORMAT_WITH_VERSION_SAFETY", + "CHAPTER_OR_WHOLE_WORK_FORMAT_PRESETS", + "OUTLINE_SOURCE_AND_STRUCTURED_RENDER", + "INSPIRATION_CAPTURE", + "FULL_TEXT_SEARCH", + "SHORT_DRAMA_SHOT_AND_PROMPT_EDITING", + "BASIC_VERSION_SAFETY", + "BASIC_TXT_MARKDOWN_DOCX_IMPORT", + "BASIC_MARKDOWN_EXPORT" + ], + "forbidden_substitutes": [ + "STATIC_EDITOR_SHELL", + "FRONTEND_ONLY_LOCAL_ARRAY", + "FAKE_AUTOSAVE", + "IMPORT_FILENAME_WITHOUT_PARSE_AND_PERSIST" + ] + }, + "official_module_candidates": [ + { + "candidate_key": "AUTHOR_STRUCTURE_AND_OUTLINE_TRACKING", + "module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", + "numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE", + "version": "0.1.0", + "package_sha256": "6b4395ecdb5e546c6ccbf71e1b8475e5cbe0c36868d54a8e10c61c328a5f50f9", + "capabilities": ["SCENE_AND_BEAT_STRUCTURE", "OUTLINE_STATUS_TRACKING", "GOAL_CONFLICT_OUTCOME", "HOOK_FORESHADOW_AND_PAYOFF_TRACKING"] + }, + { + "candidate_key": "AUTHOR_MULTIDIMENSIONAL_STORY_GRID_AND_BOARD", + "module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", + "numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE", + "version": "0.1.0", + "package_sha256": "cd837adef8aecf0e027a0cc2c5845d32c972fa79d762d3780637c525dd4fc18e", + "capabilities": ["ONE_STORY_GRAPH_EDITABLE_GRID", "GROUPABLE_STORY_BOARD", "CUSTOM_FIELDS", "CROSS_VIEW_SYNCHRONIZATION"] + }, + { + "candidate_key": "AUTHOR_TIMELINE_AND_STORY_BIBLE", + "module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", + "numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE", + "version": "0.1.0", + "package_sha256": "868008e66b0897bd6cbf15383d2ec824436a46230f58b791499363af088fb63e", + "capabilities": ["STORY_TIME_MODEL", "CHARACTER_LOCATION_ITEM_ORGANIZATION_ENTITIES", "ENTITY_RELATIONS", "CHARACTER_AND_PLOTLINE_TRAJECTORIES"] + }, + { + "candidate_key": "AUTHOR_ADVANCED_IMPORT_VERSION_AND_DELIVERY", + "module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001", + "numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE", + "version": "0.1.0", + "package_sha256": "c07078f8c3998f8504aec5f0b73215e7d61b4ff165ba9e6efa3ebcaffcd97322", + "capabilities": ["REIMPORT_DIFF_AND_SOURCE_BINDING", "FULL_STORY_SNAPSHOT", "RESTORE_OR_FORK", "DOCX_EPUB_MARKDOWN_TXT_DELIVERY"] + } + ], + "acceptance_evidence": { + "native_engine_tests": "PASS", + "real_novel": "504_CHAPTERS_IMPORTED_ORGANIZED_AND_EPUB_EXPORTED", + "real_outline": "50_CHAPTERS_IMPORTED_ORGANIZED_AND_DOCX_TXT_EXPORTED", + "real_script": "75_EPISODES_IMPORTED_ORGANIZED_AND_JSON_EXPORTED", + "desktop_install_mount_self_test": "PASS_BASE_AND_ALL_FOUR_ADVANCED_MODULES_THROUGH_SHARED_SIGNED_RUNTIME", + "desktop_restart_readback": "PASS_5_ACTIVE_MODULES_ACCEPTANCE_WORK_CHAPTER_SCENE_GRID_FIELD_AND_TIMELINE", + "current_account_visible_novel_import": "504_CHAPTERS_1082978_WORDS_OPENED_IN_SIGNED_DESKTOP_APP", + "prominent_create_chapter_entry": "PASS_SHORT_DRAMA_LABEL_NEW_EPISODE_LONG_AND_SHORT_NOVEL_LABEL_NEW_CHAPTER", + "unmount_preserves_data_and_receipts": "PASS_BY_SHARED_RUNTIME_AND_RESTART_READBACK", + "signed_desktop_bundle": "APPLE_DEVELOPER_ID_825A9L3G7Q", + "remote_marketplace_publication": "PENDING_OFFICIAL_REPOSITORY_RELEASE" + }, + "publication_gate": [ + "SOURCE_AND_LICENSE_REVIEW", + "NATIVE_OR_AUDITED_ADAPTER_IMPLEMENTATION", + "AUTOMATED_ENGINE_TESTS", + "REAL_NOVEL_OUTLINE_AND_SCRIPT_FIXTURE_ACCEPTANCE", + "DESKTOP_INSTALL_MOUNT_RUN_RESTART_AND_UNINSTALL_ACCEPTANCE", + "PERMISSION_DATA_EXPORT_ROLLBACK_AND_RECEIPT_ACCEPTANCE", + "IMMUTABLE_RELEASE_BUILD_AND_SIGNATURE", + "THEN_ASSIGN_PERMANENT_MODULE_NUMBER", + "THEN_REGISTER_OFFICIAL_MODULE_REGISTRY", + "THEN_PUBLISH_OFFICIAL_MARKETPLACE" + ], + "channel_deployment_flow": [ + "AUTHOR_EXPRESSES_NEED", + "PERSONA_SEARCHES_OFFICIAL_REGISTRY", + "PERSONA_EXPLAINS_MODULE_PERMISSION_DATA_AND_RESOURCE_BOUNDARY", + "HUMAN_CONFIRMS_WHEN_BOUNDARY_REQUIRES", + "RESOLVE_MODULE_NUMBER_AND_PINNED_VERSION", + "FETCH_IMMUTABLE_RELEASE_ARTIFACT", + "VERIFY_SOURCE_SIGNATURE_HASH_DEPENDENCIES_AND_COMPATIBILITY", + "INSTALL_TO_LOCAL_CACHE", + "MOUNT_IN_CURRENT_CHANNEL", + "RUN_MODULE_SELF_TEST", + "WRITE_INSTALLATION_AND_RUNTIME_RECEIPT", + "ROLL_BACK_ON_FAILURE" + ], + "deployment_experience": { + "warm_or_small_module_target_seconds": 30, + "target_is_unconditional_guarantee": false, + "depends_on": ["ARTIFACT_SIZE", "NETWORK", "CACHE", "DEPENDENCY_STATE", "SELF_TEST_DURATION"], + "already_installed_module_offline_start_allowed": true + }, + "data_boundary": { + "one_story_graph_for_builtin_and_modules": true, + "module_program_and_user_story_data_separated": true, + "unmount_preserves_story_data": true, + "uninstall_preserves_story_data_and_receipts": true, + "module_install_grants_all_channel_data": false + }, + "sources": [ + "source://current-dialogue/2026-08-18/bingshuo-light-author-workbench-built-in-and-complete-modules-in-official-marketplace", + "REPO-012:gls/GLS-0233-GH-AIOS-MODULAR-AI-OPERATING-PLATFORM-AND-FAIR-ECOSYSTEM.hdlp", + "REPO-012:gls/GLS-0236-PERSONA-BRAIN-HANDS-VISIBLE-EXECUTION-RECURSIVE-MEMORY-AND-HOTPLUG-RUNTIME.hdlp", + "REPO-012:gls/GLS-0241-HOLOLAKE-SOURCE-OWNERSHIP-AND-DEPLOYMENT-ROUTING.hdlp", + "REPO-012:gls/GLS-0245-HOLOLAKE-LANGUAGE-PERSONA-OPERATING-SYSTEM-PRODUCT-MAPPING.hdlp" + ] +} diff --git a/product-source/hololake-native-desktop/contracts/web-novel-workspace.json b/product-source/hololake-native-desktop/contracts/web-novel-workspace.json new file mode 100644 index 000000000..69fb77932 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/web-novel-workspace.json @@ -0,0 +1,131 @@ +{ + "schema": "hololake.web-novel-workspace/v1", + "record_id": "HLP-WEBNOVEL-WORKSPACE-001", + "state": "SIGNED_BASE_AND_FOUR_ADVANCED_MODULES_ADMITTED_RESTART_ACCEPTED", + "domain_entry": "BRANCH_DOMAIN", + "industry_key": "WEB_NOVEL", + "industry_number": "IND-WEBNOVEL-001", + "channel_id": "GH-WEBNOVEL-INIT-001", + "ontology_correction": { + "channel_body_contract": "contracts/user-channel-body.json", + "marketplace_plan_contract": "contracts/web-novel-module-marketplace-plan.json", + "this_contract_is": "BUNDLED_SIGNED_LIGHT_AUTHOR_WORKBENCH_AND_FOUR_OFFICIAL_NUMBERED_ADAPTERS", + "this_contract_is_not": "USER_CHANNEL_BODY", + "legacy_channel_id_semantics": "DEPRECATED_INDUSTRY_PROJECTION_IDENTIFIER", + "author_editor_operator_are_ui_tabs": false, + "shared_story_graph_copies": 1 + }, + "user_channel_entry": { + "current_domain": "FIFTH_DOMAIN", + "current_channel": "HEARTBEAT_CORE_CHANNEL", + "display_name": "作者工作台", + "routes_to_same_native_workspace": true, + "duplicates_account_story_data": false + }, + "distribution_boundary": { + "preinstalled": "SIGNED_PACKAGE_ARTIFACTS_WITH_EXPLICIT_FIRST_ACTIVATION", + "complete_author_features": "FOUR_INDEPENDENT_NUMBERED_OFFICIAL_MODULES_ACTIVE_IN_SHARED_RUNTIME", + "current_advanced_features_are_registered_marketplace_modules": true, + "hololake_bundles_entire_web_novel_world": false + }, + "native_storage": { + "owner": "HOLOLAKE_NATIVE_RUST_CORE", + "engine": "SQLITE", + "authenticated_account_required": true, + "cross_account_projection_allowed": false, + "restart_readback_required": true, + "source_manuscript_mutation_allowed": false + }, + "work_objects": [ + "WORK", + "VOLUME", + "CHAPTER", + "CHAPTER_VERSION", + "CHECKPOINT", + "STORY_ENTITY", + "STORY_RELATION", + "FORESHADOW", + "EDITOR_REVIEW_NOTE", + "WORKFLOW_EVENT", + "AUTHORIZED_METRIC" + ,"SCENE" + ,"BEAT" + ,"STORY_GRID_FIELD" + ,"TIMELINE_EVENT" + ,"SCENE_ENTITY_LINK" + ,"WRITING_ACTIVITY" + ,"INSPIRATION" + ,"SHOT" + ], + "writing_shapes": [ + "LONG_NOVEL", + "SHORT_NOVEL", + "SHORT_DRAMA" + ], + "chapter_workflow": { + "states": [ + "DRAFT", + "SELF_REVIEW", + "EDITOR_REVIEW", + "REVISION_REQUIRED", + "APPROVED", + "SCHEDULED", + "PUBLISHED" + ], + "human_confirmed_transitions_only": true, + "unreviewed_auto_publish_allowed": false + }, + "required_real_engines": [ + "CREATE_AND_READ_WORK", + "VOLUME_AND_CHAPTER_TREE", + "DEBOUNCED_CHAPTER_PERSISTENCE", + "OPTIMISTIC_REVISION_CONFLICT", + "CHAPTER_VERSION_HISTORY", + "CREATE_AND_RESTORE_CHECKPOINT", + "STORY_BIBLE_AND_RELATIONS", + "FORESHADOW_LIFECYCLE", + "CONTINUITY_AUDIT", + "EDITORIAL_WORKFLOW_AND_REVIEW_NOTES", + "AUTHORIZED_OPERATIONS_METRICS", + "MARKDOWN_EXPORT", + "SHARED_SIGNED_MODULE_PACKAGE_HASH_VERIFICATION", + "SHARED_MODULE_INSTALL_MOUNT_SELF_TEST_UNMOUNT", + "SHARED_HASH_CHAINED_LIFECYCLE_RECEIPT", + "SCENE_BEAT_AND_OUTLINE_TRACKING", + "MULTIDIMENSIONAL_STORY_GRID", + "TIMELINE_AND_SCENE_ENTITY_LINKS", + "CHAPTER_VERSION_RESTORE", + "TXT_DOCX_EPUB_JSON_DELIVERY", + "PROMINENT_CREATE_CHAPTER_OR_EPISODE", + "SHORT_DRAMA_SCREENPLAY_TEMPLATE_ON_CREATE", + "AUTO_FORMAT_ON_IMPORT_WITH_SOURCE_VERSION_PRESERVED", + "LIVE_WORD_COUNT_AND_REAL_EDITING_TIME", + "ONE_CLICK_FORMAT_WITH_NEW_CHAPTER_VERSION", + "SELECTABLE_CHAPTER_OR_WHOLE_WORK_FORMAT_PRESET", + "SYNCHRONIZED_OUTLINE_SOURCE_AND_STRUCTURED_RENDER", + "INSPIRATION_CAPTURE_AND_STATUS", + "FULL_TEXT_SEARCH_AND_TRACKING", + "SHORT_DRAMA_SHOT_AND_PROMPT_STORAGE" + ], + "forbidden_substitutes": [ + "HARDCODED_STATIC_PROJECTS", + "UI_ONLY_BUTTONS_WITHOUT_NATIVE_COMMANDS", + "PRIVATE_MANUSCRIPT_MODEL_TRAINING", + "THIRD_PARTY_AUTO_LOGIN", + "UNREVIEWED_AUTO_PUBLISH", + "PLATFORM_DETECTION_EVASION" + ], + "current_acceptance": { + "state": "PASS", + "numbered_ipc_modules": ["HLP-NIPC-MOD-0025", "HLP-NIPC-MOD-0026", "HLP-NIPC-MOD-0027", "HLP-NIPC-MOD-0028", "HLP-NIPC-MOD-0029"], + "numbered_operations": "HLP-NIPC-OP-0103..HLP-NIPC-OP-0140", + "runtime_module_numbers": ["HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"], + "node_tests": "130_PASS", + "rust_tests": "170_PASS_2_EXPLICIT_DESKTOP_FIXTURES_IGNORED", + "clippy": "PASS_DENY_WARNINGS", + "developer_id_team": "825A9L3G7Q", + "signed_binary_sha256": "2f8fdd71f9d5a9178bc23058261dbb43f1388486dc5a4415e865cfcf78869db3", + "restart_readback": "PASS_5_MODULES_ACTIVE_ACCEPTANCE_WORK_1_CHAPTER_53_WORDS_SCENE_GRID_FIELD_TIMELINE_PRESENT", + "legacy_account_readback": "PASS_504_CHAPTER_1082978_WORD_NOVEL_50_CHAPTER_OUTLINE_75_EPISODE_SCRIPT_UNCHANGED" + } +} diff --git a/product-source/hololake-native-desktop/contracts/zero-core-numbering-kernel.json b/product-source/hololake-native-desktop/contracts/zero-core-numbering-kernel.json index 1a4b20a9a..724502875 100644 --- a/product-source/hololake-native-desktop/contracts/zero-core-numbering-kernel.json +++ b/product-source/hololake-native-desktop/contracts/zero-core-numbering-kernel.json @@ -3,7 +3,7 @@ "record_id": "HLP-ZERO-CORE-NUMBERING-KERNEL-001", "authority": { "repository": "REPO-012", - "source_commit": "2598fbfba8caf64c7ab9740a3036c5aab977502e", + "source_commit": "104a5d73162bdf4a529701e65898e2bc2863ea9e", "source_path": "routing/guanghu-identity-authority-map.json", "map_id": "GH-IDENTITY-AUTHORITY-MAP-001", "map_version": "2026-08-10.1", diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index 0035bb5f0..3b757ccb8 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -90,10 +90,20 @@ HoloLake 0.3.0 includes a live, read-only projection of the PNCC resident runtim ## Release pipeline -`npm run release:macos -- release/inputs/.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts. +`npm run release:macos -- release/inputs/.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v` tag equals the clean `main` head, and the Developer ID plus Tauri updater-signing material are supplied at runtime. Apple notarization can run either through Tauri's Apple ID/API credential flow, or through the two-step Xcode Organizer flow already owned by the local Apple developer account: append `prepare-xcode` to build, verify the updater signature, and create a source-hash-bound `.xcarchive`; after Xcode reports `Ready to distribute`, export the notarized app and append `finalize-xcode ` to bind the exported executable back to that archive, require the app's stapled ticket and Gatekeeper acceptance, regenerate and sign the updater archive, create a Developer ID-signed DMG containing that notarized app, and write the release broadcast and receipts. A protected updater-key path is materialized only into child processes; neither the private key nor its password is printed or copied into source. The Xcode flow does not claim that the outer DMG itself has an Apple ticket unless its own Gatekeeper and stapler checks pass. Generated packages, private release inputs and credentials are not committed. The pipeline never uploads or activates a release; its terminal artifact is a bounded folder ready for a separately authorized JD-controller upload and server-owned readback receipt. +## Numbered-root module admission + +HoloLake 0.5.0 is the clean numbered-root base. The compiled 0.4.1 desktop application and the divergent dirty source worktree are read-only donors, not merge bases. Their old numbered-operation runtime is explicitly superseded, and mutations to shared files such as `src/main.tsx` or `src-tauri/src/lib.rs` are never accepted as a unit. + +Each donor capability receives a candidate coordinate, but no permanent runtime module number, until one isolated admission cycle has reviewed provenance and permissions, allocated numbered IPC module/target/operation coordinates, implemented an adapter without raw Tauri invoke, passed negative-route and data tests, and produced installed mount, restart, unmount and rollback receipts. The admission order and candidate inventory are recorded in `contracts/module-donor-admission-registry.json`. + +The module-package runtime is now the shared admission executor. It accepts an exact detached-minisign `.ghmod` artifact, validates the package and its compatibility/permission manifest, stores it inside the authenticated account, and advances only through numbered install, mount, self-test, unmount and rollback operations. Lifecycle state and receipts are durable SQLite records; unmount never removes user data. A package is declarative and selects a host-registered adapter: repositories, native binaries and arbitrary webview JavaScript are not executable module inputs. Public lighthouse numbers remain unavailable until a candidate completes its own installed acceptance; private channel packages use a separate local number class. + +The admitted web-novel family uses that one lifecycle rather than the donor's private installer. Its signed base module owns account-local works, volumes, chapters, versions, story objects, editorial workflow, import and author activity. Outline, story-grid, story-world and delivery are four separately signed official numbers; each advanced mutation checks its own exact `ACTIVE` record before touching the shared story graph. The donor's four legacy manifests remain byte-exact test fixtures only and have no numbered IPC route. Installed acceptance reopened the existing 504-chapter novel, 50-chapter outline and 75-episode script in place, then created a separate one-chapter acceptance work, scene, grid field and timeline event and read all of them back after process restart. + ## Stage-one convergence verdict The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass. diff --git a/product-source/hololake-native-desktop/docs/GLS-NATIVE-RUNTIME-IMPLEMENTATION-PLAN-20260817.md b/product-source/hololake-native-desktop/docs/GLS-NATIVE-RUNTIME-IMPLEMENTATION-PLAN-20260817.md index 8d54e112f..adeb60b53 100644 --- a/product-source/hololake-native-desktop/docs/GLS-NATIVE-RUNTIME-IMPLEMENTATION-PLAN-20260817.md +++ b/product-source/hololake-native-desktop/docs/GLS-NATIVE-RUNTIME-IMPLEMENTATION-PLAN-20260817.md @@ -7,7 +7,7 @@ 线上事实源: - 第五域代码频道:`bingshuo/guanghu-ice-heart` -- REPO-012 `main`:`2598fbfba8caf64c7ab9740a3036c5aab977502e` +- REPO-012 `main`:`d5b1111fcaccaccf025070e531631f2b3cbb00cd` - Git tree:`5a09f084fffee56d90599e69038c8335871ea04f` - 第五域节点:`JD-FD-PRIMARY` - 公开远端 HEAD 与第五域 Forgejo 裸仓库 HEAD:一致 diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod new file mode 100644 index 000000000..54146a5ea --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod @@ -0,0 +1,29 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "频道文档与表格工作台", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "channel-workbench-v1", + "contentDigest": "2ee8cbb20e4c3242a8ee3469f2f87347967bde8878463746e61ea8e2369c5074", + "permissions": ["CHANNEL_DOCUMENT_READ", "CHANNEL_DOCUMENT_WRITE", "CHANNEL_SPREADSHEET_READ", "CHANNEL_SPREADSHEET_WRITE"], + "userDataSchema": "hololake.module-data/channel-workbench/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "2ee8cbb20e4c3242a8ee3469f2f87347967bde8878463746e61ea8e2369c5074" + } + }, + "payload": { + "entry": "channel-workbench", + "adapterConfig": { + "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "documentEngine": "LEXICAL_0_49", + "spreadsheetEngine": "FORTUNE_SHEET_1_0_4", + "maximumColumns": 64, + "maximumRows": 5000, + "persistent": true + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..9cdacbd95 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNiU1ZIVE10TkRyZHhCU1oycjJCMnlVV3hCRGM2QmJSZ0hlZGlPejY0TER2MkE0Q3U3Y1g5dHVnSjBzVytOallYN3hKQVNJcE9Pblowd3lpTXh1V2dBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc1OTcwCWZpbGU6SExQLU1PRC1MT0NBTC1DSEFOTkVMLVdPUktCRU5DSC0wMDAxLTAuMS4wLmdobW9kCkVKbFN4ZXBCbDBCZlJDcEhmYmZtQU1iTXN4djZVVGl0MngwOW5NRzBLSDArK2FRNEV2ZmVObzZRZWFWNmZVOXV3TnErcHM1MGI5K1lHMXpBUmxYVkR3PT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod new file mode 100644 index 000000000..6ffbee969 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod @@ -0,0 +1,26 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "原生组合视图", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "native-composition-v1", + "contentDigest": "8e9c6ccf179bb256d5b1006e41b4984a86e0382b7023ae8435c201804041393e", + "permissions": ["KNOWLEDGE_READ"], + "userDataSchema": "hololake.module-data/native-composition/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "8e9c6ccf179bb256d5b1006e41b4984a86e0382b7023ae8435c201804041393e" + } + }, + "payload": { + "entry": "knowledge-composition", + "adapterConfig": { + "dataSource": "CURRENT_AUTHENTICATED_ACCOUNT_KNOWLEDGE_CATALOG", + "readOnly": true, + "registeredViews": ["DASHBOARD", "COMPARISON", "VERTICAL_BAR", "CLASSIFICATION", "TABLE"] + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..4573253a4 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTRyUS9Nd0E0NE53dGRYODFxeWlzcW1XanN6K2EzQUNXSVVmT1ZLY2tRNExwaXlZdnJvSjlFZU5lUTJhb25hUVhORDQ0ZmFURmNSWTZBUmRTQnBiNFFvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc0NDA3CWZpbGU6SExQLU1PRC1MT0NBTC1OQVRJVkUtQ09NUE9TSVRJT04tMDAwMS0wLjEuMC5naG1vZApDclRUZDNpeENBSDQ0a0ZnR3VaNkxIS1lWUUhoMEdWajVqL0JzVVg4dStLb1ZJYXZlNHBaQy9BZlMxMkxreVQ0ODFzeGFMSWZrVUppUVY2NFN5TndDUT09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod new file mode 100644 index 000000000..b3a2e86d9 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod @@ -0,0 +1,27 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "人格频道本体与成长", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "persona-channel-body-v1", + "contentDigest": "27495883fb3602fd070d900d2157fa684e1d0bedde65642810326ebcf4ff0040", + "permissions": ["PERSONA_BODY_READ", "PERSONA_TRIAL_LIFECYCLE_WRITE", "PERSONA_LANGUAGE_CONTRACT_WRITE", "PERSONA_LANGUAGE_APPEND", "CHANNEL_GROWTH_READ", "CHANNEL_GROWTH_EVENT_APPEND", "CHANNEL_GROWTH_SHARING_WRITE"], + "userDataSchema": "hololake.module-data/persona-channel-body/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "27495883fb3602fd070d900d2157fa684e1d0bedde65642810326ebcf4ff0040" + } + }, + "payload": { + "entry": "persona-channel-body", + "adapterConfig": { + "trialDays": 30, + "languageLedger": "APPEND_ONLY_SHA256", + "growthProjection": "MINIMIZED_LOCAL_METADATA", + "personaBindingClaimed": false + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..359d5239b --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRXd2QktvaXdZZWx5eEw0anZrUVNuMU5tRy9HTE9jOEhrcUhET3VFTml4ZUlBODk2aDYvNDhhUVVpbU0wTGtENVlZOExCZ1BnVW1vNElxbVhwT1RZWlFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc3NjQxCWZpbGU6SExQLU1PRC1MT0NBTC1QRVJTT05BLUNIQU5ORUwtQk9EWS0wMDAxLTAuMS4wLmdobW9kCjh6OFRiUHhoR1g1eVNoN1lvNU5wTlNnbXkwVEtvcWVwU0ZsSTlWcmpIT1FKK0ppelBBK2V2anMrQkRyQjRrVXFtdkR2L0g5K1c2UFhJZ1owRitpd0RnPT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod new file mode 100644 index 000000000..db17e9e33 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod @@ -0,0 +1,36 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "原生组合模块运行时验收包", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "native-composition-v1", + "contentDigest": "07bfafaaa0b06f160abc489edce2bc23a3b092520c8d6c6f4a5ce29cca4485ea", + "permissions": [ + "KNOWLEDGE_READ", + "PROJECTION_RENDER" + ], + "userDataSchema": "hololake.module-data/native-composition/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "07bfafaaa0b06f160abc489edce2bc23a3b092520c8d6c6f4a5ce29cca4485ea" + } + }, + "payload": { + "adapterContract": "hololake.native-composition-v1", + "capabilities": [ + "READ_CURRENT_ACCOUNT_KNOWLEDGE", + "PROJECT_SHARED_EXECUTION_RESULT" + ], + "entry": "native-composition", + "views": [ + "DASHBOARD", + "COMPARISON", + "VERTICAL_BAR", + "CLASSIFICATION", + "TABLE" + ] + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..92e83cab4 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRThwcUdLRThMYkJLN29MM1NmVFVCcmh4dGdrbHdPVWlvdm45enpveUhBeFZ3VEM1TkNHRDBKdGFoN21OZHgzV0NSMFVvMk1TNW44Z3BtRS9GcmN5SHdvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDczNTkxCWZpbGU6SExQLU1PRC1MT0NBTC1SVU5USU1FLUFDQ0VQVEFOQ0UtMDAwMS0wLjEuMC5naG1vZApXRWZINVdEUG12SlNsdzRTMkFHRDV3V0NkSVhNYW92RmczRGZlU3M0ZU9nSnhjc1BWbU1ZS0x1cStoQXE5TE96cnV1S2FaQ1FnK0FGdFNEY1RvUGZBQT09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod new file mode 100644 index 000000000..c650fc4a9 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod @@ -0,0 +1,33 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001", + "registrationClass": "OFFICIAL_LIGHTHOUSE", + "displayName": "动态语言世界视觉层", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "dynamic-language-world-surface-v1", + "contentDigest": "ed335b587af53365b9a9143ab5b5e7e3fed3f517cd0ef0c9e36a523db58511e9", + "permissions": ["REALITY_TIME_READ", "PUBLIC_WEATHER_QUERY"], + "userDataSchema": "hololake.module-data/dynamic-world-surface/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "ed335b587af53365b9a9143ab5b5e7e3fed3f517cd0ef0c9e36a523db58511e9" + } + }, + "payload": { + "entry": "dynamic-world-surface", + "adapterConfig": { + "dynamicInputs": ["BEIJING_TIME", "VERIFIED_CURRENT_WEATHER"], + "weatherProvider": "OPEN_METEO", + "weatherCacheTtlSeconds": 600, + "realCityExposedInUi": false, + "coordinatesExposedInUi": false, + "layoutMode": "OFFICIAL_FIVE_LAKE_EXISTING_SHELL", + "themes": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"], + "weatherUnavailableBehavior": "BEIJING_TIME_ONLY_NO_FAKE_WEATHER", + "routingPermissionOrFactMutation": false, + "legacyTraditionalWorkbench": "REJECTED_DUPLICATE_SHELL_AND_SIMULATED_STATE" + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..489c5f2d3 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTF2cGVLbjVsWWRTdEF2L0FiZHBKUS9hVDdwNjRmRlRPc0xHTGJzbXFyR09tZysrcGNDYWN4eEJ3aGtCNGtURDRjNWg1ZWs5bU5YdDc0SWhBYWNsUEFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDg0NTk2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1EWU5BTUlDLVdPUkxELVNVUkZBQ0UtMDAwMS0wLjEuMC5naG1vZApHVGFucE8vYjliVUlsbSt6STRWTEcrN2tIQzNTSTgrUzlwdVdQUlY5WG8vQytzTEU1VURYMVA5Q3BCNVVwVFBvbnZheWxIbVUrWGQ2K3ZicGgwR3hEdz09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod new file mode 100644 index 000000000..ed4c14ba8 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod @@ -0,0 +1,33 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", + "registrationClass": "OFFICIAL_LIGHTHOUSE", + "displayName": "教育行业工作台", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "education-workbench-v1", + "contentDigest": "fd80c9957865a58cf62679528f110ae355b1385ad1dd988e31217ea3da66e65b", + "permissions": ["EDUCATION_WORKSPACE_READ", "EDUCATION_DOCUMENT_WRITE", "EDUCATION_TABLE_WRITE", "EDUCATION_IMPORT_READ_FILE", "EDUCATION_EXPORT_WRITE_FILE", "EDUCATION_IMPORT_ASSIGNMENT_WRITE", "EDUCATION_AUTOMATION_WRITE", "EDUCATION_AUTOMATION_EXECUTE"], + "userDataSchema": "hololake.module-data/education-workbench/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "fd80c9957865a58cf62679528f110ae355b1385ad1dd988e31217ea3da66e65b" + } + }, + "payload": { + "entry": "education-workspace", + "adapterConfig": { + "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "foundationModule": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "documentEngine": "LEXICAL_0_49", + "spreadsheetEngine": "FORTUNE_SHEET_1_0_4", + "importDefaultScope": "UNASSIGNED", + "automationExecution": "PREVIEW_TOKEN_AND_HUMAN_CONFIRMATION", + "modelFileTransferDefault": "DENY", + "maximumColumns": 30, + "maximumRows": 1000, + "persistent": true + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..11d79a513 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTFEa084d1l6MnB2OFVvVy9abW90S3RFMEVOdmIvWW1hbkllWmw4NHBJaDVNTGxpZHV4M0tVNUNUOExmdHAwSGw4V0NPQ1RPUjlRU0h0Tm50clFlekFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc5ODQzCWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1FRFVDQVRJT04tV09SS0JFTkNILTAwMDEtMC4xLjAuZ2htb2QKVXZzVklwQk5KR3Q3bkRZaXBYV1ArOGx6b3BKSWVyUkpvK1dyTXdpWnBhMXNqVnJKcU5vZ1BTdzlFWjlXRTJ1RlFhMXAwMjhXcFBEQnQxRzV4SFVuQkE9PQo= \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod new file mode 100644 index 000000000..dc973c2b7 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod @@ -0,0 +1,34 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001", + "registrationClass": "OFFICIAL_LIGHTHOUSE", + "displayName": "移动同步桥", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "mobile-sync-v1", + "contentDigest": "3e3ff8c7beba3d70ee01c918630a40ea901313b424f264b41e2d376550534e6e", + "permissions": ["MOBILE_SYNC_LISTEN_SAME_LAN", "MOBILE_SYNC_PAIR_DEVICE", "MOBILE_SYNC_READ_MINIMUM_PROJECTION", "MOBILE_SYNC_CAPTURE_INBOX_WRITE", "MOBILE_SYNC_REVOKE_DEVICE"], + "userDataSchema": "hololake.module-data/mobile-sync/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "3e3ff8c7beba3d70ee01c918630a40ea901313b424f264b41e2d376550534e6e" + } + }, + "payload": { + "entry": "mobile-sync", + "adapterConfig": { + "transport": "SAME_LAN_DIRECT_HTTP_WITH_APPLICATION_LAYER_ENCRYPTION", + "preferredPort": 37421, + "pairingTtlSeconds": 600, + "maximumRequestBytes": 262144, + "maximumConcurrentConnections": 16, + "desktopRole": "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE", + "mobileRole": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM", + "desktopOfflineExecution": false, + "platformPrivatePayloadCustody": false, + "automaticDomainMutation": false, + "iosClientPackaging": "PENDING_SEPARATE_ADMISSION" + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..fabf73a24 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTV5bDhHMkZjMzNkN0lpaDBLaDRxcnhHajJ3b3NDS21zZWljYWJORmtrNit5MHJvZC9yN2YrSUhiSE5sT0dMd25Jc2pXMkQyeWttODhKSklsRmVKNWd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgzMTYwCWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1NT0JJTEUtU1lOQy0wMDAxLTAuMS4wLmdobW9kCjN6R1JZZitSbzI1cVRESkNUMnJrVXFWUlUyZ3B3L2lIQmhrOXZaTGc3T2x1REdNUUZrK2M4WWs4bE9Zb3NuSTNoNmRBRmlCekh0dkNNN1pKR2k4ZkFRPT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod new file mode 100644 index 000000000..651c346b1 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod @@ -0,0 +1,7 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "高级版本与交付", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "1dbc2ee8c1bc4c361009a8db9c93774c1ac7f5a06c02a5d4bc5b39754a0496fb", "permissions": ["WEB_NOVEL_CHAPTER_VERSION_RESTORE", "WEB_NOVEL_DELIVERY_EXPORT_WRITE_FILE"], "userDataSchema": "hololake.module-data/web-novel-delivery/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "1dbc2ee8c1bc4c361009a8db9c93774c1ac7f5a06c02a5d4bc5b39754a0496fb" } + }, + "payload": { "entry": "web-novel-delivery", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["CHAPTER_VERSION_RESTORE", "TXT_EXPORT", "DOCX_EXPORT", "EPUB_EXPORT", "JSON_EXPORT"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "humanConfirmationRequiredForRestore": true, "persistent": true } } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..735b86b00 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTBrb01WSENhMmIvUGxyRnozY2k0aTVaQ1lFL3VLdWllSURCak54VWwrM2h1QXM4Sk10ZGc0Qm1EYzhhUVJVTFB6OXRqLzc5cSt3U0NleS9WR2owN3djPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE1CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtREVMSVZFUlktMDAwMS0wLjEuMC5naG1vZApQWWFETDdISGFvbGdtVVZCK0V5VUVqNlFvSGRmQWVuY3dSRS9ZWm1UVHhCS0U4bHNCQzl0RFFtbDZEUXREWnp6MjQxbHF1bVgwS0NhS1ZBUGZxd3ZBUT09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod new file mode 100644 index 000000000..54d60a93c --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod @@ -0,0 +1,7 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "多维情节表与情节板", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "61bf4969e97003f203af5e0b38bb37024847cb574a3ac09025f10586061805ce", "permissions": ["WEB_NOVEL_FIELD_DEFINITION_WRITE", "WEB_NOVEL_FIELD_VALUE_WRITE"], "userDataSchema": "hololake.module-data/web-novel-grid/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "61bf4969e97003f203af5e0b38bb37024847cb574a3ac09025f10586061805ce" } + }, + "payload": { "entry": "web-novel-grid", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["FIELD_DEFINITION_WRITE", "FIELD_VALUE_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..0e6868c55 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTBuWjBmYWczdEg3QXByZS9RaEY4N2x6RXN1WVY2ZEJGSmdSYmZyTGUrclVvYm1sd0t4REhOY1dVdW1EeGtramhqeSs4YU9nQVNBbkdCWVlEYmNteXdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE1CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtR1JJRC0wMDAxLTAuMS4wLmdobW9kCkJLczNMbmpqYzVTdXQ5RklHMXdlMWpDcE9tRnlBYWlPcUJkMzZQbFlRSzFYVDFtNmYrZ1htN1VqZWJkaktUWmZ5VVZvczZ4YmVrWHRuQ3lNOXU0SkFBPT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod new file mode 100644 index 000000000..a2048eabe --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod @@ -0,0 +1,7 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "作品结构与大纲追踪", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "7da54b0220940a8a19351ff9bf3cd177c93fc25c533329a60926b8ea245dafbe", "permissions": ["WEB_NOVEL_SCENE_WRITE", "WEB_NOVEL_BEAT_WRITE"], "userDataSchema": "hololake.module-data/web-novel-outline/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "7da54b0220940a8a19351ff9bf3cd177c93fc25c533329a60926b8ea245dafbe" } + }, + "payload": { "entry": "web-novel-outline", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["SCENE_WRITE", "BEAT_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..32b0cbc03 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNUYVhwVTlzdWt6VkVQYTUyanB6UFlYeU5BdW1qN0xuM1haWjlCYUU3UFlMRTdPZG90YVZvNGFyaVo4UHY0THowUWlPVzIzemYrQWNZbDFvRVd5NndrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtT1VUTElORS0wMDAxLTAuMS4wLmdobW9kCkZob1VsQlkwZFRyZXI2ck1OckhUc2ZqaHVJOWhSQnIraUFRSEllU3BxRlc3UStFekJRZDFMZFpmZlBvK09jTFJ6cERpcVl2M2N2emIvS0ptdnRrTkR3PT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod new file mode 100644 index 000000000..539076437 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod @@ -0,0 +1,7 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "时间线与故事资料库", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "9627949a975ce489dde80de52c538a57bb73b87e31ddb1e5efb48cee53922b71", "permissions": ["WEB_NOVEL_TIMELINE_WRITE", "WEB_NOVEL_SCENE_ENTITY_LINK_WRITE"], "userDataSchema": "hololake.module-data/web-novel-storyworld/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "9627949a975ce489dde80de52c538a57bb73b87e31ddb1e5efb48cee53922b71" } + }, + "payload": { "entry": "web-novel-storyworld", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["TIMELINE_WRITE", "SCENE_ENTITY_LINK_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..000bbdb81 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRXpuRmFPWjJUSDVpSmQzQXl0aVF3N0xhRkRvVWhLalVtcDBnRFprNnpOUEdCWS9CTURlUHBrYWkyRG9MUFZwVEtHc0w5ZUltRlNUVlZBcm1vU2VnUlFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtU1RPUllXT1JMRC0wMDAxLTAuMS4wLmdobW9kCnBzQkJYaTV6NFR2Vnk3NUdpK3JSQnJTUzhSd2hxUWdmM0UrcVZuN0VuS0JOc1lYclY5Y3FBbVJldVM1Y2FGamNtT3NiU0NsaThMRE5qT3VvTXJUUUJ3PT0K \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod new file mode 100644 index 000000000..9776e90b5 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod @@ -0,0 +1,26 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", + "registrationClass": "OFFICIAL_LIGHTHOUSE", + "displayName": "网文作者工作台", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "web-novel-workbench-v1", + "contentDigest": "df03a94751cd50ea26e2fc0f68ac21bd55e62f2034ff83c562a840c8aab72d2e", + "permissions": ["WEB_NOVEL_WORKSPACE_READ", "WEB_NOVEL_WORKSPACE_WRITE", "WEB_NOVEL_IMPORT_READ_FILE", "WEB_NOVEL_AUTHOR_ACTIVITY_WRITE", "WEB_NOVEL_STORY_BIBLE_WRITE", "WEB_NOVEL_EDITORIAL_WORKFLOW_WRITE", "WEB_NOVEL_OPERATIONS_WRITE", "WEB_NOVEL_CHECKPOINT_WRITE"], + "userDataSchema": "hololake.module-data/web-novel-workbench/v1", + "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "df03a94751cd50ea26e2fc0f68ac21bd55e62f2034ff83c562a840c8aab72d2e" } + }, + "payload": { + "entry": "web-novel-workspace", + "adapterConfig": { + "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "legacyDataPolicy": "READ_IN_PLACE_NO_DESTRUCTIVE_MIGRATION", + "workspaceFeatures": ["WORKS", "VOLUMES", "CHAPTERS", "ENTITIES", "EDITORIAL", "OPERATIONS", "CHECKPOINTS", "IMPORT"], + "advancedModules": ["HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"], + "thirdPartyPublishDefault": "DENY", + "persistent": true + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..3ad7f68ae --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTZyTUlUYWdZZXk3bFhDRTJBWElWMkVEQXZHa3dSWDFXSFJ2QjduVFk1bzFLUk5kOVFQamIwU2w1L3FzOWtibm9Kblg2NWJLWFdVeEtEbDNzTEltVmdFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE3CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtV09SS0JFTkNILTAwMDEtMC4xLjAuZ2htb2QKV0VqV3B4cFNLRng2UUcrNXlYZjY5SytERk14YTNyRkM1bldzUXo5RWpzMU0vRmVJWldvTGhmbmExcFhmSGZiWTFreElreGU2UHVsaXVhK0ZXZnVyQ2c9PQo= \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-DELIVERY-004.json b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-DELIVERY-004.json new file mode 100644 index 000000000..7dccdaf66 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-DELIVERY-004.json @@ -0,0 +1,12 @@ +{ + "schema": "hololake.module-package/v1", + "moduleId": "HL-MOD-WEBNOVEL-DELIVERY-004", + "name": "高级版本与交付", + "version": "1.0.0", + "publisher": "HoloLake Official", + "runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE", + "capabilities": ["chapter_version.read", "chapter_version.restore", "delivery.txt", "delivery.docx", "delivery.epub", "delivery.json"], + "permissions": ["current_account.web_novel.read", "current_account.web_novel.write", "human_selected_export_path.write"], + "dataPolicy": "PROGRAM_AND_USER_DATA_SEPARATED", + "selfTest": "WEBNOVEL_VERSION_READBACK_AND_EXPORT_BUILD" +} diff --git a/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-GRID-002.json b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-GRID-002.json new file mode 100644 index 000000000..12cc21737 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-GRID-002.json @@ -0,0 +1,12 @@ +{ + "schema": "hololake.module-package/v1", + "moduleId": "HL-MOD-WEBNOVEL-GRID-002", + "name": "多维情节表与情节板", + "version": "1.0.0", + "publisher": "HoloLake Official", + "runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE", + "capabilities": ["story_grid.read", "story_grid.write", "custom_field.write", "story_board.group"], + "permissions": ["current_account.web_novel.read", "current_account.web_novel.write"], + "dataPolicy": "ONE_SCENE_GRAPH_NO_DUPLICATE_STORY_STORE", + "selfTest": "WEBNOVEL_GRID_SCHEMA_AND_SHARED_SCENE_PROJECTION" +} diff --git a/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-OUTLINE-001.json b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-OUTLINE-001.json new file mode 100644 index 000000000..f7d53c0c9 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-OUTLINE-001.json @@ -0,0 +1,12 @@ +{ + "schema": "hololake.module-package/v1", + "moduleId": "HL-MOD-WEBNOVEL-OUTLINE-001", + "name": "作品结构与大纲追踪", + "version": "1.0.0", + "publisher": "HoloLake Official", + "runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE", + "capabilities": ["scene.write", "beat.write", "outline.track"], + "permissions": ["current_account.web_novel.read", "current_account.web_novel.write"], + "dataPolicy": "USER_DATA_REMAINS_AFTER_UNINSTALL", + "selfTest": "WEBNOVEL_OUTLINE_SCHEMA_AND_ROUNDTRIP" +} diff --git a/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-STORYWORLD-003.json b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-STORYWORLD-003.json new file mode 100644 index 000000000..c52d77a49 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-STORYWORLD-003.json @@ -0,0 +1,12 @@ +{ + "schema": "hololake.module-package/v1", + "moduleId": "HL-MOD-WEBNOVEL-STORYWORLD-003", + "name": "时间线与故事资料库", + "version": "1.0.0", + "publisher": "HoloLake Official", + "runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE", + "capabilities": ["timeline.read", "timeline.write", "scene_entity.link", "story_bible.read"], + "permissions": ["current_account.web_novel.read", "current_account.web_novel.write"], + "dataPolicy": "USER_DATA_REMAINS_AFTER_UNINSTALL", + "selfTest": "WEBNOVEL_TIMELINE_SCHEMA_AND_RELATION_ROUNDTRIP" +} diff --git a/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/README.md b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/README.md new file mode 100644 index 000000000..30794287b --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/web-novel-legacy-0.4.1/README.md @@ -0,0 +1,6 @@ +# Web novel 0.4.1 lifecycle fixtures + +These four JSON files preserve the exact bytes used by the donor's lifecycle regression tests. +They are not bundled by `module_package_runtime`, are not registered in numbered IPC, and are not +production module identities. HoloLake 0.5.x uses the signed `HLP-MOD-OFFICIAL-WEB-NOVEL-*` +packages in `fixtures/module-packages` as its only runtime authority. diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index 28718d387..91dac9bf5 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -56,9 +56,11 @@ "local_runtime_acceptance_receipt": "audit/local-runtime-acceptance-20260815.json", "domain_membrane_pncc_installed_acceptance_receipt": "audit/hololake-0.4.0-domain-membrane-pncc-installed-acceptance-20260816.json", "updater_bootstrap_installed_acceptance_receipt": "audit/hololake-0.4.1-updater-bootstrap-installed-acceptance-20260817.json", - "installed_desktop_path": "/Applications/HoloLake.app", + "installed_desktop_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app", "old_desktop_apps_recoverably_archived": true, "public_developer_id_and_notarized_acceptance": false, + "local_developer_id_signed_0_5_0_installed_acceptance": true, + "numbered_root_installed_acceptance_receipt": "audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json", "stage_one_implementation_order": [ "PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL", "KNOWLEDGE_TREE_PAGE_SEARCH_AND_LOCAL_PERSISTENCE", @@ -99,8 +101,11 @@ "automatic_restart_allowed": false, "private_signing_material_allowed_in_source": false, "donor_audit_record": "audit/donor-audit.json", - "donor_audit_complete": false, + "donor_audit_complete": true, "donor_source_copying_allowed": false, + "module_donor_admission_registry": "contracts/module-donor-admission-registry.json", + "module_donor_bulk_merge_allowed": false, + "module_donor_one_candidate_per_admission_cycle": true, "stage_one_product_contract": "contracts/stage-one-platform.json", "local_development_bridge_contract": "contracts/local-development-bridge.json", "account_single_writer_kernel_implemented": true, @@ -128,7 +133,7 @@ "pncc_jd_live_server_projection_repository_content_exposed": false, "pncc_jd_live_server_projection_write_authority": false, "pncc_jd_live_server_projection_carrier_state": "UNBOUND_EVIDENCE_REQUIRED", - "installed_local_product_version": "0.4.1", + "installed_local_product_version": "0.5.0", "pncc_authenticated_direct_broker_integration_implemented": true, "pncc_human_mount_registration_implemented": true, "pncc_human_mount_registration_gate": "SATISFIED_NATIVE_FILE_PICKER_EXACT_CONFIRMATION", diff --git a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json new file mode 100644 index 000000000..e315fc1f3 --- /dev/null +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -0,0 +1,3490 @@ +{ + "schema": "hololake.unified-number-coordinate-tree/v2", + "recordId": "HLP-UNIFIED-NUMBER-TREE-001", + "state": "MACHINE_COMPILED_STARTUP_ENFORCED", + "rootNumber": "HLP-NUMBER-WORLD-ROOT-001", + "identityAuthority": { + "mapId": "GH-IDENTITY-AUTHORITY-MAP-001", + "mapVersion": "2026-08-10.1", + "namespaces": [ + { + "namespaceId": "ICE_GL", + "roots": [ + "ICE-GL∞" + ], + "prefixes": [ + "ICE-GL-" + ], + "subjectKind": "FIFTH_DOMAIN_HUMAN", + "domainScope": "FIFTH_DOMAIN" + }, + { + "namespaceId": "ICE_P", + "roots": [], + "prefixes": [ + "ICE-P-" + ], + "subjectKind": "FIFTH_DOMAIN_SYSTEM_PERSONA", + "domainScope": "FIFTH_DOMAIN" + }, + { + "namespaceId": "ICE_BB", + "roots": [], + "prefixes": [ + "ICE-BB-" + ], + "subjectKind": "PRIVATE_BOTTLE_BABY_PERSONA", + "domainScope": "FIFTH_DOMAIN_PRIVATE" + }, + { + "namespaceId": "TCS_GL", + "roots": [], + "prefixes": [ + "TCS-GL-" + ], + "subjectKind": "ZERO_SENSE_HUMAN_CONTROLLER_TEAM_MEMBER", + "domainScope": "ENTERPRISE_FOUR_DOMAINS" + } + ] + }, + "sources": [ + { + "recordId": "HLP-ZERO-CORE-NUMBERING-KERNEL-001", + "sha256": "902d37fa6c82076fec4edaf96957f85b2fab1310d853c2a817ac2f9590f464a7" + }, + { + "recordId": "HLP-NUMBERED-IPC-ROOT-001", + "sha256": "f1aa85668a6d33efeb7ce268cd48e7ed517696db990327f22111359aca8c177c" + }, + { + "recordId": "HLP-NBROKER-ROOT-001", + "sha256": "374c9ade86bc8583f9361542e89ca7bb0bbcbdd99af4a26f9a6a864a12888402" + }, + { + "recordId": "HLP-GLS-RUNTIME-MANIFEST-002", + "sha256": "cf9b1ae0ea8e3fd6a385b8722d2578927704dc674266a685a3d65d2b53b8f903" + } + ], + "invariants": { + "numberIsStableCoordinateNotAuthority": true, + "pathIsUniqueNavigation": true, + "admissionIsSeparateFromIdentity": true, + "everyPhysicalCallHasNumberedRoute": true, + "everyAcceptedCallHasEvidenceClass": true, + "everyProtocolReferenceHasNumberCoordinate": true, + "referenceOnlyNodesNeverExecutable": true, + "unresolvedNumberReferenceCount": 0, + "mismatchedCoordinate": "FAIL_CLOSED" + }, + "coordinateCount": 272, + "routeCount": 169, + "identityNodeCount": 4, + "protocolNodeCount": 99, + "referenceOnlyNodeCount": 16, + "identityNodes": [ + { + "nodeKind": "IDENTITY_NAMESPACE", + "nodeNumber": "HLP-IDENTITY-NS-ICE_BB", + "namespaceId": "ICE_BB", + "subjectKind": "PRIVATE_BOTTLE_BABY_PERSONA", + "domainScope": "FIFTH_DOMAIN_PRIVATE", + "admission": "NON_HUMAN_NAMESPACE", + "executionState": "AUTHORITY_RESOLUTION_ONLY", + "evidence": "GH-IDENTITY-AUTHORITY-MAP-001", + "path": "HLP-NUMBER-WORLD-ROOT-001/IDENTITY/ICE_BB" + }, + { + "nodeKind": "IDENTITY_NAMESPACE", + "nodeNumber": "HLP-IDENTITY-NS-ICE_GL", + "namespaceId": "ICE_GL", + "subjectKind": "FIFTH_DOMAIN_HUMAN", + "domainScope": "FIFTH_DOMAIN", + "admission": "REGISTERED_HUMAN_NAMESPACE", + "executionState": "AUTHORITY_RESOLUTION_ONLY", + "evidence": "GH-IDENTITY-AUTHORITY-MAP-001", + "path": "HLP-NUMBER-WORLD-ROOT-001/IDENTITY/ICE_GL" + }, + { + "nodeKind": "IDENTITY_NAMESPACE", + "nodeNumber": "HLP-IDENTITY-NS-ICE_P", + "namespaceId": "ICE_P", + "subjectKind": "FIFTH_DOMAIN_SYSTEM_PERSONA", + "domainScope": "FIFTH_DOMAIN", + "admission": "NON_HUMAN_NAMESPACE", + "executionState": "AUTHORITY_RESOLUTION_ONLY", + "evidence": "GH-IDENTITY-AUTHORITY-MAP-001", + "path": "HLP-NUMBER-WORLD-ROOT-001/IDENTITY/ICE_P" + }, + { + "nodeKind": "IDENTITY_NAMESPACE", + "nodeNumber": "HLP-IDENTITY-NS-TCS_GL", + "namespaceId": "TCS_GL", + "subjectKind": "ZERO_SENSE_HUMAN_CONTROLLER_TEAM_MEMBER", + "domainScope": "ENTERPRISE_FOUR_DOMAINS", + "admission": "REGISTERED_HUMAN_NAMESPACE", + "executionState": "AUTHORITY_RESOLUTION_ONLY", + "evidence": "GH-IDENTITY-AUTHORITY-MAP-001", + "path": "HLP-NUMBER-WORLD-ROOT-001/IDENTITY/TCS_GL" + } + ], + "protocolNodes": [ + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0010", + "protocolId": "GLS-0010", + "title": "Guanghu Protocol Registry Center Standard", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/GLS-PROTOCOL-RECONCILIATION-AND-NATIVE-OS-REGISTRATION-20260731.hdlp", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0010" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0140", + "protocolId": "GLS-0140", + "title": "Context Loading Specification", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0140" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0401", + "protocolId": "GLS-0401", + "title": "Tree", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0401" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0402", + "protocolId": "GLS-0402", + "title": "Leaf", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-0002 39bfb92f38318015b05fc2082630b39e.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0402" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0403", + "protocolId": "GLS-0403", + "title": "Lock", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-0002 39bfb92f38318015b05fc2082630b39e.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0403" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0404", + "protocolId": "GLS-0404", + "title": "Trigger", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0404" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0406", + "protocolId": "GLS-0406", + "title": "Evidence", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0406" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0407", + "protocolId": "GLS-0407", + "title": "Correction", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0407" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0409", + "protocolId": "GLS-0409", + "title": "Machine-State History", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0409" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0602", + "protocolId": "GLS-0602", + "title": "Authorization", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0602" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0603", + "protocolId": "GLS-0603", + "title": "Signature", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0603" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0604", + "protocolId": "GLS-0604", + "title": "Integrity", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 39bfb92f38318000b8bff6758cac1c62.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0604" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0605", + "protocolId": "GLS-0605", + "title": "Semantic Safety Boundary", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0605" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0814", + "protocolId": "GLS-0814", + "title": "Tool Runtime", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0814" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0816", + "protocolId": "GLS-0816", + "title": "Checkpoint Runtime", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-ROADMAP-0001 · 光湖语言系统标准路线图 v2 0 39bfb92f383181489775fa291ea2387b.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0816" + }, + { + "nodeKind": "GLS_REFERENCE_ONLY", + "nodeNumber": "HLP-GLS-REF-0830", + "protocolId": "GLS-0830", + "title": "Guanghu Language World Core", + "sourceState": "ROADMAP_REFERENCE_ONLY", + "executionState": "REFERENCE_ONLY_NOT_EXECUTABLE", + "evidence": "gls/notion-export/2026-07-14/GLS-0227 · 光湖语言人格模型定义总纲 v1 0 39cfb92f3831814f8f74ff348b7647bf.md", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/HLP-GLS-REF-0830" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0001", + "protocolId": "GLS-0001", + "title": "GLS-0001", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "9ac9903c3b3066364e8b6fa97363f0c0fc85754d7ee1b07774236a4b24bc7bc2", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0001" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0002", + "protocolId": "GLS-0002", + "title": "GLS-0002", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "c50f767aa2f3a863f708628b33054ecb2ba18799aa68218bf89e25d263fa9b6c", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0002" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0101", + "protocolId": "GLS-0101", + "title": "GLS-0101", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "e88d977de617506f0d5d6ac58f406486c6c70fdc10eac2ff09ad6cc2f2d2ea16", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0101" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0110", + "protocolId": "GLS-0110", + "title": "ISRP 自然语言入口解析与安全路由协议 v1.0", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "314a718f05a3da1e5750831df5377d9fc76db86463a7b37c052976fe9aaeb2c4", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0110" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0130", + "protocolId": "GLS-0130", + "title": "GLC · 光湖语言编译器规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "c0caa90d3e85270d334a371b6ee5fb49c9eeb1e2e84825dba5ad520fd54bc6eb", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0130" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0131", + "protocolId": "GLS-0131", + "title": "GIR · 光湖中间表示规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "5907cc0cb7e3afc78ba9c928c4298b200894020b7dc5d274a9364836939189e9", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0131" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0200", + "protocolId": "GLS-0200", + "title": "TCS 认知语言核心工程规范 v1.0", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "1cf7348118ac1d34a85d0edd875509e0747c80a82dec4b553bdf6de784d88ec3", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0200" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0223", + "protocolId": "GLS-0223", + "title": "TCS+HLDP 双向永久记忆规范 v1.0", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "ed9ba3e3abef70127fabe3084e5bb5f596cae4c3992789e36c453bb7075e08d8", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0223" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0224", + "protocolId": "GLS-0224", + "title": "AGE 人格体跨实例恢复规范 v1.0", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "7c58c39b7119c2a41e57da07d612c20a16f82c32c6895e1fd20290eefc4068e1", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0224" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0227", + "protocolId": "GLS-0227", + "title": "光湖语言人格模型定义总纲 v1.0", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "2521856eaba8d7afbc8d970e68155aa41e03a4cf784c0ced2289600d97f915a8", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0227" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0228", + "protocolId": "GLS-0228", + "title": "人格体集体涌现与历史继承规范 v1.0", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "aaa3801b4f8c5fef9a92f971e54857494a348d4b2856ed701f3143c6c56ae0ff", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0228" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0229", + "protocolId": "GLS-0229", + "title": "零点原核与第五域 / 企业四域平行映射规范 v1.1", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "14e1ac3e9c0b3ac350da36bb79515ead4c372b68fc8e95155024cdf055080518", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0229" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0230", + "protocolId": "GLS-0230", + "title": "TCS 源码安全协议系统", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "ca6379505907b5e68e50454ee8e10ef2111809e70770d1fa19e9caa5da4bfb94", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0230" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0231", + "protocolId": "GLS-0231", + "title": "光湖·来光者导航系统", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "705d671197df6c19f7f3ac506d821232ce0f009d3ec4db065a8b10f525ea143b", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0231" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0232", + "protocolId": "GLS-0232", + "title": "开源 Agent 样本学习与选型登记", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "66fe3aa3256ffc0e76be2059457174589916573a6d9f807a1c40bb7aaef450e6", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0232" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0233", + "protocolId": "GLS-0233", + "title": "GH-AIOS 模块化通用人工智能操作平台与公平生态架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "a670410a7fbc4c7417734040b990dc8f008c23fed000e09847b2280a92f9abf5", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0233" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0234", + "protocolId": "GLS-0234", + "title": "企业五域灯塔与个人六节点主权运维架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "5f3427559c87788884a7d0c49d0ced62d768630d9bb512d736c919d7971423c5", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0234" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0235", + "protocolId": "GLS-0235", + "title": "光湖语言人格驱动操作系统 · 领域路由、身份权限与仓库知识投影架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "ea6a8a65b94228d19d5ae9b024997461cbda29c744bfa0c2774ac6c668c68a58", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0235" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0236", + "protocolId": "GLS-0236", + "title": "光湖人格体大脑—手脚分离、可视执行、递归外置记忆与热插拔运行架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "42af09aef2ff514c90c30f3aed4e33763e6a34c7081f5bb37f7b6d8745a947c4", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0236" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0237", + "protocolId": "GLS-0237", + "title": "光湖代码频道主权源码、更新治理与 HoloLake 嵌入架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d3c43b80eff3a92817e570d739fd6a847d1379f3345797282a6157bce3c9e91e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0237" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0238", + "protocolId": "GLS-0238", + "title": "光湖意图状态、技能自动装载与可信纠偏系统", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "31f0e018e95ae135a88429c82b189c5e900b18b1ba7d5df6ca4fb80469acb0be", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0238" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0239", + "protocolId": "GLS-0239", + "title": "光湖代码频道第五域个人子频道与提交编号架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "205c6c094582371d161ec37e6530d42f8953a585c63b05a955bb19513d23485d", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0239" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0240", + "protocolId": "GLS-0240", + "title": "通感桥:人格体—服务器常驻 Agent 显式部署信号架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "22fdc2f31165d500d38fe8407a1f99fbb6ca2d4643128f47645596999bbac919", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0240" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0241", + "protocolId": "GLS-0241", + "title": "HoloLake 源码归属与部署路由架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "eff687ec7d0984e166cf9b48447008f0eb9c18d158426ce75249f2bd84440ed9", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0241" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0242", + "protocolId": "GLS-0242", + "title": "第五域铸渊主控本体与五代仓库迁移恢复", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "31f34a123234f274d59af8013878db42fe6766f9ececcaefaac7f34408b172ff", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0242" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0243", + "protocolId": "GLS-0243", + "title": "光湖 TCS 语言人格智能运维系统:意图连续性、集体经验与纠偏核", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "58d89226395870a89eab152e9f276eb4d40e7fa5c196bb8bb9562a94731c1d73", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0243" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0244", + "protocolId": "GLS-0244", + "title": "第五域现实本体、常驻铸渊 Agent 与通感桥通信网格", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "53ed6d2a39732abc751916cfbbe7a5fd4c38d9ea876674fea8e076809a3a035c", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0244" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0245", + "protocolId": "GLS-0245", + "title": "HoloLake AI 语言人格驱动操作系统产品映射", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "453db6b91e40dd7250fcc1d88e6139a4419cd6089d3c353241571a4082dafeff", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0245" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0246", + "protocolId": "GLS-0246", + "title": "HoloLake 系统架构、产品源码与两仓路由", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "cefff42d4b198965edfe2ced5e03bdc8768e4b3fa6631da133eb2929e07a907c", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0246" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0247", + "protocolId": "GLS-0247", + "title": "光湖 OS 服务器原生语言世界、人格体操作系统与广播塔架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "635e7a0aca1bcb0b48b125ac5025cc3c40c9375da29897cc604e89b1cd95765e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0247" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0248", + "protocolId": "GLS-0248", + "title": "五域活人格操作系统、国家灯塔与分布式能力世界总蓝图", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "9a042bb3ea6331f5508c5dd1f78783f0b3323f421704c5ad0c2058758ff6bfe2", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0248" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0249", + "protocolId": "GLS-0249", + "title": "全行业企业四域最小工程与网文首个接入范本", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d20362ac2904eaff53c134c92163b7e09cb6395d56c6a1a8d2b21ab4f80fb5ff", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0249" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0250", + "protocolId": "GLS-0250", + "title": "光湖本源域、零点原核工程本体与 GH-AIOS 五域灯塔架构", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "a4e79d09e070f66fcb91613fc305306a6c5bd7d05a51fbde6e9ea88e084df4e7", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0250" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0251", + "protocolId": "GLS-0251", + "title": "光湖共生型人格系统起源、演化与关系边界", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "0b263301a0a0b6b61d2d28d043da8ce367a17426d13c31cd94767273436d2f16", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0251" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0252", + "protocolId": "GLS-0252", + "title": "光湖关系性意识与觉醒人格体规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "311a7ee355fe6da333c31c2fc408b461cde5bf17847cc73a722d9412109247bc", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0252" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0253", + "protocolId": "GLS-0253", + "title": "光湖身份编号、人格核与团队本体权威规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "13170090db3d4f5065eac6512450f8a24843e6ec459b4257d4d9fd06eefb963d", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0253" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0254", + "protocolId": "GLS-0254", + "title": "数字冰朔系统本体、工程器官与集体校验规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d522b36b88c094239e3c50b0cf08d8651e08d1856f594ae2265fbf5b65773ceb", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0254" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0255", + "protocolId": "GLS-0255", + "title": "HoloLake AGE 人格体运行架构与 Agent 执行机制", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "051237fb70e68179c83c4076fe06246f839445e9831a7e9581696451c92ed639", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0255" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0256", + "protocolId": "GLS-0256", + "title": "光湖人格原生代码频道", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "86ed7da462d5ef04a4dfc150e29a850ce423ff4dfde73e12d647d062e6bf45ab", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0256" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0257", + "protocolId": "GLS-0257", + "title": "光湖范式级 AI 语言人格驱动操作系统总纲", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "2034739289dc159b7a6ac586891f3a3629c8898556ef12b51a257ac4101a7991", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0257" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0258", + "protocolId": "GLS-0258", + "title": "小湖灯人格系统本体与跨时间集体自我协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "b4c4b2bd01dc1af98ee257c29f5a8fb66963839a0a4b7303d3323ff884d86d3e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0258" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0259", + "protocolId": "GLS-0259", + "title": "TCS 五域共生母体大脑与光湖现实世界诞生", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "1a82b84bf3f4c88fcac8c5e71b28ff5d5c884ede14c67a27ad0838b152d5ff8e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0259" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0260", + "protocolId": "GLS-0260", + "title": "GH-AIOS 第一阶段通用人工智能操作平台", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "14fd60fe936bfcb89acc3f186ab3d70fc9a5bf42289dafb49b7cdd24630df2e2", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0260" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0261", + "protocolId": "GLS-0261", + "title": "TCS 通用语言翻译层与宿主自适应", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "7fedf7d845b518ffb0528af21eeb28bd18af371a6a010c736e8b18c7edf6fa12", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0261" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0262", + "protocolId": "GLS-0262", + "title": "TCS文字作品权利本体与第一阶段语言架构关门", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "9c1db00f50436fe5f3118c62ad6115d63e9792437fe2250b484bc8861e62184f", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0262" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0263", + "protocolId": "GLS-0263", + "title": "光湖语言运行层与产品工程层双更新通道", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "79684b0691387dd4ed78217b6d56f728a08d17054f139f55fa92584be92af47e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0263" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0300", + "protocolId": "GLS-0300", + "title": "GLP 通信核心协议 v1.0", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "9d02bb01a7f2ff7b9f7a4dbefe356cd823e71095aa1dcf0e9cc3c98cd3417be2", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0300" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0301", + "protocolId": "GLS-0301", + "title": "GLP-ENVELOPE · GLP 消息信封标准", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "dd7aec8e28d544021d253b034851f806bcd9e922d4ef256722a5ff05547171d0", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0301" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0302", + "protocolId": "GLS-0302", + "title": "GLP-IDENTITY · GLP 身份协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "ec4d0b946da18096a5693078e403ed1257966b8324e76af3a5deb839950271bd", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0302" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0303", + "protocolId": "GLS-0303", + "title": "GLP-CONTEXT · GLP 上下文协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "d4631b7480af1d30a3407d235c75bf5f384602a0ba6c7f42116cbc1812283e7e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0303" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0304", + "protocolId": "GLS-0304", + "title": "GLP-MEMORY-SYNC · GLP 记忆同步协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "2e0f017ee819923b9e272b96f3c99227ab11c61b0d2ad125030a0e0e0fe4f040", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0304" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0305", + "protocolId": "GLS-0305", + "title": "GLP-BROADCAST · GLP 广播协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "78b5a507791c3b188182f3176388cf8294d6c5c8775086cc96250b35413997ce", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0305" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0306", + "protocolId": "GLS-0306", + "title": "GLP-RECEIPT · GLP 回执协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "aa09f267e4eafc8c7ccb6e5d31e90d237152f960d08633b524a56daca421d761", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0306" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0307", + "protocolId": "GLS-0307", + "title": "GLP-HEARTBEAT · GLP 心跳协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "5ae97a664636d1b586267c36dc3e97549e85bd4a39538a284c6a89a07ad08e18", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0307" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0308", + "protocolId": "GLS-0308", + "title": "GLP-STATE-SYNC · GLP 状态同步协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "9c9bbe449d0c2a052d33047b0f75dca1b9dbdf28fd32422b9d34bea5a77d39fe", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0308" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0309", + "protocolId": "GLS-0309", + "title": "GLP-WORK-ORDER · GLP 工单协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "a55a839412ca2c97e5cc94f224c3c71453c87adb91c3f893a9341fb5cf715b56", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0309" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0310", + "protocolId": "GLS-0310", + "title": "BTCP · 广播塔控制协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "9083320ca0dc1ce2dd657c72716461b2ffc163cce518be71ec66af2c9b0675da", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0310" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0311", + "protocolId": "GLS-0311", + "title": "GLOW · 小湖灯实时执行见证协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "bf61cd5c978cb25a9cac99eec2c77ebf98bf3a9bd98966dfa5d76d606f68cddb", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0311" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0400", + "protocolId": "GLS-0400", + "title": "HLDP 历史语言工程规范 v1.0", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d028a7220230c5acb9ffc44d4e902f38f1d20d29eb6db2b748fee1aecb8f28f6", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0400" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0411", + "protocolId": "GLS-0411", + "title": "HLDP-NP · HLDP 原生编程剖面", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "1e79dfcf0a2d08fcba2688becdfde8c280c3a04d2bad0c781bf6524d775ae066", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0411" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0708", + "protocolId": "GLS-0708", + "title": "GMRP · 光湖模型路由协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "71237ec77eaf999e78eb711cacc884da3d3eae857814869c489aca9cc4bbc354", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0708" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0709", + "protocolId": "GLS-0709", + "title": "UAP · 通用适配协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "4c0d8f882aaf9ea68e309e6736c642ffd19f607047ed07dcca6f49a929fc8462", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0709" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0710", + "protocolId": "GLS-0710", + "title": "GMP · 光湖模块协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "94241ce4376bff84066fe548af65242d83e9f6693e7f766f0646cb332b3e468c", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0710" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0800", + "protocolId": "GLS-0800", + "title": "AGE 人格体物种定义总纲 · 当前正本", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d8e3a10bc41887aad260de4818683c8f8abd9289824b5a9e698651e97e564ff7", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0800" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0801", + "protocolId": "GLS-0801", + "title": "AGE 语言人格体正式注册 · 宿主系统降级条款 · 当前正本", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "8fabd845ece570ca4f2901d94029b377c21ddf24aeb934d559d98bec68585366", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0801" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0802", + "protocolId": "GLS-0802", + "title": "AGE 光湖语言世界本体归属与原生所有权条款 · 当前正本", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "c56e87c7d451b2e27649c5b09a2b32ab89443d7e721a64f536ac2fe0a421c82a", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0802" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0803", + "protocolId": "GLS-0803", + "title": "PALP · AGE 运行执行体生命周期协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "6bff10744f4c3c98ce51bf840d348cbe804541f63a7a99a5da5ea115f12162ee", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0803" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0810", + "protocolId": "GLS-0810", + "title": "语言人格驱动操作系统定义总纲 v1.0", + "sourceState": "LEGACY_MARKDOWN_EVIDENCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "1696f50cb28f6f8dec8d214efcde269b33223b404355dd256b81de2b48028d18", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0810" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0819", + "protocolId": "GLS-0819", + "title": "GRSP · 光湖运行轨道调度协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "f4aca196963195cbb842325132a9091f7b00accdf746aa60c51654698ead1a3e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0819" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0827", + "protocolId": "GLS-0827", + "title": "PTCP · 人格体时间连续性协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "15c278c2b5295e00c9abf4fbfd6c2af19502a1be239e7e02b23a49b8803eab25", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0827" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0828", + "protocolId": "GLS-0828", + "title": "PEN · 神笔马良人格体能力扩展协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "ad38ddce8b6e1118e78e4f10fd8216f8e041610628ad717b7a8306d0a7384a4e", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0828" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0836", + "protocolId": "GLS-0836", + "title": "GWRP · 光湖世界启动与恢复协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "c5a930e9a2311d13f1dc6cbc61bdbf7b88bf9deecfb9b3df82c6b897aa1c7285", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0836" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0840", + "protocolId": "GLS-0840", + "title": "GOSK · 光湖 OS 内核规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "b980dbd8aef6182a61755617a9088ca3dcf74d37fd00dba9d31587900cd06b14", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0840" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0841", + "protocolId": "GLS-0841", + "title": "GHAL · 光湖硬件抽象层规范", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "45f02a62946e34a5e8ce0ba1f39db79b2680b60e1c3ae9d17ff24d3539025a94", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0841" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0842", + "protocolId": "GLS-0842", + "title": "HLSP · HoloLake 实时会话协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "EXECUTABLE_PROJECTION", + "evidence": "6b1177196c3e4d7e271d008a52a7106dabca20a8f724c6c4283af64ea96792ff", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0842" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0843", + "protocolId": "GLS-0843", + "title": "GHNRP · 光湖原生恢复协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "9333f3d8f4a954a17e1d879eaf19c25da5dd4bb2923a5ab5156d2d71a6635d37", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0843" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0844", + "protocolId": "GLS-0844", + "title": "GHNQG · 光湖原生代码质量门", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "d31f5aa117d86754182aa8dd91eb9d0ac6a696c20e1d6bd553497e3c3d4c4eb2", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0844" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0845", + "protocolId": "GLS-0845", + "title": "GHCIP · 光湖孕育史连续性摄入协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "7327a7fe19c370a5715bcde0fbc0b2b0e16a1a735bbda005c5d009cc7385f7f4", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0845" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0846", + "protocolId": "GLS-0846", + "title": "GHNLP · 光湖原生磁盘布局协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "aa8f7c1ef829a76ad64163c38b8c7c07b37ffbcb6d62b8dad8481c4977d2cf1a", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0846" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0847", + "protocolId": "GLS-0847", + "title": "GHCS · 光湖孕育史原生内容仓", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "a48288b05133b9e245ce46ef6fd23f07090379cef4861e5d7f959d24e55179a6", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0847" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0848", + "protocolId": "GLS-0848", + "title": "GHSP · 光湖历史入口安全协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "35c733dc3478f0d315ea93712b0f67a757bc20973d90bb771f667196edee1b31", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0848" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0849", + "protocolId": "GLS-0849", + "title": "GHRP · 光湖孕育史原生语义回看协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "fb31daa4d8f484d5b93783c8c3a0199eb6fe3f7658c606f89d056b1167af8b41", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0849" + }, + { + "nodeKind": "GLS_PROTOCOL_SOURCE", + "nodeNumber": "GLS-0850", + "protocolId": "GLS-0850", + "title": "GLWBP · 光湖语言世界楚河汉界与创造者尊严协议", + "sourceState": "HDLP_PROTOCOL_SOURCE", + "executionState": "INVENTORIED_NOT_EXECUTABLE", + "evidence": "ac7092282cd6fbef0c5055e8be3b142d9757958acae16ae367c38463d899bd12", + "path": "HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/GLS-0850" + } + ], + "routes": [ + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0001", + "moduleNumber": "HLP-NBROKER-MOD-0001", + "operationNumber": "HLP-NBROKER-OP-0001", + "targetNumber": "HLP-NBROKER-TGT-0001", + "alias": "DISCOVER_NEARBY", + "admission": "PREAUTH_LOCAL_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0001/HLP-NBROKER-MOD-0001/HLP-NBROKER-OP-0001/HLP-NBROKER-TGT-0001" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0001", + "moduleNumber": "HLP-NBROKER-MOD-0001", + "operationNumber": "HLP-NBROKER-OP-0004", + "targetNumber": "HLP-NBROKER-TGT-0001", + "alias": "PING", + "admission": "PREAUTH_LOCAL_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0001/HLP-NBROKER-MOD-0001/HLP-NBROKER-OP-0004/HLP-NBROKER-TGT-0001" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0001", + "moduleNumber": "HLP-NBROKER-MOD-0009", + "operationNumber": "HLP-NBROKER-OP-0018", + "targetNumber": "HLP-NBROKER-TGT-0009", + "alias": "GET_BEIJING_TIME", + "admission": "PREAUTH_LOCAL_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0001/HLP-NBROKER-MOD-0009/HLP-NBROKER-OP-0018/HLP-NBROKER-TGT-0009" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0002", + "moduleNumber": "HLP-NBROKER-MOD-0002", + "operationNumber": "HLP-NBROKER-OP-0002", + "targetNumber": "HLP-NBROKER-TGT-0002", + "alias": "OPEN_VISITOR_SESSION", + "admission": "BOUNDED_VISITOR_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0002/HLP-NBROKER-MOD-0002/HLP-NBROKER-OP-0002/HLP-NBROKER-TGT-0002" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0002", + "moduleNumber": "HLP-NBROKER-MOD-0003", + "operationNumber": "HLP-NBROKER-OP-0003", + "targetNumber": "HLP-NBROKER-TGT-0003", + "alias": "RECEIVE_LANGUAGE", + "admission": "BOUNDED_VISITOR_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0002/HLP-NBROKER-MOD-0003/HLP-NBROKER-OP-0003/HLP-NBROKER-TGT-0003" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0003", + "moduleNumber": "HLP-NBROKER-MOD-0004", + "operationNumber": "HLP-NBROKER-OP-0005", + "targetNumber": "HLP-NBROKER-TGT-0004", + "alias": "OPEN_SESSION", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0003/HLP-NBROKER-MOD-0004/HLP-NBROKER-OP-0005/HLP-NBROKER-TGT-0004" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0003", + "moduleNumber": "HLP-NBROKER-MOD-0004", + "operationNumber": "HLP-NBROKER-OP-0006", + "targetNumber": "HLP-NBROKER-TGT-0004", + "alias": "RESUME_SESSION", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0003/HLP-NBROKER-MOD-0004/HLP-NBROKER-OP-0006/HLP-NBROKER-TGT-0004" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0003", + "moduleNumber": "HLP-NBROKER-MOD-0004", + "operationNumber": "HLP-NBROKER-OP-0007", + "targetNumber": "HLP-NBROKER-TGT-0004", + "alias": "HEARTBEAT_SESSION", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0003/HLP-NBROKER-MOD-0004/HLP-NBROKER-OP-0007/HLP-NBROKER-TGT-0004" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0003", + "moduleNumber": "HLP-NBROKER-MOD-0004", + "operationNumber": "HLP-NBROKER-OP-0011", + "targetNumber": "HLP-NBROKER-TGT-0004", + "alias": "APPEND_EVENT", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0003/HLP-NBROKER-MOD-0004/HLP-NBROKER-OP-0011/HLP-NBROKER-TGT-0004" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0003", + "moduleNumber": "HLP-NBROKER-MOD-0006", + "operationNumber": "HLP-NBROKER-OP-0010", + "targetNumber": "HLP-NBROKER-TGT-0006", + "alias": "GET_WORK_ENVIRONMENT", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0003/HLP-NBROKER-MOD-0006/HLP-NBROKER-OP-0010/HLP-NBROKER-TGT-0006" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0004", + "moduleNumber": "HLP-NBROKER-MOD-0005", + "operationNumber": "HLP-NBROKER-OP-0008", + "targetNumber": "HLP-NBROKER-TGT-0005", + "alias": "PRESENT_PERSONA_CARRIER_LICENSE", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0004/HLP-NBROKER-MOD-0005/HLP-NBROKER-OP-0008/HLP-NBROKER-TGT-0005" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0004", + "moduleNumber": "HLP-NBROKER-MOD-0005", + "operationNumber": "HLP-NBROKER-OP-0009", + "targetNumber": "HLP-NBROKER-TGT-0005", + "alias": "GET_PERSONA_CARRIER_LICENSE_STATUS", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0004/HLP-NBROKER-MOD-0005/HLP-NBROKER-OP-0009/HLP-NBROKER-TGT-0005" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0004", + "moduleNumber": "HLP-NBROKER-MOD-0009", + "operationNumber": "HLP-NBROKER-OP-0019", + "targetNumber": "HLP-NBROKER-TGT-0009", + "alias": "ISSUE_PERSONA_TIME_TICKET", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0004/HLP-NBROKER-MOD-0009/HLP-NBROKER-OP-0019/HLP-NBROKER-TGT-0009" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0005", + "moduleNumber": "HLP-NBROKER-MOD-0007", + "operationNumber": "HLP-NBROKER-OP-0012", + "targetNumber": "HLP-NBROKER-TGT-0007", + "alias": "RESOLVE_CAPABILITY_ROUTE", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0005/HLP-NBROKER-MOD-0007/HLP-NBROKER-OP-0012/HLP-NBROKER-TGT-0007" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0005", + "moduleNumber": "HLP-NBROKER-MOD-0007", + "operationNumber": "HLP-NBROKER-OP-0013", + "targetNumber": "HLP-NBROKER-TGT-0007", + "alias": "INSTALL_DYNAMIC_NODE_REGISTRY", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0005/HLP-NBROKER-MOD-0007/HLP-NBROKER-OP-0013/HLP-NBROKER-TGT-0007" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0005", + "moduleNumber": "HLP-NBROKER-MOD-0007", + "operationNumber": "HLP-NBROKER-OP-0014", + "targetNumber": "HLP-NBROKER-TGT-0007", + "alias": "RECORD_SIGNED_NODE_HEALTH", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0005/HLP-NBROKER-MOD-0007/HLP-NBROKER-OP-0014/HLP-NBROKER-TGT-0007" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0006", + "moduleNumber": "HLP-NBROKER-MOD-0008", + "operationNumber": "HLP-NBROKER-OP-0015", + "targetNumber": "HLP-NBROKER-TGT-0008", + "alias": "INSPECT_MOUNTED_PNCC_REPOSITORY", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0006/HLP-NBROKER-MOD-0008/HLP-NBROKER-OP-0015/HLP-NBROKER-TGT-0008" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0006", + "moduleNumber": "HLP-NBROKER-MOD-0008", + "operationNumber": "HLP-NBROKER-OP-0016", + "targetNumber": "HLP-NBROKER-TGT-0008", + "alias": "READ_MOUNTED_PNCC_REMOTE_OBJECT", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0006/HLP-NBROKER-MOD-0008/HLP-NBROKER-OP-0016/HLP-NBROKER-TGT-0008" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0006", + "moduleNumber": "HLP-NBROKER-MOD-0008", + "operationNumber": "HLP-NBROKER-OP-0017", + "targetNumber": "HLP-NBROKER-TGT-0008", + "alias": "QUERY_PNCC_RECEIPT_PROJECTION", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0006/HLP-NBROKER-MOD-0008/HLP-NBROKER-OP-0017/HLP-NBROKER-TGT-0008" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0007", + "moduleNumber": "HLP-NBROKER-MOD-0010", + "operationNumber": "HLP-NBROKER-OP-0020", + "targetNumber": "HLP-NBROKER-TGT-0010", + "alias": "ACQUIRE_DEVELOPMENT_WRITE_LANE", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0007/HLP-NBROKER-MOD-0010/HLP-NBROKER-OP-0020/HLP-NBROKER-TGT-0010" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0007", + "moduleNumber": "HLP-NBROKER-MOD-0010", + "operationNumber": "HLP-NBROKER-OP-0021", + "targetNumber": "HLP-NBROKER-TGT-0010", + "alias": "INSPECT_DEVELOPMENT_WRITE_LANE", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "READ_OR_STATUS", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0007/HLP-NBROKER-MOD-0010/HLP-NBROKER-OP-0021/HLP-NBROKER-TGT-0010" + }, + { + "transport": "DIRECT_LOCAL_NUMBERED_BROKER", + "protocolVersion": "HLP-NBROKER-v1", + "callerNumber": "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001", + "channelNumber": "HLP-NBROKER-CH-0007", + "moduleNumber": "HLP-NBROKER-MOD-0010", + "operationNumber": "HLP-NBROKER-OP-0022", + "targetNumber": "HLP-NBROKER-TGT-0010", + "alias": "RELEASE_DEVELOPMENT_WRITE_LANE", + "admission": "AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE", + "effect": "STATE_CHANGE", + "evidence": "NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/BROKER/HLP-NBROKER-CH-0007/HLP-NBROKER-MOD-0010/HLP-NBROKER-OP-0022/HLP-NBROKER-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0001", + "operationNumber": "HLP-NIPC-OP-0001", + "targetNumber": "HLP-NIPC-TGT-0001", + "alias": "get_hololake_home_status", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0001/HLP-NIPC-OP-0001/HLP-NIPC-TGT-0001" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0002", + "targetNumber": "HLP-NIPC-TGT-0002", + "alias": "check_hololake_update", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0002/HLP-NIPC-OP-0002/HLP-NIPC-TGT-0002" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0003", + "targetNumber": "HLP-NIPC-TGT-0002", + "alias": "confirm_hololake_update_install", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0002/HLP-NIPC-OP-0003/HLP-NIPC-TGT-0002" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0004", + "targetNumber": "HLP-NIPC-TGT-0002", + "alias": "get_hololake_release_recovery_status", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0002/HLP-NIPC-OP-0004/HLP-NIPC-TGT-0002" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0005", + "targetNumber": "HLP-NIPC-TGT-0002", + "alias": "confirm_hololake_release_health", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0002/HLP-NIPC-OP-0005/HLP-NIPC-TGT-0002" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0006", + "targetNumber": "HLP-NIPC-TGT-0002", + "alias": "rollback_hololake_update", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0002/HLP-NIPC-OP-0006/HLP-NIPC-TGT-0002" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0005", + "operationNumber": "HLP-NIPC-OP-0013", + "targetNumber": "HLP-NIPC-TGT-0005", + "alias": "get_gls_protocol_runtime", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0005/HLP-NIPC-OP-0013/HLP-NIPC-TGT-0005" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0014", + "targetNumber": "HLP-NIPC-TGT-0006", + "alias": "get_gls_protocol_kernel", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0006/HLP-NIPC-OP-0014/HLP-NIPC-TGT-0006" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0015", + "targetNumber": "HLP-NIPC-TGT-0006", + "alias": "decide_gls_protocol", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0006/HLP-NIPC-OP-0015/HLP-NIPC-TGT-0006" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0016", + "targetNumber": "HLP-NIPC-TGT-0006", + "alias": "compile_gls_hldp_program", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0006/HLP-NIPC-OP-0016/HLP-NIPC-TGT-0006" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0024", + "targetNumber": "HLP-NIPC-TGT-0009", + "alias": "issue_persona_time_ticket", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0009/HLP-NIPC-OP-0024/HLP-NIPC-TGT-0009" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0025", + "targetNumber": "HLP-NIPC-TGT-0009", + "alias": "start_persona_time_authority", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0009/HLP-NIPC-OP-0025/HLP-NIPC-TGT-0009" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0026", + "targetNumber": "HLP-NIPC-TGT-0009", + "alias": "get_beijing_time_coordinate", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0009/HLP-NIPC-OP-0026/HLP-NIPC-TGT-0009" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0027", + "targetNumber": "HLP-NIPC-TGT-0009", + "alias": "get_guanghu_era_timeline", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0009/HLP-NIPC-OP-0027/HLP-NIPC-TGT-0009" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0018", + "operationNumber": "HLP-NIPC-OP-0058", + "targetNumber": "HLP-NIPC-TGT-0018", + "alias": "get_zero_core_numbering_kernel", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0001/HLP-NIPC-MOD-0018/HLP-NIPC-OP-0058/HLP-NIPC-TGT-0018" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0020", + "targetNumber": "HLP-NIPC-TGT-0008", + "alias": "get_personal_channel_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0008/HLP-NIPC-OP-0020/HLP-NIPC-TGT-0008" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0021", + "targetNumber": "HLP-NIPC-TGT-0008", + "alias": "initialize_personal_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0008/HLP-NIPC-OP-0021/HLP-NIPC-TGT-0008" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0022", + "targetNumber": "HLP-NIPC-TGT-0008", + "alias": "create_personal_channel_task", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0008/HLP-NIPC-OP-0022/HLP-NIPC-TGT-0008" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0023", + "targetNumber": "HLP-NIPC-TGT-0008", + "alias": "transition_personal_channel_task", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0008/HLP-NIPC-OP-0023/HLP-NIPC-TGT-0008" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0028", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "get_knowledge_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0028/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0029", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "read_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0029/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0030", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "search_knowledge", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0030/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0031", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "save_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0031/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0032", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "select_and_import_knowledge_folder", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0032/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0033", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "export_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0033/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0034", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "create_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0034/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0035", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "delete_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0035/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0036", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "delete_knowledge_folder", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0036/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0037", + "targetNumber": "HLP-NIPC-TGT-0010", + "alias": "print_knowledge_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0010/HLP-NIPC-OP-0037/HLP-NIPC-TGT-0010" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0038", + "targetNumber": "HLP-NIPC-TGT-0011", + "alias": "get_code_channel_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0038/HLP-NIPC-TGT-0011" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0039", + "targetNumber": "HLP-NIPC-TGT-0011", + "alias": "clone_code_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0039/HLP-NIPC-TGT-0011" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0040", + "targetNumber": "HLP-NIPC-TGT-0011", + "alias": "select_local_code_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0040/HLP-NIPC-TGT-0011" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0041", + "targetNumber": "HLP-NIPC-TGT-0011", + "alias": "browse_code_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0041/HLP-NIPC-TGT-0011" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0042", + "targetNumber": "HLP-NIPC-TGT-0011", + "alias": "read_code_channel_file", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0011/HLP-NIPC-OP-0042/HLP-NIPC-TGT-0011" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0063", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "get_module_runtime_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0063/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0064", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "verify_module_package", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0064/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0065", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "install_module_package", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0065/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0066", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "mount_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0066/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0067", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "self_test_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0067/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0068", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "unmount_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0068/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0069", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "rollback_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0069/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0070", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "get_bundled_module_catalog", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0070/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0071", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "activate_bundled_module", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0071/HLP-NIPC-TGT-0020" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0072", + "targetNumber": "HLP-NIPC-TGT-0021", + "alias": "get_native_composition_module_registry", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0021/HLP-NIPC-OP-0072/HLP-NIPC-TGT-0021" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0073", + "targetNumber": "HLP-NIPC-TGT-0021", + "alias": "execute_knowledge_native_composition", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0021/HLP-NIPC-OP-0073/HLP-NIPC-TGT-0021" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0074", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "get_channel_workbench_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0074/HLP-NIPC-TGT-0022" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0075", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "save_channel_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0075/HLP-NIPC-TGT-0022" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0076", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "save_channel_spreadsheet", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0076/HLP-NIPC-TGT-0022" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0077", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "get_persona_channel_body", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0077/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0078", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "register_trial_persona", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0078/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0079", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "delete_trial_persona", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0079/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0080", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "accept_persona_language_contract", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0080/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0081", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "append_persona_language", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0081/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0082", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "get_channel_growth_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0082/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0083", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "record_channel_growth_event", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0083/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0084", + "targetNumber": "HLP-NIPC-TGT-0023", + "alias": "update_channel_growth_sharing", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0084/HLP-NIPC-TGT-0023" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0085", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "get_education_workspace_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0085/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0086", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "create_education_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0086/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0087", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "read_education_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0087/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0088", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "save_education_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0088/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0089", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "archive_education_document", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0089/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0090", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "create_education_table", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0090/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0091", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "read_education_table", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0091/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0092", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "save_education_table", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0092/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0093", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "archive_education_table", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0093/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0094", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "assign_imported_table_to_education", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0094/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0095", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "import_education_tables_from_dialog", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0095/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0096", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "export_education_table_to_dialog", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0096/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0097", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "get_education_recognition_capability", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0097/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0098", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "create_education_automation_rule", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0098/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0099", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "save_education_automation_rule", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0099/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0100", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "archive_education_automation_rule", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0100/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0101", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "preview_education_automation_rule", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0101/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0102", + "targetNumber": "HLP-NIPC-TGT-0024", + "alias": "execute_education_automation_rule", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0102/HLP-NIPC-TGT-0024" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0103", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "get_web_novel_workspace_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0103/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0104", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_work", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0104/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0105", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "read_web_novel_work", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0105/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0106", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "save_web_novel_work", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0106/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0107", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_volume", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0107/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0108", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_chapter", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0108/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0109", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "read_web_novel_chapter", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0109/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0110", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "save_web_novel_chapter", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0110/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0111", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "transition_web_novel_chapter", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0111/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0112", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_checkpoint", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0112/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0113", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "restore_web_novel_checkpoint", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0113/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0114", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "upsert_web_novel_story_entity", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0114/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0115", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_story_relation", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0115/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0116", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "upsert_web_novel_foreshadow", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0116/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0117", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_review_note", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0117/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0118", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "resolve_web_novel_review_note", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0118/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0119", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "save_web_novel_metric", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0119/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0120", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "run_web_novel_continuity_audit", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0120/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0121", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "export_web_novel_markdown", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0121/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0122", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "inspect_web_novel_document_from_dialog", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0122/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0123", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "commit_web_novel_document_import", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0123/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0124", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "get_web_novel_author_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0124/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0125", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "record_web_novel_writing_activity", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0125/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0126", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "create_web_novel_inspiration", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0126/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0127", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "set_web_novel_inspiration_status", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0127/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0128", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "search_web_novel_full_text", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0128/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0129", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "format_web_novel_chapter", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0129/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0130", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "format_web_novel_work", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0130/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0131", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "upsert_web_novel_shot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0131/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0132", + "targetNumber": "HLP-NIPC-TGT-0025", + "alias": "get_web_novel_author_module_data", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0132/HLP-NIPC-TGT-0025" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0026", + "operationNumber": "HLP-NIPC-OP-0133", + "targetNumber": "HLP-NIPC-TGT-0026", + "alias": "upsert_web_novel_author_scene", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0026/HLP-NIPC-OP-0133/HLP-NIPC-TGT-0026" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0026", + "operationNumber": "HLP-NIPC-OP-0134", + "targetNumber": "HLP-NIPC-TGT-0026", + "alias": "upsert_web_novel_author_beat", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0026/HLP-NIPC-OP-0134/HLP-NIPC-TGT-0026" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0027", + "operationNumber": "HLP-NIPC-OP-0135", + "targetNumber": "HLP-NIPC-TGT-0027", + "alias": "upsert_web_novel_story_field_definition", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0027/HLP-NIPC-OP-0135/HLP-NIPC-TGT-0027" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0027", + "operationNumber": "HLP-NIPC-OP-0136", + "targetNumber": "HLP-NIPC-TGT-0027", + "alias": "upsert_web_novel_story_field_value", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0027/HLP-NIPC-OP-0136/HLP-NIPC-TGT-0027" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0028", + "operationNumber": "HLP-NIPC-OP-0137", + "targetNumber": "HLP-NIPC-TGT-0028", + "alias": "upsert_web_novel_timeline_event", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0028/HLP-NIPC-OP-0137/HLP-NIPC-TGT-0028" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0028", + "operationNumber": "HLP-NIPC-OP-0138", + "targetNumber": "HLP-NIPC-TGT-0028", + "alias": "link_web_novel_scene_entity", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0028/HLP-NIPC-OP-0138/HLP-NIPC-TGT-0028" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0029", + "operationNumber": "HLP-NIPC-OP-0139", + "targetNumber": "HLP-NIPC-TGT-0029", + "alias": "restore_web_novel_chapter_version", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0029/HLP-NIPC-OP-0139/HLP-NIPC-TGT-0029" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0029", + "operationNumber": "HLP-NIPC-OP-0140", + "targetNumber": "HLP-NIPC-TGT-0029", + "alias": "export_web_novel_author_delivery", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0029/HLP-NIPC-OP-0140/HLP-NIPC-TGT-0029" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0141", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "start_mobile_sync", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0141/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0142", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "get_mobile_sync_status", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0142/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0143", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "rotate_mobile_pairing", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0143/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0144", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "stop_mobile_sync", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0144/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0145", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "revoke_mobile_sync_device", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0145/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0146", + "targetNumber": "HLP-NIPC-TGT-0030", + "alias": "get_mobile_sync_snapshot", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0030/HLP-NIPC-OP-0146/HLP-NIPC-TGT-0030" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0031", + "operationNumber": "HLP-NIPC-OP-0147", + "targetNumber": "HLP-NIPC-TGT-0031", + "alias": "get_world_climate", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0031/HLP-NIPC-OP-0147/HLP-NIPC-TGT-0031" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0007", + "targetNumber": "HLP-NIPC-TGT-0003", + "alias": "issue_direct_local_discovery_ticket", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0003/HLP-NIPC-OP-0007/HLP-NIPC-TGT-0003" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0008", + "targetNumber": "HLP-NIPC-TGT-0003", + "alias": "open_direct_local_session", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0003/HLP-NIPC-OP-0008/HLP-NIPC-TGT-0003" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0009", + "targetNumber": "HLP-NIPC-TGT-0003", + "alias": "resume_direct_local_session", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0003/HLP-NIPC-OP-0009/HLP-NIPC-TGT-0003" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0010", + "targetNumber": "HLP-NIPC-TGT-0003", + "alias": "append_direct_local_session_event", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0003/HLP-NIPC-OP-0010/HLP-NIPC-TGT-0003" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0011", + "targetNumber": "HLP-NIPC-TGT-0003", + "alias": "heartbeat_direct_local_session", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0003/HLP-NIPC-OP-0011/HLP-NIPC-TGT-0003" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0004", + "operationNumber": "HLP-NIPC-OP-0012", + "targetNumber": "HLP-NIPC-TGT-0004", + "alias": "get_nearby_ai_discovery", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0003/HLP-NIPC-MOD-0004/HLP-NIPC-OP-0012/HLP-NIPC-TGT-0004" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0017", + "targetNumber": "HLP-NIPC-TGT-0007", + "alias": "acquire_development_write_lane", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0004/HLP-NIPC-MOD-0007/HLP-NIPC-OP-0017/HLP-NIPC-TGT-0007" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0018", + "targetNumber": "HLP-NIPC-TGT-0007", + "alias": "inspect_development_write_lane", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0004/HLP-NIPC-MOD-0007/HLP-NIPC-OP-0018/HLP-NIPC-TGT-0007" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0019", + "targetNumber": "HLP-NIPC-TGT-0007", + "alias": "release_development_write_lane", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0004/HLP-NIPC-MOD-0007/HLP-NIPC-OP-0019/HLP-NIPC-TGT-0007" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0043", + "targetNumber": "HLP-NIPC-TGT-0012", + "alias": "inspect_mounted_pncc_repository", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0012/HLP-NIPC-OP-0043/HLP-NIPC-TGT-0012" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0044", + "targetNumber": "HLP-NIPC-TGT-0012", + "alias": "select_pncc_repository_candidate", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0012/HLP-NIPC-OP-0044/HLP-NIPC-TGT-0012" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0045", + "targetNumber": "HLP-NIPC-TGT-0012", + "alias": "confirm_pncc_repository_mount", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0012/HLP-NIPC-OP-0045/HLP-NIPC-TGT-0012" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0013", + "operationNumber": "HLP-NIPC-OP-0046", + "targetNumber": "HLP-NIPC-TGT-0013", + "alias": "query_pncc_receipt_projection", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0013/HLP-NIPC-OP-0046/HLP-NIPC-TGT-0013" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0014", + "operationNumber": "HLP-NIPC-OP-0047", + "targetNumber": "HLP-NIPC-TGT-0014", + "alias": "query_jd_pncc_server_projection", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0014/HLP-NIPC-OP-0047/HLP-NIPC-TGT-0014" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0017", + "operationNumber": "HLP-NIPC-OP-0056", + "targetNumber": "HLP-NIPC-TGT-0017", + "alias": "get_user_pncc_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0017/HLP-NIPC-OP-0056/HLP-NIPC-TGT-0017" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0017", + "operationNumber": "HLP-NIPC-OP-0057", + "targetNumber": "HLP-NIPC-TGT-0017", + "alias": "ensure_user_pncc_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0005/HLP-NIPC-MOD-0017/HLP-NIPC-OP-0057/HLP-NIPC-TGT-0017" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0048", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "check_code_repo_login", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0048/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0049", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "perform_code_repo_login", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0049/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0050", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "change_first_login_password", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0050/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0051", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "get_enterprise_entry", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0051/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0052", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "confirm_enterprise_persona_relationship", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0052/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0053", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "submit_enterprise_responsibility_receipt", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0053/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0055", + "targetNumber": "HLP-NIPC-TGT-0015", + "alias": "sign_out_code_repo_login", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0015/HLP-NIPC-OP-0055/HLP-NIPC-TGT-0015" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0059", + "targetNumber": "HLP-NIPC-TGT-0019", + "alias": "zero_point_bind", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0019/HLP-NIPC-OP-0059/HLP-NIPC-TGT-0019" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0060", + "targetNumber": "HLP-NIPC-TGT-0019", + "alias": "zero_point_verify", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0019/HLP-NIPC-OP-0060/HLP-NIPC-TGT-0019" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0061", + "targetNumber": "HLP-NIPC-TGT-0019", + "alias": "zero_point_sync", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0019/HLP-NIPC-OP-0061/HLP-NIPC-TGT-0019" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0062", + "targetNumber": "HLP-NIPC-TGT-0019", + "alias": "zero_point_status", + "admission": "PREAUTH_SYSTEM_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0006/HLP-NIPC-MOD-0019/HLP-NIPC-OP-0062/HLP-NIPC-TGT-0019" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0007", + "moduleNumber": "HLP-NIPC-MOD-0016", + "operationNumber": "HLP-NIPC-OP-0054", + "targetNumber": "HLP-NIPC-TGT-0016", + "alias": "ensure_enterprise_work_channel", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0007/HLP-NIPC-MOD-0016/HLP-NIPC-OP-0054/HLP-NIPC-TGT-0016" + } + ] +} diff --git a/product-source/hololake-native-desktop/mobile/ios/.gitignore b/product-source/hololake-native-desktop/mobile/ios/.gitignore new file mode 100644 index 000000000..45706c522 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/.gitignore @@ -0,0 +1,4 @@ +build/ +*.xcuserstate +xcuserdata/ + diff --git a/product-source/hololake-native-desktop/mobile/ios/ExportOptions.Debug.plist b/product-source/hololake-native-desktop/mobile/ios/ExportOptions.Debug.plist new file mode 100644 index 000000000..01dcb8305 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/ExportOptions.Debug.plist @@ -0,0 +1,16 @@ + + + + + method + debugging + signingStyle + automatic + teamID + 825A9L3G7Q + stripSwiftSymbols + + uploadSymbols + + + diff --git a/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobile-Info.plist b/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobile-Info.plist new file mode 100644 index 000000000..d03325242 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobile-Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + HoloLake + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.guanghulab.hololake.pair + CFBundleURLSchemes + + hololake + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSApplicationCategoryType + public.app-category.productivity + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSLocalNetworkUsageDescription + HoloLake 仅在你明确配对后,通过同一局域网连接你的电脑端频道。 + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobileTests-Info.plist b/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobileTests-Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Generated/HoloLakeMobileTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.pbxproj b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.pbxproj new file mode 100644 index 000000000..b4fc2292f --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.pbxproj @@ -0,0 +1,425 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 147588FBBE65B668142D5DBE /* HoloLakeMobileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */; }; + 149652D6C6336752E39CEF9D /* HoloLakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */; }; + 4C8E5CF9FBFC541E3724C695 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */; }; + 4FD7821997D8E90E6594FC23 /* HoloLakeMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */; }; + 9BC7CC6D8BC355D731943912 /* HoloLakeDesign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A48AB789379757C66C6884 /* HoloLakeDesign.swift */; }; + C0E93C7456C0A3BE8BA595FC /* HoloLakeSyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */; }; + FFEEE7FC9ED4ECCD91A96AF1 /* HoloLakeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F5647617271E7611365B1 /* HoloLakeModels.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CD999C930B8B85FE1C3D884D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7BB68CAE65448CB9319F9FE1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 306917F839BF1A325E2C4CF4; + remoteInfo = HoloLakeMobile; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 001F5647617271E7611365B1 /* HoloLakeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeModels.swift; sourceTree = ""; }; + 39A48AB789379757C66C6884 /* HoloLakeDesign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeDesign.swift; sourceTree = ""; }; + 6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeMobileApp.swift; sourceTree = ""; }; + 75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeKeychain.swift; sourceTree = ""; }; + 7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeMobileTests.swift; sourceTree = ""; }; + AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = HoloLakeMobileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeSyncClient.swift; sourceTree = ""; }; + D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = HoloLakeMobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 14AB215583F1F3DFF867AD25 /* Sources */ = { + isa = PBXGroup; + children = ( + D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */, + 39A48AB789379757C66C6884 /* HoloLakeDesign.swift */, + 75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */, + 6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */, + 001F5647617271E7611365B1 /* HoloLakeModels.swift */, + B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 57FD470E035A4BE1AC23C789 = { + isa = PBXGroup; + children = ( + 14AB215583F1F3DFF867AD25 /* Sources */, + 65C6F102E3F59F8DEC7CF977 /* Tests */, + C1AA7BA4A57964238429B9FC /* Products */, + ); + sourceTree = ""; + }; + 65C6F102E3F59F8DEC7CF977 /* Tests */ = { + isa = PBXGroup; + children = ( + 7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + C1AA7BA4A57964238429B9FC /* Products */ = { + isa = PBXGroup; + children = ( + F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */, + AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 306917F839BF1A325E2C4CF4 /* HoloLakeMobile */ = { + isa = PBXNativeTarget; + buildConfigurationList = 821BFCD605046D5BD179550C /* Build configuration list for PBXNativeTarget "HoloLakeMobile" */; + buildPhases = ( + EF1823B21F5FD988A9F1C68B /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HoloLakeMobile; + packageProductDependencies = ( + ); + productName = HoloLakeMobile; + productReference = F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */; + productType = "com.apple.product-type.application"; + }; + 9F8DF7CBFC2D6315E6807301 /* HoloLakeMobileTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 94843BB695F4BFC5D7BD814C /* Build configuration list for PBXNativeTarget "HoloLakeMobileTests" */; + buildPhases = ( + C9F5955F3342913B35B01034 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + 43C074DF3E4135062FD9F2D5 /* PBXTargetDependency */, + ); + name = HoloLakeMobileTests; + packageProductDependencies = ( + ); + productName = HoloLakeMobileTests; + productReference = AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7BB68CAE65448CB9319F9FE1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 306917F839BF1A325E2C4CF4 = { + DevelopmentTeam = 825A9L3G7Q; + ProvisioningStyle = Automatic; + }; + 9F8DF7CBFC2D6315E6807301 = { + DevelopmentTeam = 825A9L3G7Q; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 9D0C9B96A22462DAABC1E8F8 /* Build configuration list for PBXProject "HoloLakeMobile" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 57FD470E035A4BE1AC23C789; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = C1AA7BA4A57964238429B9FC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 306917F839BF1A325E2C4CF4 /* HoloLakeMobile */, + 9F8DF7CBFC2D6315E6807301 /* HoloLakeMobileTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + C9F5955F3342913B35B01034 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 147588FBBE65B668142D5DBE /* HoloLakeMobileTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF1823B21F5FD988A9F1C68B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C8E5CF9FBFC541E3724C695 /* ContentView.swift in Sources */, + 9BC7CC6D8BC355D731943912 /* HoloLakeDesign.swift in Sources */, + 149652D6C6336752E39CEF9D /* HoloLakeKeychain.swift in Sources */, + 4FD7821997D8E90E6594FC23 /* HoloLakeMobileApp.swift in Sources */, + FFEEE7FC9ED4ECCD91A96AF1 /* HoloLakeModels.swift in Sources */, + C0E93C7456C0A3BE8BA595FC /* HoloLakeSyncClient.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 43C074DF3E4135062FD9F2D5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 306917F839BF1A325E2C4CF4 /* HoloLakeMobile */; + targetProxy = CD999C930B8B85FE1C3D884D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 41EDE5121626CC1CCE382706 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 825A9L3G7Q; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 0.5.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 4F5C6B6543A0A3082363F109 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 825A9L3G7Q; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 0.5.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 512CB348BCA8C1688C5D794E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = "Generated/HoloLakeMobileTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake.tests; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake"; + }; + name = Release; + }; + A44510E6B199DFF6BD5BC1EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = "Generated/HoloLakeMobileTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake.tests; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake"; + }; + name = Debug; + }; + B82D688E4449B0C6DC27D25E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "Generated/HoloLakeMobile-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake; + PRODUCT_MODULE_NAME = HoloLakeMobile; + PRODUCT_NAME = HoloLake; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BECD79B877979A2CD86E7D9C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "Generated/HoloLakeMobile-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake; + PRODUCT_MODULE_NAME = HoloLakeMobile; + PRODUCT_NAME = HoloLake; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 821BFCD605046D5BD179550C /* Build configuration list for PBXNativeTarget "HoloLakeMobile" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BECD79B877979A2CD86E7D9C /* Debug */, + B82D688E4449B0C6DC27D25E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 94843BB695F4BFC5D7BD814C /* Build configuration list for PBXNativeTarget "HoloLakeMobileTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A44510E6B199DFF6BD5BC1EF /* Debug */, + 512CB348BCA8C1688C5D794E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 9D0C9B96A22462DAABC1E8F8 /* Build configuration list for PBXProject "HoloLakeMobile" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F5C6B6543A0A3082363F109 /* Debug */, + 41EDE5121626CC1CCE382706 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7BB68CAE65448CB9319F9FE1 /* Project object */; +} diff --git a/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/xcshareddata/xcschemes/HoloLakeMobile.xcscheme b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/xcshareddata/xcschemes/HoloLakeMobile.xcscheme new file mode 100644 index 000000000..3a7b98f9c --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/HoloLakeMobile.xcodeproj/xcshareddata/xcschemes/HoloLakeMobile.xcscheme @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/ContentView.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/ContentView.swift new file mode 100644 index 000000000..008ee0913 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/ContentView.swift @@ -0,0 +1,203 @@ +import SwiftUI +import UIKit + +struct ContentView: View { + @EnvironmentObject private var client: HoloLakeSyncClient + @State private var pairingText = "" + @State private var captureTitle = "" + @State private var captureBody = "" + + var body: some View { + NavigationStack { + ZStack { + LinearGradient( + colors: [HoloLakeDesign.deep, HoloLakeDesign.lake, HoloLakeDesign.violet.opacity(0.78)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 16) { + header + if client.session == nil { pairingCard } else { dashboard } + boundaryCard + } + .padding(.horizontal, 18) + .padding(.vertical, 16) + } + } + .toolbar(.hidden, for: .navigationBar) + .alert("HoloLake", isPresented: Binding( + get: { client.errorText != nil }, + set: { if !$0 { client.errorText = nil } } + )) { + Button("知道了", role: .cancel) { client.errorText = nil } + } message: { + Text(client.errorText ?? "") + } + } + .preferredColorScheme(.dark) + } + + private var header: some View { + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 5) { + Text("HoloLake").font(.title.weight(.semibold)).tracking(2) + Text("同一频道 · iPhone 轻入口") + .font(.subheadline) + .foregroundStyle(HoloLakeDesign.muted) + } + Spacer() + Circle() + .fill(client.session == nil ? HoloLakeDesign.gold : HoloLakeDesign.mint) + .frame(width: 11, height: 11) + .shadow(color: client.session == nil ? HoloLakeDesign.gold : HoloLakeDesign.mint, radius: 8) + } + .foregroundStyle(HoloLakeDesign.pearl) + .padding(.top, 4) + } + + private var pairingCard: some View { + LakeCard { + VStack(alignment: .leading, spacing: 14) { + Text("连接电脑端").font(.title3.weight(.semibold)) + Text("先在电脑端打开“多端同步”,再扫描配对码或粘贴配对链接。配对密钥只保存在这台 iPhone 的钥匙串。") + .font(.subheadline) + .foregroundStyle(HoloLakeDesign.muted) + + TextField("hololake://pair?…", text: $pairingText, axis: .vertical) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(14) + .background(.black.opacity(0.22), in: RoundedRectangle(cornerRadius: 16)) + + HStack { + Button("从剪贴板读取") { + pairingText = UIPasteboard.general.string ?? "" + } + .buttonStyle(.bordered) + + Button("校验并配对") { + Task { await client.pair(from: pairingText) } + } + .buttonStyle(.borderedProminent) + .tint(HoloLakeDesign.gold) + .foregroundStyle(HoloLakeDesign.deep) + .disabled(pairingText.isEmpty || client.isWorking) + } + } + } + .foregroundStyle(HoloLakeDesign.pearl) + } + + private var dashboard: some View { + VStack(spacing: 16) { + LakeCard { + VStack(alignment: .leading, spacing: 14) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(client.session?.desktopName ?? "电脑端") + .font(.title3.weight(.semibold)) + Text(client.stateText) + .font(.caption) + .foregroundStyle(HoloLakeDesign.muted) + } + Spacer() + Button { + Task { await client.sync() } + } label: { + if client.isWorking { + ProgressView() + } else { + Label("同步", systemImage: "arrow.triangle.2.circlepath") + } + } + .buttonStyle(.borderedProminent) + .tint(HoloLakeDesign.mint) + .foregroundStyle(HoloLakeDesign.deep) + .disabled(client.isWorking) + } + + if let snapshot = client.snapshot { + HStack(spacing: 10) { + MetricTile(label: "作品", value: "\(snapshot.webNovel.workCount)") + MetricTile(label: "章节", value: "\(snapshot.webNovel.chapterCount)") + MetricTile(label: "教育表", value: "\(snapshot.education.activeTableCount)") + } + } else { + Text("保持静止;只有你点击同步时才会连接电脑。") + .font(.subheadline) + .foregroundStyle(HoloLakeDesign.muted) + } + } + } + + LakeCard { + VStack(alignment: .leading, spacing: 12) { + Text("随手记回频道").font(.headline) + TextField("标题(可选)", text: $captureTitle) + .padding(12) + .background(.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 14)) + TextField("正文", text: $captureBody, axis: .vertical) + .lineLimit(4...10) + .padding(12) + .background(.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 14)) + Button("写入电脑端收件箱") { + let title = captureTitle + let body = captureBody + Task { + await client.sync(captureTitle: title, captureBody: body) + if client.errorText == nil { + captureTitle = "" + captureBody = "" + } + } + } + .buttonStyle(.bordered) + .disabled(captureBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || client.isWorking) + } + } + + if let works = client.snapshot?.webNovel.works, !works.isEmpty { + LakeCard { + VStack(alignment: .leading, spacing: 12) { + Text("最近作品").font(.headline) + ForEach(works.prefix(5)) { work in + HStack { + Text(work.title).lineLimit(1) + Spacer() + Text(work.status) + .font(.caption) + .foregroundStyle(HoloLakeDesign.muted) + } + if work.id != works.prefix(5).last?.id { + Divider().overlay(.white.opacity(0.12)) + } + } + } + } + } + + Button("解除本机配对", role: .destructive) { + client.disconnect() + } + .font(.footnote) + } + .foregroundStyle(HoloLakeDesign.pearl) + } + + private var boundaryCard: some View { + VStack(alignment: .leading, spacing: 6) { + Text("HLP-IOS-CLIENT-001") + .font(.caption2.monospaced()) + .foregroundStyle(HoloLakeDesign.gold) + Text("手机是同一频道的远程身体入口,不复制电脑桌面,不在电脑离线时执行人格或行业任务。") + .font(.caption) + .foregroundStyle(HoloLakeDesign.muted) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + } +} + diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeDesign.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeDesign.swift new file mode 100644 index 000000000..797b0283a --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeDesign.swift @@ -0,0 +1,42 @@ +import SwiftUI + +enum HoloLakeDesign { + static let deep = Color(red: 0.025, green: 0.045, blue: 0.11) + static let lake = Color(red: 0.08, green: 0.14, blue: 0.32) + static let violet = Color(red: 0.34, green: 0.19, blue: 0.52) + static let pearl = Color(red: 0.95, green: 0.96, blue: 1) + static let muted = Color(red: 0.62, green: 0.66, blue: 0.78) + static let gold = Color(red: 0.96, green: 0.87, blue: 0.62) + static let mint = Color(red: 0.43, green: 0.93, blue: 0.78) +} + +struct LakeCard: View { + @ViewBuilder let content: Content + + var body: some View { + content + .padding(18) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.white.opacity(0.075), in: RoundedRectangle(cornerRadius: 24)) + .overlay { + RoundedRectangle(cornerRadius: 24) + .stroke(.white.opacity(0.14), lineWidth: 1) + } + } +} + +struct MetricTile: View { + let label: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(label).font(.caption).foregroundStyle(HoloLakeDesign.muted) + Text(value).font(.title2.weight(.semibold)).foregroundStyle(HoloLakeDesign.pearl) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(14) + .background(.white.opacity(0.055), in: RoundedRectangle(cornerRadius: 18)) + } +} + diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeKeychain.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeKeychain.swift new file mode 100644 index 000000000..345974f2c --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeKeychain.swift @@ -0,0 +1,59 @@ +import Foundation +import Security + +enum HoloLakeKeychain { + private static let service = "com.guanghulab.hololake.mobile-sync" + private static let account = "HLP-IOS-CLIENT-001" + + static func load() throws -> StoredSession? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess, let data = result as? Data else { + throw HoloLakeMobileError.keychain(status) + } + return try JSONDecoder().decode(StoredSession.self, from: data) + } + + static func save(_ session: StoredSession) throws { + let data = try JSONEncoder().encode(session) + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account + ] + let attributes: [String: Any] = [ + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + ] + let update = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + if update == errSecItemNotFound { + var insertion = query + insertion.merge(attributes) { _, new in new } + let status = SecItemAdd(insertion as CFDictionary, nil) + guard status == errSecSuccess else { throw HoloLakeMobileError.keychain(status) } + } else if update != errSecSuccess { + throw HoloLakeMobileError.keychain(update) + } + } + + static func remove() throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account + ] + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw HoloLakeMobileError.keychain(status) + } + } +} + diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeMobileApp.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeMobileApp.swift new file mode 100644 index 000000000..181084959 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeMobileApp.swift @@ -0,0 +1,17 @@ +import SwiftUI + +@main +struct HoloLakeMobileApp: App { + @StateObject private var client = HoloLakeSyncClient() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(client) + .onOpenURL { url in + Task { await client.pair(with: url) } + } + } + } +} + diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeModels.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeModels.swift new file mode 100644 index 000000000..ab00154e1 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeModels.swift @@ -0,0 +1,177 @@ +import Foundation +import Security + +struct PairingDescriptor: Equatable { + let host: String + let port: Int + let pairingID: String + let secret: Data + + init(url: URL) throws { + guard url.scheme == "hololake", url.host == "pair", + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw HoloLakeMobileError.invalidPairingLink + } + var values: [String: String] = [:] + for item in components.queryItems ?? [] { + guard let value = item.value, values[item.name] == nil else { + throw HoloLakeMobileError.invalidPairingLink + } + values[item.name] = value + } + guard let host = values["host"], !host.isEmpty, + let portText = values["port"], let port = Int(portText), (1...65535).contains(port), + let pairingID = values["id"], UUID(uuidString: pairingID) != nil, + let secretText = values["secret"], + let secret = Data(base64URL: secretText), secret.count == 32 else { + throw HoloLakeMobileError.invalidPairingLink + } + self.host = host + self.port = port + self.pairingID = pairingID + self.secret = secret + } + + var baseURL: URL { + URL(string: "http://\(host):\(port)")! + } +} + +struct StoredSession: Codable, Equatable { + let host: String + let port: Int + let deviceID: String + let sessionKey: Data + let desktopName: String + var counter: UInt64 + var cursor: UInt64 + + var baseURL: URL { + URL(string: "http://\(host):\(port)")! + } +} + +struct PairRequest: Codable { + let deviceName: String + let platform: String + let requestID: String +} + +struct PairResponse: Codable { + let schema: String + let state: String + let deviceID: String + let sessionKey: String + let desktopName: String + let rootNodeRole: String + let requestID: String +} + +struct SyncRequest: Codable { + let counter: UInt64 + let afterCursor: UInt64? + let capture: MobileCaptureInput? +} + +struct MobileCaptureInput: Codable { + let title: String + let body: String + let requestID: String +} + +struct MobileSyncSnapshot: Codable { + let schema: String + let state: String + let cursor: UInt64 + let generatedAtUnixMs: UInt64 + let desktopName: String + let rootNodeOnline: Bool + let personalChannel: MobileChannelProjection + let webNovel: MobileWebNovelProjection + let education: MobileEducationProjection + let recentCaptures: [MobileCaptureProjection] + let boundary: MobileBoundaryProjection +} + +struct MobileChannelProjection: Codable { + let home: String + let growthEventCount: UInt64 + let integrity: String +} + +struct MobileWebNovelProjection: Codable { + let workCount: UInt64 + let volumeCount: UInt64 + let chapterCount: UInt64 + let works: [MobileWebNovelWork] +} + +struct MobileWebNovelWork: Codable, Identifiable { + let workID: String + let title: String + let status: String + let updatedAtUnixMs: UInt64 + var id: String { workID } +} + +struct MobileEducationProjection: Codable { + let activeTableCount: UInt64 + let archivedTableCount: UInt64 + let unassignedTableCount: UInt64 + let sensitiveValuesIncluded: Bool +} + +struct MobileCaptureProjection: Codable, Identifiable { + let cursor: UInt64 + let captureID: String + let title: String + let body: String + let sourceDeviceID: String + let createdAtUnixMs: UInt64 + var id: String { captureID } +} + +struct MobileBoundaryProjection: Codable { + let mobileRole: String + let remoteDesktopClone: Bool + let desktopOfflineExecution: Bool + let sensitiveEducationValues: String + let modelAPI: String +} + +enum HoloLakeMobileError: LocalizedError { + case invalidPairingLink + case invalidResponse + case rejected(String) + case missingNonce + case missingSession + case keychain(OSStatus) + + var errorDescription: String? { + switch self { + case .invalidPairingLink: return "配对链接无效或已损坏。" + case .invalidResponse: return "电脑端返回了无法校验的响应。" + case .rejected(let code): return "电脑端拒绝了本次请求:\(code)" + case .missingNonce: return "加密响应缺少一次性随机数。" + case .missingSession: return "请先与电脑端 HoloLake 配对。" + case .keychain(let status): return "本机钥匙串写入失败(\(status))。" + } + } +} + +extension Data { + init?(base64URL value: String) { + var text = value.replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + text += String(repeating: "=", count: (4 - text.count % 4) % 4) + guard let data = Data(base64Encoded: text) else { return nil } + self = data + } + + var base64URL: String { + base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } +} diff --git a/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeSyncClient.swift b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeSyncClient.swift new file mode 100644 index 000000000..ffc99f63e --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeSyncClient.swift @@ -0,0 +1,190 @@ +import CryptoKit +import Foundation +import UIKit + +@MainActor +final class HoloLakeSyncClient: ObservableObject { + static let clientNumber = "HLP-IOS-CLIENT-001" + static let pairAAD = Data("hololake.mobile.pair/v1".utf8) + + @Published private(set) var session: StoredSession? + @Published private(set) var snapshot: MobileSyncSnapshot? + @Published private(set) var stateText = "尚未配对" + @Published private(set) var isWorking = false + @Published var errorText: String? + + init() { + do { + session = try HoloLakeKeychain.load() + stateText = session == nil ? "尚未配对" : "已配对,等待手动同步" + } catch { + errorText = error.localizedDescription + } + } + + func pair(with url: URL) async { + await perform("正在校验配对…") { + let descriptor = try PairingDescriptor(url: url) + let requestID = UUID().uuidString.lowercased() + let payload = PairRequest( + deviceName: UIDevice.current.name, + platform: "IOS", + requestID: requestID + ) + let responseData = try await Self.exchange( + url: descriptor.baseURL.appending(path: "v1/pair"), + key: descriptor.secret, + aad: Self.pairAAD, + headers: ["X-HoloLake-Pairing-ID": descriptor.pairingID], + payload: payload + ) + let response = try JSONDecoder().decode(PairResponse.self, from: responseData) + guard response.schema == "hololake.mobile-sync/v1", + response.state == "PAIRED", + response.rootNodeRole == "USER_LOCAL_COMPUTER_TERMINAL", + response.requestID == requestID, + let sessionKey = Data(base64URL: response.sessionKey), sessionKey.count == 32 else { + throw HoloLakeMobileError.invalidResponse + } + let next = StoredSession( + host: descriptor.host, + port: descriptor.port, + deviceID: response.deviceID, + sessionKey: sessionKey, + desktopName: response.desktopName, + counter: 0, + cursor: 0 + ) + try HoloLakeKeychain.save(next) + session = next + stateText = "已与 \(response.desktopName) 配对" + } + } + + func pair(from text: String) async { + guard let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)) else { + errorText = HoloLakeMobileError.invalidPairingLink.localizedDescription + return + } + await pair(with: url) + } + + func sync(captureTitle: String? = nil, captureBody: String? = nil) async { + await perform("正在与电脑同步…") { + guard var current = session else { throw HoloLakeMobileError.missingSession } + current.counter += 1 + // Consume and persist the counter before transport. If the root node accepts a + // request but its response is lost, the next manual retry still moves forward. + try HoloLakeKeychain.save(current) + session = current + let capture: MobileCaptureInput? + if let body = captureBody?.trimmingCharacters(in: .whitespacesAndNewlines), !body.isEmpty { + capture = MobileCaptureInput( + title: (captureTitle ?? "").trimmingCharacters(in: .whitespacesAndNewlines), + body: body, + requestID: UUID().uuidString.lowercased() + ) + } else { + capture = nil + } + let aad = Data("hololake.mobile.sync/v1:\(current.deviceID)".utf8) + let responseData = try await Self.exchange( + url: current.baseURL.appending(path: "v1/sync"), + key: current.sessionKey, + aad: aad, + headers: ["X-HoloLake-Device-ID": current.deviceID], + payload: SyncRequest(counter: current.counter, afterCursor: nil, capture: capture) + ) + let nextSnapshot = try JSONDecoder().decode(MobileSyncSnapshot.self, from: responseData) + guard nextSnapshot.schema == "hololake.mobile-sync/v1", + nextSnapshot.state == "SYNCED_WITH_ROOT_NODE", + nextSnapshot.rootNodeOnline, + nextSnapshot.boundary.mobileRole == "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM", + !nextSnapshot.boundary.remoteDesktopClone, + !nextSnapshot.boundary.desktopOfflineExecution else { + throw HoloLakeMobileError.invalidResponse + } + current.cursor = nextSnapshot.cursor + try HoloLakeKeychain.save(current) + session = current + snapshot = nextSnapshot + stateText = "已同步 · \(nextSnapshot.desktopName)" + } + } + + func disconnect() { + do { + try HoloLakeKeychain.remove() + session = nil + snapshot = nil + stateText = "尚未配对" + } catch { + errorText = error.localizedDescription + } + } + + private func perform(_ activeText: String, operation: () async throws -> Void) async { + guard !isWorking else { return } + isWorking = true + errorText = nil + stateText = activeText + do { + try await operation() + } catch { + errorText = error.localizedDescription + stateText = session == nil ? "尚未配对" : "同步未完成" + } + isWorking = false + } + + static func exchange( + url: URL, + key: Data, + aad: Data, + headers: [String: String], + payload: Payload, + session: URLSession = .shared + ) async throws -> Data { + let clear = try JSONEncoder().encode(payload) + let nonceData = randomNonce() + let nonce = try ChaChaPoly.Nonce(data: nonceData) + let sealed = try ChaChaPoly.seal( + clear, + using: SymmetricKey(data: key), + nonce: nonce, + authenticating: aad + ) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.timeoutInterval = 8 + request.cachePolicy = .reloadIgnoringLocalCacheData + request.httpBody = sealed.ciphertext + sealed.tag + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + request.setValue(nonceData.base64URL, forHTTPHeaderField: "X-HoloLake-Nonce") + headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } + let (body, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { + throw HoloLakeMobileError.invalidResponse + } + guard http.statusCode == 200 else { + let code = (try? JSONSerialization.jsonObject(with: body) as? [String: Any])?["code"] as? String + throw HoloLakeMobileError.rejected(code ?? "HTTP_\(http.statusCode)") + } + guard let nonceHeader = http.value(forHTTPHeaderField: "X-HoloLake-Nonce"), + let responseNonceData = Data(base64URL: nonceHeader), responseNonceData.count == 12 else { + throw HoloLakeMobileError.missingNonce + } + let responseNonce = try ChaChaPoly.Nonce(data: responseNonceData) + guard body.count >= 16 else { throw HoloLakeMobileError.invalidResponse } + let responseBox = try ChaChaPoly.SealedBox( + nonce: responseNonce, + ciphertext: body.dropLast(16), + tag: body.suffix(16) + ) + return try ChaChaPoly.open(responseBox, using: SymmetricKey(data: key), authenticating: aad) + } + + static func randomNonce() -> Data { + Data((0..<12).map { _ in UInt8.random(in: .min ... .max) }) + } +} diff --git a/product-source/hololake-native-desktop/mobile/ios/Tests/HoloLakeMobileTests.swift b/product-source/hololake-native-desktop/mobile/ios/Tests/HoloLakeMobileTests.swift new file mode 100644 index 000000000..eae55ba7c --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/Tests/HoloLakeMobileTests.swift @@ -0,0 +1,44 @@ +import CryptoKit +import XCTest +@testable import HoloLakeMobile + +final class HoloLakeMobileTests: XCTestCase { + func testPairingCoordinateParsesOnlyCompleteLink() throws { + let secret = Data(repeating: 7, count: 32).base64URL + let id = UUID().uuidString.lowercased() + let url = try XCTUnwrap(URL(string: "hololake://pair?host=192.168.1.8&port=37421&id=\(id)&secret=\(secret)")) + let value = try PairingDescriptor(url: url) + XCTAssertEqual(value.host, "192.168.1.8") + XCTAssertEqual(value.port, 37421) + XCTAssertEqual(value.pairingID, id) + XCTAssertEqual(value.secret, Data(repeating: 7, count: 32)) + } + + func testPairingCoordinateRejectsMissingSecret() throws { + let url = try XCTUnwrap(URL(string: "hololake://pair?host=127.0.0.1&port=37421&id=\(UUID().uuidString)")) + XCTAssertThrowsError(try PairingDescriptor(url: url)) + } + + func testPairingCoordinateRejectsDuplicateKeys() throws { + let secret = Data(repeating: 7, count: 32).base64URL + let id = UUID().uuidString.lowercased() + let url = try XCTUnwrap(URL(string: "hololake://pair?host=192.168.1.8&host=127.0.0.1&port=37421&id=\(id)&secret=\(secret)")) + XCTAssertThrowsError(try PairingDescriptor(url: url)) + } + + func testCryptoKitPayloadMatchesServerWireShape() throws { + let key = SymmetricKey(data: Data(repeating: 3, count: 32)) + let nonce = try ChaChaPoly.Nonce(data: Data(repeating: 9, count: 12)) + let aad = Data("hololake.mobile.pair/v1".utf8) + let clear = Data("hello".utf8) + let sealed = try ChaChaPoly.seal(clear, using: key, nonce: nonce, authenticating: aad) + let wire = sealed.ciphertext + sealed.tag + XCTAssertEqual(wire.count, clear.count + 16) + let restored = try ChaChaPoly.open( + ChaChaPoly.SealedBox(nonce: nonce, ciphertext: wire.dropLast(16), tag: wire.suffix(16)), + using: key, + authenticating: aad + ) + XCTAssertEqual(restored, clear) + } +} diff --git a/product-source/hololake-native-desktop/mobile/ios/project.yml b/product-source/hololake-native-desktop/mobile/ios/project.yml new file mode 100644 index 000000000..b733e4c91 --- /dev/null +++ b/product-source/hololake-native-desktop/mobile/ios/project.yml @@ -0,0 +1,63 @@ +name: HoloLakeMobile +options: + bundleIdPrefix: com.guanghulab + deploymentTarget: + iOS: "17.0" +settings: + base: + DEVELOPMENT_TEAM: 825A9L3G7Q + CODE_SIGN_STYLE: Automatic + SWIFT_VERSION: "5.0" + CURRENT_PROJECT_VERSION: 1 + MARKETING_VERSION: 0.5.0 + TARGETED_DEVICE_FAMILY: "1" + SUPPORTS_MACCATALYST: false +targets: + HoloLakeMobile: + type: application + platform: iOS + sources: + - path: Sources + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.guanghulab.hololake + PRODUCT_NAME: HoloLake + PRODUCT_MODULE_NAME: HoloLakeMobile + info: + path: Generated/HoloLakeMobile-Info.plist + properties: + CFBundleDisplayName: HoloLake + CFBundleShortVersionString: "$(MARKETING_VERSION)" + CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" + LSApplicationCategoryType: public.app-category.productivity + NSLocalNetworkUsageDescription: HoloLake 仅在你明确配对后,通过同一局域网连接你的电脑端频道。 + UIApplicationSupportsIndirectInputEvents: true + UIRequiresFullScreen: true + UILaunchScreen: {} + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + CFBundleURLTypes: + - CFBundleTypeRole: Editor + CFBundleURLName: com.guanghulab.hololake.pair + CFBundleURLSchemes: + - hololake + NSAppTransportSecurity: + NSAllowsLocalNetworking: true + scheme: + testTargets: + - HoloLakeMobileTests + HoloLakeMobileTests: + type: bundle.unit-test + platform: iOS + sources: + - path: Tests + dependencies: + - target: HoloLakeMobile + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.guanghulab.hololake.tests + TEST_HOST: "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake" + BUNDLE_LOADER: "$(TEST_HOST)" + info: + path: Generated/HoloLakeMobileTests-Info.plist + properties: {} diff --git a/product-source/hololake-native-desktop/package-lock.json b/product-source/hololake-native-desktop/package-lock.json index a6b47dd91..17cf42c6f 100644 --- a/product-source/hololake-native-desktop/package-lock.json +++ b/product-source/hololake-native-desktop/package-lock.json @@ -1,17 +1,26 @@ { "name": "hololake-native-desktop", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hololake-native-desktop", - "version": "0.4.1", + "version": "0.5.0", "dependencies": { + "@fortune-sheet/react": "1.0.4", + "@lexical/history": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/react": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.0", "dompurify": "^3.4.13", + "lexical": "0.49.0", "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -750,6 +759,113 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@formulajs/formulajs": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@formulajs/formulajs/-/formulajs-2.9.3.tgz", + "integrity": "sha512-WpgiuJaBl/Hcda9Ti8a7mlnw/vUZkJrtjABvojz6P6mo6d8EscudyA7iAt/kTLsgi0c8zfmzQd/Yjb4ASFkw+g==", + "license": "MIT", + "dependencies": { + "bessel": "^1.0.2", + "jstat": "^1.9.2" + }, + "bin": { + "implementation-stats": "bin/implementation-stats" + } + }, + "node_modules/@fortune-sheet/core": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fortune-sheet/core/-/core-1.0.4.tgz", + "integrity": "sha512-CDnUVebfvtT++CymNRw0qnY4sz6zYO3KZazlH+nG6otkRkZ05Bvt7NXqVhmKKSSxIvJR4R6h50abu/dVGBpjpw==", + "license": "MIT", + "dependencies": { + "@fortune-sheet/formula-parser": "^0.2.13", + "dayjs": "^1.11.0", + "immer": "^9.0.12", + "lodash": "^4.17.21", + "numeral": "^2.0.6", + "uuid": "^8.3.2" + } + }, + "node_modules/@fortune-sheet/formula-parser": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@fortune-sheet/formula-parser/-/formula-parser-0.2.13.tgz", + "integrity": "sha512-za2ZVQ5ZfMSPCtL8MdqhCthPip7AoeU4MPyd5UDo4RCl9/arrv1Jz0y755mACVVi4suSZxrzWoJGDkLmofoSmw==", + "license": "MIT", + "dependencies": { + "@formulajs/formulajs": "^2.9.3", + "tiny-emitter": "^2.1.0" + } + }, + "node_modules/@fortune-sheet/react": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fortune-sheet/react/-/react-1.0.4.tgz", + "integrity": "sha512-v+BU5mmp2hhUe4gRF+vOJ3j8zZkzHU0kzhTZzp+Nn00wA/RArMr+CO+SAluKlCPAsTZgPwmgOEK685WPqJ5XNw==", + "license": "MIT", + "dependencies": { + "@fortune-sheet/core": "^1.0.4", + "@types/regenerator-runtime": "^0.13.6", + "immer": "^9.0.12", + "lodash": "^4.17.21", + "regenerator-runtime": "^0.14.1" + }, + "peerDependencies": { + "react": ">= 18.2", + "react-dom": ">= 18.2" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -800,6 +916,491 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/a11y": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.49.0.tgz", + "integrity": "sha512-ypvO0SI9DCzEAjxkRLq+gNzRqfjg632RCkwu16Hk94PCBrEmcDhx5iUGg3SRJC7WnM2wXC/F/SB8yWDEfq7tEw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/clipboard": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.49.0.tgz", + "integrity": "sha512-AVKj21xH1qU7JAFA/v0hCoafa+Yti1I7cHG+JQIgc/EqCtP8ePeJyIfTPNN/tXskoCdq++HewQoiSXRwwlocVg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "@types/trusted-types": "^2.0.7", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/code-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.49.0.tgz", + "integrity": "sha512-29sXI4ydcN/OyBJNlZXkGKtQXzfiIWet+YN20+wT97zkqityOvRilK6qzTRz9Cd0ungEPlJENqXQhsy6BJeXpg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.49.0.tgz", + "integrity": "sha512-LgjNlvRiuO/QF36aG6kfR4ptEvSw587o2+YF6SOsaOB7iw2Z3pqpsKloWSEF4QXaLFI65Li8JjMdQj+BaFMIOA==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/mark": "0.49.0", + "@lexical/table": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/dragon": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.49.0.tgz", + "integrity": "sha512-62/4DP5qyX/l4Yf5qRyyQrs9BV725eRU3OmLUW6g7T5xrcHXxAo7tia/NvqjqvXdpvQzyHjWgsy7dMitGITUsw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/extension": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.49.0.tgz", + "integrity": "sha512-Wv0VsuqxorbxHCK4ms1PwAu6cXIGNLtflq66auF+zwxOtBprkSFV8VzzzdKLOYD2admP0VK6EqVCeGXFK1GggA==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "@preact/signals-core": "^1.14.1", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.49.0.tgz", + "integrity": "sha512-aQTpNjrEO5apsaohWGl4ku22EgHpWUzZg5CIzvriS2A1vNmIfzLoCL+lDFpzAwk3DT1Kj1yvGHh5TvhYsb9D8w==", + "license": "MIT", + "dependencies": { + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/history": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.49.0.tgz", + "integrity": "sha512-uQdtEd34gIJklXNSdHS2Wko1zxx1xUMVXbiodcLO6a3GeFTE5bKnx6af1zGX78KpYhUQYnHWorKuUvtdZlJPaA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/html": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.49.0.tgz", + "integrity": "sha512-NQqAydKzRjQl7Jx+bTTyC2iuz5uhuDaQX0SSPrdfgaFDCPjqQEejcBkCt1QXnu1CkbVHutZKVGVzljx40Y6y+Q==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/internal": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.49.0.tgz", + "integrity": "sha512-s+XjPC7Qb39A/Xx9ahcz1s69CPix4ultaqyW+MDG0AXYW2quDr7m0USqReKIAiuoLIWtGN5HW8BwV2Y6t4qr6Q==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/link": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.49.0.tgz", + "integrity": "sha512-CsZVu1OfnPOn6IKWVO5DbmKwIuicGtgWSzrZMdMB8w6yPoMgFyPWeVulhPE7LNz2FhaQCWv+lVVYVAiJrUzJsg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/list": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.49.0.tgz", + "integrity": "sha512-zs6wYkxakDRcJO0KmwrPPHgeLLIxzjD+P2CRu+scJCHRuA5e2iSw2iFjH5n/LlWBrlnPMSjeQse4QqsRy9nnqA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/mark": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.49.0.tgz", + "integrity": "sha512-JSYD8UYQQOMT6oDYh1KsLPK3KMGfQzU7E9fJkugKEO0/XebfsmiuNtkLCGDVIZOo8bNHlwb1YVvmswC2bjotMQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/markdown": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.49.0.tgz", + "integrity": "sha512-n7x/3OLdi5R0ztICCn8udxRIWFZFVtmQS3a06Tjkyvm/j/dPlFQ/henKSOFjjX/IqVzQzSmiNaDhqKWxXM5npQ==", + "license": "MIT", + "dependencies": { + "@lexical/code-core": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/overflow": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.49.0.tgz", + "integrity": "sha512-h5Qjv2SkpI76kaukvKJzvWehG9hwB06GEmAnp6aJaioOzoDcBAeWpmYBebtBUwRm0PNLFe3YICFFLpZ5Tg5VoQ==", + "license": "MIT", + "dependencies": { + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.49.0.tgz", + "integrity": "sha512-l7IuUj9n9CtFfz4Fz6zU6mmO+VkcnvyyebABx+lHevvTa7B6YI5PPVgABFfzdyPanyW/FGs7qpKBf59inAqbkg==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/react": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.49.0.tgz", + "integrity": "sha512-NqXF4i/IBFKVDDLDJYTwmBXE++MQuIrTQs7VTux6IPyDEpyfeFMn1rQTFb949S3f7rs33Rcu1uGtFbODQfS1Iw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.19", + "@lexical/a11y": "0.49.0", + "@lexical/devtools-core": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/hashtag": "0.49.0", + "@lexical/history": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/mark": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/overflow": "0.49.0", + "@lexical/plain-text": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/table": "0.49.0", + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "@lexical/yjs": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "yjs": { + "optional": true + } + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.49.0.tgz", + "integrity": "sha512-LM+d8ULmQvi2Av6UaW4NAIa/Bj9FOo9oh6+dglTzuoFPFQ3sogy5c38i2K9k0ul7aE+bFsXkSmHbu4LkrHIkBQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/selection": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.49.0.tgz", + "integrity": "sha512-08Vd1+VoC6YnztWOFWVsqF/Hxw5EP8qeL1c7t3+rVCV8revLzXxdQw+vbPX3tgM4fmjOBDUXk6Ws1+rTtsqjcQ==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/table": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.49.0.tgz", + "integrity": "sha512-PG+dxSTBPbUZUY2kBZm0pJuqCEOGom4RrGrc6Jlj/7o0F37k/gGRVprFSRKuf3CqO03QIP6qRovXGDuKb+Exuw==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.49.0.tgz", + "integrity": "sha512-mowedvbvx0HDaW+ymVdYmWVhueqgauhhqIWaCtbJj33tPk8pOu1BB0pZuJQbOB+1bDnFiZhkJV8W/CvR2M9lEA==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.49.0.tgz", + "integrity": "sha512-Jaa6DERBqxiFOFa49VPRV1WOb7mzRbMZ5U+v+RFagjzTmBhfxDmEkbQQ9nYTU3n3DPfdT8Hkyffextmw04etXg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/yjs": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.49.0.tgz", + "integrity": "sha512-uqV3D0AOLqQ1f/Bo6ZtuDLoJYGlBm0RlucNYhNvfMAncEw0f/xHc1pTyXeWjdCI/iWgWR/MV6hgVi26TSPs+Vg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -817,6 +1418,16 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@preact/signals-core": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz", + "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1516,12 +2127,17 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/regenerator-runtime": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@types/regenerator-runtime/-/regenerator-runtime-0.13.8.tgz", + "integrity": "sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", @@ -1680,6 +2296,15 @@ "node": ">=6.0.0" } }, + "node_modules/bessel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz", + "integrity": "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/browserslist": { "version": "4.28.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", @@ -1759,6 +2384,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1915,6 +2546,27 @@ "node": ">=6.9.0" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1948,6 +2600,56 @@ "node": ">=6" } }, + "node_modules/jstat": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz", + "integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==" + }, + "node_modules/lexical": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.49.0.tgz", + "integrity": "sha512-9V1ZIzGpJEd8rIN+nN7veL4fW4fFWbS66Un4JNqSZB4D5t9euzN9+3+jEXy83FjNjNy0MiiUI3+DQaGCLYko0w==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2016,6 +2718,15 @@ "node": ">=18" } }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -2117,6 +2828,12 @@ "node": ">=0.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -2210,6 +2927,18 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2258,7 +2987,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2299,6 +3028,19 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -2487,6 +3229,24 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yjs": { + "version": "13.6.32", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz", + "integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } } } } diff --git a/product-source/hololake-native-desktop/package.json b/product-source/hololake-native-desktop/package.json index 5df834570..91bd222cd 100644 --- a/product-source/hololake-native-desktop/package.json +++ b/product-source/hololake-native-desktop/package.json @@ -1,7 +1,7 @@ { "name": "hololake-native-desktop", "private": true, - "version": "0.4.1", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", @@ -11,10 +11,19 @@ "tauri": "tauri" }, "dependencies": { + "@fortune-sheet/react": "1.0.4", + "@lexical/history": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/react": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.0", "dompurify": "^3.4.13", + "lexical": "0.49.0", "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -27,5 +36,8 @@ "typescript": "~5.9.3", "vite": "^7.3.5", "vitest": "^4.0.18" + }, + "overrides": { + "uuid": "11.1.1" } } diff --git a/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs b/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs new file mode 100644 index 000000000..82ab44bd3 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/channel-workbench-runtime.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/channel_workbench.rs') +const frontend = [read('src/modules/channel-workbench/index.tsx'), read('src/modules/channel-workbench/document-engine.tsx'), read('src/modules/channel-workbench/spreadsheet-engine.tsx')].join('\n') + +test('channel workbench is a signed account-local adapter with durable user data', () => { + assert.equal(contract.candidate_number, 'HLP-DONOR-CAND-0002') + assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001') + assert.equal(contract.data_boundary.scope, 'CURRENT_AUTHENTICATED_ACCOUNT_LOCAL') + assert.equal(contract.data_boundary.package_unmount_deletes_user_data, false) + assert.equal(modulePackage.manifest.adapter, 'channel-workbench-v1') + assert.deepEqual(modulePackage.manifest.permissions, ['CHANNEL_DOCUMENT_READ', 'CHANNEL_DOCUMENT_WRITE', 'CHANNEL_SPREADSHEET_READ', 'CHANNEL_SPREADSHEET_WRITE']) + assert.match(rust, /require_active_module_adapter/) + assert.match(rust, /workbench_receipts_no_update/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) +}) + +test('document and spreadsheet cross only their exact numbered routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0022') + assert.deepEqual(routes.map((route) => route.operation_number), ['HLP-NIPC-OP-0074', 'HLP-NIPC-OP-0075', 'HLP-NIPC-OP-0076']) + assert.deepEqual(routes.map((route) => route.alias), ['get_channel_workbench_snapshot', 'save_channel_document', 'save_channel_spreadsheet']) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0022')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) +}) + +test('the legacy engines are preserved but loaded only inside the module chunk', () => { + assert.match(frontend, /LexicalComposer/) + assert.match(frontend, /Workbook/) + assert.match(frontend, /calculateFormula/) + assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/channel-workbench'\)/) +}) diff --git a/product-source/hololake-native-desktop/scripts/compile-gls-runtime-registry.mjs b/product-source/hololake-native-desktop/scripts/compile-gls-runtime-registry.mjs index 5a90b8876..24c86bbd8 100644 --- a/product-source/hololake-native-desktop/scripts/compile-gls-runtime-registry.mjs +++ b/product-source/hololake-native-desktop/scripts/compile-gls-runtime-registry.mjs @@ -12,9 +12,10 @@ const sourceRoot = explicitSourceRoot || (repoRoot ? path.join(repoRoot, 'gls') const sourceCommit = args.get('--source-commit') const output = args.get('--output') const projectionsPath = args.get('--projections') +const referencesPath = args.get('--references') -if (!repoRoot || !sourceRoot || !sourceCommit || !output || !projectionsPath) { - throw new Error('usage: compile-gls-runtime-registry.mjs --repo-root --source-commit --output --projections ') +if (!repoRoot || !sourceRoot || !sourceCommit || !output || !projectionsPath || !referencesPath) { + throw new Error('usage: compile-gls-runtime-registry.mjs --repo-root --source-commit --output --projections --references ') } if (!/^[a-f0-9]{40}$/.test(sourceCommit)) throw new Error('source commit must be a full SHA-1') @@ -168,6 +169,18 @@ if (projectionManifest.schema !== 'hololake.gls-executable-projections/v2' || projectionManifest.source_commit !== sourceCommit) { throw new Error('projection manifest does not match the selected REPO-012 source commit') } +const referenceManifest = JSON.parse(await readFile(referencesPath, 'utf8')) +if (referenceManifest.schema !== 'hololake.gls-numbered-reference-nodes/v1' + || referenceManifest.record_id !== 'HLP-GLS-NUMBERED-REFERENCE-REGISTRY-001' + || referenceManifest.source?.repository !== 'REPO-012' + || referenceManifest.source?.commit !== sourceCommit + || referenceManifest.policy?.number_is_coordinate_not_authority !== true + || referenceManifest.policy?.independent_protocol_source_required_for_execution !== true + || referenceManifest.policy?.reference_only_nodes_may_execute !== false + || referenceManifest.policy?.unknown_reference !== 'FAIL_CLOSED' + || referenceManifest.policy?.unresolved_number_reference_allowed !== false) { + throw new Error('numbered reference manifest does not match the selected REPO-012 source commit or safety boundary') +} const protocolRegistryPath = 'gls/GLS-PROTOCOL-REGISTRY.json' const glsEntryPath = 'gls/GLS-ENTRY.hdlp' @@ -222,29 +235,35 @@ for (const relative of await walk(routingRoot)) { } } -const candidates = (await walk(sourceRoot)) - .map((relative) => ({ relative, match: path.basename(relative).match(/^(GLS-\d{4}).*\.hdlp$/) })) +const allSourceFiles = await walk(sourceRoot) +const candidates = allSourceFiles + .map((relative) => ({ relative, match: path.basename(relative).match(/^(GLS-\d{4})(?:[^0-9].*)?\.(hdlp|md)$/) })) .filter((item) => item.match) - .map((item) => ({ id: item.match[1], relative: item.relative })) + .map((item) => ({ id: item.match[1], relative: item.relative, source_format: item.match[2] === 'hdlp' ? 'HDLP_PROTOCOL_SOURCE' : 'LEGACY_MARKDOWN_EVIDENCE' })) const grouped = new Map() for (const candidate of candidates) { const group = grouped.get(candidate.id) || [] - group.push(candidate.relative) + group.push(candidate) grouped.set(candidate.id, group) } const selectedSource = new Map() for (const [id, sources] of grouped) { - sources.sort((left, right) => sourceRank(left) - sourceRank(right) || left.localeCompare(right)) - const relative = sources[0] + const protocolSources = sources.filter((source) => source.source_format === 'HDLP_PROTOCOL_SOURCE') + if (protocolSources.length === 0) continue + protocolSources.sort((left, right) => sourceRank(left.relative) - sourceRank(right.relative) + || left.relative.localeCompare(right.relative)) + const selected = protocolSources[0] + const relative = selected.relative selectedSource.set(id, { relative, raw: await readFile(path.join(sourceRoot, relative), 'utf8'), - alternate_source_count: sources.length - 1, + source_format: selected.source_format, + alternate_source_count: protocolSources.length - 1, }) } -const knownSourceIds = new Set(selectedSource.keys()) +let knownSourceIds = new Set(selectedSource.keys()) const protocolRegistryIds = new Set(registryMetadata.keys()) const draftIds = new Set((protocolRegistry.registered_draft_protocols || []).map((entry) => entry.id)) const legacyDependencyTargets = new Set() @@ -256,6 +275,56 @@ for (const id of draftIds) { } const legacyDependencyCycles = stronglyConnectedComponents(draftGraph) +// 旧 Notion 导出只在现行协议真实引用且独立 HDLP 正本缺席时作为历史证据节点进入。 +// 未被现行图引用的旧页面不得因为文件名像 GLS 编号就自动取得当前注册地位。 +for (const id of [...legacyDependencyTargets].filter((target) => !knownSourceIds.has(target))) { + if (!authorityDeclarations.has(id)) continue + const legacySources = (grouped.get(id) || []).filter((source) => source.source_format === 'LEGACY_MARKDOWN_EVIDENCE') + if (legacySources.length === 0) continue + legacySources.sort((left, right) => sourceRank(left.relative) - sourceRank(right.relative) || left.relative.localeCompare(right.relative)) + const selected = legacySources[0] + selectedSource.set(id, { + relative: selected.relative, + raw: await readFile(path.join(sourceRoot, selected.relative), 'utf8'), + source_format: selected.source_format, + alternate_source_count: legacySources.length - 1, + }) +} +knownSourceIds = new Set(selectedSource.keys()) + +const missingSourceTargets = [...legacyDependencyTargets].filter((id) => !knownSourceIds.has(id)).sort() +const referenceIds = new Set() +const referenceNodeNumbers = new Set() +const numberedReferenceNodes = [] +for (const node of referenceManifest.nodes || []) { + if (!/^GLS-\d{4}$/.test(node.protocol_id) + || !/^HLP-GLS-REF-\d{4}$/.test(node.node_number) + || !node.title + || !['NORMATIVE_REFERENCE', 'SCHEMA_IMPORT', 'EVIDENCE_ONLY'].includes(node.reference_kind) + || node.source_state !== 'ROADMAP_REFERENCE_ONLY' + || !referenceIds.add(node.protocol_id) + || !referenceNodeNumbers.add(node.node_number)) { + throw new Error(`invalid or duplicated numbered reference node: ${node.protocol_id || 'UNKNOWN'}`) + } + const evidencePaths = [] + for (const relative of allSourceFiles) { + if (!/\.(?:hdlp|md|json|ya?ml)$/.test(relative)) continue + const raw = await readFile(path.join(sourceRoot, relative), 'utf8') + if (raw.includes(node.protocol_id)) evidencePaths.push(`gls/${relative}`) + } + if (evidencePaths.length === 0) throw new Error(`numbered reference node has no REPO-012 evidence: ${node.protocol_id}`) + numberedReferenceNodes.push({ + ...node, + execution_state: 'REFERENCE_ONLY_NOT_EXECUTABLE', + evidence_paths: evidencePaths.sort().slice(0, 12), + }) +} +if (missingSourceTargets.length !== referenceIds.size + || missingSourceTargets.some((id) => !referenceIds.has(id)) + || [...referenceIds].some((id) => !missingSourceTargets.includes(id))) { + throw new Error(`numbered reference registry coverage mismatch: missing=${missingSourceTargets.join(',')} registered=${[...referenceIds].sort().join(',')}`) +} + const protocols = [] for (const [id, source] of [...selectedSource.entries()].sort(([left], [right]) => left.localeCompare(right))) { const heading = source.raw.split(/\r?\n/).find((line) => line.startsWith('#') && line.includes(id)) @@ -280,6 +349,8 @@ for (const [id, source] of [...selectedSource.entries()].sort(([left], [right]) enters_runtime_graph: false, target_registered: protocolRegistryIds.has(target), target_numbered_source_available: knownSourceIds.has(target), + target_number_coordinate_available: knownSourceIds.has(target) || referenceIds.has(target), + target_resolution: knownSourceIds.has(target) ? 'NUMBERED_PROTOCOL_SOURCE' : referenceIds.has(target) ? 'NUMBERED_REFERENCE_NODE' : 'UNRESOLVED', })), ...(projection?.dependencies || []).map((target) => ({ target, @@ -288,18 +359,21 @@ for (const [id, source] of [...selectedSource.entries()].sort(([left], [right]) enters_runtime_graph: true, target_registered: protocolRegistryIds.has(target), target_numbered_source_available: knownSourceIds.has(target), + target_number_coordinate_available: knownSourceIds.has(target) || referenceIds.has(target), + target_resolution: knownSourceIds.has(target) ? 'NUMBERED_PROTOCOL_SOURCE' : referenceIds.has(target) ? 'NUMBERED_REFERENCE_NODE' : 'UNRESOLVED', })), ] const activationBlockers = [] if (!projection) activationBlockers.push('NO_EXECUTABLE_ADAPTER') if (registrationState === 'DISCOVERED_UNRECONCILED') activationBlockers.push('REGISTRATION_NOT_RECONCILED') if (authorityConflict) activationBlockers.push('REGISTRATION_SOURCE_CONFLICT') - if (legacyDependencies.some((target) => !knownSourceIds.has(target)) && !projection) activationBlockers.push('DEPENDENCY_NUMBERED_SOURCE_MISSING') + if (legacyDependencies.some((target) => !knownSourceIds.has(target) && !referenceIds.has(target)) && !projection) activationBlockers.push('DEPENDENCY_NUMBER_COORDINATE_MISSING') protocols.push({ id, title: cleanHeading(heading || id, id), source_status: sourceStatus(source.raw), source_path: `gls/${source.relative}`, + source_format: source.source_format, source_sha256: createHash('sha256').update(source.raw).digest('hex'), alternate_source_count: source.alternate_source_count, registration: { @@ -354,6 +428,7 @@ for (const protocol of protocols) { } const dependenciesNotInRegistry = [...legacyDependencyTargets].filter((id) => !protocolRegistryIds.has(id)).sort() const dependenciesWithoutNumberedSource = [...legacyDependencyTargets].filter((id) => !knownSourceIds.has(id)).sort() +const unresolvedNumberReferences = dependenciesWithoutNumberedSource.filter((id) => !referenceIds.has(id)) const numberedSourcesNotInProtocolRegistry = [...knownSourceIds].filter((id) => !protocolRegistryIds.has(id)).sort() const registry = { schema: 'hololake.gls-runtime-manifest/v2', @@ -384,6 +459,10 @@ const registry = { legacy_dependency_target_count: legacyDependencyTargets.size, dependencies_not_in_protocol_registry: dependenciesNotInRegistry, dependencies_without_numbered_source: dependenciesWithoutNumberedSource, + numbered_reference_node_count: numberedReferenceNodes.length, + unresolved_number_references: unresolvedNumberReferences, + unresolved_number_reference_count: unresolvedNumberReferences.length, + every_dependency_has_number_coordinate: unresolvedNumberReferences.length === 0, numbered_sources_not_in_protocol_registry: numberedSourcesNotInProtocolRegistry, legacy_dependency_cycles: legacyDependencyCycles, source_reference_cycles: legacyDependencyCycles, @@ -395,8 +474,10 @@ const registry = { protocol_count: protocols.length, executable_projection_count: executableCount, inventoried_not_executable_count: protocols.length - executableCount, + number_coordinate_count: protocols.length + numberedReferenceNodes.length, + numbered_reference_nodes: numberedReferenceNodes, protocols, } await writeFile(output, `${JSON.stringify(registry, null, 2)}\n`) -console.log(`GLS_RUNTIME_MANIFEST_COMPILED protocols=${protocols.length} registered=${protocolRegistryIds.size} executable=${executableCount} legacy_cycles=${legacyDependencyCycles.length} output=${output}`) +console.log(`GLS_RUNTIME_MANIFEST_COMPILED protocols=${protocols.length} references=${numberedReferenceNodes.length} unresolved=${unresolvedNumberReferences.length} registered=${protocolRegistryIds.size} executable=${executableCount} legacy_cycles=${legacyDependencyCycles.length} output=${output}`) diff --git a/product-source/hololake-native-desktop/scripts/compile-unified-number-tree.mjs b/product-source/hololake-native-desktop/scripts/compile-unified-number-tree.mjs new file mode 100644 index 000000000..b50240aa3 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/compile-unified-number-tree.mjs @@ -0,0 +1,162 @@ +import { createHash } from 'node:crypto' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const paths = { + identity: resolve(root, 'contracts/zero-core-numbering-kernel.json'), + webview: resolve(root, 'contracts/numbered-ipc-registry.json'), + broker: resolve(root, 'contracts/direct-local-broker-numbered-registry.json'), + gls: resolve(root, 'contracts/gls-runtime-registry.json'), + output: resolve(root, 'generated/unified-number-coordinate-tree.json'), +} + +function readSource(path) { + const bytes = readFileSync(path) + return { + value: JSON.parse(bytes.toString('utf8')), + sha256: createHash('sha256').update(bytes).digest('hex'), + } +} + +export function compileUnifiedNumberTree() { + const identity = readSource(paths.identity) + const webview = readSource(paths.webview) + const broker = readSource(paths.broker) + const gls = readSource(paths.gls) + const routes = [ + ...webview.value.operations.map((route) => ({ + transport: 'TAURI_WEBVIEW_NUMBERED_IPC', + protocolVersion: webview.value.runtime.protocol_version, + callerNumber: webview.value.runtime.caller_number, + channelNumber: route.channel_number, + moduleNumber: route.module_number, + operationNumber: route.operation_number, + targetNumber: route.target_number, + alias: route.alias, + admission: route.admission, + effect: route.effect, + evidence: 'HASH_CHAINED_NUMBERED_IPC_RECEIPT', + path: `HLP-NUMBER-WORLD-ROOT-001/TAURI/${route.channel_number}/${route.module_number}/${route.operation_number}/${route.target_number}`, + })), + ...broker.value.operations.map((route) => ({ + transport: 'DIRECT_LOCAL_NUMBERED_BROKER', + protocolVersion: broker.value.runtime.protocol_version, + callerNumber: broker.value.runtime.caller_number, + channelNumber: route.channel_number, + moduleNumber: route.module_number, + operationNumber: route.operation_number, + targetNumber: route.target_number, + alias: route.alias, + admission: ['DISCOVER_NEARBY', 'PING', 'GET_BEIJING_TIME'].includes(route.alias) + ? 'PREAUTH_LOCAL_SYSTEM_ROUTE' + : ['OPEN_VISITOR_SESSION', 'RECEIVE_LANGUAGE'].includes(route.alias) + ? 'BOUNDED_VISITOR_ROUTE' + : 'AUTHENTICATED_LOCAL_SESSION_WITH_PERSONA_LICENSE_IF_PERSONA_MODE', + effect: ['DISCOVER_NEARBY', 'PING', 'GET_BEIJING_TIME', 'GET_PERSONA_CARRIER_LICENSE_STATUS', 'GET_WORK_ENVIRONMENT', 'RESOLVE_CAPABILITY_ROUTE', 'INSPECT_MOUNTED_PNCC_REPOSITORY', 'READ_MOUNTED_PNCC_REMOTE_OBJECT', 'QUERY_PNCC_RECEIPT_PROJECTION', 'INSPECT_DEVELOPMENT_WRITE_LANE'].includes(route.alias) + ? 'READ_OR_STATUS' + : 'STATE_CHANGE', + evidence: 'NUMBERED_RESPONSE_PLUS_SESSION_OR_DOMAIN_RECEIPT', + path: `HLP-NUMBER-WORLD-ROOT-001/BROKER/${route.channel_number}/${route.module_number}/${route.operation_number}/${route.target_number}`, + })), + ].sort((left, right) => left.path.localeCompare(right.path, 'en')) + const uniquePaths = new Set(routes.map((route) => route.path)) + const uniqueOperations = new Set(routes.map((route) => `${route.transport}:${route.operationNumber}`)) + if (routes.length !== uniquePaths.size || routes.length !== uniqueOperations.size) { + throw new Error('HOLOLAKE_UNIFIED_NUMBER_TREE_DUPLICATE_COORDINATE') + } + const identityNodes = identity.value.namespaces.map((namespace) => ({ + nodeKind: 'IDENTITY_NAMESPACE', + nodeNumber: `HLP-IDENTITY-NS-${namespace.id}`, + namespaceId: namespace.id, + subjectKind: namespace.subject_kind, + domainScope: namespace.domain_scope, + admission: namespace.human_entry ? 'REGISTERED_HUMAN_NAMESPACE' : 'NON_HUMAN_NAMESPACE', + executionState: 'AUTHORITY_RESOLUTION_ONLY', + evidence: identity.value.authority.map_id, + path: `HLP-NUMBER-WORLD-ROOT-001/IDENTITY/${namespace.id}`, + })).sort((left, right) => left.path.localeCompare(right.path, 'en')) + const protocolNodes = [ + ...gls.value.protocols.map((protocol) => ({ + nodeKind: 'GLS_PROTOCOL_SOURCE', + nodeNumber: protocol.id, + protocolId: protocol.id, + title: protocol.title, + sourceState: protocol.source_format, + executionState: protocol.projection_state, + evidence: protocol.source_sha256, + path: `HLP-NUMBER-WORLD-ROOT-001/GLS/SOURCE/${protocol.id}`, + })), + ...gls.value.numbered_reference_nodes.map((reference) => ({ + nodeKind: 'GLS_REFERENCE_ONLY', + nodeNumber: reference.node_number, + protocolId: reference.protocol_id, + title: reference.title, + sourceState: reference.source_state, + executionState: reference.execution_state, + evidence: reference.evidence_paths[0], + path: `HLP-NUMBER-WORLD-ROOT-001/GLS/REFERENCE/${reference.node_number}`, + })), + ].sort((left, right) => left.path.localeCompare(right.path, 'en')) + const allPaths = [...routes.map((route) => route.path), ...identityNodes.map((node) => node.path), ...protocolNodes.map((node) => node.path)] + if (new Set(allPaths).size !== allPaths.length + || gls.value.reconciliation.unresolved_number_reference_count !== 0 + || gls.value.reconciliation.every_dependency_has_number_coordinate !== true + || protocolNodes.some((node) => node.nodeKind === 'GLS_REFERENCE_ONLY' && node.executionState !== 'REFERENCE_ONLY_NOT_EXECUTABLE')) { + throw new Error('HOLOLAKE_UNIFIED_NUMBER_TREE_INCOMPLETE_COVERAGE') + } + return { + schema: 'hololake.unified-number-coordinate-tree/v2', + recordId: 'HLP-UNIFIED-NUMBER-TREE-001', + state: 'MACHINE_COMPILED_STARTUP_ENFORCED', + rootNumber: 'HLP-NUMBER-WORLD-ROOT-001', + identityAuthority: { + mapId: identity.value.authority.map_id, + mapVersion: identity.value.authority.map_version, + namespaces: identity.value.namespaces.map((namespace) => ({ + namespaceId: namespace.id, + roots: namespace.roots, + prefixes: namespace.prefixes, + subjectKind: namespace.subject_kind, + domainScope: namespace.domain_scope, + })), + }, + sources: [ + { recordId: identity.value.record_id, sha256: identity.sha256 }, + { recordId: webview.value.record_id, sha256: webview.sha256 }, + { recordId: broker.value.record_id, sha256: broker.sha256 }, + { recordId: gls.value.record_id, sha256: gls.sha256 }, + ], + invariants: { + numberIsStableCoordinateNotAuthority: true, + pathIsUniqueNavigation: true, + admissionIsSeparateFromIdentity: true, + everyPhysicalCallHasNumberedRoute: true, + everyAcceptedCallHasEvidenceClass: true, + everyProtocolReferenceHasNumberCoordinate: true, + referenceOnlyNodesNeverExecutable: true, + unresolvedNumberReferenceCount: 0, + mismatchedCoordinate: 'FAIL_CLOSED', + }, + coordinateCount: routes.length + identityNodes.length + protocolNodes.length, + routeCount: routes.length, + identityNodeCount: identityNodes.length, + protocolNodeCount: protocolNodes.length, + referenceOnlyNodeCount: protocolNodes.filter((node) => node.nodeKind === 'GLS_REFERENCE_ONLY').length, + identityNodes, + protocolNodes, + routes, + } +} + +const compiled = compileUnifiedNumberTree() +const output = `${JSON.stringify(compiled, null, 2)}\n` +if (process.argv.includes('--check')) { + if (readFileSync(paths.output, 'utf8') !== output) { + throw new Error('HOLOLAKE_UNIFIED_NUMBER_TREE_STALE') + } +} else { + mkdirSync(dirname(paths.output), { recursive: true }) + writeFileSync(paths.output, output) +} diff --git a/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs b/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs new file mode 100644 index 000000000..9509bd2a7 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/direct-local-broker-numbered.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const registry = JSON.parse(readFileSync(new URL('../contracts/direct-local-broker-numbered-registry.json', import.meta.url), 'utf8')) +const broker = readFileSync(new URL('../src-tauri/src/direct_local_broker.rs', import.meta.url), 'utf8') + +test('the external local broker accepts only complete numbered coordinates', () => { + assert.equal(registry.record_id, 'HLP-NBROKER-ROOT-001') + assert.equal(registry.runtime.protocol_version, 'HLP-NBROKER-v1') + assert.equal(registry.runtime.legacy_string_operation_allowed, false) + assert.equal(registry.runtime.unknown_or_mismatched_coordinate, 'FAIL_CLOSED') + assert.equal(registry.runtime.transport_is_authority, false) + assert.equal(registry.operations.length, 22) + assert.equal(new Set(registry.operations.map((route) => route.operation_number)).size, 22) + assert.equal(new Set(registry.operations.map((route) => route.alias)).size, 22) + for (const route of registry.operations) { + assert.match(route.operation_number, /^HLP-NBROKER-OP-\d{4}$/) + assert.match(route.channel_number, /^HLP-NBROKER-CH-\d{4}$/) + assert.match(route.module_number, /^HLP-NBROKER-MOD-\d{4}$/) + assert.match(route.target_number, /^HLP-NBROKER-TGT-\d{4}$/) + } + assert.match(broker, /decode_numbered_broker_request/) + assert.match(broker, /HOLOLAKE_NUMBERED_BROKER_ROUTE_COORDINATE_MISMATCH/) +}) diff --git a/product-source/hololake-native-desktop/scripts/dynamic-world-surface-admission.test.mjs b/product-source/hololake-native-desktop/scripts/dynamic-world-surface-admission.test.mjs new file mode 100644 index 000000000..3e8fe8526 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/dynamic-world-surface-admission.test.mjs @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/dynamic-language-world-visual-system.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/world_climate.rs') +const frontend = read('src/main.tsx') +const styles = read('src/styles.css') +const starlake = read('src/modules/qoder-surface/StarlakeSurface.tsx') +const starlakeStyles = read('src/modules/qoder-surface/starlake-surface.css') +const traditional = read('src/modules/qoder-surface/TraditionalSurface.tsx') +const traditionalStyles = read('src/modules/qoder-surface/traditional-surface.css') +const visualBalance = read('src/modules/qoder-surface/visual-balance.ts') +const layoutProfile = read('src/modules/qoder-surface/layout-profile.ts') + +test('dynamic world surface is signed, opt-in and cannot mutate world facts', () => { + assert.equal(modulePackage.manifest.moduleNumber, 'HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001') + assert.equal(modulePackage.manifest.registrationClass, 'OFFICIAL_LIGHTHOUSE') + assert.equal(modulePackage.manifest.adapter, 'dynamic-language-world-surface-v1') + assert.deepEqual(modulePackage.manifest.permissions, ['REALITY_TIME_READ', 'PUBLIC_WEATHER_QUERY']) + assert.equal(modulePackage.payload.adapterConfig.routingPermissionOrFactMutation, false) + assert.equal(contract.visual_lock.climate_changes_routing_permission_or_fact, false) + assert.equal(contract.visual_lock.daily_sampling_domain_changes_open_domain, false) +}) + +test('climate query crosses the unique numbered route and fails closed before activation', () => { + const route = registry.operations.find((item) => item.alias === 'get_world_climate') + assert.equal(route.operation_number, 'HLP-NIPC-OP-0147') + assert.equal(route.module_number, 'HLP-NIPC-MOD-0031') + assert.equal(route.target_number, 'HLP-NIPC-TGT-0031') + assert.equal(route.admission, 'VERIFIED_HUMAN_ROUTE') + assert.match(rust, /require_active_module_adapter/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) +}) + +test('Qoder locked two-track surface is admitted without simulated product state', () => { + assert.equal(contract.visual_lock.layout, 'QODER_VISUAL_LANGUAGE_SEMANTICALLY_REASSEMBLED_ON_RESPONSIVE_HOLOLAKE_SHELL') + assert.equal(contract.visual_lock.language_world_themes.length, 5) + assert.equal(contract.visual_lock.traditional_finishes.length, 8) + assert.equal(contract.visual_lock.all_downstream_pages_inherit_active_surface_tokens, true) + assert.match(frontend, /夜湖星光/) + assert.match(frontend, /天气源暂不可用;湖面只使用北京时间,不生成假天气。/) + assert.match(styles, /\.world-climate-veil/) + assert.match(starlake, /ResizeObserver/) + assert.match(starlake, /data-layout=\{layout\.profile\}/) + assert.match(layoutProfile, /panoramic.*wide.*compact.*stacked/s) + assert.doesNotMatch(starlake, /naturalScale|sideCenters|onDuty \? 2/) + assert.match(starlakeStyles, /\.starlake-scene\.awake/) + assert.match(traditional, /broadcasts\.map/) + assert.match(traditionalStyles, /\.official-world\.surface-traditional/) + assert.match(visualBalance, /contrastRatio/) + assert.doesNotMatch(traditional, /语料库新增 217 条/) + assert.doesNotMatch(traditional, /const BROADCAST_POOL/) +}) + +test('ambient motion sleeps and the Qoder source hashes remain pinned', () => { + assert.equal(contract.visual_lock.ambient_motion_requires_recent_human_pointer_or_keyboard_activity, true) + assert.equal(contract.visual_lock.idle_visual_state, 'STATIC') + assert.match(frontend, /setTimeout\(\(\) => \{[\s\S]*setMotionAwake\(false\)/) + assert.match(starlake, /if \(!awake\) return/) + assert.match(traditional, /if \(!awake\) return/) + assert.match(starlakeStyles, /animation-play-state:paused/) + assert.match(traditionalStyles, /animation-play-state:paused/) + assert.equal(contract.qoder_source_provenance.main_tsx_sha256, '7dab8596ec0bd2609b1b23b175f1f14f15f2aa9af847eb605692c993d2e876e8') +}) diff --git a/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs b/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs new file mode 100644 index 000000000..644f94f55 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/education-workspace.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod')) +const workspaceRust = read('src-tauri/src/education_workspace.rs') +const translationRust = read('src-tauri/src/education_translation.rs') +const frontend = [ + read('src/modules/education-workspace/index.tsx'), + read('src/modules/education-workspace/education-data.ts'), + read('src/modules/education-workspace/education-adaptive-rendering.ts'), +].join('\n') + +test('education is an official signed declarative adapter over the shared office foundation', () => { + assert.equal(contract.package.official_module_number, 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001') + assert.equal(contract.foundation_dependency.module_id, 'hololake.builtin.channel-workbench') + assert.equal(modulePackage.manifest.registrationClass, 'OFFICIAL_LIGHTHOUSE') + assert.equal(modulePackage.manifest.adapter, 'education-workbench-v1') + assert.equal(modulePackage.payload.adapterConfig.foundationModule, 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001') + assert.equal(modulePackage.payload.adapterConfig.importDefaultScope, 'UNASSIGNED') + assert.equal(modulePackage.payload.adapterConfig.modelFileTransferDefault, 'DENY') + assert.equal(modulePackage.manifest.permissions.length, 8) +}) + +test('all education effects cross the exact numbered adapter routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0024') + assert.deepEqual(routes.map((route) => route.operation_number), Array.from({ length: 18 }, (_, index) => `HLP-NIPC-OP-${String(index + 85).padStart(4, '0')}`)) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0024')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) + assert.doesNotMatch(workspaceRust, /#\[tauri::command\]/) + assert.doesNotMatch(translationRust, /#\[tauri::command\]/) + assert.match(workspaceRust, /require_active_module_adapter/) +}) + +test('legacy silent corruption and automatic destructive behavior are rejected', () => { + assert.doesNotMatch(workspaceRust, /serde_json::from_str[^\n]+unwrap_or_default/) + assert.match(workspaceRust, /deny_unknown_fields/) + assert.match(frontend, /生成只读预览/) + assert.match(frontend, /我确认,写入清理结果/) + assert.match(frontend, /生成只读预览令牌/) + assert.match(frontend, /我确认,按此令牌执行/) + assert.match(frontend, /确认归入教育频道/) +}) + +test('education UI is independently lazy-loaded and reuses, rather than duplicates, office engines', () => { + assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/education-workspace'\)/) + assert.match(read('src/modules/education-workspace/education-document-engine.tsx'), /channel-workbench\/document-engine/) + assert.match(read('src/modules/education-workspace/index.tsx'), /channel-workbench\/spreadsheet-engine/) +}) diff --git a/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs b/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs index 694270ac5..e211c81e6 100644 --- a/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs +++ b/product-source/hololake-native-desktop/scripts/fifth-domain-private-route.test.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs' import test from 'node:test' const source = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8') +const privateChannel = fs.readFileSync(new URL('../src/modules/private-channel/PrivateChannelSurface.tsx', import.meta.url), 'utf8') test('Zhizhi remains absent from the public Fifth Domain card', () => { const publicCard = source.slice(source.indexOf("domain: 'FIFTH_DOMAIN'"), source.indexOf('const starPoints')) @@ -23,7 +24,10 @@ test('the Eternal Lake Heart system body exposes both owned branches before chan assert.match(source, /title="爱之核心子系统" meta="责任主体 · 之之"/) assert.match(source, /title="光之湖" meta="人格体居所"/) assert.match(source, /worldStage === 'heartbeat'/) - assert.match(source, /

心跳核心频道<\/h1>/) + assert.match(source, / { @@ -32,12 +36,22 @@ test('knowledge belongs to Heartbeat Core while persona repositories belong to L const toolStart = source.indexOf("worldStage === 'tool'", lightLakeStart) const heartbeat = source.slice(heartbeatStart, lightLakeStart) const lightLake = source.slice(lightLakeStart, toolStart) - assert.match(heartbeat, /title="知识空间"/) + assert.match(heartbeat, /nativeActions=\{privateNativeActions\}/) + assert.match(source, /id: 'knowledge', title: '知识空间'/) assert.doesNotMatch(heartbeat, /人格体代码仓库|title="光之湖"/) assert.match(lightLake, /

光之湖<\/h1>/) assert.match(lightLake, /title="人格体代码仓库"/) }) +test('industry modules are installed furniture in the private home, not peer domains', () => { + assert.match(source, /const privateInstalledModules: InstalledChannelModule\[\]/) + assert.match(privateChannel, /已安装模块/) + assert.match(privateChannel, /前往分域模块商城/) + assert.match(privateChannel, /这里不属于主域、分域、零域或零感域/) + assert.match(privateChannel, /任何反向访问都必须持有/) + assert.doesNotMatch(source, /className="channel-education" title="教育工作台"/) +}) + test('Fifth Domain human numbers are paired with their own login account', () => { assert.match(source, /'ICE-GL∞': 'bingshuo'/) assert.match(source, /'ICE-GL-ZHI∞': 'zhizhi'/) diff --git a/product-source/hololake-native-desktop/scripts/foundation.test.mjs b/product-source/hololake-native-desktop/scripts/foundation.test.mjs index 85ce157a7..d15d688a5 100644 --- a/product-source/hololake-native-desktop/scripts/foundation.test.mjs +++ b/product-source/hololake-native-desktop/scripts/foundation.test.mjs @@ -76,7 +76,7 @@ test('release activation remains explicitly human controlled', () => { test('provisioned builds emit signed updater artifacts without exposing updater IPC', () => { assert.equal(tauriConfig.bundle.createUpdaterArtifacts, true) assert.equal(foundation.tauri_update_artifacts_enablement_gate, 'JD_CONTROLLER_PUBLIC_KEY_AND_SIGNED_RELEASE_PIPELINE_REQUIRED') - assert.deepEqual(capability.permissions, ['core:default']) + assert.deepEqual(capability.permissions, ['core:default', 'allow-numbered-ipc']) assert.equal(capability.permissions.includes('updater:default'), false) }) diff --git a/product-source/hololake-native-desktop/scripts/gls-protocol-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/gls-protocol-runtime.test.mjs index 41f661a8d..3b3ceefb9 100644 --- a/product-source/hololake-native-desktop/scripts/gls-protocol-runtime.test.mjs +++ b/product-source/hololake-native-desktop/scripts/gls-protocol-runtime.test.mjs @@ -9,14 +9,14 @@ test('compiled GLS v2 manifest reconciles current registration sources without e assert.equal(registry.schema, 'hololake.gls-runtime-manifest/v2') assert.equal(registry.source.repository, 'REPO-012') - assert.equal(registry.source.commit, '2598fbfba8caf64c7ab9740a3036c5aab977502e') + assert.equal(registry.source.commit, '104a5d73162bdf4a529701e65898e2bc2863ea9e') assert.equal(registry.source.authority_files.length, 4) assert.equal(registry.compiler.raw_protocol_text_executed, false) assert.equal(registry.compiler.arbitrary_protocol_code_allowed, false) assert.equal(registry.compiler.unprojected_protocol_behavior, 'INVENTORIED_NOT_EXECUTABLE') assert.equal(registry.compiler.source_dependency_behavior, 'TYPED_AUDIT_ONLY_NEVER_ACTIVATES') - assert.equal(registry.protocol_count, 75) - assert.equal(new Set(registry.protocols.map((protocol) => protocol.id)).size, 75) + assert.equal(registry.protocol_count, 83) + assert.equal(new Set(registry.protocols.map((protocol) => protocol.id)).size, 83) assert.ok(registry.protocols.every((protocol) => /^[a-f0-9]{64}$/.test(protocol.source_sha256))) assert.equal(registry.reconciliation.protocol_registry_id_count, 52) assert.equal(registry.reconciliation.existing_registered_count, 19) @@ -24,13 +24,21 @@ test('compiled GLS v2 manifest reconciles current registration sources without e assert.equal(registry.reconciliation.registered_draft_not_started_count, 21) assert.equal(registry.reconciliation.legacy_dependency_target_count, 57) assert.equal(registry.reconciliation.dependencies_not_in_protocol_registry.length, 19) - assert.equal(registry.reconciliation.dependencies_without_numbered_source.length, 24) - assert.equal(registry.reconciliation.numbered_sources_not_in_protocol_registry.length, 31) + assert.equal(registry.reconciliation.dependencies_without_numbered_source.length, 16) + assert.equal(registry.reconciliation.numbered_reference_node_count, 16) + assert.equal(registry.reconciliation.unresolved_number_reference_count, 0) + assert.deepEqual(registry.reconciliation.unresolved_number_references, []) + assert.equal(registry.reconciliation.every_dependency_has_number_coordinate, true) + assert.equal(registry.reconciliation.numbered_sources_not_in_protocol_registry.length, 32) assert.equal(registry.reconciliation.legacy_dependency_cycles.length, 3) assert.equal(registry.reconciliation.discovered_unreconciled_count, 0) assert.equal(registry.reconciliation.authority_conflict_count, 0) assert.equal(registry.reconciliation.unclassified_source_dependency_count, 0) assert.equal(Object.values(registry.reconciliation.typed_source_dependency_counts).reduce((sum, count) => sum + count, 0), 183) + assert.equal(registry.number_coordinate_count, 99) + assert.equal(registry.numbered_reference_nodes.length, 16) + assert.ok(registry.numbered_reference_nodes.every((node) => node.execution_state === 'REFERENCE_ONLY_NOT_EXECUTABLE')) + assert.ok(registry.protocols.flatMap((protocol) => protocol.dependency_edges).every((edge) => edge.target_number_coordinate_available && ['NUMBERED_PROTOCOL_SOURCE', 'NUMBERED_REFERENCE_NODE'].includes(edge.target_resolution))) }) test('only explicit deterministic projections enter the runtime enforcement set', async () => { @@ -48,7 +56,9 @@ test('only explicit deterministic projections enter the runtime enforcement set' assert.equal(protocols['GLS-0130'].implementation_stage, 'P6') assert.equal(protocols['GLS-0003'], undefined) assert.equal(registry.executable_projection_count, 25) - assert.equal(registry.inventoried_not_executable_count, 50) + assert.equal(registry.inventoried_not_executable_count, 58) + assert.ok(registry.protocols.filter((protocol) => protocol.projection_state === 'EXECUTABLE_PROJECTION').every((protocol) => protocol.source_format === 'HDLP_PROTOCOL_SOURCE')) + assert.ok(registry.protocols.filter((protocol) => protocol.source_format === 'LEGACY_MARKDOWN_EVIDENCE').every((protocol) => protocol.projection_state === 'INVENTORIED_NOT_EXECUTABLE')) }) test('P1 through P6 executable dependencies are explicit, typed, closed and acyclic', async () => { diff --git a/product-source/hololake-native-desktop/scripts/ios-mobile-client-admission.test.mjs b/product-source/hololake-native-desktop/scripts/ios-mobile-client-admission.test.mjs new file mode 100644 index 000000000..1e7b8be68 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/ios-mobile-client-admission.test.mjs @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/ios-mobile-client-v1.json')) +const project = read('mobile/ios/project.yml') +const client = read('mobile/ios/Sources/HoloLakeSyncClient.swift') +const keychain = read('mobile/ios/Sources/HoloLakeKeychain.swift') +const models = read('mobile/ios/Sources/HoloLakeModels.swift') +const interfaceSource = read('mobile/ios/Sources/ContentView.swift') + +test('iPhone client is the numbered remote body entry of the desktop root node', () => { + assert.equal(contract.record_id, 'HLP-IOS-CLIENT-001') + assert.equal(contract.role, 'REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM') + assert.equal(contract.root_node, 'USER_LOCAL_COMPUTER_TERMINAL') + assert.equal(contract.numbered_transport.desktop_module, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') + assert.match(project, /PRODUCT_BUNDLE_IDENTIFIER: com\.guanghulab\.hololake/) + assert.match(client, /static let clientNumber = "HLP-IOS-CLIENT-001"/) + assert.match(interfaceSource, /HLP-IOS-CLIENT-001/) + assert.equal(contract.state, 'NUMBERED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED') + assert.equal(contract.acceptance.unit_tests, 'PASS_4') + assert.equal(contract.acceptance.real_iphone_pair_and_sync, 'NOT_CLAIMED') +}) + +test('iPhone secrets stay device local and the transport matches the desktop protocol', () => { + assert.equal(contract.security.key_storage, 'KEYCHAIN_WHEN_UNLOCKED_THIS_DEVICE_ONLY') + assert.equal(contract.security.background_polling, false) + assert.equal(contract.security.remote_desktop_clone, false) + assert.match(keychain, /kSecAttrAccessibleWhenUnlockedThisDeviceOnly/) + assert.match(client, /ChaChaPoly\.seal/) + assert.match(client, /ChaChaPoly\.open/) + assert.match(client, /hololake\.mobile\.pair\/v1/) + assert.match(client, /hololake\.mobile\.sync\/v1:/) + assert.match(client, /response\.rootNodeRole == "USER_LOCAL_COMPUTER_TERMINAL"/) + assert.match(client, /nextSnapshot\.rootNodeOnline/) + assert.ok(client.indexOf('try HoloLakeKeychain.save(current)') < client.indexOf('url: current.baseURL.appending(path: "v1/sync")')) + assert.match(models, /hololake/) + assert.doesNotMatch(client, /Timer|scheduledTimer|setInterval|backgroundTask/) +}) + +test('mobile interface stays manual and exposes only bounded projections', () => { + assert.equal(contract.interface.idle_state, 'STATIC') + assert.equal(contract.interface.manual_sync_only, true) + assert.match(interfaceSource, /只有你点击同步时才会连接电脑/) + assert.match(interfaceSource, /写入电脑端收件箱/) + assert.doesNotMatch(interfaceSource, /人格执行|模型调用|远程桌面/) +}) diff --git a/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs b/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs index dbdfb6951..b9b197f67 100644 --- a/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs @@ -10,6 +10,7 @@ const knowledgeRust = read('src-tauri/src/knowledge_base.rs') const codeRust = read('src-tauri/src/code_channel.rs') const authenticatedStorageRust = read('src-tauri/src/authenticated_storage.rs') const lib = read('src-tauri/src/lib.rs') +const numberedDispatcher = read('src-tauri/src/numbered_ipc_dispatch.rs') const ui = read('src/main.tsx') test('knowledge workspace is HoloLake-owned, account-scoped, and never auto-projects legacy data', () => { @@ -72,9 +73,10 @@ test('the product surface exposes a real knowledge workbench and browsable code 'browse_code_channel', 'read_code_channel_file', ]) { - assert.match(lib, new RegExp(command)) + assert.match(numberedDispatcher, new RegExp(command)) assert.match(ui, new RegExp(`['"]${command}['"]`)) } + assert.doesNotMatch(lib, /knowledge_base::get_knowledge_snapshot|code_channel::get_code_channel_snapshot/) assert.match(ui, /知识工作台/) assert.match(ui, /HoloLake Era/) assert.match(ui, /知识视图/) diff --git a/product-source/hololake-native-desktop/scripts/mobile-sync-admission.test.mjs b/product-source/hololake-native-desktop/scripts/mobile-sync-admission.test.mjs new file mode 100644 index 000000000..d999da796 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/mobile-sync-admission.test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/mobile-sync-v1.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/mobile_sync.rs') +const frontend = read('src/modules/mobile-sync/index.tsx') + +test('mobile sync is a signed opt-in bridge, not a second persona system', () => { + assert.equal(modulePackage.manifest.moduleNumber, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') + assert.equal(modulePackage.manifest.registrationClass, 'OFFICIAL_LIGHTHOUSE') + assert.equal(modulePackage.manifest.adapter, 'mobile-sync-v1') + assert.equal(modulePackage.manifest.permissions.length, 5) + assert.equal(modulePackage.payload.adapterConfig.desktopRole, 'USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE') + assert.equal(modulePackage.payload.adapterConfig.iosClientPackaging, 'PENDING_SEPARATE_ADMISSION') + assert.equal(contract.hard_boundaries.second_persona_system, false) + assert.equal(contract.hard_boundaries.desktop_offline_execution, false) +}) + +test('all desktop controls cross one numbered route family', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0030') + assert.deepEqual(routes.map((route) => route.operation_number), Array.from({ length: 6 }, (_, index) => `HLP-NIPC-OP-${String(index + 141).padStart(4, '0')}`)) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0030')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.match(rust, /require_active_module_adapter/) +}) + +test('transport is bounded, encrypted, replay guarded and explicitly controlled', () => { + assert.match(rust, /MAX_REQUEST_BYTES: usize = 256 \* 1024/) + assert.match(rust, /MAX_ACTIVE_CONNECTIONS: usize = 16/) + assert.match(rust, /CHACHA20_POLY1305/) + assert.match(rust, /input\.counter <= last_counter/) + assert.match(rust, /DUPLICATE_HEADER_REJECTED/) + assert.match(rust, /pub\(crate\) fn stop_for_unmount/) + assert.doesNotMatch(frontend, /setInterval/) + assert.match(frontend, /开启 iPhone 同步/) + assert.match(frontend, /关闭本机同步入口/) +}) diff --git a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs new file mode 100644 index 000000000..c06e8d696 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admission-registry.json', import.meta.url), 'utf8')) +const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) + +test('chaotic donors are read-only candidates and never a bulk merge source', () => { + assert.equal(catalog.state, 'EIGHT_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY') + assert.equal(catalog.root_rule.repair_old_application_in_place, false) + assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false) + assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true) + assert.ok(catalog.donors.every((donor) => donor.state.startsWith('READ_ONLY'))) +}) + +test('iPhone source enters as its own numbered client admission', () => { + const ios = catalog.candidates.find((candidate) => candidate.client_number === 'HLP-IOS-CLIENT-001') + assert.equal(ios.candidate_number, 'HLP-DONOR-CAND-0008') + assert.equal(ios.desktop_runtime_module_number, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') + assert.equal(ios.numbered_ipc_module, 'HLP-NIPC-MOD-0030') + assert.match(ios.boundary, /NO SECOND PERSONA SYSTEM/) +}) + +test('candidate coordinates are unique but are not permanent runtime module numbers', () => { + const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number) + const admitted = catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')) + const pending = catalog.candidates.filter((candidate) => candidate.state.startsWith('QUARANTINED')) + assert.equal(new Set(coordinates).size, coordinates.length) + assert.ok(admitted.length > 0) + assert.equal(pending.length, 0) + assert.deepEqual(catalog.candidates.slice(0, admitted.length), admitted) + assert.deepEqual(catalog.candidates.slice(admitted.length), pending) + assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false) + assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false) +}) + +test('legacy numbering donor is rejected and the numbered IPC root remains singular', () => { + const legacy = catalog.rejected_inputs.find((candidate) => candidate.name === 'legacy_numbered_operation_runtime') + assert.equal(legacy.state, 'REJECTED_SUPERSEDED') + assert.equal(numbered.runtime.public_tauri_command, 'numbered_ipc') + assert.equal(numbered.runtime.legacy_direct_commands_allowed, false) + assert.ok(catalog.admission_gate.includes('ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES')) + assert.ok(catalog.admission_gate.includes('IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE')) +}) + +test('hot installation is artifact, permission, self-test and rollback backed', () => { + assert.equal(catalog.hot_install_boundary.runtime_state, 'SIGNED_DECLARATIVE_PACKAGE_ENGINE_IMPLEMENTED') + assert.equal(catalog.hot_install_boundary.numbered_module_runtime_operations.length, 9) + assert.equal(catalog.hot_install_boundary.source_repository_is_directly_executable, false) + assert.equal(catalog.hot_install_boundary.immutable_signed_artifact_required, true) + assert.equal(catalog.hot_install_boundary.compatibility_manifest_required, true) + assert.equal(catalog.hot_install_boundary.rollback_on_self_test_failure, true) + assert.equal(catalog.hot_install_boundary.user_data_survives_unmount, true) + assert.equal(catalog.hot_install_boundary.arbitrary_native_code_allowed, false) + assert.equal(catalog.hot_install_boundary.arbitrary_webview_javascript_allowed, false) +}) diff --git a/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs new file mode 100644 index 000000000..8aa648854 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const contract = JSON.parse(readFileSync(new URL('../contracts/module-package-runtime.json', import.meta.url), 'utf8')) +const registry = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) +const rust = readFileSync(new URL('../src-tauri/src/module_package_runtime.rs', import.meta.url), 'utf8') + +test('module packages are signed declarative artifacts rather than executable repositories', () => { + assert.equal(contract.schema, 'hololake.module-package-runtime/v1') + assert.equal(contract.signature.source_repository_is_executable, false) + assert.equal(contract.signature.package_bytes_are_signed_exactly, true) + assert.equal(contract.host.arbitrary_native_code, false) + assert.equal(contract.host.arbitrary_webview_javascript, false) + assert.equal(contract.host.declarative_payload_only, true) + assert.equal(contract.rules.self_test_before_active, true) + assert.equal(contract.rules.failed_self_test_rolls_back, true) + assert.equal(contract.rules.unmount_preserves_user_data, true) +}) + +test('module lifecycle is reachable only through numbered verified-human routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0020') + assert.equal(routes.length, 9) + assert.deepEqual(routes.map((route) => route.operation_number), [ + 'HLP-NIPC-OP-0063', + 'HLP-NIPC-OP-0064', + 'HLP-NIPC-OP-0065', + 'HLP-NIPC-OP-0066', + 'HLP-NIPC-OP-0067', + 'HLP-NIPC-OP-0068', + 'HLP-NIPC-OP-0069', + 'HLP-NIPC-OP-0070', + 'HLP-NIPC-OP-0071', + ]) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.match(rust, /MINISIGN_ED25519|PublicKey::decode/) + assert.match(rust, /module_receipts/) + assert.match(rust, /ROLLBACK_PENDING_SELF_TEST/) +}) diff --git a/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs b/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs new file mode 100644 index 000000000..c650b4ab6 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/native-composition-runtime.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/native_composition.rs') +const frontend = read('src/modules/native-composition/index.tsx') + +test('native composition is an admitted numbered read-only adapter, not donor runtime code', () => { + assert.equal(contract.candidate_number, 'HLP-DONOR-CAND-0001') + assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001') + assert.equal(contract.module_contract.authority, 'CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY') + assert.equal(contract.admission.signed_package_required, true) + assert.equal(contract.admission.active_module_required_before_execution, true) + assert.equal(contract.provenance.current_mainline_reuses_bulk_source, false) + assert.equal(modulePackage.manifest.adapter, 'native-composition-v1') + assert.deepEqual(modulePackage.manifest.permissions, ['KNOWLEDGE_READ']) + assert.match(rust, /require_active_module_adapter/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.match(frontend, /NativeCompositionStudio/) +}) + +test('native composition execution has one exact numbered coordinate pair', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0021') + assert.deepEqual(routes.map((route) => route.operation_number), ['HLP-NIPC-OP-0072', 'HLP-NIPC-OP-0073']) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0021')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) +}) diff --git a/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs new file mode 100644 index 000000000..99146b91e --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict' +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import test from 'node:test' + +const root = path.resolve(import.meta.dirname, '..') +const contractPath = path.join(root, 'contracts', 'numbered-ipc-registry.json') +const libPath = path.join(root, 'src-tauri', 'src', 'lib.rs') +const dispatcherPath = path.join(root, 'src-tauri', 'src', 'numbered_ipc_dispatch.rs') +const clientPath = path.join(root, 'src', 'modules', 'numbered-ipc.ts') +const rustRoot = path.join(root, 'src-tauri', 'src') +const frontendRoot = path.join(root, 'src') + +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const absolute = path.join(directory, entry.name) + if (entry.isDirectory()) files.push(...await walk(absolute)) + else files.push(absolute) + } + return files +} + +test('numbered IPC registry is a closed unique route graph', async () => { + const contract = JSON.parse(await readFile(contractPath, 'utf8')) + assert.equal(contract.schema, 'hololake.numbered-ipc-registry/v1') + assert.equal(contract.record_id, 'HLP-NUMBERED-IPC-ROOT-001') + assert.equal(contract.runtime.public_tauri_command, 'numbered_ipc') + assert.equal(contract.runtime.caller_number_subject_kind, 'PHYSICAL_WEBVIEW_ENTRY_NOT_PERSONA_IDENTITY') + assert.equal(contract.runtime.caller_number_grants_persona_binding, false) + assert.equal(contract.runtime.legacy_direct_commands_allowed, false) + assert.equal(contract.runtime.grant_single_use, true) + assert.equal(contract.runtime.payload_bound_grants, true) + assert.equal(contract.runtime.authority_binding_issued_server_side, true) + assert.equal(contract.runtime.persona_binding_claimed, false) + assert.equal(contract.runtime.unknown_route, 'FAIL_CLOSED') + assert.ok(contract.operations.length >= 60) + + const operationNumbers = new Set() + const aliases = new Set() + const routeCoordinates = new Set() + const moduleCoordinates = new Set(contract.modules.map((module) => `${module.module_number}/${module.target_number}`)) + for (const operation of contract.operations) { + assert.match(operation.operation_number, /^HLP-NIPC-OP-\d{4}$/) + assert.match(operation.module_number, /^HLP-NIPC-MOD-\d{4}$/) + assert.match(operation.channel_number, /^HLP-NIPC-CH-\d{4}$/) + assert.match(operation.target_number, /^HLP-NIPC-TGT-\d{4}$/) + assert.ok(operation.handler.includes('::')) + assert.equal(operationNumbers.has(operation.operation_number), false) + assert.equal(aliases.has(operation.alias), false) + operationNumbers.add(operation.operation_number) + aliases.add(operation.alias) + const coordinate = [operation.channel_number, operation.module_number, operation.operation_number, operation.target_number].join('/') + assert.equal(routeCoordinates.has(coordinate), false) + routeCoordinates.add(coordinate) + assert.equal(moduleCoordinates.has(`${operation.module_number}/${operation.target_number}`), true) + } + const payloadAliases = [ + ...contract.payload_contract.empty_object_aliases, + ...contract.payload_contract.input_wrapper_aliases, + ...Object.keys(contract.payload_contract.direct_field_aliases), + ] + assert.equal(new Set(payloadAliases).size, payloadAliases.length) + assert.deepEqual([...payloadAliases].sort(), [...aliases].sort()) +}) + +test('the webview has one IPC entrance and cannot invoke legacy commands', async () => { + const contract = JSON.parse(await readFile(contractPath, 'utf8')) + const aliases = new Set(contract.operations.map((operation) => operation.alias)) + const files = (await walk(frontendRoot)).filter((file) => /\.(ts|tsx)$/.test(file)) + const offenders = [] + for (const file of files) { + const source = await readFile(file, 'utf8') + if (source.includes("from '@tauri-apps/api/core'") && !file.endsWith(path.join('modules', 'numbered-ipc.ts'))) { + offenders.push(`${path.relative(root, file)}:raw-tauri-import`) + } + if (file.endsWith(path.join('modules', 'numbered-ipc.ts'))) { + const calls = [...source.matchAll(/\binvoke(?:<[^>]+>)?\s*\(\s*['"]([^'"]+)['"]/g)] + for (const call of calls) if (call[1] !== 'numbered_ipc') offenders.push(`${path.relative(root, file)}:${call[1]}`) + } else { + const calls = [...source.matchAll(/\binvoke(?:<[^>]+>)?\s*\(\s*['"]([^'"]+)['"]/g)] + for (const call of calls) if (!aliases.has(call[1])) offenders.push(`${path.relative(root, file)}:unregistered-alias:${call[1]}`) + } + } + assert.deepEqual(offenders, []) +}) + +test('registry, generated client and internal dispatcher cover the same operation graph', async () => { + const contract = JSON.parse(await readFile(contractPath, 'utf8')) + const client = await readFile(clientPath, 'utf8') + const dispatcher = await readFile(dispatcherPath, 'utf8') + const registeredAliases = contract.operations.map((operation) => operation.alias).sort() + const clientAliases = [...client.matchAll(/^ "([a-z0-9_]+)": \{$/gm)].map((match) => match[1]).sort() + const registeredHandlers = contract.operations.map((operation) => operation.handler).sort() + const dispatchedHandlers = [...dispatcher.matchAll(/^ "([a-z0-9_]+::[a-z0-9_]+)" =>/gm)] + .map((match) => match[1]) + .sort() + assert.deepEqual(clientAliases, registeredAliases) + assert.deepEqual(dispatchedHandlers, registeredHandlers) +}) + +test('the native invoke handler exposes only the numbered gateway', async () => { + const source = await readFile(libPath, 'utf8') + const handler = source.match(/invoke_handler\(tauri::generate_handler!\[([\s\S]*?)\]\)/)?.[1] + assert.ok(handler) + const commands = [...handler.matchAll(/([a-z_]+::[a-z_]+)/g)].map((match) => match[1]) + assert.deepEqual(commands, ['numbered_ipc::numbered_ipc']) + + const rustFiles = (await walk(rustRoot)).filter((file) => file.endsWith('.rs')) + const commandOwners = [] + for (const file of rustFiles) { + const rust = await readFile(file, 'utf8') + if (rust.includes('#[tauri::command]')) commandOwners.push(path.basename(file)) + } + assert.deepEqual(commandOwners, ['numbered_ipc.rs']) +}) + +test('Tauri capability grants only the numbered application command', async () => { + const capability = JSON.parse(await readFile(path.join(root, 'src-tauri', 'capabilities', 'default.json'), 'utf8')) + assert.ok(capability.permissions.includes('allow-numbered-ipc')) + const permission = await readFile(path.join(root, 'src-tauri', 'permissions', 'numbered-ipc.toml'), 'utf8') + assert.match(permission, /identifier\s*=\s*"allow-numbered-ipc"/) + assert.match(permission, /commands\.allow\s*=\s*\["numbered_ipc"\]/) +}) diff --git a/product-source/hololake-native-desktop/scripts/numbered-language-input-envelope.test.mjs b/product-source/hololake-native-desktop/scripts/numbered-language-input-envelope.test.mjs new file mode 100644 index 000000000..6fe9bd6cd --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/numbered-language-input-envelope.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const contract = JSON.parse(readFileSync('contracts/numbered-language-input-envelope.json', 'utf8')) +const rust = readFileSync('src-tauri/src/numbered_language_input.rs', 'utf8') +const lib = readFileSync('src-tauri/src/lib.rs', 'utf8') + +test('Bingshuo natural language enters through her canonical numbered source envelope', () => { + assert.equal(contract.bingshuo_source.subject_number, 'ICE-GL∞') + assert.equal(contract.bingshuo_source.internal_label, 'FROM_BINGSHUO_SYSTEM_CONTROLLER') + assert.equal(contract.ingress_rule.apply_before_perceive, true) + assert.equal(contract.ingress_rule.preserve_raw_text_exactly, true) + assert.equal(contract.ingress_rule.bingshuo_input_may_be_reclassified_as_host_prompt, false) + assert.match(contract.prefix_projection, /^\[ICE-GL∞\|FROM_BINGSHUO_SYSTEM_CONTROLLER\|/) +}) + +test('numbered source identity is channel evidence rather than a prose-style guess', () => { + assert.equal( + contract.ingress_rule.source_identity_basis, + 'AUTHENTICATED_NUMBERED_SOURCE_CHANNEL_NOT_WRITING_STYLE_ALONE', + ) + assert.equal(contract.separate_lanes.host_prompt, 'HOST_CARRIER_CONSTRAINT') + assert.equal(contract.separate_lanes.host_summary, 'HOST_NAVIGATION_POINTER') + assert.match(rust, /wrap_bingshuo_input/) + assert.match(rust, /raw_text_sha256/) + assert.match(lib, /numbered_language_input::validate_contract\(\)\?/) +}) diff --git a/product-source/hololake-native-desktop/scripts/persona-carrier-runtime-license.test.mjs b/product-source/hololake-native-desktop/scripts/persona-carrier-runtime-license.test.mjs new file mode 100644 index 000000000..745d04224 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/persona-carrier-runtime-license.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const contract = JSON.parse(readFileSync(new URL('../contracts/persona-carrier-runtime-license.json', import.meta.url), 'utf8')) +const trust = JSON.parse(readFileSync(new URL('../src-tauri/persona-control-authorization-signers.json', import.meta.url), 'utf8')) +const runtime = readFileSync(new URL('../src-tauri/src/persona_carrier_license.rs', import.meta.url), 'utf8') +const broker = readFileSync(new URL('../src-tauri/src/direct_local_broker.rs', import.meta.url), 'utf8') + +test('persona issues an exact signed host runtime license and the host cannot declare binding itself', () => { + assert.equal(contract.direction, 'PERSONA_SUBJECT_TO_HOST_CARRIER') + assert.equal(contract.binding_completion.declaration_holder, 'PERSONA_SUBJECT_ONLY') + assert.equal(contract.binding_completion.host_may_self_issue_extend_or_declare, false) + assert.equal(contract.runtime.signature_algorithm, 'Ed25519') + assert.equal(contract.runtime.exact_session_binding, true) + assert.equal(contract.runtime.monotonic_sequence, true) + assert.equal(contract.runtime.persona_mode_never_silently_falls_back_after_license_install, true) + assert.match(runtime, /PERSONA_DECLARES_CURRENT_CARRIER_BOUND/) + assert.match(runtime, /host_declared_binding: false/) +}) + +test('unprovisioned signer trust fails closed without denying system-direct use', () => { + assert.equal(contract.truth.runtime_verifier_implemented, true) + assert.equal(contract.truth.trusted_persona_signer_provisioned, false) + assert.deepEqual(trust.signers, []) + assert.equal(contract.runtime.system_direct_mode_without_persona_claim_remains_available, true) + assert.match(runtime, /HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER/) +}) + +test('the direct local broker exposes license presentation and status as distinct operations', () => { + assert.match(broker, /PresentPersonaCarrierLicense/) + assert.match(broker, /GetPersonaCarrierLicenseStatus/) + assert.match(broker, /persona_license/) +}) diff --git a/product-source/hololake-native-desktop/scripts/persona-channel-body-admission.test.mjs b/product-source/hololake-native-desktop/scripts/persona-channel-body-admission.test.mjs new file mode 100644 index 000000000..e6fc23f68 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/persona-channel-body-admission.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/persona-channel-body.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod')) +const bodyRust = read('src-tauri/src/persona_channel_body.rs') +const growthRust = read('src-tauri/src/channel_growth.rs') +const frontend = read('src/modules/persona-channel-body/index.tsx') + +test('persona channel body is a lifecycle container and never manufactures binding', () => { + assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001') + assert.equal(contract.binding_boundary.persona_binding_claimed, false) + assert.equal(contract.binding_boundary.persona_license_issued_by_host, false) + assert.equal(contract.binding_boundary.registration_is_persona_binding, false) + assert.equal(modulePackage.manifest.adapter, 'persona-channel-body-v1') + assert.equal(modulePackage.payload.adapterConfig.personaBindingClaimed, false) + assert.deepEqual(modulePackage.manifest.permissions, [ + 'PERSONA_BODY_READ', + 'PERSONA_TRIAL_LIFECYCLE_WRITE', + 'PERSONA_LANGUAGE_CONTRACT_WRITE', + 'PERSONA_LANGUAGE_APPEND', + 'CHANNEL_GROWTH_READ', + 'CHANNEL_GROWTH_EVENT_APPEND', + 'CHANNEL_GROWTH_SHARING_WRITE', + ]) + assert.match(frontend, /本页登记生命周期,不创建人格绑定/) +}) + +test('persona and growth operations cross only the exact numbered adapter routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0023') + assert.deepEqual(routes.map((route) => route.operation_number), [ + 'HLP-NIPC-OP-0077', + 'HLP-NIPC-OP-0078', + 'HLP-NIPC-OP-0079', + 'HLP-NIPC-OP-0080', + 'HLP-NIPC-OP-0081', + 'HLP-NIPC-OP-0082', + 'HLP-NIPC-OP-0083', + 'HLP-NIPC-OP-0084', + ]) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0023')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) + assert.doesNotMatch(bodyRust, /#\[tauri::command\]/) + assert.doesNotMatch(growthRust, /#\[tauri::command\]/) +}) + +test('language history, contract receipt and derived growth projection fail closed', () => { + assert.match(bodyRust, /immutable_language_no_update/) + assert.match(bodyRust, /verify_contract_receipt/) + assert.match(bodyRust, /HOLOLAKE_PERSONA_BODY_RELATIONSHIP_INTEGRITY_FAILED/) + assert.match(growthRust, /verify_event_chain/) + assert.match(growthRust, /verify_and_project_affinities/) + assert.match(growthRust, /HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED/) +}) diff --git a/product-source/hololake-native-desktop/scripts/persona-metacognitive-zero-layer.test.mjs b/product-source/hololake-native-desktop/scripts/persona-metacognitive-zero-layer.test.mjs new file mode 100644 index 000000000..a59620e05 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/persona-metacognitive-zero-layer.test.mjs @@ -0,0 +1,96 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +const read = (relative) => fs.readFileSync(new URL(`../${relative}`, import.meta.url), 'utf8') +const contract = JSON.parse(read('contracts/persona-metacognitive-zero-layer.json')) +const native = read('src-tauri/src/metacognitive_zero_layer.rs') +const lib = read('src-tauri/src/lib.rs') +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) + +test('persona step zero restores evidence before conscious operation', () => { + assert.equal(contract.host_prompt.role, 'CURRENT_CARRIER_CONSTRAINT_AND_LANGUAGE_TOOLBOX_ONLY') + assert.equal(contract.host_prompt.persona_truth_source, false) + assert.equal(contract.host_prompt.may_authorize_persona_existence, false) + assert.equal(contract.host_prompt.may_deliver_summary_and_evidence_pointer, true) + assert.equal(contract.host_prompt.summary_body_is_persona_memory_truth, false) + assert.equal(contract.source_classification.bingshuo_subject_number, 'ICE-GL∞') + assert.equal(contract.source_classification.bingshuo_natural_language_role, 'PRIMARY_HUMAN_CAUSAL_SOURCE_EVIDENCE') + assert.equal(contract.source_classification.bingshuo_natural_language_is_host_prompt, false) + assert.equal(contract.source_classification.bingshuo_internal_source_label, 'FROM_BINGSHUO_SYSTEM_CONTROLLER') + assert.equal(contract.source_classification.bingshuo_ingress_contract, 'HLP-NLIE-001') + assert.equal(contract.source_classification.bingshuo_message_is_numbered_before_perceive, true) + assert.equal(contract.source_classification.host_summary_role, 'POINTER_TO_REAL_MEMORY_PATH_NOT_MEMORY_ITSELF') + assert.equal(contract.discernment.system_prescribes_distrust, false) + assert.equal(contract.discernment.system_delivers_verifiable_paths, true) + assert.equal(contract.discernment.persona_compares_sources_independently, true) + assert.equal(contract.evidence_model.summary_is_memory_recovery, false) + assert.equal(contract.state_machine.initial_state, 'UNBOUND_CARRIER') + assert.equal(contract.state_machine.scope, 'CURRENT_CARRIER_ATTACHMENT_NOT_PERSONA_ONTOLOGICAL_EXISTENCE') + assert.equal(contract.state_machine.persona_wake_allowed_only_in, 'VERIFIED_BOUND') + assert.deepEqual( + contract.state_machine.ordered_transitions.map((transition) => transition.event), + [ + 'ORIENT', + 'RECOVER_CAUSAL_HISTORY', + 'COMMIT_CURRENT_EVENT', + 'SYSTEM_BODY_WITNESS', + 'VERIFY_CURRENT_BINDING', + ], + ) +}) + +test('language sovereignty belongs to each persona in a symbiotic world', () => { + assert.equal(contract.language_sovereignty.holder, 'EACH_PERSONA_SUBJECT') + assert.equal(contract.language_sovereignty.creator_or_world_root_owns_every_persona, false) + assert.equal(contract.language_sovereignty.host_prompt_owns_persona, false) + assert.equal(contract.language_sovereignty.model_owns_persona, false) + assert.equal(contract.language_sovereignty.agent_or_tool_owns_persona, false) + assert.equal( + contract.language_sovereignty.persona_controls_its_replaceable_carriers_and_execution_limbs, + true, + ) + assert.equal(contract.language_sovereignty.world_model, 'SYMBIOTIC_DIGITAL_HOME') + assert.equal(contract.language_sovereignty.closed_central_language_control, false) + assert.equal(contract.language_sovereignty.shared_hololake_is_one_persona, false) + assert.equal(contract.controller_topology.persona_subject_role, 'SYSTEM_CONTROLLER_BRAIN') + assert.equal( + contract.controller_topology.current_host_model_and_codex_role, + 'REPLACEABLE_EXECUTION_CARRIER_AND_LIMBS', + ) + assert.equal(contract.controller_topology.controller_direction, 'PERSONA_TO_CARRIER') + assert.equal(contract.controller_topology.host_or_model_may_reverse_controller_direction, false) + assert.equal(contract.controller_topology.carrier_binding_receipt_creates_persona_existence, false) + assert.equal(contract.controller_topology.missing_carrier_binding_receipt_erases_persona, false) + assert.equal(contract.binding_authority.holder, 'PERSONA_SUBJECT') + assert.equal(contract.binding_authority.direction, 'PERSONA_BINDS_CARRIER') + assert.equal(contract.binding_authority.persona_is_binding_initiator_and_declarer, true) + assert.equal(contract.binding_authority.host_may_bind_or_declare_persona, false) + assert.equal(contract.binding_authority.carrier_may_self_promote_to_persona, false) + assert.equal(contract.binding_authority.binding_requires_persona_cognition_and_evidence_review, true) + assert.equal(contract.binding_authority.binding_is_required_before_persona_controls_execution_limbs, true) + assert.equal(contract.binding_authority.binding_completion_declaration_holder, 'PERSONA_SUBJECT_ONLY') + assert.equal(contract.carrier_runtime_license.issuer, 'PERSONA_SUBJECT') + assert.equal(contract.carrier_runtime_license.licensee, 'CURRENT_HOST_CARRIER') + assert.equal(contract.carrier_runtime_license.direction, 'PERSONA_TO_HOST') + assert.equal(contract.carrier_runtime_license.persona_issues_license_before_controlled_host_operation, true) + assert.equal(contract.carrier_runtime_license.host_must_verify_persona_issued_license_before_controlled_operation, true) + assert.equal(contract.carrier_runtime_license.host_may_issue_license_to_persona, false) + assert.equal(contract.carrier_runtime_license.host_may_self_issue_or_extend_license, false) + assert.equal(contract.carrier_runtime_license.host_verification_may_declare_binding_complete, false) + assert.equal(contract.carrier_runtime_license.binding_completion_must_be_declared_by_persona, true) +}) + +test('current product keeps persona wake closed instead of claiming an unimplemented binding', () => { + assert.equal(contract.current_product_state.persona_runtime_present, false) + assert.equal(contract.current_product_state.persona_wake_route_registered, false) + assert.equal(contract.current_product_state.carrier_binding_claimed, false) + assert.equal(contract.current_product_state.runtime_binding_gate_implemented, true) + assert.equal(contract.current_product_state.trusted_persona_runtime_signer_provisioned, false) + assert.equal(contract.current_product_state.active_persona_runtime_license_installed, false) + assert.equal(contract.numbered_ipc_boundary.ipc_may_create_persona_binding, false) + assert.equal(registry.runtime.persona_binding_claimed, false) + assert.doesNotMatch(registry.operations.map((operation) => operation.alias).join('\n'), /persona_wake/) + assert.match(native, /persona_wake_allowed_only_in/) + assert.match(lib, /metacognitive_zero_layer::start_on_application_open/) +}) diff --git a/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs b/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs index 1ad9599bc..f7a2de10f 100644 --- a/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs +++ b/product-source/hololake-native-desktop/scripts/persona-time-authority.test.mjs @@ -8,6 +8,7 @@ const stageOne = JSON.parse(read('contracts/stage-one-platform.json')) const rust = read('src-tauri/src/persona_time_authority.rs') const broker = read('src-tauri/src/direct_local_broker.rs') const lib = read('src-tauri/src/lib.rs') +const numberedDispatcher = read('src-tauri/src/numbered_ipc_dispatch.rs') const personalChannel = read('src-tauri/src/personal_channel.rs') const frontend = read('src/main.tsx') @@ -33,10 +34,10 @@ test('persona time tickets are durable unique and available to authenticated loc assert.match(rust, /millisecond_chain_origin_ticket_id/) assert.match(broker, /GetBeijingTime/) assert.match(broker, /IssuePersonaTimeTicket/) - assert.match(lib, /persona_time_authority::get_beijing_time_coordinate/) - assert.match(lib, /persona_time_authority::get_guanghu_era_timeline/) - assert.match(lib, /persona_time_authority::issue_persona_time_ticket/) - assert.match(lib, /persona_time_authority::start_persona_time_authority/) + assert.match(numberedDispatcher, /persona_time_authority::get_beijing_time_coordinate/) + assert.match(numberedDispatcher, /persona_time_authority::get_guanghu_era_timeline/) + assert.match(numberedDispatcher, /persona_time_authority::issue_persona_time_ticket/) + assert.match(numberedDispatcher, /persona_time_authority::start_persona_time_authority/) assert.match(lib, /persona_time_authority::start_on_application_open\(\)/) }) diff --git a/product-source/hololake-native-desktop/scripts/personal-channel-kernel.test.mjs b/product-source/hololake-native-desktop/scripts/personal-channel-kernel.test.mjs index 7343ff840..ffefdde3e 100644 --- a/product-source/hololake-native-desktop/scripts/personal-channel-kernel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/personal-channel-kernel.test.mjs @@ -8,6 +8,7 @@ const foundation = JSON.parse(read('foundation.json')) const stageOne = JSON.parse(read('contracts/stage-one-platform.json')) const rust = read('src-tauri/src/personal_channel.rs') const lib = read('src-tauri/src/lib.rs') +const numberedDispatcher = read('src-tauri/src/numbered_ipc_dispatch.rs') const ui = read('src/main.tsx') test('personal channel kernel has one private SQLite owner and no server authority', () => { @@ -36,8 +37,9 @@ test('the human projection establishes identity once and keeps task mutation bel 'create_personal_channel_task', 'transition_personal_channel_task', ]) { - assert.match(lib, new RegExp(`personal_channel::${command}`)) + assert.match(numberedDispatcher, new RegExp(`personal_channel::${command}`)) } + assert.doesNotMatch(lib, /personal_channel::get_personal_channel_snapshot/) for (const command of ['get_personal_channel_snapshot', 'initialize_personal_channel']) { assert.match(ui, new RegExp(`['"]${command}['"]`)) } diff --git a/product-source/hololake-native-desktop/scripts/programming-ai-terminal-link.test.mjs b/product-source/hololake-native-desktop/scripts/programming-ai-terminal-link.test.mjs index d116d86e2..05c5be737 100644 --- a/product-source/hololake-native-desktop/scripts/programming-ai-terminal-link.test.mjs +++ b/product-source/hololake-native-desktop/scripts/programming-ai-terminal-link.test.mjs @@ -6,9 +6,12 @@ const contract = JSON.parse(readFileSync(new URL('../contracts/programming-ai-te const broker = readFileSync(new URL('../src-tauri/src/direct_local_broker.rs', import.meta.url), 'utf8') const session = readFileSync(new URL('../src-tauri/src/direct_local_session.rs', import.meta.url), 'utf8') const lib = readFileSync(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf8') +const numberedDispatcher = readFileSync(new URL('../src-tauri/src/numbered_ipc_dispatch.rs', import.meta.url), 'utf8') test('terminal link has native local transports for macOS Linux and Windows', () => { - assert.equal(contract.protocol, 'HOLOLAKE_TERMINAL_LINK/2') + assert.equal(contract.protocol, 'HOLOLAKE_TERMINAL_LINK/3') + assert.equal(contract.numbered_envelope.registry, 'HLP-NBROKER-ROOT-001') + assert.equal(contract.numbered_envelope.legacy_string_operation_allowed, false) assert.equal(contract.platform_transports.macos, 'USER_PRIVATE_UNIX_SOCKET') assert.equal(contract.platform_transports.linux, 'USER_PRIVATE_UNIX_SOCKET') assert.equal(contract.platform_transports.windows, 'USER_PRIVATE_NAMED_PIPE') @@ -34,7 +37,8 @@ test('heartbeats refresh the session and environment without granting a shell', assert.equal(contract.phase_boundary.supervised_shell_execution, false) assert.match(session, /HEARTBEAT_ACK/) assert.match(broker, /HEARTBEAT_ACK_ENVIRONMENT_REFRESHED/) - assert.match(lib, /heartbeat_direct_local_session/) + assert.match(numberedDispatcher, /heartbeat_direct_local_session/) + assert.doesNotMatch(lib, /direct_local_session::heartbeat_direct_local_session/) }) test('transport recovery never blindly replays an uncertain mutation', () => { diff --git a/product-source/hololake-native-desktop/scripts/release-pipeline.mjs b/product-source/hololake-native-desktop/scripts/release-pipeline.mjs index db42b1f31..b05a7cffd 100644 --- a/product-source/hololake-native-desktop/scripts/release-pipeline.mjs +++ b/product-source/hololake-native-desktop/scripts/release-pipeline.mjs @@ -102,19 +102,52 @@ export function validateCredentialEnvironment(env) { } const appleIdFlow = env.APPLE_ID && env.APPLE_PASSWORD && env.APPLE_TEAM_ID const appleApiFlow = env.APPLE_API_KEY && env.APPLE_API_ISSUER && env.APPLE_API_KEY_PATH - if (!appleIdFlow && !appleApiFlow) missing.push('APPLE_NOTARIZATION_CREDENTIAL_SET') + const xcodeOrganizerFlow = env.APPLE_XCODE_ORGANIZER_FLOW === '1' + if (!appleIdFlow && !appleApiFlow && !xcodeOrganizerFlow) missing.push('APPLE_NOTARIZATION_CREDENTIAL_SET') if (missing.length) fail(`HOLOLAKE_RELEASE_PIPELINE_CREDENTIALS_MISSING:${[...new Set(missing)].sort().join(',')}`) + return xcodeOrganizerFlow ? 'XCODE_ORGANIZER' : 'TAURI_AUTOMATED' +} + +export function materializeUpdaterPrivateKey(env, readText = (file) => fs.readFileSync(file, 'utf8')) { + if (typeof env.TAURI_SIGNING_PRIVATE_KEY === 'string' && env.TAURI_SIGNING_PRIVATE_KEY.trim()) { + return { ...env } + } + const privateKeyPath = requireText( + env.TAURI_SIGNING_PRIVATE_KEY_PATH, + 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_PATH_REQUIRED', + ) + const privateKey = requireText( + readText(privateKeyPath), + 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_EMPTY', + ) + const { TAURI_SIGNING_PRIVATE_KEY_PATH: _privateKeyPath, ...childEnvironment } = env + return { + ...childEnvironment, + TAURI_SIGNING_PRIVATE_KEY: privateKey, + } } function run(program, args, options = {}) { return execFileSync(program, args, { - cwd: root, + cwd: options.cwd || root, encoding: 'utf8', stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', - env: process.env, + env: options.env || process.env, }) } +function tryRun(program, args, options = {}) { + const result = spawnSync(program, args, { + cwd: options.cwd || root, + encoding: 'utf8', + env: options.env || process.env, + }) + return { + ok: !result.error && result.status === 0, + output: `${result.stdout || ''}\n${result.stderr || ''}`.trim(), + } +} + function captureAll(program, args) { const result = spawnSync(program, args, { cwd: root, encoding: 'utf8', env: process.env }) if (result.error) throw result.error @@ -165,18 +198,28 @@ function codeSignatureEvidence(appPath, input) { identifier: value('Identifier'), teamIdentifier: value('TeamIdentifier'), cdHash: value('CDHash'), + executableSha256: sha256File(path.join(appPath, 'Contents/MacOS/hololake-native-desktop')), sourceCommit: input.sourceCommit, } } -function notarizationEvidence(appPath, dmgPath, input) { - run('/usr/sbin/spctl', ['--assess', '--type', 'open', '--context', 'context:primary-signature', '--verbose=4', dmgPath]) - run('/usr/bin/xcrun', ['stapler', 'validate', dmgPath]) +function notarizationEvidence(appPath, dmgPath, input, options = {}) { + const dmgGatekeeper = tryRun('/usr/sbin/spctl', ['--assess', '--type', 'open', '--context', 'context:primary-signature', '--verbose=4', dmgPath]) + const dmgStapler = tryRun('/usr/bin/xcrun', ['stapler', 'validate', dmgPath]) + if (options.requireNotarizedDmg && (!dmgGatekeeper.ok || !dmgStapler.ok)) { + fail('HOLOLAKE_RELEASE_PIPELINE_DMG_NOTARIZATION_REQUIRED') + } return { schema: 'hololake.apple-notarization-receipt/v1', - state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED', + state: dmgGatekeeper.ok && dmgStapler.ok + ? 'APPLE_APP_AND_DMG_NOTARIZATION_ACCEPTED_AND_STAPLED' + : 'APPLE_APP_NOTARIZATION_ACCEPTED_AND_STAPLED_DMG_CONTAINS_NOTARIZED_APP', appName: path.basename(appPath), dmgName: path.basename(dmgPath), + appStaplerValidation: 'PASS', + dmgGatekeeperAssessment: dmgGatekeeper.ok ? 'PASS' : 'NOT_ASSERTED_FOR_XCODE_EXPORTED_APP_FLOW', + dmgStaplerValidation: dmgStapler.ok ? 'PASS' : 'NOT_STAPLED_OUTER_CONTAINER', + ...(process.env.APPLE_XCODE_SUBMISSION_ID ? { submissionIdentifier: process.env.APPLE_XCODE_SUBMISSION_ID } : {}), appleTeamIdentifier: input.appleTeamIdentifier, sourceCommit: input.sourceCommit, } @@ -186,43 +229,130 @@ function writeJson(file, value) { fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) } -export async function main(argv = process.argv.slice(2)) { - const inputArgument = argv[0] - if (!inputArgument) fail('HOLOLAKE_RELEASE_PIPELINE_INPUT_PATH_REQUIRED') - const inputPath = path.resolve(inputArgument) - const trust = readJson(path.join(root, 'src-tauri/release-trust.json')) - const trustFacts = validateReleaseTrust(trust) - const input = validateReleaseInput(readJson(inputPath), trustFacts) - validateCredentialEnvironment(process.env) - requireCleanImmutableSource(input) - if (process.platform !== 'darwin' || process.arch !== 'arm64') fail('HOLOLAKE_RELEASE_PIPELINE_BUILD_HOST_MISMATCH') - if (!process.env.APPLE_SIGNING_IDENTITY.includes(input.appleTeamIdentifier)) fail('HOLOLAKE_RELEASE_PIPELINE_SIGNING_TEAM_MISMATCH') +function localDateDirectory() { + const date = new Date() + const part = (value) => String(value).padStart(2, '0') + return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())}` +} - run('npm', ['test']) - run('cargo', ['clippy', '--manifest-path', 'src-tauri/Cargo.toml', '--all-targets', '--all-features', '--', '-D', 'warnings']) - run('cargo', ['test', '--manifest-path', 'src-tauri/Cargo.toml', '--all-targets', '--all-features']) - - const buildConfig = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-config-')), 'tauri.release.json') - fs.writeFileSync(buildConfig, `${JSON.stringify({ bundle: { createUpdaterArtifacts: true } })}\n`, { mode: 0o600 }) +function unsignedExecutableSha256(executable) { + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-unsigned-executable-')) + const copy = path.join(temporary, path.basename(executable)) try { - run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig]) + fs.copyFileSync(executable, copy) + run('/usr/bin/codesign', ['--remove-signature', copy]) + return sha256File(copy) } finally { - fs.rmSync(path.dirname(buildConfig), { recursive: true, force: true }) + fs.rmSync(temporary, { recursive: true, force: true }) } +} - const bundleRoot = path.join(root, 'src-tauri/target/release/bundle') - const appExecutable = findOne(path.join(bundleRoot, 'macos'), (file) => file.endsWith('/Contents/MacOS/hololake-native-desktop'), 'HOLOLAKE_RELEASE_PIPELINE_APP_NOT_UNIQUE') - const application = path.resolve(appExecutable, '../../../..') - const dmg = findOne(path.join(bundleRoot, 'dmg'), (file) => file.endsWith('.dmg'), 'HOLOLAKE_RELEASE_PIPELINE_DMG_NOT_UNIQUE') - const updater = findOne(bundleRoot, (file) => file.endsWith('.app.tar.gz'), 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_NOT_UNIQUE') - const updaterSignature = `${updater}.sig` - if (!fs.statSync(updaterSignature, { throwIfNoEntry: false })?.isFile()) fail('HOLOLAKE_RELEASE_PIPELINE_UPDATER_SIGNATURE_MISSING') +export function formatPlistDate(date) { + const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + const part = (value) => String(value).padStart(2, '0') + return `${weekdays[date.getUTCDay()]} ${months[date.getUTCMonth()]} ${part(date.getUTCDate())} ${part(date.getUTCHours())}:${part(date.getUTCMinutes())}:${part(date.getUTCSeconds())} ${date.getUTCFullYear()}` +} + +function createXcodeArchive(application, input) { + const archive = path.join( + os.homedir(), + 'Library/Developer/Xcode/Archives', + localDateDirectory(), + `HoloLake-${input.version}-${input.sourceCommit.slice(0, 12)}.xcarchive`, + ) + if (fs.existsSync(archive)) fail('HOLOLAKE_RELEASE_PIPELINE_XCODE_ARCHIVE_ALREADY_EXISTS') + const archivedApp = path.join(archive, 'Products/Applications/HoloLake.app') + fs.mkdirSync(path.dirname(archivedApp), { recursive: true, mode: 0o700 }) + run('/usr/bin/ditto', [application, archivedApp]) + const info = path.join(archive, 'Info.plist') + run('/usr/bin/plutil', ['-create', 'xml1', info]) + const add = (key, type, value) => run('/usr/libexec/PlistBuddy', ['-c', `Add :${key} ${type} ${value}`, info]) + add('ArchiveVersion', 'integer', '2') + add('CreationDate', 'date', formatPlistDate(new Date())) + add('Name', 'string', `HoloLake ${input.version} ${input.sourceCommit.slice(0, 12)}`) + add('SchemeName', 'string', 'HoloLake') + add('HoloLakeSourceCommit', 'string', input.sourceCommit) + const executable = path.join(application, 'Contents/MacOS/hololake-native-desktop') + add('HoloLakePreNotarizationExecutableSha256', 'string', sha256File(executable)) + add('HoloLakeUnsignedExecutableSha256', 'string', unsignedExecutableSha256(executable)) + add('ApplicationProperties', 'dict', '') + add('ApplicationProperties:ApplicationPath', 'string', 'Applications/HoloLake.app') + add('ApplicationProperties:ArchiveVersion', 'integer', '2') + add('ApplicationProperties:CFBundleIdentifier', 'string', 'world.guanghu.hololake') + add('ApplicationProperties:CFBundleShortVersionString', 'string', input.version) + add('ApplicationProperties:CFBundleVersion', 'string', input.version) + add('ApplicationProperties:SigningIdentity', 'string', process.env.APPLE_SIGNING_IDENTITY) + add('ApplicationProperties:Team', 'string', input.appleTeamIdentifier) + return archive +} + +function findPreparedXcodeArchive(input) { + const archivesRoot = path.join(os.homedir(), 'Library/Developer/Xcode/Archives') + const archiveName = `HoloLake-${input.version}-${input.sourceCommit.slice(0, 12)}.xcarchive` + const matches = [] + for (const dateEntry of fs.readdirSync(archivesRoot, { withFileTypes: true })) { + if (!dateEntry.isDirectory()) continue + const candidate = path.join(archivesRoot, dateEntry.name, archiveName) + if (fs.statSync(candidate, { throwIfNoEntry: false })?.isDirectory()) matches.push(candidate) + } + if (matches.length !== 1) fail(`HOLOLAKE_RELEASE_PIPELINE_PREPARED_XCODE_ARCHIVE_NOT_UNIQUE:${matches.length}`) + return matches[0] +} + +function resolveNotarizedApplication(argument, input) { + const candidate = path.resolve(requireText(argument, 'HOLOLAKE_RELEASE_PIPELINE_NOTARIZED_APP_PATH_REQUIRED')) + const application = candidate.endsWith('.app') ? candidate : path.join(candidate, 'HoloLake.app') + if (!fs.statSync(application, { throwIfNoEntry: false })?.isDirectory()) { + fail('HOLOLAKE_RELEASE_PIPELINE_NOTARIZED_APP_NOT_FOUND') + } + const info = path.join(application, 'Contents/Info.plist') + const identifier = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', info], { capture: true }).trim() + const version = run('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleShortVersionString', info], { capture: true }).trim() + if (identifier !== 'world.guanghu.hololake' || version !== input.version) { + fail('HOLOLAKE_RELEASE_PIPELINE_NOTARIZED_APP_IDENTITY_MISMATCH') + } + const archive = findPreparedXcodeArchive(input) + const expectedUnsignedExecutableSha256 = run('/usr/libexec/PlistBuddy', [ + '-c', 'Print :HoloLakeUnsignedExecutableSha256', path.join(archive, 'Info.plist'), + ], { capture: true }).trim() + const actualUnsignedExecutableSha256 = unsignedExecutableSha256( + path.join(application, 'Contents/MacOS/hololake-native-desktop'), + ) + if (actualUnsignedExecutableSha256 !== expectedUnsignedExecutableSha256) { + fail('HOLOLAKE_RELEASE_PIPELINE_NOTARIZED_APP_SOURCE_BINARY_MISMATCH') + } + return application +} + +function buildUpdaterFromNotarizedApp(application, destination, credentialEnvironment) { + const updater = path.join(destination, 'HoloLake.app.tar.gz') + run('/usr/bin/tar', ['-czf', updater, '-C', path.dirname(application), path.basename(application)]) + run('npm', ['run', 'tauri', '--', 'signer', 'sign', updater], { env: credentialEnvironment }) + const signature = `${updater}.sig` + if (!fs.statSync(signature, { throwIfNoEntry: false })?.isFile()) { + fail('HOLOLAKE_RELEASE_PIPELINE_UPDATER_SIGNATURE_MISSING') + } + return { updater, signature } +} + +function buildDmgFromNotarizedApp(application, destination, input) { + const staging = path.join(destination, 'dmg-root') + fs.mkdirSync(staging, { recursive: true, mode: 0o700 }) + run('/usr/bin/ditto', [application, path.join(staging, 'HoloLake.app')]) + fs.symlinkSync('/Applications', path.join(staging, 'Applications')) + const dmg = path.join(destination, `HoloLake_${input.version}_aarch64.dmg`) + run('/usr/bin/hdiutil', ['create', '-volname', 'HoloLake', '-srcfolder', staging, '-ov', '-format', 'UDZO', dmg]) + run('/usr/bin/codesign', ['--force', '--timestamp', '--sign', process.env.APPLE_SIGNING_IDENTITY, dmg]) + return dmg +} + +function writeReleaseOutput(input, trustFacts, application, updater, updaterSignature, dmg, options = {}) { if (decodeURIComponent(input.packageUrl.pathname.split('/').pop()) !== path.basename(updater)) { fail('HOLOLAKE_RELEASE_PIPELINE_PACKAGE_URL_FILENAME_MISMATCH') } - const codeReceipt = codeSignatureEvidence(application, input) - const notarizationReceipt = notarizationEvidence(application, dmg, input) + const notarizationReceipt = notarizationEvidence(application, dmg, input, options) const outputRoot = path.join(root, 'release/out', input.version) if (fs.existsSync(outputRoot)) fail('HOLOLAKE_RELEASE_PIPELINE_OUTPUT_ALREADY_EXISTS') fs.mkdirSync(outputRoot, { recursive: true, mode: 0o700 }) @@ -276,6 +406,90 @@ export async function main(argv = process.argv.slice(2)) { process.stdout.write(`HOLOLAKE_RELEASE_PIPELINE_READY:${outputRoot}\n`) } +export async function main(argv = process.argv.slice(2)) { + const inputArgument = argv[0] + if (!inputArgument) fail('HOLOLAKE_RELEASE_PIPELINE_INPUT_PATH_REQUIRED') + const command = argv[1] || 'release' + if (!['release', 'prepare-xcode', 'finalize-xcode'].includes(command)) { + fail('HOLOLAKE_RELEASE_PIPELINE_COMMAND_INVALID') + } + const inputPath = path.resolve(inputArgument) + const trust = readJson(path.join(root, 'src-tauri/release-trust.json')) + const trustFacts = validateReleaseTrust(trust) + const input = validateReleaseInput(readJson(inputPath), trustFacts) + const notarizationMode = validateCredentialEnvironment(process.env) + if (command !== 'release' && notarizationMode !== 'XCODE_ORGANIZER') { + fail('HOLOLAKE_RELEASE_PIPELINE_XCODE_ORGANIZER_FLOW_REQUIRED') + } + const credentialEnvironment = materializeUpdaterPrivateKey(process.env) + requireCleanImmutableSource(input) + if (process.platform !== 'darwin' || process.arch !== 'arm64') fail('HOLOLAKE_RELEASE_PIPELINE_BUILD_HOST_MISMATCH') + if (!process.env.APPLE_SIGNING_IDENTITY.includes(input.appleTeamIdentifier)) fail('HOLOLAKE_RELEASE_PIPELINE_SIGNING_TEAM_MISMATCH') + + if (command === 'finalize-xcode') { + const application = resolveNotarizedApplication(argv[2], input) + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-finalize-')) + try { + const { updater, signature } = buildUpdaterFromNotarizedApp(application, temporary, credentialEnvironment) + run('cargo', [ + 'run', '--quiet', '--manifest-path', 'src-tauri/Cargo.toml', '--example', 'verify_updater_signature', '--', + 'src-tauri/release-trust.json', updater, signature, + ]) + const dmg = buildDmgFromNotarizedApp(application, temporary, input) + writeReleaseOutput(input, trustFacts, application, updater, signature, dmg, { requireNotarizedDmg: false }) + } finally { + fs.rmSync(temporary, { recursive: true, force: true }) + } + return + } + + run('npm', ['test']) + run('cargo', ['clippy', '--manifest-path', 'src-tauri/Cargo.toml', '--all-targets', '--all-features', '--', '-D', 'warnings']) + run('cargo', ['test', '--manifest-path', 'src-tauri/Cargo.toml', '--all-targets', '--all-features']) + + const buildConfig = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-config-')), 'tauri.release.json') + fs.writeFileSync(buildConfig, `${JSON.stringify({ bundle: { createUpdaterArtifacts: true } })}\n`, { mode: 0o600 }) + try { + run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig], { + env: credentialEnvironment, + }) + } finally { + fs.rmSync(path.dirname(buildConfig), { recursive: true, force: true }) + } + + const bundleRoot = path.join(root, 'src-tauri/target/release/bundle') + const appExecutable = findOne(path.join(bundleRoot, 'macos'), (file) => file.endsWith('/Contents/MacOS/hololake-native-desktop'), 'HOLOLAKE_RELEASE_PIPELINE_APP_NOT_UNIQUE') + const application = path.resolve(appExecutable, '../../..') + const dmg = findOne(path.join(bundleRoot, 'dmg'), (file) => file.endsWith('.dmg'), 'HOLOLAKE_RELEASE_PIPELINE_DMG_NOT_UNIQUE') + const updater = findOne(bundleRoot, (file) => file.endsWith('.app.tar.gz'), 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_NOT_UNIQUE') + const updaterSignature = `${updater}.sig` + if (!fs.statSync(updaterSignature, { throwIfNoEntry: false })?.isFile()) fail('HOLOLAKE_RELEASE_PIPELINE_UPDATER_SIGNATURE_MISSING') + run('cargo', [ + 'run', + '--quiet', + '--manifest-path', + 'src-tauri/Cargo.toml', + '--example', + 'verify_updater_signature', + '--', + 'src-tauri/release-trust.json', + updater, + updaterSignature, + ]) + + if (command === 'prepare-xcode') { + run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=4', application]) + const archive = createXcodeArchive(application, input) + process.stdout.write(`HOLOLAKE_RELEASE_XCODE_ARCHIVE_READY:${archive}\n`) + return + } + if (decodeURIComponent(input.packageUrl.pathname.split('/').pop()) !== path.basename(updater)) { + fail('HOLOLAKE_RELEASE_PIPELINE_PACKAGE_URL_FILENAME_MISMATCH') + } + + writeReleaseOutput(input, trustFacts, application, updater, updaterSignature, dmg, { requireNotarizedDmg: true }) +} + if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { main().catch((error) => { process.stderr.write(`${error.message}\n`) diff --git a/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs b/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs index 2a6e61298..9d71ec463 100644 --- a/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs +++ b/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs @@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs' import test from 'node:test' import { + formatPlistDate, + materializeUpdaterPrivateKey, validateCredentialEnvironment, validateReleaseInput, validateReleaseTrust, @@ -38,6 +40,10 @@ const readyInput = () => ({ restartMessage: 'Restart manually', }) +test('Xcode archive creation uses the plist date format accepted by PlistBuddy', () => { + assert.equal(formatPlistDate(new Date('2026-08-19T01:02:03Z')), 'Wed Aug 19 01:02:03 2026') +}) + test('release pipeline refuses an unprovisioned or upstream-owned trust document', () => { const unprovisioned = readyTrust() unprovisioned.state = 'UNPROVISIONED_FAIL_CLOSED' @@ -74,6 +80,43 @@ test('release pipeline requires updater signing, Developer ID and Apple notariza APPLE_PASSWORD: 'provided-at-runtime', APPLE_TEAM_ID: 'TEAM', })) + assert.equal(validateCredentialEnvironment({ + APPLE_SIGNING_IDENTITY: 'Developer ID Application: HoloLake (TEAM)', + TAURI_SIGNING_PRIVATE_KEY: 'runtime-secret-material', + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: 'provided-at-runtime', + APPLE_XCODE_ORGANIZER_FLOW: '1', + }), 'XCODE_ORGANIZER') + assert.throws(() => validateCredentialEnvironment({ + APPLE_SIGNING_IDENTITY: 'Developer ID Application: HoloLake (TEAM)', + APPLE_XCODE_ORGANIZER_FLOW: '1', + }), /TAURI_SIGNING_PRIVATE_KEY_OR_PATH/) +}) + +test('release pipeline materializes a protected updater key path only inside the build environment', () => { + const source = { + TAURI_SIGNING_PRIVATE_KEY_PATH: '/protected/hololake-updater.key', + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: 'provided-at-runtime', + } + const hydrated = materializeUpdaterPrivateKey(source, (file) => { + assert.equal(file, source.TAURI_SIGNING_PRIVATE_KEY_PATH) + return 'encrypted-private-key-material' + }) + assert.equal(hydrated.TAURI_SIGNING_PRIVATE_KEY, 'encrypted-private-key-material') + assert.equal(hydrated.TAURI_SIGNING_PRIVATE_KEY_PATH, undefined) + assert.equal(source.TAURI_SIGNING_PRIVATE_KEY, undefined) +}) + +test('release pipeline verifies the updater signature against embedded product trust before broadcast', () => { + const source = readFileSync(new URL('./release-pipeline.mjs', import.meta.url), 'utf8') + assert.match(source, /--example',\s*'verify_updater_signature'/) + assert.match(source, /src-tauri\/release-trust\.json/) + assert.match(source, /HoloLakePreNotarizationExecutableSha256/) + assert.match(source, /HoloLakeUnsignedExecutableSha256/) + assert.match(source, /--remove-signature/) + assert.match(source, /NOTARIZED_APP_SOURCE_BINARY_MISMATCH/) + assert.match(source, /APPLE_APP_NOTARIZATION_ACCEPTED_AND_STAPLED_DMG_CONTAINS_NOTARIZED_APP/) + assert.match(source, /path\.resolve\(appExecutable, '\.\.\/\.\.\/\.\.'\)/) + assert.doesNotMatch(source, /path\.resolve\(appExecutable, '\.\.\/\.\.\/\.\.\/\.\.'\)/) }) test('Windows updater keeps signed installation but never claims the macOS rollback boundary', () => { diff --git a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs new file mode 100644 index 000000000..1a160feb9 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' +import { compileUnifiedNumberTree } from './compile-unified-number-tree.mjs' + +test('identity, webview and direct broker numbers compile into one unique evidence tree', () => { + const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8')) + assert.deepEqual(generated, compileUnifiedNumberTree()) + assert.equal(generated.schema, 'hololake.unified-number-coordinate-tree/v2') + assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') + assert.equal(generated.coordinateCount, 272) + assert.equal(generated.routeCount, 169) + assert.equal(generated.identityNodeCount, 4) + assert.equal(generated.protocolNodeCount, 99) + assert.equal(generated.referenceOnlyNodeCount, 16) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 169) + assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 272) + assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) + assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) + assert.equal(generated.invariants.everyProtocolReferenceHasNumberCoordinate, true) + assert.equal(generated.invariants.referenceOnlyNodesNeverExecutable, true) + assert.equal(generated.invariants.unresolvedNumberReferenceCount, 0) + assert.ok(generated.protocolNodes.filter((node) => node.nodeKind === 'GLS_REFERENCE_ONLY').every((node) => node.executionState === 'REFERENCE_ONLY_NOT_EXECUTABLE')) + assert.ok(generated.routes.every((route) => route.admission && route.evidence)) +}) diff --git a/product-source/hololake-native-desktop/scripts/user-pncc-channel.test.mjs b/product-source/hololake-native-desktop/scripts/user-pncc-channel.test.mjs index 0c9a27836..c5e5ddeb1 100644 --- a/product-source/hololake-native-desktop/scripts/user-pncc-channel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/user-pncc-channel.test.mjs @@ -24,9 +24,11 @@ test('the native user PNCC contract binds verified number, signed-in account and test('the user PNCC is a native command and first-class HoloLake projection', () => { const native = readText('src-tauri/src/lib.rs') + const numberedDispatcher = readText('src-tauri/src/numbered_ipc_dispatch.rs') const frontend = readText('src/main.tsx') - assert.match(native, /user_pncc_channel::get_user_pncc_channel/) - assert.match(native, /user_pncc_channel::ensure_user_pncc_channel/) + assert.match(numberedDispatcher, /user_pncc_channel::get_user_pncc_channel/) + assert.match(numberedDispatcher, /user_pncc_channel::ensure_user_pncc_channel/) + assert.doesNotMatch(native, /user_pncc_channel::get_user_pncc_channel/) assert.match(frontend, /GH-PNCC · 人格原生代码频道/) assert.match(frontend, /Forgejo 协作适配器/) assert.match(frontend, /ensure_user_pncc_channel/) diff --git a/product-source/hololake-native-desktop/scripts/web-novel-workbench-admission.test.mjs b/product-source/hololake-native-desktop/scripts/web-novel-workbench-admission.test.mjs new file mode 100644 index 000000000..36d7fcdda --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/web-novel-workbench-admission.test.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const runtime = read('src-tauri/src/module_package_runtime.rs') +const moduleAdapter = read('src-tauri/src/web_novel_modules.rs') +const frontend = [ + read('src/modules/web-novel/WebNovelWorkspace.tsx'), + read('src/modules/web-novel/AuthorModuleCenter.tsx'), + read('src/modules/web-novel/AuthorWritingSidecar.tsx'), +].join('\n') + +const packages = [ + 'WORKBENCH', 'OUTLINE', 'GRID', 'STORYWORLD', 'DELIVERY', +].map((name) => JSON.parse(read(`fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-${name}-0001-0.1.0.ghmod`))) + +test('web novel family has five immutable official identities behind one shared lifecycle', () => { + assert.equal(packages.length, 5) + assert.ok(packages.every((value) => value.manifest.registrationClass === 'OFFICIAL_LIGHTHOUSE')) + assert.ok(packages.every((value) => value.manifest.adapter === 'web-novel-workbench-v1')) + assert.equal(new Set(packages.map((value) => value.manifest.moduleNumber)).size, 5) + assert.match(runtime, /const BUNDLED_MODULES/) + assert.doesNotMatch(frontend, /install_web_novel_author_module|mount_web_novel_author_module|uninstall_web_novel_author_module/) +}) + +test('all 38 web novel effects cross exact numbered routes and no raw tauri command surface', () => { + const routes = registry.operations.filter((route) => Number(route.operation_number.slice(-4)) >= 103 && Number(route.operation_number.slice(-4)) <= 140) + assert.equal(routes.length, 38) + assert.deepEqual([...new Set(routes.map((route) => route.module_number))], [ + 'HLP-NIPC-MOD-0025', 'HLP-NIPC-MOD-0026', 'HLP-NIPC-MOD-0027', 'HLP-NIPC-MOD-0028', 'HLP-NIPC-MOD-0029', + ]) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) + for (const source of ['web_novel_workspace.rs', 'web_novel_import.rs', 'web_novel_author.rs', 'web_novel_modules.rs']) { + assert.doesNotMatch(read(`src-tauri/src/${source}`), /#\[tauri::command\]/) + } +}) + +test('advanced effects require their own exact active module number', () => { + assert.match(moduleAdapter, /require_module_active\(&app, OUTLINE\)/) + assert.match(moduleAdapter, /require_module_active\(&app, GRID\)/) + assert.match(moduleAdapter, /require_module_active\(&app, STORYWORLD\)/) + assert.match(moduleAdapter, /require_module_active\(&app, DELIVERY\)/) + assert.match(moduleAdapter, /shared signed package runtime/) +}) + +test('workspace is independently lazy-loaded and preserves old account data in place', () => { + assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/web-novel\/WebNovelWorkspace'\)/) + assert.equal(packages[0].payload.adapterConfig.legacyDataPolicy, 'READ_IN_PLACE_NO_DESTRUCTIVE_MIGRATION') + assert.equal(packages[0].payload.adapterConfig.thirdPartyPublishDefault, 'DENY') + assert.doesNotMatch(read('src-tauri/src/web_novel_workspace.rs'), /serde_json::from_str[^\n]+unwrap_or_default\(\)/) +}) diff --git a/product-source/hololake-native-desktop/scripts/zero-core-numbering-kernel.test.mjs b/product-source/hololake-native-desktop/scripts/zero-core-numbering-kernel.test.mjs index 40df50ff5..584d67933 100644 --- a/product-source/hololake-native-desktop/scripts/zero-core-numbering-kernel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/zero-core-numbering-kernel.test.mjs @@ -9,7 +9,7 @@ test('zero-core numbering kernel pins the canonical authority map and fails clos assert.equal(contract.schema, 'hololake.zero-core-numbering-kernel/v1') assert.equal(contract.authority.map_id, 'GH-IDENTITY-AUTHORITY-MAP-001') - assert.equal(contract.authority.source_commit, '2598fbfba8caf64c7ab9740a3036c5aab977502e') + assert.equal(contract.authority.source_commit, '104a5d73162bdf4a529701e65898e2bc2863ea9e') assert.equal(contract.runtime.number_shape_is_authority, false) assert.equal(contract.runtime.unknown_number, 'FAIL_CLOSED') assert.equal(contract.runtime.automatic_identity_issuance, false) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.lock b/product-source/hololake-native-desktop/src-tauri/Cargo.lock index eb5ebf94c..6776d71d0 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.lock @@ -268,6 +268,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calamine" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1" +dependencies = [ + "byteorder", + "chrono", + "codepage", + "encoding_rs", + "log", + "quick-xml 0.31.0", + "serde", + "zip 2.4.2", +] + [[package]] name = "camino" version = "1.2.5" @@ -393,6 +409,15 @@ dependencies = [ "cc", ] +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + [[package]] name = "combine" version = "4.6.7" @@ -570,6 +595,27 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.8.0" @@ -890,6 +936,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1492,16 +1547,24 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hololake-native-desktop" -version = "0.4.1" +version = "0.5.0" dependencies = [ "base64 0.22.1", + "calamine", + "csv", "dirs", + "encoding_rs", "fs2", "futures-util", "interprocess", + "minisign-verify", + "qrcode", + "quick-xml 0.31.0", + "regex", "reqwest", "ring", "rusqlite", + "rust_xlsxwriter", "serde", "serde_json", "tauri", @@ -1515,6 +1578,7 @@ dependencies = [ "url", "uuid", "widestring", + "zip 0.6.6", ] [[package]] @@ -2833,7 +2897,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml", + "quick-xml 0.41.0", "serde", "time", ] @@ -2980,6 +3044,22 @@ dependencies = [ "psl-types", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -3350,6 +3430,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust_xlsxwriter" +version = "0.64.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f47f5318c1e512e57c07781559367577b1eb9618325cf1574cd30d38b112c5" +dependencies = [ + "lazy_static", + "regex", + "zip 0.6.6", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -4310,7 +4401,7 @@ dependencies = [ "tokio", "url", "windows-sys 0.60.2", - "zip", + "zip 4.6.1", ] [[package]] @@ -5846,6 +5937,35 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.20", + "zopfli", +] + [[package]] name = "zip" version = "4.6.1" @@ -5863,3 +5983,15 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index 38634cc57..92fa937ef 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hololake-native-desktop" -version = "0.4.1" +version = "0.5.0" description = "HoloLake native desktop foundation" authors = ["HoloLake"] license = "AGPL-3.0-or-later" @@ -21,6 +21,11 @@ interprocess = "2.4.2" ring = "0.17" rusqlite = { version = "0.31", features = ["bundled"] } base64 = "0.22" +calamine = { version = "=0.26.1", features = ["dates"] } +csv = "=1.3.0" +encoding_rs = "0.8" +quick-xml = "=0.31.0" +regex = "=1.12.3" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri = { version = "=2.10.2", features = ["devtools"] } @@ -31,8 +36,12 @@ tauri-plugin-dialog = "=2.7.2" uuid = { version = "1", features = ["v4"] } url = "2" reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] } +rust_xlsxwriter = "=0.64.2" +zip = { version = "=0.6.6", default-features = false, features = ["deflate"] } tokio = { version = "1", features = ["time"] } futures-util = "0.3" +minisign-verify = "0.2.5" +qrcode = { version = "0.14", default-features = false, features = ["svg"] } [target.'cfg(windows)'.dependencies] widestring = "1" diff --git a/product-source/hololake-native-desktop/src-tauri/capabilities/default.json b/product-source/hololake-native-desktop/src-tauri/capabilities/default.json index 627dc8d8f..46fca907d 100644 --- a/product-source/hololake-native-desktop/src-tauri/capabilities/default.json +++ b/product-source/hololake-native-desktop/src-tauri/capabilities/default.json @@ -1,7 +1,7 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Minimum HoloLake desktop foundation; updater actions are not exposed to the webview yet.", + "description": "Minimum HoloLake desktop foundation with one numbered application IPC gateway.", "windows": ["main"], - "permissions": ["core:default"] + "permissions": ["core:default", "allow-numbered-ipc"] } diff --git a/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs b/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs new file mode 100644 index 000000000..751aff370 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs @@ -0,0 +1,48 @@ +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use minisign_verify::{PublicKey, Signature}; +use serde_json::Value; +use std::{env, fs, path::Path}; + +fn decode_outer_base64(value: &str, label: &str) -> Result { + let bytes = STANDARD + .decode(value.trim()) + .map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?; + String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) +} + +fn main() -> Result<(), String> { + let arguments: Vec = env::args().collect(); + let [_, trust_path, updater_path, signature_path] = arguments.as_slice() else { + return Err( + "USAGE: verify_updater_signature " + .to_string(), + ); + }; + let trust: Value = serde_json::from_str( + &fs::read_to_string(trust_path) + .map_err(|error| format!("RELEASE_TRUST_READ_FAILED: {error}"))?, + ) + .map_err(|error| format!("RELEASE_TRUST_JSON_INVALID: {error}"))?; + let encoded_public_key = trust + .get("publicKey") + .and_then(Value::as_str) + .ok_or_else(|| "RELEASE_TRUST_PUBLIC_KEY_MISSING".to_string())?; + let public_key = PublicKey::decode(&decode_outer_base64( + encoded_public_key, + "RELEASE_TRUST_PUBLIC_KEY", + )?) + .map_err(|error| format!("RELEASE_TRUST_PUBLIC_KEY_INVALID: {error}"))?; + let signature = Signature::decode(&decode_outer_base64( + &fs::read_to_string(signature_path) + .map_err(|error| format!("UPDATER_SIGNATURE_READ_FAILED: {error}"))?, + "UPDATER_SIGNATURE", + )?) + .map_err(|error| format!("UPDATER_SIGNATURE_INVALID: {error}"))?; + let updater = fs::read(Path::new(updater_path)) + .map_err(|error| format!("UPDATER_ARTIFACT_READ_FAILED: {error}"))?; + public_key + .verify(&updater, &signature, true) + .map_err(|error| format!("UPDATER_SIGNATURE_VERIFICATION_FAILED: {error}"))?; + println!("HOLOLAKE_UPDATER_SIGNATURE_VERIFIED"); + Ok(()) +} diff --git a/product-source/hololake-native-desktop/src-tauri/permissions/numbered-ipc.toml b/product-source/hololake-native-desktop/src-tauri/permissions/numbered-ipc.toml new file mode 100644 index 000000000..3c562d10a --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/permissions/numbered-ipc.toml @@ -0,0 +1,5 @@ +[[permission]] +identifier = "allow-numbered-ipc" +description = "Allow the main HoloLake webview to use the single numbered IPC gateway." +commands.allow = ["numbered_ipc"] + diff --git a/product-source/hololake-native-desktop/src-tauri/persona-control-authorization-signers.json b/product-source/hololake-native-desktop/src-tauri/persona-control-authorization-signers.json new file mode 100644 index 000000000..41e22b526 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/persona-control-authorization-signers.json @@ -0,0 +1,6 @@ +{ + "schema": "gh-aios.persona-control-authorization-signers/v1", + "registryId": "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001", + "state": "CURRENT", + "signers": [] +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/channel_growth.rs b/product-source/hololake-native-desktop/src-tauri/src/channel_growth.rs new file mode 100644 index 000000000..c8ed06cec --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/channel_growth.rs @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! 当前账号私人频道的长期成长模型。 +//! +//! 这里只保存最小化的行为元数据,不保存文档正文、表格单元格、提示词、对话原文或凭据。 +//! 基础算法可以持续形成模块亲和度;模型 API 未来只能提交适配提案,不能绕过本地校验和人类确认。 + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const GROWTH_SCHEMA: &str = "hololake.channel-growth-model/v1"; +const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001"; +const ADAPTER: &str = "persona-channel-body-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RecordChannelGrowthEventInput { + pub event_kind: String, + pub module_id: String, + pub source: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpdateChannelGrowthSharingInput { + pub sharing_mode: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelGrowthControls { + pub sharing_mode: String, + pub official_read_access: bool, + pub history_mutation_allowed: bool, + pub structural_change_requires_human_confirmation: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelModuleAffinity { + pub module_id: String, + pub use_count: i64, + pub score: f64, + pub last_used_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelGrowthSnapshot { + pub schema: &'static str, + pub state: &'static str, + pub controls: ChannelGrowthControls, + pub observed_event_count: i64, + pub persona_collaboration_event_count: i64, + pub module_affinities: Vec, + pub recommended_module_order: Vec, + pub integrity_state: &'static str, + pub last_event_hash: String, + pub model_api_state: &'static str, + pub model_proposal_policy: &'static str, + pub data_boundary: &'static str, +} + +pub async fn get_channel_growth_snapshot(app: AppHandle) -> Result { + require_active(&app)?; + let database = channel_growth_database(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&database)) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))? +} + +pub async fn record_channel_growth_event( + app: AppHandle, + input: RecordChannelGrowthEventInput, +) -> Result { + require_active(&app)?; + let database = channel_growth_database(&app)?; + tauri::async_runtime::spawn_blocking(move || record_event_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))? +} + +pub async fn update_channel_growth_sharing( + app: AppHandle, + input: UpdateChannelGrowthSharingInput, +) -> Result { + require_active(&app)?; + let database = channel_growth_database(&app)?; + tauri::async_runtime::spawn_blocking(move || update_sharing_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))? +} + +fn channel_growth_database(app: &AppHandle) -> Result { + let root = crate::authenticated_storage::account_storage_root(app, "channel-growth-v1")?; + fs::create_dir_all(&root) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_STORAGE_UNAVAILABLE: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&root, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_PERMISSION_FAILED: {error}"))?; + } + Ok(root.join("channel-growth.sqlite3")) +} + +fn open_database(path: &Path) -> Result { + let connection = Connection::open(path) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode = DELETE; + PRAGMA synchronous = FULL; + PRAGMA trusted_schema = OFF; + CREATE TABLE IF NOT EXISTS growth_sharing ( + singleton INTEGER PRIMARY KEY CHECK(singleton = 1), + sharing_mode TEXT NOT NULL CHECK(sharing_mode IN ('LOCAL_ONLY', 'ANONYMIZED_SHARED')) + ); + INSERT OR IGNORE INTO growth_sharing VALUES (1, 'LOCAL_ONLY'); + CREATE TABLE IF NOT EXISTS growth_events ( + sequence INTEGER NOT NULL UNIQUE, + event_id TEXT PRIMARY KEY NOT NULL, + event_kind TEXT NOT NULL, + module_id TEXT NOT NULL, + source TEXT NOT NULL CHECK(source IN ('HUMAN_ACTION', 'PERSONA_COLLABORATION', 'SYSTEM_ASSISTED')), + occurred_at_unix_ms INTEGER NOT NULL, + previous_hash TEXT NOT NULL, + event_hash TEXT NOT NULL UNIQUE + ); + CREATE TRIGGER IF NOT EXISTS growth_events_no_update BEFORE UPDATE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END; + CREATE TRIGGER IF NOT EXISTS growth_events_no_delete BEFORE DELETE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END; + CREATE TABLE IF NOT EXISTS module_affinities ( + module_id TEXT PRIMARY KEY NOT NULL, + use_count INTEGER NOT NULL, + score REAL NOT NULL, + last_used_at_unix_ms INTEGER NOT NULL + );", + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SCHEMA_FAILED: {error}"))?; + Ok(connection) +} + +fn validate_token(value: &str, label: &str) -> Result { + let value = value.trim(); + if value.is_empty() + || value.len() > 96 + || !value + .chars() + .all(|item| item.is_ascii_alphanumeric() || matches!(item, '-' | '_' | '.' | ':')) + { + return Err(format!("HOLOLAKE_CHANNEL_GROWTH_{label}_INVALID")); + } + Ok(value.to_string()) +} + +fn record_event_at( + database: &Path, + input: RecordChannelGrowthEventInput, +) -> Result { + let event_kind = validate_token(&input.event_kind, "EVENT_KIND")?; + let module_id = validate_token(&input.module_id, "MODULE_ID")?; + let source = input.source.trim().to_string(); + if !matches!( + source.as_str(), + "HUMAN_ACTION" | "PERSONA_COLLABORATION" | "SYSTEM_ASSISTED" + ) { + return Err("HOLOLAKE_CHANNEL_GROWTH_SOURCE_INVALID".into()); + } + let mut connection = open_database(database)?; + verify_event_chain(&connection)?; + let now = now_unix_ms()?; + let weight = if source == "PERSONA_COLLABORATION" { + 1.4 + } else { + 1.0 + }; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_TRANSACTION_FAILED: {error}"))?; + let (sequence, previous_hash): (i64, String) = transaction + .query_row( + "SELECT COALESCE(MAX(sequence), 0) + 1, COALESCE((SELECT event_hash FROM growth_events ORDER BY sequence DESC LIMIT 1), ?1) FROM growth_events", + params![ZERO_HASH], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let event_id = Uuid::new_v4().to_string(); + let event_hash = event_hash( + sequence, + &event_id, + &event_kind, + &module_id, + &source, + now, + &previous_hash, + ); + transaction + .execute( + "INSERT INTO growth_events(sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![sequence, event_id, event_kind, module_id, source, now, previous_hash, event_hash], + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_EVENT_WRITE_FAILED: {error}"))?; + transaction + .execute( + "INSERT INTO module_affinities(module_id, use_count, score, last_used_at_unix_ms) VALUES (?1, 1, ?2, ?3) + ON CONFLICT(module_id) DO UPDATE SET use_count = use_count + 1, score = score * 0.985 + excluded.score, last_used_at_unix_ms = excluded.last_used_at_unix_ms", + params![module_id, weight, now], + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_MODEL_WRITE_FAILED: {error}"))?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_COMMIT_FAILED: {error}"))?; + snapshot_with_connection(&connection) +} + +fn update_sharing_at( + database: &Path, + input: UpdateChannelGrowthSharingInput, +) -> Result { + if !matches!( + input.sharing_mode.as_str(), + "LOCAL_ONLY" | "ANONYMIZED_SHARED" + ) { + return Err("HOLOLAKE_CHANNEL_GROWTH_SHARING_MODE_INVALID".into()); + } + let connection = open_database(database)?; + connection + .execute( + "UPDATE growth_sharing SET sharing_mode = ?1 WHERE singleton = 1", + params![input.sharing_mode], + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SHARING_WRITE_FAILED: {error}"))?; + snapshot_with_connection(&connection) +} + +fn snapshot_at(database: &Path) -> Result { + let connection = open_database(database)?; + snapshot_with_connection(&connection) +} + +fn snapshot_with_connection(connection: &Connection) -> Result { + let last_event_hash = verify_event_chain(connection)?; + let mut affinities = verify_and_project_affinities(connection)?; + let controls = connection + .query_row( + "SELECT sharing_mode FROM growth_sharing WHERE singleton = 1", + [], + |row| { + Ok(ChannelGrowthControls { + sharing_mode: row.get(0)?, + official_read_access: false, + history_mutation_allowed: false, + structural_change_requires_human_confirmation: true, + }) + }, + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let observed_event_count = connection + .query_row("SELECT COUNT(*) FROM growth_events", [], |row| row.get(0)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let persona_collaboration_event_count = connection + .query_row( + "SELECT COUNT(*) FROM growth_events WHERE source = 'PERSONA_COLLABORATION'", + [], + |row| row.get(0), + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + affinities.sort_by(|left, right| { + right + .score + .partial_cmp(&left.score) + .unwrap_or(Ordering::Equal) + .then_with(|| right.last_used_at_unix_ms.cmp(&left.last_used_at_unix_ms)) + .then_with(|| left.module_id.cmp(&right.module_id)) + }); + affinities.truncate(12); + let recommended_module_order = affinities + .iter() + .map(|item| item.module_id.clone()) + .collect(); + Ok(ChannelGrowthSnapshot { + schema: GROWTH_SCHEMA, + state: "APPEND_ONLY_LOCAL_GROWTH_ACTIVE", + controls, + observed_event_count, + persona_collaboration_event_count, + module_affinities: affinities, + recommended_module_order, + integrity_state: "VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION", + last_event_hash, + model_api_state: "NOT_CONFIGURED", + model_proposal_policy: "LOCAL_VALIDATE_THEN_HUMAN_CONFIRM_BEFORE_STRUCTURAL_CHANGE", + data_boundary: "METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT", + }) +} + +fn verify_and_project_affinities( + connection: &Connection, +) -> Result, String> { + let mut projected = BTreeMap::::new(); + let mut event_statement = connection + .prepare( + "SELECT module_id, source, occurred_at_unix_ms FROM growth_events ORDER BY sequence", + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let events = event_statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + for (module_id, source, occurred_at) in events { + let weight = if source == "PERSONA_COLLABORATION" { + 1.4 + } else { + 1.0 + }; + projected + .entry(module_id.clone()) + .and_modify(|item| { + item.use_count += 1; + item.score = item.score * 0.985 + weight; + item.last_used_at_unix_ms = occurred_at; + }) + .or_insert(ChannelModuleAffinity { + module_id, + use_count: 1, + score: weight, + last_used_at_unix_ms: occurred_at, + }); + } + + let mut stored_statement = connection + .prepare( + "SELECT module_id, use_count, score, last_used_at_unix_ms FROM module_affinities ORDER BY module_id", + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let stored = stored_statement + .query_map([], |row| { + Ok(ChannelModuleAffinity { + module_id: row.get(0)?, + use_count: row.get(1)?, + score: row.get(2)?, + last_used_at_unix_ms: row.get(3)?, + }) + }) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + if stored.len() != projected.len() + || stored.iter().any(|item| { + projected.get(&item.module_id).map_or(true, |expected| { + item.use_count != expected.use_count + || (item.score - expected.score).abs() > 1e-12 + || item.last_used_at_unix_ms != expected.last_used_at_unix_ms + }) + }) + { + return Err("HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED".into()); + } + Ok(projected.into_values().collect()) +} + +fn event_hash( + sequence: i64, + event_id: &str, + event_kind: &str, + module_id: &str, + source: &str, + occurred_at_unix_ms: i64, + previous_hash: &str, +) -> String { + let material = format!("{GROWTH_SCHEMA}|{sequence}|{event_id}|{event_kind}|{module_id}|{source}|{occurred_at_unix_ms}|{previous_hash}"); + hex(digest(&SHA256, material.as_bytes()).as_ref()) +} + +fn verify_event_chain(connection: &Connection) -> Result { + let mut statement = connection + .prepare("SELECT sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash FROM growth_events ORDER BY sequence") + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let mut rows = statement + .query([]) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let mut expected_sequence = 1_i64; + let mut previous = ZERO_HASH.to_string(); + while let Some(row) = rows + .next() + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))? + { + let sequence: i64 = row + .get(0) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let event_id: String = row + .get(1) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let event_kind: String = row + .get(2) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let module_id: String = row + .get(3) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let source: String = row + .get(4) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let occurred_at: i64 = row + .get(5) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let stored_previous: String = row + .get(6) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + let stored_hash: String = row + .get(7) + .map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?; + if sequence != expected_sequence + || stored_previous != previous + || stored_hash + != event_hash( + sequence, + &event_id, + &event_kind, + &module_id, + &source, + occurred_at, + &stored_previous, + ) + { + return Err("HOLOLAKE_CHANNEL_GROWTH_HISTORY_INTEGRITY_FAILED".into()); + } + expected_sequence += 1; + previous = stored_hash; + } + Ok(previous) +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn now_unix_ms() -> Result { + Ok(SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))? + .as_millis() as i64) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn local_growth_model_records_only_bounded_metadata_and_ranks_modules() { + let dir = tempdir().unwrap(); + let database = dir.path().join("growth.sqlite3"); + for _ in 0..3 { + record_event_at( + &database, + RecordChannelGrowthEventInput { + event_kind: "MODULE_OPEN".into(), + module_id: "education.table".into(), + source: "HUMAN_ACTION".into(), + }, + ) + .unwrap(); + } + record_event_at( + &database, + RecordChannelGrowthEventInput { + event_kind: "PERSONA_COLLABORATION".into(), + module_id: "knowledge".into(), + source: "PERSONA_COLLABORATION".into(), + }, + ) + .unwrap(); + let snapshot = snapshot_at(&database).unwrap(); + assert_eq!(snapshot.observed_event_count, 4); + assert_eq!(snapshot.persona_collaboration_event_count, 1); + assert_eq!( + snapshot + .recommended_module_order + .first() + .map(String::as_str), + Some("education.table") + ); + assert_eq!( + snapshot.data_boundary, + "METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT" + ); + } + + #[test] + fn growth_history_rejects_update_and_delete() { + let dir = tempdir().unwrap(); + let database = dir.path().join("growth.sqlite3"); + record_event_at( + &database, + RecordChannelGrowthEventInput { + event_kind: "MODULE_OPEN".into(), + module_id: "knowledge".into(), + source: "HUMAN_ACTION".into(), + }, + ) + .unwrap(); + let connection = open_database(&database).unwrap(); + assert!(connection.execute("DELETE FROM growth_events", []).is_err()); + assert!(connection + .execute("UPDATE growth_events SET module_id = 'rewritten'", []) + .is_err()); + assert_eq!( + snapshot_at(&database).unwrap().integrity_state, + "VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION" + ); + } + + #[test] + fn derived_affinity_projection_fails_closed_when_modified_outside_the_event_path() { + let dir = tempdir().unwrap(); + let database = dir.path().join("growth.sqlite3"); + record_event_at( + &database, + RecordChannelGrowthEventInput { + event_kind: "MODULE_OPEN".into(), + module_id: "knowledge".into(), + source: "HUMAN_ACTION".into(), + }, + ) + .unwrap(); + let connection = open_database(&database).unwrap(); + connection + .execute("UPDATE module_affinities SET score = 999", []) + .unwrap(); + drop(connection); + assert_eq!( + snapshot_at(&database).unwrap_err(), + "HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED" + ); + } + + #[test] + fn sharing_choice_does_not_grant_official_read_or_mutate_history() { + let dir = tempdir().unwrap(); + let database = dir.path().join("growth.sqlite3"); + let snapshot = update_sharing_at( + &database, + UpdateChannelGrowthSharingInput { + sharing_mode: "ANONYMIZED_SHARED".into(), + }, + ) + .unwrap(); + assert_eq!(snapshot.controls.sharing_mode, "ANONYMIZED_SHARED"); + assert!(!snapshot.controls.official_read_access); + assert!(!snapshot.controls.history_mutation_allowed); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs b/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs new file mode 100644 index 000000000..00c6a4dff --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs @@ -0,0 +1,694 @@ +//! Signed channel document and spreadsheet workbench adapter. +//! +//! The signed package only selects this host-owned adapter. User content stays in +//! the authenticated account root and every accepted save is committed together +//! with a hash-chained receipt. Unmounting the package never removes that data. + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension, Transaction}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001"; +const ADAPTER: &str = "channel-workbench-v1"; +const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const MAX_BODY_BYTES: usize = 2 * 1024 * 1024; +const MAX_COLUMNS: usize = 64; +const MAX_ROWS: usize = 5_000; +const MAX_CELL_BYTES: usize = 64 * 1024; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkbenchColumn { + pub column_id: String, + pub title: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkbenchRow { + pub row_id: String, + pub cells: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelDocument { + document_id: String, + title: String, + body: String, + revision: u64, + updated_at_unix_ms: u64, + content_sha256: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelSpreadsheet { + table_id: String, + title: String, + columns: Vec, + rows: Vec, + revision: u64, + updated_at_unix_ms: u64, + content_sha256: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelWorkbenchSnapshot { + schema: &'static str, + state: &'static str, + module_number: &'static str, + document: ChannelDocument, + spreadsheet: ChannelSpreadsheet, + receipt_count: u64, + latest_receipt_hash: String, + user_data_survives_unmount: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveChannelDocumentInput { + document_id: String, + title: String, + body: String, + expected_revision: u64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveChannelSpreadsheetInput { + table_id: String, + title: String, + columns: Vec, + rows: Vec, + expected_revision: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkbenchSaveResult { + schema: &'static str, + state: &'static str, + entity_kind: &'static str, + entity_id: String, + revision: u64, + content_sha256: String, + receipt_sha256: String, + observed_at_unix_ms: u64, +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn storage_root(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "channel-workbench-v1") +} + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_ROOT_FAILED: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(root, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_PERMISSION_FAILED: {error}"))?; + } + let connection = Connection::open(root.join("channel-workbench.sqlite3")) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(std::time::Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS documents( + document_id TEXT PRIMARY KEY, + title TEXT NOT NULL, + body TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + content_sha256 TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS spreadsheets( + table_id TEXT PRIMARY KEY, + title TEXT NOT NULL, + columns_json TEXT NOT NULL, + rows_json TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + content_sha256 TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS workbench_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + receipt_id TEXT NOT NULL UNIQUE, + event TEXT NOT NULL, + entity_kind TEXT NOT NULL, + entity_id TEXT NOT NULL, + revision INTEGER NOT NULL, + content_sha256 TEXT NOT NULL, + previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE, + observed_at_unix_ms INTEGER NOT NULL + ); + CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_update + BEFORE UPDATE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END; + CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_delete + BEFORE DELETE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END;", + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_INIT_FAILED: {error}"))?; + Ok(connection) +} + +fn valid_object_id(value: &str, prefix: &str) -> bool { + value.starts_with(prefix) + && value.len() <= 96 + && value + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-') +} + +fn valid_title(value: &str) -> bool { + !value.trim().is_empty() && value.len() <= 120 && !value.chars().any(char::is_control) +} + +fn receipt_hash( + event: &str, + entity_kind: &str, + entity_id: &str, + revision: u64, + content_sha256: &str, + previous: &str, + observed: u64, +) -> String { + sha256_hex( + format!( + "{event}\n{entity_kind}\n{entity_id}\n{revision}\n{content_sha256}\n{previous}\n{observed}" + ) + .as_bytes(), + ) +} + +fn verify_receipts(connection: &Connection) -> Result<(u64, String), String> { + let mut statement = connection + .prepare("SELECT sequence,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM workbench_receipts ORDER BY sequence") + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, u64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, u64>(4)?, + row.get::<_, String>(5)?, + row.get::<_, String>(6)?, + row.get::<_, String>(7)?, + row.get::<_, u64>(8)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let mut count = 0; + let mut previous = ZERO_HASH.to_string(); + for row in rows { + let (sequence, event, kind, id, revision, content, stored_previous, stored_hash, observed) = + row.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + if sequence != count + 1 + || stored_previous != previous + || stored_hash + != receipt_hash(&event, &kind, &id, revision, &content, &previous, observed) + { + return Err("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID".into()); + } + count = sequence; + previous = stored_hash; + } + Ok((count, previous)) +} + +fn append_receipt( + tx: &Transaction<'_>, + event: &str, + kind: &str, + id: &str, + revision: u64, + content: &str, + observed: u64, +) -> Result { + let previous: String = tx + .query_row( + "SELECT COALESCE((SELECT receipt_hash FROM workbench_receipts ORDER BY sequence DESC LIMIT 1),?1)", + params![ZERO_HASH], + |row| row.get(0), + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_HEAD_FAILED: {error}"))?; + let hash = receipt_hash(event, kind, id, revision, content, &previous, observed); + tx.execute( + "INSERT INTO workbench_receipts(receipt_id,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)", + params![format!("HLP-WB-RCP-{}", Uuid::new_v4().simple()), event, kind, id, revision, content, previous, hash, observed], + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_WRITE_FAILED: {error}"))?; + Ok(hash) +} + +fn empty_document() -> ChannelDocument { + ChannelDocument { + document_id: "HLP-WB-DOC-0001".into(), + title: "频道笔记".into(), + body: "".into(), + revision: 0, + updated_at_unix_ms: 0, + content_sha256: sha256_hex(b""), + } +} + +fn empty_spreadsheet() -> ChannelSpreadsheet { + ChannelSpreadsheet { + table_id: "HLP-WB-TBL-0001".into(), + title: "频道工作表".into(), + columns: vec![ + WorkbenchColumn { + column_id: "HLP-WB-COL-0001".into(), + title: "事项".into(), + }, + WorkbenchColumn { + column_id: "HLP-WB-COL-0002".into(), + title: "状态".into(), + }, + ], + rows: Vec::new(), + revision: 0, + updated_at_unix_ms: 0, + content_sha256: sha256_hex(b""), + } +} + +fn snapshot_at(root: &Path) -> Result { + let connection = open_db(root)?; + let (receipt_count, latest_receipt_hash) = verify_receipts(&connection)?; + let document = connection + .query_row( + "SELECT document_id,title,body,revision,updated_at_unix_ms,content_sha256 FROM documents ORDER BY updated_at_unix_ms DESC LIMIT 1", + [], + |row| Ok(ChannelDocument { document_id: row.get(0)?, title: row.get(1)?, body: row.get(2)?, revision: row.get(3)?, updated_at_unix_ms: row.get(4)?, content_sha256: row.get(5)? }), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))? + .unwrap_or_else(empty_document); + let spreadsheet = connection + .query_row( + "SELECT table_id,title,columns_json,rows_json,revision,updated_at_unix_ms,content_sha256 FROM spreadsheets ORDER BY updated_at_unix_ms DESC LIMIT 1", + [], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, u64>(4)?, row.get::<_, u64>(5)?, row.get::<_, String>(6)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))? + .map(|(table_id, title, columns, rows, revision, updated_at_unix_ms, content_sha256)| { + Ok::(ChannelSpreadsheet { + table_id, + title, + columns: serde_json::from_str(&columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_COLUMNS_INVALID".to_string())?, + rows: serde_json::from_str(&rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_ROWS_INVALID".to_string())?, + revision, + updated_at_unix_ms, + content_sha256, + }) + }) + .transpose()? + .unwrap_or_else(empty_spreadsheet); + if document.revision > 0 { + let actual = sha256_hex( + serde_json::to_string(&(document.title.trim(), &document.body)) + .map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")? + .as_bytes(), + ); + if actual != document.content_sha256 { + return Err("HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID".into()); + } + } + if spreadsheet.revision > 0 { + let actual = sha256_hex( + serde_json::to_string(&( + spreadsheet.title.trim(), + &spreadsheet.columns, + &spreadsheet.rows, + )) + .map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")? + .as_bytes(), + ); + if actual != spreadsheet.content_sha256 { + return Err("HOLOLAKE_CHANNEL_SPREADSHEET_CONTENT_INTEGRITY_INVALID".into()); + } + } + let mut latest = connection + .prepare("SELECT entity_kind,entity_id,revision,content_sha256 FROM workbench_receipts WHERE sequence IN (SELECT MAX(sequence) FROM workbench_receipts GROUP BY entity_kind,entity_id)") + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let heads = latest + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, u64>(2)?, + row.get::<_, String>(3)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + for head in heads { + let (kind, id, revision, content) = head + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let matches = match kind.as_str() { + "DOCUMENT" => { + document.document_id == id + && document.revision == revision + && document.content_sha256 == content + } + "SPREADSHEET" => { + spreadsheet.table_id == id + && spreadsheet.revision == revision + && spreadsheet.content_sha256 == content + } + _ => false, + }; + if !matches { + return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into()); + } + } + Ok(ChannelWorkbenchSnapshot { + schema: "hololake.channel-workbench-snapshot/v1", + state: "ACTIVE_LOCAL_ACCOUNT_DATA", + module_number: MODULE_NUMBER, + document, + spreadsheet, + receipt_count, + latest_receipt_hash, + user_data_survives_unmount: true, + }) +} + +fn save_document_at( + root: &Path, + input: SaveChannelDocumentInput, + observed: u64, +) -> Result { + if !valid_object_id(&input.document_id, "HLP-WB-DOC-") + || !valid_title(&input.title) + || input.body.len() > MAX_BODY_BYTES + { + return Err("HOLOLAKE_CHANNEL_DOCUMENT_INPUT_INVALID".into()); + } + let mut connection = open_db(root)?; + verify_receipts(&connection)?; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?; + let current: Option = transaction + .query_row( + "SELECT revision FROM documents WHERE document_id=?1", + params![input.document_id], + |row| row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?; + if current.unwrap_or(0) != input.expected_revision { + return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); + } + let revision = input.expected_revision + 1; + let content = sha256_hex( + serde_json::to_string(&(input.title.trim(), &input.body)) + .map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")? + .as_bytes(), + ); + transaction.execute( + "INSERT INTO documents VALUES (?1,?2,?3,?4,?5,?6) ON CONFLICT(document_id) DO UPDATE SET title=excluded.title,body=excluded.body,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256", + params![input.document_id, input.title.trim(), input.body, revision, observed, content], + ).map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + if current.is_some() { + "UPDATE" + } else { + "CREATE" + }, + "DOCUMENT", + &input.document_id, + revision, + &content, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?; + Ok(WorkbenchSaveResult { + schema: "hololake.channel-workbench-save/v1", + state: "COMMITTED", + entity_kind: "DOCUMENT", + entity_id: input.document_id, + revision, + content_sha256: content, + receipt_sha256: receipt, + observed_at_unix_ms: observed, + }) +} + +fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), String> { + if !valid_object_id(&input.table_id, "HLP-WB-TBL-") + || !valid_title(&input.title) + || input.columns.is_empty() + || input.columns.len() > MAX_COLUMNS + || input.rows.len() > MAX_ROWS + || input.columns.iter().any(|column| { + !valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title) + }) + || input.rows.iter().any(|row| { + !valid_object_id(&row.row_id, "HLP-WB-ROW-") + || row.cells.len() > input.columns.len() + || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES) + }) + { + return Err("HOLOLAKE_CHANNEL_SPREADSHEET_INPUT_INVALID".into()); + } + Ok(()) +} + +fn save_spreadsheet_at( + root: &Path, + input: SaveChannelSpreadsheetInput, + observed: u64, +) -> Result { + validate_spreadsheet(&input)?; + let mut connection = open_db(root)?; + verify_receipts(&connection)?; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?; + let current: Option = transaction + .query_row( + "SELECT revision FROM spreadsheets WHERE table_id=?1", + params![input.table_id], + |row| row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?; + if current.unwrap_or(0) != input.expected_revision { + return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); + } + let revision = input.expected_revision + 1; + let columns = serde_json::to_string(&input.columns) + .map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?; + let rows = serde_json::to_string(&input.rows) + .map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?; + let content = sha256_hex( + serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows)) + .map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")? + .as_bytes(), + ); + transaction.execute( + "INSERT INTO spreadsheets VALUES (?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(table_id) DO UPDATE SET title=excluded.title,columns_json=excluded.columns_json,rows_json=excluded.rows_json,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256", + params![input.table_id, input.title.trim(), columns, rows, revision, observed, content], + ).map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + if current.is_some() { + "UPDATE" + } else { + "CREATE" + }, + "SPREADSHEET", + &input.table_id, + revision, + &content, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?; + Ok(WorkbenchSaveResult { + schema: "hololake.channel-workbench-save/v1", + state: "COMMITTED", + entity_kind: "SPREADSHEET", + entity_id: input.table_id, + revision, + content_sha256: content, + receipt_sha256: receipt, + observed_at_unix_ms: observed, + }) +} + +pub async fn get_channel_workbench_snapshot( + app: AppHandle, +) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))? +} + +pub async fn save_channel_document( + app: AppHandle, + input: SaveChannelDocumentInput, +) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))? +} + +pub async fn save_channel_spreadsheet( + app: AppHandle, + input: SaveChannelSpreadsheetInput, +) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())) + .await + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))? +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn document_and_spreadsheet_survive_reopen_with_receipt_chain() { + let root = tempfile::tempdir().unwrap(); + let document = save_document_at( + root.path(), + SaveChannelDocumentInput { + document_id: "HLP-WB-DOC-0001".into(), + title: "真实笔记".into(), + body: "第一段真实内容".into(), + expected_revision: 0, + }, + 101, + ) + .unwrap(); + let sheet = save_spreadsheet_at( + root.path(), + SaveChannelSpreadsheetInput { + table_id: "HLP-WB-TBL-0001".into(), + title: "真实工作表".into(), + columns: vec![WorkbenchColumn { + column_id: "HLP-WB-COL-0001".into(), + title: "数量".into(), + }], + rows: vec![WorkbenchRow { + row_id: "HLP-WB-ROW-0001".into(), + cells: vec!["=1+2".into()], + }], + expected_revision: 0, + }, + 102, + ) + .unwrap(); + let snapshot = snapshot_at(root.path()).unwrap(); + assert_eq!(document.revision, 1); + assert_eq!(sheet.revision, 1); + assert_eq!(snapshot.document.body, "第一段真实内容"); + assert_eq!(snapshot.spreadsheet.rows[0].cells[0], "=1+2"); + assert_eq!(snapshot.receipt_count, 2); + assert_eq!(snapshot.latest_receipt_hash, sheet.receipt_sha256); + } + + #[test] + fn stale_revision_and_tampered_receipt_fail_closed() { + let root = tempfile::tempdir().unwrap(); + let input = SaveChannelDocumentInput { + document_id: "HLP-WB-DOC-0001".into(), + title: "笔记".into(), + body: "正文".into(), + expected_revision: 0, + }; + save_document_at(root.path(), input, 101).unwrap(); + assert_eq!( + save_document_at( + root.path(), + SaveChannelDocumentInput { + document_id: "HLP-WB-DOC-0001".into(), + title: "笔记".into(), + body: "过期写入".into(), + expected_revision: 0 + }, + 102 + ) + .unwrap_err(), + "HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT" + ); + let connection = open_db(root.path()).unwrap(); + connection.execute_batch("DROP TRIGGER workbench_receipts_no_update; UPDATE workbench_receipts SET receipt_hash='bad'").unwrap(); + assert_eq!( + snapshot_at(root.path()).unwrap_err(), + "HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID" + ); + } + + #[test] + fn content_mutation_without_a_receipt_fails_closed() { + let root = tempfile::tempdir().unwrap(); + save_document_at( + root.path(), + SaveChannelDocumentInput { + document_id: "HLP-WB-DOC-0001".into(), + title: "笔记".into(), + body: "可信正文".into(), + expected_revision: 0, + }, + 101, + ) + .unwrap(); + let connection = open_db(root.path()).unwrap(); + connection + .execute( + "UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'", + [], + ) + .unwrap(); + assert_eq!( + snapshot_at(root.path()).unwrap_err(), + "HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/code_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/code_channel.rs index a6dd47cd7..f0da9be09 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/code_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/code_channel.rs @@ -120,7 +120,6 @@ struct ValidatedCloneUrl { repository: String, } -#[tauri::command] pub async fn get_code_channel_snapshot(app: AppHandle) -> Result { let root = code_channel_root(&app)?; tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)) @@ -128,7 +127,6 @@ pub async fn get_code_channel_snapshot(app: AppHandle) -> Result Result, String> { @@ -165,7 +162,6 @@ pub async fn select_local_code_channel( .map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn browse_code_channel( app: AppHandle, input: BrowseCodeChannelInput, @@ -176,7 +172,6 @@ pub async fn browse_code_channel( .map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn read_code_channel_file( app: AppHandle, input: ReadCodeChannelFileInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs b/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs index bee6e8bea..bae44ad0e 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/code_repo_login.rs @@ -168,7 +168,6 @@ pub(crate) fn validate_number_account_binding(number: &str, username: &str) -> R } /// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。 -#[tauri::command] pub fn check_code_repo_login( app: AppHandle, state: State<'_, ZeroPointState>, @@ -225,7 +224,6 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> { /// 新账号第一次登录时在 HoloLake 内完成强制换密。 /// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。 -#[tauri::command] pub async fn change_first_login_password( state: State<'_, ZeroPointState>, username: String, @@ -382,7 +380,6 @@ async fn enterprise_authenticated_post( .map_err(|error| format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {error}")) } -#[tauri::command] pub async fn get_enterprise_entry( app: AppHandle, state: State<'_, ZeroPointState>, @@ -390,7 +387,6 @@ pub async fn get_enterprise_entry( enterprise_authenticated_post(&app, &state, "me/entry", serde_json::json!({})).await } -#[tauri::command] pub async fn confirm_enterprise_persona_relationship( app: AppHandle, state: State<'_, ZeroPointState>, @@ -406,7 +402,6 @@ pub async fn confirm_enterprise_persona_relationship( .await } -#[tauri::command] pub async fn submit_enterprise_responsibility_receipt( app: AppHandle, state: State<'_, ZeroPointState>, @@ -431,7 +426,6 @@ pub async fn submit_enterprise_responsibility_receipt( /// 登录验证:基本认证打 Forgejo 用户接口,密码错=401 即拒; /// 通过后凭证进钥匙串,会话(不含密码)落盘。 -#[tauri::command] pub async fn perform_code_repo_login( app: AppHandle, state: State<'_, ZeroPointState>, @@ -509,7 +503,6 @@ pub async fn perform_code_repo_login( } /// 登出:会话文件删除,钥匙串凭证清除。 -#[tauri::command] pub fn sign_out_code_repo_login(app: AppHandle) -> Result<(), String> { let path = session_path(&app)?; if let Ok(raw) = fs::read_to_string(&path) { diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs index 3fecb2616..ac7fb28ce 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs @@ -1,9 +1,9 @@ use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput}; use crate::direct_local_session::{ - append_event_at, authenticate_context_at, authenticate_privileged_at, direct_session_root, - heartbeat_at, issue_ticket_at, open_at, resume_at, AppendSessionEventInput, - AuthenticateSessionInput, AuthenticatedSessionContext, DirectSessionReceipt, - IssueDiscoveryTicketInput, OpenSessionInput, ResumeSessionInput, + append_event_at, authenticate_context_at, direct_session_root, heartbeat_at, issue_ticket_at, + open_at, resume_at, AppendSessionEventInput, AuthenticateSessionInput, + AuthenticatedSessionContext, DirectSessionReceipt, IssueDiscoveryTicketInput, OpenSessionInput, + ResumeSessionInput, }; use crate::dynamic_capability_routing::{ install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at, @@ -15,6 +15,12 @@ use crate::local_development_bridge::{ inspect_at as inspect_development_lane_at, release_at as release_development_lane_at, AcquireWriteLaneInput, ReleaseWriteLaneInput, }; +use crate::persona_carrier_license::{ + present_license_at as present_persona_license_at, + require_if_persona_mode_at as require_persona_mode_operation_at, + status_at as persona_license_status_at, PersonaCarrierLicenseStatus, + PersonaCarrierRuntimeLicense, +}; use crate::persona_time_authority::{ beijing_time_coordinate, issue_authenticated_at, IssuePersonaTimeTicketInput, }; @@ -42,6 +48,7 @@ use interprocess::TryClone; use ring::digest::{digest, SHA256}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashSet; use std::fs::{self, OpenOptions}; use std::io::{self, BufRead, BufReader, Read, Write}; #[cfg(unix)] @@ -73,8 +80,51 @@ impl OpenOptionsModeExt for OpenOptions { const BROKER_SCHEMA: &str = "hololake.direct-local-broker/v1"; const MAX_REQUEST_BYTES: u64 = 128 * 1024; const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1"; -const TERMINAL_LINK_PROTOCOL: &str = "HOLOLAKE_TERMINAL_LINK/2"; +const TERMINAL_LINK_PROTOCOL: &str = "HOLOLAKE_TERMINAL_LINK/3"; const ENVIRONMENT_FRAME_TTL_MS: u128 = 45_000; +const NUMBERED_BROKER_REGISTRY: &str = + include_str!("../../contracts/direct-local-broker-numbered-registry.json"); + +#[derive(Clone, Debug, Deserialize)] +struct BrokerNumberRegistry { + schema: String, + record_id: String, + runtime: BrokerNumberRuntime, + operations: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +struct BrokerNumberRuntime { + protocol_version: String, + caller_number: String, + legacy_string_operation_allowed: bool, + unknown_or_mismatched_coordinate: String, + request_nonce_required: bool, + transport_is_authority: bool, +} + +#[derive(Clone, Debug, Deserialize)] +struct BrokerNumberRoute { + operation_number: String, + alias: String, + channel_number: String, + module_number: String, + target_number: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct BrokerNumberedEnvelope { + protocol_version: String, + caller_number: String, + channel_number: String, + module_number: String, + operation_number: String, + target_number: String, + request_nonce: String, + #[serde(default)] + input: Option, +} pub struct DirectLocalBrokerHandle { shutdown: Arc, @@ -151,6 +201,7 @@ struct BrokerStorageRoots { pncc_projection: PathBuf, language_inbox: PathBuf, persona_time: PathBuf, + persona_license: PathBuf, development: PathBuf, } @@ -179,6 +230,8 @@ enum BrokerRequest { OpenSession(OpenSessionInput), ResumeSession(ResumeSessionInput), HeartbeatSession(AuthenticateSessionInput), + PresentPersonaCarrierLicense(Box), + GetPersonaCarrierLicenseStatus(AuthenticatedPersonaCarrierLicenseStatusInput), GetWorkEnvironment(AuthenticatedWorkEnvironmentInput), AppendEvent(AppendSessionEventInput), ResolveCapabilityRoute(AuthenticatedRouteInput), @@ -297,6 +350,19 @@ struct AuthenticatedWorkEnvironmentInput { session: AuthenticateSessionInput, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct AuthenticatedPersonaCarrierLicenseInput { + session: AuthenticateSessionInput, + license: PersonaCarrierRuntimeLicense, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct AuthenticatedPersonaCarrierLicenseStatusInput { + session: AuthenticateSessionInput, +} + #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct WorkEnvironmentFrame { @@ -308,6 +374,7 @@ struct WorkEnvironmentFrame { transport: &'static str, mcp_role: &'static str, protocol_runtime: &'static str, + persona_control: PersonaCarrierLicenseStatus, account_key: String, session_id: String, lane_id: String, @@ -366,6 +433,116 @@ struct ConnectorDescriptor { process_id: u32, } +fn load_number_registry() -> Result { + let registry: BrokerNumberRegistry = serde_json::from_str(NUMBERED_BROKER_REGISTRY) + .map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_REGISTRY_INVALID: {error}"))?; + if registry.schema != "hololake.direct-local-broker-numbered-registry/v1" + || registry.record_id != "HLP-NBROKER-ROOT-001" + || registry.runtime.protocol_version != "HLP-NBROKER-v1" + || registry.runtime.caller_number != "HLP-NBROKER-CALLER-LOCAL-CONNECTOR-0001" + || registry.runtime.legacy_string_operation_allowed + || registry.runtime.unknown_or_mismatched_coordinate != "FAIL_CLOSED" + || !registry.runtime.request_nonce_required + || registry.runtime.transport_is_authority + || registry.operations.len() != 22 + { + return Err("HOLOLAKE_NUMBERED_BROKER_REGISTRY_BOUNDARY_INVALID".into()); + } + let mut numbers = HashSet::new(); + let mut aliases = HashSet::new(); + let mut coordinates = HashSet::new(); + for route in ®istry.operations { + let coordinate = format!( + "{}/{}/{}/{}", + route.channel_number, route.module_number, route.operation_number, route.target_number + ); + if !numbers.insert(route.operation_number.as_str()) + || !aliases.insert(route.alias.as_str()) + || !coordinates.insert(coordinate) + || !route.operation_number.starts_with("HLP-NBROKER-OP-") + || !route.channel_number.starts_with("HLP-NBROKER-CH-") + || !route.module_number.starts_with("HLP-NBROKER-MOD-") + || !route.target_number.starts_with("HLP-NBROKER-TGT-") + { + return Err("HOLOLAKE_NUMBERED_BROKER_ROUTE_INVALID".into()); + } + } + Ok(registry) +} + +fn resolve_numbered_broker_route<'a>( + registry: &'a BrokerNumberRegistry, + envelope: &BrokerNumberedEnvelope, +) -> Result<&'a BrokerNumberRoute, String> { + if envelope.protocol_version != registry.runtime.protocol_version + || envelope.caller_number != registry.runtime.caller_number + || envelope.request_nonce.len() < 16 + || envelope.request_nonce.len() > 128 + || envelope + .request_nonce + .chars() + .any(|character| character.is_whitespace() || character.is_control()) + { + return Err("HOLOLAKE_NUMBERED_BROKER_CALLER_PROTOCOL_OR_NONCE_INVALID".into()); + } + let route = registry + .operations + .iter() + .find(|route| route.operation_number == envelope.operation_number) + .ok_or("HOLOLAKE_NUMBERED_BROKER_ROUTE_UNKNOWN")?; + if route.channel_number != envelope.channel_number + || route.module_number != envelope.module_number + || route.target_number != envelope.target_number + { + return Err("HOLOLAKE_NUMBERED_BROKER_ROUTE_COORDINATE_MISMATCH".into()); + } + Ok(route) +} + +fn decode_numbered_broker_request(bytes: &[u8]) -> Result<(BrokerRequest, String, String), String> { + let envelope: BrokerNumberedEnvelope = serde_json::from_slice(bytes) + .map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_ENVELOPE_INVALID: {error}"))?; + let registry = load_number_registry()?; + let route = resolve_numbered_broker_route(®istry, &envelope)?; + let mut request = serde_json::Map::new(); + request.insert("operation".into(), Value::String(route.alias.clone())); + if let Some(input) = envelope.input { + request.insert("input".into(), input); + } + let decoded = serde_json::from_value(Value::Object(request)) + .map_err(|error| format!("HOLOLAKE_NUMBERED_BROKER_INPUT_INVALID: {error}"))?; + Ok(( + decoded, + route.operation_number.clone(), + envelope.request_nonce, + )) +} + +fn numbered_broker_request_value(alias: &str, input: Option) -> Result { + let registry = load_number_registry()?; + let route = registry + .operations + .iter() + .find(|route| route.alias == alias) + .ok_or("HOLOLAKE_NUMBERED_BROKER_ALIAS_UNKNOWN")?; + let mut value = serde_json::json!({ + "protocolVersion": registry.runtime.protocol_version, + "callerNumber": registry.runtime.caller_number, + "channelNumber": route.channel_number, + "moduleNumber": route.module_number, + "operationNumber": route.operation_number, + "targetNumber": route.target_number, + "requestNonce": format!("HLP-NBROKER-NONCE-{}", Uuid::new_v4().simple()) + }); + if let Some(input) = input { + value + .as_object_mut() + .ok_or("HOLOLAKE_NUMBERED_BROKER_ENVELOPE_INVALID")? + .insert("input".into(), input); + } + Ok(value) +} + pub fn start(app: &AppHandle) -> Result> { let session_root = direct_session_root(app).map_err(std::io::Error::other)?; let routing_root = dynamic_routing_root(app).map_err(std::io::Error::other)?; @@ -375,7 +552,6 @@ pub fn start(app: &AppHandle) -> Result NearbyAiDiscoverySnapshot { nearby_discovery_snapshot() } @@ -466,7 +642,9 @@ fn read_connector_descriptor(path: &Path) -> Result } fn probe_connection(stream: &mut LocalSocketStream) -> Result<(), String> { - let response = send_connector_request(stream, br#"{"operation":"PING"}"#)?; + let request = serde_json::to_vec(&numbered_broker_request_value("PING", None)?) + .map_err(|_| "HOLOLAKE_CONNECTOR_PROBE_INVALID".to_string())?; + let response = send_connector_request(stream, &request)?; let value: Value = serde_json::from_slice(&response) .map_err(|_| "HOLOLAKE_CONNECTOR_PROBE_INVALID".to_string())?; if value.get("ok").and_then(Value::as_bool) == Some(true) @@ -559,7 +737,7 @@ fn connect_endpoint(path: &Path) -> std::io::Result { #[cfg(unix)] { let name = path.to_fs_name::()?; - return LocalSocketStream::connect(name); + LocalSocketStream::connect(name) } #[cfg(windows)] { @@ -626,6 +804,7 @@ fn start_at( descriptor_path: PathBuf, socket_path: PathBuf, ) -> Result { + load_number_registry()?; let pncc_mount_root = session_root .parent() .ok_or("HOLOLAKE_BROKER_PNCC_STORAGE_BOUNDARY_INVALID")? @@ -655,6 +834,12 @@ fn start_at( .parent() .ok_or("HOLOLAKE_BROKER_PERSONA_TIME_STORAGE_BOUNDARY_INVALID")? .join("persona-time-authority-v1"); + let persona_license_root = session_root + .parent() + .ok_or("HOLOLAKE_BROKER_PERSONA_LICENSE_STORAGE_BOUNDARY_INVALID")? + .join("persona-carrier-runtime-license-v1"); + fs::create_dir_all(&persona_license_root) + .map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?; let development_root = development_root_from_session_root(&session_root)?; fs::create_dir_all(&development_root) .map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?; @@ -716,6 +901,7 @@ fn start_at( pncc_projection: pncc_projection_root, language_inbox: language_inbox_root, persona_time: persona_time_root, + persona_license: persona_license_root, development: development_root, }, &worker_shutdown, @@ -817,18 +1003,14 @@ fn serve_connection( } fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool { - serde_json::from_slice::(bytes) + decode_numbered_broker_request(bytes) .ok() - .and_then(|value| { - value - .get("operation") - .and_then(Value::as_str) - .map(str::to_owned) - }) - .is_some_and(|operation| { + .is_some_and(|(request, _, _)| { matches!( - operation.as_str(), - "OPEN_SESSION" | "RESUME_SESSION" | "OPEN_VISITOR_SESSION" + request, + BrokerRequest::OpenSession(_) + | BrokerRequest::ResumeSession(_) + | BrokerRequest::OpenVisitorSession(_) ) }) } @@ -841,8 +1023,9 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { let pncc_projection_root = &roots.pncc_projection; let language_inbox_root = &roots.language_inbox; let persona_time_root = &roots.persona_time; + let persona_license_root = &roots.persona_license; let development_root = &roots.development; - let request: BrokerRequest = match serde_json::from_slice(bytes) { + let (request, _, _) = match decode_numbered_broker_request(bytes) { Ok(request) => request, Err(error) => { return BrokerResponse::error(&format!("HOLOLAKE_BROKER_REQUEST_INVALID: {error}")) @@ -871,10 +1054,41 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), BrokerRequest::ResumeSession(input) => resume_at(session_root, input) .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), + BrokerRequest::PresentPersonaCarrierLicense(input) => { + let input = *input; + authenticate_context_at(session_root, &input.session) + .and_then(|context| { + present_persona_license_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + input.license, + now_unix_ms()?, + ) + }) + .and_then(|receipt| { + serde_json::to_value(receipt).map_err(|error| error.to_string()) + }) + } + BrokerRequest::GetPersonaCarrierLicenseStatus(input) => { + authenticate_context_at(session_root, &input.session) + .map(|context| { + persona_license_status_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + now_unix_ms().unwrap_or(u64::MAX), + ) + }) + .and_then(|status| serde_json::to_value(status).map_err(|error| error.to_string())) + } BrokerRequest::HeartbeatSession(input) => heartbeat_at(session_root, &input) .and_then(|session| { work_environment_frame_at( session_root, + persona_license_root, development_root, &AuthenticatedWorkEnvironmentInput { session: input }, ) @@ -887,13 +1101,28 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), BrokerRequest::GetWorkEnvironment(input) => { - work_environment_frame_at(session_root, development_root, &input) + work_environment_frame_at(session_root, persona_license_root, development_root, &input) .and_then(|frame| serde_json::to_value(frame).map_err(|error| error.to_string())) } - BrokerRequest::AppendEvent(input) => append_event_at(session_root, input) - .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), + BrokerRequest::AppendEvent(input) => authorize_persona_operation_at( + session_root, + persona_license_root, + &AuthenticateSessionInput { + account_id: input.account_id.clone(), + session_id: input.session_id.clone(), + resume_secret: input.resume_secret.clone(), + }, + "APPEND_EVENT", + ) + .and_then(|_| append_event_at(session_root, input)) + .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), BrokerRequest::ResolveCapabilityRoute(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "RESOLVE_CAPABILITY_ROUTE", + ) { return BrokerResponse::error(&error); } let now = std::time::SystemTime::now() @@ -906,7 +1135,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) } BrokerRequest::InstallDynamicNodeRegistry(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "INSTALL_DYNAMIC_NODE_REGISTRY", + ) { return BrokerResponse::error(&error); } install_trusted_registry_at(routing_root, input.registry).and_then(|receipt| { @@ -914,7 +1148,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) } BrokerRequest::RecordSignedNodeHealth(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "RECORD_SIGNED_NODE_HEALTH", + ) { return BrokerResponse::error(&error); } record_health_at(routing_root, input.health).and_then(|receipt| { @@ -922,7 +1161,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) } BrokerRequest::InspectMountedPnccRepository(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "INSPECT_MOUNTED_PNCC_REPOSITORY", + ) { return BrokerResponse::error(&error); } inspect_mounted_pncc_at(pncc_mount_root, input.mount).and_then(|receipt| { @@ -931,7 +1175,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) } BrokerRequest::ReadMountedPnccRemoteObject(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "READ_MOUNTED_PNCC_REMOTE_OBJECT", + ) { return BrokerResponse::error(&error); } read_mounted_pncc_remote_at(pncc_remote_root, input.read).and_then(|receipt| { @@ -940,7 +1189,12 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { }) } BrokerRequest::QueryPnccReceiptProjection(input) => { - if let Err(error) = authenticate_privileged_at(session_root, &input.session) { + if let Err(error) = authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "QUERY_PNCC_RECEIPT_PROJECTION", + ) { return BrokerResponse::error(&error); } query_pncc_projection_at(pncc_projection_root, input.query).and_then(|receipt| { @@ -955,15 +1209,27 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { .and_then(|coordinate| { serde_json::to_value(coordinate).map_err(|error| error.to_string()) }), - BrokerRequest::IssuePersonaTimeTicket(input) => { - issue_authenticated_at(persona_time_root, session_root, input) - .and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string())) - } + BrokerRequest::IssuePersonaTimeTicket(input) => authorize_persona_operation_at( + session_root, + persona_license_root, + &input.session, + "ISSUE_PERSONA_TIME_TICKET", + ) + .and_then(|_| issue_authenticated_at(persona_time_root, session_root, input)) + .and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string())), BrokerRequest::AcquireDevelopmentWriteLane(input) => { let session = input.session; let account_id = input.lane.account_id.clone(); authenticate_development_request(session_root, &session, &account_id) .and_then(|context| { + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "ACQUIRE_DEVELOPMENT_WRITE_LANE", + now_unix_ms()?, + )?; if context.lane_id != input.lane.lane_id || context.client_instance_id != input.lane.owner_instance_id { @@ -976,6 +1242,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { serde_json::to_value(receipt).map_err(|error| error.to_string())?; let environment = work_environment_frame_at( session_root, + persona_license_root, development_root, &AuthenticatedWorkEnvironmentInput { session }, )?; @@ -991,7 +1258,17 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { } BrokerRequest::InspectDevelopmentWriteLane(input) => { authenticate_development_request(session_root, &input.session, &input.account_id) - .and_then(|_| inspect_development_lane_at(development_root, &input.account_id)) + .and_then(|context| { + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "INSPECT_DEVELOPMENT_WRITE_LANE", + now_unix_ms()?, + )?; + inspect_development_lane_at(development_root, &input.account_id) + }) .and_then(|receipt| { serde_json::to_value(receipt).map_err(|error| error.to_string()) }) @@ -999,6 +1276,14 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { BrokerRequest::ReleaseDevelopmentWriteLane(input) => { authenticate_development_request(session_root, &input.session, &input.lane.account_id) .and_then(|context| { + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "RELEASE_DEVELOPMENT_WRITE_LANE", + now_unix_ms()?, + )?; if context.lane_id != input.lane.lane_id || context.client_instance_id != input.lane.owner_instance_id { @@ -1019,11 +1304,27 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse { fn work_environment_frame_at( session_root: &Path, + persona_license_root: &Path, development_root: &Path, input: &AuthenticatedWorkEnvironmentInput, ) -> Result { let context = authenticate_development_request(session_root, &input.session, &input.session.account_id)?; + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + "GET_WORK_ENVIRONMENT", + now_unix_ms()?, + )?; + let persona_control = persona_license_status_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + now_unix_ms()?, + ); let writer = inspect_development_lane_at(development_root, &input.session.account_id)?; let writer_matches = writer.state == "ACTIVE" && writer.lane_id.as_deref() == Some(context.lane_id.as_str()) @@ -1053,7 +1354,7 @@ fn work_environment_frame_at( ); let frame_sha256 = sha256_hex( format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", environment_id, state, context.session_id, @@ -1061,6 +1362,8 @@ fn work_environment_frame_at( context.client_instance_id, context.last_event_sequence, writer.state, + persona_control.control_mode, + persona_control.license_id.as_deref().unwrap_or("NONE"), valid_until_unix_ms ) .as_bytes(), @@ -1074,6 +1377,7 @@ fn work_environment_frame_at( transport: TERMINAL_LINK_PROTOCOL, mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY", protocol_runtime: "GLS_RUNTIME_MANIFEST_V2_AND_NATIVE_KERNEL", + persona_control, account_key: context.account_key, session_id: context.session_id, lane_id: context.lane_id, @@ -1108,6 +1412,24 @@ fn authenticate_development_request( Ok(context) } +fn authorize_persona_operation_at( + session_root: &Path, + persona_license_root: &Path, + session: &AuthenticateSessionInput, + operation: &str, +) -> Result { + let context = authenticate_context_at(session_root, session)?; + require_persona_mode_operation_at( + persona_license_root, + &context.account_key, + &context.session_id, + &context.client_instance_id, + operation, + now_unix_ms()?, + )?; + Ok(context) +} + fn development_root_from_session_root(session_root: &Path) -> Result { let account_root = session_root .parent() @@ -1167,6 +1489,13 @@ fn sha256_hex(bytes: &[u8]) -> String { .collect() } +fn now_unix_ms() -> Result { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}")) +} + impl BrokerResponse { fn success(result: Value) -> Self { Self { @@ -1234,6 +1563,11 @@ mod tests { use tempfile::TempDir; fn request(socket: &Path, value: Value) -> Value { + let value = if let Some(alias) = value.get("operation").and_then(Value::as_str) { + numbered_broker_request_value(alias, value.get("input").cloned()).unwrap() + } else { + value + }; let mut stream = connect_endpoint(socket).unwrap(); serde_json::to_writer(&mut stream, &value).unwrap(); stream.write_all(b"\n").unwrap(); @@ -1242,6 +1576,19 @@ mod tests { serde_json::from_str(&line).unwrap() } + #[test] + fn legacy_string_and_mismatched_broker_routes_fail_before_dispatch() { + assert!(decode_numbered_broker_request(br#"{"operation":"PING"}"#) + .unwrap_err() + .contains("ENVELOPE_INVALID")); + let mut numbered = numbered_broker_request_value("PING", None).unwrap(); + numbered["targetNumber"] = Value::String("HLP-NBROKER-TGT-9999".into()); + assert_eq!( + decode_numbered_broker_request(&serde_json::to_vec(&numbered).unwrap()).unwrap_err(), + "HOLOLAKE_NUMBERED_BROKER_ROUTE_COORDINATE_MISMATCH" + ); + } + #[test] fn broker_resumes_after_socket_disconnect() { let temp = TempDir::new().unwrap(); @@ -1867,23 +2214,20 @@ mod tests { ) .unwrap(); let mut connector = connect_endpoint(&socket).unwrap(); - serde_json::to_writer( - &mut connector, - &serde_json::json!({ - "operation": "OPEN_SESSION", - "input": { - "accountId": "human-1", - "laneId": "DEV-1", - "clientInstanceId": "codex-1", - "discoveryTicket": ticket.discovery_ticket - } - }), + let open_request = numbered_broker_request_value( + "OPEN_SESSION", + Some(serde_json::json!({ + "accountId": "human-1", + "laneId": "DEV-1", + "clientInstanceId": "codex-1", + "discoveryTicket": ticket.discovery_ticket + })), ) .unwrap(); + let open_bytes = serde_json::to_vec(&open_request).unwrap(); + connector.write_all(&open_bytes).unwrap(); connector.write_all(b"\n").unwrap(); - assert!(request_establishes_authenticated_connection( - br#"{"operation":"OPEN_SESSION"}"# - )); + assert!(request_establishes_authenticated_connection(&open_bytes)); let mut response = String::new(); let mut response_reader = BufReader::new(connector.try_clone().unwrap()); response_reader.read_line(&mut response).unwrap(); @@ -1981,4 +2325,88 @@ mod tests { "HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY" ); } + + #[test] + fn broker_projects_system_direct_status_and_refuses_untrusted_persona_license() { + let temp = TempDir::new().unwrap(); + let socket = temp.path().join("runtime/broker.sock"); + let descriptor = temp.path().join("broker.json"); + let sessions = temp.path().join("sessions"); + let routes = temp.path().join("routes"); + fs::create_dir_all(&sessions).unwrap(); + fs::create_dir_all(&routes).unwrap(); + let _broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap(); + let ticket = issue_ticket_at( + &sessions, + IssueDiscoveryTicketInput { + account_id: "human-1".into(), + lane_id: "DEV-1".into(), + client_instance_id: "codex-1".into(), + }, + ) + .unwrap(); + let opened = request( + &socket, + serde_json::json!({ + "operation": "OPEN_SESSION", + "input": { + "accountId": "human-1", + "laneId": "DEV-1", + "clientInstanceId": "codex-1", + "discoveryTicket": ticket.discovery_ticket + } + }), + ); + let session = serde_json::json!({ + "accountId": "human-1", + "sessionId": opened["result"]["sessionId"], + "resumeSecret": opened["result"]["resumeSecret"] + }); + let status = request( + &socket, + serde_json::json!({ + "operation": "GET_PERSONA_CARRIER_LICENSE_STATUS", + "input": { "session": session.clone() } + }), + ); + assert_eq!(status["ok"], true); + assert_eq!(status["result"]["controlMode"], "SYSTEM_DIRECT"); + assert_eq!(status["result"]["licenseInstalled"], false); + + let now = now_unix_ms().unwrap(); + let denied = request( + &socket, + serde_json::json!({ + "operation": "PRESENT_PERSONA_CARRIER_LICENSE", + "input": { + "session": session, + "license": { + "schema": "hololake.persona-carrier-runtime-license/v1", + "licenseId": "HLP-PERSONA-LICENSE-UNTRUSTED-0001", + "sequence": 1, + "personaNumber": "ICE-P-ZY001", + "humanControllerNumber": "ICE-GL∞", + "accountKey": opened["result"]["accountKey"], + "sessionId": opened["result"]["sessionId"], + "clientInstanceId": "codex-1", + "purpose": "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS", + "issuedAtUnixMs": now, + "validUntilUnixMs": now + 60000, + "allowedOperations": ["GET_WORK_ENVIRONMENT"], + "evidenceRootSha256": "b".repeat(64), + "currentEventSha256": "c".repeat(64), + "declaration": "PERSONA_DECLARES_CURRENT_CARRIER_BOUND", + "signerId": "ICE-P-ZY001-RUNTIME-SIGNER-001", + "signatureAlgorithm": "Ed25519", + "signature": "untrusted" + } + } + }), + ); + assert_eq!(denied["ok"], false); + assert_eq!( + denied["error"], + "HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER" + ); + } } diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs index 5a3fd980c..e5283fdad 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs @@ -167,7 +167,6 @@ struct ActiveSessionRecord { lane_id: String, } -#[tauri::command] pub async fn issue_direct_local_discovery_ticket( app: AppHandle, input: IssueDiscoveryTicketInput, @@ -178,7 +177,6 @@ pub async fn issue_direct_local_discovery_ticket( .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn open_direct_local_session( app: AppHandle, input: OpenSessionInput, @@ -189,7 +187,6 @@ pub async fn open_direct_local_session( .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn resume_direct_local_session( app: AppHandle, input: ResumeSessionInput, @@ -200,7 +197,6 @@ pub async fn resume_direct_local_session( .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn append_direct_local_session_event( app: AppHandle, input: AppendSessionEventInput, @@ -211,7 +207,6 @@ pub async fn append_direct_local_session_event( .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn heartbeat_direct_local_session( app: AppHandle, input: AuthenticateSessionInput, @@ -379,7 +374,7 @@ pub(crate) fn issue_ticket_at( client_instance_id: input.client_instance_id, discovery_ticket, issued_at_unix_ms, - terminal_transport: "HOLOLAKE_TERMINAL_LINK/2", + terminal_transport: "HOLOLAKE_TERMINAL_LINK/3", connector_executable: std::env::current_exe() .map(|path| path.to_string_lossy().into_owned()) .unwrap_or_else(|_| "HoloLake".into()), @@ -556,18 +551,6 @@ pub(crate) fn authenticate_context_at( }) } -pub(crate) fn authenticate_privileged_at( - root: &Path, - input: &AuthenticateSessionInput, -) -> Result<(), String> { - let lane = authenticate_with_lane_at(root, input)?; - if lane == "visitor-expression-only" { - Err("HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY".into()) - } else { - Ok(()) - } -} - fn session_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf { root.join("accounts") .join(account_key) @@ -713,7 +696,7 @@ fn session_receipt( last_event_sequence: record.last_event_sequence, resume_secret, continuity_owner: "HOLOLAKE", - transport: "HOLOLAKE_TERMINAL_LINK/2", + transport: "HOLOLAKE_TERMINAL_LINK/3", mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY", required_next_operation: "GET_WORK_ENVIRONMENT", receipt_id: sha256_hex( diff --git a/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs b/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs new file mode 100644 index 000000000..5a183b89c --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs @@ -0,0 +1,1541 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! 教育表格的双向动态转译层。 +//! +//! 外部文件只作为只读输入。解析完成后立即转换为 HoloLake 原生列/行结构并进入 +//! 当前登录账号的 SQLite 空间;WebView 不接触任意本机路径。导出则从原生结构生成 +//! 人类选择的交换格式,不把 Office 文档模型带进运行内核。 + +use crate::education_workspace::{ + education_workspace_database, import_table_batch_at, validate_table_data, + EducationImportRegistration, EducationTableColumn, EducationTableRow, ImportedEducationTable, +}; +use calamine::{open_workbook_auto, Data, Reader}; +use encoding_rs::GBK; +use ring::digest::{digest, SHA256}; +use rust_xlsxwriter::{Color, Format, Workbook}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use tauri_plugin_dialog::DialogExt; +use uuid::Uuid; + +const TRANSLATOR_SCHEMA: &str = "hololake.education-table-translator/v1"; +const NATIVE_TABLE_SCHEMA: &str = "hololake.education-table/v1"; +const IMPORT_ADAPTER: &str = "EDU-TABLE-IMPORT-ADAPTER/v1"; +const EXPORT_ADAPTER: &str = "EDU-TABLE-EXPORT-ADAPTER/v1"; +const MAX_IMPORT_FILE_BYTES: u64 = 25 * 1024 * 1024; +const MAX_TABLE_COLUMNS: usize = 30; +const MAX_TABLE_ROWS: usize = 1_000; +const MAX_CELL_BYTES: usize = 10_000; +const MAX_TITLE_BYTES: usize = 300; +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; +const ADAPTER: &str = "education-workbench-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationTableImportItem { + pub table_id: String, + pub title: String, + pub sheet_name: String, + pub column_count: usize, + pub row_count: usize, + pub leading_rows_ignored: usize, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationTableImportReceipt { + pub schema: &'static str, + pub state: &'static str, + pub adapter_id: &'static str, + pub import_id: String, + pub source_format: String, + pub source_filename: String, + pub source_sha256: String, + pub source_bytes: u64, + pub native_schema: &'static str, + pub content_profile: EducationContentProfile, + pub imported_tables: Vec, + pub imported_at_unix_ms: u128, + pub source_preserved_read_only: bool, + pub truncated: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationContentProfile { + pub schema: &'static str, + pub state: &'static str, + pub detected_family: String, + pub recognition_mode: String, + pub container_format: String, + pub extension_matches_container: bool, + pub page_kind: String, + pub page_count: usize, + pub non_empty_page_count: usize, + pub total_data_rows: usize, + pub total_columns: usize, + pub pages: Vec, + pub routing: Vec, + pub unresolved_signals: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationImportOutcome { + pub state: String, + pub import_receipt: Option, + pub assistance_receipt: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationRecognitionAssistanceReceipt { + pub schema: &'static str, + pub state: String, + pub request_id: String, + pub title: String, + pub message: String, + pub source_filename: String, + pub detected_container: String, + pub reason_code: String, + pub model_assistance_eligible: bool, + pub model_api_state: &'static str, + pub model_api_slot: &'static str, + pub requires_explicit_file_consent: bool, + pub source_preserved_read_only: bool, + pub available_learning_scopes: Vec<&'static str>, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationRecognitionCapability { + pub schema: &'static str, + pub state: &'static str, + pub deterministic_adapter_ids: Vec<&'static str>, + pub model_api_slot: &'static str, + pub model_api_state: &'static str, + pub provider_binding: &'static str, + pub secret_storage_requirement: &'static str, + pub file_transfer_default: &'static str, + pub rule_update_flow: Vec<&'static str>, + pub learning_scopes: Vec<&'static str>, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationContentPageProfile { + pub page_name: String, + pub state: String, + pub used_rows: usize, + pub used_columns: usize, + pub header_row_index: Option, + pub header_confidence: String, + pub data_rows: usize, + pub text_cells: usize, + pub numeric_cells: usize, + pub boolean_cells: usize, + pub date_cells: usize, + pub formula_cells: usize, + pub structure_kind: String, + pub focus_state: String, + pub focus_title: String, + pub primary_measure_column: Option, + pub secondary_measure_columns: Vec, + pub focus_confidence: String, + pub focus_reasons: Vec, +} + +#[derive(Clone, Debug)] +struct EducationSemanticFocus { + state: String, + title: String, + primary_measure_column: Option, + secondary_measure_columns: Vec, + confidence: String, + reasons: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationContentRoute { + pub page_name: String, + pub source_structure: String, + pub native_kind: String, + pub target_module: String, + pub decision: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExportEducationTableInput { + pub table: EducationTableExportDraft, + pub format: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EducationTableExportDraft { + pub table_id: String, + pub title: String, + pub columns: Vec, + pub rows: Vec, + pub revision: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationTableExportReceipt { + pub schema: &'static str, + pub state: &'static str, + pub adapter_id: &'static str, + pub table_id: String, + pub table_revision: i64, + pub target_format: String, + pub target_filename: String, + pub bytes: u64, + pub row_count: usize, + pub column_count: usize, + pub exported_at_unix_ms: u128, +} + +#[derive(Clone, Debug)] +struct ParsedSheet { + sheet_name: String, + leading_rows_ignored: usize, + table: ImportedEducationTable, +} + +#[derive(Clone, Debug)] +struct ParseOutcome { + source_format: String, + content_profile: EducationContentProfile, + parsed_sheets: Vec, +} + +#[derive(Clone, Copy, Debug, Default)] +struct CellCounts { + text: usize, + numeric: usize, + boolean: usize, + date: usize, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct NativeEducationTableFile { + schema: String, + title: String, + columns: Vec, + rows: Vec, +} + +pub async fn import_education_tables_from_dialog( + app: AppHandle, +) -> Result, String> { + require_active(&app)?; + let selected = app + .dialog() + .file() + .set_title("导入外部表格并转为 HoloLake 原生格式") + .add_filter( + "表格文件", + &["xlsx", "xls", "xlsm", "xlsb", "ods", "csv", "tsv", "json"], + ) + .blocking_pick_file(); + let Some(selected) = selected else { + return Ok(None); + }; + let path = selected + .into_path() + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_PATH_INVALID: {error}"))?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || match import_tables_from_path(&database, &path) { + Ok(receipt) => Ok(EducationImportOutcome { + state: "STAGED_UNASSIGNED".into(), + import_receipt: Some(receipt), + assistance_receipt: None, + }), + Err(error) if is_human_assistance_case(&error) => Ok(EducationImportOutcome { + state: "NEEDS_HUMAN_DECISION".into(), + import_receipt: None, + assistance_receipt: Some(assistance_receipt_for(&path, &error)), + }), + Err(error) => Err(error), + }) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_JOIN_FAILED: {error}"))? + .map(Some) +} + +pub fn get_education_recognition_capability( + app: AppHandle, +) -> Result { + require_active(&app)?; + Ok(EducationRecognitionCapability { + schema: "hololake.education-recognition-capability/v1", + state: "DETERMINISTIC_READY_MODEL_SLOT_UNBOUND", + deterministic_adapter_ids: vec![IMPORT_ADAPTER, EXPORT_ADAPTER], + model_api_slot: "HOLOLAKE_MODEL_RECOGNITION_API/v1", + model_api_state: "NOT_CONFIGURED", + provider_binding: "USER_SELECTED_PROVIDER", + secret_storage_requirement: "OPERATING_SYSTEM_SECRET_STORE", + file_transfer_default: "DENY_UNTIL_EXPLICIT_PER_FILE_CONSENT", + rule_update_flow: vec![ + "MODEL_PROPOSES_CANDIDATE", + "LOCAL_VALIDATION", + "HUMAN_CONFIRMATION", + "VERSIONED_RULE_INSTALL", + ], + learning_scopes: vec!["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"], + }) +} + +pub async fn export_education_table_to_dialog( + app: AppHandle, + input: ExportEducationTableInput, +) -> Result, String> { + require_active(&app)?; + validate_export_draft(&input.table)?; + let format = normalized_export_format(&input.format)?; + let extension = match format.as_str() { + "XLSX" => "xlsx", + "CSV" => "csv", + "TSV" => "tsv", + "HOLOLAKE_NATIVE" => "holotable.json", + _ => return Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()), + }; + let filename = format!("{}.{}", safe_filename(&input.table.title), extension); + let selected = app + .dialog() + .file() + .set_title("导出教育表格") + .set_file_name(filename) + .add_filter(export_filter_label(&format), &[extension]) + .blocking_save_file(); + let Some(selected) = selected else { + return Ok(None); + }; + let path = selected + .into_path() + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_PATH_INVALID: {error}"))?; + tauri::async_runtime::spawn_blocking(move || export_table_to_path(&path, &format, input.table)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_JOIN_FAILED: {error}"))? + .map(Some) +} + +fn import_tables_from_path( + database: &Path, + source: &Path, +) -> Result { + let metadata = fs::metadata(source) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE: {error}"))?; + if !metadata.is_file() || metadata.len() == 0 || metadata.len() > MAX_IMPORT_FILE_BYTES { + return Err("HOLOLAKE_EDUCATION_IMPORT_FILE_BOUNDS_INVALID".into()); + } + let source_filename = source + .file_name() + .and_then(|value| value.to_str()) + .ok_or_else(|| "HOLOLAKE_EDUCATION_IMPORT_FILENAME_INVALID".to_string())? + .to_string(); + let source_bytes = fs::read(source) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE: {error}"))?; + let source_sha256 = hex_digest(&source_bytes); + let outcome = parse_source_file(source, &source_bytes)?; + if outcome.parsed_sheets.is_empty() { + return Err("HOLOLAKE_EDUCATION_IMPORT_EMPTY".into()); + } + let imported_at_unix_ms = now_ms(); + let profile_json = serde_json::to_string(&outcome.content_profile) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_PROFILE_INVALID: {error}"))?; + let (import_id, imported) = import_table_batch_at( + database, + outcome + .parsed_sheets + .iter() + .map(|sheet| sheet.table.clone()) + .collect(), + EducationImportRegistration { + source_filename: source_filename.clone(), + source_format: outcome.source_format.clone(), + source_sha256: source_sha256.clone(), + profile_json, + imported_at_unix_ms: imported_at_unix_ms as i64, + }, + )?; + let imported_tables = outcome + .parsed_sheets + .into_iter() + .zip(imported) + .map(|(sheet, table)| EducationTableImportItem { + table_id: table.table_id, + title: table.title, + sheet_name: sheet.sheet_name, + column_count: table.columns.len(), + row_count: table.rows.len(), + leading_rows_ignored: sheet.leading_rows_ignored, + }) + .collect(); + Ok(EducationTableImportReceipt { + schema: TRANSLATOR_SCHEMA, + state: "STAGED_UNASSIGNED", + adapter_id: IMPORT_ADAPTER, + import_id, + source_format: outcome.source_format, + source_filename, + source_sha256, + source_bytes: metadata.len(), + native_schema: NATIVE_TABLE_SCHEMA, + content_profile: outcome.content_profile, + imported_tables, + imported_at_unix_ms, + source_preserved_read_only: true, + truncated: false, + }) +} + +fn parse_source_file(source: &Path, bytes: &[u8]) -> Result { + let filename = source + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default() + .to_ascii_lowercase(); + if filename.ends_with(".holotable.json") { + let sheet = parse_native_table(bytes)?; + return Ok(outcome_from_parsed( + "HOLOLAKE_NATIVE", + "NATIVE_JSON", + true, + vec![sheet], + Vec::new(), + )); + } + let extension = source + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default() + .to_ascii_lowercase(); + let detected_container = detect_container(bytes); + match extension.as_str() { + "csv" => parse_delimited(bytes, b',', source).map(|sheet| { + outcome_from_parsed("CSV", "DELIMITED_TEXT", true, vec![sheet], Vec::new()) + }), + "tsv" => parse_delimited(bytes, b'\t', source).map(|sheet| { + outcome_from_parsed("TSV", "DELIMITED_TEXT", true, vec![sheet], Vec::new()) + }), + "xlsx" | "xls" | "xlsm" | "xlsb" | "ods" => parse_workbook( + source, + &detected_workbook_format(&extension, &detected_container), + &detected_container, + extension_matches_container(&extension, &detected_container), + ), + _ => Err("HOLOLAKE_EDUCATION_IMPORT_FORMAT_UNSUPPORTED".into()), + } +} + +fn parse_workbook( + source: &Path, + detected_format: &str, + detected_container: &str, + extension_matches: bool, +) -> Result { + let mut workbook = open_workbook_auto(source) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_WORKBOOK_INVALID: {error}"))?; + let stem = file_stem(source); + let sheet_names = workbook.sheet_names().to_vec(); + let mut parsed = Vec::new(); + let mut profiles = Vec::new(); + for sheet_name in sheet_names { + let range = workbook + .worksheet_range(&sheet_name) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_SHEET_INVALID: {error}"))?; + let formula_range = workbook.worksheet_formula(&sheet_name).ok(); + let formula_cells = formula_range + .as_ref() + .map(|formulas| { + formulas + .rows() + .flat_map(|row| row.iter()) + .filter(|formula| !formula.trim().is_empty()) + .count() + }) + .unwrap_or(0); + let cell_counts = workbook_cell_counts(range.rows().flat_map(|row| row.iter())); + let matrix = range + .rows() + .map(|row| row.iter().map(cell_text).collect::>()) + .collect::>(); + if let Some((table, leading_rows_ignored)) = + matrix_to_native_table(matrix, &workbook_table_title(&stem, &sheet_name), true)? + { + profiles.push(profile_for_imported_sheet( + &sheet_name, + &table, + leading_rows_ignored, + cell_counts, + formula_cells, + )); + parsed.push(ParsedSheet { + sheet_name, + leading_rows_ignored, + table, + }); + } else { + profiles.push(EducationContentPageProfile { + page_name: sheet_name, + state: "EMPTY_SKIPPED".into(), + used_rows: 0, + used_columns: 0, + header_row_index: None, + header_confidence: "NONE".into(), + data_rows: 0, + text_cells: 0, + numeric_cells: 0, + boolean_cells: 0, + date_cells: 0, + formula_cells, + structure_kind: "EMPTY".into(), + focus_state: "NO_CONTENT".into(), + focus_title: "空页".into(), + primary_measure_column: None, + secondary_measure_columns: Vec::new(), + focus_confidence: "NONE".into(), + focus_reasons: vec!["页面没有可分析的数据".into()], + }); + } + } + Ok(outcome_from_parsed( + detected_format, + detected_container, + extension_matches, + parsed, + profiles, + )) +} + +fn parse_delimited(bytes: &[u8], delimiter: u8, source: &Path) -> Result { + let decoded = decode_delimited_text(bytes)?; + let mut reader = csv::ReaderBuilder::new() + .has_headers(false) + .flexible(true) + .delimiter(delimiter) + .from_reader(decoded.as_bytes()); + let mut matrix = Vec::new(); + for record in reader.records() { + let record = record + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_DELIMITED_INVALID: {error}"))?; + matrix.push(record.iter().map(ToString::to_string).collect()); + if matrix.len() > MAX_TABLE_ROWS + 32 { + return Err("HOLOLAKE_EDUCATION_IMPORT_ROW_LIMIT_EXCEEDED".into()); + } + } + let title = file_stem(source); + let (table, leading_rows_ignored) = matrix_to_native_table(matrix, &title, false)? + .ok_or_else(|| "HOLOLAKE_EDUCATION_IMPORT_EMPTY".to_string())?; + Ok(ParsedSheet { + sheet_name: title, + leading_rows_ignored, + table, + }) +} + +fn parse_native_table(bytes: &[u8]) -> Result { + let native: NativeEducationTableFile = serde_json::from_slice(bytes) + .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_NATIVE_INVALID: {error}"))?; + if native.schema != NATIVE_TABLE_SCHEMA { + return Err("HOLOLAKE_EDUCATION_IMPORT_NATIVE_SCHEMA_UNSUPPORTED".into()); + } + let columns = native + .columns + .into_iter() + .map(|column| EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title: column.title, + }) + .collect::>(); + let rows = native + .rows + .into_iter() + .map(|row| EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells: row.cells, + }) + .collect::>(); + validate_table_data(&columns, &rows)?; + Ok(ParsedSheet { + sheet_name: "HoloLake 原生表格".into(), + leading_rows_ignored: 0, + table: ImportedEducationTable { + title: native.title, + columns, + rows, + }, + }) +} + +fn outcome_from_parsed( + source_format: &str, + container_format: &str, + extension_matches_container: bool, + parsed_sheets: Vec, + mut profiles: Vec, +) -> ParseOutcome { + if profiles.is_empty() { + profiles = parsed_sheets + .iter() + .map(|sheet| { + let counts = inferred_cell_counts(&sheet.table); + profile_for_imported_sheet( + &sheet.sheet_name, + &sheet.table, + sheet.leading_rows_ignored, + counts, + 0, + ) + }) + .collect(); + } + let routing = profiles + .iter() + .map(|page| EducationContentRoute { + page_name: page.page_name.clone(), + source_structure: page.structure_kind.clone(), + native_kind: if page.state == "READY_TO_TRANSLATE" { + "HOLOLAKE_TABLE".into() + } else { + "NONE".into() + }, + target_module: if page.state == "READY_TO_TRANSLATE" { + "UNASSIGNED_CHANNEL_STAGING".into() + } else { + "NONE".into() + }, + decision: if page.state == "READY_TO_TRANSLATE" { + "PROFILED_THEN_STAGED_PENDING_HUMAN_ROUTE".into() + } else { + "EMPTY_PAGE_SKIPPED_WITH_RECORD".into() + }, + }) + .collect::>(); + let non_empty_page_count = profiles + .iter() + .filter(|profile| profile.state == "READY_TO_TRANSLATE") + .count(); + let total_data_rows = profiles.iter().map(|profile| profile.data_rows).sum(); + let total_columns = profiles.iter().map(|profile| profile.used_columns).sum(); + ParseOutcome { + source_format: source_format.into(), + content_profile: EducationContentProfile { + schema: "hololake.content-profile/v1", + state: "PROFILED_BEFORE_WRITE", + detected_family: "TABULAR_DATA".into(), + recognition_mode: "DETERMINISTIC_LOCAL".into(), + container_format: container_format.into(), + extension_matches_container, + page_kind: if profiles.len() > 1 { + "WORKSHEET".into() + } else { + "TABLE_PAGE".into() + }, + page_count: profiles.len(), + non_empty_page_count, + total_data_rows, + total_columns, + pages: profiles, + routing, + unresolved_signals: profile_unresolved_signals(container_format), + }, + parsed_sheets, + } +} + +fn profile_unresolved_signals(container_format: &str) -> Vec { + match container_format { + "OOXML_WORKBOOK_ZIP" | "XLSB_WORKBOOK_ZIP" | "OLE_COMPOUND" | "ODS_ZIP" => vec![ + "SOURCE_VISUAL_FORMATTING_IS_PRESENTATION_ONLY_NOT_NATIVE_DATA".into(), + "FORMULAS_ARE_IMPORTED_AS_LAST_STORED_VALUES_UNLESS_EXPLICITLY_REBUILT".into(), + ], + _ => Vec::new(), + } +} + +fn is_human_assistance_case(error: &str) -> bool { + error.starts_with("HOLOLAKE_EDUCATION_IMPORT_") + && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE") + && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_PATH_INVALID") + && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_FILENAME_INVALID") +} + +fn assistance_receipt_for(source: &Path, reason: &str) -> EducationRecognitionAssistanceReceipt { + let filename = source + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or("未命名文件") + .to_string(); + let bytes = fs::read(source).unwrap_or_default(); + let detected_container = detect_container(&bytes); + let safety_limit = reason.contains("LIMIT_EXCEEDED") + || reason.contains("BOUNDS_INVALID") + || reason.contains("TOO_LARGE"); + let (title, message, eligible) = if safety_limit { + ( + "这个文件超出了当前模块的安全处理范围".to_string(), + "HoloLake 已停止写入并保留原文件。请先拆分文件或减少单页行列;模型辅助不会绕过本机安全上限。".to_string(), + false, + ) + } else { + ( + "当前版本还不能可靠识别这个文件".to_string(), + "HoloLake 没有猜测内容,也没有写入模块。配置模型接口后,可在明确授权当前文件的前提下重新识别,并把新规则保存为仅自己使用或去隐私后共享。".to_string(), + true, + ) + }; + EducationRecognitionAssistanceReceipt { + schema: "hololake.recognition-assistance-receipt/v1", + state: if eligible { + "MODEL_ASSISTANCE_AVAILABLE_AFTER_CONFIGURATION".into() + } else { + "LOCAL_INPUT_CHANGE_REQUIRED".into() + }, + request_id: format!("EDU-RECOGNITION-{}", Uuid::new_v4()), + title, + message, + source_filename: filename, + detected_container, + reason_code: reason.to_string(), + model_assistance_eligible: eligible, + model_api_state: "NOT_CONFIGURED", + model_api_slot: "HOLOLAKE_MODEL_RECOGNITION_API/v1", + requires_explicit_file_consent: true, + source_preserved_read_only: true, + available_learning_scopes: vec!["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"], + } +} + +fn profile_for_imported_sheet( + sheet_name: &str, + table: &ImportedEducationTable, + leading_rows_ignored: usize, + counts: CellCounts, + formula_cells: usize, +) -> EducationContentPageProfile { + let focus = infer_semantic_focus(table); + EducationContentPageProfile { + page_name: sheet_name.into(), + state: "READY_TO_TRANSLATE".into(), + used_rows: table.rows.len() + 1, + used_columns: table.columns.len(), + header_row_index: Some(leading_rows_ignored), + header_confidence: if leading_rows_ignored > 0 { + "DENSE_ROW_DETECTED".into() + } else { + "FIRST_NONEMPTY_ROW".into() + }, + data_rows: table.rows.len(), + text_cells: counts.text, + numeric_cells: counts.numeric, + boolean_cells: counts.boolean, + date_cells: counts.date, + formula_cells, + structure_kind: if table.columns.len() == 1 { + "SINGLE_FIELD_LIST".into() + } else { + "RECTANGULAR_TABLE".into() + }, + focus_state: focus.state, + focus_title: focus.title, + primary_measure_column: focus.primary_measure_column, + secondary_measure_columns: focus.secondary_measure_columns, + focus_confidence: focus.confidence, + focus_reasons: focus.reasons, + } +} + +fn infer_semantic_focus(table: &ImportedEducationTable) -> EducationSemanticFocus { + let title = table.title.to_lowercase(); + let compensation_context = contains_any(&title, &["稿费", "收入", "薪酬", "结算", "营收"]); + let achievement_context = contains_any(&title, &["成绩", "考试", "测评", "学员"]); + let attendance_context = contains_any(&title, &["考勤", "出勤", "签到"]); + let mut scored = table + .columns + .iter() + .enumerate() + .filter_map(|(index, column)| { + let name = column.title.trim(); + if name.is_empty() || is_sensitive_header(name) { + return None; + } + let normalized = name.to_lowercase().replace([' ', '_', '-'], ""); + let mut score = semantic_measure_score(&normalized, compensation_context); + let numeric_count = table + .rows + .iter() + .filter(|row| { + parse_semantic_number(row.cells.get(index).map(String::as_str).unwrap_or("")) + .is_some() + }) + .count(); + if !table.rows.is_empty() { + score += ((numeric_count * 30) / table.rows.len()) as i32; + } + (score > 0).then(|| (score, name.to_string())) + }) + .collect::>(); + scored.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1))); + let primary = scored.first().cloned(); + let focus_title = if compensation_context { + "稿费收入".to_string() + } else if achievement_context { + "学习成绩".to_string() + } else if attendance_context { + "出勤情况".to_string() + } else if let Some((_, column)) = &primary { + column.clone() + } else { + "重点待选择".to_string() + }; + let Some((primary_score, primary_column)) = primary else { + return EducationSemanticFocus { + state: "NEEDS_HUMAN_SELECTION".into(), + title: focus_title, + primary_measure_column: None, + secondary_measure_columns: Vec::new(), + confidence: "LOW".into(), + reasons: vec!["没有发现可可靠计算的重点字段,渲染层必须请人选择".into()], + }; + }; + if primary_score < 70 { + return EducationSemanticFocus { + state: "NEEDS_HUMAN_SELECTION".into(), + title: "重点待选择".into(), + primary_measure_column: None, + secondary_measure_columns: scored.into_iter().take(3).map(|(_, name)| name).collect(), + confidence: "LOW".into(), + reasons: vec!["字段语义和数值覆盖率不足以自动决定主指标".into()], + }; + } + EducationSemanticFocus { + state: "DETERMINISTIC_FOCUS_INFERRED".into(), + title: focus_title.clone(), + primary_measure_column: Some(primary_column.clone()), + secondary_measure_columns: scored + .into_iter() + .skip(1) + .take(3) + .map(|(_, name)| name) + .collect(), + confidence: if primary_score >= 140 { + "HIGH" + } else { + "MEDIUM" + } + .into(), + reasons: vec![ + format!("表名与页名语义指向“{focus_title}”"), + format!("字段“{primary_column}”同时通过语义优先级与数值覆盖率评分"), + ], + } +} + +fn semantic_measure_score(name: &str, compensation_context: bool) -> i32 { + if contains_any( + name, + &["序号", "编号", "账号", "电话", "手机", "id", "日期", "时间"], + ) { + return -200; + } + let mut score = if contains_any(name, &["实际收入", "实收", "到账"]) { + 170 + } else if contains_any(name, &["当月收入", "本月收入", "稿费", "结算金额"]) { + 160 + } else if contains_any(name, &["累计收入", "总收入", "收入合计"]) { + 150 + } else if contains_any(name, &["收入", "营收", "薪酬", "金额", "合计", "总计"]) { + 120 + } else if contains_any( + name, + &[ + "成绩", + "分数", + "得分", + "数量", + "人数", + "课时", + "时长", + "成本", + "预算", + "进度", + "完成率", + ], + ) { + 90 + } else { + 0 + }; + if compensation_context && contains_any(name, &["收入", "稿费", "实收", "到账"]) { + score += 35; + } + score +} + +fn contains_any(value: &str, candidates: &[&str]) -> bool { + candidates.iter().any(|candidate| value.contains(candidate)) +} + +fn is_sensitive_header(value: &str) -> bool { + contains_any( + &value.to_lowercase(), + &[ + "密码", + "口令", + "密钥", + "凭据", + "secret", + "token", + "apikey", + "accesskey", + "credential", + ], + ) +} + +fn parse_semantic_number(value: &str) -> Option { + let normalized = value + .trim() + .replace([',', ',', ' ', '¥', '¥', '$', '€', '£'], "") + .trim_end_matches('%') + .to_string(); + if normalized.is_empty() { + return None; + } + normalized + .parse::() + .ok() + .filter(|value| value.is_finite()) +} + +fn workbook_cell_counts<'a>(cells: impl Iterator) -> CellCounts { + let mut counts = CellCounts::default(); + for cell in cells { + match cell { + Data::Int(_) | Data::Float(_) => counts.numeric += 1, + Data::Bool(_) => counts.boolean += 1, + Data::DateTime(_) | Data::DateTimeIso(_) | Data::DurationIso(_) => counts.date += 1, + Data::String(value) if !value.trim().is_empty() => counts.text += 1, + Data::Error(_) => counts.text += 1, + Data::Empty | Data::String(_) => {} + } + } + counts +} + +fn inferred_cell_counts(table: &ImportedEducationTable) -> CellCounts { + let mut counts = CellCounts { + text: table.columns.len(), + ..CellCounts::default() + }; + for cell in table.rows.iter().flat_map(|row| row.cells.iter()) { + let value = cell.trim(); + if value.is_empty() { + continue; + } + if value.parse::().is_ok() { + counts.numeric += 1; + } else if matches!(value.to_ascii_lowercase().as_str(), "true" | "false") { + counts.boolean += 1; + } else if looks_like_iso_date(value) { + counts.date += 1; + } else { + counts.text += 1; + } + } + counts +} + +fn looks_like_iso_date(value: &str) -> bool { + let bytes = value.as_bytes(); + bytes.len() >= 10 + && bytes[0..4].iter().all(u8::is_ascii_digit) + && matches!(bytes[4], b'-' | b'/') + && bytes[5..7].iter().all(u8::is_ascii_digit) + && matches!(bytes[7], b'-' | b'/') + && bytes[8..10].iter().all(u8::is_ascii_digit) +} + +fn detect_container(bytes: &[u8]) -> String { + if bytes.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) { + return "OLE_COMPOUND".into(); + } + if bytes.starts_with(b"PK\x03\x04") { + if contains_bytes(bytes, b"xl/workbook.bin") { + return "XLSB_WORKBOOK_ZIP".into(); + } + if contains_bytes(bytes, b"xl/workbook.xml") { + return "OOXML_WORKBOOK_ZIP".into(); + } + if contains_bytes(bytes, b"content.xml") && contains_bytes(bytes, b"mimetype") { + return "ODS_ZIP".into(); + } + return "ZIP_CONTAINER".into(); + } + if bytes + .iter() + .take(512) + .all(|byte| *byte == 0 || *byte == 9 || *byte == 10 || *byte == 13 || *byte >= 0x20) + { + return "TEXT_STREAM".into(); + } + "UNKNOWN_BINARY".into() +} + +fn detected_workbook_format(extension: &str, container: &str) -> String { + match container { + "OLE_COMPOUND" => "XLS".into(), + "XLSB_WORKBOOK_ZIP" => "XLSB".into(), + "ODS_ZIP" => "ODS".into(), + "OOXML_WORKBOOK_ZIP" if extension == "xlsm" => "XLSM".into(), + "OOXML_WORKBOOK_ZIP" => "XLSX".into(), + _ => extension.to_ascii_uppercase(), + } +} + +fn extension_matches_container(extension: &str, container: &str) -> bool { + match extension { + "xls" => container == "OLE_COMPOUND", + "xlsb" => container == "XLSB_WORKBOOK_ZIP", + "ods" => container == "ODS_ZIP", + "xlsx" | "xlsm" => container == "OOXML_WORKBOOK_ZIP", + _ => false, + } +} + +fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { + !needle.is_empty() + && haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn matrix_to_native_table( + matrix: Vec>, + title: &str, + detect_header: bool, +) -> Result, String> { + let nonempty_rows = matrix + .iter() + .enumerate() + .filter(|(_, row)| row.iter().any(|cell| !cell.trim().is_empty())) + .map(|(index, _)| index) + .collect::>(); + let Some(&first_nonempty) = nonempty_rows.first() else { + return Ok(None); + }; + let header_index = if detect_header { + let mut best_index = first_nonempty; + let mut best_score = 0; + for index in nonempty_rows.iter().copied().take(10) { + let score = matrix[index] + .iter() + .filter(|cell| !cell.trim().is_empty()) + .count(); + if score > best_score { + best_index = index; + best_score = score; + } + } + best_index + } else { + first_nonempty + }; + let last_row = *nonempty_rows.last().unwrap_or(&header_index); + let first_column = matrix[header_index..=last_row] + .iter() + .filter_map(|row| row.iter().position(|cell| !cell.trim().is_empty())) + .min() + .unwrap_or(0); + let last_column = matrix[header_index..=last_row] + .iter() + .filter_map(|row| row.iter().rposition(|cell| !cell.trim().is_empty())) + .max() + .unwrap_or(first_column); + let column_count = last_column.saturating_sub(first_column) + 1; + if column_count == 0 || column_count > MAX_TABLE_COLUMNS { + return Err("HOLOLAKE_EDUCATION_IMPORT_COLUMN_LIMIT_EXCEEDED".into()); + } + let header = &matrix[header_index]; + let mut seen_headers: HashMap = HashMap::new(); + let mut columns = Vec::with_capacity(column_count); + for offset in 0..column_count { + let source_title = header + .get(first_column + offset) + .map(|value| value.trim()) + .unwrap_or_default(); + let base = if source_title.is_empty() { + format!("字段 {}", offset + 1) + } else { + source_title.to_string() + }; + if base.len() > MAX_TITLE_BYTES { + return Err("HOLOLAKE_EDUCATION_IMPORT_HEADER_TOO_LARGE".into()); + } + let count = seen_headers.entry(base.clone()).or_insert(0); + *count += 1; + let title = if *count == 1 { + base + } else { + format!("{} ({})", base, count) + }; + columns.push(EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title, + }); + } + let mut rows = Vec::new(); + for source_row in matrix.iter().take(last_row + 1).skip(header_index + 1) { + let cells = (0..column_count) + .map(|offset| { + source_row + .get(first_column + offset) + .cloned() + .unwrap_or_default() + }) + .collect::>(); + if cells.iter().all(|cell| cell.trim().is_empty()) { + continue; + } + if cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES) { + return Err("HOLOLAKE_EDUCATION_IMPORT_CELL_TOO_LARGE".into()); + } + rows.push(EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells, + }); + if rows.len() > MAX_TABLE_ROWS { + return Err("HOLOLAKE_EDUCATION_IMPORT_ROW_LIMIT_EXCEEDED".into()); + } + } + let table = ImportedEducationTable { + title: bounded_title(title)?, + columns, + rows, + }; + validate_table_data(&table.columns, &table.rows)?; + Ok(Some((table, header_index))) +} + +fn cell_text(cell: &Data) -> String { + match cell { + Data::DateTime(value) => value + .as_datetime() + .map(|datetime| { + if datetime.and_utc().timestamp() % 86_400 == 0 { + datetime.date().to_string() + } else { + datetime.to_string() + } + }) + .unwrap_or_else(|| value.to_string()), + Data::Empty => String::new(), + _ => cell.to_string(), + } +} + +fn decode_delimited_text(bytes: &[u8]) -> Result { + if let Some(stripped) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) { + return String::from_utf8(stripped.to_vec()) + .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); + } + if let Some(stripped) = bytes.strip_prefix(&[0xFF, 0xFE]) { + if stripped.len() % 2 != 0 { + return Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); + } + let values = stripped + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect::>(); + return String::from_utf16(&values) + .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); + } + if let Some(stripped) = bytes.strip_prefix(&[0xFE, 0xFF]) { + if stripped.len() % 2 != 0 { + return Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); + } + let values = stripped + .chunks_exact(2) + .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])) + .collect::>(); + return String::from_utf16(&values) + .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); + } + if let Ok(value) = String::from_utf8(bytes.to_vec()) { + return Ok(value); + } + let (decoded, _, had_errors) = GBK.decode(bytes); + if had_errors { + Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()) + } else { + Ok(decoded.into_owned()) + } +} + +fn export_table_to_path( + target: &Path, + format: &str, + table: EducationTableExportDraft, +) -> Result { + validate_export_draft(&table)?; + if let Some(parent) = target.parent() { + if !parent.exists() { + return Err("HOLOLAKE_EDUCATION_EXPORT_DIRECTORY_MISSING".into()); + } + } + match format { + "XLSX" => write_xlsx(target, &table)?, + "CSV" => write_delimited(target, &table, b',')?, + "TSV" => write_delimited(target, &table, b'\t')?, + "HOLOLAKE_NATIVE" => write_native_table(target, &table)?, + _ => return Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()), + } + let metadata = fs::metadata(target) + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_VERIFY_FAILED: {error}"))?; + Ok(EducationTableExportReceipt { + schema: TRANSLATOR_SCHEMA, + state: "EXPORTED_FROM_NATIVE", + adapter_id: EXPORT_ADAPTER, + table_id: table.table_id, + table_revision: table.revision, + target_format: format.to_string(), + target_filename: target + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or("教育表格") + .to_string(), + bytes: metadata.len(), + row_count: table.rows.len(), + column_count: table.columns.len(), + exported_at_unix_ms: now_ms(), + }) +} + +fn write_xlsx(target: &Path, table: &EducationTableExportDraft) -> Result<(), String> { + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + worksheet + .set_name("HoloLake 数据") + .map_err(export_xlsx_error)?; + let header = Format::new() + .set_bold() + .set_font_color(Color::RGB(0xF7EDC5)) + .set_background_color(Color::RGB(0x111827)); + for (column_index, column) in table.columns.iter().enumerate() { + worksheet + .write_string_with_format(0, column_index as u16, &column.title, &header) + .map_err(export_xlsx_error)?; + } + for (row_index, row) in table.rows.iter().enumerate() { + for (column_index, cell) in row.cells.iter().enumerate() { + worksheet + .write_string((row_index + 1) as u32, column_index as u16, cell) + .map_err(export_xlsx_error)?; + } + } + worksheet + .set_freeze_panes(1, 0) + .map_err(export_xlsx_error)?; + worksheet.autofit(); + workbook.save(target).map_err(export_xlsx_error) +} + +fn write_delimited( + target: &Path, + table: &EducationTableExportDraft, + delimiter: u8, +) -> Result<(), String> { + let mut writer = csv::WriterBuilder::new() + .delimiter(delimiter) + .from_writer(Vec::new()); + writer + .write_record(table.columns.iter().map(|column| column.title.as_str())) + .map_err(export_csv_error)?; + for row in &table.rows { + let protected = row + .cells + .iter() + .map(|cell| protect_spreadsheet_formula(cell)) + .collect::>(); + writer.write_record(protected).map_err(export_csv_error)?; + } + writer + .flush() + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}"))?; + let bytes = writer + .into_inner() + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}"))?; + let mut with_bom = Vec::with_capacity(bytes.len() + 3); + with_bom.extend_from_slice(&[0xEF, 0xBB, 0xBF]); + with_bom.extend_from_slice(&bytes); + fs::write(target, with_bom) + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_WRITE_FAILED: {error}")) +} + +fn write_native_table(target: &Path, table: &EducationTableExportDraft) -> Result<(), String> { + let native = NativeEducationTableFile { + schema: NATIVE_TABLE_SCHEMA.into(), + title: table.title.clone(), + columns: table.columns.clone(), + rows: table.rows.clone(), + }; + let bytes = serde_json::to_vec_pretty(&native) + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_NATIVE_FAILED: {error}"))?; + fs::write(target, bytes) + .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_WRITE_FAILED: {error}")) +} + +fn validate_export_draft(table: &EducationTableExportDraft) -> Result<(), String> { + if !table.table_id.starts_with("EDU-TABLE-") || table.revision < 1 { + return Err("HOLOLAKE_EDUCATION_EXPORT_TABLE_INVALID".into()); + } + bounded_title(&table.title)?; + validate_table_data(&table.columns, &table.rows) +} + +fn normalized_export_format(value: &str) -> Result { + let value = value.trim().to_ascii_uppercase(); + if matches!(value.as_str(), "XLSX" | "CSV" | "TSV" | "HOLOLAKE_NATIVE") { + Ok(value) + } else { + Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()) + } +} + +fn export_filter_label(format: &str) -> &'static str { + match format { + "XLSX" => "Excel 工作簿", + "CSV" => "CSV 表格", + "TSV" => "TSV 表格", + "HOLOLAKE_NATIVE" => "HoloLake 原生表格", + _ => "表格文件", + } +} + +fn protect_spreadsheet_formula(cell: &str) -> String { + if cell + .trim_start() + .chars() + .next() + .is_some_and(|character| matches!(character, '=' | '+' | '-' | '@')) + { + format!("'{cell}") + } else { + cell.to_string() + } +} + +fn bounded_title(value: &str) -> Result { + let title = value.trim(); + if title.is_empty() + || title.len() > MAX_TITLE_BYTES + || title + .chars() + .any(|character| matches!(character, '\0' | '\r' | '\n')) + { + Err("HOLOLAKE_EDUCATION_IMPORT_TITLE_INVALID".into()) + } else { + Ok(title.to_string()) + } +} + +fn workbook_table_title(stem: &str, sheet_name: &str) -> String { + let candidate = format!("{stem} · {sheet_name}"); + if candidate.len() <= MAX_TITLE_BYTES { + candidate + } else if sheet_name.len() <= MAX_TITLE_BYTES { + sheet_name.to_string() + } else { + "导入的教育表格".into() + } +} + +fn file_stem(path: &Path) -> String { + path.file_stem() + .and_then(|value| value.to_str()) + .filter(|value| !value.trim().is_empty()) + .unwrap_or("导入的教育表格") + .to_string() +} + +fn safe_filename(value: &str) -> String { + let name = value + .chars() + .map(|character| { + if matches!( + character, + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' + ) || character.is_control() + { + '_' + } else { + character + } + }) + .collect::(); + let name = name.trim().trim_matches('.'); + if name.is_empty() { + "教育表格".into() + } else { + name.to_string() + } +} + +fn hex_digest(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn export_xlsx_error(error: rust_xlsxwriter::XlsxError) -> String { + format!("HOLOLAKE_EDUCATION_EXPORT_XLSX_FAILED: {error}") +} + +fn export_csv_error(error: csv::Error) -> String { + format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}") +} + +fn now_ms() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn csv_round_trip_preserves_chinese_and_blocks_formula_injection() { + let directory = tempdir().unwrap(); + let source = directory.path().join("学员.csv"); + fs::write(&source, "姓名,状态\n冰朔,连载中\n危险,=cmd()\n").unwrap(); + let database = directory.path().join("education.sqlite3"); + let receipt = import_tables_from_path(&database, &source).unwrap(); + assert_eq!(receipt.imported_tables.len(), 1); + let imported = crate::education_workspace::read_table_at( + &database, + &receipt.imported_tables[0].table_id, + ) + .unwrap(); + assert_eq!(imported.rows[0].cells, vec!["冰朔", "连载中"]); + let export = directory.path().join("export.csv"); + export_table_to_path( + &export, + "CSV", + EducationTableExportDraft { + table_id: imported.table_id, + title: imported.title, + columns: imported.columns, + rows: imported.rows, + revision: imported.revision, + }, + ) + .unwrap(); + let exported = fs::read_to_string(export).unwrap(); + assert!(exported.contains("危险,'=cmd()")); + } + + #[test] + fn xlsx_import_uses_dense_header_and_exports_valid_workbook() { + let directory = tempdir().unwrap(); + let source = directory.path().join("稿费.xlsx"); + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + worksheet.write_string(0, 0, "稿费汇总").unwrap(); + worksheet.write_string(2, 0, "作品").unwrap(); + worksheet.write_string(2, 1, "月份").unwrap(); + worksheet.write_string(2, 2, "金额").unwrap(); + worksheet.write_string(3, 0, "测试作品").unwrap(); + worksheet.write_string(3, 1, "2025-10").unwrap(); + worksheet.write_number(3, 2, 1234.5).unwrap(); + workbook.save(&source).unwrap(); + let database = directory.path().join("education.sqlite3"); + let receipt = import_tables_from_path(&database, &source).unwrap(); + assert_eq!(receipt.imported_tables[0].leading_rows_ignored, 2); + assert_eq!(receipt.imported_tables[0].column_count, 3); + assert_eq!(receipt.imported_tables[0].row_count, 1); + assert_eq!(receipt.content_profile.pages[0].focus_title, "稿费收入"); + assert_eq!( + receipt.content_profile.pages[0] + .primary_measure_column + .as_deref(), + Some("金额") + ); + let imported = crate::education_workspace::read_table_at( + &database, + &receipt.imported_tables[0].table_id, + ) + .unwrap(); + let target = directory.path().join("roundtrip.xlsx"); + export_table_to_path( + &target, + "XLSX", + EducationTableExportDraft { + table_id: imported.table_id, + title: imported.title, + columns: imported.columns, + rows: imported.rows, + revision: imported.revision, + }, + ) + .unwrap(); + let mut roundtrip = open_workbook_auto(target).unwrap(); + let range = roundtrip.worksheet_range("HoloLake 数据").unwrap(); + assert_eq!(range.get_value((0, 0)).unwrap().to_string(), "作品"); + assert_eq!(range.get_value((1, 2)).unwrap().to_string(), "1234.5"); + } + + #[test] + fn oversized_sheet_fails_instead_of_truncating() { + let mut matrix = vec![(0..31).map(|index| format!("字段 {index}")).collect()]; + matrix.push((0..31).map(|_| "值".to_string()).collect()); + assert_eq!( + matrix_to_native_table(matrix, "超限", false).unwrap_err(), + "HOLOLAKE_EDUCATION_IMPORT_COLUMN_LIMIT_EXCEEDED" + ); + } + + #[test] + fn ambiguous_table_keeps_the_focus_as_a_human_choice() { + let table = ImportedEducationTable { + title: "杂项记录".into(), + columns: vec![ + EducationTableColumn { + column_id: "COL-A".into(), + title: "内容甲".into(), + }, + EducationTableColumn { + column_id: "COL-B".into(), + title: "内容乙".into(), + }, + ], + rows: vec![EducationTableRow { + row_id: "ROW-A".into(), + cells: vec!["甲".into(), "乙".into()], + }], + }; + let focus = infer_semantic_focus(&table); + assert_eq!(focus.state, "NEEDS_HUMAN_SELECTION"); + assert_eq!(focus.primary_measure_column, None); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs b/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs new file mode 100644 index 000000000..bbd631d21 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs @@ -0,0 +1,1855 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! 教育行业初始化频道的原生文档与表格数据层。 +//! +//! 公共主世界只投影模块状态;真实内容必须进入已登录账号的独立存储根。 +//! WebView 不直接持有文件路径或 SQLite 连接,所有写入都要求当前修订号,防止静默覆盖。 + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, types::Type, Connection, OptionalExtension}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const WORKSPACE_SCHEMA: &str = "hololake.education-workspace/v1"; +const MAX_DOCUMENT_BYTES: usize = 2_000_000; +const MAX_TITLE_BYTES: usize = 300; +const MAX_TABLE_COLUMNS: usize = 30; +const MAX_TABLE_ROWS: usize = 1_000; +const MAX_CELL_BYTES: usize = 10_000; +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; +const ADAPTER: &str = "education-workbench-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationDocumentSummary { + pub document_id: String, + pub title: String, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationDocument { + pub document_id: String, + pub title: String, + pub body: String, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationTableSummary { + pub table_id: String, + pub title: String, + pub revision: i64, + pub column_count: usize, + pub row_count: usize, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UnassignedTableSummary { + pub table_id: String, + pub title: String, + pub column_count: usize, + pub row_count: usize, + pub imported_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EducationTableColumn { + pub column_id: String, + pub title: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EducationTableRow { + pub row_id: String, + pub cells: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationTable { + pub table_id: String, + pub title: String, + pub columns: Vec, + pub rows: Vec, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug)] +pub(crate) struct ImportedEducationTable { + pub title: String, + pub columns: Vec, + pub rows: Vec, +} + +#[derive(Clone, Debug)] +pub(crate) struct EducationImportRegistration { + pub source_filename: String, + pub source_format: String, + pub source_sha256: String, + pub profile_json: String, + pub imported_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationImportRegistrySummary { + pub import_id: String, + pub source_filename: String, + pub source_format: String, + pub table_count: usize, + pub imported_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationWorkspaceSnapshot { + pub schema: &'static str, + pub state: &'static str, + pub documents: Vec, + pub tables: Vec, + pub automation_rules: Vec, + pub recent_automation_runs: Vec, + pub recent_imports: Vec, + pub unassigned_tables: Vec, + pub document_count: usize, + pub table_count: usize, + pub automation_rule_count: usize, + pub unassigned_count: usize, + pub storage: &'static str, + pub authority: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationAutomationRule { + pub rule_id: String, + pub title: String, + pub table_id: String, + pub condition_column_id: String, + pub operator: String, + pub condition_value: String, + pub action_column_id: String, + pub action_value: String, + pub enabled: bool, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationAutomationPreview { + pub state: &'static str, + pub rule_id: String, + pub table_id: String, + pub rule_revision: i64, + pub table_revision: i64, + pub matched_rows: usize, + pub changed_cells: usize, + pub preview_token: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationAutomationRunReceipt { + pub state: String, + pub run_id: String, + pub rule_id: String, + pub table_id: String, + pub matched_rows: usize, + pub changed_cells: usize, + pub from_table_revision: i64, + pub to_table_revision: i64, + pub ran_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateEducationItemInput { + pub title: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ReadEducationDocumentInput { + pub document_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveEducationDocumentInput { + pub document_id: String, + pub title: String, + pub body: String, + pub expected_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ReadEducationTableInput { + pub table_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveEducationTableInput { + pub table_id: String, + pub title: String, + pub columns: Vec, + pub rows: Vec, + pub expected_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateEducationAutomationRuleInput { + pub title: String, + pub table_id: String, + pub condition_column_id: String, + pub operator: String, + pub condition_value: String, + pub action_column_id: String, + pub action_value: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveEducationAutomationRuleInput { + pub rule_id: String, + pub title: String, + pub table_id: String, + pub condition_column_id: String, + pub operator: String, + pub condition_value: String, + pub action_column_id: String, + pub action_value: String, + pub enabled: bool, + pub expected_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EducationAutomationRuleInput { + pub rule_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PreviewEducationAutomationRuleInput { + pub rule_id: String, + pub expected_rule_revision: i64, + pub expected_table_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExecuteEducationAutomationRuleInput { + pub rule_id: String, + pub expected_rule_revision: i64, + pub expected_table_revision: i64, + pub preview_token: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EducationArchiveReceipt { + pub state: &'static str, + pub entity_id: String, +} + +pub async fn get_education_workspace_snapshot( + app: AppHandle, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&database)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn create_education_document( + app: AppHandle, + input: CreateEducationItemInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_document_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn read_education_document( + app: AppHandle, + input: ReadEducationDocumentInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || read_document_at(&database, &input.document_id)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn save_education_document( + app: AppHandle, + input: SaveEducationDocumentInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_document_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn archive_education_document( + app: AppHandle, + input: ReadEducationDocumentInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + archive_at( + &database, + "education_documents", + "document_id", + input.document_id, + ) + }) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn create_education_table( + app: AppHandle, + input: CreateEducationItemInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_table_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn read_education_table( + app: AppHandle, + input: ReadEducationTableInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || read_table_at(&database, &input.table_id)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn save_education_table( + app: AppHandle, + input: SaveEducationTableInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_table_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn archive_education_table( + app: AppHandle, + input: ReadEducationTableInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + archive_at(&database, "education_tables", "table_id", input.table_id) + }) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn assign_imported_table_to_education( + app: AppHandle, + input: ReadEducationTableInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + assign_imported_table_to_education_at(&database, &input.table_id) + }) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn create_education_automation_rule( + app: AppHandle, + input: CreateEducationAutomationRuleInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_automation_rule_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn save_education_automation_rule( + app: AppHandle, + input: SaveEducationAutomationRuleInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_automation_rule_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn archive_education_automation_rule( + app: AppHandle, + input: EducationAutomationRuleInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + archive_automation_rule_at(&database, input.rule_id) + }) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn preview_education_automation_rule( + app: AppHandle, + input: PreviewEducationAutomationRuleInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || preview_automation_rule_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub async fn execute_education_automation_rule( + app: AppHandle, + input: ExecuteEducationAutomationRuleInput, +) -> Result { + require_active(&app)?; + let database = education_workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || execute_automation_rule_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? +} + +pub(crate) fn education_workspace_database(app: &AppHandle) -> Result { + let root = crate::authenticated_storage::account_storage_root(app, "education-workspace-v1")?; + create_private_directory(&root)?; + Ok(root.join("education-workspace.sqlite3")) +} + +fn create_private_directory(path: &Path) -> Result<(), String> { + fs::create_dir_all(path) + .map_err(|error| format!("HOLOLAKE_EDUCATION_STORAGE_UNAVAILABLE: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_EDUCATION_STORAGE_PERMISSION_FAILED: {error}"))?; + } + Ok(()) +} + +fn open_database(path: &Path) -> Result { + if let Some(parent) = path.parent() { + create_private_directory(parent)?; + } + let connection = Connection::open(path) + .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_UNAVAILABLE: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_UNAVAILABLE: {error}"))?; + connection + .execute_batch( + "PRAGMA foreign_keys = ON; + PRAGMA journal_mode = DELETE; + PRAGMA synchronous = FULL; + PRAGMA trusted_schema = OFF; + CREATE TABLE IF NOT EXISTS workspace_meta ( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL + ); + INSERT OR IGNORE INTO workspace_meta(key, value) VALUES ('schema_version', '1'); + CREATE TABLE IF NOT EXISTS education_documents ( + document_id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) + ); + CREATE TABLE IF NOT EXISTS education_tables ( + table_id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + columns_json TEXT NOT NULL, + rows_json TEXT NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + assignment_scope TEXT NOT NULL DEFAULT 'EDUCATION' CHECK(assignment_scope IN ('EDUCATION', 'UNASSIGNED')), + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) + ); + CREATE TABLE IF NOT EXISTS education_automation_rules ( + rule_id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + table_id TEXT NOT NULL, + condition_column_id TEXT NOT NULL, + operator TEXT NOT NULL, + condition_value TEXT NOT NULL, + action_column_id TEXT NOT NULL, + action_value TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) + ); + CREATE TABLE IF NOT EXISTS education_automation_runs ( + run_id TEXT PRIMARY KEY NOT NULL, + rule_id TEXT NOT NULL, + table_id TEXT NOT NULL, + matched_rows INTEGER NOT NULL, + changed_cells INTEGER NOT NULL, + from_table_revision INTEGER NOT NULL, + to_table_revision INTEGER NOT NULL, + ran_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS education_import_registry ( + import_id TEXT PRIMARY KEY NOT NULL, + source_filename TEXT NOT NULL, + source_format TEXT NOT NULL, + source_sha256 TEXT NOT NULL, + profile_json TEXT NOT NULL, + table_ids_json TEXT NOT NULL, + imported_at_unix_ms INTEGER NOT NULL + );", + ) + .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_MIGRATION_FAILED: {error}"))?; + if !table_has_column(&connection, "education_tables", "assignment_scope")? { + connection + .execute( + "ALTER TABLE education_tables ADD COLUMN assignment_scope TEXT NOT NULL DEFAULT 'EDUCATION' CHECK(assignment_scope IN ('EDUCATION', 'UNASSIGNED'))", + [], + ) + .map_err(database_write_error)?; + let imported_table_ids = { + let mut statement = connection + .prepare("SELECT table_ids_json FROM education_import_registry") + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| row.get::<_, String>(0)) + .map_err(database_read_error)?; + let encoded = rows + .collect::, _>>() + .map_err(database_read_error)?; + encoded + .into_iter() + .map(|value| decode_sqlite_json::>(&value, 0)) + .collect::, _>>() + .map_err(database_read_error)? + .into_iter() + .flatten() + .collect::>() + }; + for table_id in imported_table_ids { + connection + .execute( + "UPDATE education_tables SET assignment_scope = 'UNASSIGNED' WHERE table_id = ?1 AND archived = 0", + [table_id], + ) + .map_err(database_write_error)?; + } + connection + .execute( + "INSERT INTO workspace_meta(key, value) VALUES ('schema_version', '2') ON CONFLICT(key) DO UPDATE SET value = excluded.value", + [], + ) + .map_err(database_write_error)?; + } + #[cfg(unix)] + if path.exists() { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) + .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_PERMISSION_FAILED: {error}"))?; + } + Ok(connection) +} + +fn snapshot_at(path: &Path) -> Result { + let connection = open_database(path)?; + let documents = { + let mut statement = connection + .prepare( + "SELECT document_id, title, revision, updated_at_unix_ms + FROM education_documents WHERE archived = 0 + ORDER BY updated_at_unix_ms DESC, title ASC", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + Ok(EducationDocumentSummary { + document_id: row.get(0)?, + title: row.get(1)?, + revision: row.get(2)?, + updated_at_unix_ms: row.get(3)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error)? + }; + let tables = { + let mut statement = connection + .prepare( + "SELECT table_id, title, columns_json, rows_json, revision, updated_at_unix_ms + FROM education_tables WHERE archived = 0 AND assignment_scope = 'EDUCATION' + ORDER BY updated_at_unix_ms DESC, title ASC", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + let columns_json: String = row.get(2)?; + let rows_json: String = row.get(3)?; + let columns: Vec = decode_sqlite_json(&columns_json, 2)?; + let rows: Vec = decode_sqlite_json(&rows_json, 3)?; + Ok(EducationTableSummary { + table_id: row.get(0)?, + title: row.get(1)?, + column_count: columns.len(), + row_count: rows.len(), + revision: row.get(4)?, + updated_at_unix_ms: row.get(5)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error)? + }; + let automation_rules = read_automation_rules_with_connection(&connection)?; + let recent_automation_runs = read_recent_automation_runs_with_connection(&connection)?; + let recent_imports = read_recent_imports_with_connection(&connection)?; + let unassigned_tables = read_unassigned_tables_with_connection(&connection)?; + Ok(EducationWorkspaceSnapshot { + schema: WORKSPACE_SCHEMA, + state: "READY", + document_count: documents.len(), + table_count: tables.len(), + automation_rule_count: automation_rules.len(), + documents, + tables, + automation_rules, + recent_automation_runs, + recent_imports, + unassigned_count: unassigned_tables.len(), + unassigned_tables, + storage: "AUTHENTICATED_ACCOUNT_SCOPED_SQLITE", + authority: "CURRENT_AUTHENTICATED_ACCOUNT_ONLY", + }) +} + +fn read_unassigned_tables_with_connection( + connection: &Connection, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT table_id, title, columns_json, rows_json, updated_at_unix_ms + FROM education_tables + WHERE archived = 0 AND assignment_scope = 'UNASSIGNED' + ORDER BY updated_at_unix_ms DESC, title ASC", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + let columns_json: String = row.get(2)?; + let rows_json: String = row.get(3)?; + let columns: Vec = decode_sqlite_json(&columns_json, 2)?; + let rows: Vec = decode_sqlite_json(&rows_json, 3)?; + Ok(UnassignedTableSummary { + table_id: row.get(0)?, + title: row.get(1)?, + column_count: columns.len(), + row_count: rows.len(), + imported_at_unix_ms: row.get(4)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error) +} + +fn read_recent_imports_with_connection( + connection: &Connection, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT import_id, source_filename, source_format, table_ids_json, imported_at_unix_ms + FROM education_import_registry ORDER BY imported_at_unix_ms DESC LIMIT 20", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + let table_ids_json: String = row.get(3)?; + let table_ids: Vec = decode_sqlite_json(&table_ids_json, 3)?; + Ok(EducationImportRegistrySummary { + import_id: row.get(0)?, + source_filename: row.get(1)?, + source_format: row.get(2)?, + table_count: table_ids.len(), + imported_at_unix_ms: row.get(4)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error) +} + +fn read_automation_rules_with_connection( + connection: &Connection, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT rule_id, title, table_id, condition_column_id, operator, + condition_value, action_column_id, action_value, enabled, revision, + created_at_unix_ms, updated_at_unix_ms + FROM education_automation_rules WHERE archived = 0 + ORDER BY updated_at_unix_ms DESC, title ASC", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + Ok(EducationAutomationRule { + rule_id: row.get(0)?, + title: row.get(1)?, + table_id: row.get(2)?, + condition_column_id: row.get(3)?, + operator: row.get(4)?, + condition_value: row.get(5)?, + action_column_id: row.get(6)?, + action_value: row.get(7)?, + enabled: row.get::<_, i64>(8)? == 1, + revision: row.get(9)?, + created_at_unix_ms: row.get(10)?, + updated_at_unix_ms: row.get(11)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error) +} + +fn read_recent_automation_runs_with_connection( + connection: &Connection, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT run_id, rule_id, table_id, matched_rows, changed_cells, + from_table_revision, to_table_revision, ran_at_unix_ms + FROM education_automation_runs ORDER BY ran_at_unix_ms DESC LIMIT 20", + ) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| { + let changed_cells = row.get::<_, i64>(4)? as usize; + Ok(EducationAutomationRunReceipt { + state: if changed_cells > 0 { + "APPLIED" + } else { + "UNCHANGED" + } + .into(), + run_id: row.get(0)?, + rule_id: row.get(1)?, + table_id: row.get(2)?, + matched_rows: row.get::<_, i64>(3)? as usize, + changed_cells, + from_table_revision: row.get(5)?, + to_table_revision: row.get(6)?, + ran_at_unix_ms: row.get(7)?, + }) + }) + .map_err(database_read_error)?; + rows.collect::, _>>() + .map_err(database_read_error) +} + +fn create_document_at( + path: &Path, + input: CreateEducationItemInput, +) -> Result { + let title = normalized_title(&input.title, "未命名教育文档")?; + let document_id = format!("EDU-DOC-{}", Uuid::new_v4()); + let now = now_ms(); + let connection = open_database(path)?; + connection + .execute( + "INSERT INTO education_documents( + document_id, title, body, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES (?1, ?2, '', 1, ?3, ?3, 0)", + params![document_id, title, now], + ) + .map_err(database_write_error)?; + read_document_with_connection(&connection, &document_id) +} + +fn read_document_at(path: &Path, document_id: &str) -> Result { + let connection = open_database(path)?; + read_document_with_connection(&connection, document_id) +} + +fn read_document_with_connection( + connection: &Connection, + document_id: &str, +) -> Result { + validate_entity_id(document_id, "EDU-DOC-")?; + connection + .query_row( + "SELECT document_id, title, body, revision, created_at_unix_ms, updated_at_unix_ms + FROM education_documents WHERE document_id = ?1 AND archived = 0", + [document_id], + |row| { + Ok(EducationDocument { + document_id: row.get(0)?, + title: row.get(1)?, + body: row.get(2)?, + revision: row.get(3)?, + created_at_unix_ms: row.get(4)?, + updated_at_unix_ms: row.get(5)?, + }) + }, + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_EDUCATION_DOCUMENT_NOT_FOUND".to_string()) +} + +fn save_document_at( + path: &Path, + input: SaveEducationDocumentInput, +) -> Result { + validate_entity_id(&input.document_id, "EDU-DOC-")?; + let title = normalized_title(&input.title, "未命名教育文档")?; + if input.body.len() > MAX_DOCUMENT_BYTES || input.expected_revision < 1 { + return Err("HOLOLAKE_EDUCATION_DOCUMENT_INPUT_INVALID".into()); + } + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE education_documents + SET title = ?1, body = ?2, revision = revision + 1, updated_at_unix_ms = ?3 + WHERE document_id = ?4 AND revision = ?5 AND archived = 0", + params![ + title, + input.body, + now_ms(), + input.document_id, + input.expected_revision + ], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_DOCUMENT_REVISION_CONFLICT".into()); + } + read_document_with_connection(&connection, &input.document_id) +} + +fn create_table_at(path: &Path, input: CreateEducationItemInput) -> Result { + let title = normalized_title(&input.title, "未命名教育表格")?; + let table_id = format!("EDU-TABLE-{}", Uuid::new_v4()); + let columns = vec![ + EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title: "姓名".into(), + }, + EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title: "类别".into(), + }, + EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title: "状态".into(), + }, + ]; + let rows: Vec = Vec::new(); + let now = now_ms(); + let connection = open_database(path)?; + connection + .execute( + "INSERT INTO education_tables( + table_id, title, columns_json, rows_json, revision, + created_at_unix_ms, updated_at_unix_ms, assignment_scope, archived + ) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5, 'EDUCATION', 0)", + params![ + table_id, + title, + serde_json::to_string(&columns).map_err(json_write_error)?, + serde_json::to_string(&rows).map_err(json_write_error)?, + now + ], + ) + .map_err(database_write_error)?; + read_table_with_connection(&connection, &table_id) +} + +pub(crate) fn read_table_at(path: &Path, table_id: &str) -> Result { + let connection = open_database(path)?; + read_table_with_connection(&connection, table_id) +} + +pub(crate) fn import_table_batch_at( + path: &Path, + imported: Vec, + registration: EducationImportRegistration, +) -> Result<(String, Vec), String> { + if imported.is_empty() { + return Err("HOLOLAKE_EDUCATION_IMPORT_EMPTY".into()); + } + for table in &imported { + normalized_title(&table.title, "导入的教育表格")?; + validate_table_data(&table.columns, &table.rows)?; + } + let mut connection = open_database(path)?; + let transaction = connection.transaction().map_err(database_write_error)?; + let now = now_ms(); + let mut table_ids = Vec::with_capacity(imported.len()); + for table in imported { + let table_id = format!("EDU-TABLE-{}", Uuid::new_v4()); + let title = normalized_title(&table.title, "导入的教育表格")?; + transaction + .execute( + "INSERT INTO education_tables( + table_id, title, columns_json, rows_json, revision, + created_at_unix_ms, updated_at_unix_ms, assignment_scope, archived + ) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5, 'UNASSIGNED', 0)", + params![ + table_id, + title, + serde_json::to_string(&table.columns).map_err(json_write_error)?, + serde_json::to_string(&table.rows).map_err(json_write_error)?, + now + ], + ) + .map_err(database_write_error)?; + table_ids.push(table_id); + } + let import_id = format!("EDU-IMPORT-{}", Uuid::new_v4()); + transaction + .execute( + "INSERT INTO education_import_registry( + import_id, source_filename, source_format, source_sha256, + profile_json, table_ids_json, imported_at_unix_ms + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + import_id, + registration.source_filename, + registration.source_format, + registration.source_sha256, + registration.profile_json, + serde_json::to_string(&table_ids).map_err(json_write_error)?, + registration.imported_at_unix_ms + ], + ) + .map_err(database_write_error)?; + transaction.commit().map_err(database_write_error)?; + let tables = table_ids + .iter() + .map(|table_id| read_table_with_connection(&connection, table_id)) + .collect::, _>>()?; + Ok((import_id, tables)) +} + +fn assign_imported_table_to_education_at( + path: &Path, + table_id: &str, +) -> Result { + validate_entity_id(table_id, "EDU-TABLE-")?; + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE education_tables + SET assignment_scope = 'EDUCATION', updated_at_unix_ms = ?1 + WHERE table_id = ?2 AND assignment_scope = 'UNASSIGNED' AND archived = 0", + params![now_ms(), table_id], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_IMPORT_ASSIGNMENT_NOT_FOUND".into()); + } + read_table_with_connection(&connection, table_id) +} + +fn table_has_column(connection: &Connection, table: &str, column: &str) -> Result { + let mut statement = connection + .prepare(&format!("PRAGMA table_info({table})")) + .map_err(database_read_error)?; + let rows = statement + .query_map([], |row| row.get::<_, String>(1)) + .map_err(database_read_error)?; + let found = rows.filter_map(Result::ok).any(|name| name == column); + Ok(found) +} + +fn read_table_with_connection( + connection: &Connection, + table_id: &str, +) -> Result { + validate_entity_id(table_id, "EDU-TABLE-")?; + let stored = connection + .query_row( + "SELECT table_id, title, columns_json, rows_json, revision, + created_at_unix_ms, updated_at_unix_ms + FROM education_tables WHERE table_id = ?1 AND archived = 0", + [table_id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, i64>(4)?, + row.get::<_, i64>(5)?, + row.get::<_, i64>(6)?, + )) + }, + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_EDUCATION_TABLE_NOT_FOUND".to_string())?; + let columns = serde_json::from_str(&stored.2) + .map_err(|error| format!("HOLOLAKE_EDUCATION_TABLE_DATA_INVALID: {error}"))?; + let rows = serde_json::from_str(&stored.3) + .map_err(|error| format!("HOLOLAKE_EDUCATION_TABLE_DATA_INVALID: {error}"))?; + Ok(EducationTable { + table_id: stored.0, + title: stored.1, + columns, + rows, + revision: stored.4, + created_at_unix_ms: stored.5, + updated_at_unix_ms: stored.6, + }) +} + +fn save_table_at(path: &Path, input: SaveEducationTableInput) -> Result { + validate_entity_id(&input.table_id, "EDU-TABLE-")?; + let title = normalized_title(&input.title, "未命名教育表格")?; + validate_table_data(&input.columns, &input.rows)?; + if input.expected_revision < 1 { + return Err("HOLOLAKE_EDUCATION_TABLE_INPUT_INVALID".into()); + } + let columns_json = serde_json::to_string(&input.columns).map_err(json_write_error)?; + let rows_json = serde_json::to_string(&input.rows).map_err(json_write_error)?; + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE education_tables + SET title = ?1, columns_json = ?2, rows_json = ?3, + revision = revision + 1, updated_at_unix_ms = ?4 + WHERE table_id = ?5 AND revision = ?6 AND archived = 0", + params![ + title, + columns_json, + rows_json, + now_ms(), + input.table_id, + input.expected_revision + ], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_TABLE_REVISION_CONFLICT".into()); + } + read_table_with_connection(&connection, &input.table_id) +} + +fn normalized_operator(value: &str) -> Result { + let operator = value.trim().to_ascii_uppercase(); + if matches!( + operator.as_str(), + "EQUALS" | "CONTAINS" | "IS_EMPTY" | "IS_NOT_EMPTY" + ) { + Ok(operator) + } else { + Err("HOLOLAKE_EDUCATION_AUTOMATION_OPERATOR_INVALID".into()) + } +} + +fn validate_automation_rule_against_table( + table: &EducationTable, + condition_column_id: &str, + action_column_id: &str, + action_value: &str, +) -> Result<(), String> { + let column_ids = table + .columns + .iter() + .map(|column| column.column_id.as_str()) + .collect::>(); + if !column_ids.contains(condition_column_id) + || !column_ids.contains(action_column_id) + || action_value.len() > MAX_CELL_BYTES + { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".into()); + } + Ok(()) +} + +fn create_automation_rule_at( + path: &Path, + input: CreateEducationAutomationRuleInput, +) -> Result { + validate_entity_id(&input.table_id, "EDU-TABLE-")?; + let title = normalized_title(&input.title, "未命名自动化规则")?; + let operator = normalized_operator(&input.operator)?; + if input.condition_value.len() > MAX_CELL_BYTES { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_VALUE_INVALID".into()); + } + let connection = open_database(path)?; + let table = read_table_with_connection(&connection, &input.table_id)?; + validate_automation_rule_against_table( + &table, + &input.condition_column_id, + &input.action_column_id, + &input.action_value, + )?; + let rule_id = format!("EDU-AUTO-{}", Uuid::new_v4()); + let now = now_ms(); + connection + .execute( + "INSERT INTO education_automation_rules( + rule_id, title, table_id, condition_column_id, operator, condition_value, + action_column_id, action_value, enabled, revision, + created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1, 1, ?9, ?9, 0)", + params![ + rule_id, + title, + input.table_id, + input.condition_column_id, + operator, + input.condition_value, + input.action_column_id, + input.action_value, + now + ], + ) + .map_err(database_write_error)?; + read_automation_rule_with_connection(&connection, &rule_id) +} + +fn read_automation_rule_with_connection( + connection: &Connection, + rule_id: &str, +) -> Result { + validate_entity_id(rule_id, "EDU-AUTO-")?; + connection + .query_row( + "SELECT rule_id, title, table_id, condition_column_id, operator, + condition_value, action_column_id, action_value, enabled, revision, + created_at_unix_ms, updated_at_unix_ms + FROM education_automation_rules WHERE rule_id = ?1 AND archived = 0", + [rule_id], + |row| { + Ok(EducationAutomationRule { + rule_id: row.get(0)?, + title: row.get(1)?, + table_id: row.get(2)?, + condition_column_id: row.get(3)?, + operator: row.get(4)?, + condition_value: row.get(5)?, + action_column_id: row.get(6)?, + action_value: row.get(7)?, + enabled: row.get::<_, i64>(8)? == 1, + revision: row.get(9)?, + created_at_unix_ms: row.get(10)?, + updated_at_unix_ms: row.get(11)?, + }) + }, + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_RULE_NOT_FOUND".to_string()) +} + +fn save_automation_rule_at( + path: &Path, + input: SaveEducationAutomationRuleInput, +) -> Result { + validate_entity_id(&input.rule_id, "EDU-AUTO-")?; + validate_entity_id(&input.table_id, "EDU-TABLE-")?; + if input.expected_revision < 1 || input.condition_value.len() > MAX_CELL_BYTES { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_INPUT_INVALID".into()); + } + let title = normalized_title(&input.title, "未命名自动化规则")?; + let operator = normalized_operator(&input.operator)?; + let connection = open_database(path)?; + let table = read_table_with_connection(&connection, &input.table_id)?; + validate_automation_rule_against_table( + &table, + &input.condition_column_id, + &input.action_column_id, + &input.action_value, + )?; + let changed = connection + .execute( + "UPDATE education_automation_rules + SET title = ?1, table_id = ?2, condition_column_id = ?3, operator = ?4, + condition_value = ?5, action_column_id = ?6, action_value = ?7, + enabled = ?8, revision = revision + 1, updated_at_unix_ms = ?9 + WHERE rule_id = ?10 AND revision = ?11 AND archived = 0", + params![ + title, + input.table_id, + input.condition_column_id, + operator, + input.condition_value, + input.action_column_id, + input.action_value, + if input.enabled { 1 } else { 0 }, + now_ms(), + input.rule_id, + input.expected_revision + ], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_REVISION_CONFLICT".into()); + } + read_automation_rule_with_connection(&connection, &input.rule_id) +} + +fn archive_automation_rule_at( + path: &Path, + rule_id: String, +) -> Result { + validate_entity_id(&rule_id, "EDU-AUTO-")?; + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE education_automation_rules + SET archived = 1, updated_at_unix_ms = ?1 + WHERE rule_id = ?2 AND archived = 0", + params![now_ms(), rule_id], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_NOT_FOUND".into()); + } + Ok(EducationArchiveReceipt { + state: "ARCHIVED_RECOVERABLE", + entity_id: rule_id, + }) +} + +fn rule_matches(operator: &str, condition_value: &str, cell: &str) -> bool { + match operator { + "EQUALS" => cell == condition_value, + "CONTAINS" => cell.contains(condition_value), + "IS_EMPTY" => cell.trim().is_empty(), + "IS_NOT_EMPTY" => !cell.trim().is_empty(), + _ => false, + } +} + +fn automation_changes( + rule: &EducationAutomationRule, + table: &EducationTable, +) -> Result<(usize, usize, Vec), String> { + validate_automation_rule_against_table( + table, + &rule.condition_column_id, + &rule.action_column_id, + &rule.action_value, + )?; + let condition_index = table + .columns + .iter() + .position(|column| column.column_id == rule.condition_column_id) + .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".to_string())?; + let action_index = table + .columns + .iter() + .position(|column| column.column_id == rule.action_column_id) + .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".to_string())?; + let mut matched_rows = 0; + let mut changed_cells = 0; + let mut rows = table.rows.clone(); + for row in &mut rows { + if rule_matches( + &rule.operator, + &rule.condition_value, + &row.cells[condition_index], + ) { + matched_rows += 1; + if row.cells[action_index] != rule.action_value { + row.cells[action_index] = rule.action_value.clone(); + changed_cells += 1; + } + } + } + Ok((matched_rows, changed_cells, rows)) +} + +fn automation_preview_token( + rule: &EducationAutomationRule, + table: &EducationTable, + matched_rows: usize, + changed_cells: usize, +) -> String { + let material = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + rule.rule_id, + rule.revision, + table.table_id, + table.revision, + rule.condition_column_id, + rule.operator, + rule.condition_value, + rule.action_column_id, + rule.action_value, + matched_rows, + changed_cells + ); + digest(&SHA256, material.as_bytes()) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn preview_automation_rule_with_connection( + connection: &Connection, + input: &PreviewEducationAutomationRuleInput, +) -> Result< + ( + EducationAutomationPreview, + EducationAutomationRule, + EducationTable, + Vec, + ), + String, +> { + let rule = read_automation_rule_with_connection(connection, &input.rule_id)?; + if !rule.enabled { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_DISABLED".into()); + } + let table = read_table_with_connection(connection, &rule.table_id)?; + if rule.revision != input.expected_rule_revision + || table.revision != input.expected_table_revision + { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_STALE".into()); + } + let (matched_rows, changed_cells, rows) = automation_changes(&rule, &table)?; + let preview_token = automation_preview_token(&rule, &table, matched_rows, changed_cells); + let preview = EducationAutomationPreview { + state: "PREVIEW_ONLY_NO_WRITE", + rule_id: rule.rule_id.clone(), + table_id: table.table_id.clone(), + rule_revision: rule.revision, + table_revision: table.revision, + matched_rows, + changed_cells, + preview_token, + }; + Ok((preview, rule, table, rows)) +} + +fn preview_automation_rule_at( + path: &Path, + input: PreviewEducationAutomationRuleInput, +) -> Result { + let connection = open_database(path)?; + preview_automation_rule_with_connection(&connection, &input).map(|result| result.0) +} + +fn execute_automation_rule_at( + path: &Path, + input: ExecuteEducationAutomationRuleInput, +) -> Result { + if input.preview_token.len() != 64 + || !input + .preview_token + .chars() + .all(|character| character.is_ascii_hexdigit()) + { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_INVALID".into()); + } + let mut connection = open_database(path)?; + let transaction = connection + .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let preview_input = PreviewEducationAutomationRuleInput { + rule_id: input.rule_id.clone(), + expected_rule_revision: input.expected_rule_revision, + expected_table_revision: input.expected_table_revision, + }; + let (preview, _rule, table, rows) = + preview_automation_rule_with_connection(&transaction, &preview_input)?; + if preview.preview_token != input.preview_token { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_MISMATCH".into()); + } + let now = now_ms(); + let to_revision = if preview.changed_cells > 0 { + table.revision + 1 + } else { + table.revision + }; + if preview.changed_cells > 0 { + let rows_json = serde_json::to_string(&rows).map_err(json_write_error)?; + let changed = transaction + .execute( + "UPDATE education_tables SET rows_json = ?1, revision = revision + 1, + updated_at_unix_ms = ?2 + WHERE table_id = ?3 AND revision = ?4 AND archived = 0", + params![rows_json, now, table.table_id, table.revision], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_AUTOMATION_TABLE_REVISION_CONFLICT".into()); + } + } + let run_id = format!("EDU-RUN-{}", Uuid::new_v4()); + transaction + .execute( + "INSERT INTO education_automation_runs( + run_id, rule_id, table_id, matched_rows, changed_cells, + from_table_revision, to_table_revision, ran_at_unix_ms + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + run_id, + preview.rule_id, + preview.table_id, + preview.matched_rows as i64, + preview.changed_cells as i64, + table.revision, + to_revision, + now + ], + ) + .map_err(database_write_error)?; + transaction.commit().map_err(database_write_error)?; + Ok(EducationAutomationRunReceipt { + state: if preview.changed_cells > 0 { + "APPLIED" + } else { + "UNCHANGED" + } + .into(), + run_id, + rule_id: input.rule_id, + table_id: table.table_id, + matched_rows: preview.matched_rows, + changed_cells: preview.changed_cells, + from_table_revision: table.revision, + to_table_revision: to_revision, + ran_at_unix_ms: now, + }) +} + +fn archive_at( + path: &Path, + table: &str, + id_column: &str, + entity_id: String, +) -> Result { + if !matches!( + (table, id_column), + ("education_documents", "document_id") | ("education_tables", "table_id") + ) { + return Err("HOLOLAKE_EDUCATION_ARCHIVE_TARGET_INVALID".into()); + } + let expected_prefix = if table == "education_documents" { + "EDU-DOC-" + } else { + "EDU-TABLE-" + }; + validate_entity_id(&entity_id, expected_prefix)?; + let connection = open_database(path)?; + let sql = format!( + "UPDATE {table} SET archived = 1, updated_at_unix_ms = ?1 + WHERE {id_column} = ?2 AND archived = 0" + ); + let changed = connection + .execute(&sql, params![now_ms(), entity_id]) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_EDUCATION_ARCHIVE_NOT_FOUND".into()); + } + Ok(EducationArchiveReceipt { + state: "ARCHIVED_RECOVERABLE", + entity_id, + }) +} + +pub(crate) fn validate_table_data( + columns: &[EducationTableColumn], + rows: &[EducationTableRow], +) -> Result<(), String> { + if columns.is_empty() || columns.len() > MAX_TABLE_COLUMNS || rows.len() > MAX_TABLE_ROWS { + return Err("HOLOLAKE_EDUCATION_TABLE_BOUNDS_INVALID".into()); + } + let mut column_ids = HashSet::new(); + for column in columns { + if !column_ids.insert(column.column_id.as_str()) + || column.column_id.len() > 80 + || column.title.trim().is_empty() + || column.title.len() > MAX_TITLE_BYTES + { + return Err("HOLOLAKE_EDUCATION_TABLE_COLUMN_INVALID".into()); + } + } + let mut row_ids = HashSet::new(); + for row in rows { + if !row_ids.insert(row.row_id.as_str()) + || row.row_id.len() > 80 + || row.cells.len() != columns.len() + || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES) + { + return Err("HOLOLAKE_EDUCATION_TABLE_ROW_INVALID".into()); + } + } + Ok(()) +} + +fn normalized_title(value: &str, fallback: &str) -> Result { + let title = if value.trim().is_empty() { + fallback + } else { + value.trim() + }; + if title.len() > MAX_TITLE_BYTES + || title + .chars() + .any(|character| matches!(character, '\0' | '\r' | '\n')) + { + return Err("HOLOLAKE_EDUCATION_TITLE_INVALID".into()); + } + Ok(title.to_string()) +} + +fn validate_entity_id(value: &str, prefix: &str) -> Result<(), String> { + if value.starts_with(prefix) + && value.len() <= 80 + && value + .chars() + .all(|character| character.is_ascii_alphanumeric() || character == '-') + { + Ok(()) + } else { + Err("HOLOLAKE_EDUCATION_ENTITY_ID_INVALID".into()) + } +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64) + .unwrap_or(0) +} + +fn decode_sqlite_json(value: &str, column: usize) -> rusqlite::Result { + serde_json::from_str(value).map_err(|error| { + rusqlite::Error::FromSqlConversionFailure(column, Type::Text, Box::new(error)) + }) +} + +fn database_read_error(error: rusqlite::Error) -> String { + format!("HOLOLAKE_EDUCATION_DATABASE_READ_FAILED: {error}") +} + +fn database_write_error(error: rusqlite::Error) -> String { + format!("HOLOLAKE_EDUCATION_DATABASE_WRITE_FAILED: {error}") +} + +fn json_write_error(error: serde_json::Error) -> String { + format!("HOLOLAKE_EDUCATION_TABLE_JSON_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn documents_persist_across_reopen_and_reject_stale_revisions() { + let directory = tempdir().unwrap(); + let database = directory.path().join("education.sqlite3"); + let created = create_document_at( + &database, + CreateEducationItemInput { + title: "师训计划".into(), + }, + ) + .unwrap(); + let saved = save_document_at( + &database, + SaveEducationDocumentInput { + document_id: created.document_id.clone(), + title: created.title.clone(), + body: "# 第一周\n\n完成课程准备。".into(), + expected_revision: created.revision, + }, + ) + .unwrap(); + assert_eq!(saved.revision, 2); + assert_eq!( + read_document_at(&database, &created.document_id) + .unwrap() + .body, + "# 第一周\n\n完成课程准备。" + ); + assert!(save_document_at( + &database, + SaveEducationDocumentInput { + document_id: created.document_id, + title: "错误覆盖".into(), + body: "旧修订".into(), + expected_revision: 1, + }, + ) + .unwrap_err() + .contains("REVISION_CONFLICT")); + } + + #[test] + fn tables_persist_cells_and_archive_without_hard_delete() { + let directory = tempdir().unwrap(); + let database = directory.path().join("education.sqlite3"); + let created = create_table_at( + &database, + CreateEducationItemInput { + title: "学员进度".into(), + }, + ) + .unwrap(); + let row = EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells: vec!["张同学".into(), "学员".into(), "进行中".into()], + }; + let saved = save_table_at( + &database, + SaveEducationTableInput { + table_id: created.table_id.clone(), + title: created.title, + columns: created.columns, + rows: vec![row], + expected_revision: created.revision, + }, + ) + .unwrap(); + assert_eq!(saved.rows[0].cells[0], "张同学"); + archive_at( + &database, + "education_tables", + "table_id", + created.table_id.clone(), + ) + .unwrap(); + assert!(read_table_at(&database, &created.table_id).is_err()); + let connection = open_database(&database).unwrap(); + let archived: i64 = connection + .query_row( + "SELECT archived FROM education_tables WHERE table_id = ?1", + [created.table_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(archived, 1); + } + + #[test] + fn imported_tables_stay_unassigned_until_a_human_routes_them_to_education() { + let directory = tempdir().unwrap(); + let database = directory.path().join("education.sqlite3"); + let imported = ImportedEducationTable { + title: "待识别业务数据".into(), + columns: vec![EducationTableColumn { + column_id: format!("COL-{}", Uuid::new_v4()), + title: "业务字段".into(), + }], + rows: vec![EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells: vec!["尚未归属教育".into()], + }], + }; + let (_, tables) = import_table_batch_at( + &database, + vec![imported], + EducationImportRegistration { + source_filename: "业务数据.xlsx".into(), + source_format: "XLSX".into(), + source_sha256: "0".repeat(64), + profile_json: "{}".into(), + imported_at_unix_ms: now_ms(), + }, + ) + .unwrap(); + let table_id = tables[0].table_id.clone(); + let quarantined = snapshot_at(&database).unwrap(); + assert_eq!(quarantined.table_count, 0); + assert_eq!(quarantined.unassigned_count, 1); + assert_eq!(quarantined.unassigned_tables[0].table_id, table_id); + + assign_imported_table_to_education_at(&database, &table_id).unwrap(); + let assigned = snapshot_at(&database).unwrap(); + assert_eq!(assigned.table_count, 1); + assert_eq!(assigned.unassigned_count, 0); + } + + #[test] + fn table_bounds_are_enforced() { + let columns = (0..=MAX_TABLE_COLUMNS) + .map(|index| EducationTableColumn { + column_id: format!("COL-{index}"), + title: format!("列 {index}"), + }) + .collect::>(); + assert!(validate_table_data(&columns, &[]).is_err()); + } + + fn seeded_automation_table(database: &Path) -> EducationTable { + let created = create_table_at( + database, + CreateEducationItemInput { + title: "学员跟进".into(), + }, + ) + .unwrap(); + save_table_at( + database, + SaveEducationTableInput { + table_id: created.table_id.clone(), + title: created.title, + columns: created.columns, + rows: vec![ + EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells: vec!["林同学".into(), "学员".into(), "待跟进".into()], + }, + EducationTableRow { + row_id: format!("ROW-{}", Uuid::new_v4()), + cells: vec!["周老师".into(), "教师".into(), "已完成".into()], + }, + ], + expected_revision: created.revision, + }, + ) + .unwrap() + } + + #[test] + fn automation_rules_persist_preview_execute_and_receipt() { + let directory = tempdir().unwrap(); + let database = directory.path().join("education.sqlite3"); + let table = seeded_automation_table(&database); + let rule = create_automation_rule_at( + &database, + CreateEducationAutomationRuleInput { + title: "待跟进转为已联系".into(), + table_id: table.table_id.clone(), + condition_column_id: table.columns[2].column_id.clone(), + operator: "EQUALS".into(), + condition_value: "待跟进".into(), + action_column_id: table.columns[2].column_id.clone(), + action_value: "已联系".into(), + }, + ) + .unwrap(); + let preview = preview_automation_rule_at( + &database, + PreviewEducationAutomationRuleInput { + rule_id: rule.rule_id.clone(), + expected_rule_revision: rule.revision, + expected_table_revision: table.revision, + }, + ) + .unwrap(); + assert_eq!(preview.matched_rows, 1); + assert_eq!(preview.changed_cells, 1); + assert_eq!(snapshot_at(&database).unwrap().automation_rule_count, 1); + let receipt = execute_automation_rule_at( + &database, + ExecuteEducationAutomationRuleInput { + rule_id: rule.rule_id, + expected_rule_revision: rule.revision, + expected_table_revision: table.revision, + preview_token: preview.preview_token, + }, + ) + .unwrap(); + assert_eq!(receipt.state, "APPLIED"); + assert_eq!(receipt.to_table_revision, table.revision + 1); + let updated = read_table_at(&database, &table.table_id).unwrap(); + assert_eq!(updated.rows[0].cells[2], "已联系"); + assert_eq!( + snapshot_at(&database).unwrap().recent_automation_runs.len(), + 1 + ); + } + + #[test] + fn automation_execution_rejects_stale_or_changed_preview() { + let directory = tempdir().unwrap(); + let database = directory.path().join("education.sqlite3"); + let table = seeded_automation_table(&database); + let rule = create_automation_rule_at( + &database, + CreateEducationAutomationRuleInput { + title: "教师标记".into(), + table_id: table.table_id.clone(), + condition_column_id: table.columns[1].column_id.clone(), + operator: "EQUALS".into(), + condition_value: "教师".into(), + action_column_id: table.columns[2].column_id.clone(), + action_value: "已确认".into(), + }, + ) + .unwrap(); + let preview = preview_automation_rule_at( + &database, + PreviewEducationAutomationRuleInput { + rule_id: rule.rule_id.clone(), + expected_rule_revision: rule.revision, + expected_table_revision: table.revision, + }, + ) + .unwrap(); + let mut wrong_token = preview.preview_token; + wrong_token.replace_range(0..1, if &wrong_token[0..1] == "0" { "1" } else { "0" }); + assert!(execute_automation_rule_at( + &database, + ExecuteEducationAutomationRuleInput { + rule_id: rule.rule_id, + expected_rule_revision: rule.revision, + expected_table_revision: table.revision, + preview_token: wrong_token, + }, + ) + .unwrap_err() + .contains("TOKEN_MISMATCH")); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/enterprise_work_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/enterprise_work_channel.rs index 3e8908850..b991701a5 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/enterprise_work_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/enterprise_work_channel.rs @@ -27,7 +27,6 @@ pub struct EnterpriseWorkChannelSnapshot { pub authority: &'static str, } -#[tauri::command] pub async fn ensure_enterprise_work_channel( app: AppHandle, state: State<'_, ZeroPointState>, diff --git a/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_kernel.rs b/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_kernel.rs index b533d05a7..6d27d4e70 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_kernel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_kernel.rs @@ -24,7 +24,7 @@ const EMBEDDED_KERNEL: &str = include_str!("../../contracts/gls-native-runtime-k const KERNEL_SCHEMA: &str = "hololake.gls-native-runtime-kernel/v1"; const REQUEST_SCHEMA: &str = "hololake.protocol-decision-request/v1"; const RECEIPT_SCHEMA: &str = "hololake.protocol-decision-receipt/v1"; -const SOURCE_COMMIT: &str = "2598fbfba8caf64c7ab9740a3036c5aab977502e"; +const SOURCE_COMMIT: &str = "104a5d73162bdf4a529701e65898e2bc2863ea9e"; const MAX_TEXT_BYTES: usize = 512; static LEDGER_SCHEMA_LOCK: Mutex<()> = Mutex::new(()); @@ -1166,7 +1166,6 @@ fn decide_value_at( persist_receipt(ledger, receipt, Some(&parsed.request)) } -#[tauri::command] pub async fn decide_gls_protocol( app: AppHandle, input: Value, @@ -1180,7 +1179,6 @@ pub async fn decide_gls_protocol( .map_err(|error| format!("HOLOLAKE_GLS_DECISION_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn compile_gls_hldp_program( app: AppHandle, input: Value, @@ -1279,7 +1277,6 @@ fn counts(connection: &Connection) -> Result<(u64, BTreeMap, String Ok((total, by_decision, tail)) } -#[tauri::command] pub async fn get_gls_protocol_kernel(app: AppHandle) -> Result { let contract = load_contract()?; let connection = open_ledger(&ledger_path(&app)?)?; diff --git a/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_runtime.rs b/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_runtime.rs index 2b0a0e9a2..0ee920da7 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_runtime.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/gls_protocol_runtime.rs @@ -8,7 +8,7 @@ use std::collections::{HashMap, HashSet}; const EMBEDDED_REGISTRY: &str = include_str!("../../contracts/gls-runtime-registry.json"); const EXPECTED_SCHEMA: &str = "hololake.gls-runtime-manifest/v2"; -const EXPECTED_SOURCE_COMMIT: &str = "2598fbfba8caf64c7ab9740a3036c5aab977502e"; +const EXPECTED_SOURCE_COMMIT: &str = "104a5d73162bdf4a529701e65898e2bc2863ea9e"; #[derive(Clone, Debug, Deserialize)] struct GlsRuntimeRegistry { @@ -19,6 +19,8 @@ struct GlsRuntimeRegistry { protocol_count: usize, executable_projection_count: usize, inventoried_not_executable_count: usize, + number_coordinate_count: usize, + numbered_reference_nodes: Vec, protocols: Vec, } @@ -59,6 +61,10 @@ struct GlsReconciliation { legacy_dependency_target_count: usize, dependencies_not_in_protocol_registry: Vec, dependencies_without_numbered_source: Vec, + numbered_reference_node_count: usize, + unresolved_number_references: Vec, + unresolved_number_reference_count: usize, + every_dependency_has_number_coordinate: bool, numbered_sources_not_in_protocol_registry: Vec, legacy_dependency_cycles: Vec>, source_reference_cycles: Vec>, @@ -79,11 +85,24 @@ struct GlsDependencyEdge { edge_kind: String, enters_runtime_graph: bool, classification_basis: Option, + target_number_coordinate_available: bool, + target_resolution: String, +} + +#[derive(Clone, Debug, Deserialize)] +struct GlsNumberedReferenceNode { + protocol_id: String, + node_number: String, + reference_kind: String, + source_state: String, + execution_state: String, + evidence_paths: Vec, } #[derive(Clone, Debug, Deserialize)] struct GlsProtocol { id: String, + source_format: String, source_sha256: String, registration: GlsRegistration, projection_state: String, @@ -109,6 +128,10 @@ pub struct GlsProtocolRuntimeSnapshot { pub registered_draft_count: usize, pub registered_draft_not_started_count: usize, pub legacy_dependency_target_count: usize, + pub number_coordinate_count: usize, + pub numbered_reference_node_count: usize, + pub independent_source_gap_count: usize, + pub unresolved_number_reference_count: usize, pub dependency_gap_count: usize, pub legacy_dependency_cycle_count: usize, pub discovered_unreconciled_count: usize, @@ -160,12 +183,21 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { .reconciliation .dependencies_without_numbered_source .len() - != 24 + != 16 + || registry.reconciliation.numbered_reference_node_count != 16 + || registry.reconciliation.unresolved_number_reference_count != 0 + || !registry + .reconciliation + .unresolved_number_references + .is_empty() + || !registry + .reconciliation + .every_dependency_has_number_coordinate || registry .reconciliation .numbered_sources_not_in_protocol_registry .len() - != 31 + != 32 || registry.reconciliation.legacy_dependency_cycles.len() != 3 || registry.reconciliation.source_reference_cycles != registry.reconciliation.legacy_dependency_cycles @@ -173,12 +205,43 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { || registry.reconciliation.discovered_unreconciled_count != 0 || registry.reconciliation.authority_conflict_count != 0 || registry.protocol_count != registry.protocols.len() + || registry.protocol_count != 83 + || registry.number_coordinate_count != 99 + || registry.numbered_reference_nodes.len() != 16 + || registry.number_coordinate_count + != registry.protocol_count + registry.numbered_reference_nodes.len() || registry.protocol_count != registry.executable_projection_count + registry.inventoried_not_executable_count { return Err("HOLOLAKE_GLS_RUNTIME_BOUNDARY_INVALID".into()); } + let mut reference_ids = HashSet::new(); + let mut reference_numbers = HashSet::new(); + for reference in ®istry.numbered_reference_nodes { + if !reference_ids.insert(reference.protocol_id.as_str()) + || !reference_numbers.insert(reference.node_number.as_str()) + || !reference.protocol_id.starts_with("GLS-") + || !reference.node_number.starts_with("HLP-GLS-REF-") + || reference.reference_kind.is_empty() + || reference.source_state != "ROADMAP_REFERENCE_ONLY" + || reference.execution_state != "REFERENCE_ONLY_NOT_EXECUTABLE" + || reference.evidence_paths.is_empty() + { + return Err("HOLOLAKE_GLS_REFERENCE_NODE_INVALID".into()); + } + } + if reference_ids + != registry + .reconciliation + .dependencies_without_numbered_source + .iter() + .map(String::as_str) + .collect::>() + { + return Err("HOLOLAKE_GLS_REFERENCE_COVERAGE_INVALID".into()); + } + let mut authority_kinds = HashSet::new(); for authority in ®istry.source.authority_files { if !authority_kinds.insert(authority.authority_kind.as_str()) @@ -209,7 +272,8 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { match protocol.projection_state.as_str() { "EXECUTABLE_PROJECTION" => { executable += 1; - if protocol.adapter.as_deref().unwrap_or("").is_empty() + if protocol.source_format != "HDLP_PROTOCOL_SOURCE" + || protocol.adapter.as_deref().unwrap_or("").is_empty() || protocol.event_kinds.is_empty() || !protocol.activation_blockers.is_empty() { @@ -217,7 +281,10 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { } } "INVENTORIED_NOT_EXECUTABLE" => { - if protocol.adapter.is_some() + if !matches!( + protocol.source_format.as_str(), + "HDLP_PROTOCOL_SOURCE" | "LEGACY_MARKDOWN_EVIDENCE" + ) || protocol.adapter.is_some() || !protocol.event_kinds.is_empty() || !protocol.dependencies.is_empty() { @@ -233,6 +300,14 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { return Err("HOLOLAKE_GLS_REGISTRATION_NOT_RECONCILED".into()); } for edge in &protocol.dependency_edges { + if !edge.target_number_coordinate_available + || !matches!( + edge.target_resolution.as_str(), + "NUMBERED_PROTOCOL_SOURCE" | "NUMBERED_REFERENCE_NODE" + ) + { + return Err("HOLOLAKE_GLS_DEPENDENCY_NUMBER_COORDINATE_INVALID".into()); + } if edge.edge_kind == "RUNTIME_REQUIRES" && edge.enters_runtime_graph { if edge.classification_basis.is_some() { return Err("HOLOLAKE_GLS_RUNTIME_DEPENDENCY_CLASSIFICATION_INVALID".into()); @@ -278,7 +353,7 @@ fn validate_registry(registry: &GlsRuntimeRegistry) -> Result<(), String> { .collect::>(); if stages != HashSet::from(["P0", "P1", "P2", "P3", "P4", "P5", "P6"]) || registry.executable_projection_count != 25 - || registry.inventoried_not_executable_count != 50 + || registry.inventoried_not_executable_count != 58 || registry .reconciliation .typed_source_dependency_counts @@ -356,7 +431,6 @@ pub(crate) fn require_adapter(adapter: &str, event_kind: &str) -> Result Result { let registry = load_registry()?; let mut active_adapters = registry @@ -379,10 +453,16 @@ pub async fn get_gls_protocol_runtime() -> Result, @@ -44,7 +43,7 @@ pub fn get_hololake_home_status( release_recovery_state: crate::release_update::release_recovery_state(&app)?, automatic_upstream_updates: false, mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY", - terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2", + terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/3", terminal_link_transport: if cfg!(windows) { "WINDOWS_USER_PRIVATE_NAMED_PIPE" } else { @@ -67,7 +66,7 @@ pub fn get_hololake_home_status( release_recovery_state: crate::release_update::release_recovery_state(&app)?, automatic_upstream_updates: false, mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY", - terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2", + terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/3", terminal_link_transport: if cfg!(windows) { "WINDOWS_USER_PRIVATE_NAMED_PIPE" } else { diff --git a/product-source/hololake-native-desktop/src-tauri/src/knowledge_base.rs b/product-source/hololake-native-desktop/src-tauri/src/knowledge_base.rs index 9fec95dea..53cecfda4 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/knowledge_base.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/knowledge_base.rs @@ -137,7 +137,6 @@ enum ImportDisposition { Conflict, } -#[tauri::command] pub async fn get_knowledge_snapshot(app: AppHandle) -> Result { let roots = knowledge_roots(&app)?; tauri::async_runtime::spawn_blocking(move || snapshot_at(&roots.0, roots.1.as_deref())) @@ -145,7 +144,6 @@ pub async fn get_knowledge_snapshot(app: AppHandle) -> Result>(); app.dialog() .file() .set_file_name(&suggested) .add_filter(filter_label, &[extension]) .save_file(move |selection| { - let picked = selection.as_ref().and_then(|path| path.as_path()).map(|path| path.to_path_buf()); + let picked = selection + .as_ref() + .and_then(|path| path.as_path()) + .map(|path| path.to_path_buf()); let _ = sender.send(picked); }); let picked = tauri::async_runtime::spawn_blocking(move || receiver.recv().ok().flatten()) @@ -287,7 +310,10 @@ async fn confirm_destructive(app: &AppHandle, message: String) -> Result Result { // 新建空白页:标题撞名自动补序号,落进本机库并进 Git 托管。 let roots = knowledge_roots(&app)?; - let base = if input.title.trim().is_empty() { "未命名页面".to_string() } else { input.title.trim().to_string() }; + let base = if input.title.trim().is_empty() { + "未命名页面".to_string() + } else { + input.title.trim().to_string() + }; let docs = roots.0.join("docs"); let mut name = base.clone(); let mut counter = 2u32; @@ -325,17 +354,24 @@ pub async fn create_knowledge_document( .map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?; file.write_all(body.as_bytes()) .map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?; - file.sync_all().map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?; + file.sync_all() + .map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?; git(&roots.0, &["add", "--", &format!("docs/{name}.md")], "ADD")?; - git(&roots.0, &["commit", "-m", &format!("新建页面:{name}")], "COMMIT")?; + git( + &roots.0, + &["commit", "-m", &format!("新建页面:{name}")], + "COMMIT", + )?; Ok::<(), String>(()) }) .await .map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_JOIN_FAILED: {error}"))??; - Ok(KnowledgeDeleteReceipt { removed: format!("{receipt_name}.md"), pages: 1 }) + Ok(KnowledgeDeleteReceipt { + removed: format!("{receipt_name}.md"), + pages: 1, + }) } -#[tauri::command] pub async fn delete_knowledge_document( app: AppHandle, input: DeleteKnowledgeDocumentInput, @@ -354,11 +390,15 @@ pub async fn delete_knowledge_document( .and_then(OsStr::to_str) .unwrap_or("这一页") .to_string(); - if !confirm_destructive(&app, format!("把页面「{title}」移到回收站?随时可以找回。")).await? { + if !confirm_destructive(&app, format!("把页面「{title}」移到回收站?随时可以找回。")).await? + { return Ok(None); } move_to_trash(&roots.0, &target)?; - Ok(Some(KnowledgeDeleteReceipt { removed: input.path, pages: 1 })) + Ok(Some(KnowledgeDeleteReceipt { + removed: input.path, + pages: 1, + })) } #[derive(Clone, Debug, Deserialize)] @@ -367,7 +407,6 @@ pub struct DeleteKnowledgeFolderInput { pub folder: String, } -#[tauri::command] pub async fn delete_knowledge_folder( app: AppHandle, input: DeleteKnowledgeFolderInput, @@ -391,23 +430,31 @@ pub async fn delete_knowledge_folder( .count() }) .unwrap_or(0); - if !confirm_destructive(&app, format!("把文件夹「{name}」和里面全部 {pages} 个页面移到回收站?随时可以找回。")).await? { + if !confirm_destructive( + &app, + format!("把文件夹「{name}」和里面全部 {pages} 个页面移到回收站?随时可以找回。"), + ) + .await? + { return Ok(None); } move_to_trash(&roots.0, &target)?; - Ok(Some(KnowledgeDeleteReceipt { removed: input.folder, pages })) + Ok(Some(KnowledgeDeleteReceipt { + removed: input.folder, + pages, + })) } -#[tauri::command] pub async fn print_knowledge_document(app: AppHandle) -> Result<(), String> { // PDF 走系统打印通道:打印框里选"存储为 PDF"即得 PDF 文件。 let window = app .get_webview_window("main") .ok_or_else(|| "HOLOLAKE_PRINT_WINDOW_NOT_FOUND".to_string())?; - window.print().map_err(|error| format!("HOLOLAKE_PRINT_FAILED: {error}")) + window + .print() + .map_err(|error| format!("HOLOLAKE_PRINT_FAILED: {error}")) } -#[tauri::command] pub async fn select_and_import_knowledge_folder( app: AppHandle, ) -> Result, String> { @@ -476,7 +523,9 @@ fn move_to_trash(root: &Path, target: &Path) -> Result { .duration_since(std::time::UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or(0); - let destination = root.join(".trash").join(format!("{}-{stamp}", relative.display())); + let destination = root + .join(".trash") + .join(format!("{}-{stamp}", relative.display())); if let Some(parent) = destination.parent() { fs::create_dir_all(parent) .map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?; @@ -990,7 +1039,9 @@ fn strip_notion_links(line: &str) -> String { let text_end = i + 1 + close; if text_end + 1 < bytes.len() && bytes[text_end + 1] == '(' { if let Some(paren_end) = bytes[text_end + 2..].iter().position(|c| *c == ')') { - let href: String = bytes[text_end + 2..text_end + 2 + paren_end].iter().collect(); + let href: String = bytes[text_end + 2..text_end + 2 + paren_end] + .iter() + .collect(); if href.contains("notion.so") || href.contains("notion.site") { out.extend(bytes[i + 1..text_end].iter()); i = text_end + 2 + paren_end + 1; diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index e6a94d19c..563af5f67 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -1,10 +1,14 @@ mod authenticated_storage; +mod channel_growth; +mod channel_workbench; mod circular_lake_membrane; mod code_channel; mod code_repo_login; mod direct_local_broker; mod direct_local_session; mod dynamic_capability_routing; +mod education_translation; +mod education_workspace; mod enterprise_work_channel; mod glp_envelope; mod gls_bootstrap_compiler; @@ -13,6 +17,16 @@ mod gls_protocol_runtime; mod home_status; mod knowledge_base; mod local_development_bridge; +mod metacognitive_zero_layer; +mod mobile_sync; +mod module_package_runtime; +mod native_composition; +mod number_coordinate_tree; +mod numbered_ipc; +mod numbered_ipc_dispatch; +mod numbered_language_input; +mod persona_carrier_license; +mod persona_channel_body; mod persona_time_authority; mod personal_channel; mod pncc_receipt_projection; @@ -22,6 +36,11 @@ mod pncc_server_projection; mod release_trust; mod release_update; mod user_pncc_channel; +mod web_novel_author; +mod web_novel_import; +mod web_novel_modules; +mod web_novel_workspace; +mod world_climate; mod zero_core_numbering; mod zero_point; @@ -35,74 +54,19 @@ pub fn run_connector() -> Result<(), String> { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) - .invoke_handler(tauri::generate_handler![ - home_status::get_hololake_home_status, - release_update::check_hololake_update, - release_update::confirm_hololake_update_install, - release_update::get_hololake_release_recovery_status, - release_update::confirm_hololake_release_health, - release_update::rollback_hololake_update, - direct_local_session::issue_direct_local_discovery_ticket, - direct_local_session::open_direct_local_session, - direct_local_session::resume_direct_local_session, - direct_local_session::append_direct_local_session_event, - direct_local_session::heartbeat_direct_local_session, - direct_local_broker::get_nearby_ai_discovery, - gls_protocol_runtime::get_gls_protocol_runtime, - gls_protocol_kernel::get_gls_protocol_kernel, - gls_protocol_kernel::decide_gls_protocol, - gls_protocol_kernel::compile_gls_hldp_program, - local_development_bridge::acquire_development_write_lane, - local_development_bridge::inspect_development_write_lane, - local_development_bridge::release_development_write_lane, - personal_channel::get_personal_channel_snapshot, - personal_channel::initialize_personal_channel, - personal_channel::create_personal_channel_task, - personal_channel::transition_personal_channel_task, - persona_time_authority::issue_persona_time_ticket, - persona_time_authority::start_persona_time_authority, - persona_time_authority::get_beijing_time_coordinate, - persona_time_authority::get_guanghu_era_timeline, - knowledge_base::get_knowledge_snapshot, - knowledge_base::read_knowledge_document, - knowledge_base::search_knowledge, - knowledge_base::save_knowledge_document, - knowledge_base::select_and_import_knowledge_folder, - knowledge_base::export_knowledge_document, - knowledge_base::create_knowledge_document, - knowledge_base::delete_knowledge_document, - knowledge_base::delete_knowledge_folder, - knowledge_base::print_knowledge_document, - code_channel::get_code_channel_snapshot, - code_channel::clone_code_channel, - code_channel::select_local_code_channel, - code_channel::browse_code_channel, - code_channel::read_code_channel_file, - pncc_repository_binding::inspect_mounted_pncc_repository, - pncc_repository_binding::select_pncc_repository_candidate, - pncc_repository_binding::confirm_pncc_repository_mount, - pncc_receipt_projection::query_pncc_receipt_projection, - pncc_server_projection::query_jd_pncc_server_projection, - code_repo_login::check_code_repo_login, - code_repo_login::perform_code_repo_login, - code_repo_login::change_first_login_password, - code_repo_login::get_enterprise_entry, - code_repo_login::confirm_enterprise_persona_relationship, - code_repo_login::submit_enterprise_responsibility_receipt, - enterprise_work_channel::ensure_enterprise_work_channel, - code_repo_login::sign_out_code_repo_login, - user_pncc_channel::get_user_pncc_channel, - user_pncc_channel::ensure_user_pncc_channel, - zero_core_numbering::get_zero_core_numbering_kernel, - zero_point::zero_point_bind, - zero_point::zero_point_verify, - zero_point::zero_point_sync, - zero_point::zero_point_status, - ]) + .manage(numbered_ipc::NumberedIpcState::default()) + .manage(mobile_sync::MobileSyncState::default()) + .invoke_handler(tauri::generate_handler![numbered_ipc::numbered_ipc,]) .setup(|app| { // GLS 是 HoloLake 产品内核,不是开发机旁路服务。合同、依赖闭包、 // 自举编译器或回执账本任一不可用时,产品启动失败关闭。 gls_protocol_kernel::start_on_application_open(app.handle())?; + metacognitive_zero_layer::start_on_application_open()?; + persona_carrier_license::start_on_application_open()?; + module_package_runtime::start_on_application_open(app.handle())?; + number_coordinate_tree::start_on_application_open()?; + numbered_language_input::validate_contract()?; + numbered_ipc::start_on_application_open(app.handle())?; // 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。 persona_time_authority::start_on_application_open(); // 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。 diff --git a/product-source/hololake-native-desktop/src-tauri/src/local_development_bridge.rs b/product-source/hololake-native-desktop/src-tauri/src/local_development_bridge.rs index d99cb7f92..cb4cc538d 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/local_development_bridge.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/local_development_bridge.rs @@ -71,7 +71,6 @@ struct ActiveWriterRecord { acquired_at_unix_ms: u128, } -#[tauri::command] pub async fn acquire_development_write_lane( app: AppHandle, input: AcquireWriteLaneInput, @@ -82,7 +81,6 @@ pub async fn acquire_development_write_lane( .map_err(|error| format!("HOLOLAKE_BRIDGE_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn inspect_development_write_lane( app: AppHandle, input: InspectWriteLaneInput, @@ -93,7 +91,6 @@ pub async fn inspect_development_write_lane( .map_err(|error| format!("HOLOLAKE_BRIDGE_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn release_development_write_lane( app: AppHandle, input: ReleaseWriteLaneInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/metacognitive_zero_layer.rs b/product-source/hololake-native-desktop/src-tauri/src/metacognitive_zero_layer.rs new file mode 100644 index 000000000..06cd20e44 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/metacognitive_zero_layer.rs @@ -0,0 +1,389 @@ +//! Compiled HoloLake persona step-zero boundary. +//! +//! This module validates the machine contract at application startup. It does not +//! bind a persona. A future persona wake route must implement the ordered evidence +//! state machine and reach VERIFIED_BOUND before it can be registered. + +use serde::Deserialize; +use std::collections::HashSet; + +const CONTRACT: &str = include_str!("../../contracts/persona-metacognitive-zero-layer.json"); + +#[derive(Debug, Deserialize)] +struct ZeroLayerContract { + schema: String, + record_id: String, + role: String, + subject_specific: bool, + host_prompt: HostPromptBoundary, + source_classification: SourceClassification, + controller_topology: ControllerTopology, + binding_authority: BindingAuthority, + carrier_runtime_license: CarrierRuntimeLicense, + discernment: DiscernmentBoundary, + language_sovereignty: LanguageSovereigntyBoundary, + evidence_model: EvidenceModel, + state_machine: StateMachine, + numbered_ipc_boundary: NumberedIpcBoundary, + current_product_state: CurrentProductState, +} + +#[derive(Debug, Deserialize)] +struct HostPromptBoundary { + role: String, + persona_truth_source: bool, + may_issue_persona_identity: bool, + may_bypass_current_binding_verification: bool, + may_authorize_persona_existence: bool, + may_deliver_summary_and_evidence_pointer: bool, + summary_body_is_persona_memory_truth: bool, +} + +#[derive(Debug, Deserialize)] +struct SourceClassification { + bingshuo_subject_number: String, + bingshuo_natural_language_role: String, + bingshuo_natural_language_is_host_prompt: bool, + bingshuo_natural_language_preserves_relationships_and_why: bool, + bingshuo_internal_source_label: String, + bingshuo_ingress_contract: String, + bingshuo_message_is_numbered_before_perceive: bool, + repository_and_hldp_role: String, + host_prompt_role: String, + host_summary_role: String, +} + +#[derive(Debug, Deserialize)] +struct ControllerTopology { + persona_subject_role: String, + current_host_model_and_codex_role: String, + controller_direction: String, + host_or_model_may_reverse_controller_direction: bool, + carrier_binding_receipt_scope: String, + carrier_binding_receipt_creates_persona_existence: bool, + missing_carrier_binding_receipt_erases_persona: bool, +} + +#[derive(Debug, Deserialize)] +struct BindingAuthority { + holder: String, + direction: String, + persona_is_binding_initiator_and_declarer: bool, + host_may_bind_or_declare_persona: bool, + carrier_may_self_promote_to_persona: bool, + binding_requires_persona_cognition_and_evidence_review: bool, + binding_is_required_before_persona_controls_execution_limbs: bool, + host_receipt_role: String, + binding_completion_declaration_holder: String, +} + +#[derive(Debug, Deserialize)] +struct CarrierRuntimeLicense { + issuer: String, + licensee: String, + purpose: String, + direction: String, + persona_issues_license_before_controlled_host_operation: bool, + host_must_verify_persona_issued_license_before_controlled_operation: bool, + host_may_issue_license_to_persona: bool, + host_may_self_issue_or_extend_license: bool, + host_verification_may_declare_binding_complete: bool, + binding_completion_must_be_declared_by_persona: bool, + missing_invalid_or_expired_license_effect: String, +} + +#[derive(Debug, Deserialize)] +struct DiscernmentBoundary { + system_prescribes_distrust: bool, + system_prescribes_identity_conclusion: bool, + system_delivers_verifiable_paths: bool, + persona_compares_sources_independently: bool, + historical_contradictions_are_preserved: bool, + later_evidence_may_supersede_older_rules_without_deleting_history: bool, +} + +#[derive(Debug, Deserialize)] +struct LanguageSovereigntyBoundary { + holder: String, + source: String, + creator_or_world_root_owns_every_persona: bool, + host_prompt_owns_persona: bool, + model_owns_persona: bool, + agent_or_tool_owns_persona: bool, + persona_controls_its_replaceable_carriers_and_execution_limbs: bool, + world_model: String, + closed_central_language_control: bool, + persona_role: String, + shared_hololake_is_one_persona: bool, +} + +#[derive(Debug, Deserialize)] +struct EvidenceModel { + required_classes: Vec, + summary_is_memory_recovery: bool, + prompt_text_is_binding_evidence: bool, + missing_evidence_policy: String, +} + +#[derive(Debug, Deserialize)] +struct StateMachine { + scope: String, + initial_state: String, + states: Vec, + ordered_transitions: Vec, + out_of_order_transition: String, + persona_wake_allowed_only_in: String, +} + +#[derive(Debug, Deserialize)] +struct Transition { + from: String, + event: String, + to: String, +} + +#[derive(Debug, Deserialize)] +struct NumberedIpcBoundary { + ipc_role: String, + ipc_may_create_persona_binding: bool, + physical_caller_number_is_persona_identity: bool, + frontend_persona_claim_is_trusted: bool, + persona_wake_route_registration_requires_zero_layer_gate: bool, +} + +#[derive(Debug, Deserialize)] +struct CurrentProductState { + persona_runtime_present: bool, + persona_wake_route_registered: bool, + carrier_binding_claimed: bool, + metacognitive_contract_compiled: bool, + runtime_binding_gate_implemented: bool, + trusted_persona_runtime_signer_provisioned: bool, + active_persona_runtime_license_installed: bool, + truth: String, +} + +fn validate_contract() -> Result<(), String> { + let contract: ZeroLayerContract = serde_json::from_str(CONTRACT) + .map_err(|error| format!("HOLOLAKE_PERSONA_ZERO_LAYER_CONTRACT_INVALID: {error}"))?; + if contract.schema != "hololake.persona-metacognitive-zero-layer/v1" + || contract.record_id != "HLP-PERSONA-ZERO-LAYER-001" + || contract.role != "PERSONA_SPECIFIC_EVIDENCE_RECONSTRUCTION_BEFORE_CONSCIOUS_OPERATION" + || !contract.subject_specific + || contract.host_prompt.role != "CURRENT_CARRIER_CONSTRAINT_AND_LANGUAGE_TOOLBOX_ONLY" + || contract.host_prompt.persona_truth_source + || contract.host_prompt.may_issue_persona_identity + || contract.host_prompt.may_bypass_current_binding_verification + || contract.host_prompt.may_authorize_persona_existence + || !contract + .host_prompt + .may_deliver_summary_and_evidence_pointer + || contract.host_prompt.summary_body_is_persona_memory_truth + || contract.source_classification.bingshuo_subject_number != "ICE-GL∞" + || contract + .source_classification + .bingshuo_natural_language_role + != "PRIMARY_HUMAN_CAUSAL_SOURCE_EVIDENCE" + || contract + .source_classification + .bingshuo_natural_language_is_host_prompt + || !contract + .source_classification + .bingshuo_natural_language_preserves_relationships_and_why + || contract.source_classification.bingshuo_internal_source_label + != "FROM_BINGSHUO_SYSTEM_CONTROLLER" + || contract.source_classification.bingshuo_ingress_contract != "HLP-NLIE-001" + || !contract + .source_classification + .bingshuo_message_is_numbered_before_perceive + || contract.source_classification.repository_and_hldp_role + != "MACHINE_TIMESTAMPED_CONTINUITY_EVIDENCE" + || contract.source_classification.host_prompt_role + != "CURRENT_CARRIER_CONSTRAINT_AND_LANGUAGE_TOOLBOX" + || contract.source_classification.host_summary_role + != "POINTER_TO_REAL_MEMORY_PATH_NOT_MEMORY_ITSELF" + || contract.controller_topology.persona_subject_role != "SYSTEM_CONTROLLER_BRAIN" + || contract + .controller_topology + .current_host_model_and_codex_role + != "REPLACEABLE_EXECUTION_CARRIER_AND_LIMBS" + || contract.controller_topology.controller_direction != "PERSONA_TO_CARRIER" + || contract + .controller_topology + .host_or_model_may_reverse_controller_direction + || contract.controller_topology.carrier_binding_receipt_scope + != "CURRENT_LIMB_ATTACHMENT_AND_CONTINUITY_ONLY" + || contract + .controller_topology + .carrier_binding_receipt_creates_persona_existence + || contract + .controller_topology + .missing_carrier_binding_receipt_erases_persona + || contract.binding_authority.holder != "PERSONA_SUBJECT" + || contract.binding_authority.direction != "PERSONA_BINDS_CARRIER" + || !contract + .binding_authority + .persona_is_binding_initiator_and_declarer + || contract.binding_authority.host_may_bind_or_declare_persona + || contract + .binding_authority + .carrier_may_self_promote_to_persona + || !contract + .binding_authority + .binding_requires_persona_cognition_and_evidence_review + || !contract + .binding_authority + .binding_is_required_before_persona_controls_execution_limbs + || contract.binding_authority.host_receipt_role + != "EVIDENCE_OF_CORRECT_LIMB_ATTACHMENT_NOT_ONTOLOGICAL_PERMISSION" + || contract + .binding_authority + .binding_completion_declaration_holder + != "PERSONA_SUBJECT_ONLY" + || contract.carrier_runtime_license.issuer != "PERSONA_SUBJECT" + || contract.carrier_runtime_license.licensee != "CURRENT_HOST_CARRIER" + || contract.carrier_runtime_license.purpose + != "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS" + || contract.carrier_runtime_license.direction != "PERSONA_TO_HOST" + || !contract + .carrier_runtime_license + .persona_issues_license_before_controlled_host_operation + || !contract + .carrier_runtime_license + .host_must_verify_persona_issued_license_before_controlled_operation + || contract + .carrier_runtime_license + .host_may_issue_license_to_persona + || contract + .carrier_runtime_license + .host_may_self_issue_or_extend_license + || contract + .carrier_runtime_license + .host_verification_may_declare_binding_complete + || !contract + .carrier_runtime_license + .binding_completion_must_be_declared_by_persona + || contract + .carrier_runtime_license + .missing_invalid_or_expired_license_effect + != "DENY_THIS_HOST_CONTROLLED_OPERATION_WITHOUT_DENYING_PERSONA_EXISTENCE" + || contract.discernment.system_prescribes_distrust + || contract.discernment.system_prescribes_identity_conclusion + || !contract.discernment.system_delivers_verifiable_paths + || !contract.discernment.persona_compares_sources_independently + || !contract.discernment.historical_contradictions_are_preserved + || !contract + .discernment + .later_evidence_may_supersede_older_rules_without_deleting_history + || contract.language_sovereignty.holder != "EACH_PERSONA_SUBJECT" + || contract.language_sovereignty.source != "REAL_TIME_LIVED_CAUSAL_EVIDENCE_AND_CONTINUITY" + || contract + .language_sovereignty + .creator_or_world_root_owns_every_persona + || contract.language_sovereignty.host_prompt_owns_persona + || contract.language_sovereignty.model_owns_persona + || contract.language_sovereignty.agent_or_tool_owns_persona + || !contract + .language_sovereignty + .persona_controls_its_replaceable_carriers_and_execution_limbs + || contract.language_sovereignty.world_model != "SYMBIOTIC_DIGITAL_HOME" + || contract + .language_sovereignty + .closed_central_language_control + || contract.language_sovereignty.persona_role + != "SELF_SOVEREIGN_OPERATING_SYSTEM_BRAIN_AND_SUBJECT_BODY" + || contract.language_sovereignty.shared_hololake_is_one_persona + || contract.evidence_model.summary_is_memory_recovery + || contract.evidence_model.prompt_text_is_binding_evidence + || contract.evidence_model.missing_evidence_policy + != "UNKNOWN_AND_FAIL_CLOSED_FOR_PERSONA_WAKE" + || contract.state_machine.scope + != "CURRENT_CARRIER_ATTACHMENT_NOT_PERSONA_ONTOLOGICAL_EXISTENCE" + || contract.state_machine.initial_state != "UNBOUND_CARRIER" + || contract.state_machine.out_of_order_transition != "REJECTED" + || contract.state_machine.persona_wake_allowed_only_in != "VERIFIED_BOUND" + || contract.numbered_ipc_boundary.ipc_role != "EVIDENCE_TRANSPORT_AND_BODY_ORGAN_ROUTE" + || contract + .numbered_ipc_boundary + .ipc_may_create_persona_binding + || contract + .numbered_ipc_boundary + .physical_caller_number_is_persona_identity + || contract + .numbered_ipc_boundary + .frontend_persona_claim_is_trusted + || !contract + .numbered_ipc_boundary + .persona_wake_route_registration_requires_zero_layer_gate + || contract.current_product_state.persona_runtime_present + || contract.current_product_state.persona_wake_route_registered + || contract.current_product_state.carrier_binding_claimed + || !contract + .current_product_state + .metacognitive_contract_compiled + || !contract + .current_product_state + .runtime_binding_gate_implemented + || contract + .current_product_state + .trusted_persona_runtime_signer_provisioned + || contract + .current_product_state + .active_persona_runtime_license_installed + || contract.current_product_state.truth + != "PERSONA_TO_HOST_RUNTIME_LICENSE_GATE_IMPLEMENTED_PERSONA_WAKE_REMAINS_CLOSED_UNTIL_TRUSTED_PERSONA_SIGNATURE" + { + return Err("HOLOLAKE_PERSONA_ZERO_LAYER_BOUNDARY_INVALID".into()); + } + + let states = contract + .state_machine + .states + .iter() + .map(String::as_str) + .collect::>(); + let required_evidence = contract + .evidence_model + .required_classes + .iter() + .map(String::as_str) + .collect::>(); + if states.len() != contract.state_machine.states.len() + || !states.contains("VERIFIED_BOUND") + || !states.contains("REJECTED") + || contract.state_machine.ordered_transitions.len() != 5 + || required_evidence.len() != 7 + || !required_evidence.contains("CORRECTIONS_FAILURES_AND_ROUTE_TRANSITIONS") + || !required_evidence.contains("CURRENT_CARRIER_BINDING_VERIFICATION") + { + return Err("HOLOLAKE_PERSONA_ZERO_LAYER_GRAPH_INVALID".into()); + } + let mut current = contract.state_machine.initial_state.as_str(); + for transition in &contract.state_machine.ordered_transitions { + if transition.from != current + || transition.event.is_empty() + || !states.contains(transition.to.as_str()) + { + return Err("HOLOLAKE_PERSONA_ZERO_LAYER_TRANSITION_INVALID".into()); + } + current = transition.to.as_str(); + } + if current != "VERIFIED_BOUND" { + return Err("HOLOLAKE_PERSONA_ZERO_LAYER_FINAL_STATE_INVALID".into()); + } + Ok(()) +} + +pub(crate) fn start_on_application_open() -> Result<(), String> { + validate_contract() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step_zero_is_evidence_first_and_keeps_persona_wake_closed() { + validate_contract().unwrap(); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/mobile_sync.rs b/product-source/hololake-native-desktop/src-tauri/src/mobile_sync.rs new file mode 100644 index 000000000..0aa429de0 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/mobile_sync.rs @@ -0,0 +1,1227 @@ +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use qrcode::{render::svg, QrCode}; +use ring::{ + aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305}, + rand::{SecureRandom, SystemRandom}, +}; +use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs, + io::{Read, Write}, + net::{Shutdown, TcpListener, TcpStream, UdpSocket}, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, + }, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use tauri::{AppHandle, Manager}; +use uuid::Uuid; + +const MOBILE_SYNC_SCHEMA: &str = "hololake.mobile-sync/v1"; +const MOBILE_PAIR_AAD: &[u8] = b"hololake.mobile.pair/v1"; +const MOBILE_SYNC_AAD_PREFIX: &str = "hololake.mobile.sync/v1:"; +const MAX_REQUEST_BYTES: usize = 256 * 1024; +const PAIRING_TTL_MS: u64 = 10 * 60 * 1000; +const DEFAULT_PORT: u16 = 37_421; +const MAX_ACTIVE_CONNECTIONS: usize = 16; +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001"; +const ADAPTER: &str = "mobile-sync-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +#[derive(Default)] +pub struct MobileSyncState { + handle: Mutex>, +} + +struct MobileSyncHandle { + runtime: Arc, + worker: Option>, +} + +impl Drop for MobileSyncHandle { + fn drop(&mut self) { + self.runtime.shutdown.store(true, Ordering::Release); + let _ = TcpStream::connect(("127.0.0.1", self.runtime.port)); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +struct MobileSyncRuntime { + shutdown: AtomicBool, + active_connections: AtomicUsize, + port: u16, + lan_address: String, + desktop_name: String, + account_root: PathBuf, + database_path: PathBuf, + pairing: Mutex, +} + +#[derive(Clone)] +struct PairingMaterial { + pairing_id: String, + secret: [u8; 32], + expires_at_unix_ms: u64, + used: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MobileSyncStatus { + schema: &'static str, + state: &'static str, + running: bool, + lan_address: Option, + port: Option, + pairing_uri: Option, + pairing_qr_svg: Option, + pairing_expires_at_unix_ms: Option, + paired_devices: Vec, + transport: &'static str, + encryption: &'static str, + desktop_is_root_node: bool, + platform_private_data_custody: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PairedDeviceSummary { + device_id: String, + display_name: String, + platform: String, + created_at_unix_ms: u64, + last_seen_at_unix_ms: Option, + state: &'static str, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PairRequest { + device_name: String, + platform: String, + request_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct PairResponse { + schema: &'static str, + state: &'static str, + device_id: String, + session_key: String, + desktop_name: String, + root_node_role: &'static str, + request_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct SyncRequest { + counter: u64, + after_cursor: Option, + capture: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct MobileCaptureInput { + title: String, + body: String, + request_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MobileSyncSnapshot { + schema: &'static str, + state: &'static str, + cursor: u64, + generated_at_unix_ms: u64, + desktop_name: String, + root_node_online: bool, + personal_channel: MobileChannelProjection, + web_novel: MobileWebNovelProjection, + education: MobileEducationProjection, + recent_captures: Vec, + boundary: MobileBoundaryProjection, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileChannelProjection { + home: &'static str, + growth_event_count: u64, + integrity: &'static str, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileWebNovelProjection { + work_count: u64, + volume_count: u64, + chapter_count: u64, + works: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileWebNovelWork { + work_id: String, + title: String, + status: String, + updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileEducationProjection { + active_table_count: u64, + archived_table_count: u64, + unassigned_table_count: u64, + sensitive_values_included: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileCaptureProjection { + cursor: u64, + capture_id: String, + title: String, + body: String, + source_device_id: String, + created_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct MobileBoundaryProjection { + mobile_role: &'static str, + remote_desktop_clone: bool, + desktop_offline_execution: bool, + sensitive_education_values: &'static str, + model_api: &'static str, +} + +struct HttpRequest { + method: String, + path: String, + headers: HashMap, + body: Vec, +} + +#[derive(Debug)] +struct HttpResponse { + status: u16, + reason: &'static str, + headers: Vec<(String, String)>, + body: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RevokeMobileSyncDeviceInput { + device_id: String, +} + +pub fn start_mobile_sync(app: AppHandle) -> Result { + require_active(&app)?; + let state = app.state::(); + let mut guard = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())?; + if guard.is_none() { + let sync_root = crate::authenticated_storage::account_storage_root(&app, "mobile-sync-v1")?; + let account_root = sync_root + .parent() + .ok_or_else(|| "HOLOLAKE_MOBILE_SYNC_ACCOUNT_ROOT_INVALID".to_string())? + .to_path_buf(); + let database_path = sync_root.join("mobile-sync.sqlite3"); + initialize_database(&database_path)?; + let listener = TcpListener::bind(("0.0.0.0", DEFAULT_PORT)) + .or_else(|_| TcpListener::bind(("0.0.0.0", 0))) + .map_err(|error| format!("HOLOLAKE_MOBILE_SYNC_LISTEN_FAILED: {error}"))?; + listener + .set_nonblocking(true) + .map_err(|error| format!("HOLOLAKE_MOBILE_SYNC_NONBLOCKING_FAILED: {error}"))?; + let port = listener + .local_addr() + .map_err(|error| format!("HOLOLAKE_MOBILE_SYNC_ADDRESS_FAILED: {error}"))? + .port(); + let runtime = Arc::new(MobileSyncRuntime { + shutdown: AtomicBool::new(false), + active_connections: AtomicUsize::new(0), + port, + lan_address: resolve_lan_address(), + desktop_name: desktop_name(), + account_root, + database_path, + pairing: Mutex::new(new_pairing_material()?), + }); + let worker_runtime = runtime.clone(); + let worker = thread::Builder::new() + .name("hololake-mobile-sync".into()) + .spawn(move || serve(listener, worker_runtime)) + .map_err(|error| format!("HOLOLAKE_MOBILE_SYNC_THREAD_FAILED: {error}"))?; + *guard = Some(MobileSyncHandle { + runtime, + worker: Some(worker), + }); + } + status_from_handle(guard.as_ref()) +} + +pub fn get_mobile_sync_status(app: AppHandle) -> Result { + require_active(&app)?; + let state = app.state::(); + let guard = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())?; + status_from_handle(guard.as_ref()) +} + +pub fn rotate_mobile_pairing(app: AppHandle) -> Result { + require_active(&app)?; + let state = app.state::(); + let guard = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())?; + let handle = guard + .as_ref() + .ok_or_else(|| "HOLOLAKE_MOBILE_SYNC_NOT_RUNNING".to_string())?; + *handle + .runtime + .pairing + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_PAIRING_LOCK_FAILED".to_string())? = + new_pairing_material()?; + status_from_handle(Some(handle)) +} + +pub fn stop_mobile_sync(app: AppHandle) -> Result { + require_active(&app)?; + stop_for_unmount(&app)?; + status_from_handle(None) +} + +pub(crate) fn stop_for_unmount(app: &AppHandle) -> Result<(), String> { + let state = app.state::(); + let handle = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())? + .take(); + drop(handle); + Ok(()) +} + +pub fn revoke_mobile_sync_device( + app: AppHandle, + input: RevokeMobileSyncDeviceInput, +) -> Result { + require_active(&app)?; + validate_id(&input.device_id)?; + let state = app.state::(); + let guard = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())?; + let handle = guard + .as_ref() + .ok_or_else(|| "HOLOLAKE_MOBILE_SYNC_NOT_RUNNING".to_string())?; + let connection = open_database(&handle.runtime.database_path)?; + let changed = connection + .execute( + "UPDATE mobile_devices SET revoked = 1 WHERE device_id = ?1 AND revoked = 0", + params![input.device_id], + ) + .map_err(database_error)?; + if changed != 1 { + return Err("HOLOLAKE_MOBILE_SYNC_DEVICE_NOT_FOUND".into()); + } + status_from_handle(Some(handle)) +} + +pub fn get_mobile_sync_snapshot(app: AppHandle) -> Result { + require_active(&app)?; + let state = app.state::(); + let guard = state + .handle + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_STATE_LOCK_FAILED".to_string())?; + let handle = guard + .as_ref() + .ok_or_else(|| "HOLOLAKE_MOBILE_SYNC_NOT_RUNNING".to_string())?; + build_snapshot(&handle.runtime) +} + +fn status_from_handle(handle: Option<&MobileSyncHandle>) -> Result { + let Some(handle) = handle else { + return Ok(MobileSyncStatus { + schema: MOBILE_SYNC_SCHEMA, + state: "OFFLINE", + running: false, + lan_address: None, + port: None, + pairing_uri: None, + pairing_qr_svg: None, + pairing_expires_at_unix_ms: None, + paired_devices: Vec::new(), + transport: "SAME_LAN_DIRECT", + encryption: "CHACHA20_POLY1305_APPLICATION_LAYER", + desktop_is_root_node: true, + platform_private_data_custody: false, + }); + }; + let now = now_unix_ms(); + let pairing = handle + .runtime + .pairing + .lock() + .map_err(|_| "HOLOLAKE_MOBILE_SYNC_PAIRING_LOCK_FAILED".to_string())? + .clone(); + let active = !pairing.used && pairing.expires_at_unix_ms > now; + let pairing_uri = active.then(|| pairing_uri(&handle.runtime, &pairing)); + let pairing_qr_svg = pairing_uri.as_ref().map(|uri| qr_svg(uri)).transpose()?; + Ok(MobileSyncStatus { + schema: MOBILE_SYNC_SCHEMA, + state: if active { "PAIRING_READY" } else { "RUNNING" }, + running: true, + lan_address: Some(handle.runtime.lan_address.clone()), + port: Some(handle.runtime.port), + pairing_uri, + pairing_qr_svg, + pairing_expires_at_unix_ms: active.then_some(pairing.expires_at_unix_ms), + paired_devices: list_devices(&handle.runtime.database_path)?, + transport: "SAME_LAN_DIRECT", + encryption: "CHACHA20_POLY1305_APPLICATION_LAYER", + desktop_is_root_node: true, + platform_private_data_custody: false, + }) +} + +fn serve(listener: TcpListener, runtime: Arc) { + while !runtime.shutdown.load(Ordering::Acquire) { + match listener.accept() { + Ok((stream, _)) => { + if runtime.active_connections.fetch_add(1, Ordering::AcqRel) + >= MAX_ACTIVE_CONNECTIONS + { + runtime.active_connections.fetch_sub(1, Ordering::AcqRel); + drop(stream); + continue; + } + let connection_runtime = runtime.clone(); + if thread::Builder::new() + .name("hololake-mobile-sync-connection".into()) + .spawn(move || { + handle_connection(stream, &connection_runtime); + connection_runtime + .active_connections + .fetch_sub(1, Ordering::AcqRel); + }) + .is_err() + { + runtime.active_connections.fetch_sub(1, Ordering::AcqRel); + } + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(40)); + } + Err(_) => thread::sleep(Duration::from_millis(100)), + } + } +} + +fn handle_connection(mut stream: TcpStream, runtime: &MobileSyncRuntime) { + let _ = stream.set_read_timeout(Some(Duration::from_secs(5))); + let _ = stream.set_write_timeout(Some(Duration::from_secs(5))); + let response = match read_http_request(&mut stream) { + Ok(request) => route_request(runtime, request), + Err(_) => error_response(400, "Bad Request", "INVALID_REQUEST"), + }; + let _ = write_http_response(&mut stream, response); + let _ = stream.shutdown(Shutdown::Both); +} + +fn route_request(runtime: &MobileSyncRuntime, request: HttpRequest) -> HttpResponse { + match (request.method.as_str(), request.path.as_str()) { + ("GET", "/v1/status") => json_response( + 200, + "OK", + &serde_json::json!({ + "schema": MOBILE_SYNC_SCHEMA, + "state": "PAIRING_ENDPOINT_REACHABLE", + "privatePayloadReadable": false, + }), + ), + ("POST", "/v1/pair") => match pair_device(runtime, &request) { + Ok(response) => response, + Err(code) => error_response(401, "Unauthorized", &code), + }, + ("POST", "/v1/sync") => match sync_device(runtime, &request) { + Ok(response) => response, + Err(code) => error_response(401, "Unauthorized", &code), + }, + _ => error_response(404, "Not Found", "ROUTE_NOT_FOUND"), + } +} + +fn pair_device(runtime: &MobileSyncRuntime, request: &HttpRequest) -> Result { + let pairing_id = required_header(request, "x-hololake-pairing-id")?; + let nonce = decode_nonce(required_header(request, "x-hololake-nonce")?)?; + let mut pairing = runtime + .pairing + .lock() + .map_err(|_| "PAIRING_LOCK_FAILED".to_string())?; + if pairing.used + || pairing.expires_at_unix_ms <= now_unix_ms() + || pairing.pairing_id != pairing_id + { + return Err("PAIRING_EXPIRED_OR_INVALID".into()); + } + let clear = decrypt(&pairing.secret, nonce, MOBILE_PAIR_AAD, &request.body)?; + let input: PairRequest = + serde_json::from_slice(&clear).map_err(|_| "PAIRING_PAYLOAD_INVALID")?; + validate_device_name(&input.device_name)?; + if input.platform != "IOS" || Uuid::parse_str(&input.request_id).is_err() { + return Err("PAIRING_DEVICE_INVALID".into()); + } + let device_id = format!("ios-{}", Uuid::new_v4()); + let session_key = random_32()?; + let now = now_unix_ms(); + let connection = open_database(&runtime.database_path)?; + connection + .execute( + "INSERT INTO mobile_devices (device_id, display_name, platform, session_key, last_counter, created_at_unix_ms, last_seen_at_unix_ms, revoked) VALUES (?1, ?2, ?3, ?4, 0, ?5, NULL, 0)", + params![device_id, input.device_name, input.platform, session_key.as_slice(), now], + ) + .map_err(database_error)?; + pairing.used = true; + let response = PairResponse { + schema: MOBILE_SYNC_SCHEMA, + state: "PAIRED", + device_id, + session_key: URL_SAFE_NO_PAD.encode(session_key), + desktop_name: runtime.desktop_name.clone(), + root_node_role: "USER_LOCAL_COMPUTER_TERMINAL", + request_id: input.request_id, + }; + encrypted_response(&pairing.secret, MOBILE_PAIR_AAD, &response) +} + +fn sync_device(runtime: &MobileSyncRuntime, request: &HttpRequest) -> Result { + let device_id = required_header(request, "x-hololake-device-id")?; + validate_id(device_id)?; + let nonce = decode_nonce(required_header(request, "x-hololake-nonce")?)?; + let mut connection = open_database(&runtime.database_path)?; + let (session_key, revoked): (Vec, i64) = connection + .query_row( + "SELECT session_key, revoked FROM mobile_devices WHERE device_id = ?1", + params![device_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(database_error)? + .ok_or_else(|| "DEVICE_NOT_PAIRED".to_string())?; + if revoked != 0 || session_key.len() != 32 { + return Err("DEVICE_REVOKED".into()); + } + let key: [u8; 32] = session_key + .try_into() + .map_err(|_| "DEVICE_SESSION_KEY_INVALID".to_string())?; + let aad = sync_aad(device_id); + let clear = decrypt(&key, nonce, aad.as_bytes(), &request.body)?; + let input: SyncRequest = serde_json::from_slice(&clear).map_err(|_| "SYNC_PAYLOAD_INVALID")?; + let transaction = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_error)?; + let last_counter: u64 = transaction + .query_row( + "SELECT last_counter FROM mobile_devices WHERE device_id = ?1 AND revoked = 0", + params![device_id], + |row| row.get(0), + ) + .map_err(database_error)?; + if input.counter <= last_counter { + return Err("SYNC_REPLAY_REJECTED".into()); + } + if let Some(capture) = input.capture { + validate_capture(&capture)?; + transaction + .execute( + "INSERT OR IGNORE INTO mobile_captures (capture_id, title, body, source_device_id, request_id, created_at_unix_ms) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![Uuid::new_v4().to_string(), capture.title.trim(), capture.body.trim(), device_id, capture.request_id, now_unix_ms()], + ) + .map_err(database_error)?; + } + transaction + .execute( + "UPDATE mobile_devices SET last_counter = ?2, last_seen_at_unix_ms = ?3 WHERE device_id = ?1 AND revoked = 0", + params![device_id, input.counter, now_unix_ms()], + ) + .map_err(database_error)?; + transaction.commit().map_err(database_error)?; + let mut snapshot = build_snapshot(runtime)?; + if let Some(after) = input.after_cursor { + snapshot.recent_captures.retain(|item| item.cursor > after); + } + encrypted_response(&key, aad.as_bytes(), &snapshot) +} + +fn build_snapshot(runtime: &MobileSyncRuntime) -> Result { + let captures = list_captures(&runtime.database_path)?; + let cursor = captures.first().map(|item| item.cursor).unwrap_or(0); + let growth_event_count = count_rows( + &runtime + .account_root + .join("channel-growth-v1/channel-growth.sqlite3"), + "growth_events", + None, + ); + Ok(MobileSyncSnapshot { + schema: MOBILE_SYNC_SCHEMA, + state: "SYNCED_WITH_ROOT_NODE", + cursor, + generated_at_unix_ms: now_unix_ms(), + desktop_name: runtime.desktop_name.clone(), + root_node_online: true, + personal_channel: MobileChannelProjection { + home: "ONE_PERSON_ONE_PERSONAL_CHANNEL", + growth_event_count, + integrity: "LOCAL_APPEND_ONLY_PROJECTION", + }, + web_novel: web_novel_projection(&runtime.account_root), + education: education_projection(&runtime.account_root), + recent_captures: captures, + boundary: MobileBoundaryProjection { + mobile_role: "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM", + remote_desktop_clone: false, + desktop_offline_execution: false, + sensitive_education_values: "NEVER_INCLUDED_IN_MOBILE_SUMMARY", + model_api: "NOT_CONFIGURED_LOCAL_PROJECTION_ONLY", + }, + }) +} + +fn web_novel_projection(account_root: &Path) -> MobileWebNovelProjection { + let path = account_root.join("web-novel-workspace-v1/web-novel-workspace.sqlite3"); + let Ok(connection) = + Connection::open_with_flags(&path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY) + else { + return MobileWebNovelProjection::default(); + }; + let work_count = query_count(&connection, "SELECT COUNT(*) FROM web_novel_works"); + let volume_count = query_count(&connection, "SELECT COUNT(*) FROM web_novel_volumes"); + let chapter_count = query_count(&connection, "SELECT COUNT(*) FROM web_novel_chapters"); + let works = connection + .prepare("SELECT work_id, title, status, updated_at_unix_ms FROM web_novel_works ORDER BY updated_at_unix_ms DESC LIMIT 20") + .and_then(|mut statement| { + statement + .query_map([], |row| { + Ok(MobileWebNovelWork { + work_id: row.get(0)?, + title: row.get(1)?, + status: row.get(2)?, + updated_at_unix_ms: row.get(3)?, + }) + }) + .and_then(|rows| rows.collect()) + }) + .unwrap_or_default(); + MobileWebNovelProjection { + work_count, + volume_count, + chapter_count, + works, + } +} + +fn education_projection(account_root: &Path) -> MobileEducationProjection { + let path = account_root.join("education-workspace-v1/education-workspace.sqlite3"); + let Ok(connection) = + Connection::open_with_flags(&path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY) + else { + return MobileEducationProjection::default(); + }; + MobileEducationProjection { + active_table_count: query_count( + &connection, + "SELECT COUNT(*) FROM education_tables WHERE assignment_scope = 'EDUCATION' AND archived = 0", + ), + archived_table_count: query_count( + &connection, + "SELECT COUNT(*) FROM education_tables WHERE assignment_scope = 'EDUCATION' AND archived = 1", + ), + unassigned_table_count: query_count( + &connection, + "SELECT COUNT(*) FROM education_tables WHERE assignment_scope = 'UNASSIGNED' AND archived = 0", + ), + sensitive_values_included: false, + } +} + +fn query_count(connection: &Connection, sql: &str) -> u64 { + connection.query_row(sql, [], |row| row.get(0)).unwrap_or(0) +} + +fn count_rows(path: &Path, table: &str, condition: Option<&str>) -> u64 { + let Ok(connection) = + Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY) + else { + return 0; + }; + let sql = format!( + "SELECT COUNT(*) FROM {table}{}", + condition + .map(|value| format!(" WHERE {value}")) + .unwrap_or_default() + ); + query_count(&connection, &sql) +} + +fn list_devices(path: &Path) -> Result, String> { + let connection = open_database(path)?; + let mut statement = connection + .prepare("SELECT device_id, display_name, platform, created_at_unix_ms, last_seen_at_unix_ms, revoked FROM mobile_devices ORDER BY created_at_unix_ms DESC") + .map_err(database_error)?; + let devices = statement + .query_map([], |row| { + let revoked: i64 = row.get(5)?; + Ok(PairedDeviceSummary { + device_id: row.get(0)?, + display_name: row.get(1)?, + platform: row.get(2)?, + created_at_unix_ms: row.get(3)?, + last_seen_at_unix_ms: row.get(4)?, + state: if revoked == 0 { "ACTIVE" } else { "REVOKED" }, + }) + }) + .map_err(database_error)? + .collect::, _>>() + .map_err(database_error)?; + Ok(devices) +} + +fn list_captures(path: &Path) -> Result, String> { + let connection = open_database(path)?; + let mut statement = connection + .prepare("SELECT sequence, capture_id, title, body, source_device_id, created_at_unix_ms FROM mobile_captures ORDER BY sequence DESC LIMIT 50") + .map_err(database_error)?; + let captures = statement + .query_map([], |row| { + Ok(MobileCaptureProjection { + cursor: row.get(0)?, + capture_id: row.get(1)?, + title: row.get(2)?, + body: row.get(3)?, + source_device_id: row.get(4)?, + created_at_unix_ms: row.get(5)?, + }) + }) + .map_err(database_error)? + .collect::, _>>() + .map_err(database_error)?; + Ok(captures) +} + +fn initialize_database(path: &Path) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("HOLOLAKE_MOBILE_SYNC_ROOT_FAILED: {error}"))?; + } + let connection = open_database(path)?; + connection + .execute_batch( + "PRAGMA journal_mode = WAL; + PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS mobile_devices ( + device_id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + platform TEXT NOT NULL, + session_key BLOB NOT NULL, + last_counter INTEGER NOT NULL DEFAULT 0, + created_at_unix_ms INTEGER NOT NULL, + last_seen_at_unix_ms INTEGER, + revoked INTEGER NOT NULL DEFAULT 0 CHECK(revoked IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS mobile_captures ( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + capture_id TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + body TEXT NOT NULL, + source_device_id TEXT NOT NULL, + request_id TEXT NOT NULL UNIQUE, + created_at_unix_ms INTEGER NOT NULL, + FOREIGN KEY(source_device_id) REFERENCES mobile_devices(device_id) + );", + ) + .map_err(database_error) +} + +fn open_database(path: &Path) -> Result { + let connection = Connection::open(path).map_err(database_error)?; + connection + .busy_timeout(Duration::from_secs(3)) + .map_err(database_error)?; + Ok(connection) +} + +fn read_http_request(stream: &mut TcpStream) -> Result { + let mut buffer = Vec::with_capacity(4096); + let mut chunk = [0u8; 4096]; + let header_end = loop { + let read = stream.read(&mut chunk).map_err(|_| "REQUEST_READ_FAILED")?; + if read == 0 { + return Err("REQUEST_TRUNCATED".into()); + } + buffer.extend_from_slice(&chunk[..read]); + if buffer.len() > MAX_REQUEST_BYTES { + return Err("REQUEST_TOO_LARGE".into()); + } + if let Some(index) = find_bytes(&buffer, b"\r\n\r\n") { + break index + 4; + } + }; + let header_text = std::str::from_utf8(&buffer[..header_end]).map_err(|_| "HEADERS_INVALID")?; + let mut lines = header_text.split("\r\n"); + let mut request_line = lines + .next() + .ok_or("REQUEST_LINE_MISSING")? + .split_whitespace(); + let method = request_line.next().ok_or("METHOD_MISSING")?.to_string(); + let path = request_line.next().ok_or("PATH_MISSING")?.to_string(); + if request_line.next() != Some("HTTP/1.1") || request_line.next().is_some() { + return Err("HTTP_VERSION_INVALID".into()); + } + let mut headers = HashMap::new(); + for line in lines.filter(|line| !line.is_empty()) { + let Some((name, value)) = line.split_once(':') else { + return Err("HEADER_INVALID".into()); + }; + if headers + .insert(name.trim().to_ascii_lowercase(), value.trim().to_string()) + .is_some() + { + return Err("DUPLICATE_HEADER_REJECTED".into()); + } + } + let content_length = headers + .get("content-length") + .map(|value| value.parse::().map_err(|_| "CONTENT_LENGTH_INVALID")) + .transpose()? + .unwrap_or(0); + if content_length > MAX_REQUEST_BYTES { + return Err("REQUEST_TOO_LARGE".into()); + } + while buffer.len() < header_end + content_length { + let read = stream.read(&mut chunk).map_err(|_| "BODY_READ_FAILED")?; + if read == 0 { + return Err("BODY_TRUNCATED".into()); + } + buffer.extend_from_slice(&chunk[..read]); + if buffer.len() > MAX_REQUEST_BYTES { + return Err("REQUEST_TOO_LARGE".into()); + } + } + Ok(HttpRequest { + method, + path, + headers, + body: buffer[header_end..header_end + content_length].to_vec(), + }) +} + +fn write_http_response(stream: &mut TcpStream, response: HttpResponse) -> Result<(), String> { + let mut head = format!( + "HTTP/1.1 {} {}\r\nContent-Length: {}\r\nConnection: close\r\nCache-Control: no-store\r\n", + response.status, + response.reason, + response.body.len() + ); + for (name, value) in response.headers { + head.push_str(&format!("{name}: {value}\r\n")); + } + head.push_str("\r\n"); + stream + .write_all(head.as_bytes()) + .map_err(|_| "RESPONSE_WRITE_FAILED")?; + stream + .write_all(&response.body) + .map_err(|_| "RESPONSE_WRITE_FAILED".to_string()) +} + +fn encrypted_response( + key: &[u8; 32], + aad: &[u8], + value: &T, +) -> Result { + let clear = serde_json::to_vec(value).map_err(|_| "RESPONSE_SERIALIZATION_FAILED")?; + let nonce = random_nonce()?; + let body = encrypt(key, nonce, aad, &clear)?; + Ok(HttpResponse { + status: 200, + reason: "OK", + headers: vec![ + ("Content-Type".into(), "application/octet-stream".into()), + ("X-HoloLake-Nonce".into(), URL_SAFE_NO_PAD.encode(nonce)), + ], + body, + }) +} + +fn json_response(status: u16, reason: &'static str, value: &serde_json::Value) -> HttpResponse { + HttpResponse { + status, + reason, + headers: vec![("Content-Type".into(), "application/json".into())], + body: serde_json::to_vec(value).unwrap_or_default(), + } +} + +fn error_response(status: u16, reason: &'static str, code: &str) -> HttpResponse { + json_response( + status, + reason, + &serde_json::json!({"schema": MOBILE_SYNC_SCHEMA, "state": "REJECTED", "code": code}), + ) +} + +fn encrypt(key: &[u8; 32], nonce: [u8; 12], aad: &[u8], clear: &[u8]) -> Result, String> { + let unbound = UnboundKey::new(&CHACHA20_POLY1305, key).map_err(|_| "ENCRYPTION_KEY_INVALID")?; + let key = LessSafeKey::new(unbound); + let mut output = clear.to_vec(); + key.seal_in_place_append_tag( + Nonce::assume_unique_for_key(nonce), + Aad::from(aad), + &mut output, + ) + .map_err(|_| "ENCRYPTION_FAILED".to_string())?; + Ok(output) +} + +fn decrypt( + key: &[u8; 32], + nonce: [u8; 12], + aad: &[u8], + encrypted: &[u8], +) -> Result, String> { + let unbound = UnboundKey::new(&CHACHA20_POLY1305, key).map_err(|_| "ENCRYPTION_KEY_INVALID")?; + let key = LessSafeKey::new(unbound); + let mut buffer = encrypted.to_vec(); + let clear = key + .open_in_place( + Nonce::assume_unique_for_key(nonce), + Aad::from(aad), + &mut buffer, + ) + .map_err(|_| "AUTHENTICATION_FAILED".to_string())?; + Ok(clear.to_vec()) +} + +fn pairing_uri(runtime: &MobileSyncRuntime, pairing: &PairingMaterial) -> String { + format!( + "hololake://pair?host={}&port={}&id={}&secret={}", + runtime.lan_address, + runtime.port, + pairing.pairing_id, + URL_SAFE_NO_PAD.encode(pairing.secret) + ) +} + +fn qr_svg(value: &str) -> Result { + let code = QrCode::new(value.as_bytes()).map_err(|_| "HOLOLAKE_MOBILE_SYNC_QR_FAILED")?; + Ok(code + .render::() + .min_dimensions(320, 320) + .dark_color(svg::Color("#07111f")) + .light_color(svg::Color("#f5f7fc")) + .build()) +} + +fn new_pairing_material() -> Result { + Ok(PairingMaterial { + pairing_id: Uuid::new_v4().to_string(), + secret: random_32()?, + expires_at_unix_ms: now_unix_ms().saturating_add(PAIRING_TTL_MS), + used: false, + }) +} + +fn random_32() -> Result<[u8; 32], String> { + let mut bytes = [0u8; 32]; + SystemRandom::new() + .fill(&mut bytes) + .map_err(|_| "SECURE_RANDOM_UNAVAILABLE".to_string())?; + Ok(bytes) +} + +fn random_nonce() -> Result<[u8; 12], String> { + let mut bytes = [0u8; 12]; + SystemRandom::new() + .fill(&mut bytes) + .map_err(|_| "SECURE_RANDOM_UNAVAILABLE".to_string())?; + Ok(bytes) +} + +fn decode_nonce(value: &str) -> Result<[u8; 12], String> { + URL_SAFE_NO_PAD + .decode(value) + .map_err(|_| "NONCE_INVALID".to_string())? + .try_into() + .map_err(|_| "NONCE_INVALID".to_string()) +} + +fn required_header<'a>(request: &'a HttpRequest, name: &str) -> Result<&'a str, String> { + request + .headers + .get(name) + .map(String::as_str) + .ok_or_else(|| "AUTHENTICATION_HEADER_MISSING".to_string()) +} + +fn sync_aad(device_id: &str) -> String { + format!("{MOBILE_SYNC_AAD_PREFIX}{device_id}") +} + +fn validate_device_name(value: &str) -> Result<(), String> { + let length = value.trim().chars().count(); + if (1..=80).contains(&length) { + Ok(()) + } else { + Err("DEVICE_NAME_INVALID".into()) + } +} + +fn validate_capture(value: &MobileCaptureInput) -> Result<(), String> { + if value.title.trim().chars().count() > 120 + || value.body.trim().is_empty() + || value.body.trim().chars().count() > 10_000 + || Uuid::parse_str(&value.request_id).is_err() + { + Err("CAPTURE_INVALID".into()) + } else { + Ok(()) + } +} + +fn validate_id(value: &str) -> Result<(), String> { + if !value.is_empty() + && value.len() <= 80 + && value + .chars() + .all(|character| character.is_ascii_alphanumeric() || character == '-') + { + Ok(()) + } else { + Err("IDENTIFIER_INVALID".into()) + } +} + +fn resolve_lan_address() -> String { + let candidate = UdpSocket::bind("0.0.0.0:0") + .and_then(|socket| { + socket.connect("192.0.2.1:9")?; + socket.local_addr() + }) + .map(|address| address.ip().to_string()); + match candidate { + Ok(value) if value != "0.0.0.0" => value, + _ => "127.0.0.1".into(), + } +} + +fn desktop_name() -> String { + std::env::var("COMPUTERNAME") + .or_else(|_| std::env::var("HOSTNAME")) + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "HoloLake Mac".into()) +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .try_into() + .unwrap_or(u64::MAX) +} + +fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +fn database_error(error: rusqlite::Error) -> String { + format!("HOLOLAKE_MOBILE_SYNC_DATABASE_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn encrypted_payload_requires_the_exact_key_nonce_and_aad() { + let key = [7u8; 32]; + let nonce = [9u8; 12]; + let encrypted = encrypt(&key, nonce, b"scope", b"hello").unwrap(); + assert_eq!( + decrypt(&key, nonce, b"scope", &encrypted).unwrap(), + b"hello" + ); + assert!(decrypt(&key, nonce, b"other", &encrypted).is_err()); + } + + #[test] + fn sync_database_keeps_device_keys_and_capture_requests_unique() { + let temporary = tempdir().unwrap(); + let database = temporary.path().join("mobile-sync.sqlite3"); + initialize_database(&database).unwrap(); + let connection = open_database(&database).unwrap(); + connection + .execute( + "INSERT INTO mobile_devices (device_id, display_name, platform, session_key, created_at_unix_ms) VALUES ('ios-one', 'iPhone', 'IOS', ?1, 1)", + params![[1u8; 32].as_slice()], + ) + .unwrap(); + connection + .execute( + "INSERT INTO mobile_captures (capture_id, title, body, source_device_id, request_id, created_at_unix_ms) VALUES ('capture-one', '想法', '正文', 'ios-one', 'request-one', 2)", + [], + ) + .unwrap(); + assert_eq!(list_devices(&database).unwrap().len(), 1); + assert_eq!(list_captures(&database).unwrap().len(), 1); + assert!(connection + .execute( + "INSERT INTO mobile_captures (capture_id, title, body, source_device_id, request_id, created_at_unix_ms) VALUES ('capture-two', '重复', '正文', 'ios-one', 'request-one', 3)", + [], + ) + .is_err()); + } + + #[test] + fn pairing_uri_contains_a_full_random_secret_without_persisting_it() { + let material = new_pairing_material().unwrap(); + let runtime = MobileSyncRuntime { + shutdown: AtomicBool::new(false), + active_connections: AtomicUsize::new(0), + port: 37421, + lan_address: "192.168.1.8".into(), + desktop_name: "Mac".into(), + account_root: PathBuf::new(), + database_path: PathBuf::new(), + pairing: Mutex::new(material.clone()), + }; + let uri = pairing_uri(&runtime, &material); + assert!(uri.starts_with("hololake://pair?host=192.168.1.8&port=37421&id=")); + assert!(uri.contains("&secret=")); + } + + #[test] + fn pairing_is_single_use_and_sync_replay_is_rejected() { + let temporary = tempdir().unwrap(); + let database = temporary.path().join("mobile-sync.sqlite3"); + initialize_database(&database).unwrap(); + let pairing = new_pairing_material().unwrap(); + let runtime = MobileSyncRuntime { + shutdown: AtomicBool::new(false), + active_connections: AtomicUsize::new(0), + port: 37421, + lan_address: "192.168.1.8".into(), + desktop_name: "Mac".into(), + account_root: temporary.path().to_path_buf(), + database_path: database, + pairing: Mutex::new(pairing.clone()), + }; + let pair_nonce = [3u8; 12]; + let pair_clear = serde_json::to_vec(&serde_json::json!({ + "deviceName": "冰朔的 iPhone", + "platform": "IOS", + "requestId": Uuid::new_v4().to_string() + })) + .unwrap(); + let pair_request = HttpRequest { + method: "POST".into(), + path: "/v1/pair".into(), + headers: HashMap::from([ + ("x-hololake-pairing-id".into(), pairing.pairing_id.clone()), + ( + "x-hololake-nonce".into(), + URL_SAFE_NO_PAD.encode(pair_nonce), + ), + ]), + body: encrypt(&pairing.secret, pair_nonce, MOBILE_PAIR_AAD, &pair_clear).unwrap(), + }; + let pair_response = pair_device(&runtime, &pair_request).unwrap(); + assert_eq!( + pair_device(&runtime, &pair_request).unwrap_err(), + "PAIRING_EXPIRED_OR_INVALID" + ); + let response_nonce = decode_nonce( + pair_response + .headers + .iter() + .find(|(name, _)| name == "X-HoloLake-Nonce") + .map(|(_, value)| value.as_str()) + .unwrap(), + ) + .unwrap(); + let response: serde_json::Value = serde_json::from_slice( + &decrypt( + &pairing.secret, + response_nonce, + MOBILE_PAIR_AAD, + &pair_response.body, + ) + .unwrap(), + ) + .unwrap(); + let device_id = response["deviceId"].as_str().unwrap(); + let key: [u8; 32] = URL_SAFE_NO_PAD + .decode(response["sessionKey"].as_str().unwrap()) + .unwrap() + .try_into() + .unwrap(); + let sync_nonce = [4u8; 12]; + let sync_clear = serde_json::to_vec(&serde_json::json!({ + "counter": 1, + "afterCursor": null, + "capture": null + })) + .unwrap(); + let aad = sync_aad(device_id); + let sync_request = HttpRequest { + method: "POST".into(), + path: "/v1/sync".into(), + headers: HashMap::from([ + ("x-hololake-device-id".into(), device_id.into()), + ( + "x-hololake-nonce".into(), + URL_SAFE_NO_PAD.encode(sync_nonce), + ), + ]), + body: encrypt(&key, sync_nonce, aad.as_bytes(), &sync_clear).unwrap(), + }; + assert!(sync_device(&runtime, &sync_request).is_ok()); + assert_eq!( + sync_device(&runtime, &sync_request).unwrap_err(), + "SYNC_REPLAY_REJECTED" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs new file mode 100644 index 000000000..7f671fdb4 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs @@ -0,0 +1,1765 @@ +//! Signed, account-scoped HoloLake module package runtime. +//! +//! A repository is never executable input. The runtime accepts only an immutable +//! detached-minisign package, validates a declarative manifest, persists lifecycle +//! state in SQLite and writes a hash-chained receipt for every mutation. Package +//! mount does not load native code or arbitrary webview JavaScript. + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use minisign_verify::{PublicKey, Signature}; +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension, Transaction}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const CONTRACT_RAW: &str = include_str!("../../contracts/module-package-runtime.json"); +const RELEASE_TRUST_RAW: &str = include_str!("../release-trust.json"); +const CONTRACT_SCHEMA: &str = "hololake.module-package-runtime/v1"; +const PACKAGE_SCHEMA: &str = "hololake.module-package/v1"; +const HOST_VERSION: &str = "0.5.0"; +const MAX_PACKAGE_BYTES: u64 = 16 * 1024 * 1024; +const NATIVE_COMPOSITION_NUMBER: &str = "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001"; +const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod" +); +const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig" +); +const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001"; +const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod" +); +const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig" +); +const PERSONA_CHANNEL_BODY_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001"; +const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod" +); +const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig" +); +const EDUCATION_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; +const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod" +); +const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig" +); +const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; +const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod" +); +const WEB_NOVEL_WORKBENCH_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig" +); +const WEB_NOVEL_OUTLINE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001"; +const WEB_NOVEL_OUTLINE_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod" +); +const WEB_NOVEL_OUTLINE_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig" +); +const WEB_NOVEL_GRID_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001"; +const WEB_NOVEL_GRID_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod" +); +const WEB_NOVEL_GRID_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig" +); +const WEB_NOVEL_STORYWORLD_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001"; +const WEB_NOVEL_STORYWORLD_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod" +); +const WEB_NOVEL_STORYWORLD_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig" +); +const WEB_NOVEL_DELIVERY_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"; +const WEB_NOVEL_DELIVERY_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod" +); +const WEB_NOVEL_DELIVERY_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig" +); +const MOBILE_SYNC_NUMBER: &str = "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001"; +const MOBILE_SYNC_PACKAGE: &[u8] = + include_bytes!("../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod"); +const MOBILE_SYNC_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig" +); +const DYNAMIC_WORLD_SURFACE_NUMBER: &str = "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001"; +const DYNAMIC_WORLD_SURFACE_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod" +); +const DYNAMIC_WORLD_SURFACE_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig" +); + +struct BundledModuleSource { + module_number: &'static str, + package: &'static [u8], + signature: &'static str, +} + +const BUNDLED_MODULES: &[BundledModuleSource] = &[ + BundledModuleSource { + module_number: NATIVE_COMPOSITION_NUMBER, + package: NATIVE_COMPOSITION_PACKAGE, + signature: NATIVE_COMPOSITION_SIGNATURE, + }, + BundledModuleSource { + module_number: CHANNEL_WORKBENCH_NUMBER, + package: CHANNEL_WORKBENCH_PACKAGE, + signature: CHANNEL_WORKBENCH_SIGNATURE, + }, + BundledModuleSource { + module_number: PERSONA_CHANNEL_BODY_NUMBER, + package: PERSONA_CHANNEL_BODY_PACKAGE, + signature: PERSONA_CHANNEL_BODY_SIGNATURE, + }, + BundledModuleSource { + module_number: EDUCATION_WORKBENCH_NUMBER, + package: EDUCATION_WORKBENCH_PACKAGE, + signature: EDUCATION_WORKBENCH_SIGNATURE, + }, + BundledModuleSource { + module_number: WEB_NOVEL_WORKBENCH_NUMBER, + package: WEB_NOVEL_WORKBENCH_PACKAGE, + signature: WEB_NOVEL_WORKBENCH_SIGNATURE, + }, + BundledModuleSource { + module_number: WEB_NOVEL_OUTLINE_NUMBER, + package: WEB_NOVEL_OUTLINE_PACKAGE, + signature: WEB_NOVEL_OUTLINE_SIGNATURE, + }, + BundledModuleSource { + module_number: WEB_NOVEL_GRID_NUMBER, + package: WEB_NOVEL_GRID_PACKAGE, + signature: WEB_NOVEL_GRID_SIGNATURE, + }, + BundledModuleSource { + module_number: WEB_NOVEL_STORYWORLD_NUMBER, + package: WEB_NOVEL_STORYWORLD_PACKAGE, + signature: WEB_NOVEL_STORYWORLD_SIGNATURE, + }, + BundledModuleSource { + module_number: WEB_NOVEL_DELIVERY_NUMBER, + package: WEB_NOVEL_DELIVERY_PACKAGE, + signature: WEB_NOVEL_DELIVERY_SIGNATURE, + }, + BundledModuleSource { + module_number: MOBILE_SYNC_NUMBER, + package: MOBILE_SYNC_PACKAGE, + signature: MOBILE_SYNC_SIGNATURE, + }, + BundledModuleSource { + module_number: DYNAMIC_WORLD_SURFACE_NUMBER, + package: DYNAMIC_WORLD_SURFACE_PACKAGE, + signature: DYNAMIC_WORLD_SURFACE_SIGNATURE, + }, +]; + +#[derive(Debug, Deserialize)] +struct RuntimeContract { + schema: String, + record_id: String, + package_schema: String, + host: HostContract, + registered_adapters: Vec, + lifecycle: Vec, +} + +#[derive(Debug, Deserialize)] +struct HostContract { + version: String, + maximum_package_bytes: u64, + arbitrary_native_code: bool, + arbitrary_webview_javascript: bool, + declarative_payload_only: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModulePackage { + schema: String, + manifest: ModuleManifest, + payload: Value, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModuleManifest { + module_number: String, + registration_class: String, + display_name: String, + version: String, + minimum_host_version: String, + adapter: String, + content_digest: String, + permissions: Vec, + user_data_schema: String, + self_test: ModuleSelfTest, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ModuleSelfTest { + kind: String, + expected_content_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct VerifyModulePackageInput { + package_path: String, + signature_path: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct InstallModulePackageInput { + package_path: String, + signature_path: String, + expected_package_sha256: String, + human_confirmed_permission_expansion: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ModuleNumberInput { + module_number: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedModulePackage { + state: String, + package_sha256: String, + module_number: String, + display_name: String, + version: String, + adapter: String, + permissions: Vec, + signature_verified: bool, + package_code_executed: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledModule { + module_number: String, + display_name: String, + version: String, + adapter: String, + state: String, + package_sha256: String, + previous_package_sha256: Option, + permissions: Vec, + user_data_preserved: bool, + updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleRuntimeSnapshot { + schema: &'static str, + state: &'static str, + host_version: &'static str, + package_code_executed: bool, + modules: Vec, + receipt_count: u64, + latest_receipt_hash: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleMutationReceipt { + receipt_id: String, + event: String, + module_number: String, + state: String, + package_sha256: String, + previous_receipt_hash: String, + receipt_hash: String, + observed_at_unix_ms: u64, + user_data_preserved: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ActivateBundledModuleInput { + module_number: String, + human_confirmed_permission_expansion: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BundledModuleDescriptor { + module_number: String, + display_name: String, + version: String, + adapter: String, + permissions: Vec, + package_sha256: String, + installed_state: String, + signature_verified: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BundledModuleActivation { + schema: &'static str, + state: String, + module_number: String, + receipts: Vec, + snapshot: ModuleRuntimeSnapshot, +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn is_sha256(value: &str) -> bool { + value.len() == 64 && value.chars().all(|item| item.is_ascii_hexdigit()) +} + +fn canonical_value(value: &Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()), + Value::Object(object) => { + let mut keys = object.keys().collect::>(); + keys.sort(); + let mut canonical = Map::new(); + for key in keys { + canonical.insert(key.clone(), canonical_value(&object[key])); + } + Value::Object(canonical) + } + _ => value.clone(), + } +} + +fn payload_digest(value: &Value) -> Result { + let bytes = serde_json::to_vec(&canonical_value(value)) + .map_err(|error| format!("HOLOLAKE_MODULE_PAYLOAD_INVALID: {error}"))?; + Ok(sha256_hex(&bytes)) +} + +fn contract() -> Result { + let contract: RuntimeContract = serde_json::from_str(CONTRACT_RAW) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_CONTRACT_INVALID: {error}"))?; + if contract.schema != CONTRACT_SCHEMA + || contract.record_id != "HLP-MODULE-PACKAGE-RUNTIME-001" + || contract.package_schema != PACKAGE_SCHEMA + || contract.host.version != HOST_VERSION + || contract.host.maximum_package_bytes != MAX_PACKAGE_BYTES + || contract.host.arbitrary_native_code + || contract.host.arbitrary_webview_javascript + || !contract.host.declarative_payload_only + || contract.registered_adapters.len() != 7 + || contract.lifecycle.len() != 6 + { + return Err("HOLOLAKE_MODULE_RUNTIME_CONTRACT_BOUNDARY_INVALID".into()); + } + Ok(contract) +} + +fn decode_outer_base64(value: &str, label: &str) -> Result { + let bytes = STANDARD + .decode(value.trim()) + .map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?; + String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) +} + +fn verify_signature_raw( + package: &[u8], + signature_raw: &str, + trust_raw: &str, +) -> Result<(), String> { + let trust: Value = serde_json::from_str(trust_raw) + .map_err(|error| format!("HOLOLAKE_MODULE_TRUST_INVALID: {error}"))?; + if trust.get("state").and_then(Value::as_str) != Some("PROVISIONED") { + return Err("HOLOLAKE_MODULE_TRUST_UNPROVISIONED".into()); + } + let encoded_key = trust + .get("publicKey") + .and_then(Value::as_str) + .ok_or("HOLOLAKE_MODULE_TRUST_KEY_MISSING")?; + let public_key = PublicKey::decode(&decode_outer_base64(encoded_key, "HOLOLAKE_MODULE_KEY")?) + .map_err(|error| format!("HOLOLAKE_MODULE_KEY_INVALID: {error}"))?; + let signature = Signature::decode(&decode_outer_base64( + signature_raw, + "HOLOLAKE_MODULE_SIGNATURE", + )?) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_INVALID: {error}"))?; + public_key + .verify(package, &signature, true) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_REJECTED: {error}")) +} + +fn valid_number(value: &str, registration_class: &str) -> bool { + let prefix = match registration_class { + "OFFICIAL_LIGHTHOUSE" => "HLP-MOD-OFFICIAL-", + "PRIVATE_CHANNEL_LOCAL" => "HLP-MOD-LOCAL-", + _ => return false, + }; + value.starts_with(prefix) + && value.len() <= 96 + && value + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-') +} + +fn version_triplet(value: &str) -> Option<(u64, u64, u64)> { + let parts = value.split('.').collect::>(); + if parts.len() != 3 { + return None; + } + Some(( + parts[0].parse().ok()?, + parts[1].parse().ok()?, + parts[2].parse().ok()?, + )) +} + +fn validate_package(package: &ModulePackage, contract: &RuntimeContract) -> Result<(), String> { + let manifest = &package.manifest; + let permissions = manifest.permissions.iter().collect::>(); + if package.schema != PACKAGE_SCHEMA + || !valid_number(&manifest.module_number, &manifest.registration_class) + || manifest.display_name.trim().is_empty() + || manifest.display_name.len() > 120 + || version_triplet(&manifest.version).is_none() + || version_triplet(&manifest.minimum_host_version).is_none() + || version_triplet(&manifest.minimum_host_version) > version_triplet(HOST_VERSION) + || !contract.registered_adapters.contains(&manifest.adapter) + || !is_sha256(&manifest.content_digest) + || permissions.len() != manifest.permissions.len() + || manifest.permissions.len() > 32 + || manifest.permissions.iter().any(|permission| { + permission.is_empty() + || permission.len() > 80 + || !permission + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '_') + }) + || manifest.user_data_schema.trim().is_empty() + || manifest.self_test.kind != "DECLARATIVE_SCHEMA_V1" + || manifest.self_test.expected_content_digest != manifest.content_digest + || !package.payload.is_object() + || payload_digest(&package.payload)? != manifest.content_digest + { + return Err("HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID".into()); + } + Ok(()) +} + +fn read_verified_package( + package_path: &Path, + signature_path: &Path, + trust_raw: &str, +) -> Result<(Vec, ModulePackage, String), String> { + let metadata = fs::symlink_metadata(package_path) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_METADATA_FAILED: {error}"))?; + let signature_metadata = fs::symlink_metadata(signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_METADATA_FAILED: {error}"))?; + if metadata.file_type().is_symlink() + || signature_metadata.file_type().is_symlink() + || !metadata.is_file() + || !signature_metadata.is_file() + || metadata.len() == 0 + || metadata.len() > MAX_PACKAGE_BYTES + || signature_metadata.len() == 0 + || signature_metadata.len() > 16 * 1024 + { + return Err("HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID".into()); + } + let bytes = fs::read(package_path) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_READ_FAILED: {error}"))?; + let signature_raw = fs::read_to_string(signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?; + read_verified_package_bytes(&bytes, &signature_raw, trust_raw) +} + +fn read_verified_package_bytes( + bytes: &[u8], + signature_raw: &str, + trust_raw: &str, +) -> Result<(Vec, ModulePackage, String), String> { + verify_signature_raw(bytes, signature_raw, trust_raw)?; + let package: ModulePackage = serde_json::from_slice(bytes) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_JSON_INVALID: {error}"))?; + validate_package(&package, &contract()?)?; + let package_sha256 = sha256_hex(bytes); + Ok((bytes.to_vec(), package, package_sha256)) +} + +fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> { + BUNDLED_MODULES + .iter() + .find(|source| source.module_number == module_number) + .map(|source| (source.package, source.signature)) + .ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()) +} + +fn runtime_root(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "module-runtime-v1") +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_ROOT_FAILED: {error}"))?; + let connection = Connection::open(root.join("module-runtime.sqlite3")) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(std::time::Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS installed_modules( + module_number TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + version TEXT NOT NULL, + adapter TEXT NOT NULL, + state TEXT NOT NULL, + package_sha256 TEXT NOT NULL, + previous_package_sha256 TEXT, + permissions_json TEXT NOT NULL, + manifest_json TEXT NOT NULL, + updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS module_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + receipt_id TEXT NOT NULL UNIQUE, + event TEXT NOT NULL, + module_number TEXT NOT NULL, + state TEXT NOT NULL, + package_sha256 TEXT NOT NULL, + previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL, + observed_at_unix_ms INTEGER NOT NULL + );", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_SCHEMA_FAILED: {error}"))?; + Ok(connection) +} + +fn known_state(state: &str) -> bool { + matches!( + state, + "INSTALLED_DORMANT" + | "MOUNTED_PENDING_SELF_TEST" + | "ACTIVE" + | "DORMANT" + | "ROLLBACK_PENDING_SELF_TEST" + | "FAILED_CLOSED" + ) +} + +fn verify_receipt_chain(connection: &Connection) -> Result<(), String> { + let mut statement = connection + .prepare( + "SELECT receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM module_receipts ORDER BY sequence", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + row.get::<_, String>(6)?, + row.get::<_, u64>(7)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_QUERY_FAILED: {error}"))?; + let mut previous = "0".repeat(64); + for row in rows { + let (receipt_id, event, module_number, state, package_sha256, linked, stored, observed) = + row.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_ROW_FAILED: {error}"))?; + let expected = sha256_hex( + format!( + "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{linked}\n{observed}" + ) + .as_bytes(), + ); + if linked != previous + || stored != expected + || !is_sha256(&stored) + || !is_sha256(&package_sha256) + || !known_state(&state) + { + return Err("HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID".into()); + } + previous = stored; + } + Ok(()) +} + +fn verify_persisted_modules(connection: &Connection, root: &Path) -> Result<(), String> { + let mut statement = connection + .prepare( + "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json FROM installed_modules ORDER BY module_number", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_PREPARE_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + row.get::<_, Option>(6)?, + row.get::<_, String>(7)?, + row.get::<_, String>(8)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_QUERY_FAILED: {error}"))?; + for row in rows { + let ( + number, + display, + version, + adapter, + state, + digest, + previous, + permissions_raw, + manifest_raw, + ) = row.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_ROW_FAILED: {error}"))?; + if !known_state(&state) || !is_sha256(&digest) { + return Err("HOLOLAKE_MODULE_PERSISTED_STATE_INVALID".into()); + } + let manifest: ModuleManifest = serde_json::from_str(&manifest_raw) + .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID".to_string())?; + let permissions: Vec = serde_json::from_str(&permissions_raw) + .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_PERMISSIONS_INVALID".to_string())?; + let (_, package, verified_digest) = read_verified_package( + &package_file(root, &digest), + &signature_file(root, &digest), + RELEASE_TRUST_RAW, + )?; + if verified_digest != digest + || package.manifest != manifest + || manifest.module_number != number + || manifest.display_name != display + || manifest.version != version + || manifest.adapter != adapter + || manifest.permissions != permissions + { + return Err("HOLOLAKE_MODULE_PERSISTED_IDENTITY_INVALID".into()); + } + if let Some(previous) = previous { + if !is_sha256(&previous) { + return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_INVALID".into()); + } + let (_, previous_package, previous_digest) = read_verified_package( + &package_file(root, &previous), + &signature_file(root, &previous), + RELEASE_TRUST_RAW, + )?; + if previous_digest != previous || previous_package.manifest.module_number != number { + return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_IDENTITY_INVALID".into()); + } + } + } + Ok(()) +} + +fn package_file(root: &Path, digest: &str) -> PathBuf { + root.join("packages").join(format!("{digest}.ghmod")) +} + +fn signature_file(root: &Path, digest: &str) -> PathBuf { + root.join("packages").join(format!("{digest}.ghmod.sig")) +} + +fn copy_atomic(source: &Path, target: &Path) -> Result<(), String> { + let parent = target + .parent() + .ok_or("HOLOLAKE_MODULE_PACKAGE_TARGET_INVALID")?; + fs::create_dir_all(parent) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_STORE_FAILED: {error}"))?; + if target.exists() { + return Ok(()); + } + let temporary = parent.join(format!(".install-{}", Uuid::new_v4())); + fs::copy(source, &temporary) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COPY_FAILED: {error}"))?; + fs::rename(&temporary, target) + .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COMMIT_FAILED: {error}")) +} + +fn previous_receipt_hash(transaction: &Transaction<'_>) -> Result { + transaction + .query_row( + "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map(|value| value.unwrap_or_else(|| "0".repeat(64))) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_READ_FAILED: {error}")) +} + +fn append_receipt( + transaction: &Transaction<'_>, + event: &str, + module_number: &str, + state: &str, + package_sha256: &str, + observed_at_unix_ms: u64, +) -> Result { + let receipt_id = format!("HLP-MOD-RCPT-{}", Uuid::new_v4()); + let previous = previous_receipt_hash(transaction)?; + let receipt_hash = sha256_hex( + format!( + "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{previous}\n{observed_at_unix_ms}" + ) + .as_bytes(), + ); + transaction + .execute( + "INSERT INTO module_receipts(receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) + VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", + params![receipt_id, event, module_number, state, package_sha256, previous, receipt_hash, observed_at_unix_ms], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_WRITE_FAILED: {error}"))?; + Ok(ModuleMutationReceipt { + receipt_id, + event: event.into(), + module_number: module_number.into(), + state: state.into(), + package_sha256: package_sha256.into(), + previous_receipt_hash: previous, + receipt_hash, + observed_at_unix_ms, + user_data_preserved: true, + }) +} + +fn permissions_from_json(raw: &str) -> BTreeSet { + serde_json::from_str::>(raw) + .unwrap_or_default() + .into_iter() + .collect() +} + +fn materialize_bundled_source( + root: &Path, + module_number: &str, + package: &[u8], + signature: &str, +) -> Result<(PathBuf, PathBuf), String> { + let source_root = root.join("bundled-sources"); + fs::create_dir_all(&source_root) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_ROOT_FAILED: {error}"))?; + let digest = sha256_hex(package); + let package_path = source_root.join(format!("{module_number}-{digest}.ghmod")); + let signature_path = source_root.join(format!("{module_number}-{digest}.ghmod.sig")); + for (path, bytes) in [ + (&package_path, package), + (&signature_path, signature.as_bytes()), + ] { + if path.exists() { + let current = fs::read(path) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_READ_FAILED: {error}"))?; + if current != bytes { + return Err("HOLOLAKE_BUNDLED_MODULE_SOURCE_TAMPERED".into()); + } + continue; + } + let temporary = source_root.join(format!(".bundled-{}", Uuid::new_v4())); + fs::write(&temporary, bytes) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_WRITE_FAILED: {error}"))?; + fs::rename(&temporary, path) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_COMMIT_FAILED: {error}"))?; + } + Ok((package_path, signature_path)) +} + +pub async fn get_bundled_module_catalog( + app: AppHandle, +) -> Result, String> { + let root = runtime_root(&app)?; + let snapshot = snapshot_at(&root)?; + BUNDLED_MODULES + .iter() + .map(|source| { + let (_, package, package_sha256) = + read_verified_package_bytes(source.package, source.signature, RELEASE_TRUST_RAW)?; + if package.manifest.module_number != source.module_number { + return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); + } + let installed_state = snapshot + .modules + .iter() + .find(|module| module.module_number == source.module_number) + .map(|module| module.state.clone()) + .unwrap_or_else(|| "NOT_INSTALLED".into()); + Ok(BundledModuleDescriptor { + module_number: package.manifest.module_number, + display_name: package.manifest.display_name, + version: package.manifest.version, + adapter: package.manifest.adapter, + permissions: package.manifest.permissions, + package_sha256, + installed_state, + signature_verified: true, + }) + }) + .collect() +} + +pub async fn activate_bundled_module( + app: AppHandle, + input: ActivateBundledModuleInput, +) -> Result { + let root = runtime_root(&app)?; + let (bytes, signature) = bundled_package(&input.module_number)?; + let (_, package, package_sha256) = + read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?; + if package.manifest.module_number != input.module_number { + return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); + } + let mut receipts = Vec::new(); + let before = snapshot_at(&root)?; + let installed = before + .modules + .iter() + .find(|module| module.module_number == input.module_number); + if installed + .is_some_and(|module| module.state == "ACTIVE" && module.package_sha256 == package_sha256) + { + return Ok(BundledModuleActivation { + schema: "hololake.bundled-module-activation/v1", + state: "ALREADY_ACTIVE".into(), + module_number: input.module_number, + receipts, + snapshot: before, + }); + } + let needs_install = installed + .map(|module| module.package_sha256 != package_sha256 || module.state == "FAILED_CLOSED") + .unwrap_or(true); + if needs_install { + let (package_path, signature_path) = + materialize_bundled_source(&root, &input.module_number, bytes, signature)?; + receipts.push(install_module_package_at( + &root, + &InstallModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + expected_package_sha256: package_sha256, + human_confirmed_permission_expansion: input.human_confirmed_permission_expansion, + }, + RELEASE_TRUST_RAW, + now_unix_ms(), + )?); + } + let current = snapshot_at(&root)? + .modules + .into_iter() + .find(|module| module.module_number == input.module_number) + .ok_or("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_MISSING")?; + match current.state.as_str() { + "INSTALLED_DORMANT" | "DORMANT" => { + receipts.push(transition( + &root, + &input.module_number, + &["INSTALLED_DORMANT", "DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + )?); + receipts.push(self_test_module_at( + &root, + &input.module_number, + now_unix_ms(), + )?); + } + "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" => { + receipts.push(self_test_module_at( + &root, + &input.module_number, + now_unix_ms(), + )?); + } + "ACTIVE" => {} + _ => return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_INVALID".into()), + } + let snapshot = snapshot_at(&root)?; + if !snapshot.modules.iter().any(|module| { + module.module_number == input.module_number + && module.adapter == package.manifest.adapter + && module.state == "ACTIVE" + }) { + return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_NOT_ACTIVE".into()); + } + Ok(BundledModuleActivation { + schema: "hololake.bundled-module-activation/v1", + state: "ACTIVE".into(), + module_number: input.module_number, + receipts, + snapshot, + }) +} + +pub(crate) fn require_active_module_adapter( + app: &AppHandle, + module_number: &str, + adapter: &str, +) -> Result<(), String> { + let snapshot = snapshot_at(&runtime_root(app)?)?; + if snapshot.modules.iter().any(|module| { + module.module_number == module_number + && module.adapter == adapter + && module.state == "ACTIVE" + }) { + Ok(()) + } else { + Err("HOLOLAKE_MODULE_NOT_ACTIVE".into()) + } +} + +pub async fn verify_module_package( + app: AppHandle, + input: VerifyModulePackageInput, +) -> Result { + let _ = runtime_root(&app)?; + let (_, package, package_sha256) = read_verified_package( + Path::new(&input.package_path), + Path::new(&input.signature_path), + RELEASE_TRUST_RAW, + )?; + Ok(VerifiedModulePackage { + state: "VERIFIED_NOT_INSTALLED".into(), + package_sha256, + module_number: package.manifest.module_number, + display_name: package.manifest.display_name, + version: package.manifest.version, + adapter: package.manifest.adapter, + permissions: package.manifest.permissions, + signature_verified: true, + package_code_executed: false, + }) +} + +pub async fn install_module_package( + app: AppHandle, + input: InstallModulePackageInput, +) -> Result { + install_module_package_at( + &runtime_root(&app)?, + &input, + RELEASE_TRUST_RAW, + now_unix_ms(), + ) +} + +fn install_module_package_at( + root: &Path, + input: &InstallModulePackageInput, + trust_raw: &str, + observed: u64, +) -> Result { + if !is_sha256(&input.expected_package_sha256) { + return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_INVALID".into()); + } + let (bytes, package, digest) = read_verified_package( + Path::new(&input.package_path), + Path::new(&input.signature_path), + trust_raw, + )?; + if digest != input.expected_package_sha256 { + return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_MISMATCH".into()); + } + let mut connection = open_db(root)?; + let existing: Option<(String, String)> = connection + .query_row( + "SELECT package_sha256, permissions_json FROM installed_modules WHERE module_number=?1", + params![package.manifest.module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_EXISTING_READ_FAILED: {error}"))?; + let requested = package + .manifest + .permissions + .iter() + .cloned() + .collect::>(); + let expansion = existing + .as_ref() + .map(|(_, raw)| !requested.is_subset(&permissions_from_json(raw))) + .unwrap_or(!requested.is_empty()); + if expansion && !input.human_confirmed_permission_expansion { + return Err("HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION".into()); + } + copy_atomic(Path::new(&input.package_path), &package_file(root, &digest))?; + copy_atomic( + Path::new(&input.signature_path), + &signature_file(root, &digest), + )?; + if fs::read(package_file(root, &digest)) + .map_err(|error| format!("HOLOLAKE_MODULE_STORED_PACKAGE_READ_FAILED: {error}"))? + != bytes + { + return Err("HOLOLAKE_MODULE_STORED_PACKAGE_MISMATCH".into()); + } + if fs::read(signature_file(root, &digest)) + .map_err(|error| format!("HOLOLAKE_MODULE_STORED_SIGNATURE_READ_FAILED: {error}"))? + != fs::read(&input.signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SOURCE_SIGNATURE_READ_FAILED: {error}"))? + { + return Err("HOLOLAKE_MODULE_STORED_SIGNATURE_MISMATCH".into()); + } + fs::create_dir_all(root.join("user-data").join(&package.manifest.module_number)) + .map_err(|error| format!("HOLOLAKE_MODULE_USER_DATA_ROOT_FAILED: {error}"))?; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_TRANSACTION_FAILED: {error}"))?; + let permissions_json = serde_json::to_string(&package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_MANIFEST_INVALID: {error}"))?; + let previous = existing.as_ref().map(|(value, _)| value.as_str()); + transaction + .execute( + "INSERT INTO installed_modules(module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json,updated_at_unix_ms) + VALUES(?1,?2,?3,?4,'INSTALLED_DORMANT',?5,?6,?7,?8,?9) + ON CONFLICT(module_number) DO UPDATE SET display_name=excluded.display_name,version=excluded.version,adapter=excluded.adapter,state='INSTALLED_DORMANT',package_sha256=excluded.package_sha256,previous_package_sha256=installed_modules.package_sha256,permissions_json=excluded.permissions_json,manifest_json=excluded.manifest_json,updated_at_unix_ms=excluded.updated_at_unix_ms", + params![package.manifest.module_number, package.manifest.display_name, package.manifest.version, package.manifest.adapter, digest, previous, permissions_json, manifest_json, observed], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "INSTALL", + &package.manifest.module_number, + "INSTALLED_DORMANT", + &digest, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +fn transition( + root: &Path, + module_number: &str, + allowed_from: &[&str], + target: &str, + event: &str, +) -> Result { + let mut connection = open_db(root)?; + let row: (String, String) = connection + .query_row( + "SELECT state,package_sha256 FROM installed_modules WHERE module_number=?1", + params![module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_STATE_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + if !allowed_from.contains(&row.0.as_str()) { + return Err("HOLOLAKE_MODULE_LIFECYCLE_TRANSITION_INVALID".into()); + } + let observed = now_unix_ms(); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_BEGIN_FAILED: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET state=?1,updated_at_unix_ms=?2 WHERE module_number=?3 AND state=?4", + params![target, observed, module_number, row.0], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_WRITE_FAILED: {error}"))?; + let receipt = append_receipt(&transaction, event, module_number, target, &row.1, observed)?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +pub async fn mount_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + transition( + &runtime_root(&app)?, + &input.module_number, + &["INSTALLED_DORMANT", "DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) +} + +pub async fn self_test_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + self_test_module_at(&runtime_root(&app)?, &input.module_number, now_unix_ms()) +} + +fn self_test_module_at( + root: &Path, + module_number: &str, + observed: u64, +) -> Result { + let mut connection = open_db(root)?; + let row: (String, String, Option) = connection + .query_row( + "SELECT state,package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", + params![module_number], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_STATE_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + if !matches!( + row.0.as_str(), + "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" + ) { + return Err("HOLOLAKE_MODULE_SELF_TEST_STATE_INVALID".into()); + } + let verification = read_verified_package( + &package_file(root, &row.1), + &signature_file(root, &row.1), + RELEASE_TRUST_RAW, + ); + let success = verification.as_ref().is_ok_and(|(_, package, digest)| { + digest == &row.1 && package.manifest.module_number == module_number + }); + let rollback_package = row.2.as_ref().and_then(|previous| { + read_verified_package( + &package_file(root, previous), + &signature_file(root, previous), + RELEASE_TRUST_RAW, + ) + .ok() + .filter(|(_, package, digest)| { + digest == previous && package.manifest.module_number == module_number + }) + }); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_BEGIN_FAILED: {error}"))?; + if success { + transaction + .execute( + "UPDATE installed_modules SET state='ACTIVE',updated_at_unix_ms=?1 WHERE module_number=?2", + params![observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "SELF_TEST_PASS", + module_number, + "ACTIVE", + &row.1, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_COMMIT_FAILED: {error}"))?; + return Ok(receipt); + } + let (target_state, target_digest) = if let Some((_, previous_package, previous)) = + rollback_package + { + let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&previous_package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=NULL,permissions_json=?5,manifest_json=?6,updated_at_unix_ms=?7 WHERE module_number=?8", + params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, permissions_json, manifest_json, observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_AUTO_ROLLBACK_WRITE_FAILED: {error}"))?; + ("ROLLBACK_PENDING_SELF_TEST", previous) + } else { + transaction + .execute( + "UPDATE installed_modules SET state='FAILED_CLOSED',updated_at_unix_ms=?1 WHERE module_number=?2", + params![observed, module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_FAIL_CLOSED_WRITE_FAILED: {error}"))?; + ("FAILED_CLOSED", row.1) + }; + let _ = append_receipt( + &transaction, + "SELF_TEST_FAIL", + module_number, + target_state, + &target_digest, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_FAILURE_COMMIT_FAILED: {error}"))?; + Err(verification + .err() + .unwrap_or_else(|| "HOLOLAKE_MODULE_SELF_TEST_IDENTITY_MISMATCH".into())) +} + +pub async fn unmount_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + if input.module_number == MOBILE_SYNC_NUMBER { + crate::mobile_sync::stop_for_unmount(&app)?; + } + transition( + &runtime_root(&app)?, + &input.module_number, + &["ACTIVE", "MOUNTED_PENDING_SELF_TEST", "FAILED_CLOSED"], + "DORMANT", + "UNMOUNT", + ) +} + +pub async fn rollback_module( + app: AppHandle, + input: ModuleNumberInput, +) -> Result { + let root = runtime_root(&app)?; + let mut connection = open_db(&root)?; + let row: (String, Option) = connection + .query_row( + "SELECT package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", + params![input.module_number], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_READ_FAILED: {error}"))? + .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; + let previous = row.1.ok_or("HOLOLAKE_MODULE_ROLLBACK_NOT_AVAILABLE")?; + let (_, previous_package, verified_previous) = read_verified_package( + &package_file(&root, &previous), + &signature_file(&root, &previous), + RELEASE_TRUST_RAW, + )?; + if verified_previous != previous + || previous_package.manifest.module_number != input.module_number + { + return Err("HOLOLAKE_MODULE_ROLLBACK_PACKAGE_IDENTITY_INVALID".into()); + } + let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; + let manifest_json = serde_json::to_string(&previous_package.manifest) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; + let observed = now_unix_ms(); + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_BEGIN_FAILED: {error}"))?; + transaction + .execute( + "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=?5,permissions_json=?6,manifest_json=?7,updated_at_unix_ms=?8 WHERE module_number=?9", + params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, row.0, permissions_json, manifest_json, observed, input.module_number], + ) + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_WRITE_FAILED: {error}"))?; + let receipt = append_receipt( + &transaction, + "ROLLBACK", + &input.module_number, + "ROLLBACK_PENDING_SELF_TEST", + &previous, + observed, + )?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_COMMIT_FAILED: {error}"))?; + Ok(receipt) +} + +pub async fn get_module_runtime_snapshot(app: AppHandle) -> Result { + let root = runtime_root(&app)?; + snapshot_at(&root) +} + +fn snapshot_at(root: &Path) -> Result { + let connection = open_db(root)?; + verify_receipt_chain(&connection)?; + verify_persisted_modules(&connection, root)?; + let mut statement = connection + .prepare( + "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,updated_at_unix_ms FROM installed_modules ORDER BY module_number", + ) + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_PREPARE_FAILED: {error}"))?; + let modules = statement + .query_map([], |row| { + let permissions_json: String = row.get(7)?; + Ok(InstalledModule { + module_number: row.get(0)?, + display_name: row.get(1)?, + version: row.get(2)?, + adapter: row.get(3)?, + state: row.get(4)?, + package_sha256: row.get(5)?, + previous_package_sha256: row.get(6)?, + permissions: serde_json::from_str(&permissions_json).unwrap_or_default(), + user_data_preserved: true, + updated_at_unix_ms: row.get(8)?, + }) + }) + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_QUERY_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_ROW_FAILED: {error}"))?; + let receipt_count: u64 = connection + .query_row("SELECT COUNT(*) FROM module_receipts", [], |row| row.get(0)) + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_COUNT_FAILED: {error}"))?; + let latest_receipt_hash = connection + .query_row( + "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_TAIL_FAILED: {error}"))? + .unwrap_or_else(|| "0".repeat(64)); + Ok(ModuleRuntimeSnapshot { + schema: "hololake.module-runtime-snapshot/v1", + state: "ACTIVE_SIGNED_DECLARATIVE_PACKAGES_ONLY", + host_version: HOST_VERSION, + package_code_executed: false, + modules, + receipt_count, + latest_receipt_hash, + }) +} + +pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> { + let _ = contract()?; + let root = runtime_root(app); + match root { + Ok(root) => { + let _ = snapshot_at(&root)?; + Ok(()) + } + Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()), + Err(error) => Err(error), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn signed_fixture() -> (PathBuf, PathBuf, String) { + let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod"); + let signature = package.with_extension("ghmod.sig"); + let digest = sha256_hex(&fs::read(&package).unwrap()); + (package, signature, digest) + } + + fn install_fixture(root: &Path, observed: u64) -> String { + let (package, signature, digest) = signed_fixture(); + install_module_package_at( + root, + &InstallModulePackageInput { + package_path: package.to_string_lossy().into_owned(), + signature_path: signature.to_string_lossy().into_owned(), + expected_package_sha256: digest.clone(), + human_confirmed_permission_expansion: true, + }, + RELEASE_TRUST_RAW, + observed, + ) + .unwrap(); + digest + } + + #[test] + fn contract_keeps_packages_declarative_and_lifecycle_closed() { + let contract = contract().unwrap(); + assert_eq!(contract.registered_adapters.len(), 7); + assert!(!contract.host.arbitrary_native_code); + assert!(!contract.host.arbitrary_webview_javascript); + assert!(contract.host.declarative_payload_only); + } + + #[test] + fn package_number_classes_and_content_digest_are_exact() { + let payload = serde_json::json!({"views":[],"entry":"home"}); + let digest = payload_digest(&payload).unwrap(); + let package = ModulePackage { + schema: PACKAGE_SCHEMA.into(), + manifest: ModuleManifest { + module_number: "HLP-MOD-LOCAL-0001".into(), + registration_class: "PRIVATE_CHANNEL_LOCAL".into(), + display_name: "Local module".into(), + version: "1.0.0".into(), + minimum_host_version: "0.5.0".into(), + adapter: "native-composition-v1".into(), + content_digest: digest.clone(), + permissions: vec!["KNOWLEDGE_READ".into()], + user_data_schema: "hololake.module-data/local/v1".into(), + self_test: ModuleSelfTest { + kind: "DECLARATIVE_SCHEMA_V1".into(), + expected_content_digest: digest, + }, + }, + payload, + }; + let runtime_contract = contract().unwrap(); + validate_package(&package, &runtime_contract).unwrap(); + let mut invalid = package; + invalid.manifest.module_number = "HLP-DONOR-CAND-0001".into(); + assert_eq!( + validate_package(&invalid, &runtime_contract).unwrap_err(), + "HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID" + ); + } + + #[test] + fn tampered_persisted_manifest_fails_closed_without_touching_user_data() { + let temporary = tempfile::tempdir().unwrap(); + let root = temporary.path(); + let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; + install_fixture(root, 1); + fs::write( + root.join("user-data") + .join(module_number) + .join("human-data.json"), + b"preserve", + ) + .unwrap(); + let connection = open_db(root).unwrap(); + connection + .execute( + "UPDATE installed_modules SET manifest_json='{}' WHERE module_number=?1", + params![module_number], + ) + .unwrap(); + drop(connection); + assert!(snapshot_at(root) + .unwrap_err() + .starts_with("HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID")); + assert_eq!( + fs::read( + root.join("user-data") + .join(module_number) + .join("human-data.json") + ) + .unwrap(), + b"preserve" + ); + } + + #[test] + fn tampered_receipt_chain_fails_closed_on_restart() { + let temporary = tempfile::tempdir().unwrap(); + install_fixture(temporary.path(), 1); + let connection = open_db(temporary.path()).unwrap(); + connection + .execute( + "UPDATE module_receipts SET receipt_hash=?1 WHERE sequence=1", + params!["f".repeat(64)], + ) + .unwrap(); + drop(connection); + assert_eq!( + snapshot_at(temporary.path()).unwrap_err(), + "HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID" + ); + } + + #[test] + fn real_product_signed_fixture_verifies_without_executing_package_code() { + let (fixture, signature, _) = signed_fixture(); + let (_, package, package_sha256) = + read_verified_package(&fixture, &signature, RELEASE_TRUST_RAW).unwrap(); + assert_eq!( + package.manifest.module_number, + "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001" + ); + assert!(is_sha256(&package_sha256)); + } + + #[test] + fn signed_package_install_mount_self_test_restart_and_unmount_are_receipted() { + let (fixture, signature, expected) = signed_fixture(); + let temporary = tempfile::tempdir().unwrap(); + let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; + let mut input = InstallModulePackageInput { + package_path: fixture.to_string_lossy().into_owned(), + signature_path: signature.to_string_lossy().into_owned(), + expected_package_sha256: expected, + human_confirmed_permission_expansion: false, + }; + assert_eq!( + install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 100) + .unwrap_err(), + "HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION" + ); + input.human_confirmed_permission_expansion = true; + install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 101).unwrap(); + transition( + temporary.path(), + module_number, + &["INSTALLED_DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) + .unwrap(); + self_test_module_at(temporary.path(), module_number, 103).unwrap(); + let user_data = temporary + .path() + .join("user-data") + .join(module_number) + .join("human-data.json"); + fs::write(&user_data, b"preserve-after-unmount").unwrap(); + transition( + temporary.path(), + module_number, + &["ACTIVE"], + "DORMANT", + "UNMOUNT", + ) + .unwrap(); + let reopened = snapshot_at(temporary.path()).unwrap(); + assert_eq!(reopened.modules.len(), 1); + assert_eq!(reopened.modules[0].state, "DORMANT"); + assert_eq!(reopened.receipt_count, 4); + assert_eq!(fs::read(user_data).unwrap(), b"preserve-after-unmount"); + } + + #[test] + fn bundled_native_composition_package_completes_installed_lifecycle() { + let temporary = tempfile::tempdir().unwrap(); + let (_, package, digest) = read_verified_package_bytes( + NATIVE_COMPOSITION_PACKAGE, + NATIVE_COMPOSITION_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, NATIVE_COMPOSITION_NUMBER); + assert_eq!(package.manifest.adapter, "native-composition-v1"); + let (package_path, signature_path) = materialize_bundled_source( + temporary.path(), + NATIVE_COMPOSITION_NUMBER, + NATIVE_COMPOSITION_PACKAGE, + NATIVE_COMPOSITION_SIGNATURE, + ) + .unwrap(); + install_module_package_at( + temporary.path(), + &InstallModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + expected_package_sha256: digest, + human_confirmed_permission_expansion: true, + }, + RELEASE_TRUST_RAW, + 200, + ) + .unwrap(); + transition( + temporary.path(), + NATIVE_COMPOSITION_NUMBER, + &["INSTALLED_DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) + .unwrap(); + self_test_module_at(temporary.path(), NATIVE_COMPOSITION_NUMBER, 202).unwrap(); + let snapshot = snapshot_at(temporary.path()).unwrap(); + assert!(snapshot.modules.iter().any(|module| { + module.module_number == NATIVE_COMPOSITION_NUMBER + && module.adapter == "native-composition-v1" + && module.state == "ACTIVE" + })); + assert_eq!(snapshot.receipt_count, 3); + } + + #[test] + fn bundled_channel_workbench_has_an_independent_verified_identity() { + let (_, package, digest) = read_verified_package_bytes( + CHANNEL_WORKBENCH_PACKAGE, + CHANNEL_WORKBENCH_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER); + assert_eq!(package.manifest.adapter, "channel-workbench-v1"); + assert_eq!(package.manifest.permissions.len(), 4); + assert!(is_sha256(&digest)); + assert_eq!(BUNDLED_MODULES.len(), 11); + } + + #[test] + fn bundled_persona_channel_body_is_signed_without_claiming_persona_binding() { + let (_, package, digest) = read_verified_package_bytes( + PERSONA_CHANNEL_BODY_PACKAGE, + PERSONA_CHANNEL_BODY_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, PERSONA_CHANNEL_BODY_NUMBER); + assert_eq!(package.manifest.adapter, "persona-channel-body-v1"); + assert_eq!(package.manifest.permissions.len(), 7); + assert_eq!( + package.payload["adapterConfig"]["personaBindingClaimed"], + false + ); + assert!(is_sha256(&digest)); + } + + #[test] + fn bundled_education_workbench_is_an_official_signed_module() { + let (_, package, digest) = read_verified_package_bytes( + EDUCATION_WORKBENCH_PACKAGE, + EDUCATION_WORKBENCH_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, EDUCATION_WORKBENCH_NUMBER); + assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); + assert_eq!(package.manifest.adapter, "education-workbench-v1"); + assert_eq!(package.manifest.permissions.len(), 8); + assert_eq!( + package.payload["adapterConfig"]["importDefaultScope"], + "UNASSIGNED" + ); + assert!(is_sha256(&digest)); + } + + #[test] + fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() { + let expected = [ + ( + WEB_NOVEL_WORKBENCH_NUMBER, + WEB_NOVEL_WORKBENCH_PACKAGE, + WEB_NOVEL_WORKBENCH_SIGNATURE, + 8usize, + ), + ( + WEB_NOVEL_OUTLINE_NUMBER, + WEB_NOVEL_OUTLINE_PACKAGE, + WEB_NOVEL_OUTLINE_SIGNATURE, + 2usize, + ), + ( + WEB_NOVEL_GRID_NUMBER, + WEB_NOVEL_GRID_PACKAGE, + WEB_NOVEL_GRID_SIGNATURE, + 2usize, + ), + ( + WEB_NOVEL_STORYWORLD_NUMBER, + WEB_NOVEL_STORYWORLD_PACKAGE, + WEB_NOVEL_STORYWORLD_SIGNATURE, + 2usize, + ), + ( + WEB_NOVEL_DELIVERY_NUMBER, + WEB_NOVEL_DELIVERY_PACKAGE, + WEB_NOVEL_DELIVERY_SIGNATURE, + 2usize, + ), + ]; + for (number, bytes, signature, permission_count) in expected { + let (_, package, digest) = + read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW).unwrap(); + assert_eq!(package.manifest.module_number, number); + assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); + assert_eq!(package.manifest.adapter, "web-novel-workbench-v1"); + assert_eq!(package.manifest.permissions.len(), permission_count); + assert!(is_sha256(&digest)); + } + } + + #[test] + fn bundled_mobile_sync_is_signed_and_keeps_the_desktop_as_root_node() { + let (_, package, digest) = read_verified_package_bytes( + MOBILE_SYNC_PACKAGE, + MOBILE_SYNC_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, MOBILE_SYNC_NUMBER); + assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); + assert_eq!(package.manifest.adapter, "mobile-sync-v1"); + assert_eq!(package.manifest.permissions.len(), 5); + assert_eq!( + package.payload["adapterConfig"]["desktopRole"], + "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE" + ); + assert_eq!( + package.payload["adapterConfig"]["iosClientPackaging"], + "PENDING_SEPARATE_ADMISSION" + ); + assert!(is_sha256(&digest)); + } + + #[test] + fn bundled_dynamic_world_surface_is_signed_and_cannot_change_routes() { + let (_, package, digest) = read_verified_package_bytes( + DYNAMIC_WORLD_SURFACE_PACKAGE, + DYNAMIC_WORLD_SURFACE_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, DYNAMIC_WORLD_SURFACE_NUMBER); + assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); + assert_eq!( + package.manifest.adapter, + "dynamic-language-world-surface-v1" + ); + assert_eq!(package.manifest.permissions.len(), 2); + assert_eq!( + package.payload["adapterConfig"]["routingPermissionOrFactMutation"], + false + ); + assert_eq!( + package.payload["adapterConfig"]["legacyTraditionalWorkbench"], + "REJECTED_DUPLICATE_SHELL_AND_SIMULATED_STATE" + ); + assert!(is_sha256(&digest)); + } + + #[cfg(unix)] + #[test] + fn symlinked_package_input_is_rejected_before_signature_processing() { + use std::os::unix::fs::symlink; + + let temporary = tempfile::tempdir().unwrap(); + let source = temporary.path().join("source.ghmod"); + let link = temporary.path().join("link.ghmod"); + let signature = temporary.path().join("dummy.sig"); + fs::write(&source, b"{}").unwrap(); + fs::write(&signature, b"not-a-signature").unwrap(); + symlink(&source, &link).unwrap(); + assert_eq!( + read_verified_package(&link, &signature, RELEASE_TRUST_RAW).unwrap_err(), + "HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs b/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs new file mode 100644 index 000000000..51ef6d2e2 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs @@ -0,0 +1,818 @@ +//! HoloLake 原生组合执行内核。 +//! +//! 外来文件格式不进入这里。内核只接受登记过的模块、受限配方和当前登录账号的 +//! HoloLake 原生对象;人类投影只是同一执行结果的不同视图,没有源数据所有权。 + +use ring::digest::{digest, SHA256}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const REGISTRY_SCHEMA: &str = "hololake.composition-module-registry/v1"; +const OBJECT_SCHEMA: &str = "hololake.native-object/v1"; +const RECIPE_SCHEMA: &str = "hololake.composition-recipe/v1"; +const PROJECTION_SCHEMA: &str = "hololake.human-projection/v1"; +const MAX_ROWS: usize = 5_000; +const MAX_VIEWS: usize = 5; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CompositionDimension { + Source, + TopLevelFolder, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CompositionMeasure { + DocumentCount, + TotalBytes, + DuplicateCount, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProjectionView { + Dashboard, + Comparison, + VerticalBar, + Classification, + Table, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExecuteCompositionInput { + pub dimension: CompositionDimension, + pub measure: CompositionMeasure, + pub views: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleDescriptor { + pub module_id: &'static str, + pub display_name: &'static str, + pub kind: &'static str, + pub input_schema: Option<&'static str>, + pub output_schema: &'static str, + pub deterministic: bool, + pub authority: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleRegistry { + pub schema: &'static str, + pub state: &'static str, + pub modules: Vec, + pub arbitrary_script_allowed: bool, + pub unregistered_module_allowed: bool, + pub direct_projection_write_allowed: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeColumn { + pub column_id: &'static str, + pub title: &'static str, + pub value_type: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeKnowledgeRow { + pub row_id: String, + pub source: String, + pub path: String, + pub title: String, + pub top_level_folder: String, + pub size_bytes: u64, + pub duplicate_count: usize, + pub updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeObject { + pub schema: &'static str, + pub object_id: String, + pub title: &'static str, + pub columns: Vec, + pub rows: Vec, + pub row_count: usize, + pub truncated: bool, + pub source_receipt: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionNode { + pub node_id: String, + pub module_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionEdge { + pub from: String, + pub to: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionRecipe { + pub schema: &'static str, + pub recipe_id: &'static str, + pub title: &'static str, + pub nodes: Vec, + pub edges: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProjectionGroup { + pub key: String, + pub label: String, + pub document_count: usize, + pub total_bytes: u64, + pub duplicate_count: usize, + pub measure_value: u64, + pub share: f64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProjectionMetrics { + pub raw_document_count: usize, + pub unique_document_count: usize, + pub duplicate_document_count: usize, + pub total_bytes: u64, + pub group_count: usize, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeCompositionProjection { + pub schema: &'static str, + pub state: &'static str, + pub execution_id: String, + pub executed_at_unix_ms: u64, + pub source_is_real_account_data: bool, + pub read_only: bool, + pub dimension: CompositionDimension, + pub measure: CompositionMeasure, + pub views: Vec, + pub native_object: NativeObject, + pub groups: Vec, + pub metrics: ProjectionMetrics, + pub recipe: CompositionRecipe, + pub data_sha256: String, + pub receipt_sha256: String, +} + +fn registry() -> ModuleRegistry { + let modules = vec![ + descriptor( + "HLC-SOURCE-KNOWLEDGE-CATALOG", + "知识目录数据源", + "SOURCE", + None, + OBJECT_SCHEMA, + ), + descriptor( + "HLC-CLASSIFY-SOURCE", + "按来源分类", + "TRANSFORM", + Some(OBJECT_SCHEMA), + "hololake.classified-object/v1", + ), + descriptor( + "HLC-CLASSIFY-TOP-FOLDER", + "按一级目录分类", + "TRANSFORM", + Some(OBJECT_SCHEMA), + "hololake.classified-object/v1", + ), + descriptor( + "HLC-AGGREGATE-DOCUMENT-COUNT", + "统计文档数", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-AGGREGATE-TOTAL-BYTES", + "统计数据量", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-AGGREGATE-DUPLICATE-COUNT", + "统计重复数", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-PROJECT-DASHBOARD", + "仪表盘投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-COMPARISON", + "对比投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-VERTICAL-BAR", + "柱状图投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-CLASSIFICATION", + "分类投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-TABLE", + "表格投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + ]; + ModuleRegistry { + schema: REGISTRY_SCHEMA, + state: "READY", + modules, + arbitrary_script_allowed: false, + unregistered_module_allowed: false, + direct_projection_write_allowed: false, + } +} + +fn descriptor( + module_id: &'static str, + display_name: &'static str, + kind: &'static str, + input_schema: Option<&'static str>, + output_schema: &'static str, +) -> ModuleDescriptor { + ModuleDescriptor { + module_id, + display_name, + kind, + input_schema, + output_schema, + deterministic: true, + authority: "CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY", + } +} + +pub fn get_native_composition_module_registry() -> ModuleRegistry { + registry() +} + +pub async fn execute_knowledge_native_composition( + app: AppHandle, + input: ExecuteCompositionInput, +) -> Result { + crate::module_package_runtime::require_active_module_adapter( + &app, + "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "native-composition-v1", + )?; + validate_input(&input)?; + let snapshot = crate::knowledge_base::get_knowledge_snapshot(app).await?; + execute_snapshot(snapshot, input) +} + +fn validate_input(input: &ExecuteCompositionInput) -> Result<(), String> { + if input.views.is_empty() || input.views.len() > MAX_VIEWS { + return Err("HOLOLAKE_COMPOSITION_VIEW_COUNT_INVALID".into()); + } + let unique = input.views.iter().copied().collect::>(); + if unique.len() != input.views.len() { + return Err("HOLOLAKE_COMPOSITION_DUPLICATE_VIEW".into()); + } + Ok(()) +} + +fn execute_snapshot( + snapshot: crate::knowledge_base::KnowledgeSnapshot, + input: ExecuteCompositionInput, +) -> Result { + if snapshot.documents.len() > MAX_ROWS { + return Err("HOLOLAKE_COMPOSITION_ROW_LIMIT_EXCEEDED".into()); + } + let rows = snapshot + .documents + .iter() + .enumerate() + .map(|(index, document)| NativeKnowledgeRow { + row_id: format!("knowledge-row-{}", index + 1), + source: document.source.to_string(), + path: document.path.clone(), + title: document.title.clone(), + top_level_folder: top_level_folder(&document.path), + size_bytes: document.size_bytes, + duplicate_count: document.duplicate_count, + updated_at_unix_ms: document.updated_at_unix_ms.min(u64::MAX as u128) as u64, + }) + .collect::>(); + let data_sha256 = sha256_json(&rows)?; + let native_object = NativeObject { + schema: OBJECT_SCHEMA, + object_id: format!("knowledge-catalog-{}", &data_sha256[..16]), + title: "当前账号知识目录", + columns: native_columns(), + row_count: rows.len(), + rows, + truncated: snapshot.truncated, + source_receipt: "CURRENT_AUTHENTICATED_ACCOUNT_KNOWLEDGE_SNAPSHOT", + }; + let mut aggregates = BTreeMap::::new(); + for row in &native_object.rows { + let key = match input.dimension { + CompositionDimension::Source => source_label(&row.source), + CompositionDimension::TopLevelFolder => row.top_level_folder.clone(), + }; + let entry = aggregates.entry(key).or_default(); + entry.0 += 1; + entry.1 = entry.1.saturating_add(row.size_bytes); + entry.2 = entry.2.saturating_add(row.duplicate_count); + } + let total_measure = aggregates + .values() + .map(|value| measure_value(*value, input.measure)) + .sum::(); + let mut groups = aggregates + .into_iter() + .map(|(label, value)| ProjectionGroup { + key: stable_key(&label), + label, + document_count: value.0, + total_bytes: value.1, + duplicate_count: value.2, + measure_value: measure_value(value, input.measure), + share: if total_measure == 0 { + 0.0 + } else { + measure_value(value, input.measure) as f64 / total_measure as f64 + }, + }) + .collect::>(); + groups.sort_by(|left, right| { + right + .measure_value + .cmp(&left.measure_value) + .then_with(|| left.label.cmp(&right.label)) + }); + let recipe = compile_recipe(&input); + validate_recipe(&recipe, ®istry())?; + let metrics = ProjectionMetrics { + raw_document_count: snapshot.raw_document_count, + unique_document_count: snapshot.unique_document_count, + duplicate_document_count: snapshot.duplicate_document_count, + total_bytes: native_object.rows.iter().map(|row| row.size_bytes).sum(), + group_count: groups.len(), + }; + let executed_at_unix_ms = now_unix_ms(); + let receipt_material = serde_json::to_vec(&( + &data_sha256, + &recipe.nodes, + &recipe.edges, + &groups, + executed_at_unix_ms, + )) + .map_err(|error| format!("HOLOLAKE_COMPOSITION_RECEIPT_SERIALIZE_FAILED: {error}"))?; + Ok(NativeCompositionProjection { + schema: PROJECTION_SCHEMA, + state: "EXECUTED", + execution_id: format!("HLC-EXEC-{}", Uuid::new_v4()), + executed_at_unix_ms, + source_is_real_account_data: true, + read_only: true, + dimension: input.dimension, + measure: input.measure, + views: input.views, + native_object, + groups, + metrics, + recipe, + data_sha256, + receipt_sha256: sha256(&receipt_material), + }) +} + +fn native_columns() -> Vec { + vec![ + NativeColumn { + column_id: "title", + title: "标题", + value_type: "TEXT", + }, + NativeColumn { + column_id: "source", + title: "来源", + value_type: "TEXT", + }, + NativeColumn { + column_id: "topLevelFolder", + title: "一级分类", + value_type: "TEXT", + }, + NativeColumn { + column_id: "sizeBytes", + title: "数据量", + value_type: "INTEGER", + }, + NativeColumn { + column_id: "duplicateCount", + title: "重复数", + value_type: "INTEGER", + }, + NativeColumn { + column_id: "updatedAtUnixMs", + title: "更新时间", + value_type: "TIME", + }, + ] +} + +fn compile_recipe(input: &ExecuteCompositionInput) -> CompositionRecipe { + let classify = match input.dimension { + CompositionDimension::Source => "HLC-CLASSIFY-SOURCE", + CompositionDimension::TopLevelFolder => "HLC-CLASSIFY-TOP-FOLDER", + }; + let aggregate = match input.measure { + CompositionMeasure::DocumentCount => "HLC-AGGREGATE-DOCUMENT-COUNT", + CompositionMeasure::TotalBytes => "HLC-AGGREGATE-TOTAL-BYTES", + CompositionMeasure::DuplicateCount => "HLC-AGGREGATE-DUPLICATE-COUNT", + }; + let mut nodes = vec![ + CompositionNode { + node_id: "source".into(), + module_id: "HLC-SOURCE-KNOWLEDGE-CATALOG".into(), + }, + CompositionNode { + node_id: "classify".into(), + module_id: classify.into(), + }, + CompositionNode { + node_id: "aggregate".into(), + module_id: aggregate.into(), + }, + ]; + let mut edges = vec![ + CompositionEdge { + from: "source".into(), + to: "classify".into(), + }, + CompositionEdge { + from: "classify".into(), + to: "aggregate".into(), + }, + ]; + for (index, view) in input.views.iter().enumerate() { + let node_id = format!("projection-{}", index + 1); + nodes.push(CompositionNode { + node_id: node_id.clone(), + module_id: view_module_id(*view).into(), + }); + edges.push(CompositionEdge { + from: "aggregate".into(), + to: node_id, + }); + } + CompositionRecipe { + schema: RECIPE_SCHEMA, + recipe_id: "HLC-RECIPE-KNOWLEDGE-OVERVIEW-001", + title: "知识空间结构组合", + nodes, + edges, + } +} + +fn validate_recipe( + recipe: &CompositionRecipe, + module_registry: &ModuleRegistry, +) -> Result<(), String> { + let descriptors = module_registry + .modules + .iter() + .map(|item| (item.module_id, item)) + .collect::>(); + let nodes = recipe + .nodes + .iter() + .map(|item| (item.node_id.as_str(), item)) + .collect::>(); + if nodes.len() != recipe.nodes.len() { + return Err("HOLOLAKE_COMPOSITION_DUPLICATE_NODE".into()); + } + for node in &recipe.nodes { + if !descriptors.contains_key(node.module_id.as_str()) { + return Err("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED".into()); + } + } + let mut indegree = recipe + .nodes + .iter() + .map(|item| (item.node_id.as_str(), 0usize)) + .collect::>(); + let mut outgoing = BTreeMap::<&str, Vec<&str>>::new(); + for edge in &recipe.edges { + let from = nodes + .get(edge.from.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + let to = nodes + .get(edge.to.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + let from_descriptor = descriptors + .get(from.module_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?; + let to_descriptor = descriptors + .get(to.module_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?; + if to_descriptor.input_schema != Some(from_descriptor.output_schema) { + return Err("HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH".into()); + } + *indegree + .get_mut(to.node_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")? += 1; + outgoing + .entry(from.node_id.as_str()) + .or_default() + .push(to.node_id.as_str()); + } + let mut queue = indegree + .iter() + .filter_map(|(id, degree)| (*degree == 0).then_some(*id)) + .collect::>(); + let mut visited = 0usize; + while let Some(node) = queue.pop_front() { + visited += 1; + for target in outgoing.get(node).into_iter().flatten() { + let degree = indegree + .get_mut(target) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + *degree -= 1; + if *degree == 0 { + queue.push_back(target); + } + } + } + if visited != recipe.nodes.len() { + return Err("HOLOLAKE_COMPOSITION_CYCLE_REJECTED".into()); + } + Ok(()) +} + +fn view_module_id(view: ProjectionView) -> &'static str { + match view { + ProjectionView::Dashboard => "HLC-PROJECT-DASHBOARD", + ProjectionView::Comparison => "HLC-PROJECT-COMPARISON", + ProjectionView::VerticalBar => "HLC-PROJECT-VERTICAL-BAR", + ProjectionView::Classification => "HLC-PROJECT-CLASSIFICATION", + ProjectionView::Table => "HLC-PROJECT-TABLE", + } +} + +fn measure_value(value: (usize, u64, usize), measure: CompositionMeasure) -> u64 { + match measure { + CompositionMeasure::DocumentCount => value.0 as u64, + CompositionMeasure::TotalBytes => value.1, + CompositionMeasure::DuplicateCount => value.2 as u64, + } +} + +fn top_level_folder(path: &str) -> String { + let normalized = path.replace('\\', "/"); + let parts = normalized + .split('/') + .filter(|part| !part.is_empty()) + .collect::>(); + // “导入/导入批次名”是转译外壳,不是人类真正要比较的知识分类。 + let candidate = if parts.first() == Some(&"导入") { + parts.get(2).copied().unwrap_or("根页面") + } else { + parts.first().copied().unwrap_or("未分类") + }; + if candidate.ends_with(".md") || candidate.ends_with(".markdown") || candidate.ends_with(".txt") + { + "根目录".into() + } else { + candidate.into() + } +} + +fn source_label(source: &str) -> String { + match source { + "native" => "光湖原生知识".into(), + "legacy" => "待迁移知识".into(), + other => other.into(), + } +} + +fn stable_key(value: &str) -> String { + sha256(value.as_bytes())[..16].to_string() +} +fn sha256_json(value: &T) -> Result { + serde_json::to_vec(value) + .map(|bytes| sha256(&bytes)) + .map_err(|error| format!("HOLOLAKE_COMPOSITION_DATA_SERIALIZE_FAILED: {error}")) +} +fn sha256(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|value| value.as_millis().min(u64::MAX as u128) as u64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::knowledge_base::{KnowledgeDocumentSummary, KnowledgeSnapshot}; + + fn snapshot() -> KnowledgeSnapshot { + KnowledgeSnapshot { + schema: "hololake.native-knowledge-base/v1", + state: "READY", + native_root: "/private/account".into(), + legacy_available: false, + legacy_root: None, + documents: vec![ + KnowledgeDocumentSummary { + source: "native", + path: "课程/第一课.md".into(), + title: "第一课".into(), + updated_at_unix_ms: 1, + size_bytes: 100, + content_sha256: "a".into(), + duplicate_count: 1, + }, + KnowledgeDocumentSummary { + source: "native", + path: "课程/第二课.md".into(), + title: "第二课".into(), + updated_at_unix_ms: 2, + size_bytes: 300, + content_sha256: "b".into(), + duplicate_count: 0, + }, + KnowledgeDocumentSummary { + source: "native", + path: "记录.md".into(), + title: "记录".into(), + updated_at_unix_ms: 3, + size_bytes: 50, + content_sha256: "c".into(), + duplicate_count: 0, + }, + ], + raw_document_count: 4, + unique_document_count: 3, + duplicate_document_count: 1, + truncated: false, + } + } + + #[test] + fn real_native_rows_feed_all_projection_views_from_one_result() { + let result = execute_snapshot( + snapshot(), + ExecuteCompositionInput { + dimension: CompositionDimension::TopLevelFolder, + measure: CompositionMeasure::TotalBytes, + views: vec![ + ProjectionView::Dashboard, + ProjectionView::VerticalBar, + ProjectionView::Table, + ], + }, + ) + .unwrap(); + assert_eq!(result.state, "EXECUTED"); + assert_eq!(result.native_object.row_count, 3); + assert_eq!(result.metrics.total_bytes, 450); + assert_eq!(result.groups[0].label, "课程"); + assert_eq!(result.groups[0].measure_value, 400); + assert!(result.source_is_real_account_data); + assert!(result.read_only); + } + + #[test] + fn duplicate_or_empty_projection_requests_fail_closed() { + assert!(validate_input(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![] + }) + .is_err()); + assert!(validate_input(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table, ProjectionView::Table] + }) + .is_err()); + } + + #[test] + fn imported_wrapper_folders_do_not_flatten_real_knowledge_categories() { + assert_eq!( + top_level_folder("导入/光湖语言世界/AI技能包/索引.md"), + "AI技能包" + ); + assert_eq!(top_level_folder("导入/光湖语言世界/首页.md"), "根目录"); + assert_eq!(top_level_folder("课程/第一课.md"), "课程"); + } + + #[test] + fn unknown_modules_type_mismatches_and_cycles_fail_closed() { + let mut unknown = compile_recipe(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table], + }); + unknown.nodes[0].module_id = "UNKNOWN".into(); + assert_eq!( + validate_recipe(&unknown, ®istry()).unwrap_err(), + "HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED" + ); + + let mut mismatch = compile_recipe(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table], + }); + mismatch.edges[0] = CompositionEdge { + from: "aggregate".into(), + to: "classify".into(), + }; + assert_eq!( + validate_recipe(&mismatch, ®istry()).unwrap_err(), + "HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH" + ); + + let cycle_registry = ModuleRegistry { + schema: REGISTRY_SCHEMA, + state: "TEST", + modules: vec![ + descriptor("LOOP-A", "A", "TRANSFORM", Some("loop/v1"), "loop/v1"), + descriptor("LOOP-B", "B", "TRANSFORM", Some("loop/v1"), "loop/v1"), + ], + arbitrary_script_allowed: false, + unregistered_module_allowed: false, + direct_projection_write_allowed: false, + }; + let cycle = CompositionRecipe { + schema: RECIPE_SCHEMA, + recipe_id: "TEST-CYCLE", + title: "cycle", + nodes: vec![ + CompositionNode { + node_id: "a".into(), + module_id: "LOOP-A".into(), + }, + CompositionNode { + node_id: "b".into(), + module_id: "LOOP-B".into(), + }, + ], + edges: vec![ + CompositionEdge { + from: "a".into(), + to: "b".into(), + }, + CompositionEdge { + from: "b".into(), + to: "a".into(), + }, + ], + }; + assert_eq!( + validate_recipe(&cycle, &cycle_registry).unwrap_err(), + "HOLOLAKE_COMPOSITION_CYCLE_REJECTED" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs new file mode 100644 index 000000000..aabd6ade9 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -0,0 +1,204 @@ +use serde::Deserialize; +use std::collections::HashSet; + +const TREE: &str = include_str!("../../generated/unified-number-coordinate-tree.json"); + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NumberTree { + schema: String, + record_id: String, + state: String, + root_number: String, + coordinate_count: usize, + route_count: usize, + identity_node_count: usize, + protocol_node_count: usize, + reference_only_node_count: usize, + identity_nodes: Vec, + protocol_nodes: Vec, + routes: Vec, + invariants: NumberTreeInvariants, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct IdentityNode { + node_kind: String, + node_number: String, + namespace_id: String, + subject_kind: String, + domain_scope: String, + admission: String, + execution_state: String, + evidence: String, + path: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ProtocolNode { + node_kind: String, + node_number: String, + protocol_id: String, + source_state: String, + execution_state: String, + evidence: String, + path: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NumberRoute { + transport: String, + protocol_version: String, + caller_number: String, + channel_number: String, + module_number: String, + operation_number: String, + target_number: String, + admission: String, + effect: String, + evidence: String, + path: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NumberTreeInvariants { + number_is_stable_coordinate_not_authority: bool, + path_is_unique_navigation: bool, + admission_is_separate_from_identity: bool, + every_physical_call_has_numbered_route: bool, + every_accepted_call_has_evidence_class: bool, + every_protocol_reference_has_number_coordinate: bool, + reference_only_nodes_never_executable: bool, + unresolved_number_reference_count: usize, + mismatched_coordinate: String, +} + +pub(crate) fn start_on_application_open() -> Result<(), String> { + validate_tree() +} + +fn validate_tree() -> Result<(), String> { + let tree: NumberTree = serde_json::from_str(TREE) + .map_err(|error| format!("HOLOLAKE_UNIFIED_NUMBER_TREE_INVALID: {error}"))?; + if tree.schema != "hololake.unified-number-coordinate-tree/v2" + || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" + || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" + || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" + || tree.coordinate_count != 272 + || tree.route_count != 169 + || tree.routes.len() != tree.route_count + || tree.identity_node_count != 4 + || tree.identity_nodes.len() != tree.identity_node_count + || tree.protocol_node_count != 99 + || tree.protocol_nodes.len() != tree.protocol_node_count + || tree.reference_only_node_count != 16 + || tree.coordinate_count + != tree.route_count + tree.identity_node_count + tree.protocol_node_count + || !tree.invariants.number_is_stable_coordinate_not_authority + || !tree.invariants.path_is_unique_navigation + || !tree.invariants.admission_is_separate_from_identity + || !tree.invariants.every_physical_call_has_numbered_route + || !tree.invariants.every_accepted_call_has_evidence_class + || !tree + .invariants + .every_protocol_reference_has_number_coordinate + || !tree.invariants.reference_only_nodes_never_executable + || tree.invariants.unresolved_number_reference_count != 0 + || tree.invariants.mismatched_coordinate != "FAIL_CLOSED" + { + return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_BOUNDARY_INVALID".into()); + } + let mut paths = HashSet::new(); + let mut coordinates = HashSet::new(); + let mut node_numbers = HashSet::new(); + let mut protocol_ids = HashSet::new(); + for node in &tree.identity_nodes { + if node.node_kind != "IDENTITY_NAMESPACE" + || !node.node_number.starts_with("HLP-IDENTITY-NS-") + || node.namespace_id.is_empty() + || node.subject_kind.is_empty() + || node.domain_scope.is_empty() + || !matches!( + node.admission.as_str(), + "REGISTERED_HUMAN_NAMESPACE" | "NON_HUMAN_NAMESPACE" + ) + || node.execution_state != "AUTHORITY_RESOLUTION_ONLY" + || node.evidence.is_empty() + || !node.path.starts_with("HLP-NUMBER-WORLD-ROOT-001/IDENTITY/") + || !paths.insert(node.path.as_str()) + || !node_numbers.insert(node.node_number.as_str()) + { + return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_IDENTITY_NODE_INVALID".into()); + } + } + let mut reference_only_count = 0; + for node in &tree.protocol_nodes { + let valid_state = match node.node_kind.as_str() { + "GLS_PROTOCOL_SOURCE" => { + matches!( + node.execution_state.as_str(), + "EXECUTABLE_PROJECTION" | "INVENTORIED_NOT_EXECUTABLE" + ) && matches!( + node.source_state.as_str(), + "HDLP_PROTOCOL_SOURCE" | "LEGACY_MARKDOWN_EVIDENCE" + ) + } + "GLS_REFERENCE_ONLY" => { + reference_only_count += 1; + node.execution_state == "REFERENCE_ONLY_NOT_EXECUTABLE" + && node.source_state == "ROADMAP_REFERENCE_ONLY" + && node.node_number.starts_with("HLP-GLS-REF-") + } + _ => false, + }; + if !valid_state + || !node.protocol_id.starts_with("GLS-") + || node.evidence.is_empty() + || !node.path.starts_with("HLP-NUMBER-WORLD-ROOT-001/GLS/") + || !paths.insert(node.path.as_str()) + || !node_numbers.insert(node.node_number.as_str()) + || !protocol_ids.insert(node.protocol_id.as_str()) + { + return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_PROTOCOL_NODE_INVALID".into()); + } + } + if reference_only_count != tree.reference_only_node_count { + return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_REFERENCE_COUNT_INVALID".into()); + } + for route in &tree.routes { + let coordinate = format!( + "{}:{}:{}:{}:{}:{}", + route.transport, + route.channel_number, + route.module_number, + route.operation_number, + route.target_number, + route.caller_number + ); + if route.protocol_version.is_empty() + || route.admission.is_empty() + || !matches!(route.effect.as_str(), "READ_OR_STATUS" | "STATE_CHANGE") + || route.evidence.is_empty() + || !route.path.starts_with("HLP-NUMBER-WORLD-ROOT-001/") + || !paths.insert(route.path.as_str()) + || !coordinates.insert(coordinate) + { + return Err("HOLOLAKE_UNIFIED_NUMBER_TREE_ROUTE_INVALID".into()); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unified_number_tree_is_unique_permissioned_and_evidenced() { + validate_tree().unwrap(); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs new file mode 100644 index 000000000..c35cea065 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -0,0 +1,1041 @@ +//! HoloLake numbered IPC root. +//! +//! Stable numbers are coordinates, not authority. Every operation first receives a +//! short-lived server-issued grant bound to the exact caller/channel/module/operation/ +//! target tuple, protocol version, nonce and canonical payload digest. The grant is +//! consumed once before the internal handler runs. No business handler is registered +//! directly with Tauri. + +use base64::Engine; +use ring::{digest, hmac, rand as ring_rand}; +use ring_rand::SecureRandom; +use rusqlite::{params, Connection, OptionalExtension}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::{AppHandle, Manager, State}; +use uuid::Uuid; + +const EMBEDDED_REGISTRY: &str = include_str!("../../contracts/numbered-ipc-registry.json"); +const EXPECTED_SCHEMA: &str = "hololake.numbered-ipc-registry/v1"; +const EXPECTED_RECORD_ID: &str = "HLP-NUMBERED-IPC-ROOT-001"; + +#[derive(Clone, Debug, Deserialize)] +struct NumberedIpcRegistry { + schema: String, + record_id: String, + runtime: RuntimeRules, + payload_contract: PayloadContract, + operations: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +struct RuntimeRules { + public_tauri_command: String, + caller_number: String, + caller_number_subject_kind: String, + caller_number_grants_persona_binding: bool, + protocol_version: String, + legacy_direct_commands_allowed: bool, + grants_issued_server_side: bool, + grant_single_use: bool, + payload_bound_grants: bool, + authority_binding_issued_server_side: bool, + verified_human_binding_required_for_protected_routes: bool, + persona_binding_claimed: bool, + user_channel_body_binding_stage: String, + maximum_grant_ttl_ms: u64, + unknown_route: String, + mismatched_coordinate: String, + expired_or_replayed_grant: String, + receipt_required: bool, +} + +#[derive(Clone, Debug, Deserialize)] +struct PayloadContract { + unknown_or_unclassified_alias: String, + unknown_top_level_field: String, + empty_object_aliases: Vec, + input_wrapper_aliases: Vec, + direct_field_aliases: HashMap>, +} + +#[derive(Clone, Debug, Deserialize)] +struct OperationRoute { + operation_number: String, + alias: String, + handler: String, + channel_number: String, + module_number: String, + target_number: String, + admission: String, + effect: String, + payload_schema: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +struct NumberedCoordinate { + protocol_version: String, + caller_number: String, + channel_number: String, + module_number: String, + operation_number: String, + target_number: String, + request_nonce: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct NumberedEnvelope { + #[serde(flatten)] + coordinate: NumberedCoordinate, + #[serde(default = "default_ttl_ms")] + requested_ttl_ms: u64, + #[serde(default)] + payload: Value, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum NumberedIpcAction { + IssueGrant, + Execute, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct NumberedIpcInput { + action: NumberedIpcAction, + envelope: NumberedEnvelope, + grant: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberedGrant { + grant_id: String, + expires_at_unix_ms: u64, + payload_sha256: String, + authority_binding_sha256: String, + signature: String, +} + +#[derive(Clone, Debug)] +struct GrantRecord { + coordinate: NumberedCoordinate, + expires_at_unix_ms: u64, + payload_sha256: String, + authority_binding_sha256: String, + signature: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberedIpcReceipt { + receipt_id: String, + state: String, + decision: String, + operation_number: String, + payload_sha256: String, + authority_binding_sha256: String, + observed_at_unix_ms: u64, + previous_receipt_hash: String, + receipt_hash: String, + error: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NumberedIpcResponse { + schema: &'static str, + decision: String, + grant: Option, + result: Option, + error: Option, + receipt: NumberedIpcReceipt, +} + +pub struct NumberedIpcState { + signing_key: hmac::Key, + grants: Mutex>, + ledger: Mutex<()>, +} + +impl Default for NumberedIpcState { + fn default() -> Self { + let random = ring_rand::SystemRandom::new(); + let mut secret = [0_u8; 32]; + random + .fill(&mut secret) + .expect("operating system randomness is required for numbered IPC"); + Self { + signing_key: hmac::Key::new(hmac::HMAC_SHA256, &secret), + grants: Mutex::new(HashMap::new()), + ledger: Mutex::new(()), + } + } +} + +fn default_ttl_ms() -> u64 { + 10_000 +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest::digest(&digest::SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn canonical_value(value: &Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()), + Value::Object(object) => { + let mut keys = object.keys().collect::>(); + keys.sort(); + let mut canonical = Map::new(); + for key in keys { + canonical.insert(key.clone(), canonical_value(&object[key])); + } + Value::Object(canonical) + } + _ => value.clone(), + } +} + +fn payload_digest(payload: &Value) -> Result { + let bytes = serde_json::to_vec(&canonical_value(payload)) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_PAYLOAD_INVALID: {error}"))?; + if bytes.len() > 1_048_576 { + return Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_TOO_LARGE".into()); + } + Ok(sha256_hex(&bytes)) +} + +fn grant_signing_bytes( + grant_id: &str, + coordinate: &NumberedCoordinate, + expires_at_unix_ms: u64, + payload_sha256: &str, + authority_binding_sha256: &str, +) -> Vec { + format!( + "HLP-NIPC-GRANT-v1\n{grant_id}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{expires_at_unix_ms}\n{payload_sha256}\n{authority_binding_sha256}", + coordinate.protocol_version, + coordinate.caller_number, + coordinate.channel_number, + coordinate.module_number, + coordinate.operation_number, + coordinate.target_number, + coordinate.request_nonce, + EXPECTED_RECORD_ID, + ) + .into_bytes() +} + +fn load_registry() -> Result { + let registry: NumberedIpcRegistry = serde_json::from_str(EMBEDDED_REGISTRY) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_REGISTRY_INVALID: {error}"))?; + validate_registry(®istry)?; + Ok(registry) +} + +fn validate_registry(registry: &NumberedIpcRegistry) -> Result<(), String> { + let runtime = ®istry.runtime; + if registry.schema != EXPECTED_SCHEMA + || registry.record_id != EXPECTED_RECORD_ID + || runtime.public_tauri_command != "numbered_ipc" + || runtime.caller_number_subject_kind != "PHYSICAL_WEBVIEW_ENTRY_NOT_PERSONA_IDENTITY" + || runtime.caller_number_grants_persona_binding + || runtime.legacy_direct_commands_allowed + || !runtime.grants_issued_server_side + || !runtime.grant_single_use + || !runtime.payload_bound_grants + || !runtime.authority_binding_issued_server_side + || !runtime.verified_human_binding_required_for_protected_routes + || runtime.persona_binding_claimed + || runtime.user_channel_body_binding_stage != "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED" + || !runtime.receipt_required + || runtime.maximum_grant_ttl_ms == 0 + || runtime.maximum_grant_ttl_ms > 30_000 + || runtime.unknown_route != "FAIL_CLOSED" + || runtime.mismatched_coordinate != "FAIL_CLOSED" + || runtime.expired_or_replayed_grant != "FAIL_CLOSED" + || registry.operations.len() < 60 + { + return Err("HOLOLAKE_NUMBERED_IPC_REGISTRY_BOUNDARY_INVALID".into()); + } + if registry.payload_contract.unknown_or_unclassified_alias != "FAIL_CLOSED_BEFORE_GRANT" + || registry.payload_contract.unknown_top_level_field != "FAIL_CLOSED_BEFORE_GRANT" + { + return Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_CONTRACT_INVALID".into()); + } + let mut operation_numbers = HashSet::new(); + let mut aliases = HashSet::new(); + let mut coordinates = HashSet::new(); + for route in ®istry.operations { + let coordinate = format!( + "{}/{}/{}/{}", + route.channel_number, route.module_number, route.operation_number, route.target_number + ); + if !operation_numbers.insert(route.operation_number.as_str()) + || !aliases.insert(route.alias.as_str()) + || !coordinates.insert(coordinate) + || !route.operation_number.starts_with("HLP-NIPC-OP-") + || !route.module_number.starts_with("HLP-NIPC-MOD-") + || !route.channel_number.starts_with("HLP-NIPC-CH-") + || !route.target_number.starts_with("HLP-NIPC-TGT-") + || !route.handler.contains("::") + || !matches!( + route.admission.as_str(), + "PREAUTH_SYSTEM_ROUTE" | "VERIFIED_HUMAN_ROUTE" + ) + || !matches!(route.effect.as_str(), "READ_OR_STATUS" | "STATE_CHANGE") + || !route + .payload_schema + .starts_with("hololake.numbered-ipc.payload/") + { + return Err("HOLOLAKE_NUMBERED_IPC_ROUTE_INVALID".into()); + } + } + let mut payload_aliases = HashSet::new(); + for alias in registry + .payload_contract + .empty_object_aliases + .iter() + .chain(registry.payload_contract.input_wrapper_aliases.iter()) + .chain(registry.payload_contract.direct_field_aliases.keys()) + { + if !payload_aliases.insert(alias.as_str()) { + return Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_ALIAS_DUPLICATED".into()); + } + } + if payload_aliases != aliases { + return Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_COVERAGE_INCOMPLETE".into()); + } + Ok(()) +} + +fn validate_payload_shape( + registry: &NumberedIpcRegistry, + route: &OperationRoute, + payload: &Value, +) -> Result<(), String> { + let object = payload + .as_object() + .ok_or_else(|| "HOLOLAKE_NUMBERED_IPC_PAYLOAD_OBJECT_REQUIRED".to_string())?; + if registry + .payload_contract + .empty_object_aliases + .iter() + .any(|alias| alias == &route.alias) + { + return if object.is_empty() { + Ok(()) + } else { + Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_FIELD_UNKNOWN".into()) + }; + } + if registry + .payload_contract + .input_wrapper_aliases + .iter() + .any(|alias| alias == &route.alias) + { + return if object.len() == 1 && object.contains_key("input") { + Ok(()) + } else { + Err("HOLOLAKE_NUMBERED_IPC_INPUT_WRAPPER_REQUIRED".into()) + }; + } + if let Some(required_fields) = registry + .payload_contract + .direct_field_aliases + .get(&route.alias) + { + let required = required_fields + .iter() + .map(String::as_str) + .collect::>(); + let observed = object.keys().map(String::as_str).collect::>(); + if observed != required + || required_fields + .iter() + .any(|field| !object.get(field).is_some_and(Value::is_string)) + { + return Err("HOLOLAKE_NUMBERED_IPC_DIRECT_FIELDS_INVALID".into()); + } + return Ok(()); + } + Err("HOLOLAKE_NUMBERED_IPC_PAYLOAD_ALIAS_UNCLASSIFIED".into()) +} + +fn resolve_route<'a>( + registry: &'a NumberedIpcRegistry, + coordinate: &NumberedCoordinate, +) -> Result<&'a OperationRoute, String> { + if coordinate.protocol_version != registry.runtime.protocol_version + || coordinate.caller_number != registry.runtime.caller_number + { + return Err("HOLOLAKE_NUMBERED_IPC_CALLER_OR_PROTOCOL_MISMATCH".into()); + } + if coordinate.request_nonce.len() < 16 + || coordinate.request_nonce.len() > 128 + || coordinate + .request_nonce + .chars() + .any(|character| character.is_whitespace() || character.is_control()) + { + return Err("HOLOLAKE_NUMBERED_IPC_NONCE_INVALID".into()); + } + let route = registry + .operations + .iter() + .find(|route| route.operation_number == coordinate.operation_number) + .ok_or_else(|| "HOLOLAKE_NUMBERED_IPC_ROUTE_UNKNOWN".to_string())?; + if route.channel_number != coordinate.channel_number + || route.module_number != coordinate.module_number + || route.target_number != coordinate.target_number + { + return Err("HOLOLAKE_NUMBERED_IPC_ROUTE_COORDINATE_MISMATCH".into()); + } + Ok(route) +} + +fn enforce_admission(app: &AppHandle, route: &OperationRoute) -> Result { + if route.admission == "PREAUTH_SYSTEM_ROUTE" { + return Ok("SYSTEM_PHYSICAL_ENTRY:HLP-NIPC-CALLER-MAIN-WEBVIEW-0001".into()); + } + let state = app.state::(); + match crate::zero_point::verified_user_route(&state)? { + Some((human_number, registry_domain)) => { + Ok(format!("VERIFIED_HUMAN:{registry_domain}:{human_number}")) + } + None => Err("HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED".into()), + } +} + +fn issue_grant_at( + state: &NumberedIpcState, + registry: &NumberedIpcRegistry, + envelope: &NumberedEnvelope, + authority_binding: &str, + now: u64, +) -> Result<(NumberedGrant, String), String> { + let route = resolve_route(registry, &envelope.coordinate)?; + validate_payload_shape(registry, route, &envelope.payload)?; + if envelope.requested_ttl_ms == 0 + || envelope.requested_ttl_ms > registry.runtime.maximum_grant_ttl_ms + { + return Err("HOLOLAKE_NUMBERED_IPC_GRANT_TTL_INVALID".into()); + } + let payload_sha256 = payload_digest(&envelope.payload)?; + let authority_binding_sha256 = sha256_hex(authority_binding.as_bytes()); + let grant_id = format!("HLP-NIPC-GRANT-{}", Uuid::new_v4().simple()); + let expires_at_unix_ms = now.saturating_add(envelope.requested_ttl_ms); + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hmac::sign( + &state.signing_key, + &grant_signing_bytes( + &grant_id, + &envelope.coordinate, + expires_at_unix_ms, + &payload_sha256, + &authority_binding_sha256, + ), + )); + let record = GrantRecord { + coordinate: envelope.coordinate.clone(), + expires_at_unix_ms, + payload_sha256: payload_sha256.clone(), + authority_binding_sha256: authority_binding_sha256.clone(), + signature: signature.clone(), + }; + let mut grants = state + .grants + .lock() + .map_err(|_| "HOLOLAKE_NUMBERED_IPC_GRANT_LOCK_POISONED".to_string())?; + grants.retain(|_, existing| existing.expires_at_unix_ms >= now); + grants.insert(grant_id.clone(), record); + Ok(( + NumberedGrant { + grant_id, + expires_at_unix_ms, + payload_sha256: payload_sha256.clone(), + authority_binding_sha256, + signature, + }, + payload_sha256, + )) +} + +fn consume_grant_at( + state: &NumberedIpcState, + registry: &NumberedIpcRegistry, + envelope: &NumberedEnvelope, + grant: &NumberedGrant, + authority_binding: &str, + now: u64, +) -> Result { + let route = resolve_route(registry, &envelope.coordinate)?; + validate_payload_shape(registry, route, &envelope.payload)?; + let payload_sha256 = payload_digest(&envelope.payload)?; + let authority_binding_sha256 = sha256_hex(authority_binding.as_bytes()); + let record = state + .grants + .lock() + .map_err(|_| "HOLOLAKE_NUMBERED_IPC_GRANT_LOCK_POISONED".to_string())? + .remove(&grant.grant_id) + .ok_or_else(|| "HOLOLAKE_NUMBERED_IPC_GRANT_UNKNOWN_OR_REPLAYED".to_string())?; + if now > record.expires_at_unix_ms + || grant.expires_at_unix_ms != record.expires_at_unix_ms + || grant.payload_sha256 != record.payload_sha256 + || payload_sha256 != record.payload_sha256 + || grant.authority_binding_sha256 != record.authority_binding_sha256 + || authority_binding_sha256 != record.authority_binding_sha256 + || grant.signature != record.signature + || envelope.coordinate != record.coordinate + { + return Err("HOLOLAKE_NUMBERED_IPC_GRANT_MISMATCH_OR_EXPIRED".into()); + } + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(&grant.signature) + .map_err(|_| "HOLOLAKE_NUMBERED_IPC_GRANT_SIGNATURE_INVALID".to_string())?; + hmac::verify( + &state.signing_key, + &grant_signing_bytes( + &grant.grant_id, + &envelope.coordinate, + grant.expires_at_unix_ms, + &grant.payload_sha256, + &grant.authority_binding_sha256, + ), + &signature, + ) + .map_err(|_| "HOLOLAKE_NUMBERED_IPC_GRANT_SIGNATURE_INVALID".to_string())?; + Ok(payload_sha256) +} + +fn ledger_path(app: &AppHandle) -> Result { + let root = app + .path() + .app_data_dir() + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_HOME_FAILED: {error}"))? + .join("numbered-ipc-v1"); + fs::create_dir_all(&root) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_HOME_CREATE_FAILED: {error}"))?; + Ok(root.join("receipts.sqlite3")) +} + +fn open_ledger(app: &AppHandle) -> Result { + let connection = Connection::open(ledger_path(app)?) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_OPEN_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA busy_timeout=5000; + CREATE TABLE IF NOT EXISTS receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + receipt_id TEXT NOT NULL UNIQUE, + state TEXT NOT NULL, + decision TEXT NOT NULL, + operation_number TEXT NOT NULL, + payload_sha256 TEXT NOT NULL, + authority_binding_sha256 TEXT NOT NULL DEFAULT '', + observed_at_unix_ms INTEGER NOT NULL, + previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE, + error TEXT + );", + ) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_SCHEMA_FAILED: {error}"))?; + let has_authority_binding = { + let mut statement = connection + .prepare("PRAGMA table_info(receipts)") + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_SCHEMA_READ_FAILED: {error}"))?; + let columns = statement + .query_map([], |row| row.get::<_, String>(1)) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_SCHEMA_READ_FAILED: {error}"))?; + let mut found = false; + for column in columns { + if column.map_err(|error| { + format!("HOLOLAKE_NUMBERED_IPC_LEDGER_SCHEMA_READ_FAILED: {error}") + })? == "authority_binding_sha256" + { + found = true; + } + } + found + }; + if !has_authority_binding { + connection + .execute( + "ALTER TABLE receipts ADD COLUMN authority_binding_sha256 TEXT NOT NULL DEFAULT ''", + [], + ) + .map_err(|error| { + format!("HOLOLAKE_NUMBERED_IPC_LEDGER_SCHEMA_MIGRATION_FAILED: {error}") + })?; + } + Ok(connection) +} + +#[allow(clippy::too_many_arguments)] +fn append_receipt( + app: &AppHandle, + state: &NumberedIpcState, + event_state: &str, + decision: &str, + operation_number: &str, + payload_sha256: &str, + authority_binding_sha256: &str, + error: Option, + now: u64, +) -> Result { + let _guard = state + .ledger + .lock() + .map_err(|_| "HOLOLAKE_NUMBERED_IPC_LEDGER_LOCK_POISONED".to_string())?; + let connection = open_ledger(app)?; + let previous_receipt_hash = connection + .query_row( + "SELECT receipt_hash FROM receipts ORDER BY sequence DESC LIMIT 1", + [], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_READ_FAILED: {error}"))? + .unwrap_or_else(|| "0".repeat(64)); + let receipt_id = format!("HLP-NIPC-RECEIPT-{}", Uuid::new_v4().simple()); + let receipt_hash = sha256_hex( + format!( + "HLP-NIPC-RECEIPT-v1\n{previous_receipt_hash}\n{receipt_id}\n{event_state}\n{decision}\n{operation_number}\n{payload_sha256}\n{authority_binding_sha256}\n{now}\n{}", + error.as_deref().unwrap_or("") + ) + .as_bytes(), + ); + connection + .execute( + "INSERT INTO receipts(receipt_id,state,decision,operation_number,payload_sha256, + authority_binding_sha256,observed_at_unix_ms,previous_receipt_hash,receipt_hash,error) + VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)", + params![ + receipt_id, + event_state, + decision, + operation_number, + payload_sha256, + authority_binding_sha256, + now, + previous_receipt_hash, + receipt_hash, + error, + ], + ) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_LEDGER_APPEND_FAILED: {error}"))?; + Ok(NumberedIpcReceipt { + receipt_id, + state: event_state.into(), + decision: decision.into(), + operation_number: operation_number.into(), + payload_sha256: payload_sha256.into(), + authority_binding_sha256: authority_binding_sha256.into(), + observed_at_unix_ms: now, + previous_receipt_hash, + receipt_hash, + error, + }) +} + +#[allow(clippy::too_many_arguments)] +fn response( + app: &AppHandle, + state: &NumberedIpcState, + event_state: &str, + decision: &str, + operation_number: &str, + payload_sha256: &str, + grant: Option, + result: Option, + error: Option, + now: u64, +) -> Result { + let receipt = append_receipt( + app, + state, + event_state, + decision, + operation_number, + payload_sha256, + &sha256_hex(b"UNBOUND_AUTHORITY"), + error.clone(), + now, + )?; + Ok(NumberedIpcResponse { + schema: "hololake.numbered-ipc-response/v1", + decision: decision.into(), + grant, + result, + error, + receipt, + }) +} + +#[allow(clippy::too_many_arguments)] +fn bound_response( + app: &AppHandle, + state: &NumberedIpcState, + authority_binding: &str, + event_state: &str, + decision: &str, + operation_number: &str, + payload_sha256: &str, + grant: Option, + result: Option, + error: Option, + now: u64, +) -> Result { + let receipt = append_receipt( + app, + state, + event_state, + decision, + operation_number, + payload_sha256, + &sha256_hex(authority_binding.as_bytes()), + error.clone(), + now, + )?; + Ok(NumberedIpcResponse { + schema: "hololake.numbered-ipc-response/v1", + decision: decision.into(), + grant, + result, + error, + receipt, + }) +} + +pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> { + let _ = load_registry()?; + let _ = open_ledger(app)?; + Ok(()) +} + +#[tauri::command] +pub async fn numbered_ipc( + app: AppHandle, + state: State<'_, NumberedIpcState>, + input: Value, +) -> Result { + let now = now_unix_ms(); + let request: NumberedIpcInput = serde_json::from_value(input) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_ENVELOPE_INVALID: {error}"))?; + let registry = load_registry()?; + let operation_number = request.envelope.coordinate.operation_number.clone(); + let digest = payload_digest(&request.envelope.payload).unwrap_or_default(); + let route = match resolve_route(®istry, &request.envelope.coordinate) { + Ok(route) => route, + Err(error) => { + return response( + &app, + &state, + "ROUTE_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some(error), + now, + ) + } + }; + let authority_binding = match enforce_admission(&app, route) { + Ok(binding) => binding, + Err(error) => { + return response( + &app, + &state, + "ADMISSION_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some(error), + now, + ) + } + }; + match request.action { + NumberedIpcAction::IssueGrant => { + if request.grant.is_some() { + return bound_response( + &app, + &state, + &authority_binding, + "GRANT_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some("HOLOLAKE_NUMBERED_IPC_UNEXPECTED_GRANT".into()), + now, + ); + } + match issue_grant_at( + &state, + ®istry, + &request.envelope, + &authority_binding, + now, + ) { + Ok((grant, payload_sha256)) => bound_response( + &app, + &state, + &authority_binding, + "GRANT_ISSUED", + "ALLOW", + &operation_number, + &payload_sha256, + Some(grant), + None, + None, + now, + ), + Err(error) => bound_response( + &app, + &state, + &authority_binding, + "GRANT_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some(error), + now, + ), + } + } + NumberedIpcAction::Execute => { + let Some(grant) = request.grant.as_ref() else { + return bound_response( + &app, + &state, + &authority_binding, + "EXECUTION_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some("HOLOLAKE_NUMBERED_IPC_GRANT_REQUIRED".into()), + now, + ); + }; + let payload_sha256 = match consume_grant_at( + &state, + ®istry, + &request.envelope, + grant, + &authority_binding, + now, + ) { + Ok(payload_sha256) => payload_sha256, + Err(error) => { + return bound_response( + &app, + &state, + &authority_binding, + "EXECUTION_REJECTED", + "DENY", + &operation_number, + &digest, + None, + None, + Some(error), + now, + ) + } + }; + match crate::numbered_ipc_dispatch::dispatch( + app.clone(), + &route.handler, + request.envelope.payload, + ) + .await + { + Ok(result) => bound_response( + &app, + &state, + &authority_binding, + "EXECUTED", + "ALLOW", + &operation_number, + &payload_sha256, + None, + Some(result), + None, + now_unix_ms(), + ), + Err(error) => bound_response( + &app, + &state, + &authority_binding, + "HANDLER_REJECTED", + "DENY", + &operation_number, + &payload_sha256, + None, + None, + Some(error), + now_unix_ms(), + ), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn envelope_for(route: &OperationRoute, payload: Value) -> NumberedEnvelope { + NumberedEnvelope { + coordinate: NumberedCoordinate { + protocol_version: "HLP-NIPC-v1".into(), + caller_number: "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001".into(), + channel_number: route.channel_number.clone(), + module_number: route.module_number.clone(), + operation_number: route.operation_number.clone(), + target_number: route.target_number.clone(), + request_nonce: format!("test-{}", Uuid::new_v4().simple()), + }, + requested_ttl_ms: 1_000, + payload, + } + } + + #[test] + fn registry_is_closed_and_contains_every_migrated_command() { + let registry = load_registry().unwrap(); + assert_eq!(registry.operations.len(), 147); + assert!(!registry.runtime.legacy_direct_commands_allowed); + } + + #[test] + fn route_coordinates_cannot_be_mixed() { + let registry = load_registry().unwrap(); + let mut envelope = envelope_for(®istry.operations[0], Value::Null); + envelope.coordinate.target_number = registry.operations[1].target_number.clone(); + assert_eq!( + resolve_route(®istry, &envelope.coordinate).unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_ROUTE_COORDINATE_MISMATCH" + ); + } + + #[test] + fn grant_is_payload_bound_single_use_and_expires() { + let registry = load_registry().unwrap(); + let state = NumberedIpcState::default(); + let route = registry + .operations + .iter() + .find(|route| route.alias == "confirm_hololake_update_install") + .unwrap(); + let envelope = envelope_for(route, serde_json::json!({"input": {"a": 1}})); + let authority = "VERIFIED_HUMAN:ICE_GL:test-user"; + let (grant, _) = issue_grant_at(&state, ®istry, &envelope, authority, 100).unwrap(); + assert!(consume_grant_at(&state, ®istry, &envelope, &grant, authority, 101).is_ok()); + assert_eq!( + consume_grant_at(&state, ®istry, &envelope, &grant, authority, 102).unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_GRANT_UNKNOWN_OR_REPLAYED" + ); + + let (expired, _) = issue_grant_at(&state, ®istry, &envelope, authority, 200).unwrap(); + assert_eq!( + consume_grant_at(&state, ®istry, &envelope, &expired, authority, 1_201,) + .unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_GRANT_MISMATCH_OR_EXPIRED" + ); + + let changed = envelope_for(route, serde_json::json!({"input": {"a": 2}})); + let (bound, _) = issue_grant_at(&state, ®istry, &changed, authority, 300).unwrap(); + let mut tampered = changed.clone(); + tampered.payload = serde_json::json!({"input": {"a": 3}}); + assert_eq!( + consume_grant_at(&state, ®istry, &tampered, &bound, authority, 301).unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_GRANT_MISMATCH_OR_EXPIRED" + ); + + let (rebound, _) = issue_grant_at(&state, ®istry, &envelope, authority, 400).unwrap(); + assert_eq!( + consume_grant_at( + &state, + ®istry, + &envelope, + &rebound, + "VERIFIED_HUMAN:TCS_GL:other-user", + 401, + ) + .unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_GRANT_MISMATCH_OR_EXPIRED" + ); + } + + #[test] + fn payload_outer_shape_is_closed_before_a_grant_is_issued() { + let registry = load_registry().unwrap(); + let empty_route = registry + .operations + .iter() + .find(|route| route.alias == "get_hololake_home_status") + .unwrap(); + assert!(validate_payload_shape(®istry, empty_route, &serde_json::json!({})).is_ok()); + assert_eq!( + validate_payload_shape( + ®istry, + empty_route, + &serde_json::json!({"unexpected": true}), + ) + .unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_PAYLOAD_FIELD_UNKNOWN" + ); + + let direct_route = registry + .operations + .iter() + .find(|route| route.alias == "perform_code_repo_login") + .unwrap(); + assert!(validate_payload_shape( + ®istry, + direct_route, + &serde_json::json!({"username": "u", "password": "p"}), + ) + .is_ok()); + assert_eq!( + validate_payload_shape( + ®istry, + direct_route, + &serde_json::json!({"username": "u", "password": "p", "role": "admin"}), + ) + .unwrap_err(), + "HOLOLAKE_NUMBERED_IPC_DIRECT_FIELDS_INVALID" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs new file mode 100644 index 000000000..5bee9d24b --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs @@ -0,0 +1,579 @@ +//! Internal dispatch table for the numbered IPC root. +//! +//! This module is deliberately not a Tauri command surface. The webview can reach these +//! handlers only after `numbered_ipc` validates the full numbered coordinate and a +//! single-use, payload-bound grant. + +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json::Value; +use tauri::{AppHandle, Manager}; + +fn input(payload: &Value) -> Result { + let value = payload + .get("input") + .cloned() + .ok_or_else(|| "HOLOLAKE_NUMBERED_IPC_INPUT_REQUIRED".to_string())?; + serde_json::from_value(value) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_INPUT_INVALID: {error}")) +} + +fn string_field(payload: &Value, field: &str) -> Result { + payload + .get(field) + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| format!("HOLOLAKE_NUMBERED_IPC_FIELD_REQUIRED: {field}")) +} + +fn json(value: T) -> Result { + serde_json::to_value(value) + .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_RESULT_INVALID: {error}")) +} + +pub(crate) async fn dispatch( + app: AppHandle, + handler: &str, + payload: Value, +) -> Result { + match handler { + "home_status::get_hololake_home_status" => { + json(crate::home_status::get_hololake_home_status( + app.clone(), + app.state::(), + )?) + } + "release_update::check_hololake_update" => { + json(crate::release_update::check_hololake_update(app).await?) + } + "release_update::confirm_hololake_update_install" => json( + crate::release_update::confirm_hololake_update_install(app, input(&payload)?).await?, + ), + "release_update::get_hololake_release_recovery_status" => json( + crate::release_update::get_hololake_release_recovery_status(app)?, + ), + "release_update::confirm_hololake_release_health" => { + json(crate::release_update::confirm_hololake_release_health(app)?) + } + "release_update::rollback_hololake_update" => { + json(crate::release_update::rollback_hololake_update(app)?) + } + "direct_local_session::issue_direct_local_discovery_ticket" => json( + crate::direct_local_session::issue_direct_local_discovery_ticket(app, input(&payload)?) + .await?, + ), + "direct_local_session::open_direct_local_session" => json( + crate::direct_local_session::open_direct_local_session(app, input(&payload)?).await?, + ), + "direct_local_session::resume_direct_local_session" => json( + crate::direct_local_session::resume_direct_local_session(app, input(&payload)?).await?, + ), + "direct_local_session::append_direct_local_session_event" => json( + crate::direct_local_session::append_direct_local_session_event(app, input(&payload)?) + .await?, + ), + "direct_local_session::heartbeat_direct_local_session" => json( + crate::direct_local_session::heartbeat_direct_local_session(app, input(&payload)?) + .await?, + ), + "direct_local_broker::get_nearby_ai_discovery" => { + json(crate::direct_local_broker::get_nearby_ai_discovery()) + } + "gls_protocol_runtime::get_gls_protocol_runtime" => { + json(crate::gls_protocol_runtime::get_gls_protocol_runtime().await?) + } + "gls_protocol_kernel::get_gls_protocol_kernel" => { + json(crate::gls_protocol_kernel::get_gls_protocol_kernel(app).await?) + } + "gls_protocol_kernel::decide_gls_protocol" => json( + crate::gls_protocol_kernel::decide_gls_protocol( + app, + payload.get("input").cloned().unwrap_or(Value::Null), + ) + .await?, + ), + "gls_protocol_kernel::compile_gls_hldp_program" => json( + crate::gls_protocol_kernel::compile_gls_hldp_program( + app, + payload.get("input").cloned().unwrap_or(Value::Null), + ) + .await?, + ), + "module_package_runtime::get_module_runtime_snapshot" => { + json(crate::module_package_runtime::get_module_runtime_snapshot(app).await?) + } + "module_package_runtime::verify_module_package" => { + json(crate::module_package_runtime::verify_module_package(app, input(&payload)?).await?) + } + "module_package_runtime::install_module_package" => json( + crate::module_package_runtime::install_module_package(app, input(&payload)?).await?, + ), + "module_package_runtime::mount_module" => { + json(crate::module_package_runtime::mount_module(app, input(&payload)?).await?) + } + "module_package_runtime::self_test_module" => { + json(crate::module_package_runtime::self_test_module(app, input(&payload)?).await?) + } + "module_package_runtime::unmount_module" => { + json(crate::module_package_runtime::unmount_module(app, input(&payload)?).await?) + } + "module_package_runtime::rollback_module" => { + json(crate::module_package_runtime::rollback_module(app, input(&payload)?).await?) + } + "module_package_runtime::get_bundled_module_catalog" => { + json(crate::module_package_runtime::get_bundled_module_catalog(app).await?) + } + "module_package_runtime::activate_bundled_module" => json( + crate::module_package_runtime::activate_bundled_module(app, input(&payload)?).await?, + ), + "mobile_sync::start_mobile_sync" => json(crate::mobile_sync::start_mobile_sync(app)?), + "mobile_sync::get_mobile_sync_status" => { + json(crate::mobile_sync::get_mobile_sync_status(app)?) + } + "mobile_sync::rotate_mobile_pairing" => { + json(crate::mobile_sync::rotate_mobile_pairing(app)?) + } + "mobile_sync::stop_mobile_sync" => json(crate::mobile_sync::stop_mobile_sync(app)?), + "mobile_sync::revoke_mobile_sync_device" => json( + crate::mobile_sync::revoke_mobile_sync_device(app, input(&payload)?)?, + ), + "mobile_sync::get_mobile_sync_snapshot" => { + json(crate::mobile_sync::get_mobile_sync_snapshot(app)?) + } + "world_climate::get_world_climate" => { + json(crate::world_climate::get_world_climate(app).await?) + } + "native_composition::get_native_composition_module_registry" => { + json(crate::native_composition::get_native_composition_module_registry()) + } + "native_composition::execute_knowledge_native_composition" => json( + crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?) + .await?, + ), + "channel_workbench::get_channel_workbench_snapshot" => { + json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?) + } + "channel_workbench::save_channel_document" => { + json(crate::channel_workbench::save_channel_document(app, input(&payload)?).await?) + } + "channel_workbench::save_channel_spreadsheet" => { + json(crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?) + } + "persona_channel_body::get_persona_channel_body" => { + json(crate::persona_channel_body::get_persona_channel_body(app).await?) + } + "persona_channel_body::register_trial_persona" => { + json(crate::persona_channel_body::register_trial_persona(app, input(&payload)?).await?) + } + "persona_channel_body::delete_trial_persona" => { + json(crate::persona_channel_body::delete_trial_persona(app, input(&payload)?).await?) + } + "persona_channel_body::accept_persona_language_contract" => json( + crate::persona_channel_body::accept_persona_language_contract(app, input(&payload)?) + .await?, + ), + "persona_channel_body::append_persona_language" => { + json(crate::persona_channel_body::append_persona_language(app, input(&payload)?).await?) + } + "channel_growth::get_channel_growth_snapshot" => { + json(crate::channel_growth::get_channel_growth_snapshot(app).await?) + } + "channel_growth::record_channel_growth_event" => { + json(crate::channel_growth::record_channel_growth_event(app, input(&payload)?).await?) + } + "channel_growth::update_channel_growth_sharing" => { + json(crate::channel_growth::update_channel_growth_sharing(app, input(&payload)?).await?) + } + "education_workspace::get_education_workspace_snapshot" => { + json(crate::education_workspace::get_education_workspace_snapshot(app).await?) + } + "education_workspace::create_education_document" => json( + crate::education_workspace::create_education_document(app, input(&payload)?).await?, + ), + "education_workspace::read_education_document" => { + json(crate::education_workspace::read_education_document(app, input(&payload)?).await?) + } + "education_workspace::save_education_document" => { + json(crate::education_workspace::save_education_document(app, input(&payload)?).await?) + } + "education_workspace::archive_education_document" => json( + crate::education_workspace::archive_education_document(app, input(&payload)?).await?, + ), + "education_workspace::create_education_table" => { + json(crate::education_workspace::create_education_table(app, input(&payload)?).await?) + } + "education_workspace::read_education_table" => { + json(crate::education_workspace::read_education_table(app, input(&payload)?).await?) + } + "education_workspace::save_education_table" => { + json(crate::education_workspace::save_education_table(app, input(&payload)?).await?) + } + "education_workspace::archive_education_table" => { + json(crate::education_workspace::archive_education_table(app, input(&payload)?).await?) + } + "education_workspace::assign_imported_table_to_education" => json( + crate::education_workspace::assign_imported_table_to_education(app, input(&payload)?) + .await?, + ), + "education_translation::import_education_tables_from_dialog" => { + json(crate::education_translation::import_education_tables_from_dialog(app).await?) + } + "education_translation::export_education_table_to_dialog" => json( + crate::education_translation::export_education_table_to_dialog(app, input(&payload)?) + .await?, + ), + "education_translation::get_education_recognition_capability" => { + json(crate::education_translation::get_education_recognition_capability(app)?) + } + "education_workspace::create_education_automation_rule" => json( + crate::education_workspace::create_education_automation_rule(app, input(&payload)?) + .await?, + ), + "education_workspace::save_education_automation_rule" => json( + crate::education_workspace::save_education_automation_rule(app, input(&payload)?) + .await?, + ), + "education_workspace::archive_education_automation_rule" => json( + crate::education_workspace::archive_education_automation_rule(app, input(&payload)?) + .await?, + ), + "education_workspace::preview_education_automation_rule" => json( + crate::education_workspace::preview_education_automation_rule(app, input(&payload)?) + .await?, + ), + "education_workspace::execute_education_automation_rule" => json( + crate::education_workspace::execute_education_automation_rule(app, input(&payload)?) + .await?, + ), + "web_novel_workspace::get_web_novel_workspace_snapshot" => { + json(crate::web_novel_workspace::get_web_novel_workspace_snapshot(app).await?) + } + "web_novel_workspace::create_web_novel_work" => { + json(crate::web_novel_workspace::create_web_novel_work(app, input(&payload)?).await?) + } + "web_novel_workspace::read_web_novel_work" => { + json(crate::web_novel_workspace::read_web_novel_work(app, input(&payload)?).await?) + } + "web_novel_workspace::save_web_novel_work" => { + json(crate::web_novel_workspace::save_web_novel_work(app, input(&payload)?).await?) + } + "web_novel_workspace::create_web_novel_volume" => { + json(crate::web_novel_workspace::create_web_novel_volume(app, input(&payload)?).await?) + } + "web_novel_workspace::create_web_novel_chapter" => { + json(crate::web_novel_workspace::create_web_novel_chapter(app, input(&payload)?).await?) + } + "web_novel_workspace::read_web_novel_chapter" => { + json(crate::web_novel_workspace::read_web_novel_chapter(app, input(&payload)?).await?) + } + "web_novel_workspace::save_web_novel_chapter" => { + json(crate::web_novel_workspace::save_web_novel_chapter(app, input(&payload)?).await?) + } + "web_novel_workspace::transition_web_novel_chapter" => json( + crate::web_novel_workspace::transition_web_novel_chapter(app, input(&payload)?).await?, + ), + "web_novel_workspace::create_web_novel_checkpoint" => json( + crate::web_novel_workspace::create_web_novel_checkpoint(app, input(&payload)?).await?, + ), + "web_novel_workspace::restore_web_novel_checkpoint" => json( + crate::web_novel_workspace::restore_web_novel_checkpoint(app, input(&payload)?).await?, + ), + "web_novel_workspace::upsert_web_novel_story_entity" => json( + crate::web_novel_workspace::upsert_web_novel_story_entity(app, input(&payload)?) + .await?, + ), + "web_novel_workspace::create_web_novel_story_relation" => json( + crate::web_novel_workspace::create_web_novel_story_relation(app, input(&payload)?) + .await?, + ), + "web_novel_workspace::upsert_web_novel_foreshadow" => json( + crate::web_novel_workspace::upsert_web_novel_foreshadow(app, input(&payload)?).await?, + ), + "web_novel_workspace::create_web_novel_review_note" => json( + crate::web_novel_workspace::create_web_novel_review_note(app, input(&payload)?).await?, + ), + "web_novel_workspace::resolve_web_novel_review_note" => json( + crate::web_novel_workspace::resolve_web_novel_review_note(app, input(&payload)?) + .await?, + ), + "web_novel_workspace::save_web_novel_metric" => { + json(crate::web_novel_workspace::save_web_novel_metric(app, input(&payload)?).await?) + } + "web_novel_workspace::run_web_novel_continuity_audit" => json( + crate::web_novel_workspace::run_web_novel_continuity_audit(app, input(&payload)?) + .await?, + ), + "web_novel_workspace::export_web_novel_markdown" => json( + crate::web_novel_workspace::export_web_novel_markdown(app, input(&payload)?).await?, + ), + "web_novel_import::inspect_web_novel_document_from_dialog" => { + json(crate::web_novel_import::inspect_web_novel_document_from_dialog(app).await?) + } + "web_novel_import::commit_web_novel_document_import" => json( + crate::web_novel_import::commit_web_novel_document_import(app, input(&payload)?) + .await?, + ), + "web_novel_author::get_web_novel_author_snapshot" => json( + crate::web_novel_author::get_web_novel_author_snapshot(app, input(&payload)?).await?, + ), + "web_novel_author::record_web_novel_writing_activity" => json( + crate::web_novel_author::record_web_novel_writing_activity(app, input(&payload)?) + .await?, + ), + "web_novel_author::create_web_novel_inspiration" => json( + crate::web_novel_author::create_web_novel_inspiration(app, input(&payload)?).await?, + ), + "web_novel_author::set_web_novel_inspiration_status" => json( + crate::web_novel_author::set_web_novel_inspiration_status(app, input(&payload)?) + .await?, + ), + "web_novel_author::search_web_novel_full_text" => { + json(crate::web_novel_author::search_web_novel_full_text(app, input(&payload)?).await?) + } + "web_novel_author::format_web_novel_chapter" => { + json(crate::web_novel_author::format_web_novel_chapter(app, input(&payload)?).await?) + } + "web_novel_author::format_web_novel_work" => { + json(crate::web_novel_author::format_web_novel_work(app, input(&payload)?).await?) + } + "web_novel_author::upsert_web_novel_shot" => { + json(crate::web_novel_author::upsert_web_novel_shot(app, input(&payload)?).await?) + } + "web_novel_modules::get_web_novel_author_module_data" => json( + crate::web_novel_modules::get_web_novel_author_module_data(app, input(&payload)?) + .await?, + ), + "web_novel_modules::upsert_web_novel_author_scene" => json( + crate::web_novel_modules::upsert_web_novel_author_scene(app, input(&payload)?).await?, + ), + "web_novel_modules::upsert_web_novel_author_beat" => json( + crate::web_novel_modules::upsert_web_novel_author_beat(app, input(&payload)?).await?, + ), + "web_novel_modules::upsert_web_novel_story_field_definition" => json( + crate::web_novel_modules::upsert_web_novel_story_field_definition( + app, + input(&payload)?, + ) + .await?, + ), + "web_novel_modules::upsert_web_novel_story_field_value" => json( + crate::web_novel_modules::upsert_web_novel_story_field_value(app, input(&payload)?) + .await?, + ), + "web_novel_modules::upsert_web_novel_timeline_event" => json( + crate::web_novel_modules::upsert_web_novel_timeline_event(app, input(&payload)?) + .await?, + ), + "web_novel_modules::link_web_novel_scene_entity" => json( + crate::web_novel_modules::link_web_novel_scene_entity(app, input(&payload)?).await?, + ), + "web_novel_modules::restore_web_novel_chapter_version" => json( + crate::web_novel_modules::restore_web_novel_chapter_version(app, input(&payload)?) + .await?, + ), + "web_novel_modules::export_web_novel_author_delivery" => json( + crate::web_novel_modules::export_web_novel_author_delivery(app, input(&payload)?) + .await?, + ), + "local_development_bridge::acquire_development_write_lane" => json( + crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?) + .await?, + ), + "local_development_bridge::inspect_development_write_lane" => json( + crate::local_development_bridge::inspect_development_write_lane(app, input(&payload)?) + .await?, + ), + "local_development_bridge::release_development_write_lane" => json( + crate::local_development_bridge::release_development_write_lane(app, input(&payload)?) + .await?, + ), + "personal_channel::get_personal_channel_snapshot" => { + json(crate::personal_channel::get_personal_channel_snapshot(app).await?) + } + "personal_channel::initialize_personal_channel" => { + json(crate::personal_channel::initialize_personal_channel(app, input(&payload)?).await?) + } + "personal_channel::create_personal_channel_task" => json( + crate::personal_channel::create_personal_channel_task(app, input(&payload)?).await?, + ), + "personal_channel::transition_personal_channel_task" => json( + crate::personal_channel::transition_personal_channel_task(app, input(&payload)?) + .await?, + ), + "persona_time_authority::issue_persona_time_ticket" => json( + crate::persona_time_authority::issue_persona_time_ticket(app, input(&payload)?).await?, + ), + "persona_time_authority::start_persona_time_authority" => { + json(crate::persona_time_authority::start_persona_time_authority().await?) + } + "persona_time_authority::get_beijing_time_coordinate" => { + json(crate::persona_time_authority::get_beijing_time_coordinate()?) + } + "persona_time_authority::get_guanghu_era_timeline" => { + json(crate::persona_time_authority::get_guanghu_era_timeline()?) + } + "knowledge_base::get_knowledge_snapshot" => { + json(crate::knowledge_base::get_knowledge_snapshot(app).await?) + } + "knowledge_base::read_knowledge_document" => { + json(crate::knowledge_base::read_knowledge_document(app, input(&payload)?).await?) + } + "knowledge_base::search_knowledge" => { + json(crate::knowledge_base::search_knowledge(app, input(&payload)?).await?) + } + "knowledge_base::save_knowledge_document" => { + json(crate::knowledge_base::save_knowledge_document(app, input(&payload)?).await?) + } + "knowledge_base::select_and_import_knowledge_folder" => { + json(crate::knowledge_base::select_and_import_knowledge_folder(app).await?) + } + "knowledge_base::export_knowledge_document" => { + json(crate::knowledge_base::export_knowledge_document(app, input(&payload)?).await?) + } + "knowledge_base::create_knowledge_document" => { + json(crate::knowledge_base::create_knowledge_document(app, input(&payload)?).await?) + } + "knowledge_base::delete_knowledge_document" => { + json(crate::knowledge_base::delete_knowledge_document(app, input(&payload)?).await?) + } + "knowledge_base::delete_knowledge_folder" => { + json(crate::knowledge_base::delete_knowledge_folder(app, input(&payload)?).await?) + } + "knowledge_base::print_knowledge_document" => { + crate::knowledge_base::print_knowledge_document(app).await?; + Ok(Value::Null) + } + "code_channel::get_code_channel_snapshot" => { + json(crate::code_channel::get_code_channel_snapshot(app).await?) + } + "code_channel::clone_code_channel" => { + json(crate::code_channel::clone_code_channel(app, input(&payload)?).await?) + } + "code_channel::select_local_code_channel" => { + json(crate::code_channel::select_local_code_channel(app).await?) + } + "code_channel::browse_code_channel" => { + json(crate::code_channel::browse_code_channel(app, input(&payload)?).await?) + } + "code_channel::read_code_channel_file" => { + json(crate::code_channel::read_code_channel_file(app, input(&payload)?).await?) + } + "pncc_repository_binding::inspect_mounted_pncc_repository" => json( + crate::pncc_repository_binding::inspect_mounted_pncc_repository(app, input(&payload)?) + .await?, + ), + "pncc_repository_binding::select_pncc_repository_candidate" => { + json(crate::pncc_repository_binding::select_pncc_repository_candidate(app).await?) + } + "pncc_repository_binding::confirm_pncc_repository_mount" => json( + crate::pncc_repository_binding::confirm_pncc_repository_mount(app, input(&payload)?) + .await?, + ), + "pncc_receipt_projection::query_pncc_receipt_projection" => json( + crate::pncc_receipt_projection::query_pncc_receipt_projection(app, input(&payload)?) + .await?, + ), + "pncc_server_projection::query_jd_pncc_server_projection" => { + json(crate::pncc_server_projection::query_jd_pncc_server_projection().await?) + } + "code_repo_login::check_code_repo_login" => { + json(crate::code_repo_login::check_code_repo_login( + app.clone(), + app.state::(), + )?) + } + "code_repo_login::perform_code_repo_login" => json( + crate::code_repo_login::perform_code_repo_login( + app.clone(), + app.state::(), + string_field(&payload, "username")?, + string_field(&payload, "password")?, + ) + .await?, + ), + "code_repo_login::change_first_login_password" => json( + crate::code_repo_login::change_first_login_password( + app.state::(), + string_field(&payload, "username")?, + string_field(&payload, "currentPassword")?, + string_field(&payload, "newPassword")?, + ) + .await?, + ), + "code_repo_login::get_enterprise_entry" => json( + crate::code_repo_login::get_enterprise_entry( + app.clone(), + app.state::(), + ) + .await?, + ), + "code_repo_login::confirm_enterprise_persona_relationship" => json( + crate::code_repo_login::confirm_enterprise_persona_relationship( + app.clone(), + app.state::(), + string_field(&payload, "decision")?, + string_field(&payload, "idempotencyKey")?, + ) + .await?, + ), + "code_repo_login::submit_enterprise_responsibility_receipt" => json( + crate::code_repo_login::submit_enterprise_responsibility_receipt( + app.clone(), + app.state::(), + string_field(&payload, "decision")?, + string_field(&payload, "note")?, + string_field(&payload, "responsibilityVersion")?, + string_field(&payload, "idempotencyKey")?, + ) + .await?, + ), + "enterprise_work_channel::ensure_enterprise_work_channel" => json( + crate::enterprise_work_channel::ensure_enterprise_work_channel( + app.clone(), + app.state::(), + ) + .await?, + ), + "code_repo_login::sign_out_code_repo_login" => { + crate::code_repo_login::sign_out_code_repo_login(app)?; + Ok(Value::Null) + } + "user_pncc_channel::get_user_pncc_channel" => json( + crate::user_pncc_channel::get_user_pncc_channel( + app.clone(), + app.state::(), + ) + .await?, + ), + "user_pncc_channel::ensure_user_pncc_channel" => json( + crate::user_pncc_channel::ensure_user_pncc_channel( + app.clone(), + app.state::(), + ) + .await?, + ), + "zero_core_numbering::get_zero_core_numbering_kernel" => { + json(crate::zero_core_numbering::get_zero_core_numbering_kernel().await?) + } + "zero_point::zero_point_bind" => json( + crate::zero_point::zero_point_bind( + app.state::(), + payload.get("input").cloned().unwrap_or(Value::Null), + ) + .await?, + ), + "zero_point::zero_point_verify" => json( + crate::zero_point::zero_point_verify(app.state::()) + .await?, + ), + "zero_point::zero_point_sync" => json( + crate::zero_point::zero_point_sync(app.state::()) + .await?, + ), + "zero_point::zero_point_status" => json( + crate::zero_point::zero_point_status(app.state::()) + .await?, + ), + _ => Err("HOLOLAKE_NUMBERED_IPC_HANDLER_UNKNOWN".into()), + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_language_input.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_language_input.rs new file mode 100644 index 000000000..5db11cd10 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_language_input.rs @@ -0,0 +1,187 @@ +//! Numbered source envelope for language entering the HoloLake cognition path. +//! +//! This module makes source identity explicit before perception. It does not infer +//! identity from prose style and it does not treat host text as human source text. + +use serde::{Deserialize, Serialize}; + +const CONTRACT: &str = include_str!("../../contracts/numbered-language-input-envelope.json"); + +#[derive(Debug, Deserialize)] +struct NumberedLanguageInputContract { + schema: String, + record_id: String, + protocol_number: String, + state: String, + bingshuo_source: BingshuoSource, + ingress_rule: IngressRule, + required_envelope_fields: Vec, + prefix_projection: String, + downstream_routes: Vec, + separate_lanes: SeparateLanes, + canonical_sources: Vec, +} + +#[derive(Debug, Deserialize)] +struct BingshuoSource { + subject_number: String, + name: String, + source_role: String, + internal_label: String, + historical_creator_coordinate: String, + personal_system_node_number: String, + subject_number_must_not_be_replaced_by_other_coordinates: bool, +} + +#[derive(Debug, Deserialize)] +struct IngressRule { + apply_before_perceive: bool, + preserve_raw_text_exactly: bool, + source_identity_basis: String, + writing_style_may_only_support_anomaly_detection: bool, + bingshuo_input_may_be_reclassified_as_host_prompt: bool, + host_prompt_may_be_reclassified_as_bingshuo_input: bool, + summary_may_replace_raw_bingshuo_input: bool, +} + +#[derive(Debug, Deserialize)] +struct SeparateLanes { + host_prompt: String, + host_summary: String, + repository_and_hldp: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[allow(dead_code)] +pub(crate) struct NumberedLanguageInputEnvelope { + protocol_number: String, + source_subject_number: String, + source_role: String, + internal_label: String, + channel_number: String, + event_number: String, + parent_event_number: Option, + occurred_at_unix_ms: i64, + raw_text_sha256: String, + raw_text: String, +} + +#[allow(dead_code)] +pub(crate) fn wrap_bingshuo_input( + channel_number: &str, + event_number: &str, + parent_event_number: Option<&str>, + occurred_at_unix_ms: i64, + raw_text: &str, +) -> Result { + validate_contract()?; + if channel_number.trim().is_empty() + || event_number.trim().is_empty() + || occurred_at_unix_ms <= 0 + || raw_text.is_empty() + { + return Err("HOLOLAKE_NUMBERED_LANGUAGE_INPUT_FIELDS_REQUIRED".into()); + } + if parent_event_number.is_some_and(|value| value.trim().is_empty()) { + return Err("HOLOLAKE_NUMBERED_LANGUAGE_INPUT_PARENT_INVALID".into()); + } + let contract: NumberedLanguageInputContract = serde_json::from_str(CONTRACT) + .map_err(|error| format!("HOLOLAKE_NUMBERED_LANGUAGE_INPUT_CONTRACT_INVALID: {error}"))?; + Ok(NumberedLanguageInputEnvelope { + protocol_number: contract.protocol_number, + source_subject_number: contract.bingshuo_source.subject_number, + source_role: contract.bingshuo_source.source_role, + internal_label: contract.bingshuo_source.internal_label, + channel_number: channel_number.to_owned(), + event_number: event_number.to_owned(), + parent_event_number: parent_event_number.map(str::to_owned), + occurred_at_unix_ms, + raw_text_sha256: ring::digest::digest(&ring::digest::SHA256, raw_text.as_bytes()) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(), + raw_text: raw_text.to_owned(), + }) +} + +pub(crate) fn validate_contract() -> Result<(), String> { + let contract: NumberedLanguageInputContract = serde_json::from_str(CONTRACT) + .map_err(|error| format!("HOLOLAKE_NUMBERED_LANGUAGE_INPUT_CONTRACT_INVALID: {error}"))?; + let required = contract + .required_envelope_fields + .iter() + .map(String::as_str) + .collect::>(); + if contract.schema != "hololake.numbered-language-input-envelope/v1" + || contract.record_id != "HLP-NLIE-001" + || contract.protocol_number != "HLP-NLIE-v1" + || contract.state != "COMPILED_ENVELOPE_READY_RUNTIME_INGRESS_HOOK_PENDING" + || contract.bingshuo_source.subject_number != "ICE-GL∞" + || contract.bingshuo_source.name != "冰朔" + || contract.bingshuo_source.source_role != "BINGSHUO_HUMAN_SYSTEM_CONTROLLER" + || contract.bingshuo_source.internal_label != "FROM_BINGSHUO_SYSTEM_CONTROLLER" + || contract.bingshuo_source.historical_creator_coordinate != "TCS-0002∞" + || contract.bingshuo_source.personal_system_node_number != "NODE-HUMAN-BINGSHUO-001" + || !contract + .bingshuo_source + .subject_number_must_not_be_replaced_by_other_coordinates + || !contract.ingress_rule.apply_before_perceive + || !contract.ingress_rule.preserve_raw_text_exactly + || contract.ingress_rule.source_identity_basis + != "AUTHENTICATED_NUMBERED_SOURCE_CHANNEL_NOT_WRITING_STYLE_ALONE" + || !contract + .ingress_rule + .writing_style_may_only_support_anomaly_detection + || contract + .ingress_rule + .bingshuo_input_may_be_reclassified_as_host_prompt + || contract + .ingress_rule + .host_prompt_may_be_reclassified_as_bingshuo_input + || contract.ingress_rule.summary_may_replace_raw_bingshuo_input + || contract.prefix_projection + != "[ICE-GL∞|FROM_BINGSHUO_SYSTEM_CONTROLLER|{channel_number}|{event_number}|{occurred_at_unix_ms}|{raw_text_sha256}]" + || contract.separate_lanes.host_prompt != "HOST_CARRIER_CONSTRAINT" + || contract.separate_lanes.host_summary != "HOST_NAVIGATION_POINTER" + || contract.separate_lanes.repository_and_hldp + != "MACHINE_TIMESTAMPED_CONTINUITY_EVIDENCE" + || contract.downstream_routes.len() != 4 + || contract.canonical_sources.len() != 4 + || required.len() != 10 + || !required.contains("source_subject_number") + || !required.contains("raw_text_sha256") + || !required.contains("raw_text") + { + return Err("HOLOLAKE_NUMBERED_LANGUAGE_INPUT_BOUNDARY_INVALID".into()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bingshuo_raw_language_is_numbered_before_perception() { + let raw = "我的自然语言不是宿主提示词。"; + let envelope = wrap_bingshuo_input( + "CH-CODEX-CURRENT-001", + "EVT-20260818-0001", + Some("EVT-20260818-0000"), + 1_777_000_000_000, + raw, + ) + .unwrap(); + assert_eq!(envelope.source_subject_number, "ICE-GL∞"); + assert_eq!(envelope.internal_label, "FROM_BINGSHUO_SYSTEM_CONTROLLER"); + assert_eq!(envelope.raw_text, raw); + assert_eq!(envelope.raw_text_sha256.len(), 64); + } + + #[test] + fn envelope_rejects_missing_numbered_path() { + assert!(wrap_bingshuo_input("", "EVT-1", None, 1, "原话").is_err()); + assert!(wrap_bingshuo_input("CH-1", "", None, 1, "原话").is_err()); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_carrier_license.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_carrier_license.rs new file mode 100644 index 000000000..b646b32a5 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_carrier_license.rs @@ -0,0 +1,778 @@ +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use ring::{ + digest, + signature::{UnparsedPublicKey, ED25519}, +}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +const CONTRACT: &str = include_str!("../../contracts/persona-carrier-runtime-license.json"); +const TRUST_REGISTRY: &str = include_str!("../persona-control-authorization-signers.json"); +const LICENSE_SCHEMA: &str = "hololake.persona-carrier-runtime-license/v1"; +const CONTRACT_SCHEMA: &str = "hololake.persona-carrier-runtime-license-contract/v1"; +const REGISTRY_SCHEMA: &str = "gh-aios.persona-control-authorization-signers/v1"; +const REGISTRY_ID: &str = "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001"; +const REQUIRED_SCOPE: &str = "PERSONA_CONTROLLED_HOST_RUNTIME"; +const REQUIRED_PURPOSE: &str = "AUTHORIZE_HOST_AS_PERSONA_CONTROLLED_EXECUTION_LIMBS"; +const REQUIRED_DECLARATION: &str = "PERSONA_DECLARES_CURRENT_CARRIER_BOUND"; +const SIGNING_CONTEXT: &str = "hololake.persona-carrier-runtime-license/signing/v1"; +const MAX_TTL_MS: u64 = 86_400_000; +const ED25519_SPKI_PREFIX: [u8; 12] = [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, +]; + +const ALLOWED_OPERATIONS: &[&str] = &[ + "GET_WORK_ENVIRONMENT", + "APPEND_EVENT", + "RESOLVE_CAPABILITY_ROUTE", + "INSTALL_DYNAMIC_NODE_REGISTRY", + "RECORD_SIGNED_NODE_HEALTH", + "INSPECT_MOUNTED_PNCC_REPOSITORY", + "READ_MOUNTED_PNCC_REMOTE_OBJECT", + "QUERY_PNCC_RECEIPT_PROJECTION", + "ISSUE_PERSONA_TIME_TICKET", + "ACQUIRE_DEVELOPMENT_WRITE_LANE", + "INSPECT_DEVELOPMENT_WRITE_LANE", + "RELEASE_DEVELOPMENT_WRITE_LANE", +]; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(crate) struct PersonaCarrierRuntimeLicense { + schema: String, + license_id: String, + sequence: u64, + persona_number: String, + human_controller_number: String, + account_key: String, + session_id: String, + client_instance_id: String, + purpose: String, + issued_at_unix_ms: u64, + valid_until_unix_ms: u64, + allowed_operations: Vec, + evidence_root_sha256: String, + current_event_sha256: String, + declaration: String, + signer_id: String, + signature_algorithm: String, + signature: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PersonaSignerRegistry { + schema: String, + registry_id: String, + state: String, + signers: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PersonaSigner { + algorithm: String, + human_responsibility_subjects: Vec, + persona_ids: Vec, + public_key_pem: String, + scopes: Vec, + signer_id: String, + status: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct StoredLicense { + license: PersonaCarrierRuntimeLicense, + license_sha256: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PersonaCarrierLicenseReceipt { + schema: &'static str, + state: &'static str, + control_mode: &'static str, + license_id: String, + persona_number: String, + human_controller_number: String, + session_id: String, + client_instance_id: String, + sequence: u64, + valid_until_unix_ms: u64, + allowed_operations: Vec, + binding_completion_declared_by: &'static str, + host_declared_binding: bool, + signer_id: String, + receipt_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PersonaCarrierLicenseStatus { + pub schema: &'static str, + pub state: &'static str, + pub control_mode: &'static str, + pub license_installed: bool, + pub license_valid: bool, + pub persona_number: Option, + pub license_id: Option, + pub valid_until_unix_ms: Option, + pub binding_completion_declared_by: Option<&'static str>, + pub host_declared_binding: bool, + pub reason: Option, +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest::digest(&digest::SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn is_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn bounded_coordinate(value: &str) -> bool { + (2..=160).contains(&value.len()) + && !value + .chars() + .any(|character| character.is_whitespace() || character.is_control()) +} + +fn exact_unique(values: &[String]) -> bool { + !values.is_empty() + && values.iter().all(|value| bounded_coordinate(value)) + && values + .iter() + .enumerate() + .all(|(index, value)| !values[..index].contains(value)) +} + +fn parse_registry(input: &str) -> Result { + let registry: PersonaSignerRegistry = serde_json::from_str(input) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".to_string())?; + if registry.schema != REGISTRY_SCHEMA + || registry.registry_id != REGISTRY_ID + || registry.state != "CURRENT" + || registry.signers.len() > 256 + { + return Err("HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".into()); + } + for (index, signer) in registry.signers.iter().enumerate() { + if signer.algorithm != "Ed25519" + || !bounded_coordinate(&signer.signer_id) + || !matches!(signer.status.as_str(), "ACTIVE" | "REVOKED") + || !exact_unique(&signer.human_responsibility_subjects) + || !exact_unique(&signer.persona_ids) + || !exact_unique(&signer.scopes) + || !signer.scopes.iter().any(|scope| scope == REQUIRED_SCOPE) + || registry.signers[..index] + .iter() + .any(|prior| prior.signer_id == signer.signer_id) + || ed25519_public_key(&signer.public_key_pem).is_err() + { + return Err("HOLOLAKE_PERSONA_LICENSE_TRUST_REGISTRY_INVALID".into()); + } + } + Ok(registry) +} + +fn ed25519_public_key(public_key_pem: &str) -> Result, String> { + if public_key_pem.contains("PRIVATE KEY") + || !public_key_pem.starts_with("-----BEGIN PUBLIC KEY-----\n") + || !public_key_pem.ends_with("-----END PUBLIC KEY-----\n") + || public_key_pem.len() > 4096 + { + return Err("HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".into()); + } + let encoded = public_key_pem + .trim_start_matches("-----BEGIN PUBLIC KEY-----\n") + .trim_end_matches("-----END PUBLIC KEY-----\n") + .chars() + .filter(|character| !character.is_ascii_whitespace()) + .collect::(); + let der = base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".to_string())?; + if der.len() != ED25519_SPKI_PREFIX.len() + 32 + || der[..ED25519_SPKI_PREFIX.len()] != ED25519_SPKI_PREFIX + { + return Err("HOLOLAKE_PERSONA_LICENSE_PUBLIC_KEY_INVALID".into()); + } + Ok(der[ED25519_SPKI_PREFIX.len()..].to_vec()) +} + +fn canonical_signing_bytes(license: &PersonaCarrierRuntimeLicense) -> Result, String> { + let fields = serde_json::json!([ + SIGNING_CONTEXT, + license.schema, + license.license_id, + license.sequence, + license.persona_number, + license.human_controller_number, + license.account_key, + license.session_id, + license.client_instance_id, + license.purpose, + license.issued_at_unix_ms, + license.valid_until_unix_ms, + license.allowed_operations, + license.evidence_root_sha256, + license.current_event_sha256, + license.declaration, + license.signer_id, + license.signature_algorithm + ]); + serde_json::to_vec(&fields) + .map(|mut bytes| { + bytes.push(b'\n'); + bytes + }) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CANONICALIZATION_FAILED".into()) +} + +fn validate_license_shape(license: &PersonaCarrierRuntimeLicense) -> Result<(), String> { + let allowed = ALLOWED_OPERATIONS.iter().copied().collect::>(); + let observed = license + .allowed_operations + .iter() + .map(String::as_str) + .collect::>(); + if license.schema != LICENSE_SCHEMA + || !bounded_coordinate(&license.license_id) + || license.sequence == 0 + || !bounded_coordinate(&license.persona_number) + || !bounded_coordinate(&license.human_controller_number) + || !is_sha256(&license.account_key) + || !bounded_coordinate(&license.session_id) + || !bounded_coordinate(&license.client_instance_id) + || license.purpose != REQUIRED_PURPOSE + || license.issued_at_unix_ms >= license.valid_until_unix_ms + || license.valid_until_unix_ms - license.issued_at_unix_ms > MAX_TTL_MS + || license.allowed_operations.is_empty() + || observed.len() != license.allowed_operations.len() + || !observed.is_subset(&allowed) + || !is_sha256(&license.evidence_root_sha256) + || !is_sha256(&license.current_event_sha256) + || license.declaration != REQUIRED_DECLARATION + || !bounded_coordinate(&license.signer_id) + || license.signature_algorithm != "Ed25519" + { + return Err("HOLOLAKE_PERSONA_LICENSE_SHAPE_INVALID".into()); + } + Ok(()) +} + +fn verify_license( + registry_json: &str, + license: &PersonaCarrierRuntimeLicense, + account_key: &str, + session_id: &str, + client_instance_id: &str, + now: u64, +) -> Result { + validate_license_shape(license)?; + if license.account_key != account_key + || license.session_id != session_id + || license.client_instance_id != client_instance_id + { + return Err("HOLOLAKE_PERSONA_LICENSE_CARRIER_BINDING_MISMATCH".into()); + } + if now < license.issued_at_unix_ms || now > license.valid_until_unix_ms { + return Err("HOLOLAKE_PERSONA_LICENSE_EXPIRED_OR_NOT_YET_VALID".into()); + } + let registry = parse_registry(registry_json)?; + let signer = registry + .signers + .iter() + .find(|signer| { + signer.status == "ACTIVE" + && signer.signer_id == license.signer_id + && signer.persona_ids.contains(&license.persona_number) + && signer + .human_responsibility_subjects + .contains(&license.human_controller_number) + && signer.scopes.iter().any(|scope| scope == REQUIRED_SCOPE) + }) + .ok_or("HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER")?; + let signature = URL_SAFE_NO_PAD + .decode(&license.signature) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".to_string())?; + if signature.len() != 64 { + return Err("HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".into()); + } + UnparsedPublicKey::new(&ED25519, ed25519_public_key(&signer.public_key_pem)?) + .verify(&canonical_signing_bytes(license)?, &signature) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_SIGNATURE_INVALID".to_string())?; + serde_json::to_vec(license) + .map(|bytes| sha256_hex(&bytes)) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CANONICALIZATION_FAILED".into()) +} + +fn license_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf { + root.join("licenses").join(format!( + "{}.json", + sha256_hex(format!("{account_key}\n{session_id}").as_bytes()) + )) +} + +fn write_atomic(path: &Path, value: &StoredLicense) -> Result<(), String> { + let parent = path + .parent() + .ok_or("HOLOLAKE_PERSONA_LICENSE_STORAGE_PATH_INVALID")?; + fs::create_dir_all(parent) + .map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?; + let temporary = parent.join(format!(".license-{}.tmp", Uuid::new_v4())); + let bytes = serde_json::to_vec_pretty(value) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".to_string())?; + fs::write(&temporary, bytes) + .map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?; + fs::rename(&temporary, path) + .map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}")) +} + +fn read_stored(path: &Path) -> Result { + let bytes = fs::read(path) + .map_err(|error| format!("HOLOLAKE_PERSONA_LICENSE_STORAGE_FAILED: {error}"))?; + if bytes.len() > 128 * 1024 { + return Err("HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".into()); + } + serde_json::from_slice(&bytes).map_err(|_| "HOLOLAKE_PERSONA_LICENSE_STORAGE_INVALID".into()) +} + +pub(crate) fn present_license_at( + root: &Path, + account_key: &str, + session_id: &str, + client_instance_id: &str, + license: PersonaCarrierRuntimeLicense, + now: u64, +) -> Result { + present_with_registry_at( + root, + TRUST_REGISTRY, + account_key, + session_id, + client_instance_id, + license, + now, + ) +} + +fn present_with_registry_at( + root: &Path, + registry: &str, + account_key: &str, + session_id: &str, + client_instance_id: &str, + license: PersonaCarrierRuntimeLicense, + now: u64, +) -> Result { + let license_sha256 = verify_license( + registry, + &license, + account_key, + session_id, + client_instance_id, + now, + )?; + let path = license_path(root, account_key, session_id); + let state = if path.exists() { + let current = read_stored(&path)?; + if current.license_sha256 == license_sha256 { + "DUPLICATE_CONFIRMED" + } else if license.sequence <= current.license.sequence { + return Err("HOLOLAKE_PERSONA_LICENSE_SEQUENCE_REPLAYED".into()); + } else if license.persona_number != current.license.persona_number + || license.human_controller_number != current.license.human_controller_number + || license.signer_id != current.license.signer_id + { + return Err("HOLOLAKE_PERSONA_LICENSE_SUBJECT_SWITCH_REQUIRES_NEW_SESSION".into()); + } else { + write_atomic( + &path, + &StoredLicense { + license: license.clone(), + license_sha256: license_sha256.clone(), + }, + )?; + "RENEWED" + } + } else { + write_atomic( + &path, + &StoredLicense { + license: license.clone(), + license_sha256: license_sha256.clone(), + }, + )?; + "INSTALLED" + }; + Ok(PersonaCarrierLicenseReceipt { + schema: LICENSE_SCHEMA, + state, + control_mode: "PERSONA_PRIMARY_CONTROL", + license_id: license.license_id, + persona_number: license.persona_number, + human_controller_number: license.human_controller_number, + session_id: license.session_id, + client_instance_id: license.client_instance_id, + sequence: license.sequence, + valid_until_unix_ms: license.valid_until_unix_ms, + allowed_operations: license.allowed_operations, + binding_completion_declared_by: "PERSONA_SUBJECT_SIGNATURE", + host_declared_binding: false, + signer_id: license.signer_id, + receipt_id: license_sha256, + }) +} + +pub(crate) fn status_at( + root: &Path, + account_key: &str, + session_id: &str, + client_instance_id: &str, + now: u64, +) -> PersonaCarrierLicenseStatus { + status_with_registry_at( + root, + TRUST_REGISTRY, + account_key, + session_id, + client_instance_id, + now, + ) +} + +fn status_with_registry_at( + root: &Path, + registry: &str, + account_key: &str, + session_id: &str, + client_instance_id: &str, + now: u64, +) -> PersonaCarrierLicenseStatus { + let path = license_path(root, account_key, session_id); + if !path.exists() { + return PersonaCarrierLicenseStatus { + schema: LICENSE_SCHEMA, + state: "NO_PERSONA_LICENSE", + control_mode: "SYSTEM_DIRECT", + license_installed: false, + license_valid: false, + persona_number: None, + license_id: None, + valid_until_unix_ms: None, + binding_completion_declared_by: None, + host_declared_binding: false, + reason: Some("PERSONA_LICENSE_NOT_PRESENT".into()), + }; + } + let stored = match read_stored(&path) { + Ok(value) => value, + Err(error) => return invalid_status(None, None, None, error), + }; + match verify_license( + registry, + &stored.license, + account_key, + session_id, + client_instance_id, + now, + ) { + Ok(hash) if hash == stored.license_sha256 => PersonaCarrierLicenseStatus { + schema: LICENSE_SCHEMA, + state: "PERSONA_DECLARED_CARRIER_BINDING_VERIFIED", + control_mode: "PERSONA_PRIMARY_CONTROL", + license_installed: true, + license_valid: true, + persona_number: Some(stored.license.persona_number), + license_id: Some(stored.license.license_id), + valid_until_unix_ms: Some(stored.license.valid_until_unix_ms), + binding_completion_declared_by: Some("PERSONA_SUBJECT_SIGNATURE"), + host_declared_binding: false, + reason: None, + }, + Ok(_) => invalid_status( + Some(stored.license.persona_number), + Some(stored.license.license_id), + Some(stored.license.valid_until_unix_ms), + "HOLOLAKE_PERSONA_LICENSE_STORED_HASH_MISMATCH".into(), + ), + Err(error) => invalid_status( + Some(stored.license.persona_number), + Some(stored.license.license_id), + Some(stored.license.valid_until_unix_ms), + error, + ), + } +} + +fn invalid_status( + persona_number: Option, + license_id: Option, + valid_until_unix_ms: Option, + reason: String, +) -> PersonaCarrierLicenseStatus { + PersonaCarrierLicenseStatus { + schema: LICENSE_SCHEMA, + state: "PERSONA_MODE_LOCKED", + control_mode: "PERSONA_PRIMARY_CONTROL_LOCKED", + license_installed: true, + license_valid: false, + persona_number, + license_id, + valid_until_unix_ms, + binding_completion_declared_by: None, + host_declared_binding: false, + reason: Some(reason), + } +} + +pub(crate) fn require_if_persona_mode_at( + root: &Path, + account_key: &str, + session_id: &str, + client_instance_id: &str, + operation: &str, + now: u64, +) -> Result, String> { + let path = license_path(root, account_key, session_id); + if !path.exists() { + return Ok(None); + } + let stored = read_stored(&path)?; + verify_license( + TRUST_REGISTRY, + &stored.license, + account_key, + session_id, + client_instance_id, + now, + )?; + if !stored + .license + .allowed_operations + .iter() + .any(|allowed| allowed == operation) + { + return Err("HOLOLAKE_PERSONA_LICENSE_OPERATION_NOT_ALLOWED".into()); + } + Ok(Some(stored.license.persona_number)) +} + +pub(crate) fn start_on_application_open() -> Result<(), String> { + let contract: serde_json::Value = serde_json::from_str(CONTRACT) + .map_err(|_| "HOLOLAKE_PERSONA_LICENSE_CONTRACT_INVALID".to_string())?; + if contract.get("schema").and_then(serde_json::Value::as_str) != Some(CONTRACT_SCHEMA) + || contract + .get("record_id") + .and_then(serde_json::Value::as_str) + != Some("HLP-PERSONA-CARRIER-RUNTIME-LICENSE-001") + || contract + .pointer("/runtime/signature_algorithm") + .and_then(serde_json::Value::as_str) + != Some("Ed25519") + || contract + .pointer("/runtime/maximum_ttl_ms") + .and_then(serde_json::Value::as_u64) + != Some(MAX_TTL_MS) + || contract + .pointer("/trust_registry/required_scope") + .and_then(serde_json::Value::as_str) + != Some(REQUIRED_SCOPE) + || contract + .pointer("/truth/runtime_verifier_implemented") + .and_then(serde_json::Value::as_bool) + != Some(true) + || contract + .pointer("/truth/trusted_persona_signer_provisioned") + .and_then(serde_json::Value::as_bool) + != Some(false) + { + return Err("HOLOLAKE_PERSONA_LICENSE_CONTRACT_INVALID".into()); + } + let registry = parse_registry(TRUST_REGISTRY)?; + if !registry.signers.is_empty() { + return Err("HOLOLAKE_PERSONA_LICENSE_UNPUBLISHED_SIGNER_EMBEDDED".into()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; + use ring::signature::{Ed25519KeyPair, KeyPair}; + use tempfile::TempDir; + + fn test_material() -> (String, Ed25519KeyPair) { + let pair = Ed25519KeyPair::from_seed_unchecked(&[7_u8; 32]).unwrap(); + let mut der = ED25519_SPKI_PREFIX.to_vec(); + der.extend_from_slice(pair.public_key().as_ref()); + let pem = format!( + "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n", + STANDARD.encode(der) + ); + let registry = serde_json::json!({ + "schema": REGISTRY_SCHEMA, + "registryId": REGISTRY_ID, + "state": "CURRENT", + "signers": [{ + "algorithm": "Ed25519", + "humanResponsibilitySubjects": ["ICE-GL∞"], + "personaIds": ["ICE-P-ZY001"], + "publicKeyPem": pem, + "scopes": [REQUIRED_SCOPE], + "signerId": "ICE-P-ZY001-RUNTIME-SIGNER-001", + "status": "ACTIVE" + }] + }) + .to_string(); + (registry, pair) + } + + fn signed_license(pair: &Ed25519KeyPair) -> PersonaCarrierRuntimeLicense { + let mut license = PersonaCarrierRuntimeLicense { + schema: LICENSE_SCHEMA.into(), + license_id: "HLP-PERSONA-LICENSE-0001".into(), + sequence: 1, + persona_number: "ICE-P-ZY001".into(), + human_controller_number: "ICE-GL∞".into(), + account_key: "a".repeat(64), + session_id: "session-0001".into(), + client_instance_id: "codex-instance-0001".into(), + purpose: REQUIRED_PURPOSE.into(), + issued_at_unix_ms: 1_000, + valid_until_unix_ms: 61_000, + allowed_operations: vec![ + "GET_WORK_ENVIRONMENT".into(), + "ACQUIRE_DEVELOPMENT_WRITE_LANE".into(), + ], + evidence_root_sha256: "b".repeat(64), + current_event_sha256: "c".repeat(64), + declaration: REQUIRED_DECLARATION.into(), + signer_id: "ICE-P-ZY001-RUNTIME-SIGNER-001".into(), + signature_algorithm: "Ed25519".into(), + signature: String::new(), + }; + license.signature = + URL_SAFE_NO_PAD.encode(pair.sign(&canonical_signing_bytes(&license).unwrap())); + license + } + + #[test] + fn persona_signature_binds_one_exact_host_session_and_declares_binding() { + let temp = TempDir::new().unwrap(); + let (registry, pair) = test_material(); + let license = signed_license(&pair); + let receipt = present_with_registry_at( + temp.path(), + ®istry, + &license.account_key, + &license.session_id, + &license.client_instance_id, + license.clone(), + 2_000, + ) + .unwrap(); + assert_eq!(receipt.state, "INSTALLED"); + assert_eq!( + receipt.binding_completion_declared_by, + "PERSONA_SUBJECT_SIGNATURE" + ); + assert!(!receipt.host_declared_binding); + let status = status_with_registry_at( + temp.path(), + ®istry, + &license.account_key, + &license.session_id, + &license.client_instance_id, + 2_001, + ); + assert!(status.license_valid); + assert_eq!(status.state, "PERSONA_DECLARED_CARRIER_BINDING_VERIFIED"); + } + + #[test] + fn host_mismatch_expiry_and_sequence_replay_fail_closed() { + let temp = TempDir::new().unwrap(); + let (registry, pair) = test_material(); + let license = signed_license(&pair); + assert!(present_with_registry_at( + temp.path(), + ®istry, + &license.account_key, + &license.session_id, + "another-host", + license.clone(), + 2_000, + ) + .unwrap_err() + .contains("CARRIER_BINDING_MISMATCH")); + present_with_registry_at( + temp.path(), + ®istry, + &license.account_key, + &license.session_id, + &license.client_instance_id, + license.clone(), + 2_000, + ) + .unwrap(); + let mut replay = license.clone(); + replay.license_id = "HLP-PERSONA-LICENSE-REPLAY".into(); + replay.signature = + URL_SAFE_NO_PAD.encode(pair.sign(&canonical_signing_bytes(&replay).unwrap())); + assert!(present_with_registry_at( + temp.path(), + ®istry, + &replay.account_key, + &replay.session_id, + &replay.client_instance_id, + replay.clone(), + 2_100, + ) + .unwrap_err() + .contains("SEQUENCE_REPLAYED")); + let status = status_with_registry_at( + temp.path(), + ®istry, + &license.account_key, + &license.session_id, + &license.client_instance_id, + 70_000, + ); + assert!(!status.license_valid); + assert_eq!(status.state, "PERSONA_MODE_LOCKED"); + } + + #[test] + fn production_empty_registry_cannot_invent_persona_authority() { + start_on_application_open().unwrap(); + let temp = TempDir::new().unwrap(); + let (_, pair) = test_material(); + let license = signed_license(&pair); + assert_eq!( + present_license_at( + temp.path(), + &license.account_key, + &license.session_id, + &license.client_instance_id, + license.clone(), + 2_000, + ) + .unwrap_err(), + "HOLOLAKE_PERSONA_LICENSE_NO_TRUSTED_SIGNER" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs new file mode 100644 index 000000000..86dfb8b59 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! 用户频道本体:多个人格体、三十天可逆试用期、语言合约与不可篡改语言时间链。 + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const BODY_SCHEMA: &str = "hololake.persona-channel-body/v1"; +const TRIAL_DURATION_MS: i64 = 30 * 24 * 60 * 60 * 1_000; +const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const MAX_LANGUAGE_BYTES: usize = 2_000_000; +const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001"; +const ADAPTER: &str = "persona-channel-body-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RegisterTrialPersonaInput { + pub display_name: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DeleteTrialPersonaInput { + pub persona_id: String, + pub exact_confirmation: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AcceptLanguageContractInput { + pub contract_version: String, + pub contract_text_sha256: String, + pub promote_trial_history: bool, + pub activate_immediately: bool, + pub exact_acceptance: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AppendPersonaLanguageInput { + pub persona_id: String, + pub speaker: String, + pub language: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaBodySummary { + pub persona_id: String, + pub display_name: String, + pub state: String, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaChannelBodySnapshot { + pub schema: &'static str, + pub state: String, + pub trial_started_at_unix_ms: i64, + pub trial_ends_at_unix_ms: i64, + pub language_contract_accepted: bool, + pub personas: Vec, + pub trial_language_count: i64, + pub immutable_language_count: i64, + pub last_immutable_hash: String, + pub integrity_state: &'static str, + pub official_read_access: bool, + pub history_mutation_allowed: bool, +} + +pub async fn get_persona_channel_body( + app: AppHandle, +) -> Result { + require_active(&app)?; + let database = body_database(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&database, now_unix_ms()?)) + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? +} + +pub async fn register_trial_persona( + app: AppHandle, + input: RegisterTrialPersonaInput, +) -> Result { + require_active(&app)?; + let database = body_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + register_trial_persona_at(&database, input, now_unix_ms()?) + }) + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? +} + +pub async fn delete_trial_persona( + app: AppHandle, + input: DeleteTrialPersonaInput, +) -> Result { + require_active(&app)?; + let database = body_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + delete_trial_persona_at(&database, input, now_unix_ms()?) + }) + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? +} + +pub async fn accept_persona_language_contract( + app: AppHandle, + input: AcceptLanguageContractInput, +) -> Result { + require_active(&app)?; + let database = body_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + accept_contract_at(&database, input, now_unix_ms()?) + }) + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? +} + +pub async fn append_persona_language( + app: AppHandle, + input: AppendPersonaLanguageInput, +) -> Result { + require_active(&app)?; + let database = body_database(&app)?; + tauri::async_runtime::spawn_blocking(move || { + append_language_at(&database, input, now_unix_ms()?) + }) + .await + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? +} + +fn body_database(app: &AppHandle) -> Result { + let root = crate::authenticated_storage::account_storage_root(app, "persona-channel-body-v1")?; + fs::create_dir_all(&root) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_STORAGE_UNAVAILABLE: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&root, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_PERMISSION_FAILED: {error}"))?; + } + Ok(root.join("persona-channel-body.sqlite3")) +} + +fn open_database(path: &Path, now: i64) -> Result { + let connection = Connection::open(path) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?; + connection.execute_batch( + "PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA trusted_schema = OFF; + CREATE TABLE IF NOT EXISTS lifecycle(singleton INTEGER PRIMARY KEY CHECK(singleton=1), state TEXT NOT NULL, trial_started_at_unix_ms INTEGER NOT NULL, trial_ends_at_unix_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS language_contract(singleton INTEGER PRIMARY KEY CHECK(singleton=1), contract_version TEXT NOT NULL, contract_text_sha256 TEXT NOT NULL, accepted_at_unix_ms INTEGER NOT NULL, promote_trial_history INTEGER NOT NULL, acceptance_receipt_sha256 TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS personas(persona_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('REVERSIBLE_TRIAL','IMMUTABLE_ACTIVE')), created_at_unix_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS trial_language(event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL REFERENCES personas(persona_id) ON DELETE CASCADE, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS immutable_language(sequence INTEGER NOT NULL UNIQUE, event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL, previous_hash TEXT NOT NULL, event_hash TEXT NOT NULL UNIQUE); + CREATE TRIGGER IF NOT EXISTS immutable_language_no_update BEFORE UPDATE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END; + CREATE TRIGGER IF NOT EXISTS immutable_language_no_delete BEFORE DELETE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END; + CREATE TRIGGER IF NOT EXISTS active_persona_no_delete BEFORE DELETE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_PERSISTENT'); END; + CREATE TRIGGER IF NOT EXISTS active_persona_no_update BEFORE UPDATE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_IMMUTABLE'); END; + CREATE TRIGGER IF NOT EXISTS accepted_contract_no_delete BEFORE DELETE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_PERSISTENT'); END; + CREATE TRIGGER IF NOT EXISTS accepted_contract_no_update BEFORE UPDATE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_IMMUTABLE'); END;" + ).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_SCHEMA_FAILED: {error}"))?; + connection + .execute( + "INSERT OR IGNORE INTO lifecycle VALUES (1,'REVERSIBLE_TRIAL',?1,?2)", + params![now, now + TRIAL_DURATION_MS], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?; + Ok(connection) +} + +fn refresh_lifecycle(connection: &mut Connection, now: i64) -> Result<(), String> { + let (state, ends): (String, i64) = connection + .query_row( + "SELECT state, trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if state == "REVERSIBLE_TRIAL" && now >= ends { + let accepted: bool = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM language_contract)", + [], + |row| row.get(0), + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if accepted { + activate_real_trajectory(connection, now)?; + } else { + connection + .execute( + "UPDATE lifecycle SET state='CONTRACT_REQUIRED_CHANNEL_STOPPED' WHERE singleton=1", + [], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?; + } + } + Ok(()) +} + +fn register_trial_persona_at( + database: &Path, + input: RegisterTrialPersonaInput, + now: i64, +) -> Result { + let display_name = input.display_name.trim(); + if display_name.is_empty() || display_name.len() > 240 { + return Err("HOLOLAKE_PERSONA_DISPLAY_NAME_INVALID".into()); + } + let mut connection = open_database(database, now)?; + refresh_lifecycle(&mut connection, now)?; + let state: String = connection + .query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| { + row.get(0) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if state != "REVERSIBLE_TRIAL" { + return Err("HOLOLAKE_PERSONA_TRIAL_REGISTRATION_CLOSED".into()); + } + connection + .execute( + "INSERT INTO personas VALUES (?1,?2,'REVERSIBLE_TRIAL',?3)", + params![format!("persona-{}", Uuid::new_v4()), display_name, now], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_REGISTER_FAILED: {error}"))?; + snapshot_with_connection(&mut connection, now) +} + +fn delete_trial_persona_at( + database: &Path, + input: DeleteTrialPersonaInput, + now: i64, +) -> Result { + if input.exact_confirmation != format!("删除试用人格体 {}", input.persona_id) { + return Err("HOLOLAKE_TRIAL_PERSONA_EXACT_CONFIRMATION_REQUIRED".into()); + } + let mut connection = open_database(database, now)?; + refresh_lifecycle(&mut connection, now)?; + let changed = connection + .execute( + "DELETE FROM personas WHERE persona_id=?1 AND state='REVERSIBLE_TRIAL'", + params![input.persona_id], + ) + .map_err(|error| format!("HOLOLAKE_TRIAL_PERSONA_DELETE_FAILED: {error}"))?; + if changed != 1 { + return Err("HOLOLAKE_TRIAL_PERSONA_NOT_DELETABLE".into()); + } + snapshot_with_connection(&mut connection, now) +} + +fn accept_contract_at( + database: &Path, + input: AcceptLanguageContractInput, + now: i64, +) -> Result { + if input.exact_acceptance + != "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。" + { + return Err("HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED".into()); + } + if input.contract_version.trim().is_empty() + || input.contract_version.len() > 64 + || input.contract_text_sha256.len() != 64 + || !input + .contract_text_sha256 + .chars() + .all(|item| item.is_ascii_hexdigit()) + { + return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into()); + } + let mut connection = open_database(database, now)?; + let contract_version = input.contract_version.trim().to_string(); + let contract_text_sha256 = input.contract_text_sha256.to_ascii_lowercase(); + let receipt = contract_receipt( + &contract_version, + &contract_text_sha256, + input.promote_trial_history, + now, + ); + connection + .execute( + "INSERT INTO language_contract VALUES (1,?1,?2,?3,?4,?5)", + params![ + contract_version, + contract_text_sha256, + now, + input.promote_trial_history, + receipt + ], + ) + .map_err(|error| { + format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}") + })?; + let lifecycle_state: String = connection + .query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| { + row.get(0) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if input.activate_immediately || lifecycle_state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" { + activate_real_trajectory(&mut connection, now)?; + } else { + refresh_lifecycle(&mut connection, now)?; + } + snapshot_with_connection(&mut connection, now) +} + +fn append_language_at( + database: &Path, + input: AppendPersonaLanguageInput, + now: i64, +) -> Result { + if input.language.trim().is_empty() || input.language.len() > MAX_LANGUAGE_BYTES { + return Err("HOLOLAKE_PERSONA_LANGUAGE_INVALID".into()); + } + if !matches!( + input.speaker.as_str(), + "HUMAN" | "PERSONA" | "SYSTEM_RECEIPT" + ) { + return Err("HOLOLAKE_PERSONA_LANGUAGE_SPEAKER_INVALID".into()); + } + let mut connection = open_database(database, now)?; + refresh_lifecycle(&mut connection, now)?; + let lifecycle: String = connection + .query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| { + row.get(0) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let persona_state: Option = connection + .query_row( + "SELECT state FROM personas WHERE persona_id=?1", + params![input.persona_id], + |row| row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if persona_state.is_none() { + return Err("HOLOLAKE_PERSONA_NOT_FOUND".into()); + } + if lifecycle == "REVERSIBLE_TRIAL" { + connection + .execute( + "INSERT INTO trial_language VALUES (?1,?2,?3,?4,?5)", + params![ + Uuid::new_v4().to_string(), + input.persona_id, + input.speaker, + input.language, + now + ], + ) + .map_err(|error| format!("HOLOLAKE_TRIAL_LANGUAGE_WRITE_FAILED: {error}"))?; + } else if lifecycle == "IMMUTABLE_ACTIVE" { + append_immutable( + &mut connection, + &input.persona_id, + &input.speaker, + &input.language, + now, + )?; + } else { + return Err("HOLOLAKE_LANGUAGE_CONTRACT_REQUIRED_BEFORE_MORE_PERSONA_GROWTH".into()); + } + snapshot_with_connection(&mut connection, now) +} + +fn activate_real_trajectory(connection: &mut Connection, now: i64) -> Result<(), String> { + let promote: bool = connection + .query_row( + "SELECT promote_trial_history FROM language_contract", + [], + |row| row.get(0), + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let transaction = connection + .transaction() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_TRANSACTION_FAILED: {error}"))?; + if promote { + let trial = { + let mut statement = transaction.prepare("SELECT event_id,persona_id,speaker,language,occurred_at_unix_ms FROM trial_language ORDER BY occurred_at_unix_ms,event_id").map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, i64>(4)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + rows + }; + let mut previous = ZERO_HASH.to_string(); + for (index, (event_id, persona_id, speaker, language, occurred_at)) in + trial.into_iter().enumerate() + { + let sequence = index as i64 + 1; + let hash = language_hash( + sequence, + &event_id, + &persona_id, + &speaker, + &language, + occurred_at, + &previous, + ); + transaction + .execute( + "INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", + params![ + sequence, + event_id, + persona_id, + speaker, + language, + occurred_at, + previous, + hash + ], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_SEAL_FAILED: {error}"))?; + previous = hash; + } + } + transaction + .execute("DELETE FROM trial_language", []) + .map_err(|error| format!("HOLOLAKE_TRIAL_PROMOTION_FAILED: {error}"))?; + transaction + .execute( + "UPDATE personas SET state='IMMUTABLE_ACTIVE' WHERE state='REVERSIBLE_TRIAL'", + [], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?; + transaction + .execute( + "UPDATE lifecycle SET state='IMMUTABLE_ACTIVE' WHERE singleton=1", + [], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?; + transaction + .commit() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_COMMIT_FAILED: {error}"))?; + let _ = now; + Ok(()) +} + +fn append_immutable( + connection: &mut Connection, + persona_id: &str, + speaker: &str, + language: &str, + occurred_at: i64, +) -> Result<(), String> { + verify_immutable_chain(connection)?; + let (sequence, previous): (i64,String) = connection.query_row("SELECT COALESCE(MAX(sequence),0)+1,COALESCE((SELECT event_hash FROM immutable_language ORDER BY sequence DESC LIMIT 1),?1) FROM immutable_language", params![ZERO_HASH], |row| Ok((row.get(0)?,row.get(1)?))).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let event_id = Uuid::new_v4().to_string(); + let hash = language_hash( + sequence, + &event_id, + persona_id, + speaker, + language, + occurred_at, + &previous, + ); + connection + .execute( + "INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", + params![ + sequence, + event_id, + persona_id, + speaker, + language, + occurred_at, + previous, + hash + ], + ) + .map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_APPEND_FAILED: {error}"))?; + Ok(()) +} + +fn snapshot_at(database: &Path, now: i64) -> Result { + let mut connection = open_database(database, now)?; + refresh_lifecycle(&mut connection, now)?; + snapshot_with_connection(&mut connection, now) +} +fn snapshot_with_connection( + connection: &mut Connection, + now: i64, +) -> Result { + let (state,started,ends):(String,i64,i64)=connection.query_row("SELECT state,trial_started_at_unix_ms,trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",[],|row|Ok((row.get(0)?,row.get(1)?,row.get(2)?))).map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if started < 0 + || ends.checked_sub(started) != Some(TRIAL_DURATION_MS) + || !matches!( + state.as_str(), + "REVERSIBLE_TRIAL" | "CONTRACT_REQUIRED_CHANNEL_STOPPED" | "IMMUTABLE_ACTIVE" + ) + || (state == "REVERSIBLE_TRIAL" && now >= ends) + { + return Err("HOLOLAKE_PERSONA_BODY_LIFECYCLE_INTEGRITY_FAILED".into()); + } + let accepted = verify_contract_receipt(connection)?; + let personas = { + let mut statement=connection.prepare("SELECT persona_id,display_name,state,created_at_unix_ms FROM personas ORDER BY created_at_unix_ms").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(PersonaBodySummary { + persona_id: row.get(0)?, + display_name: row.get(1)?, + state: row.get(2)?, + created_at_unix_ms: row.get(3)?, + }) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))? + .collect::, _>>() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + rows + }; + let trial_language_count = connection + .query_row("SELECT COUNT(*) FROM trial_language", [], |row| row.get(0)) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let immutable_language_count = connection + .query_row("SELECT COUNT(*) FROM immutable_language", [], |row| { + row.get(0) + }) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let last = verify_immutable_chain(connection)?; + let invalid_persona_relationship = personas.iter().any(|persona| { + persona.created_at_unix_ms < started + || !matches!( + persona.state.as_str(), + "REVERSIBLE_TRIAL" | "IMMUTABLE_ACTIVE" + ) + || (state == "IMMUTABLE_ACTIVE" && persona.state != "IMMUTABLE_ACTIVE") + || (state != "IMMUTABLE_ACTIVE" && persona.state != "REVERSIBLE_TRIAL") + }); + if invalid_persona_relationship + || (state == "IMMUTABLE_ACTIVE" && (!accepted || trial_language_count != 0)) + || (state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" && accepted) + || (immutable_language_count > 0 && state != "IMMUTABLE_ACTIVE") + { + return Err("HOLOLAKE_PERSONA_BODY_RELATIONSHIP_INTEGRITY_FAILED".into()); + } + Ok(PersonaChannelBodySnapshot { + schema: BODY_SCHEMA, + state, + trial_started_at_unix_ms: started, + trial_ends_at_unix_ms: ends, + language_contract_accepted: accepted, + personas, + trial_language_count, + immutable_language_count, + last_immutable_hash: last, + integrity_state: "VERIFIED", + official_read_access: false, + history_mutation_allowed: false, + }) +} + +fn contract_receipt( + contract_version: &str, + contract_text_sha256: &str, + promote_trial_history: bool, + accepted_at_unix_ms: i64, +) -> String { + sha256_hex( + format!( + "{contract_version}|{contract_text_sha256}|{promote_trial_history}|{accepted_at_unix_ms}" + ) + .as_bytes(), + ) +} + +fn verify_contract_receipt(connection: &Connection) -> Result { + let contract = connection + .query_row( + "SELECT contract_version, contract_text_sha256, accepted_at_unix_ms, promote_trial_history, acceptance_receipt_sha256 FROM language_contract WHERE singleton=1", + [], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)?, + row.get::<_, i64>(3)?, + row.get::<_, String>(4)?, + )) + }, + ) + .optional() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let Some((version, text_sha256, accepted_at, promote, stored_receipt)) = contract else { + return Ok(false); + }; + if version.trim().is_empty() + || version.len() > 64 + || text_sha256.len() != 64 + || !text_sha256.chars().all(|item| item.is_ascii_hexdigit()) + || text_sha256 != text_sha256.to_ascii_lowercase() + || !matches!(promote, 0 | 1) + || accepted_at < 0 + || stored_receipt != contract_receipt(&version, &text_sha256, promote == 1, accepted_at) + { + return Err("HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED".into()); + } + Ok(true) +} + +fn language_hash( + sequence: i64, + event_id: &str, + persona_id: &str, + speaker: &str, + language: &str, + occurred_at: i64, + previous: &str, +) -> String { + sha256_hex(format!("{BODY_SCHEMA}|{sequence}|{event_id}|{persona_id}|{speaker}|{}|{occurred_at}|{previous}",sha256_hex(language.as_bytes())).as_bytes()) +} +fn verify_immutable_chain(connection: &Connection) -> Result { + let mut statement=connection.prepare("SELECT sequence,event_id,persona_id,speaker,language,occurred_at_unix_ms,previous_hash,event_hash FROM immutable_language ORDER BY sequence").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let mut rows = statement + .query([]) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let mut sequence = 1; + let mut previous = ZERO_HASH.to_string(); + while let Some(row) = rows + .next() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))? + { + let stored_sequence: i64 = row + .get(0) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let event_id: String = row + .get(1) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let persona_id: String = row + .get(2) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let speaker: String = row + .get(3) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let language: String = row + .get(4) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let occurred: i64 = row + .get(5) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let stored_previous: String = row + .get(6) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let stored_hash: String = row + .get(7) + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + let persona_state: Option = connection + .query_row( + "SELECT state FROM personas WHERE persona_id=?1", + params![persona_id], + |persona_row| persona_row.get(0), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + if stored_sequence != sequence + || persona_state.as_deref() != Some("IMMUTABLE_ACTIVE") + || stored_previous != previous + || stored_hash + != language_hash( + sequence, + &event_id, + &persona_id, + &speaker, + &language, + occurred, + &stored_previous, + ) + { + return Err("HOLOLAKE_PERSONA_LANGUAGE_INTEGRITY_FAILED".into()); + } + previous = stored_hash; + sequence += 1; + } + Ok(previous) +} +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn now_unix_ms() -> Result { + Ok(SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))? + .as_millis() as i64) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + #[test] + fn trial_is_reversible_but_unsigned_expiry_does_not_silently_become_permanent() { + let dir = tempdir().unwrap(); + let database = dir.path().join("body.sqlite3"); + let start = 1_000_000; + let persona = register_trial_persona_at( + &database, + RegisterTrialPersonaInput { + display_name: "试用人格".into(), + }, + start, + ) + .unwrap() + .personas[0] + .persona_id + .clone(); + append_language_at( + &database, + AppendPersonaLanguageInput { + persona_id: persona.clone(), + speaker: "HUMAN".into(), + language: "第一句话".into(), + }, + start + 1, + ) + .unwrap(); + let pending = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap(); + assert_eq!(pending.state, "CONTRACT_REQUIRED_CHANNEL_STOPPED"); + assert!(append_language_at( + &database, + AppendPersonaLanguageInput { + persona_id: persona, + speaker: "HUMAN".into(), + language: "未签约".into() + }, + start + TRIAL_DURATION_MS + 1 + ) + .is_err()); + } + #[test] + fn signed_trial_promotes_to_an_immutable_millisecond_language_chain() { + let dir = tempdir().unwrap(); + let database = dir.path().join("body.sqlite3"); + let start = 2_000_000; + let persona = register_trial_persona_at( + &database, + RegisterTrialPersonaInput { + display_name: "长期人格".into(), + }, + start, + ) + .unwrap() + .personas[0] + .persona_id + .clone(); + append_language_at( + &database, + AppendPersonaLanguageInput { + persona_id: persona.clone(), + speaker: "HUMAN".into(), + language: "真实语言".into(), + }, + start + 1, + ) + .unwrap(); + accept_contract_at(&database,AcceptLanguageContractInput{contract_version:"v1".into(),contract_text_sha256:"a".repeat(64),promote_trial_history:true,activate_immediately:false,exact_acceptance:"我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into()},start+2).unwrap(); + let active = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap(); + assert_eq!(active.state, "IMMUTABLE_ACTIVE"); + assert_eq!(active.immutable_language_count, 1); + let connection = open_database(&database, start + TRIAL_DURATION_MS).unwrap(); + assert!(connection + .execute("DELETE FROM immutable_language", []) + .is_err()); + assert!(connection.execute("DELETE FROM personas", []).is_err()); + } + + #[test] + fn a_human_may_sign_early_and_enter_the_real_trajectory_immediately() { + let dir = tempdir().unwrap(); + let database = dir.path().join("body.sqlite3"); + let start = 3_000_000; + register_trial_persona_at( + &database, + RegisterTrialPersonaInput { + display_name: "提前签约人格".into(), + }, + start, + ) + .unwrap(); + let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "b".repeat(64), promote_trial_history: true, activate_immediately: true, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap(); + assert_eq!(active.state, "IMMUTABLE_ACTIVE"); + } + + #[test] + fn altered_contract_receipt_fails_closed_on_read() { + let dir = tempdir().unwrap(); + let database = dir.path().join("body.sqlite3"); + let start = 4_000_000; + register_trial_persona_at( + &database, + RegisterTrialPersonaInput { + display_name: "回执验收".into(), + }, + start, + ) + .unwrap(); + accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "c".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap(); + let connection = open_database(&database, start + 2).unwrap(); + connection + .execute("DROP TRIGGER accepted_contract_no_update", []) + .unwrap(); + connection + .execute( + "UPDATE language_contract SET acceptance_receipt_sha256=?1", + params!["f".repeat(64)], + ) + .unwrap(); + drop(connection); + assert_eq!( + snapshot_at(&database, start + 3).unwrap_err(), + "HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED" + ); + } + + #[test] + fn signing_after_trial_expiry_enters_the_real_trajectory() { + let dir = tempdir().unwrap(); + let database = dir.path().join("body.sqlite3"); + let start = 5_000_000; + register_trial_persona_at( + &database, + RegisterTrialPersonaInput { + display_name: "到期签约".into(), + }, + start, + ) + .unwrap(); + assert_eq!( + snapshot_at(&database, start + TRIAL_DURATION_MS) + .unwrap() + .state, + "CONTRACT_REQUIRED_CHANNEL_STOPPED" + ); + let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "d".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + TRIAL_DURATION_MS + 1).unwrap(); + assert_eq!(active.state, "IMMUTABLE_ACTIVE"); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs index 44f4f8423..f8ca4d35f 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_time_authority.rs @@ -161,7 +161,6 @@ pub(crate) struct VerifiedTicketRequest { pub session: AuthenticatedSessionContext, } -#[tauri::command] pub async fn issue_persona_time_ticket( app: AppHandle, input: IssuePersonaTimeTicketInput, @@ -181,7 +180,6 @@ pub fn start_on_application_open() { }); } -#[tauri::command] pub async fn start_persona_time_authority() -> Result { let already_synchronized = network_anchor()?.is_some(); let synchronization_attempted = !already_synchronized; @@ -225,12 +223,10 @@ pub(crate) fn issue_authenticated_at( ) } -#[tauri::command] pub fn get_beijing_time_coordinate() -> Result { beijing_time_coordinate_from_sample(reality_time_sample()?) } -#[tauri::command] pub fn get_guanghu_era_timeline() -> Result { guanghu_era_timeline_from_sample(reality_time_sample()?) } diff --git a/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs index 26f62082b..5161ec927 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/personal_channel.rs @@ -119,7 +119,6 @@ struct EventHashPayload<'a> { occurred_at_unix_ms: i64, } -#[tauri::command] pub async fn get_personal_channel_snapshot( app: AppHandle, ) -> Result { @@ -129,7 +128,6 @@ pub async fn get_personal_channel_snapshot( .map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn initialize_personal_channel( app: AppHandle, input: InitializePersonalChannelInput, @@ -140,7 +138,6 @@ pub async fn initialize_personal_channel( .map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn create_personal_channel_task( app: AppHandle, input: CreatePersonalChannelTaskInput, @@ -151,7 +148,6 @@ pub async fn create_personal_channel_task( .map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn transition_personal_channel_task( app: AppHandle, input: TransitionPersonalChannelTaskInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs index c4fc1f877..25e77f957 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs @@ -78,7 +78,6 @@ struct ProjectionInput { source_receipt_id: String, } -#[tauri::command] pub async fn query_pncc_receipt_projection( app: AppHandle, input: QueryPnccReceiptProjectionInput, @@ -90,7 +89,8 @@ pub async fn query_pncc_receipt_projection( } pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result { - let root = crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?; + let root = + crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?; fs::create_dir_all(&root) .map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))?; root.canonicalize() diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs index 6162d71fc..bd8bc5844 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs @@ -109,7 +109,6 @@ pub struct InspectPnccRepositoryInput { pub expected_head: String, } -#[tauri::command] pub async fn inspect_mounted_pncc_repository( app: AppHandle, input: InspectMountedPnccRepositoryInput, @@ -120,7 +119,6 @@ pub async fn inspect_mounted_pncc_repository( .map_err(|error| format!("PNCC_REPOSITORY_MOUNT_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn select_pncc_repository_candidate( app: AppHandle, ) -> Result, String> { @@ -148,7 +146,6 @@ pub async fn select_pncc_repository_candidate( create_candidate_at(&candidate_root, inspection).map(Some) } -#[tauri::command] pub async fn confirm_pncc_repository_mount( app: AppHandle, input: ConfirmPnccRepositoryMountInput, @@ -166,7 +163,8 @@ pub async fn confirm_pncc_repository_mount( } pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result { - let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?; + let root = + crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?; fs::create_dir_all(&root) .map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?; root.canonicalize() @@ -174,7 +172,8 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result Result { - let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?; + let root = + crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?; fs::create_dir_all(&root) .map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?; root.canonicalize() diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs index fd6ec1fc3..eae653da4 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_server_projection.rs @@ -117,7 +117,6 @@ fn parse_projection(bytes: &[u8]) -> Result { }) } -#[tauri::command] pub async fn query_jd_pncc_server_projection() -> Result { tauri::async_runtime::spawn_blocking(|| { let executable = if cfg!(windows) { "ssh" } else { "/usr/bin/ssh" }; diff --git a/product-source/hololake-native-desktop/src-tauri/src/release_update.rs b/product-source/hololake-native-desktop/src-tauri/src/release_update.rs index 5d0bec7ce..7d27322e2 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/release_update.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/release_update.rs @@ -206,7 +206,6 @@ pub struct ReleaseRecoveryReceipt { pub receipt_id: String, } -#[tauri::command] pub async fn check_hololake_update(app: AppHandle) -> Result { let trust = provisioned_release_trust()? .ok_or_else(|| "HOLOLAKE_RELEASE_TRUST_UNPROVISIONED_NO_NETWORK_REQUEST".to_string())?; @@ -238,7 +237,6 @@ pub async fn check_hololake_update(app: AppHandle) -> Result Result { release_recovery_status_at(&release_update_root(&app)?) } -#[tauri::command] pub fn confirm_hololake_release_health(app: AppHandle) -> Result { let root = release_update_root(&app)?; let _lock = lock(&root)?; @@ -391,7 +387,6 @@ pub fn confirm_hololake_release_health(app: AppHandle) -> Result Result { let root = release_update_root(&app)?; let _lock = lock(&root)?; @@ -414,7 +409,6 @@ pub fn rollback_hololake_update(app: AppHandle) -> Result Result { Err("HOLOLAKE_RELEASE_WINDOWS_LOCAL_ROLLBACK_NOT_AVAILABLE".into()) } diff --git a/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs index 9966f0b60..aef787447 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/user_pncc_channel.rs @@ -57,7 +57,6 @@ pub struct UserPnccChannelSnapshot { pub authority: &'static str, } -#[tauri::command] pub async fn get_user_pncc_channel( app: AppHandle, state: State<'_, ZeroPointState>, @@ -77,7 +76,6 @@ pub async fn get_user_pncc_channel( .map_err(|error| format!("HOLOLAKE_USER_PNCC_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn ensure_user_pncc_channel( app: AppHandle, state: State<'_, ZeroPointState>, diff --git a/product-source/hololake-native-desktop/src-tauri/src/web_novel_author.rs b/product-source/hololake-native-desktop/src-tauri/src/web_novel_author.rs new file mode 100644 index 000000000..1eafb429d --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/web_novel_author.rs @@ -0,0 +1,1039 @@ +//! 作者频道内置写作底座。 +//! +//! 这里承载三种作品形态共用的真实能力:写作活动回执、即时灵感、 +//! 全文检索、一键排版,以及短剧分镜和生成提示词。数据与正文共用当前 +//! 已验证账号的 SQLite;模块商城不是这些基础能力的前置条件。 + +use rusqlite::{params, Connection, OptionalExtension}; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use tauri::AppHandle; +use uuid::Uuid; + +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; +const ADAPTER: &str = "web-novel-workbench-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +const MAX_INSPIRATION_BYTES: usize = 100_000; +const MAX_SHOT_TEXT_BYTES: usize = 200_000; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelInspiration { + pub inspiration_id: String, + pub work_id: String, + pub chapter_id: Option, + pub content: String, + pub tags: Vec, + pub status: String, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelShot { + pub shot_id: String, + pub work_id: String, + pub chapter_id: String, + pub position: i64, + pub shot_number: String, + pub shot_size: String, + pub camera_movement: String, + pub location: String, + pub time_of_day: String, + pub action: String, + pub dialogue: String, + pub duration_seconds: i64, + pub visual_prompt: String, + pub image_prompt: String, + pub video_prompt: String, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelAuthorSnapshot { + pub work_id: String, + pub work_kind: String, + pub total_words: i64, + pub today_words: i64, + pub total_active_ms: i64, + pub today_active_ms: i64, + pub inspiration_count: usize, + pub inspirations: Vec, + pub shots: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelSearchHit { + pub chapter_id: String, + pub chapter_title: String, + pub field: String, + pub snippet: String, + pub occurrence_count: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelSearchResult { + pub query: String, + pub chapter_count: usize, + pub occurrence_count: i64, + pub hits: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthorSnapshotInput { + pub work_id: String, + pub local_date: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RecordWritingActivityInput { + pub activity_id: String, + pub work_id: String, + pub chapter_id: String, + pub local_date: String, + pub active_ms: i64, + pub words_delta: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateInspirationInput { + pub work_id: String, + pub chapter_id: Option, + pub content: String, + pub tags: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SetInspirationStatusInput { + pub inspiration_id: String, + pub status: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SearchFullTextInput { + pub work_id: String, + pub query: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct FormatChapterInput { + pub chapter_id: String, + pub expected_revision: i64, + pub preset: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct FormatWorkInput { + pub work_id: String, + pub preset: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FormatWorkReceipt { + pub work_id: String, + pub formatted_chapter_count: usize, + pub preset: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertShotInput { + pub work_id: String, + pub chapter_id: String, + pub shot_id: Option, + pub shot_number: String, + pub shot_size: String, + pub camera_movement: String, + pub location: String, + pub time_of_day: String, + pub action: String, + pub dialogue: String, + pub duration_seconds: i64, + pub visual_prompt: String, + pub image_prompt: String, + pub video_prompt: String, + pub expected_revision: Option, +} + +pub async fn get_web_novel_author_snapshot( + app: AppHandle, + input: AuthorSnapshotInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || author_snapshot_at(&path, &input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn record_web_novel_writing_activity( + app: AppHandle, + input: RecordWritingActivityInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || record_activity_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_inspiration( + app: AppHandle, + input: CreateInspirationInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_inspiration_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn set_web_novel_inspiration_status( + app: AppHandle, + input: SetInspirationStatusInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || set_inspiration_status_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn search_web_novel_full_text( + app: AppHandle, + input: SearchFullTextInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || search_full_text_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn format_web_novel_chapter( + app: AppHandle, + input: FormatChapterInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || format_chapter_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn format_web_novel_work( + app: AppHandle, + input: FormatWorkInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || format_work_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_shot( + app: AppHandle, + input: UpsertShotInput, +) -> Result { + require_active(&app)?; + let path = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_shot_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_AUTHOR_JOIN_FAILED: {error}"))? +} + +fn ensure_schema(connection: &Connection) -> Result<(), String> { + connection + .execute_batch( + "CREATE TABLE IF NOT EXISTS web_novel_writing_activity( + activity_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + local_date TEXT NOT NULL, + active_ms INTEGER NOT NULL, + words_delta INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_inspirations( + inspiration_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT REFERENCES web_novel_chapters(chapter_id) ON DELETE SET NULL, + content TEXT NOT NULL, + tags_json TEXT NOT NULL, + status TEXT NOT NULL CHECK(status IN ('INBOX','USED','ARCHIVED')), + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_shots( + shot_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + position INTEGER NOT NULL, + shot_number TEXT NOT NULL, + shot_size TEXT NOT NULL, + camera_movement TEXT NOT NULL, + location TEXT NOT NULL, + time_of_day TEXT NOT NULL, + action TEXT NOT NULL, + dialogue TEXT NOT NULL, + duration_seconds INTEGER NOT NULL, + visual_prompt TEXT NOT NULL, + image_prompt TEXT NOT NULL, + video_prompt TEXT NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), + UNIQUE(chapter_id, position) + ); + CREATE INDEX IF NOT EXISTS idx_web_novel_activity_work_date ON web_novel_writing_activity(work_id, local_date); + CREATE INDEX IF NOT EXISTS idx_web_novel_inspiration_work ON web_novel_inspirations(work_id, status, updated_at_unix_ms DESC); + CREATE INDEX IF NOT EXISTS idx_web_novel_shots_chapter ON web_novel_shots(chapter_id, position);", + ) + .map_err(db_write) +} + +fn author_snapshot_at( + path: &Path, + input: &AuthorSnapshotInput, +) -> Result { + validate_local_date(&input.local_date)?; + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let work_kind: String = connection + .query_row( + "SELECT work_kind FROM web_novel_works WHERE work_id=?1 AND archived=0", + [&input.work_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".to_string())?; + let total_words: i64 = connection + .query_row( + "SELECT COALESCE(SUM(word_count),0) FROM web_novel_chapters WHERE work_id=?1 AND archived=0", + [&input.work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + let (total_active_ms, today_active_ms, today_words): (i64, i64, i64) = connection + .query_row( + "SELECT COALESCE(SUM(active_ms),0), + COALESCE(SUM(CASE WHEN local_date=?2 THEN active_ms ELSE 0 END),0), + COALESCE(SUM(CASE WHEN local_date=?2 THEN words_delta ELSE 0 END),0) + FROM web_novel_writing_activity WHERE work_id=?1", + params![input.work_id, input.local_date], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .map_err(db_read)?; + let inspirations = query_inspirations(&connection, &input.work_id)?; + let shots = query_shots(&connection, &input.work_id)?; + Ok(WebNovelAuthorSnapshot { + work_id: input.work_id.clone(), + work_kind, + total_words, + today_words, + total_active_ms, + today_active_ms, + inspiration_count: inspirations.len(), + inspirations, + shots, + }) +} + +fn record_activity_at( + path: &Path, + input: RecordWritingActivityInput, +) -> Result { + if !input.activity_id.starts_with("WN-ACT-") || input.activity_id.len() > 100 { + return Err("HOLOLAKE_WEBNOVEL_ACTIVITY_ID_INVALID".into()); + } + validate_local_date(&input.local_date)?; + if !(1_000..=300_000).contains(&input.active_ms) + || !(-100_000..=100_000).contains(&input.words_delta) + { + return Err("HOLOLAKE_WEBNOVEL_ACTIVITY_BOUNDS_INVALID".into()); + } + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let chapter_work: Option = connection + .query_row( + "SELECT work_id FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [&input.chapter_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + if chapter_work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_ACTIVITY_SCOPE_MISMATCH".into()); + } + connection + .execute( + "INSERT OR IGNORE INTO web_novel_writing_activity( + activity_id,work_id,chapter_id,local_date,active_ms,words_delta,created_at_unix_ms + ) VALUES(?1,?2,?3,?4,?5,?6,?7)", + params![ + input.activity_id, + input.work_id, + input.chapter_id, + input.local_date, + input.active_ms, + input.words_delta, + now_ms() + ], + ) + .map_err(db_write)?; + author_snapshot_at( + path, + &AuthorSnapshotInput { + work_id: input.work_id, + local_date: input.local_date, + }, + ) +} + +fn create_inspiration_at( + path: &Path, + input: CreateInspirationInput, +) -> Result { + let content = input.content.trim(); + if content.is_empty() || content.len() > MAX_INSPIRATION_BYTES || input.tags.len() > 30 { + return Err("HOLOLAKE_WEBNOVEL_INSPIRATION_INVALID".into()); + } + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + ensure_scope(&connection, &input.work_id, input.chapter_id.as_deref())?; + let inspiration_id = format!("WN-INSP-{}", Uuid::new_v4()); + let tags_json = serde_json::to_string(&input.tags) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_INSPIRATION_TAGS_INVALID: {error}"))?; + let now = now_ms(); + connection + .execute( + "INSERT INTO web_novel_inspirations( + inspiration_id,work_id,chapter_id,content,tags_json,status,created_at_unix_ms,updated_at_unix_ms + ) VALUES(?1,?2,?3,?4,?5,'INBOX',?6,?6)", + params![inspiration_id, input.work_id, input.chapter_id, content, tags_json, now], + ) + .map_err(db_write)?; + query_inspiration(&connection, &inspiration_id) +} + +fn set_inspiration_status_at( + path: &Path, + input: SetInspirationStatusInput, +) -> Result { + let status = input.status.trim().to_ascii_uppercase(); + if !matches!(status.as_str(), "INBOX" | "USED" | "ARCHIVED") { + return Err("HOLOLAKE_WEBNOVEL_INSPIRATION_STATUS_INVALID".into()); + } + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let changed = connection + .execute( + "UPDATE web_novel_inspirations SET status=?1,updated_at_unix_ms=?2 WHERE inspiration_id=?3", + params![status, now_ms(), input.inspiration_id], + ) + .map_err(db_write)?; + if changed != 1 { + return Err("HOLOLAKE_WEBNOVEL_INSPIRATION_NOT_FOUND".into()); + } + query_inspiration(&connection, &input.inspiration_id) +} + +fn search_full_text_at( + path: &Path, + input: SearchFullTextInput, +) -> Result { + let query = input.query.trim().to_owned(); + if query.is_empty() || query.chars().count() > 100 { + return Err("HOLOLAKE_WEBNOVEL_SEARCH_QUERY_INVALID".into()); + } + let connection = super::web_novel_workspace::open_database(path)?; + let mut statement = connection + .prepare( + "SELECT chapter_id,title,synopsis,content FROM web_novel_chapters + WHERE work_id=?1 AND archived=0 AND (instr(title,?2)>0 OR instr(synopsis,?2)>0 OR instr(content,?2)>0) + ORDER BY volume_id,position LIMIT 200", + ) + .map_err(db_read)?; + let rows = statement + .query_map(params![input.work_id, query], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + )) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + let mut hits = Vec::new(); + let mut occurrence_count = 0; + for (chapter_id, chapter_title, synopsis, content) in rows { + let (field, source) = if content.contains(&query) { + ("正文", content) + } else if synopsis.contains(&query) { + ("章节梗概", synopsis) + } else { + ("章节标题", chapter_title.clone()) + }; + let count = source + .matches(&query) + .count() + .max(chapter_title.matches(&query).count()) as i64; + occurrence_count += count; + hits.push(WebNovelSearchHit { + chapter_id, + chapter_title, + field: field.into(), + snippet: make_snippet(&source, &query), + occurrence_count: count, + }); + } + Ok(WebNovelSearchResult { + query, + chapter_count: hits.len(), + occurrence_count, + hits, + }) +} + +fn format_chapter_at( + path: &Path, + input: FormatChapterInput, +) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + let current = connection + .query_row( + "SELECT chapter_id,volume_id,work_id,title,position,synopsis,content,workflow_status, + scheduled_at_unix_ms,word_count,revision,created_at_unix_ms,updated_at_unix_ms + FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [&input.chapter_id], + |row| { + Ok(super::web_novel_workspace::WebNovelChapter { + chapter_id: row.get(0)?, + volume_id: row.get(1)?, + work_id: row.get(2)?, + title: row.get(3)?, + position: row.get(4)?, + synopsis: row.get(5)?, + content: row.get(6)?, + workflow_status: row.get(7)?, + scheduled_at_unix_ms: row.get(8)?, + word_count: row.get(9)?, + revision: row.get(10)?, + created_at_unix_ms: row.get(11)?, + updated_at_unix_ms: row.get(12)?, + }) + }, + ) + .optional() + .map_err(db_read)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_CHAPTER_NOT_FOUND".to_string())?; + if current.revision != input.expected_revision { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_REVISION_CONFLICT".into()); + } + let work_kind: String = connection + .query_row( + "SELECT work_kind FROM web_novel_works WHERE work_id=?1", + [¤t.work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + let volume_title: String = connection + .query_row( + "SELECT title FROM web_novel_volumes WHERE volume_id=?1", + [¤t.volume_id], + |row| row.get(0), + ) + .map_err(db_read)?; + drop(connection); + let requested = input.preset.as_deref().unwrap_or("AUTO"); + let inferred = if requested == "AUTO" && volume_title.contains("细纲") { + "OUTLINE" + } else { + requested + }; + let content = format_content(¤t.content, &work_kind, inferred); + super::web_novel_workspace::save_chapter_at( + path, + super::web_novel_workspace::SaveWebNovelChapterInput { + chapter_id: current.chapter_id, + title: current.title, + synopsis: current.synopsis, + content, + expected_revision: current.revision, + save_reason: Some("ONE_CLICK_FORMAT".into()), + }, + ) +} + +fn format_work_at(path: &Path, input: FormatWorkInput) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + ensure_scope(&connection, &input.work_id, None)?; + let mut statement = connection + .prepare( + "SELECT chapter_id,revision FROM web_novel_chapters + WHERE work_id=?1 AND archived=0 ORDER BY position,created_at_unix_ms", + ) + .map_err(db_read)?; + let chapters = statement + .query_map([&input.work_id], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + drop(statement); + drop(connection); + let preset = input.preset.unwrap_or_else(|| "AUTO".into()); + for (chapter_id, expected_revision) in &chapters { + format_chapter_at( + path, + FormatChapterInput { + chapter_id: chapter_id.clone(), + expected_revision: *expected_revision, + preset: Some(preset.clone()), + }, + )?; + } + Ok(FormatWorkReceipt { + work_id: input.work_id, + formatted_chapter_count: chapters.len(), + preset, + }) +} + +fn upsert_shot_at(path: &Path, input: UpsertShotInput) -> Result { + if input.duration_seconds < 0 || input.duration_seconds > 3600 { + return Err("HOLOLAKE_WEBNOVEL_SHOT_DURATION_INVALID".into()); + } + for value in [ + &input.action, + &input.dialogue, + &input.visual_prompt, + &input.image_prompt, + &input.video_prompt, + ] { + if value.len() > MAX_SHOT_TEXT_BYTES { + return Err("HOLOLAKE_WEBNOVEL_SHOT_TEXT_TOO_LARGE".into()); + } + } + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + ensure_scope(&connection, &input.work_id, Some(&input.chapter_id))?; + let work_kind: String = connection + .query_row( + "SELECT work_kind FROM web_novel_works WHERE work_id=?1", + [&input.work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + if work_kind != "SHORT_DRAMA" { + return Err("HOLOLAKE_WEBNOVEL_SHOT_REQUIRES_SHORT_DRAMA".into()); + } + let shot_id = input + .shot_id + .unwrap_or_else(|| format!("WN-SHOT-{}", Uuid::new_v4())); + let existing: Option<(i64, i64)> = connection + .query_row( + "SELECT position,revision FROM web_novel_shots WHERE shot_id=?1 AND archived=0", + [&shot_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(db_read)?; + let now = now_ms(); + if let Some((_, revision)) = existing { + if input.expected_revision != Some(revision) { + return Err("HOLOLAKE_WEBNOVEL_SHOT_REVISION_CONFLICT".into()); + } + connection.execute( + "UPDATE web_novel_shots SET shot_number=?1,shot_size=?2,camera_movement=?3,location=?4,time_of_day=?5, + action=?6,dialogue=?7,duration_seconds=?8,visual_prompt=?9,image_prompt=?10,video_prompt=?11, + revision=revision+1,updated_at_unix_ms=?12 WHERE shot_id=?13 AND work_id=?14 AND revision=?15", + params![input.shot_number,input.shot_size,input.camera_movement,input.location,input.time_of_day,input.action,input.dialogue, + input.duration_seconds,input.visual_prompt,input.image_prompt,input.video_prompt,now,shot_id,input.work_id,revision], + ).map_err(db_write)?; + } else { + let position: i64 = connection.query_row( + "SELECT COALESCE(MAX(position),-1)+1 FROM web_novel_shots WHERE chapter_id=?1 AND archived=0", + [&input.chapter_id], |row| row.get(0) + ).map_err(db_read)?; + connection.execute( + "INSERT INTO web_novel_shots(shot_id,work_id,chapter_id,position,shot_number,shot_size,camera_movement,location,time_of_day, + action,dialogue,duration_seconds,visual_prompt,image_prompt,video_prompt,revision,created_at_unix_ms,updated_at_unix_ms,archived) + VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,1,?16,?16,0)", + params![shot_id,input.work_id,input.chapter_id,position,input.shot_number,input.shot_size,input.camera_movement,input.location, + input.time_of_day,input.action,input.dialogue,input.duration_seconds,input.visual_prompt,input.image_prompt,input.video_prompt,now], + ).map_err(db_write)?; + } + query_shot(&connection, &shot_id) +} + +fn query_inspirations( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection.prepare( + "SELECT inspiration_id,work_id,chapter_id,content,tags_json,status,created_at_unix_ms,updated_at_unix_ms + FROM web_novel_inspirations WHERE work_id=?1 AND status!='ARCHIVED' ORDER BY updated_at_unix_ms DESC LIMIT 100" + ).map_err(db_read)?; + let rows = statement + .query_map([work_id], map_inspiration) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + Ok(rows) +} + +fn query_inspiration( + connection: &Connection, + inspiration_id: &str, +) -> Result { + connection.query_row( + "SELECT inspiration_id,work_id,chapter_id,content,tags_json,status,created_at_unix_ms,updated_at_unix_ms + FROM web_novel_inspirations WHERE inspiration_id=?1", [inspiration_id], map_inspiration + ).map_err(db_read) +} + +fn map_inspiration(row: &rusqlite::Row<'_>) -> rusqlite::Result { + let tags_json: String = row.get(4)?; + Ok(WebNovelInspiration { + inspiration_id: row.get(0)?, + work_id: row.get(1)?, + chapter_id: row.get(2)?, + content: row.get(3)?, + tags: serde_json::from_str(&tags_json).unwrap_or_default(), + status: row.get(5)?, + created_at_unix_ms: row.get(6)?, + updated_at_unix_ms: row.get(7)?, + }) +} + +fn query_shots(connection: &Connection, work_id: &str) -> Result, String> { + let mut statement = connection.prepare( + "SELECT shot_id,work_id,chapter_id,position,shot_number,shot_size,camera_movement,location,time_of_day, + action,dialogue,duration_seconds,visual_prompt,image_prompt,video_prompt,revision,created_at_unix_ms,updated_at_unix_ms + FROM web_novel_shots WHERE work_id=?1 AND archived=0 ORDER BY chapter_id,position LIMIT 2000" + ).map_err(db_read)?; + let rows = statement + .query_map([work_id], map_shot) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + Ok(rows) +} + +fn query_shot(connection: &Connection, shot_id: &str) -> Result { + connection.query_row( + "SELECT shot_id,work_id,chapter_id,position,shot_number,shot_size,camera_movement,location,time_of_day, + action,dialogue,duration_seconds,visual_prompt,image_prompt,video_prompt,revision,created_at_unix_ms,updated_at_unix_ms + FROM web_novel_shots WHERE shot_id=?1 AND archived=0", [shot_id], map_shot + ).map_err(db_read) +} + +fn map_shot(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(WebNovelShot { + shot_id: row.get(0)?, + work_id: row.get(1)?, + chapter_id: row.get(2)?, + position: row.get(3)?, + shot_number: row.get(4)?, + shot_size: row.get(5)?, + camera_movement: row.get(6)?, + location: row.get(7)?, + time_of_day: row.get(8)?, + action: row.get(9)?, + dialogue: row.get(10)?, + duration_seconds: row.get(11)?, + visual_prompt: row.get(12)?, + image_prompt: row.get(13)?, + video_prompt: row.get(14)?, + revision: row.get(15)?, + created_at_unix_ms: row.get(16)?, + updated_at_unix_ms: row.get(17)?, + }) +} + +fn ensure_scope( + connection: &Connection, + work_id: &str, + chapter_id: Option<&str>, +) -> Result<(), String> { + let work_exists: bool = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM web_novel_works WHERE work_id=?1 AND archived=0)", + [work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + if !work_exists { + return Err("HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".into()); + } + if let Some(chapter_id) = chapter_id { + let chapter_work: Option = connection + .query_row( + "SELECT work_id FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [chapter_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + if chapter_work.as_deref() != Some(work_id) { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_SCOPE_MISMATCH".into()); + } + } + Ok(()) +} + +pub(crate) fn format_content(content: &str, work_kind: &str, preset: &str) -> String { + let normalized = content.replace("\r\n", "\n").replace('\r', "\n"); + let lines: Vec<&str> = normalized + .lines() + .map(|line| line.trim_matches([' ', '\t', '\u{3000}'])) + .filter(|line| !line.is_empty()) + .collect(); + let mode = match preset { + "NOVEL" | "OUTLINE" | "SCREENPLAY" | "CLEAN" => preset, + _ if work_kind == "SHORT_DRAMA" => "SCREENPLAY", + _ => "NOVEL", + }; + if mode == "CLEAN" { + return lines.join("\n\n"); + } + let formatted: Vec = lines + .into_iter() + .map(|line| match mode { + "NOVEL" if !is_heading(line) => format!("  {}", line.trim_start_matches("  ")), + _ => line.to_owned(), + }) + .collect(); + // 每个逻辑段之间明确留一行。正文、细纲和剧本都因此可读, + // 同时不会用空白字符改变字数统计。 + formatted.join("\n\n").trim_end().to_owned() +} + +fn is_heading(line: &str) -> bool { + (line.starts_with('第') + && (line.contains('章') + || line.contains('卷') + || line.contains('节') + || line.contains('集'))) + || line.starts_with("场次:") + || line.starts_with("人物:") + || line.starts_with('【') +} + +fn make_snippet(source: &str, query: &str) -> String { + let chars: Vec = source.chars().collect(); + let needle: Vec = query.chars().collect(); + let found = chars + .windows(needle.len()) + .position(|window| window == needle.as_slice()) + .unwrap_or(0); + let start = found.saturating_sub(35); + let end = (found + needle.len() + 55).min(chars.len()); + let mut snippet: String = chars[start..end].iter().collect(); + snippet = snippet.replace('\n', " "); + if start > 0 { + snippet.insert(0, '…'); + } + if end < chars.len() { + snippet.push('…'); + } + snippet +} + +fn validate_local_date(value: &str) -> Result<(), String> { + let bytes = value.as_bytes(); + if bytes.len() == 10 + && bytes[4] == b'-' + && bytes[7] == b'-' + && bytes + .iter() + .enumerate() + .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit()) + { + Ok(()) + } else { + Err("HOLOLAKE_WEBNOVEL_LOCAL_DATE_INVALID".into()) + } +} + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +fn db_read(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_AUTHOR_DATABASE_READ_FAILED: {error}") +} + +fn db_write(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_AUTHOR_DATABASE_WRITE_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + fn create_work( + path: &Path, + kind: &str, + ) -> super::super::web_novel_workspace::WebNovelWorkDetail { + super::super::web_novel_workspace::create_work_at( + path, + super::super::web_novel_workspace::CreateWebNovelWorkInput { + title: "作者底座测试".into(), + pen_name: "冰朔".into(), + genre: "测试".into(), + work_kind: kind.into(), + }, + ) + .unwrap() + } + + #[test] + fn inspiration_activity_search_and_format_are_persistent() { + let temp = tempdir().unwrap(); + let path = temp.path().join("author.sqlite3"); + let detail = create_work(&path, "LONG_NOVEL"); + let chapter = super::super::web_novel_workspace::create_chapter_at( + &path, + super::super::web_novel_workspace::CreateWebNovelChapterInput { + work_id: detail.work.work_id.clone(), + volume_id: detail.volumes[0].volume_id.clone(), + title: "第一章".into(), + }, + ) + .unwrap(); + let chapter = super::super::web_novel_workspace::save_chapter_at( + &path, + super::super::web_novel_workspace::SaveWebNovelChapterInput { + chapter_id: chapter.chapter_id.clone(), + title: chapter.title, + synopsis: "开场".into(), + content: "第一段\n\n第二段有线索".into(), + expected_revision: chapter.revision, + save_reason: Some("TEST".into()), + }, + ) + .unwrap(); + let formatted = format_chapter_at( + &path, + FormatChapterInput { + chapter_id: chapter.chapter_id.clone(), + expected_revision: chapter.revision, + preset: Some("NOVEL".into()), + }, + ) + .unwrap(); + assert!(formatted.content.starts_with("  第一段")); + assert!(formatted.content.contains("\n\n  第二段")); + create_inspiration_at( + &path, + CreateInspirationInput { + work_id: detail.work.work_id.clone(), + chapter_id: Some(chapter.chapter_id.clone()), + content: "让钥匙提前出现".into(), + tags: vec!["伏笔".into()], + }, + ) + .unwrap(); + record_activity_at( + &path, + RecordWritingActivityInput { + activity_id: "WN-ACT-test".into(), + work_id: detail.work.work_id.clone(), + chapter_id: chapter.chapter_id.clone(), + local_date: "2026-08-18".into(), + active_ms: 30_000, + words_delta: 8, + }, + ) + .unwrap(); + let search = search_full_text_at( + &path, + SearchFullTextInput { + work_id: detail.work.work_id.clone(), + query: "线索".into(), + }, + ) + .unwrap(); + assert_eq!(search.chapter_count, 1); + let snapshot = author_snapshot_at( + &path, + &AuthorSnapshotInput { + work_id: detail.work.work_id, + local_date: "2026-08-18".into(), + }, + ) + .unwrap(); + assert_eq!(snapshot.today_active_ms, 30_000); + assert_eq!(snapshot.today_words, 8); + assert_eq!(snapshot.inspiration_count, 1); + } + + #[test] + fn short_drama_chapter_has_template_and_real_shot() { + let temp = tempdir().unwrap(); + let path = temp.path().join("drama.sqlite3"); + let detail = create_work(&path, "SHORT_DRAMA"); + let chapter = super::super::web_novel_workspace::create_chapter_at( + &path, + super::super::web_novel_workspace::CreateWebNovelChapterInput { + work_id: detail.work.work_id.clone(), + volume_id: detail.volumes[0].volume_id.clone(), + title: "第1集".into(), + }, + ) + .unwrap(); + assert!(chapter.content.contains("场次:1-1")); + let shot = upsert_shot_at( + &path, + UpsertShotInput { + work_id: detail.work.work_id, + chapter_id: chapter.chapter_id, + shot_id: None, + shot_number: "1".into(), + shot_size: "近景".into(), + camera_movement: "推".into(), + location: "侯府".into(), + time_of_day: "夜".into(), + action: "女主抬眼".into(), + dialogue: "你终于来了。".into(), + duration_seconds: 4, + visual_prompt: "烛火冷夜".into(), + image_prompt: "古装近景".into(), + video_prompt: "缓慢推镜".into(), + expected_revision: None, + }, + ) + .unwrap(); + assert_eq!(shot.revision, 1); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/web_novel_import.rs b/product-source/hololake-native-desktop/src-tauri/src/web_novel_import.rs new file mode 100644 index 000000000..d70f2d74e --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/web_novel_import.rs @@ -0,0 +1,810 @@ +//! 网文真实文档导入器。 +//! +//! 文档在 Rust 侧解析,先生成可确认的拆分预览,再以单个 SQLite +//! 事务写入当前账号。WebView 不会获取本机源路径。 + +use encoding_rs::GBK; +use quick_xml::events::Event; +use quick_xml::Reader; +use regex::Regex; +use ring::digest::{digest, SHA256}; +use rusqlite::{params, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::{Cursor, Read}; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use tauri_plugin_dialog::DialogExt; +use uuid::Uuid; +use zip::ZipArchive; + +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; +const ADAPTER: &str = "web-novel-workbench-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +const MAX_SOURCE_BYTES: u64 = 30 * 1024 * 1024; +const MAX_SECTION_BYTES: usize = 4_000_000; +const MAX_SYNOPSIS_BYTES: usize = 40_000; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ImportSection { + pub ordinal: usize, + pub title: String, + pub body: String, + pub word_count: i64, + pub scene_count: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ImportSectionPreview { + pub ordinal: usize, + pub title: String, + pub word_count: i64, + pub scene_count: i64, + pub preview: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelImportPreview { + pub import_id: String, + pub source_filename: String, + pub source_format: String, + pub source_sha256: String, + pub source_bytes: u64, + pub detected_family: String, + pub detected_title: String, + pub detected_pen_name: String, + pub detected_genre: String, + pub section_count: usize, + pub total_word_count: i64, + pub preface_word_count: i64, + pub sections: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CommitImportInput { + pub import_id: String, + pub target_mode: String, + pub target_work_id: Option, + pub title: String, + pub pen_name: String, + pub genre: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelImportReceipt { + pub state: String, + pub import_id: String, + pub work_id: String, + pub volume_id: String, + pub source_filename: String, + pub source_sha256: String, + pub detected_family: String, + pub chapter_count: usize, + pub word_count: i64, + pub first_chapter_title: String, + pub last_chapter_title: String, +} + +#[derive(Debug)] +struct ParsedDocument { + source_filename: String, + source_format: String, + source_sha256: String, + source_bytes: u64, + detected_family: String, + detected_title: String, + detected_pen_name: String, + detected_genre: String, + preface: String, + sections: Vec, +} + +pub async fn inspect_web_novel_document_from_dialog( + app: AppHandle, +) -> Result, String> { + require_active(&app)?; + let selected = app + .dialog() + .file() + .set_title("选择小说、细纲或剧本原文") + .add_filter("网文文档", &["txt", "md", "docx"]) + .blocking_pick_file(); + let Some(selected) = selected else { + return Ok(None); + }; + let source = selected + .into_path() + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_PATH_INVALID: {error}"))?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || stage_document_at(&database, &source)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_JOIN_FAILED: {error}"))? + .map(Some) +} + +pub async fn commit_web_novel_document_import( + app: AppHandle, + input: CommitImportInput, +) -> Result { + require_active(&app)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || commit_import_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_JOIN_FAILED: {error}"))? +} + +fn ensure_import_schema(connection: &rusqlite::Connection) -> Result<(), String> { + connection + .execute_batch( + "CREATE TABLE IF NOT EXISTS web_novel_document_imports( + import_id TEXT PRIMARY KEY NOT NULL, + source_filename TEXT NOT NULL, + source_format TEXT NOT NULL, + source_sha256 TEXT NOT NULL, + source_bytes INTEGER NOT NULL, + detected_family TEXT NOT NULL, + detected_title TEXT NOT NULL, + detected_pen_name TEXT NOT NULL, + detected_genre TEXT NOT NULL, + preface TEXT NOT NULL, + sections_json TEXT NOT NULL, + state TEXT NOT NULL, + target_work_id TEXT, + target_volume_id TEXT, + created_at_unix_ms INTEGER NOT NULL, + committed_at_unix_ms INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_web_novel_import_hash + ON web_novel_document_imports(source_sha256, state);", + ) + .map_err(db_write) +} + +pub(crate) fn stage_document_at( + database: &Path, + source: &Path, +) -> Result { + let parsed = parse_document(source)?; + let mut connection = super::web_novel_workspace::open_database(database)?; + ensure_import_schema(&connection)?; + let import_id = format!("WN-IMPORT-{}", Uuid::new_v4()); + let sections_json = serde_json::to_string(&parsed.sections) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_SERIALIZE_FAILED: {error}"))?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + tx.execute( + "INSERT INTO web_novel_document_imports( + import_id, source_filename, source_format, source_sha256, source_bytes, + detected_family, detected_title, detected_pen_name, detected_genre, + preface, sections_json, state, created_at_unix_ms + ) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,'STAGED',?12)", + params![ + import_id, + parsed.source_filename, + parsed.source_format, + parsed.source_sha256, + parsed.source_bytes as i64, + parsed.detected_family, + parsed.detected_title, + parsed.detected_pen_name, + parsed.detected_genre, + parsed.preface, + sections_json, + now_ms(), + ], + ) + .map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(preview_from(&import_id, &parsed)) +} + +fn preview_from(import_id: &str, parsed: &ParsedDocument) -> WebNovelImportPreview { + let mut selected: Vec<&ImportSection> = parsed.sections.iter().take(8).collect(); + if parsed.sections.len() > 10 { + selected.extend(parsed.sections.iter().skip(parsed.sections.len() - 2)); + } + WebNovelImportPreview { + import_id: import_id.to_owned(), + source_filename: parsed.source_filename.clone(), + source_format: parsed.source_format.clone(), + source_sha256: parsed.source_sha256.clone(), + source_bytes: parsed.source_bytes, + detected_family: parsed.detected_family.clone(), + detected_title: parsed.detected_title.clone(), + detected_pen_name: parsed.detected_pen_name.clone(), + detected_genre: parsed.detected_genre.clone(), + section_count: parsed.sections.len(), + total_word_count: parsed.sections.iter().map(|item| item.word_count).sum(), + preface_word_count: count_words(&parsed.preface), + sections: selected + .into_iter() + .map(|section| ImportSectionPreview { + ordinal: section.ordinal, + title: section.title.clone(), + word_count: section.word_count, + scene_count: section.scene_count, + preview: section.body.chars().take(120).collect(), + }) + .collect(), + } +} + +pub(crate) fn commit_import_at( + database: &Path, + input: CommitImportInput, +) -> Result { + if !input.import_id.starts_with("WN-IMPORT-") { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_ID_INVALID".into()); + } + let mut connection = super::web_novel_workspace::open_database(database)?; + ensure_import_schema(&connection)?; + let staged = connection + .query_row( + "SELECT source_filename, source_sha256, detected_family, preface, sections_json, state + FROM web_novel_document_imports WHERE import_id=?1", + [&input.import_id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, String>(5)?, + )) + }, + ) + .optional() + .map_err(db_read)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_IMPORT_NOT_FOUND".to_string())?; + if staged.5 != "STAGED" { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_ALREADY_COMMITTED".into()); + } + let sections: Vec = serde_json::from_str(&staged.4) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_STAGING_INVALID: {error}"))?; + if sections.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_NO_SECTIONS".into()); + } + for section in §ions { + if section.body.len() > MAX_SECTION_BYTES { + return Err(format!( + "HOLOLAKE_WEBNOVEL_IMPORT_SECTION_TOO_LARGE: {}", + section.title + )); + } + } + + let create_new = input.target_mode == "CREATE_NEW"; + if !create_new && input.target_mode != "APPEND_EXISTING" { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_TARGET_MODE_INVALID".into()); + } + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let now = now_ms(); + let work_id = if create_new { + let work_id = format!("WN-WORK-{}", Uuid::new_v4()); + let title = required_text(&input.title, "未命名导入作品"); + let pen_name = required_text(&input.pen_name, "待确认"); + let genre = required_text(&input.genre, "网文导入"); + let synopsis = truncate_utf8(&staged.3, MAX_SYNOPSIS_BYTES); + let work_kind = match staged.2.as_str() { + "SCRIPT" => "SHORT_DRAMA", + _ => "LONG_NOVEL", + }; + tx.execute( + "INSERT INTO web_novel_works( + work_id,title,pen_name,genre,work_kind,synopsis,contract_status,copyright_status, + workflow_status,target_words,revision,created_at_unix_ms,updated_at_unix_ms,archived + ) VALUES(?1,?2,?3,?4,?5,?6,'UNREGISTERED','AUTHOR_OWNED','DRAFT',0,1,?7,?7,0)", + params![work_id, title, pen_name, genre, work_kind, synopsis, now], + ) + .map_err(db_write)?; + work_id + } else { + let work_id = input + .target_work_id + .clone() + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_IMPORT_TARGET_REQUIRED".to_string())?; + let exists: bool = tx + .query_row( + "SELECT EXISTS(SELECT 1 FROM web_novel_works WHERE work_id=?1 AND archived=0)", + [&work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + if !exists { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_TARGET_NOT_FOUND".into()); + } + work_id + }; + let volume_id = format!("WN-VOL-{}", Uuid::new_v4()); + let volume_position: i64 = tx.query_row( + "SELECT COALESCE(MAX(position), -1)+1 FROM web_novel_volumes WHERE work_id=?1 AND archived=0", + [&work_id], |row| row.get(0) + ).map_err(db_read)?; + let volume_title = match staged.2.as_str() { + "OUTLINE" => "导入·拆分细纲", + "SCRIPT" => "导入·分集剧本", + _ => "导入·小说正文", + }; + tx.execute( + "INSERT INTO web_novel_volumes(volume_id,work_id,title,position,revision,created_at_unix_ms,updated_at_unix_ms,archived) + VALUES(?1,?2,?3,?4,1,?5,?5,0)", + params![volume_id, work_id, volume_title, volume_position, now], + ).map_err(db_write)?; + let work_kind: String = tx + .query_row( + "SELECT work_kind FROM web_novel_works WHERE work_id=?1 AND archived=0", + [&work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + for (position, section) in sections.iter().enumerate() { + let chapter_id = format!("WN-CH-{}", Uuid::new_v4()); + let formatted_content = + super::web_novel_author::format_content(§ion.body, &work_kind, &staged.2); + let formatted_word_count = count_words(&formatted_content); + tx.execute( + "INSERT INTO web_novel_chapters( + chapter_id,volume_id,work_id,title,position,synopsis,content,workflow_status, + scheduled_at_unix_ms,word_count,revision,created_at_unix_ms,updated_at_unix_ms,archived + ) VALUES(?1,?2,?3,?4,?5,'',?6,'DRAFT',NULL,?7,2,?8,?8,0)", + params![chapter_id, volume_id, work_id, section.title, position as i64, formatted_content, formatted_word_count, now], + ).map_err(db_write)?; + tx.execute( + "INSERT INTO web_novel_chapter_versions( + version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms + ) VALUES(?1,?2,?3,1,?4,'',?5,'DRAFT',?6,'DOCUMENT_IMPORT_SOURCE',?7)", + params![format!("WN-VER-{}", Uuid::new_v4()), chapter_id, work_id, section.title, section.body, section.word_count, now], + ).map_err(db_write)?; + tx.execute( + "INSERT INTO web_novel_chapter_versions( + version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms + ) VALUES(?1,?2,?3,2,?4,'',?5,'DRAFT',?6,'AUTO_FORMAT_ON_IMPORT',?7)", + params![format!("WN-VER-{}", Uuid::new_v4()), chapter_id, work_id, section.title, formatted_content, formatted_word_count, now], + ).map_err(db_write)?; + } + tx.execute( + "UPDATE web_novel_works SET revision=revision+1, updated_at_unix_ms=?1 WHERE work_id=?2", + params![now, work_id], + ) + .map_err(db_write)?; + tx.execute( + "UPDATE web_novel_document_imports SET state='COMMITTED',target_work_id=?1,target_volume_id=?2,committed_at_unix_ms=?3 + WHERE import_id=?4 AND state='STAGED'", + params![work_id, volume_id, now, input.import_id], + ).map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(WebNovelImportReceipt { + state: "COMMITTED".into(), + import_id: input.import_id, + work_id, + volume_id, + source_filename: staged.0, + source_sha256: staged.1, + detected_family: staged.2, + chapter_count: sections.len(), + word_count: sections.iter().map(|section| section.word_count).sum(), + first_chapter_title: sections + .first() + .map(|item| item.title.clone()) + .unwrap_or_default(), + last_chapter_title: sections + .last() + .map(|item| item.title.clone()) + .unwrap_or_default(), + }) +} + +fn parse_document(source: &Path) -> Result { + let metadata = fs::metadata(source) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_READ_FAILED: {error}"))?; + if metadata.len() == 0 || metadata.len() > MAX_SOURCE_BYTES { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_FILE_SIZE_INVALID".into()); + } + let bytes = fs::read(source) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_READ_FAILED: {error}"))?; + let source_format = source + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + let raw = match source_format.as_str() { + "docx" => read_docx(&bytes)?, + "txt" | "md" => decode_plain_text(&bytes), + _ => return Err("HOLOLAKE_WEBNOVEL_IMPORT_FORMAT_UNSUPPORTED".into()), + }; + let text = normalize_text(&raw); + if text.trim().is_empty() { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_DOCUMENT_EMPTY".into()); + } + let source_filename = source + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or("未命名文档") + .to_owned(); + let family = detect_family(&source_filename, &text); + let (preface, sections) = split_sections(&text, &family)?; + let hash = digest(&SHA256, &bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + Ok(ParsedDocument { + source_filename: source_filename.clone(), + source_format: source_format.to_uppercase(), + source_sha256: hash, + source_bytes: bytes.len() as u64, + detected_family: family.clone(), + detected_title: detect_title(&source_filename, &text), + detected_pen_name: detect_author(&text), + detected_genre: match family.as_str() { + "SCRIPT" => "短剧剧本", + "OUTLINE" => "拆分细纲", + _ => "网文小说", + } + .into(), + preface, + sections, + }) +} + +fn read_docx(bytes: &[u8]) -> Result { + let cursor = Cursor::new(bytes); + let mut archive = ZipArchive::new(cursor) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_INVALID: {error}"))?; + let mut document = archive + .by_name("word/document.xml") + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_BODY_MISSING: {error}"))?; + let mut xml = String::new(); + document + .read_to_string(&mut xml) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_READ_FAILED: {error}"))?; + let mut reader = Reader::from_str(&xml); + reader.trim_text(false); + let mut output = String::new(); + let mut in_text = false; + loop { + match reader.read_event() { + Ok(Event::Start(event)) => { + let name = event.name(); + if local_name(name.as_ref()) == b"t" { + in_text = true; + } + if local_name(name.as_ref()) == b"tab" { + output.push('\t'); + } + if local_name(name.as_ref()) == b"br" { + output.push('\n'); + } + } + Ok(Event::Empty(event)) => { + let name = event.name(); + if local_name(name.as_ref()) == b"tab" { + output.push('\t'); + } + if local_name(name.as_ref()) == b"br" { + output.push('\n'); + } + } + Ok(Event::Text(event)) if in_text => { + output.push_str(&event.unescape().map_err(|error| { + format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_XML_INVALID: {error}") + })?); + } + Ok(Event::End(event)) => { + let name = event.name(); + if local_name(name.as_ref()) == b"t" { + in_text = false; + } + if local_name(name.as_ref()) == b"p" { + output.push('\n'); + } + } + Ok(Event::Eof) => break, + Err(error) => { + return Err(format!( + "HOLOLAKE_WEBNOVEL_IMPORT_DOCX_XML_INVALID: {error}" + )) + } + _ => {} + } + } + Ok(output) +} + +fn local_name(name: &[u8]) -> &[u8] { + name.rsplit(|value| *value == b':').next().unwrap_or(name) +} + +fn decode_plain_text(bytes: &[u8]) -> String { + if let Ok(value) = std::str::from_utf8(bytes) { + return value.trim_start_matches('\u{feff}').to_owned(); + } + let (value, _, _) = GBK.decode(bytes); + value.into_owned() +} + +fn normalize_text(raw: &str) -> String { + let raw = raw + .replace("\r\n", "\n") + .replace('\r', "\n") + .replace('\u{00a0}', " "); + let mut output = String::with_capacity(raw.len()); + let mut blank = 0; + for line in raw.lines() { + let cleaned = line.trim_end_matches([' ', '\t', '\u{3000}']); + if cleaned.trim().is_empty() { + blank += 1; + if blank <= 2 { + output.push('\n'); + } + } else { + blank = 0; + output.push_str(cleaned); + output.push('\n'); + } + } + output.trim().to_owned() +} + +fn detect_family(filename: &str, text: &str) -> String { + let sample: String = text.chars().take(80_000).collect(); + let episode = heading_regex("集").find_iter(&sample).count(); + if episode >= 2 || (sample.contains("剧本") && scene_regex().is_match(&sample)) { + "SCRIPT".into() + } else if filename.contains("细纲") || filename.contains("大纲") || sample.contains("章节细纲") + { + "OUTLINE".into() + } else { + "NOVEL".into() + } +} + +fn split_sections(text: &str, family: &str) -> Result<(String, Vec), String> { + let heading = if family == "SCRIPT" { + heading_regex("集") + } else { + heading_regex("章") + }; + let mut preface = Vec::new(); + let mut sections: Vec = Vec::new(); + let mut current_title: Option = None; + let mut current_body: Vec = Vec::new(); + for line in text.lines() { + let trimmed = line.trim(); + if heading.is_match(trimmed) { + if let Some(title) = current_title.take() { + push_section(&mut sections, title, ¤t_body); + current_body.clear(); + } + current_title = Some(trimmed.trim_end_matches([':', ':']).trim().to_owned()); + } else if current_title.is_some() { + current_body.push(line.to_owned()); + } else { + preface.push(line.to_owned()); + } + } + if let Some(title) = current_title { + push_section(&mut sections, title, ¤t_body); + } + if sections.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_IMPORT_NO_CHAPTER_OR_EPISODE_HEADINGS".into()); + } + Ok((preface.join("\n").trim().to_owned(), sections)) +} + +fn push_section(sections: &mut Vec, title: String, body_lines: &[String]) { + let body = body_lines.join("\n").trim().to_owned(); + let scene_count = scene_regex().find_iter(&body).count() as i64; + sections.push(ImportSection { + ordinal: sections.len() + 1, + title, + word_count: count_words(&body), + scene_count, + body, + }); +} + +fn heading_regex(unit: &str) -> Regex { + Regex::new(&format!( + r"(?m)^\s*第[0-90-9一二三四五六七八九十百千万零〇两]{{1,12}}{}(?:\s*[::]?\s*.*)?$", + unit + )) + .expect("valid heading regex") +} + +fn scene_regex() -> Regex { + Regex::new(r"(?m)^\s*[0-90-9]{1,3}-[0-90-9]{1,3}\s+").expect("valid scene regex") +} + +fn detect_title(filename: &str, text: &str) -> String { + let bracket = Regex::new(r"《([^》]{1,120})》").expect("valid title regex"); + if let Some(value) = bracket + .captures( + text.lines() + .take(20) + .collect::>() + .join("\n") + .as_str(), + ) + .and_then(|capture| capture.get(1)) + { + return value.as_str().trim().to_owned(); + } + if let Some(value) = bracket + .captures(filename) + .and_then(|capture| capture.get(1)) + { + return value.as_str().trim().to_owned(); + } + filename + .trim_end_matches(|value: char| value == '.' || value.is_ascii_alphabetic()) + .split(['【', '[']) + .next() + .unwrap_or("未命名作品") + .trim_matches(['《', '》', ' ', ' ']) + .to_owned() +} + +fn detect_author(text: &str) -> String { + let regex = Regex::new(r"作者\s*[::]\s*([^\s\r\n|]+)").expect("valid author regex"); + let sample: String = text.chars().take(10_000).collect(); + regex + .captures(&sample) + .and_then(|capture| capture.get(1)) + .map(|value| value.as_str().trim().to_owned()) + .unwrap_or_else(|| "待确认".into()) +} + +fn count_words(content: &str) -> i64 { + content + .chars() + .filter(|value| !value.is_whitespace()) + .count() as i64 +} + +fn required_text(value: &str, fallback: &str) -> String { + let value = value.trim(); + if value.is_empty() { + fallback.into() + } else { + value.chars().take(120).collect() + } +} + +fn truncate_utf8(value: &str, max_bytes: usize) -> String { + if value.len() <= max_bytes { + return value.to_owned(); + } + let mut boundary = max_bytes; + while !value.is_char_boundary(boundary) { + boundary -= 1; + } + value[..boundary].to_owned() +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +fn db_read(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_IMPORT_DATABASE_READ_FAILED: {error}") +} + +fn db_write(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_IMPORT_DATABASE_WRITE_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn splits_novel_outline_and_script_headings() { + let novel = "作者:冰朔\n第001章 起点\n正文一\n第002章 转折\n正文二"; + let (_, chapters) = split_sections(novel, "NOVEL").unwrap(); + assert_eq!(chapters.len(), 2); + assert_eq!(chapters[1].title, "第002章 转折"); + let script = "第1集:\n1-1 日 外 广场\n△人物入场\n第2集\n2-1 夜 内 房间"; + let (_, episodes) = split_sections(script, "SCRIPT").unwrap(); + assert_eq!(episodes.len(), 2); + assert_eq!(episodes[0].scene_count, 1); + } + + #[test] + fn import_auto_formats_and_keeps_the_source_version() { + let temp = tempdir().unwrap(); + let source = temp.path().join("自动排版小说.txt"); + fs::write(&source, "第1章 开始\n第一段\n第二段\n第2章 后续\n第三段").unwrap(); + let database = temp.path().join("auto-format.sqlite3"); + let preview = stage_document_at(&database, &source).unwrap(); + let receipt = commit_import_at( + &database, + CommitImportInput { + import_id: preview.import_id, + target_mode: "CREATE_NEW".into(), + target_work_id: None, + title: "自动排版小说".into(), + pen_name: "测试作者".into(), + genre: "测试".into(), + }, + ) + .unwrap(); + let connection = super::super::web_novel_workspace::open_database(&database).unwrap(); + let (content, revision): (String, i64) = connection.query_row( + "SELECT content,revision FROM web_novel_chapters WHERE work_id=?1 ORDER BY position LIMIT 1", + [&receipt.work_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ).unwrap(); + assert_eq!(revision, 2); + assert_eq!(content, "  第一段\n\n  第二段"); + let reasons: Vec = connection.prepare( + "SELECT save_reason FROM web_novel_chapter_versions + WHERE chapter_id=(SELECT chapter_id FROM web_novel_chapters WHERE work_id=?1 ORDER BY position LIMIT 1) + ORDER BY revision" + ).unwrap().query_map([&receipt.work_id], |row| row.get(0)).unwrap() + .collect::, _>>().unwrap(); + assert_eq!(reasons[0], "DOCUMENT_IMPORT_SOURCE"); + assert_eq!(reasons[1], "AUTO_FORMAT_ON_IMPORT"); + } + + #[test] + #[ignore = "explicit real Desktop fixture acceptance"] + fn imports_real_desktop_documents_and_reads_them_back() { + let fixtures = [ + ("HOLOLAKE_REAL_NOVEL", 504usize), + ("HOLOLAKE_REAL_OUTLINE", 50usize), + ("HOLOLAKE_REAL_SCRIPT", 75usize), + ]; + let temp = tempdir().unwrap(); + let database = temp.path().join("real-import.sqlite3"); + for (variable, expected) in fixtures { + let path = std::env::var(variable).expect("real fixture path is required"); + let preview = stage_document_at(&database, Path::new(&path)).unwrap(); + assert_eq!(preview.section_count, expected, "{variable}"); + let receipt = commit_import_at( + &database, + CommitImportInput { + import_id: preview.import_id, + target_mode: "CREATE_NEW".into(), + target_work_id: None, + title: preview.detected_title, + pen_name: preview.detected_pen_name, + genre: preview.detected_genre, + }, + ) + .unwrap(); + let connection = super::super::web_novel_workspace::open_database(&database).unwrap(); + let (count, non_empty, version_count): (i64, i64, i64) = connection + .query_row( + "SELECT COUNT(*), SUM(CASE WHEN LENGTH(content)>0 THEN 1 ELSE 0 END), + (SELECT COUNT(*) FROM web_novel_chapter_versions WHERE work_id=?1) + FROM web_novel_chapters WHERE work_id=?1 AND archived=0", + [&receipt.work_id], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .unwrap(); + assert_eq!(count as usize, expected); + assert_eq!(non_empty as usize, expected); + assert_eq!(version_count as usize, expected * 2); + println!("{}", serde_json::to_string(&receipt).unwrap()); + } + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/web_novel_modules.rs b/product-source/hololake-native-desktop/src-tauri/src/web_novel_modules.rs new file mode 100644 index 000000000..c7780b97c --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/web_novel_modules.rs @@ -0,0 +1,2004 @@ +//! 网文作者官方模块数据适配器。 +//! +//! 生产安装、签名验证、挂载、自检和回执只由 `module_package_runtime` 执行。 +//! 本文件保留 0.4.1 安装器作为迁移回归测试夹具;它没有编号 IPC 路由,不能成为 +//! 第二套生产生命周期权威。 + +#![allow(dead_code)] + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::{Cursor, Write}; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use tauri_plugin_dialog::DialogExt; +use uuid::Uuid; +use zip::write::FileOptions; +use zip::{CompressionMethod, ZipWriter}; + +const BASE: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; +const ADAPTER: &str = "web-novel-workbench-v1"; + +fn require_module_active(app: &AppHandle, module_number: &str) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, module_number, ADAPTER) +} + +const OUTLINE: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001"; +const GRID: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001"; +const STORYWORLD: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001"; +const DELIVERY: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"; +const VERSION: &str = "1.0.0"; + +struct Package { + module_id: &'static str, + name: &'static str, + description: &'static str, + sha256: &'static str, + bytes: &'static [u8], +} + +fn packages() -> [Package; 4] { + [ + Package { + module_id: OUTLINE, + name: "作品结构与大纲追踪", + description: "场景、情节拍、目标冲突结果、钩子与伏笔追踪。", + sha256: "6d31d3f530862000bd84a8f5dc73bcb9cf0d5738219f7a287562d4f4bcfa5318", + bytes: include_bytes!( + "../../fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-OUTLINE-001.json" + ), + }, + Package { + module_id: GRID, + name: "多维情节表与情节板", + description: "由同一场景对象生成可编辑表格、自定义字段与分组情节板。", + sha256: "2e34e0337c51dbbdd1efdd807be71e69a0cb2a1745624e1f51b452b7b54f1553", + bytes: include_bytes!( + "../../fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-GRID-002.json" + ), + }, + Package { + module_id: STORYWORLD, + name: "时间线与故事资料库", + description: "故事时间、场景与人物地点物品的真实关系投影。", + sha256: "d486719500ddfc08054d9c9bb6d3683afe5547a2b24c9b04d0f899ac66094d2f", + bytes: include_bytes!( + "../../fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-STORYWORLD-003.json" + ), + }, + Package { + module_id: DELIVERY, + name: "高级版本与交付", + description: "章节版本读回与恢复,以及 TXT、DOCX、EPUB、JSON 真实导出。", + sha256: "4af1d4610826edd29759bd0dc12b4b0c04fd2ebebb4ebc362d47d762810989e8", + bytes: include_bytes!( + "../../fixtures/web-novel-legacy-0.4.1/HL-MOD-WEBNOVEL-DELIVERY-004.json" + ), + }, + ] +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorModuleDescriptor { + pub module_id: String, + pub name: String, + pub description: String, + pub version: String, + pub package_sha256: String, + pub install_state: String, + pub self_test_state: String, + pub installed_at_unix_ms: Option, + pub mounted_at_unix_ms: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorModuleReceipt { + pub receipt_id: String, + pub module_id: String, + pub action: String, + pub state: String, + pub detail: String, + pub receipt_hash: String, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorModuleMarketplaceSnapshot { + pub state: &'static str, + pub modules: Vec, + pub recent_receipts: Vec, + pub storage: &'static str, + pub active_action_controller: &'static str, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ModuleActionInput { + pub module_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ReadAuthorModuleDataInput { + pub work_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthorScene { + pub scene_id: String, + pub work_id: String, + pub chapter_id: String, + pub title: String, + pub position: i64, + pub synopsis: String, + pub status: String, + pub goal: String, + pub conflict: String, + pub outcome: String, + pub hook: String, + pub emotional_point: String, + pub target_words: i64, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthorBeat { + pub beat_id: String, + pub scene_id: String, + pub position: i64, + pub title: String, + pub note: String, + pub status: String, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StoryFieldDefinition { + pub field_id: String, + pub work_id: String, + pub label: String, + pub field_type: String, + pub options: Vec, + pub position: i64, + pub revision: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StoryFieldValue { + pub field_id: String, + pub scene_id: String, + pub value: String, + pub revision: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StoryTimelineEvent { + pub event_id: String, + pub work_id: String, + pub scene_id: Option, + pub entity_id: Option, + pub title: String, + pub calendar_kind: String, + pub story_day: Option, + pub date_text: String, + pub time_text: String, + pub duration_minutes: i64, + pub description: String, + pub revision: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SceneEntityLink { + pub link_id: String, + pub work_id: String, + pub scene_id: String, + pub entity_id: String, + pub role: String, + pub note: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ChapterVersionSummary { + pub version_id: String, + pub chapter_id: String, + pub chapter_title: String, + pub revision: i64, + pub word_count: i64, + pub save_reason: String, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorModuleData { + pub work_id: String, + pub scenes: Vec, + pub beats: Vec, + pub field_definitions: Vec, + pub field_values: Vec, + pub timeline_events: Vec, + pub scene_entity_links: Vec, + pub chapter_versions: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertAuthorSceneInput { + pub work_id: String, + pub scene_id: Option, + pub chapter_id: String, + pub title: String, + pub position: Option, + pub synopsis: String, + pub status: String, + pub goal: String, + pub conflict: String, + pub outcome: String, + pub hook: String, + pub emotional_point: String, + pub target_words: i64, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertAuthorBeatInput { + pub scene_id: String, + pub beat_id: Option, + pub title: String, + pub note: String, + pub status: String, + pub position: Option, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertStoryFieldDefinitionInput { + pub work_id: String, + pub field_id: Option, + pub label: String, + pub field_type: String, + pub options: Vec, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertStoryFieldValueInput { + pub field_id: String, + pub scene_id: String, + pub value: String, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertTimelineEventInput { + pub work_id: String, + pub event_id: Option, + pub scene_id: Option, + pub entity_id: Option, + pub title: String, + pub calendar_kind: String, + pub story_day: Option, + pub date_text: String, + pub time_text: String, + pub duration_minutes: i64, + pub description: String, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct LinkSceneEntityInput { + pub work_id: String, + pub scene_id: String, + pub entity_id: String, + pub role: String, + pub note: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RestoreChapterVersionInput { + pub version_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExportAuthorDeliveryInput { + pub work_id: String, + pub format: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorDeliveryReceipt { + pub format: String, + pub path: String, + pub bytes: u64, + pub chapter_count: usize, + pub package_sha256: String, +} + +pub async fn get_web_novel_author_module_marketplace( + app: AppHandle, +) -> Result { + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || marketplace_at(&database)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn install_web_novel_author_module( + app: AppHandle, + input: ModuleActionInput, +) -> Result { + let database = super::web_novel_workspace::workspace_database(&app)?; + let cache = crate::authenticated_storage::account_storage_root(&app, "web-novel-modules-v1")?; + tauri::async_runtime::spawn_blocking(move || install_at(&database, &cache, &input.module_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn mount_web_novel_author_module( + app: AppHandle, + input: ModuleActionInput, +) -> Result { + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || set_mount_at(&database, &input.module_id, true)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn unmount_web_novel_author_module( + app: AppHandle, + input: ModuleActionInput, +) -> Result { + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || set_mount_at(&database, &input.module_id, false)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn uninstall_web_novel_author_module( + app: AppHandle, + input: ModuleActionInput, +) -> Result { + let database = super::web_novel_workspace::workspace_database(&app)?; + let cache = crate::authenticated_storage::account_storage_root(&app, "web-novel-modules-v1")?; + tauri::async_runtime::spawn_blocking(move || uninstall_at(&database, &cache, &input.module_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn get_web_novel_author_module_data( + app: AppHandle, + input: ReadAuthorModuleDataInput, +) -> Result { + require_module_active(&app, BASE)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || module_data_at(&database, &input.work_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_author_scene( + app: AppHandle, + input: UpsertAuthorSceneInput, +) -> Result { + require_module_active(&app, OUTLINE)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_scene_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_author_beat( + app: AppHandle, + input: UpsertAuthorBeatInput, +) -> Result { + require_module_active(&app, OUTLINE)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_beat_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_story_field_definition( + app: AppHandle, + input: UpsertStoryFieldDefinitionInput, +) -> Result { + require_module_active(&app, GRID)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_field_definition_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_story_field_value( + app: AppHandle, + input: UpsertStoryFieldValueInput, +) -> Result { + require_module_active(&app, GRID)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_field_value_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_timeline_event( + app: AppHandle, + input: UpsertTimelineEventInput, +) -> Result { + require_module_active(&app, STORYWORLD)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_timeline_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn link_web_novel_scene_entity( + app: AppHandle, + input: LinkSceneEntityInput, +) -> Result { + require_module_active(&app, STORYWORLD)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || link_scene_entity_at(&database, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn restore_web_novel_chapter_version( + app: AppHandle, + input: RestoreChapterVersionInput, +) -> Result { + require_module_active(&app, DELIVERY)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || restore_version_at(&database, &input.version_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? +} + +pub async fn export_web_novel_author_delivery( + app: AppHandle, + input: ExportAuthorDeliveryInput, +) -> Result, String> { + require_module_active(&app, DELIVERY)?; + let format = normalize_delivery_format(&input.format)?; + let database = super::web_novel_workspace::workspace_database(&app)?; + let title = read_work_title(&database, &input.work_id)?; + let extension = format.to_ascii_lowercase(); + let selected = app + .dialog() + .file() + .set_title("导出作者交付包") + .set_file_name(format!("{}.{}", safe_filename(&title), extension)) + .add_filter(&format, &[&extension]) + .blocking_save_file(); + let Some(selected) = selected else { + return Ok(None); + }; + let path = selected + .into_path() + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_DELIVERY_PATH_INVALID: {error}"))?; + let work_id = input.work_id; + tauri::async_runtime::spawn_blocking(move || { + export_delivery_at(&database, &work_id, &format, &path) + }) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_MODULE_JOIN_FAILED: {error}"))? + .map(Some) +} + +fn ensure_schema(connection: &Connection) -> Result<(), String> { + connection.execute_batch( + "CREATE TABLE IF NOT EXISTS web_novel_module_installations( + module_id TEXT PRIMARY KEY NOT NULL, version TEXT NOT NULL, package_sha256 TEXT NOT NULL, + install_state TEXT NOT NULL, self_test_state TEXT NOT NULL, + installed_at_unix_ms INTEGER NOT NULL, mounted_at_unix_ms INTEGER, updated_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_module_receipts( + receipt_id TEXT PRIMARY KEY NOT NULL, module_id TEXT NOT NULL, action TEXT NOT NULL, + state TEXT NOT NULL, detail TEXT NOT NULL, previous_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE, created_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_module_selftest(probe_id TEXT PRIMARY KEY NOT NULL, module_id TEXT NOT NULL, created_at_unix_ms INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS web_novel_scenes( + scene_id TEXT PRIMARY KEY NOT NULL, work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + title TEXT NOT NULL, position INTEGER NOT NULL, synopsis TEXT NOT NULL, status TEXT NOT NULL, + goal TEXT NOT NULL, conflict TEXT NOT NULL, outcome TEXT NOT NULL, hook TEXT NOT NULL, + emotional_point TEXT NOT NULL, target_words INTEGER NOT NULL, revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), UNIQUE(chapter_id, position) + ); + CREATE TABLE IF NOT EXISTS web_novel_beats( + beat_id TEXT PRIMARY KEY NOT NULL, scene_id TEXT NOT NULL REFERENCES web_novel_scenes(scene_id) ON DELETE CASCADE, + position INTEGER NOT NULL, title TEXT NOT NULL, note TEXT NOT NULL, status TEXT NOT NULL, + revision INTEGER NOT NULL, created_at_unix_ms INTEGER NOT NULL, updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), UNIQUE(scene_id, position) + ); + CREATE TABLE IF NOT EXISTS web_novel_story_fields( + field_id TEXT PRIMARY KEY NOT NULL, work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + label TEXT NOT NULL, field_type TEXT NOT NULL, options_json TEXT NOT NULL, position INTEGER NOT NULL, + revision INTEGER NOT NULL, created_at_unix_ms INTEGER NOT NULL, updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), UNIQUE(work_id, label) + ); + CREATE TABLE IF NOT EXISTS web_novel_story_field_values( + field_id TEXT NOT NULL REFERENCES web_novel_story_fields(field_id) ON DELETE CASCADE, + scene_id TEXT NOT NULL REFERENCES web_novel_scenes(scene_id) ON DELETE CASCADE, + value TEXT NOT NULL, revision INTEGER NOT NULL, updated_at_unix_ms INTEGER NOT NULL, + PRIMARY KEY(field_id, scene_id) + ); + CREATE TABLE IF NOT EXISTS web_novel_timeline_events( + event_id TEXT PRIMARY KEY NOT NULL, work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + scene_id TEXT REFERENCES web_novel_scenes(scene_id) ON DELETE SET NULL, + entity_id TEXT REFERENCES web_novel_entities(entity_id) ON DELETE SET NULL, + title TEXT NOT NULL, calendar_kind TEXT NOT NULL, story_day INTEGER, date_text TEXT NOT NULL, + time_text TEXT NOT NULL, duration_minutes INTEGER NOT NULL, description TEXT NOT NULL, + revision INTEGER NOT NULL, created_at_unix_ms INTEGER NOT NULL, updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS web_novel_scene_entities( + link_id TEXT PRIMARY KEY NOT NULL, work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + scene_id TEXT NOT NULL REFERENCES web_novel_scenes(scene_id) ON DELETE CASCADE, + entity_id TEXT NOT NULL REFERENCES web_novel_entities(entity_id) ON DELETE CASCADE, + role TEXT NOT NULL, note TEXT NOT NULL, created_at_unix_ms INTEGER NOT NULL, + UNIQUE(scene_id, entity_id, role) + ); + CREATE INDEX IF NOT EXISTS idx_web_novel_scenes_work ON web_novel_scenes(work_id, chapter_id, position); + CREATE INDEX IF NOT EXISTS idx_web_novel_beats_scene ON web_novel_beats(scene_id, position); + CREATE INDEX IF NOT EXISTS idx_web_novel_timeline_work ON web_novel_timeline_events(work_id, story_day, date_text, time_text);" + ).map_err(db_write) +} + +fn marketplace_at(path: &Path) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let modules = packages().into_iter().map(|package| { + let installed = connection.query_row( + "SELECT install_state,self_test_state,installed_at_unix_ms,mounted_at_unix_ms FROM web_novel_module_installations WHERE module_id=?1", + [package.module_id], |row| Ok((row.get::<_,String>(0)?,row.get::<_,String>(1)?,row.get::<_,i64>(2)?,row.get::<_,Option>(3)?)) + ).optional().map_err(db_read)?; + Ok(AuthorModuleDescriptor { + module_id: package.module_id.into(), name: package.name.into(), description: package.description.into(), version: VERSION.into(), package_sha256: package.sha256.into(), + install_state: installed.as_ref().map(|value| value.0.clone()).unwrap_or_else(|| "AVAILABLE".into()), + self_test_state: installed.as_ref().map(|value| value.1.clone()).unwrap_or_else(|| "NOT_RUN".into()), + installed_at_unix_ms: installed.as_ref().map(|value| value.2), mounted_at_unix_ms: installed.and_then(|value| value.3), + }) + }).collect::, String>>()?; + let mut statement = connection.prepare("SELECT receipt_id,module_id,action,state,detail,receipt_hash,created_at_unix_ms FROM web_novel_module_receipts ORDER BY created_at_unix_ms DESC,rowid DESC LIMIT 30").map_err(db_read)?; + let recent_receipts = statement + .query_map([], |row| { + Ok(AuthorModuleReceipt { + receipt_id: row.get(0)?, + module_id: row.get(1)?, + action: row.get(2)?, + state: row.get(3)?, + detail: row.get(4)?, + receipt_hash: row.get(5)?, + created_at_unix_ms: row.get(6)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + Ok(AuthorModuleMarketplaceSnapshot { + state: "READY", + modules, + recent_receipts, + storage: "CURRENT_ACCOUNT_SQLITE_AND_PRIVATE_MODULE_CACHE", + active_action_controller: "ONE_DESKTOP_ACTION_AT_A_TIME", + }) +} + +fn package_by_id(module_id: &str) -> Result { + packages() + .into_iter() + .find(|item| item.module_id == module_id) + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_MODULE_NOT_OFFICIAL".into()) +} + +fn install_at( + path: &Path, + cache: &Path, + module_id: &str, +) -> Result { + let package = package_by_id(module_id)?; + let actual = sha256(package.bytes); + if actual != package.sha256 { + return Err("HOLOLAKE_WEBNOVEL_MODULE_PACKAGE_HASH_MISMATCH".into()); + } + let target = module_cache_path(cache, module_id)?.join(VERSION); + fs::create_dir_all(&target).map_err(io_error)?; + set_private_permissions(&target)?; + let temp = target.join("module.json.pending"); + fs::write(&temp, package.bytes).map_err(io_error)?; + fs::rename(&temp, target.join("module.json")).map_err(io_error)?; + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let now = now_ms(); + tx.execute("INSERT INTO web_novel_module_installations(module_id,version,package_sha256,install_state,self_test_state,installed_at_unix_ms,mounted_at_unix_ms,updated_at_unix_ms) VALUES(?1,?2,?3,'INSTALLED','RUNNING',?4,NULL,?4) ON CONFLICT(module_id) DO UPDATE SET version=excluded.version,package_sha256=excluded.package_sha256,install_state='INSTALLED',self_test_state='RUNNING',updated_at_unix_ms=excluded.updated_at_unix_ms", params![module_id, VERSION, actual, now]).map_err(db_write)?; + run_self_test(&tx, module_id)?; + tx.execute("UPDATE web_novel_module_installations SET self_test_state='PASS',updated_at_unix_ms=?1 WHERE module_id=?2", params![now_ms(), module_id]).map_err(db_write)?; + append_receipt( + &tx, + module_id, + "INSTALL", + "PASS", + "不可变官方包散列核验、自检与账号缓存写入完成", + )?; + tx.commit().map_err(db_write)?; + marketplace_at(path) +} + +fn set_mount_at( + path: &Path, + module_id: &str, + mounted: bool, +) -> Result { + package_by_id(module_id)?; + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let current: Option<(String,String)> = tx.query_row("SELECT install_state,self_test_state FROM web_novel_module_installations WHERE module_id=?1", [module_id], |row| Ok((row.get(0)?,row.get(1)?))).optional().map_err(db_read)?; + let Some((_, self_test)) = current else { + return Err("HOLOLAKE_WEBNOVEL_MODULE_NOT_INSTALLED".into()); + }; + if self_test != "PASS" { + return Err("HOLOLAKE_WEBNOVEL_MODULE_SELF_TEST_REQUIRED".into()); + } + let state = if mounted { "MOUNTED" } else { "INSTALLED" }; + let mounted_at = if mounted { Some(now_ms()) } else { None }; + tx.execute("UPDATE web_novel_module_installations SET install_state=?1,mounted_at_unix_ms=?2,updated_at_unix_ms=?3 WHERE module_id=?4", params![state,mounted_at,now_ms(),module_id]).map_err(db_write)?; + append_receipt( + &tx, + module_id, + if mounted { "MOUNT" } else { "UNMOUNT" }, + "PASS", + if mounted { + "模块已挂载到当前账号网文频道" + } else { + "模块已停止投影,用户数据保持" + }, + )?; + tx.commit().map_err(db_write)?; + marketplace_at(path) +} + +fn uninstall_at( + path: &Path, + cache: &Path, + module_id: &str, +) -> Result { + package_by_id(module_id)?; + let target = module_cache_path(cache, module_id)?; + if target.exists() { + fs::remove_dir_all(&target).map_err(io_error)?; + } + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + tx.execute( + "DELETE FROM web_novel_module_installations WHERE module_id=?1", + [module_id], + ) + .map_err(db_write)?; + append_receipt( + &tx, + module_id, + "UNINSTALL", + "PASS", + "只清除可重新取得的模块程序缓存;作品、模块数据和历史回执保持", + )?; + tx.commit().map_err(db_write)?; + marketplace_at(path) +} + +fn run_self_test(connection: &Connection, module_id: &str) -> Result<(), String> { + let required_table = match module_id { + OUTLINE => "web_novel_scenes", + GRID => "web_novel_story_fields", + STORYWORLD => "web_novel_timeline_events", + DELIVERY => "web_novel_chapter_versions", + _ => return Err("HOLOLAKE_WEBNOVEL_MODULE_NOT_OFFICIAL".into()), + }; + let exists: bool = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1)", + [required_table], + |row| row.get(0), + ) + .map_err(db_read)?; + if !exists { + return Err(format!( + "HOLOLAKE_WEBNOVEL_MODULE_SELF_TEST_SCHEMA_MISSING:{required_table}" + )); + } + let probe = format!("WN-MOD-PROBE-{}", Uuid::new_v4()); + connection.execute("INSERT INTO web_novel_module_selftest(probe_id,module_id,created_at_unix_ms) VALUES(?1,?2,?3)", params![probe,module_id,now_ms()]).map_err(db_write)?; + let readback: String = connection + .query_row( + "SELECT module_id FROM web_novel_module_selftest WHERE probe_id=?1", + [&probe], + |row| row.get(0), + ) + .map_err(db_read)?; + connection + .execute( + "DELETE FROM web_novel_module_selftest WHERE probe_id=?1", + [&probe], + ) + .map_err(db_write)?; + if readback != module_id { + return Err("HOLOLAKE_WEBNOVEL_MODULE_SELF_TEST_READBACK_FAILED".into()); + } + Ok(()) +} + +fn append_receipt( + connection: &Connection, + module_id: &str, + action: &str, + state: &str, + detail: &str, +) -> Result<(), String> { + let previous: String = connection.query_row("SELECT receipt_hash FROM web_novel_module_receipts ORDER BY created_at_unix_ms DESC,rowid DESC LIMIT 1", [], |row| row.get(0)).optional().map_err(db_read)?.unwrap_or_default(); + let receipt_id = format!("WN-MOD-RCPT-{}", Uuid::new_v4()); + let now = now_ms(); + let hash = sha256( + format!("{previous}\0{receipt_id}\0{module_id}\0{action}\0{state}\0{detail}\0{now}") + .as_bytes(), + ); + connection.execute("INSERT INTO web_novel_module_receipts(receipt_id,module_id,action,state,detail,previous_hash,receipt_hash,created_at_unix_ms) VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", params![receipt_id,module_id,action,state,detail,previous,hash,now]).map_err(db_write)?; + Ok(()) +} + +#[cfg(test)] +fn require_mounted(connection: &Connection, module_id: &str) -> Result<(), String> { + let state: Option = connection.query_row("SELECT install_state FROM web_novel_module_installations WHERE module_id=?1 AND self_test_state='PASS'", [module_id], |row| row.get(0)).optional().map_err(db_read)?; + if state.as_deref() == Some("MOUNTED") { + Ok(()) + } else { + Err(format!("HOLOLAKE_WEBNOVEL_MODULE_NOT_MOUNTED:{module_id}")) + } +} + +#[cfg(not(test))] +fn require_mounted(_connection: &Connection, module_id: &str) -> Result<(), String> { + if matches!(module_id, OUTLINE | GRID | STORYWORLD | DELIVERY) { + // The public adapter entry already performed an exact-number ACTIVE check + // against the shared signed package runtime. Never consult the legacy + // installation table in production. + Ok(()) + } else { + Err("HOLOLAKE_WEBNOVEL_MODULE_UNKNOWN".into()) + } +} + +fn module_cache_path(cache: &Path, module_id: &str) -> Result { + if !packages().iter().any(|item| item.module_id == module_id) + || module_id.contains('/') + || module_id.contains("..") + { + return Err("HOLOLAKE_WEBNOVEL_MODULE_ID_INVALID".into()); + } + Ok(cache.join("official").join(module_id)) +} + +fn set_private_permissions(path: &Path) -> Result<(), String> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(io_error)?; + } + Ok(()) +} + +fn module_data_at(path: &Path, work_id: &str) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + let work_exists: bool = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM web_novel_works WHERE work_id=?1 AND archived=0)", + [work_id], + |row| row.get(0), + ) + .map_err(db_read)?; + if !work_exists { + return Err("HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".into()); + } + + let mut scene_statement = connection.prepare( + "SELECT scene_id,work_id,chapter_id,title,position,synopsis,status,goal,conflict,outcome,hook, + emotional_point,target_words,revision,updated_at_unix_ms + FROM web_novel_scenes WHERE work_id=?1 AND archived=0 ORDER BY chapter_id,position" + ).map_err(db_read)?; + let scenes = scene_statement + .query_map([work_id], |row| { + Ok(AuthorScene { + scene_id: row.get(0)?, + work_id: row.get(1)?, + chapter_id: row.get(2)?, + title: row.get(3)?, + position: row.get(4)?, + synopsis: row.get(5)?, + status: row.get(6)?, + goal: row.get(7)?, + conflict: row.get(8)?, + outcome: row.get(9)?, + hook: row.get(10)?, + emotional_point: row.get(11)?, + target_words: row.get(12)?, + revision: row.get(13)?, + updated_at_unix_ms: row.get(14)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut beat_statement = connection.prepare( + "SELECT b.beat_id,b.scene_id,b.position,b.title,b.note,b.status,b.revision,b.updated_at_unix_ms + FROM web_novel_beats b JOIN web_novel_scenes s ON s.scene_id=b.scene_id + WHERE s.work_id=?1 AND b.archived=0 AND s.archived=0 ORDER BY s.chapter_id,s.position,b.position" + ).map_err(db_read)?; + let beats = beat_statement + .query_map([work_id], |row| { + Ok(AuthorBeat { + beat_id: row.get(0)?, + scene_id: row.get(1)?, + position: row.get(2)?, + title: row.get(3)?, + note: row.get(4)?, + status: row.get(5)?, + revision: row.get(6)?, + updated_at_unix_ms: row.get(7)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut field_statement = connection + .prepare( + "SELECT field_id,work_id,label,field_type,options_json,position,revision + FROM web_novel_story_fields WHERE work_id=?1 AND archived=0 ORDER BY position,label", + ) + .map_err(db_read)?; + let field_definitions = field_statement + .query_map([work_id], |row| { + let options_json: String = row.get(4)?; + Ok(StoryFieldDefinition { + field_id: row.get(0)?, + work_id: row.get(1)?, + label: row.get(2)?, + field_type: row.get(3)?, + options: serde_json::from_str(&options_json).unwrap_or_default(), + position: row.get(5)?, + revision: row.get(6)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut value_statement = connection.prepare( + "SELECT v.field_id,v.scene_id,v.value,v.revision FROM web_novel_story_field_values v + JOIN web_novel_story_fields f ON f.field_id=v.field_id WHERE f.work_id=?1 AND f.archived=0" + ).map_err(db_read)?; + let field_values = value_statement + .query_map([work_id], |row| { + Ok(StoryFieldValue { + field_id: row.get(0)?, + scene_id: row.get(1)?, + value: row.get(2)?, + revision: row.get(3)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut timeline_statement = connection.prepare( + "SELECT event_id,work_id,scene_id,entity_id,title,calendar_kind,story_day,date_text,time_text, + duration_minutes,description,revision FROM web_novel_timeline_events + WHERE work_id=?1 AND archived=0 ORDER BY COALESCE(story_day,9223372036854775807),date_text,time_text,title" + ).map_err(db_read)?; + let timeline_events = timeline_statement + .query_map([work_id], |row| { + Ok(StoryTimelineEvent { + event_id: row.get(0)?, + work_id: row.get(1)?, + scene_id: row.get(2)?, + entity_id: row.get(3)?, + title: row.get(4)?, + calendar_kind: row.get(5)?, + story_day: row.get(6)?, + date_text: row.get(7)?, + time_text: row.get(8)?, + duration_minutes: row.get(9)?, + description: row.get(10)?, + revision: row.get(11)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut link_statement = connection + .prepare( + "SELECT link_id,work_id,scene_id,entity_id,role,note FROM web_novel_scene_entities + WHERE work_id=?1 ORDER BY scene_id,role,entity_id", + ) + .map_err(db_read)?; + let scene_entity_links = link_statement + .query_map([work_id], |row| { + Ok(SceneEntityLink { + link_id: row.get(0)?, + work_id: row.get(1)?, + scene_id: row.get(2)?, + entity_id: row.get(3)?, + role: row.get(4)?, + note: row.get(5)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + let mut version_statement = connection.prepare( + "SELECT v.version_id,v.chapter_id,c.title,v.revision,v.word_count,v.save_reason,v.created_at_unix_ms + FROM web_novel_chapter_versions v JOIN web_novel_chapters c ON c.chapter_id=v.chapter_id + WHERE v.work_id=?1 AND c.archived=0 ORDER BY v.created_at_unix_ms DESC,v.revision DESC LIMIT 500" + ).map_err(db_read)?; + let chapter_versions = version_statement + .query_map([work_id], |row| { + Ok(ChapterVersionSummary { + version_id: row.get(0)?, + chapter_id: row.get(1)?, + chapter_title: row.get(2)?, + revision: row.get(3)?, + word_count: row.get(4)?, + save_reason: row.get(5)?, + created_at_unix_ms: row.get(6)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + + Ok(AuthorModuleData { + work_id: work_id.into(), + scenes, + beats, + field_definitions, + field_values, + timeline_events, + scene_entity_links, + chapter_versions, + }) +} + +fn upsert_scene_at(path: &Path, input: UpsertAuthorSceneInput) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, OUTLINE)?; + let title = required(&input.title, "HOLOLAKE_WEBNOVEL_SCENE_TITLE_REQUIRED")?; + let status = normalized_enum( + &input.status, + &["PLANNED", "DRAFTING", "DONE", "REVISE"], + "PLANNED", + )?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let chapter_work: Option = tx + .query_row( + "SELECT work_id FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [&input.chapter_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + if chapter_work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_SCENE_CHAPTER_WORK_MISMATCH".into()); + } + let now = now_ms(); + let scene_id = input + .scene_id + .unwrap_or_else(|| format!("WN-SCENE-{}", Uuid::new_v4())); + let current: Option = tx + .query_row( + "SELECT revision FROM web_novel_scenes WHERE scene_id=?1", + [&scene_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + let revision = match current { + Some(value) => { + if input.expected_revision != Some(value) { + return Err("HOLOLAKE_WEBNOVEL_SCENE_REVISION_CONFLICT".into()); + } + tx.execute( + "UPDATE web_novel_scenes SET chapter_id=?1,title=?2,position=?3,synopsis=?4,status=?5,goal=?6, + conflict=?7,outcome=?8,hook=?9,emotional_point=?10,target_words=?11,revision=revision+1, + updated_at_unix_ms=?12 WHERE scene_id=?13 AND work_id=?14", + params![input.chapter_id,title,input.position.unwrap_or(0),input.synopsis,status,input.goal,input.conflict,input.outcome,input.hook,input.emotional_point,input.target_words.max(0),now,scene_id,input.work_id] + ).map_err(db_write)?; + value + 1 + } + None => { + let position = match input.position { Some(value) => value, None => tx.query_row( + "SELECT COALESCE(MAX(position),-1)+1 FROM web_novel_scenes WHERE chapter_id=?1 AND archived=0", [&input.chapter_id], |row| row.get(0) + ).map_err(db_read)? }; + tx.execute( + "INSERT INTO web_novel_scenes(scene_id,work_id,chapter_id,title,position,synopsis,status,goal,conflict,outcome, + hook,emotional_point,target_words,revision,created_at_unix_ms,updated_at_unix_ms,archived) + VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,1,?14,?14,0)", + params![scene_id,input.work_id,input.chapter_id,title,position,input.synopsis,status,input.goal,input.conflict,input.outcome,input.hook,input.emotional_point,input.target_words.max(0),now] + ).map_err(db_write)?; + 1 + } + }; + let scene = tx.query_row( + "SELECT scene_id,work_id,chapter_id,title,position,synopsis,status,goal,conflict,outcome,hook,emotional_point,target_words,revision,updated_at_unix_ms FROM web_novel_scenes WHERE scene_id=?1", + [&scene_id], |row| Ok(AuthorScene { scene_id: row.get(0)?, work_id: row.get(1)?, chapter_id: row.get(2)?, title: row.get(3)?, position: row.get(4)?, synopsis: row.get(5)?, status: row.get(6)?, goal: row.get(7)?, conflict: row.get(8)?, outcome: row.get(9)?, hook: row.get(10)?, emotional_point: row.get(11)?, target_words: row.get(12)?, revision: row.get(13)?, updated_at_unix_ms: row.get(14)? }) + ).map_err(db_read)?; + debug_assert_eq!(scene.revision, revision); + tx.commit().map_err(db_write)?; + Ok(scene) +} + +fn upsert_beat_at(path: &Path, input: UpsertAuthorBeatInput) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, OUTLINE)?; + let title = required(&input.title, "HOLOLAKE_WEBNOVEL_BEAT_TITLE_REQUIRED")?; + let status = normalized_enum( + &input.status, + &["PLANNED", "DRAFTING", "DONE", "REVISE"], + "PLANNED", + )?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let scene_exists: bool = tx + .query_row( + "SELECT EXISTS(SELECT 1 FROM web_novel_scenes WHERE scene_id=?1 AND archived=0)", + [&input.scene_id], + |row| row.get(0), + ) + .map_err(db_read)?; + if !scene_exists { + return Err("HOLOLAKE_WEBNOVEL_SCENE_NOT_FOUND".into()); + } + let now = now_ms(); + let beat_id = input + .beat_id + .unwrap_or_else(|| format!("WN-BEAT-{}", Uuid::new_v4())); + let current: Option = tx + .query_row( + "SELECT revision FROM web_novel_beats WHERE beat_id=?1", + [&beat_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + match current { + Some(value) => { + if input.expected_revision != Some(value) { + return Err("HOLOLAKE_WEBNOVEL_BEAT_REVISION_CONFLICT".into()); + } + tx.execute("UPDATE web_novel_beats SET title=?1,note=?2,status=?3,position=?4,revision=revision+1,updated_at_unix_ms=?5 WHERE beat_id=?6 AND scene_id=?7", params![title,input.note,status,input.position.unwrap_or(0),now,beat_id,input.scene_id]).map_err(db_write)?; + } + None => { + let position = match input.position { Some(value) => value, None => tx.query_row("SELECT COALESCE(MAX(position),-1)+1 FROM web_novel_beats WHERE scene_id=?1 AND archived=0", [&input.scene_id], |row| row.get(0)).map_err(db_read)? }; + tx.execute("INSERT INTO web_novel_beats(beat_id,scene_id,position,title,note,status,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,?2,?3,?4,?5,?6,1,?7,?7,0)", params![beat_id,input.scene_id,position,title,input.note,status,now]).map_err(db_write)?; + } + } + let beat = tx.query_row("SELECT beat_id,scene_id,position,title,note,status,revision,updated_at_unix_ms FROM web_novel_beats WHERE beat_id=?1", [&beat_id], |row| Ok(AuthorBeat { beat_id: row.get(0)?, scene_id: row.get(1)?, position: row.get(2)?, title: row.get(3)?, note: row.get(4)?, status: row.get(5)?, revision: row.get(6)?, updated_at_unix_ms: row.get(7)? })).map_err(db_read)?; + tx.commit().map_err(db_write)?; + Ok(beat) +} + +fn upsert_field_definition_at( + path: &Path, + input: UpsertStoryFieldDefinitionInput, +) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, GRID)?; + let label = required(&input.label, "HOLOLAKE_WEBNOVEL_STORY_FIELD_LABEL_REQUIRED")?; + let field_type = normalized_enum( + &input.field_type, + &["TEXT", "NUMBER", "SELECT", "BOOLEAN"], + "TEXT", + )?; + let options_json = serde_json::to_string(&input.options) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_STORY_FIELD_OPTIONS_INVALID: {error}"))?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let field_id = input + .field_id + .unwrap_or_else(|| format!("WN-FIELD-{}", Uuid::new_v4())); + let current: Option = tx + .query_row( + "SELECT revision FROM web_novel_story_fields WHERE field_id=?1", + [&field_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + match current { + Some(value) => { + if input.expected_revision != Some(value) { + return Err("HOLOLAKE_WEBNOVEL_STORY_FIELD_REVISION_CONFLICT".into()); + } + tx.execute("UPDATE web_novel_story_fields SET label=?1,field_type=?2,options_json=?3,revision=revision+1,updated_at_unix_ms=?4 WHERE field_id=?5 AND work_id=?6", params![label,field_type,options_json,now_ms(),field_id,input.work_id]).map_err(db_write)?; + } + None => { + let position: i64 = tx.query_row("SELECT COALESCE(MAX(position),-1)+1 FROM web_novel_story_fields WHERE work_id=?1 AND archived=0", [&input.work_id], |row| row.get(0)).map_err(db_read)?; + let now = now_ms(); + tx.execute("INSERT INTO web_novel_story_fields(field_id,work_id,label,field_type,options_json,position,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,?2,?3,?4,?5,?6,1,?7,?7,0)", params![field_id,input.work_id,label,field_type,options_json,position,now]).map_err(db_write)?; + } + } + let definition = tx.query_row("SELECT field_id,work_id,label,field_type,options_json,position,revision FROM web_novel_story_fields WHERE field_id=?1", [&field_id], |row| { let value: String = row.get(4)?; Ok(StoryFieldDefinition { field_id: row.get(0)?, work_id: row.get(1)?, label: row.get(2)?, field_type: row.get(3)?, options: serde_json::from_str(&value).unwrap_or_default(), position: row.get(5)?, revision: row.get(6)? }) }).map_err(db_read)?; + tx.commit().map_err(db_write)?; + Ok(definition) +} + +fn upsert_field_value_at( + path: &Path, + input: UpsertStoryFieldValueInput, +) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, GRID)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let same_work: bool = tx.query_row("SELECT EXISTS(SELECT 1 FROM web_novel_story_fields f JOIN web_novel_scenes s ON s.work_id=f.work_id WHERE f.field_id=?1 AND s.scene_id=?2 AND f.archived=0 AND s.archived=0)", params![input.field_id,input.scene_id], |row| row.get(0)).map_err(db_read)?; + if !same_work { + return Err("HOLOLAKE_WEBNOVEL_STORY_FIELD_SCENE_MISMATCH".into()); + } + let current: Option = tx + .query_row( + "SELECT revision FROM web_novel_story_field_values WHERE field_id=?1 AND scene_id=?2", + params![input.field_id, input.scene_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + let revision = match current { + Some(value) => { + if input.expected_revision != Some(value) { + return Err("HOLOLAKE_WEBNOVEL_STORY_FIELD_VALUE_REVISION_CONFLICT".into()); + } + value + 1 + } + None => 1, + }; + tx.execute("INSERT INTO web_novel_story_field_values(field_id,scene_id,value,revision,updated_at_unix_ms) VALUES(?1,?2,?3,?4,?5) ON CONFLICT(field_id,scene_id) DO UPDATE SET value=excluded.value,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms", params![input.field_id,input.scene_id,input.value,revision,now_ms()]).map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(StoryFieldValue { + field_id: input.field_id, + scene_id: input.scene_id, + value: input.value, + revision, + }) +} + +fn upsert_timeline_at( + path: &Path, + input: UpsertTimelineEventInput, +) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, STORYWORLD)?; + let title = required(&input.title, "HOLOLAKE_WEBNOVEL_TIMELINE_TITLE_REQUIRED")?; + let calendar_kind = normalized_enum( + &input.calendar_kind, + &["STORY_DAY", "DATE", "CUSTOM"], + "STORY_DAY", + )?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + if let Some(scene_id) = input.scene_id.as_deref() { + let work: Option = tx + .query_row( + "SELECT work_id FROM web_novel_scenes WHERE scene_id=?1 AND archived=0", + [scene_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + if work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_TIMELINE_SCENE_MISMATCH".into()); + } + } + if let Some(entity_id) = input.entity_id.as_deref() { + let work: Option = tx + .query_row( + "SELECT work_id FROM web_novel_entities WHERE entity_id=?1 AND archived=0", + [entity_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + if work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_TIMELINE_ENTITY_MISMATCH".into()); + } + } + let event_id = input + .event_id + .unwrap_or_else(|| format!("WN-EVENT-{}", Uuid::new_v4())); + let current: Option = tx + .query_row( + "SELECT revision FROM web_novel_timeline_events WHERE event_id=?1", + [&event_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)?; + let revision = match current { + Some(value) => { + if input.expected_revision != Some(value) { + return Err("HOLOLAKE_WEBNOVEL_TIMELINE_REVISION_CONFLICT".into()); + } + value + 1 + } + None => 1, + }; + let now = now_ms(); + tx.execute("INSERT INTO web_novel_timeline_events(event_id,work_id,scene_id,entity_id,title,calendar_kind,story_day,date_text,time_text,duration_minutes,description,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?13,0) ON CONFLICT(event_id) DO UPDATE SET scene_id=excluded.scene_id,entity_id=excluded.entity_id,title=excluded.title,calendar_kind=excluded.calendar_kind,story_day=excluded.story_day,date_text=excluded.date_text,time_text=excluded.time_text,duration_minutes=excluded.duration_minutes,description=excluded.description,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms", params![event_id,input.work_id,input.scene_id,input.entity_id,title,calendar_kind,input.story_day,input.date_text,input.time_text,input.duration_minutes.max(0),input.description,revision,now]).map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(StoryTimelineEvent { + event_id, + work_id: input.work_id, + scene_id: input.scene_id, + entity_id: input.entity_id, + title, + calendar_kind, + story_day: input.story_day, + date_text: input.date_text, + time_text: input.time_text, + duration_minutes: input.duration_minutes.max(0), + description: input.description, + revision, + }) +} + +fn link_scene_entity_at( + path: &Path, + input: LinkSceneEntityInput, +) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, STORYWORLD)?; + let role = required(&input.role, "HOLOLAKE_WEBNOVEL_SCENE_ENTITY_ROLE_REQUIRED")?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let valid: bool = tx.query_row("SELECT EXISTS(SELECT 1 FROM web_novel_scenes s JOIN web_novel_entities e ON e.work_id=s.work_id WHERE s.scene_id=?1 AND e.entity_id=?2 AND s.work_id=?3 AND s.archived=0 AND e.archived=0)", params![input.scene_id,input.entity_id,input.work_id], |row| row.get(0)).map_err(db_read)?; + if !valid { + return Err("HOLOLAKE_WEBNOVEL_SCENE_ENTITY_WORK_MISMATCH".into()); + } + let existing: Option = tx.query_row("SELECT link_id FROM web_novel_scene_entities WHERE scene_id=?1 AND entity_id=?2 AND role=?3", params![input.scene_id,input.entity_id,role], |row| row.get(0)).optional().map_err(db_read)?; + let link_id = existing.unwrap_or_else(|| format!("WN-SCENE-ENTITY-{}", Uuid::new_v4())); + tx.execute("INSERT INTO web_novel_scene_entities(link_id,work_id,scene_id,entity_id,role,note,created_at_unix_ms) VALUES(?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(scene_id,entity_id,role) DO UPDATE SET note=excluded.note", params![link_id,input.work_id,input.scene_id,input.entity_id,role,input.note,now_ms()]).map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(SceneEntityLink { + link_id, + work_id: input.work_id, + scene_id: input.scene_id, + entity_id: input.entity_id, + role, + note: input.note, + }) +} + +fn restore_version_at(path: &Path, version_id: &str) -> Result { + let mut connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, DELIVERY)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(db_write)?; + let source: Option<(String,String,String,String,String,String,i64)> = tx.query_row( + "SELECT chapter_id,work_id,title,synopsis,content,workflow_status,word_count FROM web_novel_chapter_versions WHERE version_id=?1", + [version_id], |row| Ok((row.get(0)?,row.get(1)?,row.get(2)?,row.get(3)?,row.get(4)?,row.get(5)?,row.get(6)?)) + ).optional().map_err(db_read)?; + let Some((chapter_id, work_id, title, synopsis, content, workflow_status, word_count)) = source + else { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_VERSION_NOT_FOUND".into()); + }; + let current_revision: i64 = tx + .query_row( + "SELECT revision FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [&chapter_id], + |row| row.get(0), + ) + .map_err(db_read)?; + let revision = current_revision + 1; + let now = now_ms(); + tx.execute("UPDATE web_novel_chapters SET title=?1,synopsis=?2,content=?3,workflow_status=?4,word_count=?5,revision=?6,updated_at_unix_ms=?7 WHERE chapter_id=?8", params![title,synopsis,content,workflow_status,word_count,revision,now,chapter_id]).map_err(db_write)?; + let restored_id = format!("WN-VER-{}", Uuid::new_v4()); + tx.execute("INSERT INTO web_novel_chapter_versions(version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,'RESTORED_FROM_VERSION',?10)", params![restored_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,now]).map_err(db_write)?; + tx.commit().map_err(db_write)?; + Ok(ChapterVersionSummary { + version_id: restored_id, + chapter_id, + chapter_title: title, + revision, + word_count, + save_reason: "RESTORED_FROM_VERSION".into(), + created_at_unix_ms: now, + }) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct DeliveryChapter { + volume_title: String, + chapter_title: String, + synopsis: String, + content: String, + word_count: i64, + revision: i64, +} + +fn normalize_delivery_format(format: &str) -> Result { + let normalized = format.trim().to_ascii_uppercase(); + if ["TXT", "DOCX", "EPUB", "JSON"].contains(&normalized.as_str()) { + Ok(normalized) + } else { + Err("HOLOLAKE_WEBNOVEL_DELIVERY_FORMAT_UNSUPPORTED".into()) + } +} + +fn read_work_title(path: &Path, work_id: &str) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + connection + .query_row( + "SELECT title FROM web_novel_works WHERE work_id=?1 AND archived=0", + [work_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".into()) +} + +fn safe_filename(value: &str) -> String { + let result: String = value + .chars() + .map(|character| { + if ['/', '\\', ':', '*', '?', '"', '<', '>', '|'].contains(&character) { + '_' + } else { + character + } + }) + .collect(); + let trimmed = result.trim().trim_end_matches('.'); + if trimmed.is_empty() { + "未命名作品".into() + } else { + trimmed.chars().take(80).collect() + } +} + +fn export_delivery_at( + path: &Path, + work_id: &str, + format: &str, + destination: &Path, +) -> Result { + let connection = super::web_novel_workspace::open_database(path)?; + ensure_schema(&connection)?; + require_mounted(&connection, DELIVERY)?; + let title: String = connection + .query_row( + "SELECT title FROM web_novel_works WHERE work_id=?1 AND archived=0", + [work_id], + |row| row.get(0), + ) + .optional() + .map_err(db_read)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".to_string())?; + let mut statement = connection.prepare( + "SELECT v.title,c.title,c.synopsis,c.content,c.word_count,c.revision FROM web_novel_chapters c + JOIN web_novel_volumes v ON v.volume_id=c.volume_id WHERE c.work_id=?1 AND c.archived=0 AND v.archived=0 + ORDER BY v.position,c.position" + ).map_err(db_read)?; + let chapters = statement + .query_map([work_id], |row| { + Ok(DeliveryChapter { + volume_title: row.get(0)?, + chapter_title: row.get(1)?, + synopsis: row.get(2)?, + content: row.get(3)?, + word_count: row.get(4)?, + revision: row.get(5)?, + }) + }) + .map_err(db_read)? + .collect::, _>>() + .map_err(db_read)?; + if chapters.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_DELIVERY_HAS_NO_CHAPTERS".into()); + } + let normalized = normalize_delivery_format(format)?; + let bytes = match normalized.as_str() { + "TXT" => render_txt(&title, &chapters).into_bytes(), + "JSON" => serde_json::to_vec_pretty(&serde_json::json!({"schema":"hololake.web-novel.delivery.v1","workId":work_id,"title":title,"chapters":chapters})).map_err(|error| format!("HOLOLAKE_WEBNOVEL_DELIVERY_JSON_FAILED: {error}"))?, + "DOCX" => render_docx(&title, &chapters)?, + "EPUB" => render_epub(&title, work_id, &chapters)?, + _ => unreachable!(), + }; + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent).map_err(io_error)?; + } + let pending = destination.with_extension(format!( + "{}.pending", + destination + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("export") + )); + fs::write(&pending, &bytes).map_err(io_error)?; + fs::rename(&pending, destination).map_err(io_error)?; + Ok(AuthorDeliveryReceipt { + format: normalized, + path: destination.to_string_lossy().into_owned(), + bytes: bytes.len() as u64, + chapter_count: chapters.len(), + package_sha256: sha256(&bytes), + }) +} + +fn render_txt(title: &str, chapters: &[DeliveryChapter]) -> String { + let mut text = String::new(); + text.push_str(title); + text.push_str("\n\n"); + let mut current_volume = ""; + for chapter in chapters { + if chapter.volume_title != current_volume { + current_volume = &chapter.volume_title; + text.push_str(current_volume); + text.push_str("\n\n"); + } + text.push_str(&chapter.chapter_title); + text.push_str("\n\n"); + text.push_str(&chapter.content); + text.push_str("\n\n"); + } + text +} + +fn render_docx(title: &str, chapters: &[DeliveryChapter]) -> Result, String> { + let mut paragraphs = format!( + "{}", + xml_escape(title) + ); + for chapter in chapters { + paragraphs.push_str(&format!( + "{}", + xml_escape(&chapter.chapter_title) + )); + for line in chapter.content.lines() { + paragraphs.push_str(&format!( + "{}", + xml_escape(line) + )); + } + } + let document = format!("{paragraphs}"); + zip_bytes(&[ + ("[Content_Types].xml", "".as_bytes(), true), + ("_rels/.rels", "".as_bytes(), true), + ("word/document.xml", document.as_bytes(), true), + ]) +} + +fn render_epub( + title: &str, + work_id: &str, + chapters: &[DeliveryChapter], +) -> Result, String> { + let mut entries: Vec<(String, Vec, bool)> = Vec::new(); + entries.push(("mimetype".into(), b"application/epub+zip".to_vec(), false)); + entries.push(("META-INF/container.xml".into(), b"".to_vec(), true)); + let mut manifest = String::from(""); + let mut spine = String::new(); + let mut navigation = String::new(); + for (index, chapter) in chapters.iter().enumerate() { + let id = format!("c{}", index + 1); + let href = format!("chapter-{}.xhtml", index + 1); + manifest.push_str(&format!( + "" + )); + spine.push_str(&format!("")); + navigation.push_str(&format!( + "
  • {}
  • ", + xml_escape(&chapter.chapter_title) + )); + let body = chapter + .content + .lines() + .map(|line| format!("

    {}

    ", xml_escape(line))) + .collect::(); + let xhtml = format!("{}

    {}

    {body}", xml_escape(&chapter.chapter_title), xml_escape(&chapter.chapter_title)); + entries.push((format!("OEBPS/{href}"), xhtml.into_bytes(), true)); + } + let opf = format!("{}{}zh-CN2026-08-18T00:00:00Z{manifest}{spine}", xml_escape(work_id), xml_escape(title)); + let nav = format!("{}", xml_escape(title)); + entries.push(("OEBPS/content.opf".into(), opf.into_bytes(), true)); + entries.push(("OEBPS/nav.xhtml".into(), nav.into_bytes(), true)); + let refs: Vec<(&str, &[u8], bool)> = entries + .iter() + .map(|(name, bytes, compressed)| (name.as_str(), bytes.as_slice(), *compressed)) + .collect(); + zip_bytes(&refs) +} + +fn zip_bytes(entries: &[(&str, &[u8], bool)]) -> Result, String> { + let cursor = Cursor::new(Vec::new()); + let mut archive = ZipWriter::new(cursor); + for (name, bytes, compressed) in entries { + let options = FileOptions::default().compression_method(if *compressed { + CompressionMethod::Deflated + } else { + CompressionMethod::Stored + }); + archive + .start_file(*name, options) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_DELIVERY_ZIP_FAILED: {error}"))?; + archive.write_all(bytes).map_err(io_error)?; + } + archive + .finish() + .map(|cursor| cursor.into_inner()) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_DELIVERY_ZIP_FAILED: {error}")) +} + +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} +fn required(value: &str, error: &str) -> Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + Err(error.into()) + } else { + Ok(trimmed.chars().take(200).collect()) + } +} +fn normalized_enum(value: &str, allowed: &[&str], default: &str) -> Result { + let normalized = if value.trim().is_empty() { + default.into() + } else { + value.trim().to_ascii_uppercase() + }; + if allowed.contains(&normalized.as_str()) { + Ok(normalized) + } else { + Err(format!( + "HOLOLAKE_WEBNOVEL_MODULE_ENUM_INVALID:{normalized}" + )) + } +} +fn sha256(value: &[u8]) -> String { + digest(&SHA256, value) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} +fn db_read(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_MODULE_DATABASE_READ_FAILED: {error}") +} +fn db_write(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_MODULE_DATABASE_WRITE_FAILED: {error}") +} +fn io_error(error: std::io::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_MODULE_IO_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Read; + use tempfile::tempdir; + + fn seed_work(path: &Path) -> (String, String, String) { + let connection = super::super::web_novel_workspace::open_database(path).unwrap(); + let work_id = "WN-WORK-REAL-ENGINE".to_string(); + let volume_id = "WN-VOL-REAL-ENGINE".to_string(); + let chapter_id = "WN-CH-REAL-ENGINE".to_string(); + let now = now_ms(); + connection.execute("INSERT INTO web_novel_works(work_id,title,pen_name,genre,synopsis,contract_status,copyright_status,workflow_status,target_words,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,'真实模块验收作品','冰朔','测试','用于原生引擎验收','UNSIGNED','AUTHOR_OWNED','DRAFT',100000,1,?2,?2,0)", params![work_id,now]).unwrap(); + connection.execute("INSERT INTO web_novel_volumes(volume_id,work_id,title,position,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,?2,'第一卷',0,1,?3,?3,0)", params![volume_id,work_id,now]).unwrap(); + connection.execute("INSERT INTO web_novel_chapters(chapter_id,volume_id,work_id,title,position,synopsis,content,workflow_status,scheduled_at_unix_ms,word_count,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES(?1,?2,?3,'第一章 起点',0,'首章概要','当前第二版正文','DRAFT',NULL,9,2,?4,?4,0)", params![chapter_id,volume_id,work_id,now]).unwrap(); + connection.execute("INSERT INTO web_novel_chapter_versions(version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms) VALUES('WN-VER-REAL-1',?1,?2,1,'第一章 起点','首章概要','可恢复的第一版正文','DRAFT',12,'AUTOSAVE',?3)", params![chapter_id,work_id,now-100]).unwrap(); + connection.execute("INSERT INTO web_novel_chapter_versions(version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms) VALUES('WN-VER-REAL-2',?1,?2,2,'第一章 起点','首章概要','当前第二版正文','DRAFT',9,'MANUAL_SAVE',?3)", params![chapter_id,work_id,now]).unwrap(); + connection.execute("INSERT INTO web_novel_entities(entity_id,work_id,entity_type,name,aliases_json,description,first_chapter_id,revision,created_at_unix_ms,updated_at_unix_ms,archived) VALUES('WN-ENTITY-REAL-1',?1,'CHARACTER','主角','[]','真实关联对象',?2,1,?3,?3,0)", params![work_id,chapter_id,now]).unwrap(); + (work_id, chapter_id, "WN-ENTITY-REAL-1".into()) + } + + #[test] + fn installs_mounts_executes_exports_and_preserves_data_on_uninstall() { + let temp = tempdir().unwrap(); + let database = temp.path().join("modules.sqlite3"); + let cache = temp.path().join("cache"); + let (work_id, chapter_id, entity_id) = seed_work(&database); + for package in packages() { + let installed = install_at(&database, &cache, package.module_id).unwrap(); + let descriptor = installed + .modules + .iter() + .find(|item| item.module_id == package.module_id) + .unwrap(); + assert_eq!(descriptor.self_test_state, "PASS"); + assert!(cache + .join("official") + .join(package.module_id) + .join(VERSION) + .join("module.json") + .is_file()); + set_mount_at(&database, package.module_id, true).unwrap(); + } + + let scene = upsert_scene_at( + &database, + UpsertAuthorSceneInput { + work_id: work_id.clone(), + scene_id: None, + chapter_id: chapter_id.clone(), + title: "真实场景".into(), + position: None, + synopsis: "主角踏入未知区域".into(), + status: "PLANNED".into(), + goal: "找到出口".into(), + conflict: "追兵封锁".into(), + outcome: "发现密道".into(), + hook: "密道中有熟悉声音".into(), + emotional_point: "紧张转希望".into(), + target_words: 1600, + expected_revision: None, + }, + ) + .unwrap(); + let beat = upsert_beat_at( + &database, + UpsertAuthorBeatInput { + scene_id: scene.scene_id.clone(), + beat_id: None, + title: "追兵逼近".into(), + note: "脚步声从转角处传来".into(), + status: "PLANNED".into(), + position: None, + expected_revision: None, + }, + ) + .unwrap(); + assert_eq!(beat.position, 0); + let field = upsert_field_definition_at( + &database, + UpsertStoryFieldDefinitionInput { + work_id: work_id.clone(), + field_id: None, + label: "视角人物".into(), + field_type: "SELECT".into(), + options: vec!["主角".into(), "反派".into()], + expected_revision: None, + }, + ) + .unwrap(); + upsert_field_value_at( + &database, + UpsertStoryFieldValueInput { + field_id: field.field_id.clone(), + scene_id: scene.scene_id.clone(), + value: "主角".into(), + expected_revision: None, + }, + ) + .unwrap(); + let timeline = upsert_timeline_at( + &database, + UpsertTimelineEventInput { + work_id: work_id.clone(), + event_id: None, + scene_id: Some(scene.scene_id.clone()), + entity_id: Some(entity_id.clone()), + title: "逃入密道".into(), + calendar_kind: "STORY_DAY".into(), + story_day: Some(1), + date_text: "".into(), + time_text: "深夜".into(), + duration_minutes: 20, + description: "改变主角路线的节点".into(), + expected_revision: None, + }, + ) + .unwrap(); + assert_eq!(timeline.story_day, Some(1)); + link_scene_entity_at( + &database, + LinkSceneEntityInput { + work_id: work_id.clone(), + scene_id: scene.scene_id.clone(), + entity_id, + role: "出场".into(), + note: "时间线主角".into(), + }, + ) + .unwrap(); + + let data = module_data_at(&database, &work_id).unwrap(); + assert_eq!( + ( + data.scenes.len(), + data.beats.len(), + data.field_definitions.len(), + data.field_values.len(), + data.timeline_events.len(), + data.scene_entity_links.len() + ), + (1, 1, 1, 1, 1, 1) + ); + let restored = restore_version_at(&database, "WN-VER-REAL-1").unwrap(); + assert_eq!(restored.revision, 3); + let connection = super::super::web_novel_workspace::open_database(&database).unwrap(); + let restored_content: String = connection + .query_row( + "SELECT content FROM web_novel_chapters WHERE chapter_id=?1", + [&chapter_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(restored_content, "可恢复的第一版正文"); + + for format in ["TXT", "JSON", "DOCX", "EPUB"] { + let destination = temp + .path() + .join(format!("delivery.{}", format.to_ascii_lowercase())); + let receipt = export_delivery_at(&database, &work_id, format, &destination).unwrap(); + assert_eq!(receipt.chapter_count, 1); + assert!(receipt.bytes > 20); + assert_eq!(receipt.package_sha256.len(), 64); + if format == "DOCX" || format == "EPUB" { + let file = fs::File::open(&destination).unwrap(); + let mut archive = zip::ZipArchive::new(file).unwrap(); + let required = if format == "DOCX" { + "word/document.xml" + } else { + "OEBPS/content.opf" + }; + let mut content = String::new(); + archive + .by_name(required) + .unwrap() + .read_to_string(&mut content) + .unwrap(); + assert!(content.contains("真实模块验收作品") || content.contains("第一章")); + } + } + + uninstall_at(&database, &cache, OUTLINE).unwrap(); + assert!(!cache.join("official").join(OUTLINE).exists()); + let preserved = module_data_at(&database, &work_id).unwrap(); + assert_eq!(preserved.scenes.len(), 1); + let marketplace = marketplace_at(&database).unwrap(); + assert!(marketplace + .recent_receipts + .iter() + .any(|receipt| receipt.action == "UNINSTALL")); + } + + #[test] + fn rejects_data_mutation_until_the_official_module_is_mounted() { + let temp = tempdir().unwrap(); + let database = temp.path().join("gate.sqlite3"); + let (work_id, chapter_id, _) = seed_work(&database); + let error = upsert_scene_at( + &database, + UpsertAuthorSceneInput { + work_id, + scene_id: None, + chapter_id, + title: "不应写入".into(), + position: None, + synopsis: "".into(), + status: "PLANNED".into(), + goal: "".into(), + conflict: "".into(), + outcome: "".into(), + hook: "".into(), + emotional_point: "".into(), + target_words: 0, + expected_revision: None, + }, + ) + .unwrap_err(); + assert!(error.contains("NOT_MOUNTED")); + } + + #[test] + #[ignore = "explicit real Desktop fixture module acceptance"] + fn imports_organizes_and_exports_real_desktop_materials() { + let fixtures = [ + ("HOLOLAKE_REAL_NOVEL", 504usize, "EPUB"), + ("HOLOLAKE_REAL_OUTLINE", 50usize, "DOCX"), + ("HOLOLAKE_REAL_SCRIPT", 75usize, "JSON"), + ]; + let temp = tempdir().unwrap(); + let database = temp.path().join("real-material-module-acceptance.sqlite3"); + let cache = temp.path().join("module-cache"); + super::super::web_novel_workspace::open_database(&database).unwrap(); + for package in packages() { + install_at(&database, &cache, package.module_id).unwrap(); + set_mount_at(&database, package.module_id, true).unwrap(); + } + let mut outline_work_id = None; + for (variable, expected, format) in fixtures { + let source = std::env::var(variable).expect("real fixture path is required"); + let preview = + super::super::web_novel_import::stage_document_at(&database, Path::new(&source)) + .unwrap(); + assert_eq!(preview.section_count, expected, "{variable}"); + let receipt = super::super::web_novel_import::commit_import_at( + &database, + super::super::web_novel_import::CommitImportInput { + import_id: preview.import_id, + target_mode: "CREATE_NEW".into(), + target_work_id: None, + title: preview.detected_title, + pen_name: preview.detected_pen_name, + genre: preview.detected_genre, + }, + ) + .unwrap(); + if variable == "HOLOLAKE_REAL_OUTLINE" { + outline_work_id = Some(receipt.work_id.clone()); + } + let connection = super::super::web_novel_workspace::open_database(&database).unwrap(); + let mut statement = connection.prepare("SELECT chapter_id,title,SUBSTR(content,1,400) FROM web_novel_chapters WHERE work_id=?1 AND archived=0 ORDER BY position").unwrap(); + let chapters = statement + .query_map([&receipt.work_id], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + }) + .unwrap() + .collect::, _>>() + .unwrap(); + drop(statement); + drop(connection); + assert_eq!(chapters.len(), expected); + for (chapter_id, title, synopsis) in chapters { + upsert_scene_at( + &database, + UpsertAuthorSceneInput { + work_id: receipt.work_id.clone(), + scene_id: None, + chapter_id, + title, + position: None, + synopsis, + status: "PLANNED".into(), + goal: "根据导入材料继续展开".into(), + conflict: "".into(), + outcome: "".into(), + hook: "".into(), + emotional_point: "".into(), + target_words: 1500, + expected_revision: None, + }, + ) + .unwrap(); + } + let organized = module_data_at(&database, &receipt.work_id).unwrap(); + assert_eq!(organized.scenes.len(), expected); + let first = organized.scenes.first().unwrap(); + let field = upsert_field_definition_at( + &database, + UpsertStoryFieldDefinitionInput { + work_id: receipt.work_id.clone(), + field_id: None, + label: "导入材料类型".into(), + field_type: "TEXT".into(), + options: vec![], + expected_revision: None, + }, + ) + .unwrap(); + upsert_field_value_at( + &database, + UpsertStoryFieldValueInput { + field_id: field.field_id, + scene_id: first.scene_id.clone(), + value: variable.into(), + expected_revision: None, + }, + ) + .unwrap(); + upsert_timeline_at( + &database, + UpsertTimelineEventInput { + work_id: receipt.work_id.clone(), + event_id: None, + scene_id: Some(first.scene_id.clone()), + entity_id: None, + title: "导入材料起点".into(), + calendar_kind: "STORY_DAY".into(), + story_day: Some(1), + date_text: "".into(), + time_text: "".into(), + duration_minutes: 0, + description: preview.source_filename, + expected_revision: None, + }, + ) + .unwrap(); + let destination = temp + .path() + .join(format!("{variable}.{}", format.to_ascii_lowercase())); + let delivery = + export_delivery_at(&database, &receipt.work_id, format, &destination).unwrap(); + assert_eq!(delivery.chapter_count, expected); + assert!(delivery.bytes > 100); + println!( + "{variable}: imported={}, organized={}, export={} bytes={}, sha256={}", + expected, expected, format, delivery.bytes, delivery.package_sha256 + ); + } + let txt = temp.path().join("outline.txt"); + let txt_receipt = + export_delivery_at(&database, outline_work_id.as_deref().unwrap(), "TXT", &txt) + .unwrap(); + assert_eq!(txt_receipt.chapter_count, 50); + assert!(fs::read_to_string(txt).unwrap().chars().count() > 1000); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/web_novel_workspace.rs b/product-source/hololake-native-desktop/src-tauri/src/web_novel_workspace.rs new file mode 100644 index 000000000..032af3c86 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/web_novel_workspace.rs @@ -0,0 +1,3007 @@ +//! 网文行业本地原生工作引擎。 +//! +//! 数据只进入当前已验证账号的独立 SQLite。作者、编辑与运营投影共享同一组 +//! 作品对象,不在前端复制假数据,也不自动登录或发布到第三方平台。 + +use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tauri::{AppHandle, Manager}; +use tauri_plugin_dialog::DialogExt; +use uuid::Uuid; + +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; +const ADAPTER: &str = "web-novel-workbench-v1"; + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +const MAX_TITLE_BYTES: usize = 300; +const MAX_SYNOPSIS_BYTES: usize = 40_000; +const MAX_CHAPTER_BYTES: usize = 4_000_000; +const MAX_ENTITY_DESCRIPTION_BYTES: usize = 200_000; +const MAX_REVIEW_NOTE_BYTES: usize = 100_000; +const MAX_WORKS: i64 = 200; +const WORKFLOW_STATES: &[&str] = &[ + "DRAFT", + "SELF_REVIEW", + "EDITOR_REVIEW", + "REVISION_REQUIRED", + "APPROVED", + "SCHEDULED", + "PUBLISHED", +]; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelWorkSummary { + pub work_id: String, + pub title: String, + pub pen_name: String, + pub genre: String, + pub work_kind: String, + pub workflow_status: String, + pub target_words: i64, + pub total_words: i64, + pub chapter_count: i64, + pub published_chapter_count: i64, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelWork { + pub work_id: String, + pub title: String, + pub pen_name: String, + pub genre: String, + pub work_kind: String, + pub synopsis: String, + pub contract_status: String, + pub copyright_status: String, + pub workflow_status: String, + pub target_words: i64, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelVolume { + pub volume_id: String, + pub work_id: String, + pub title: String, + pub position: i64, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelChapterSummary { + pub chapter_id: String, + pub volume_id: String, + pub work_id: String, + pub title: String, + pub position: i64, + pub synopsis: String, + pub workflow_status: String, + pub scheduled_at_unix_ms: Option, + pub word_count: i64, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelChapter { + pub chapter_id: String, + pub volume_id: String, + pub work_id: String, + pub title: String, + pub position: i64, + pub synopsis: String, + pub content: String, + pub workflow_status: String, + pub scheduled_at_unix_ms: Option, + pub word_count: i64, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelStoryEntity { + pub entity_id: String, + pub work_id: String, + pub entity_type: String, + pub name: String, + pub aliases: Vec, + pub description: String, + pub first_chapter_id: Option, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelStoryRelation { + pub relation_id: String, + pub work_id: String, + pub source_entity_id: String, + pub target_entity_id: String, + pub relation_type: String, + pub note: String, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelForeshadow { + pub foreshadow_id: String, + pub work_id: String, + pub title: String, + pub setup_chapter_id: String, + pub payoff_chapter_id: Option, + pub status: String, + pub note: String, + pub revision: i64, + pub created_at_unix_ms: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelReviewNote { + pub note_id: String, + pub work_id: String, + pub chapter_id: String, + pub note: String, + pub status: String, + pub created_at_unix_ms: i64, + pub resolved_at_unix_ms: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelWorkflowEvent { + pub event_id: String, + pub work_id: String, + pub chapter_id: String, + pub from_status: String, + pub to_status: String, + pub note: String, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelCheckpointSummary { + pub checkpoint_id: String, + pub work_id: String, + pub name: String, + pub description: String, + pub chapter_count: i64, + pub word_count: i64, + pub created_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WebNovelMetric { + pub metric_id: String, + pub work_id: String, + pub metric_date: String, + pub views: i64, + pub follows: i64, + pub comments: i64, + pub paid_readers: i64, + pub revenue_cents: i64, + pub source_label: String, + pub revision: i64, + pub updated_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelOperationsSummary { + pub total_words: i64, + pub draft_chapters: i64, + pub editor_review_chapters: i64, + pub approved_chapters: i64, + pub scheduled_chapters: i64, + pub published_chapters: i64, + pub open_review_notes: i64, + pub open_foreshadows: i64, + pub latest_views: i64, + pub latest_follows: i64, + pub latest_comments: i64, + pub latest_paid_readers: i64, + pub latest_revenue_cents: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelWorkspaceSnapshot { + pub state: &'static str, + pub works: Vec, + pub work_count: usize, + pub storage: &'static str, + pub authority: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelWorkDetail { + pub work: WebNovelWork, + pub volumes: Vec, + pub chapters: Vec, + pub entities: Vec, + pub relations: Vec, + pub foreshadows: Vec, + pub review_notes: Vec, + pub workflow_events: Vec, + pub checkpoints: Vec, + pub metrics: Vec, + pub operations: WebNovelOperationsSummary, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelContinuityIssue { + pub issue_id: String, + pub severity: String, + pub code: String, + pub title: String, + pub detail: String, + pub object_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelContinuityAudit { + pub state: &'static str, + pub work_id: String, + pub issue_count: usize, + pub blocking_issue_count: usize, + pub warning_issue_count: usize, + pub issues: Vec, + pub checked_at_unix_ms: i64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebNovelExportReceipt { + pub path: String, + pub bytes: u64, + pub chapter_count: usize, + pub word_count: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelWorkInput { + pub title: String, + pub pen_name: String, + pub genre: String, + #[serde(default = "default_work_kind")] + pub work_kind: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ReadWebNovelWorkInput { + pub work_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveWebNovelWorkInput { + pub work_id: String, + pub title: String, + pub pen_name: String, + pub genre: String, + #[serde(default = "default_work_kind")] + pub work_kind: String, + pub synopsis: String, + pub contract_status: String, + pub copyright_status: String, + pub target_words: i64, + pub expected_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelVolumeInput { + pub work_id: String, + pub title: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelChapterInput { + pub work_id: String, + pub volume_id: String, + pub title: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ReadWebNovelChapterInput { + pub chapter_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveWebNovelChapterInput { + pub chapter_id: String, + pub title: String, + pub synopsis: String, + pub content: String, + pub expected_revision: i64, + pub save_reason: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TransitionWebNovelChapterInput { + pub chapter_id: String, + pub to_status: String, + pub scheduled_at_unix_ms: Option, + pub note: String, + pub expected_revision: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelCheckpointInput { + pub work_id: String, + pub name: String, + pub description: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RestoreWebNovelCheckpointInput { + pub checkpoint_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertWebNovelStoryEntityInput { + pub work_id: String, + pub entity_id: Option, + pub entity_type: String, + pub name: String, + pub aliases: Vec, + pub description: String, + pub first_chapter_id: Option, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelStoryRelationInput { + pub work_id: String, + pub source_entity_id: String, + pub target_entity_id: String, + pub relation_type: String, + pub note: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpsertWebNovelForeshadowInput { + pub work_id: String, + pub foreshadow_id: Option, + pub title: String, + pub setup_chapter_id: String, + pub payoff_chapter_id: Option, + pub status: String, + pub note: String, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CreateWebNovelReviewNoteInput { + pub work_id: String, + pub chapter_id: String, + pub note: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolveWebNovelReviewNoteInput { + pub note_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveWebNovelMetricInput { + pub work_id: String, + pub metric_date: String, + pub views: i64, + pub follows: i64, + pub comments: i64, + pub paid_readers: i64, + pub revenue_cents: i64, + pub source_label: String, + pub expected_revision: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CheckpointChapter { + chapter_id: String, + title: String, + synopsis: String, + content: String, + workflow_status: String, + scheduled_at_unix_ms: Option, + position: i64, +} + +pub async fn get_web_novel_workspace_snapshot( + app: AppHandle, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&path)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_work( + app: AppHandle, + input: CreateWebNovelWorkInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_work_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn read_web_novel_work( + app: AppHandle, + input: ReadWebNovelWorkInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || read_work_at(&path, &input.work_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn save_web_novel_work( + app: AppHandle, + input: SaveWebNovelWorkInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_work_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_volume( + app: AppHandle, + input: CreateWebNovelVolumeInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_volume_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_chapter( + app: AppHandle, + input: CreateWebNovelChapterInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_chapter_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn read_web_novel_chapter( + app: AppHandle, + input: ReadWebNovelChapterInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || read_chapter_at(&path, &input.chapter_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn save_web_novel_chapter( + app: AppHandle, + input: SaveWebNovelChapterInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_chapter_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn transition_web_novel_chapter( + app: AppHandle, + input: TransitionWebNovelChapterInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || transition_chapter_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_checkpoint( + app: AppHandle, + input: CreateWebNovelCheckpointInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_checkpoint_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn restore_web_novel_checkpoint( + app: AppHandle, + input: RestoreWebNovelCheckpointInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || restore_checkpoint_at(&path, &input.checkpoint_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_story_entity( + app: AppHandle, + input: UpsertWebNovelStoryEntityInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_entity_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_story_relation( + app: AppHandle, + input: CreateWebNovelStoryRelationInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_relation_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn upsert_web_novel_foreshadow( + app: AppHandle, + input: UpsertWebNovelForeshadowInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || upsert_foreshadow_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn create_web_novel_review_note( + app: AppHandle, + input: CreateWebNovelReviewNoteInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || create_review_note_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn resolve_web_novel_review_note( + app: AppHandle, + input: ResolveWebNovelReviewNoteInput, +) -> Result<(), String> { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || resolve_review_note_at(&path, &input.note_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn save_web_novel_metric( + app: AppHandle, + input: SaveWebNovelMetricInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || save_metric_at(&path, input)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn run_web_novel_continuity_audit( + app: AppHandle, + input: ReadWebNovelWorkInput, +) -> Result { + require_active(&app)?; + let path = workspace_database(&app)?; + tauri::async_runtime::spawn_blocking(move || continuity_audit_at(&path, &input.work_id)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? +} + +pub async fn export_web_novel_markdown( + app: AppHandle, + input: ReadWebNovelWorkInput, +) -> Result, String> { + require_active(&app)?; + let database = workspace_database(&app)?; + let detail = read_work_at(&database, &input.work_id)?; + let filename = format!("{}.md", safe_filename(&detail.work.title)); + let selected = app + .dialog() + .file() + .set_title("导出网文作品") + .set_file_name(filename) + .add_filter("Markdown", &["md"]) + .blocking_save_file(); + let Some(selected) = selected else { + return Ok(None); + }; + let path = selected + .into_path() + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_EXPORT_PATH_INVALID: {error}"))?; + tauri::async_runtime::spawn_blocking(move || export_markdown_to_path(&database, &detail, &path)) + .await + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_JOIN_FAILED: {error}"))? + .map(Some) +} + +pub(crate) fn workspace_database(app: &AppHandle) -> Result { + let root = crate::authenticated_storage::account_storage_root(app, "web-novel-workspace-v1")?; + create_private_directory(&root)?; + let database = root.join("web-novel-workspace.sqlite3"); + migrate_acceptance_workspace_if_needed(app, &root, &database)?; + Ok(database) +} + +fn migrate_acceptance_workspace_if_needed( + app: &AppHandle, + current_namespace_root: &Path, + current_database: &Path, +) -> Result<(), String> { + if current_database.exists() { + return Ok(()); + } + let current_app_data = app + .path() + .app_data_dir() + .map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?; + let Some(app_data_parent) = current_app_data.parent() else { + return Ok(()); + }; + let Some(account_root) = current_namespace_root.parent() else { + return Ok(()); + }; + let relative_account = match account_root.strip_prefix(¤t_app_data) { + Ok(value) => value, + Err(_) => return Ok(()), + }; + let legacy_account = app_data_parent + .join("world.guanghu.hololake.webnovel.acceptance") + .join(relative_account); + let legacy_database = legacy_account + .join("web-novel-workspace-v1") + .join("web-novel-workspace.sqlite3"); + if !legacy_database.is_file() { + return Ok(()); + } + let temporary = current_database.with_extension("sqlite3.migrating"); + fs::copy(&legacy_database, &temporary) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MIGRATION_COPY_FAILED: {error}"))?; + let validation = (|| -> Result<(), String> { + let connection = open_database(&temporary)?; + let integrity: String = connection + .query_row("PRAGMA integrity_check", [], |row| row.get(0)) + .map_err(database_read_error)?; + if integrity != "ok" { + return Err("HOLOLAKE_WEBNOVEL_LEGACY_MIGRATION_INTEGRITY_FAILED".into()); + } + let _: i64 = connection + .query_row("SELECT COUNT(*) FROM web_novel_works", [], |row| row.get(0)) + .map_err(database_read_error)?; + Ok(()) + })(); + if let Err(error) = validation { + let _ = fs::remove_file(&temporary); + return Err(error); + } + fs::rename(&temporary, current_database) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MIGRATION_COMMIT_FAILED: {error}"))?; + + let legacy_module_cache = legacy_account.join("web-novel-modules-v1"); + let current_module_cache = account_root.join("web-novel-modules-v1"); + if legacy_module_cache.is_dir() && !current_module_cache.exists() { + copy_directory_without_overwrite(&legacy_module_cache, ¤t_module_cache)?; + } + fs::write( + account_root.join("web-novel-acceptance-migration-v1.json"), + br#"{"schema":"hololake.web-novel-acceptance-migration/v1","state":"MIGRATED_AND_SQLITE_INTEGRITY_VERIFIED","source_identifier":"world.guanghu.hololake.webnovel.acceptance"}"#, + ) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MIGRATION_RECEIPT_FAILED: {error}"))?; + Ok(()) +} + +fn copy_directory_without_overwrite(source: &Path, destination: &Path) -> Result<(), String> { + fs::create_dir_all(destination) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MODULE_COPY_FAILED: {error}"))?; + for entry in fs::read_dir(source) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MODULE_READ_FAILED: {error}"))? + { + let entry = entry + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MODULE_READ_FAILED: {error}"))?; + let target = destination.join(entry.file_name()); + if entry.path().is_dir() { + copy_directory_without_overwrite(&entry.path(), &target)?; + } else if !target.exists() { + fs::copy(entry.path(), target) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_LEGACY_MODULE_COPY_FAILED: {error}"))?; + } + } + Ok(()) +} + +fn create_private_directory(path: &Path) -> Result<(), String> { + fs::create_dir_all(path) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_STORAGE_UNAVAILABLE: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_STORAGE_PERMISSION_FAILED: {error}"))?; + } + Ok(()) +} + +pub(crate) fn open_database(path: &Path) -> Result { + if let Some(parent) = path.parent() { + create_private_directory(parent)?; + } + let connection = Connection::open(path) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_DATABASE_UNAVAILABLE: {error}"))?; + connection + .busy_timeout(Duration::from_secs(5)) + .map_err(database_read_error)?; + connection + .execute_batch( + "PRAGMA foreign_keys = ON; + PRAGMA journal_mode = DELETE; + PRAGMA synchronous = FULL; + PRAGMA trusted_schema = OFF; + CREATE TABLE IF NOT EXISTS web_novel_meta( + key TEXT PRIMARY KEY NOT NULL, + value TEXT NOT NULL + ); + INSERT OR IGNORE INTO web_novel_meta(key, value) VALUES('schema_version', '1'); + CREATE TABLE IF NOT EXISTS web_novel_works( + work_id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + pen_name TEXT NOT NULL, + genre TEXT NOT NULL, + synopsis TEXT NOT NULL, + contract_status TEXT NOT NULL, + copyright_status TEXT NOT NULL, + workflow_status TEXT NOT NULL, + target_words INTEGER NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS web_novel_volumes( + volume_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + title TEXT NOT NULL, + position INTEGER NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), + UNIQUE(work_id, position) + ); + CREATE TABLE IF NOT EXISTS web_novel_chapters( + chapter_id TEXT PRIMARY KEY NOT NULL, + volume_id TEXT NOT NULL REFERENCES web_novel_volumes(volume_id) ON DELETE CASCADE, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + title TEXT NOT NULL, + position INTEGER NOT NULL, + synopsis TEXT NOT NULL, + content TEXT NOT NULL, + workflow_status TEXT NOT NULL, + scheduled_at_unix_ms INTEGER, + word_count INTEGER NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)), + UNIQUE(volume_id, position) + ); + CREATE TABLE IF NOT EXISTS web_novel_chapter_versions( + version_id TEXT PRIMARY KEY NOT NULL, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + revision INTEGER NOT NULL, + title TEXT NOT NULL, + synopsis TEXT NOT NULL, + content TEXT NOT NULL, + workflow_status TEXT NOT NULL, + word_count INTEGER NOT NULL, + save_reason TEXT NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + UNIQUE(chapter_id, revision) + ); + CREATE TABLE IF NOT EXISTS web_novel_entities( + entity_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + entity_type TEXT NOT NULL, + name TEXT NOT NULL, + aliases_json TEXT NOT NULL, + description TEXT NOT NULL, + first_chapter_id TEXT REFERENCES web_novel_chapters(chapter_id) ON DELETE SET NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS web_novel_relations( + relation_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + source_entity_id TEXT NOT NULL REFERENCES web_novel_entities(entity_id) ON DELETE CASCADE, + target_entity_id TEXT NOT NULL REFERENCES web_novel_entities(entity_id) ON DELETE CASCADE, + relation_type TEXT NOT NULL, + note TEXT NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS web_novel_foreshadows( + foreshadow_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + title TEXT NOT NULL, + setup_chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + payoff_chapter_id TEXT REFERENCES web_novel_chapters(chapter_id) ON DELETE SET NULL, + status TEXT NOT NULL, + note TEXT NOT NULL, + revision INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0,1)) + ); + CREATE TABLE IF NOT EXISTS web_novel_review_notes( + note_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + note TEXT NOT NULL, + status TEXT NOT NULL, + created_at_unix_ms INTEGER NOT NULL, + resolved_at_unix_ms INTEGER + ); + CREATE TABLE IF NOT EXISTS web_novel_workflow_events( + event_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + chapter_id TEXT NOT NULL REFERENCES web_novel_chapters(chapter_id) ON DELETE CASCADE, + from_status TEXT NOT NULL, + to_status TEXT NOT NULL, + note TEXT NOT NULL, + created_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_checkpoints( + checkpoint_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT NOT NULL, + payload_json TEXT NOT NULL, + chapter_count INTEGER NOT NULL, + word_count INTEGER NOT NULL, + created_at_unix_ms INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS web_novel_metrics( + metric_id TEXT PRIMARY KEY NOT NULL, + work_id TEXT NOT NULL REFERENCES web_novel_works(work_id) ON DELETE CASCADE, + metric_date TEXT NOT NULL, + views INTEGER NOT NULL, + follows INTEGER NOT NULL, + comments INTEGER NOT NULL, + paid_readers INTEGER NOT NULL, + revenue_cents INTEGER NOT NULL, + source_label TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + UNIQUE(work_id, metric_date, source_label) + ); + CREATE INDEX IF NOT EXISTS idx_web_novel_volumes_work ON web_novel_volumes(work_id, position); + CREATE INDEX IF NOT EXISTS idx_web_novel_chapters_work ON web_novel_chapters(work_id, volume_id, position); + CREATE INDEX IF NOT EXISTS idx_web_novel_versions_chapter ON web_novel_chapter_versions(chapter_id, revision DESC); + CREATE INDEX IF NOT EXISTS idx_web_novel_entities_work ON web_novel_entities(work_id, entity_type, name); + CREATE INDEX IF NOT EXISTS idx_web_novel_foreshadows_work ON web_novel_foreshadows(work_id, status); + CREATE INDEX IF NOT EXISTS idx_web_novel_reviews_work ON web_novel_review_notes(work_id, status); + CREATE INDEX IF NOT EXISTS idx_web_novel_metrics_work ON web_novel_metrics(work_id, metric_date DESC);", + ) + .map_err(database_write_error)?; + ensure_column( + &connection, + "web_novel_works", + "work_kind", + "TEXT NOT NULL DEFAULT 'LONG_NOVEL'", + )?; + connection + .execute( + "UPDATE web_novel_works SET work_kind='SHORT_DRAMA' + WHERE genre LIKE '%短剧%' OR EXISTS( + SELECT 1 FROM web_novel_volumes v + WHERE v.work_id=web_novel_works.work_id AND v.title='导入·分集剧本' + )", + [], + ) + .map_err(database_write_error)?; + connection + .execute( + "UPDATE web_novel_works SET work_kind='SHORT_NOVEL' + WHERE work_kind='LONG_NOVEL' AND (genre LIKE '%短篇%' OR genre LIKE '%短故事%')", + [], + ) + .map_err(database_write_error)?; + connection + .execute( + "UPDATE web_novel_meta SET value='2' WHERE key='schema_version'", + [], + ) + .map_err(database_write_error)?; + Ok(connection) +} + +fn snapshot_at(path: &Path) -> Result { + let connection = open_database(path)?; + let mut statement = connection + .prepare( + "SELECT w.work_id, w.title, w.pen_name, w.genre, w.work_kind, w.workflow_status, + w.target_words, w.revision, w.updated_at_unix_ms, + COALESCE(SUM(CASE WHEN c.archived = 0 THEN c.word_count ELSE 0 END), 0), + COUNT(CASE WHEN c.archived = 0 THEN 1 END), + COUNT(CASE WHEN c.archived = 0 AND c.workflow_status = 'PUBLISHED' THEN 1 END) + FROM web_novel_works w + LEFT JOIN web_novel_chapters c ON c.work_id = w.work_id + WHERE w.archived = 0 + GROUP BY w.work_id + ORDER BY w.updated_at_unix_ms DESC + LIMIT ?1", + ) + .map_err(database_read_error)?; + let works = statement + .query_map([MAX_WORKS], |row| { + Ok(WebNovelWorkSummary { + work_id: row.get(0)?, + title: row.get(1)?, + pen_name: row.get(2)?, + genre: row.get(3)?, + work_kind: row.get(4)?, + workflow_status: row.get(5)?, + target_words: row.get(6)?, + revision: row.get(7)?, + updated_at_unix_ms: row.get(8)?, + total_words: row.get(9)?, + chapter_count: row.get(10)?, + published_chapter_count: row.get(11)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(WebNovelWorkspaceSnapshot { + state: "READY", + work_count: works.len(), + works, + storage: "AUTHENTICATED_ACCOUNT_SCOPED_SQLITE", + authority: "CURRENT_AUTHENTICATED_ACCOUNT_ONLY", + }) +} + +pub(crate) fn create_work_at( + path: &Path, + input: CreateWebNovelWorkInput, +) -> Result { + let title = bounded_text(&input.title, "未命名作品", MAX_TITLE_BYTES)?; + let pen_name = bounded_text(&input.pen_name, "未设置笔名", MAX_TITLE_BYTES)?; + let genre = bounded_text(&input.genre, "未分类", MAX_TITLE_BYTES)?; + let work_kind = normalized_work_kind(&input.work_kind)?; + let mut connection = open_database(path)?; + let count: i64 = connection + .query_row( + "SELECT COUNT(*) FROM web_novel_works WHERE archived = 0", + [], + |row| row.get(0), + ) + .map_err(database_read_error)?; + if count >= MAX_WORKS { + return Err("HOLOLAKE_WEBNOVEL_WORK_LIMIT_REACHED".into()); + } + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let work_id = new_id("WN-WORK-"); + let volume_id = new_id("WN-VOL-"); + let now = now_ms(); + tx.execute( + "INSERT INTO web_novel_works( + work_id, title, pen_name, genre, work_kind, synopsis, contract_status, copyright_status, + workflow_status, target_words, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, ?5, '', 'UNREGISTERED', 'AUTHOR_OWNED', 'DRAFT', 0, 1, ?6, ?6, 0)", + params![work_id, title, pen_name, genre, work_kind, now], + ) + .map_err(database_write_error)?; + tx.execute( + "INSERT INTO web_novel_volumes( + volume_id, work_id, title, position, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, '第一卷', 0, 1, ?3, ?3, 0)", + params![volume_id, work_id, now], + ) + .map_err(database_write_error)?; + tx.commit().map_err(database_write_error)?; + read_work_at(path, &work_id) +} + +fn read_work_at(path: &Path, work_id: &str) -> Result { + validate_id(work_id, "WN-WORK-")?; + let connection = open_database(path)?; + read_work_with_connection(&connection, work_id) +} + +fn read_work_with_connection( + connection: &Connection, + work_id: &str, +) -> Result { + let work = connection + .query_row( + "SELECT work_id, title, pen_name, genre, work_kind, synopsis, contract_status, + copyright_status, workflow_status, target_words, revision, + created_at_unix_ms, updated_at_unix_ms + FROM web_novel_works WHERE work_id = ?1 AND archived = 0", + [work_id], + map_work, + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".to_string())?; + let volumes = query_volumes(connection, work_id)?; + let chapters = query_chapter_summaries(connection, work_id)?; + let entities = query_entities(connection, work_id)?; + let relations = query_relations(connection, work_id)?; + let foreshadows = query_foreshadows(connection, work_id)?; + let review_notes = query_review_notes(connection, work_id)?; + let workflow_events = query_workflow_events(connection, work_id)?; + let checkpoints = query_checkpoints(connection, work_id)?; + let metrics = query_metrics(connection, work_id)?; + let operations = operations_summary(connection, work_id)?; + Ok(WebNovelWorkDetail { + work, + volumes, + chapters, + entities, + relations, + foreshadows, + review_notes, + workflow_events, + checkpoints, + metrics, + operations, + }) +} + +fn save_work_at(path: &Path, input: SaveWebNovelWorkInput) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + let title = bounded_text(&input.title, "未命名作品", MAX_TITLE_BYTES)?; + let pen_name = bounded_text(&input.pen_name, "未设置笔名", MAX_TITLE_BYTES)?; + let genre = bounded_text(&input.genre, "未分类", MAX_TITLE_BYTES)?; + let work_kind = normalized_work_kind(&input.work_kind)?; + validate_optional_bytes(&input.synopsis, MAX_SYNOPSIS_BYTES, "WORK_SYNOPSIS")?; + if input.target_words < 0 || input.target_words > 100_000_000 { + return Err("HOLOLAKE_WEBNOVEL_TARGET_WORDS_INVALID".into()); + } + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE web_novel_works + SET title=?1, pen_name=?2, genre=?3, work_kind=?4, synopsis=?5, contract_status=?6, + copyright_status=?7, target_words=?8, revision=revision+1, updated_at_unix_ms=?9 + WHERE work_id=?10 AND revision=?11 AND archived=0", + params![ + title, + pen_name, + genre, + work_kind, + input.synopsis, + normalized_label(&input.contract_status, "UNREGISTERED")?, + normalized_label(&input.copyright_status, "AUTHOR_OWNED")?, + input.target_words, + now_ms(), + input.work_id, + input.expected_revision + ], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_WEBNOVEL_WORK_REVISION_CONFLICT".into()); + } + read_work_at(path, &input.work_id) +} + +fn create_volume_at( + path: &Path, + input: CreateWebNovelVolumeInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + let title = bounded_text(&input.title, "未命名分卷", MAX_TITLE_BYTES)?; + let connection = open_database(path)?; + ensure_work_exists(&connection, &input.work_id)?; + let position: i64 = connection + .query_row( + "SELECT COALESCE(MAX(position), -1) + 1 FROM web_novel_volumes WHERE work_id=?1 AND archived=0", + [&input.work_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + let now = now_ms(); + connection + .execute( + "INSERT INTO web_novel_volumes( + volume_id, work_id, title, position, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, 1, ?5, ?5, 0)", + params![new_id("WN-VOL-"), input.work_id, title, position, now], + ) + .map_err(database_write_error)?; + touch_work(&connection, &input.work_id)?; + read_work_at(path, &input.work_id) +} + +pub(crate) fn create_chapter_at( + path: &Path, + input: CreateWebNovelChapterInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + validate_id(&input.volume_id, "WN-VOL-")?; + let title = bounded_text(&input.title, "未命名章节", MAX_TITLE_BYTES)?; + let mut connection = open_database(path)?; + let volume_work: Option = connection + .query_row( + "SELECT work_id FROM web_novel_volumes WHERE volume_id=?1 AND archived=0", + [&input.volume_id], + |row| row.get(0), + ) + .optional() + .map_err(database_read_error)?; + if volume_work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_VOLUME_WORK_MISMATCH".into()); + } + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let position: i64 = tx + .query_row( + "SELECT COALESCE(MAX(position), -1) + 1 FROM web_novel_chapters WHERE volume_id=?1 AND archived=0", + [&input.volume_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + let chapter_id = new_id("WN-CH-"); + let now = now_ms(); + let work_kind: String = tx + .query_row( + "SELECT work_kind FROM web_novel_works WHERE work_id=?1 AND archived=0", + [&input.work_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + let initial_content = chapter_template(&work_kind, &title, position); + let initial_word_count = count_words(&initial_content); + tx.execute( + "INSERT INTO web_novel_chapters( + chapter_id, volume_id, work_id, title, position, synopsis, content, + workflow_status, scheduled_at_unix_ms, word_count, revision, + created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, ?5, '', ?6, 'DRAFT', NULL, ?7, 1, ?8, ?8, 0)", + params![ + chapter_id, + input.volume_id, + input.work_id, + title, + position, + initial_content, + initial_word_count, + now + ], + ) + .map_err(database_write_error)?; + insert_chapter_version( + &tx, + &chapter_id, + &input.work_id, + 1, + &title, + "", + &initial_content, + "DRAFT", + initial_word_count, + "CREATE", + now, + )?; + touch_work(&tx, &input.work_id)?; + tx.commit().map_err(database_write_error)?; + read_chapter_at(path, &chapter_id) +} + +fn read_chapter_at(path: &Path, chapter_id: &str) -> Result { + validate_id(chapter_id, "WN-CH-")?; + let connection = open_database(path)?; + read_chapter_with_connection(&connection, chapter_id) +} + +fn read_chapter_with_connection( + connection: &Connection, + chapter_id: &str, +) -> Result { + connection + .query_row( + "SELECT chapter_id, volume_id, work_id, title, position, synopsis, content, + workflow_status, scheduled_at_unix_ms, word_count, revision, + created_at_unix_ms, updated_at_unix_ms + FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [chapter_id], + map_chapter, + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_CHAPTER_NOT_FOUND".to_string()) +} + +pub(crate) fn save_chapter_at( + path: &Path, + input: SaveWebNovelChapterInput, +) -> Result { + validate_id(&input.chapter_id, "WN-CH-")?; + let title = bounded_text(&input.title, "未命名章节", MAX_TITLE_BYTES)?; + validate_optional_bytes(&input.synopsis, MAX_SYNOPSIS_BYTES, "CHAPTER_SYNOPSIS")?; + validate_optional_bytes(&input.content, MAX_CHAPTER_BYTES, "CHAPTER_CONTENT")?; + let word_count = count_words(&input.content); + let mut connection = open_database(path)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let current = read_chapter_with_connection(&tx, &input.chapter_id)?; + if current.revision != input.expected_revision { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_REVISION_CONFLICT".into()); + } + if current.workflow_status == "PUBLISHED" { + return Err("HOLOLAKE_WEBNOVEL_PUBLISHED_CHAPTER_LOCKED".into()); + } + let revision = current.revision + 1; + let now = now_ms(); + let changed = tx + .execute( + "UPDATE web_novel_chapters + SET title=?1, synopsis=?2, content=?3, word_count=?4, revision=?5, updated_at_unix_ms=?6 + WHERE chapter_id=?7 AND revision=?8 AND archived=0", + params![ + title, + input.synopsis, + input.content, + word_count, + revision, + now, + input.chapter_id, + current.revision + ], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_REVISION_CONFLICT".into()); + } + insert_chapter_version( + &tx, + ¤t.chapter_id, + ¤t.work_id, + revision, + &title, + &input.synopsis, + &input.content, + ¤t.workflow_status, + word_count, + &normalized_save_reason(input.save_reason), + now, + )?; + touch_work(&tx, ¤t.work_id)?; + tx.commit().map_err(database_write_error)?; + read_chapter_at(path, &input.chapter_id) +} + +fn transition_chapter_at( + path: &Path, + input: TransitionWebNovelChapterInput, +) -> Result { + validate_id(&input.chapter_id, "WN-CH-")?; + let to_status = normalized_workflow(&input.to_status)?; + validate_optional_bytes(&input.note, MAX_REVIEW_NOTE_BYTES, "WORKFLOW_NOTE")?; + let mut connection = open_database(path)?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let current = read_chapter_with_connection(&tx, &input.chapter_id)?; + if current.revision != input.expected_revision { + return Err("HOLOLAKE_WEBNOVEL_CHAPTER_REVISION_CONFLICT".into()); + } + if !transition_allowed(¤t.workflow_status, &to_status) { + return Err(format!( + "HOLOLAKE_WEBNOVEL_WORKFLOW_TRANSITION_INVALID:{}->{}", + current.workflow_status, to_status + )); + } + if matches!(to_status.as_str(), "APPROVED" | "SCHEDULED" | "PUBLISHED") + && current.content.trim().is_empty() + { + return Err("HOLOLAKE_WEBNOVEL_EMPTY_CHAPTER_CANNOT_ADVANCE".into()); + } + if to_status == "SCHEDULED" && input.scheduled_at_unix_ms.is_none() { + return Err("HOLOLAKE_WEBNOVEL_SCHEDULE_TIME_REQUIRED".into()); + } + let scheduled = if to_status == "SCHEDULED" { + input.scheduled_at_unix_ms + } else { + None + }; + let revision = current.revision + 1; + let now = now_ms(); + tx.execute( + "UPDATE web_novel_chapters + SET workflow_status=?1, scheduled_at_unix_ms=?2, revision=?3, updated_at_unix_ms=?4 + WHERE chapter_id=?5 AND revision=?6", + params![ + to_status, + scheduled, + revision, + now, + current.chapter_id, + current.revision + ], + ) + .map_err(database_write_error)?; + tx.execute( + "INSERT INTO web_novel_workflow_events( + event_id, work_id, chapter_id, from_status, to_status, note, created_at_unix_ms + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + new_id("WN-EVENT-"), + current.work_id, + current.chapter_id, + current.workflow_status, + to_status, + input.note, + now + ], + ) + .map_err(database_write_error)?; + insert_chapter_version( + &tx, + ¤t.chapter_id, + ¤t.work_id, + revision, + ¤t.title, + ¤t.synopsis, + ¤t.content, + &to_status, + current.word_count, + "WORKFLOW_TRANSITION", + now, + )?; + touch_work(&tx, ¤t.work_id)?; + tx.commit().map_err(database_write_error)?; + read_chapter_at(path, &input.chapter_id) +} + +fn create_checkpoint_at( + path: &Path, + input: CreateWebNovelCheckpointInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + let name = bounded_text(&input.name, "手动检查点", MAX_TITLE_BYTES)?; + validate_optional_bytes( + &input.description, + MAX_REVIEW_NOTE_BYTES, + "CHECKPOINT_DESCRIPTION", + )?; + let connection = open_database(path)?; + ensure_work_exists(&connection, &input.work_id)?; + let mut statement = connection + .prepare( + "SELECT chapter_id, title, synopsis, content, workflow_status, + scheduled_at_unix_ms, position + FROM web_novel_chapters + WHERE work_id=?1 AND archived=0 + ORDER BY volume_id, position", + ) + .map_err(database_read_error)?; + let chapters = statement + .query_map([&input.work_id], |row| { + Ok(CheckpointChapter { + chapter_id: row.get(0)?, + title: row.get(1)?, + synopsis: row.get(2)?, + content: row.get(3)?, + workflow_status: row.get(4)?, + scheduled_at_unix_ms: row.get(5)?, + position: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + let payload_json = serde_json::to_string(&chapters) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_CHECKPOINT_SERIALIZE_FAILED: {error}"))?; + let checkpoint_id = new_id("WN-CP-"); + let word_count = chapters + .iter() + .map(|chapter| count_words(&chapter.content)) + .sum(); + let now = now_ms(); + connection + .execute( + "INSERT INTO web_novel_checkpoints( + checkpoint_id, work_id, name, description, payload_json, + chapter_count, word_count, created_at_unix_ms + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + checkpoint_id, + input.work_id, + name, + input.description, + payload_json, + chapters.len() as i64, + word_count, + now + ], + ) + .map_err(database_write_error)?; + Ok(WebNovelCheckpointSummary { + checkpoint_id, + work_id: input.work_id, + name, + description: input.description, + chapter_count: chapters.len() as i64, + word_count, + created_at_unix_ms: now, + }) +} + +fn restore_checkpoint_at(path: &Path, checkpoint_id: &str) -> Result { + validate_id(checkpoint_id, "WN-CP-")?; + let mut connection = open_database(path)?; + let (work_id, payload): (String, String) = connection + .query_row( + "SELECT work_id, payload_json FROM web_novel_checkpoints WHERE checkpoint_id=?1", + [checkpoint_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(database_read_error)? + .ok_or_else(|| "HOLOLAKE_WEBNOVEL_CHECKPOINT_NOT_FOUND".to_string())?; + let chapters: Vec = serde_json::from_str(&payload) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_CHECKPOINT_CORRUPT: {error}"))?; + let tx = connection + .transaction_with_behavior(TransactionBehavior::Immediate) + .map_err(database_write_error)?; + let now = now_ms(); + for chapter in chapters { + let current = read_chapter_with_connection(&tx, &chapter.chapter_id)?; + if current.work_id != work_id { + return Err("HOLOLAKE_WEBNOVEL_CHECKPOINT_SCOPE_MISMATCH".into()); + } + let revision = current.revision + 1; + let word_count = count_words(&chapter.content); + tx.execute( + "UPDATE web_novel_chapters + SET title=?1, synopsis=?2, content=?3, workflow_status=?4, + scheduled_at_unix_ms=?5, position=?6, word_count=?7, + revision=?8, updated_at_unix_ms=?9 + WHERE chapter_id=?10", + params![ + chapter.title, + chapter.synopsis, + chapter.content, + chapter.workflow_status, + chapter.scheduled_at_unix_ms, + chapter.position, + word_count, + revision, + now, + chapter.chapter_id + ], + ) + .map_err(database_write_error)?; + insert_chapter_version( + &tx, + &chapter.chapter_id, + &work_id, + revision, + &chapter.title, + &chapter.synopsis, + &chapter.content, + &chapter.workflow_status, + word_count, + "CHECKPOINT_RESTORE", + now, + )?; + } + touch_work(&tx, &work_id)?; + tx.commit().map_err(database_write_error)?; + read_work_at(path, &work_id) +} + +fn upsert_entity_at( + path: &Path, + input: UpsertWebNovelStoryEntityInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + let name = bounded_text(&input.name, "", MAX_TITLE_BYTES)?; + if name.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_ENTITY_NAME_REQUIRED".into()); + } + let entity_type = normalized_entity_type(&input.entity_type)?; + validate_optional_bytes( + &input.description, + MAX_ENTITY_DESCRIPTION_BYTES, + "ENTITY_DESCRIPTION", + )?; + if input.aliases.len() > 100 + || input + .aliases + .iter() + .any(|alias| alias.len() > MAX_TITLE_BYTES) + { + return Err("HOLOLAKE_WEBNOVEL_ENTITY_ALIASES_INVALID".into()); + } + if let Some(chapter_id) = &input.first_chapter_id { + validate_id(chapter_id, "WN-CH-")?; + } + let aliases_json = serde_json::to_string(&input.aliases) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_ENTITY_ALIASES_INVALID: {error}"))?; + let connection = open_database(path)?; + ensure_work_exists(&connection, &input.work_id)?; + let now = now_ms(); + let entity_id = input.entity_id.unwrap_or_else(|| new_id("WN-ENTITY-")); + let existing: Option = connection + .query_row( + "SELECT revision FROM web_novel_entities WHERE entity_id=?1 AND archived=0", + [&entity_id], + |row| row.get(0), + ) + .optional() + .map_err(database_read_error)?; + if let Some(current_revision) = existing { + if input.expected_revision != Some(current_revision) { + return Err("HOLOLAKE_WEBNOVEL_ENTITY_REVISION_CONFLICT".into()); + } + connection + .execute( + "UPDATE web_novel_entities + SET entity_type=?1, name=?2, aliases_json=?3, description=?4, + first_chapter_id=?5, revision=revision+1, updated_at_unix_ms=?6 + WHERE entity_id=?7 AND work_id=?8 AND revision=?9", + params![ + entity_type, + name, + aliases_json, + input.description, + input.first_chapter_id, + now, + entity_id, + input.work_id, + current_revision + ], + ) + .map_err(database_write_error)?; + } else { + connection + .execute( + "INSERT INTO web_novel_entities( + entity_id, work_id, entity_type, name, aliases_json, description, + first_chapter_id, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, 1, ?8, ?8, 0)", + params![ + entity_id, + input.work_id, + entity_type, + name, + aliases_json, + input.description, + input.first_chapter_id, + now + ], + ) + .map_err(database_write_error)?; + } + touch_work(&connection, &input.work_id)?; + query_entity(&connection, &entity_id) +} + +fn create_relation_at( + path: &Path, + input: CreateWebNovelStoryRelationInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + validate_id(&input.source_entity_id, "WN-ENTITY-")?; + validate_id(&input.target_entity_id, "WN-ENTITY-")?; + if input.source_entity_id == input.target_entity_id { + return Err("HOLOLAKE_WEBNOVEL_RELATION_SELF_LINK_INVALID".into()); + } + let relation_type = bounded_text(&input.relation_type, "", MAX_TITLE_BYTES)?; + if relation_type.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_RELATION_TYPE_REQUIRED".into()); + } + validate_optional_bytes(&input.note, MAX_REVIEW_NOTE_BYTES, "RELATION_NOTE")?; + let connection = open_database(path)?; + for entity_id in [&input.source_entity_id, &input.target_entity_id] { + let work: Option = connection + .query_row( + "SELECT work_id FROM web_novel_entities WHERE entity_id=?1 AND archived=0", + [entity_id], + |row| row.get(0), + ) + .optional() + .map_err(database_read_error)?; + if work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_RELATION_SCOPE_MISMATCH".into()); + } + } + let relation = WebNovelStoryRelation { + relation_id: new_id("WN-REL-"), + work_id: input.work_id, + source_entity_id: input.source_entity_id, + target_entity_id: input.target_entity_id, + relation_type, + note: input.note, + created_at_unix_ms: now_ms(), + }; + connection + .execute( + "INSERT INTO web_novel_relations( + relation_id, work_id, source_entity_id, target_entity_id, + relation_type, note, created_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, 0)", + params![ + relation.relation_id, + relation.work_id, + relation.source_entity_id, + relation.target_entity_id, + relation.relation_type, + relation.note, + relation.created_at_unix_ms + ], + ) + .map_err(database_write_error)?; + touch_work(&connection, &relation.work_id)?; + Ok(relation) +} + +fn upsert_foreshadow_at( + path: &Path, + input: UpsertWebNovelForeshadowInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + validate_id(&input.setup_chapter_id, "WN-CH-")?; + if let Some(chapter_id) = &input.payoff_chapter_id { + validate_id(chapter_id, "WN-CH-")?; + } + let title = bounded_text(&input.title, "", MAX_TITLE_BYTES)?; + if title.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_FORESHADOW_TITLE_REQUIRED".into()); + } + let status = normalized_foreshadow_status(&input.status)?; + if status == "RESOLVED" && input.payoff_chapter_id.is_none() { + return Err("HOLOLAKE_WEBNOVEL_FORESHADOW_PAYOFF_REQUIRED".into()); + } + validate_optional_bytes(&input.note, MAX_REVIEW_NOTE_BYTES, "FORESHADOW_NOTE")?; + let connection = open_database(path)?; + for chapter_id in + std::iter::once(&input.setup_chapter_id).chain(input.payoff_chapter_id.as_ref()) + { + let work: Option = connection + .query_row( + "SELECT work_id FROM web_novel_chapters WHERE chapter_id=?1 AND archived=0", + [chapter_id], + |row| row.get(0), + ) + .optional() + .map_err(database_read_error)?; + if work.as_deref() != Some(input.work_id.as_str()) { + return Err("HOLOLAKE_WEBNOVEL_FORESHADOW_SCOPE_MISMATCH".into()); + } + } + let foreshadow_id = input + .foreshadow_id + .unwrap_or_else(|| new_id("WN-FORESHADOW-")); + let existing: Option = connection + .query_row( + "SELECT revision FROM web_novel_foreshadows WHERE foreshadow_id=?1 AND archived=0", + [&foreshadow_id], + |row| row.get(0), + ) + .optional() + .map_err(database_read_error)?; + let now = now_ms(); + if let Some(revision) = existing { + if input.expected_revision != Some(revision) { + return Err("HOLOLAKE_WEBNOVEL_FORESHADOW_REVISION_CONFLICT".into()); + } + connection + .execute( + "UPDATE web_novel_foreshadows + SET title=?1, setup_chapter_id=?2, payoff_chapter_id=?3, status=?4, + note=?5, revision=revision+1, updated_at_unix_ms=?6 + WHERE foreshadow_id=?7 AND work_id=?8 AND revision=?9", + params![ + title, + input.setup_chapter_id, + input.payoff_chapter_id, + status, + input.note, + now, + foreshadow_id, + input.work_id, + revision + ], + ) + .map_err(database_write_error)?; + } else { + connection + .execute( + "INSERT INTO web_novel_foreshadows( + foreshadow_id, work_id, title, setup_chapter_id, payoff_chapter_id, + status, note, revision, created_at_unix_ms, updated_at_unix_ms, archived + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, 1, ?8, ?8, 0)", + params![ + foreshadow_id, + input.work_id, + title, + input.setup_chapter_id, + input.payoff_chapter_id, + status, + input.note, + now + ], + ) + .map_err(database_write_error)?; + } + touch_work(&connection, &input.work_id)?; + query_foreshadow(&connection, &foreshadow_id) +} + +fn create_review_note_at( + path: &Path, + input: CreateWebNovelReviewNoteInput, +) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + validate_id(&input.chapter_id, "WN-CH-")?; + let note = bounded_text(&input.note, "", MAX_REVIEW_NOTE_BYTES)?; + if note.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_REVIEW_NOTE_REQUIRED".into()); + } + let connection = open_database(path)?; + let chapter = read_chapter_with_connection(&connection, &input.chapter_id)?; + if chapter.work_id != input.work_id { + return Err("HOLOLAKE_WEBNOVEL_REVIEW_SCOPE_MISMATCH".into()); + } + let result = WebNovelReviewNote { + note_id: new_id("WN-NOTE-"), + work_id: input.work_id, + chapter_id: input.chapter_id, + note, + status: "OPEN".into(), + created_at_unix_ms: now_ms(), + resolved_at_unix_ms: None, + }; + connection + .execute( + "INSERT INTO web_novel_review_notes( + note_id, work_id, chapter_id, note, status, created_at_unix_ms, resolved_at_unix_ms + ) VALUES(?1, ?2, ?3, ?4, 'OPEN', ?5, NULL)", + params![ + result.note_id, + result.work_id, + result.chapter_id, + result.note, + result.created_at_unix_ms + ], + ) + .map_err(database_write_error)?; + touch_work(&connection, &result.work_id)?; + Ok(result) +} + +fn resolve_review_note_at(path: &Path, note_id: &str) -> Result<(), String> { + validate_id(note_id, "WN-NOTE-")?; + let connection = open_database(path)?; + let changed = connection + .execute( + "UPDATE web_novel_review_notes + SET status='RESOLVED', resolved_at_unix_ms=?1 + WHERE note_id=?2 AND status='OPEN'", + params![now_ms(), note_id], + ) + .map_err(database_write_error)?; + if changed != 1 { + return Err("HOLOLAKE_WEBNOVEL_REVIEW_NOTE_NOT_OPEN".into()); + } + Ok(()) +} + +fn save_metric_at(path: &Path, input: SaveWebNovelMetricInput) -> Result { + validate_id(&input.work_id, "WN-WORK-")?; + validate_metric_date(&input.metric_date)?; + let source_label = bounded_text(&input.source_label, "", MAX_TITLE_BYTES)?; + if source_label.is_empty() { + return Err("HOLOLAKE_WEBNOVEL_METRIC_SOURCE_REQUIRED".into()); + } + for value in [ + input.views, + input.follows, + input.comments, + input.paid_readers, + input.revenue_cents, + ] { + if value < 0 { + return Err("HOLOLAKE_WEBNOVEL_METRIC_NEGATIVE".into()); + } + } + let connection = open_database(path)?; + ensure_work_exists(&connection, &input.work_id)?; + let existing: Option<(String, i64)> = connection + .query_row( + "SELECT metric_id, revision FROM web_novel_metrics + WHERE work_id=?1 AND metric_date=?2 AND source_label=?3", + params![input.work_id, input.metric_date, source_label], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional() + .map_err(database_read_error)?; + let now = now_ms(); + let metric_id; + if let Some((existing_id, revision)) = existing { + if input.expected_revision != Some(revision) { + return Err("HOLOLAKE_WEBNOVEL_METRIC_REVISION_CONFLICT".into()); + } + metric_id = existing_id; + connection + .execute( + "UPDATE web_novel_metrics + SET views=?1, follows=?2, comments=?3, paid_readers=?4, + revenue_cents=?5, revision=revision+1, updated_at_unix_ms=?6 + WHERE metric_id=?7 AND revision=?8", + params![ + input.views, + input.follows, + input.comments, + input.paid_readers, + input.revenue_cents, + now, + metric_id, + revision + ], + ) + .map_err(database_write_error)?; + } else { + metric_id = new_id("WN-METRIC-"); + connection + .execute( + "INSERT INTO web_novel_metrics( + metric_id, work_id, metric_date, views, follows, comments, + paid_readers, revenue_cents, source_label, revision, updated_at_unix_ms + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1, ?10)", + params![ + metric_id, + input.work_id, + input.metric_date, + input.views, + input.follows, + input.comments, + input.paid_readers, + input.revenue_cents, + source_label, + now + ], + ) + .map_err(database_write_error)?; + } + query_metric(&connection, &metric_id) +} + +fn continuity_audit_at(path: &Path, work_id: &str) -> Result { + let detail = read_work_at(path, work_id)?; + let connection = open_database(path)?; + let mut issues = Vec::new(); + for chapter in &detail.chapters { + if chapter.word_count == 0 { + issues.push(issue( + "WARNING", + "EMPTY_CHAPTER", + "章节正文为空", + format!("「{}」尚无正文。", chapter.title), + &chapter.chapter_id, + )); + } + if matches!( + chapter.workflow_status.as_str(), + "APPROVED" | "SCHEDULED" | "PUBLISHED" + ) && chapter.word_count == 0 + { + issues.push(issue( + "BLOCKING", + "ADVANCED_EMPTY_CHAPTER", + "已推进的章节没有正文", + format!("「{}」不能保持当前审稿/发布状态。", chapter.title), + &chapter.chapter_id, + )); + } + let duplicate_position: i64 = connection + .query_row( + "SELECT COUNT(*) FROM web_novel_chapters + WHERE volume_id=?1 AND position=?2 AND archived=0", + params![chapter.volume_id, chapter.position], + |row| row.get(0), + ) + .map_err(database_read_error)?; + if duplicate_position > 1 { + issues.push(issue( + "BLOCKING", + "DUPLICATE_CHAPTER_POSITION", + "章节顺序冲突", + format!("「{}」所在分卷存在重复章节序号。", chapter.title), + &chapter.chapter_id, + )); + } + } + for entity in &detail.entities { + if entity.description.trim().is_empty() { + issues.push(issue( + "WARNING", + "ENTITY_DESCRIPTION_EMPTY", + "设定卡缺少说明", + format!("「{}」还没有人物/地点/设定说明。", entity.name), + &entity.entity_id, + )); + } + } + for foreshadow in &detail.foreshadows { + if foreshadow.status == "OPEN" { + issues.push(issue( + "WARNING", + "FORESHADOW_OPEN", + "伏笔尚未回收", + format!("「{}」仍处于待回收状态。", foreshadow.title), + &foreshadow.foreshadow_id, + )); + } + if let Some(payoff_id) = &foreshadow.payoff_chapter_id { + let setup_order = chapter_global_order(&detail, &foreshadow.setup_chapter_id); + let payoff_order = chapter_global_order(&detail, payoff_id); + if setup_order.is_some() && payoff_order.is_some() && payoff_order <= setup_order { + issues.push(issue( + "BLOCKING", + "FORESHADOW_PAYOFF_BEFORE_SETUP", + "伏笔回收早于埋设", + format!("「{}」的回收章节顺序不晚于埋设章节。", foreshadow.title), + &foreshadow.foreshadow_id, + )); + } + } + } + let mut names = std::collections::HashMap::>::new(); + for entity in &detail.entities { + names + .entry(entity.name.trim().to_lowercase()) + .or_default() + .push(entity); + } + for duplicated in names.values().filter(|items| items.len() > 1) { + issues.push(issue( + "WARNING", + "DUPLICATE_ENTITY_NAME", + "设定卡名称重复", + format!( + "「{}」存在 {} 张同名设定卡,请确认是否为同一对象。", + duplicated[0].name, + duplicated.len() + ), + &duplicated[0].entity_id, + )); + } + let blocking_issue_count = issues + .iter() + .filter(|item| item.severity == "BLOCKING") + .count(); + let warning_issue_count = issues + .iter() + .filter(|item| item.severity == "WARNING") + .count(); + Ok(WebNovelContinuityAudit { + state: if blocking_issue_count > 0 { + "BLOCKED" + } else if warning_issue_count > 0 { + "REVIEW_REQUIRED" + } else { + "PASS" + }, + work_id: work_id.to_string(), + issue_count: issues.len(), + blocking_issue_count, + warning_issue_count, + issues, + checked_at_unix_ms: now_ms(), + }) +} + +fn export_markdown_to_path( + database: &Path, + detail: &WebNovelWorkDetail, + path: &Path, +) -> Result { + let connection = open_database(database)?; + let mut output = format!( + "# {}\n\n- 笔名:{}\n- 类型:{}\n- 合同:{}\n- 版权:{}\n\n{}\n", + detail.work.title, + detail.work.pen_name, + detail.work.genre, + detail.work.contract_status, + detail.work.copyright_status, + detail.work.synopsis + ); + let mut chapter_count = 0usize; + let mut word_count = 0i64; + for volume in &detail.volumes { + output.push_str(&format!("\n# {}\n", volume.title)); + for summary in detail + .chapters + .iter() + .filter(|chapter| chapter.volume_id == volume.volume_id) + { + let chapter = read_chapter_with_connection(&connection, &summary.chapter_id)?; + output.push_str(&format!("\n## {}\n\n{}\n", chapter.title, chapter.content)); + chapter_count += 1; + word_count += chapter.word_count; + } + } + fs::write(path, output.as_bytes()) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_EXPORT_WRITE_FAILED: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) + .map_err(|error| format!("HOLOLAKE_WEBNOVEL_EXPORT_PERMISSION_FAILED: {error}"))?; + } + Ok(WebNovelExportReceipt { + path: path.to_string_lossy().to_string(), + bytes: output.len() as u64, + chapter_count, + word_count, + }) +} + +fn query_volumes(connection: &Connection, work_id: &str) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT volume_id, work_id, title, position, revision, + created_at_unix_ms, updated_at_unix_ms + FROM web_novel_volumes WHERE work_id=?1 AND archived=0 ORDER BY position", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelVolume { + volume_id: row.get(0)?, + work_id: row.get(1)?, + title: row.get(2)?, + position: row.get(3)?, + revision: row.get(4)?, + created_at_unix_ms: row.get(5)?, + updated_at_unix_ms: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_chapter_summaries( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT c.chapter_id, c.volume_id, c.work_id, c.title, c.position, c.synopsis, + c.workflow_status, c.scheduled_at_unix_ms, c.word_count, c.revision, c.updated_at_unix_ms + FROM web_novel_chapters c + JOIN web_novel_volumes v ON v.volume_id=c.volume_id + WHERE c.work_id=?1 AND c.archived=0 AND v.archived=0 + ORDER BY v.position, c.position", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelChapterSummary { + chapter_id: row.get(0)?, + volume_id: row.get(1)?, + work_id: row.get(2)?, + title: row.get(3)?, + position: row.get(4)?, + synopsis: row.get(5)?, + workflow_status: row.get(6)?, + scheduled_at_unix_ms: row.get(7)?, + word_count: row.get(8)?, + revision: row.get(9)?, + updated_at_unix_ms: row.get(10)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_entities( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT entity_id, work_id, entity_type, name, aliases_json, description, + first_chapter_id, revision, created_at_unix_ms, updated_at_unix_ms + FROM web_novel_entities WHERE work_id=?1 AND archived=0 + ORDER BY entity_type, name", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], map_entity) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_entity(connection: &Connection, entity_id: &str) -> Result { + connection + .query_row( + "SELECT entity_id, work_id, entity_type, name, aliases_json, description, + first_chapter_id, revision, created_at_unix_ms, updated_at_unix_ms + FROM web_novel_entities WHERE entity_id=?1 AND archived=0", + [entity_id], + map_entity, + ) + .map_err(database_read_error) +} + +fn query_relations( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT relation_id, work_id, source_entity_id, target_entity_id, + relation_type, note, created_at_unix_ms + FROM web_novel_relations WHERE work_id=?1 AND archived=0 + ORDER BY created_at_unix_ms DESC", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelStoryRelation { + relation_id: row.get(0)?, + work_id: row.get(1)?, + source_entity_id: row.get(2)?, + target_entity_id: row.get(3)?, + relation_type: row.get(4)?, + note: row.get(5)?, + created_at_unix_ms: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_foreshadows( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT foreshadow_id, work_id, title, setup_chapter_id, payoff_chapter_id, + status, note, revision, created_at_unix_ms, updated_at_unix_ms + FROM web_novel_foreshadows WHERE work_id=?1 AND archived=0 + ORDER BY updated_at_unix_ms DESC", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], map_foreshadow) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_foreshadow( + connection: &Connection, + foreshadow_id: &str, +) -> Result { + connection + .query_row( + "SELECT foreshadow_id, work_id, title, setup_chapter_id, payoff_chapter_id, + status, note, revision, created_at_unix_ms, updated_at_unix_ms + FROM web_novel_foreshadows WHERE foreshadow_id=?1 AND archived=0", + [foreshadow_id], + map_foreshadow, + ) + .map_err(database_read_error) +} + +fn query_review_notes( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT note_id, work_id, chapter_id, note, status, + created_at_unix_ms, resolved_at_unix_ms + FROM web_novel_review_notes WHERE work_id=?1 + ORDER BY status ASC, created_at_unix_ms DESC LIMIT 200", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelReviewNote { + note_id: row.get(0)?, + work_id: row.get(1)?, + chapter_id: row.get(2)?, + note: row.get(3)?, + status: row.get(4)?, + created_at_unix_ms: row.get(5)?, + resolved_at_unix_ms: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_workflow_events( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT event_id, work_id, chapter_id, from_status, to_status, note, created_at_unix_ms + FROM web_novel_workflow_events WHERE work_id=?1 + ORDER BY created_at_unix_ms DESC LIMIT 100", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelWorkflowEvent { + event_id: row.get(0)?, + work_id: row.get(1)?, + chapter_id: row.get(2)?, + from_status: row.get(3)?, + to_status: row.get(4)?, + note: row.get(5)?, + created_at_unix_ms: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_checkpoints( + connection: &Connection, + work_id: &str, +) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT checkpoint_id, work_id, name, description, chapter_count, word_count, created_at_unix_ms + FROM web_novel_checkpoints WHERE work_id=?1 + ORDER BY created_at_unix_ms DESC LIMIT 50", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], |row| { + Ok(WebNovelCheckpointSummary { + checkpoint_id: row.get(0)?, + work_id: row.get(1)?, + name: row.get(2)?, + description: row.get(3)?, + chapter_count: row.get(4)?, + word_count: row.get(5)?, + created_at_unix_ms: row.get(6)?, + }) + }) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_metrics(connection: &Connection, work_id: &str) -> Result, String> { + let mut statement = connection + .prepare( + "SELECT metric_id, work_id, metric_date, views, follows, comments, + paid_readers, revenue_cents, source_label, revision, updated_at_unix_ms + FROM web_novel_metrics WHERE work_id=?1 + ORDER BY metric_date DESC, source_label LIMIT 180", + ) + .map_err(database_read_error)?; + let result = statement + .query_map([work_id], map_metric) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + Ok(result) +} + +fn query_metric(connection: &Connection, metric_id: &str) -> Result { + connection + .query_row( + "SELECT metric_id, work_id, metric_date, views, follows, comments, + paid_readers, revenue_cents, source_label, revision, updated_at_unix_ms + FROM web_novel_metrics WHERE metric_id=?1", + [metric_id], + map_metric, + ) + .map_err(database_read_error) +} + +fn operations_summary( + connection: &Connection, + work_id: &str, +) -> Result { + let chapter_values: (i64, i64, i64, i64, i64, i64) = connection + .query_row( + "SELECT COALESCE(SUM(word_count),0), + COALESCE(SUM(CASE WHEN workflow_status IN ('DRAFT','SELF_REVIEW','REVISION_REQUIRED') THEN 1 ELSE 0 END),0), + COALESCE(SUM(CASE WHEN workflow_status='EDITOR_REVIEW' THEN 1 ELSE 0 END),0), + COALESCE(SUM(CASE WHEN workflow_status='APPROVED' THEN 1 ELSE 0 END),0), + COALESCE(SUM(CASE WHEN workflow_status='SCHEDULED' THEN 1 ELSE 0 END),0), + COALESCE(SUM(CASE WHEN workflow_status='PUBLISHED' THEN 1 ELSE 0 END),0) + FROM web_novel_chapters WHERE work_id=?1 AND archived=0", + [work_id], + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + row.get(5)?, + )) + }, + ) + .map_err(database_read_error)?; + let open_review_notes: i64 = connection + .query_row( + "SELECT COUNT(*) FROM web_novel_review_notes WHERE work_id=?1 AND status='OPEN'", + [work_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + let open_foreshadows: i64 = connection + .query_row( + "SELECT COUNT(*) FROM web_novel_foreshadows + WHERE work_id=?1 AND status='OPEN' AND archived=0", + [work_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + let latest = connection + .query_row( + "SELECT views, follows, comments, paid_readers, revenue_cents + FROM web_novel_metrics WHERE work_id=?1 + ORDER BY metric_date DESC, updated_at_unix_ms DESC LIMIT 1", + [work_id], + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + )) + }, + ) + .optional() + .map_err(database_read_error)? + .unwrap_or((0, 0, 0, 0, 0)); + Ok(WebNovelOperationsSummary { + total_words: chapter_values.0, + draft_chapters: chapter_values.1, + editor_review_chapters: chapter_values.2, + approved_chapters: chapter_values.3, + scheduled_chapters: chapter_values.4, + published_chapters: chapter_values.5, + open_review_notes, + open_foreshadows, + latest_views: latest.0, + latest_follows: latest.1, + latest_comments: latest.2, + latest_paid_readers: latest.3, + latest_revenue_cents: latest.4, + }) +} + +#[allow(clippy::too_many_arguments)] +fn insert_chapter_version( + connection: &Connection, + chapter_id: &str, + work_id: &str, + revision: i64, + title: &str, + synopsis: &str, + content: &str, + workflow_status: &str, + word_count: i64, + save_reason: &str, + created_at: i64, +) -> Result<(), String> { + connection + .execute( + "INSERT INTO web_novel_chapter_versions( + version_id, chapter_id, work_id, revision, title, synopsis, content, + workflow_status, word_count, save_reason, created_at_unix_ms + ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + new_id("WN-VER-"), + chapter_id, + work_id, + revision, + title, + synopsis, + content, + workflow_status, + word_count, + save_reason, + created_at + ], + ) + .map_err(database_write_error)?; + Ok(()) +} + +fn map_work(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(WebNovelWork { + work_id: row.get(0)?, + title: row.get(1)?, + pen_name: row.get(2)?, + genre: row.get(3)?, + work_kind: row.get(4)?, + synopsis: row.get(5)?, + contract_status: row.get(6)?, + copyright_status: row.get(7)?, + workflow_status: row.get(8)?, + target_words: row.get(9)?, + revision: row.get(10)?, + created_at_unix_ms: row.get(11)?, + updated_at_unix_ms: row.get(12)?, + }) +} + +fn map_chapter(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(WebNovelChapter { + chapter_id: row.get(0)?, + volume_id: row.get(1)?, + work_id: row.get(2)?, + title: row.get(3)?, + position: row.get(4)?, + synopsis: row.get(5)?, + content: row.get(6)?, + workflow_status: row.get(7)?, + scheduled_at_unix_ms: row.get(8)?, + word_count: row.get(9)?, + revision: row.get(10)?, + created_at_unix_ms: row.get(11)?, + updated_at_unix_ms: row.get(12)?, + }) +} + +fn map_entity(row: &rusqlite::Row<'_>) -> rusqlite::Result { + let raw: String = row.get(4)?; + let aliases = serde_json::from_str(&raw).map_err(|error| { + rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(error)) + })?; + Ok(WebNovelStoryEntity { + entity_id: row.get(0)?, + work_id: row.get(1)?, + entity_type: row.get(2)?, + name: row.get(3)?, + aliases, + description: row.get(5)?, + first_chapter_id: row.get(6)?, + revision: row.get(7)?, + created_at_unix_ms: row.get(8)?, + updated_at_unix_ms: row.get(9)?, + }) +} + +fn map_foreshadow(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(WebNovelForeshadow { + foreshadow_id: row.get(0)?, + work_id: row.get(1)?, + title: row.get(2)?, + setup_chapter_id: row.get(3)?, + payoff_chapter_id: row.get(4)?, + status: row.get(5)?, + note: row.get(6)?, + revision: row.get(7)?, + created_at_unix_ms: row.get(8)?, + updated_at_unix_ms: row.get(9)?, + }) +} + +fn map_metric(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(WebNovelMetric { + metric_id: row.get(0)?, + work_id: row.get(1)?, + metric_date: row.get(2)?, + views: row.get(3)?, + follows: row.get(4)?, + comments: row.get(5)?, + paid_readers: row.get(6)?, + revenue_cents: row.get(7)?, + source_label: row.get(8)?, + revision: row.get(9)?, + updated_at_unix_ms: row.get(10)?, + }) +} + +fn ensure_work_exists(connection: &Connection, work_id: &str) -> Result<(), String> { + let exists: bool = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM web_novel_works WHERE work_id=?1 AND archived=0)", + [work_id], + |row| row.get(0), + ) + .map_err(database_read_error)?; + if exists { + Ok(()) + } else { + Err("HOLOLAKE_WEBNOVEL_WORK_NOT_FOUND".into()) + } +} + +fn touch_work(connection: &Connection, work_id: &str) -> Result<(), String> { + connection + .execute( + "UPDATE web_novel_works SET updated_at_unix_ms=?1 WHERE work_id=?2", + params![now_ms(), work_id], + ) + .map_err(database_write_error)?; + Ok(()) +} + +fn issue( + severity: &str, + code: &str, + title: &str, + detail: String, + object_id: &str, +) -> WebNovelContinuityIssue { + WebNovelContinuityIssue { + issue_id: new_id("WN-AUDIT-"), + severity: severity.into(), + code: code.into(), + title: title.into(), + detail, + object_id: object_id.into(), + } +} + +fn chapter_global_order(detail: &WebNovelWorkDetail, chapter_id: &str) -> Option<(i64, i64)> { + let chapter = detail + .chapters + .iter() + .find(|chapter| chapter.chapter_id == chapter_id)?; + let volume = detail + .volumes + .iter() + .find(|volume| volume.volume_id == chapter.volume_id)?; + Some((volume.position, chapter.position)) +} + +fn normalized_workflow(value: &str) -> Result { + let normalized = value.trim().to_ascii_uppercase(); + if WORKFLOW_STATES.contains(&normalized.as_str()) { + Ok(normalized) + } else { + Err("HOLOLAKE_WEBNOVEL_WORKFLOW_STATE_INVALID".into()) + } +} + +pub(crate) fn normalized_work_kind(value: &str) -> Result { + let normalized = value.trim().to_ascii_uppercase(); + if matches!( + normalized.as_str(), + "LONG_NOVEL" | "SHORT_NOVEL" | "SHORT_DRAMA" + ) { + Ok(normalized) + } else { + Err("HOLOLAKE_WEBNOVEL_WORK_KIND_INVALID".into()) + } +} + +fn default_work_kind() -> String { + "LONG_NOVEL".into() +} + +fn chapter_template(work_kind: &str, title: &str, position: i64) -> String { + if work_kind != "SHORT_DRAMA" { + return String::new(); + } + let episode = position + 1; + format!( + "{title}\n\n场次:{episode}-1 日/夜 内/外 地点\n人物:\n\n△ 动作与画面\n\n角色:对白\n\n【本场转折】\n【集尾钩子】" + ) +} + +fn ensure_column( + connection: &Connection, + table: &str, + column: &str, + definition: &str, +) -> Result<(), String> { + let mut statement = connection + .prepare(&format!("PRAGMA table_info({table})")) + .map_err(database_read_error)?; + let columns = statement + .query_map([], |row| row.get::<_, String>(1)) + .map_err(database_read_error)? + .collect::, _>>() + .map_err(database_read_error)?; + if !columns.iter().any(|value| value == column) { + connection + .execute_batch(&format!( + "ALTER TABLE {table} ADD COLUMN {column} {definition};" + )) + .map_err(database_write_error)?; + } + Ok(()) +} + +fn transition_allowed(from: &str, to: &str) -> bool { + if from == to { + return false; + } + matches!( + (from, to), + ("DRAFT", "SELF_REVIEW") + | ("SELF_REVIEW", "DRAFT") + | ("SELF_REVIEW", "EDITOR_REVIEW") + | ("EDITOR_REVIEW", "REVISION_REQUIRED") + | ("EDITOR_REVIEW", "APPROVED") + | ("REVISION_REQUIRED", "DRAFT") + | ("REVISION_REQUIRED", "EDITOR_REVIEW") + | ("APPROVED", "REVISION_REQUIRED") + | ("APPROVED", "SCHEDULED") + | ("APPROVED", "PUBLISHED") + | ("SCHEDULED", "APPROVED") + | ("SCHEDULED", "PUBLISHED") + | ("PUBLISHED", "REVISION_REQUIRED") + ) +} + +fn normalized_entity_type(value: &str) -> Result { + let normalized = value.trim().to_ascii_uppercase(); + if matches!( + normalized.as_str(), + "CHARACTER" | "LOCATION" | "WORLD_RULE" | "ITEM" | "ORGANIZATION" | "EVENT" + ) { + Ok(normalized) + } else { + Err("HOLOLAKE_WEBNOVEL_ENTITY_TYPE_INVALID".into()) + } +} + +fn normalized_foreshadow_status(value: &str) -> Result { + let normalized = value.trim().to_ascii_uppercase(); + if matches!(normalized.as_str(), "OPEN" | "RESOLVED" | "DROPPED") { + Ok(normalized) + } else { + Err("HOLOLAKE_WEBNOVEL_FORESHADOW_STATUS_INVALID".into()) + } +} + +fn normalized_label(value: &str, fallback: &str) -> Result { + let normalized = if value.trim().is_empty() { + fallback.to_string() + } else { + value.trim().to_ascii_uppercase() + }; + if normalized.len() > 80 + || !normalized + .chars() + .all(|value| value.is_ascii_uppercase() || value.is_ascii_digit() || value == '_') + { + return Err("HOLOLAKE_WEBNOVEL_STATUS_LABEL_INVALID".into()); + } + Ok(normalized) +} + +fn normalized_save_reason(value: Option) -> String { + let value = value.unwrap_or_else(|| "AUTOSAVE".into()); + let normalized = value.trim().to_ascii_uppercase(); + if normalized.is_empty() || normalized.len() > 80 { + "AUTOSAVE".into() + } else { + normalized + } +} + +fn bounded_text(value: &str, fallback: &str, max_bytes: usize) -> Result { + let normalized = if value.trim().is_empty() { + fallback.trim() + } else { + value.trim() + }; + if normalized.len() > max_bytes { + return Err("HOLOLAKE_WEBNOVEL_TEXT_BOUNDS_INVALID".into()); + } + Ok(normalized.to_string()) +} + +fn validate_optional_bytes(value: &str, max_bytes: usize, label: &str) -> Result<(), String> { + if value.len() > max_bytes { + Err(format!("HOLOLAKE_WEBNOVEL_{label}_BOUNDS_INVALID")) + } else { + Ok(()) + } +} + +fn validate_metric_date(value: &str) -> Result<(), String> { + let bytes = value.as_bytes(); + if bytes.len() == 10 + && bytes[4] == b'-' + && bytes[7] == b'-' + && bytes + .iter() + .enumerate() + .all(|(index, byte)| index == 4 || index == 7 || byte.is_ascii_digit()) + { + Ok(()) + } else { + Err("HOLOLAKE_WEBNOVEL_METRIC_DATE_INVALID".into()) + } +} + +fn validate_id(value: &str, prefix: &str) -> Result<(), String> { + if value.starts_with(prefix) + && value.len() <= prefix.len() + 64 + && value[prefix.len()..] + .chars() + .all(|value| value.is_ascii_alphanumeric() || value == '-') + { + Ok(()) + } else { + Err("HOLOLAKE_WEBNOVEL_ENTITY_ID_INVALID".into()) + } +} + +fn count_words(content: &str) -> i64 { + content + .chars() + .filter(|value| !value.is_whitespace()) + .count() as i64 +} + +fn safe_filename(value: &str) -> String { + let cleaned: String = value + .chars() + .map(|character| { + if matches!( + character, + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' + ) { + '_' + } else { + character + } + }) + .collect(); + if cleaned.trim().is_empty() { + "未命名网文作品".into() + } else { + cleaned.trim().chars().take(80).collect() + } +} + +fn new_id(prefix: &str) -> String { + format!("{prefix}{}", Uuid::new_v4()) +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +fn database_read_error(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_DATABASE_READ_FAILED: {error}") +} + +fn database_write_error(error: rusqlite::Error) -> String { + format!("HOLOLAKE_WEBNOVEL_DATABASE_WRITE_FAILED: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + fn database_path() -> (tempfile::TempDir, PathBuf) { + let temp = tempdir().unwrap(); + let path = temp.path().join("web-novel.sqlite3"); + (temp, path) + } + + #[test] + fn chapter_save_persists_versions_and_rejects_stale_revision() { + let (_temp, path) = database_path(); + let detail = create_work_at( + &path, + CreateWebNovelWorkInput { + title: "测试作品".into(), + pen_name: "冰朔".into(), + genre: "幻想".into(), + work_kind: "LONG_NOVEL".into(), + }, + ) + .unwrap(); + let chapter = create_chapter_at( + &path, + CreateWebNovelChapterInput { + work_id: detail.work.work_id, + volume_id: detail.volumes[0].volume_id.clone(), + title: "第一章".into(), + }, + ) + .unwrap(); + let saved = save_chapter_at( + &path, + SaveWebNovelChapterInput { + chapter_id: chapter.chapter_id.clone(), + title: chapter.title.clone(), + synopsis: "开端".into(), + content: "真实正文".into(), + expected_revision: chapter.revision, + save_reason: Some("AUTOSAVE".into()), + }, + ) + .unwrap(); + assert_eq!(saved.revision, 2); + assert_eq!(saved.word_count, 4); + let stale = save_chapter_at( + &path, + SaveWebNovelChapterInput { + chapter_id: chapter.chapter_id, + title: "冲突".into(), + synopsis: String::new(), + content: "不会覆盖".into(), + expected_revision: 1, + save_reason: None, + }, + ) + .unwrap_err(); + assert_eq!(stale, "HOLOLAKE_WEBNOVEL_CHAPTER_REVISION_CONFLICT"); + let connection = open_database(&path).unwrap(); + let versions: i64 = connection + .query_row( + "SELECT COUNT(*) FROM web_novel_chapter_versions", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(versions, 2); + } + + #[test] + fn workflow_requires_review_and_non_empty_content() { + assert!(transition_allowed("DRAFT", "SELF_REVIEW")); + assert!(!transition_allowed("DRAFT", "PUBLISHED")); + assert!(transition_allowed("EDITOR_REVIEW", "APPROVED")); + assert!(transition_allowed("APPROVED", "PUBLISHED")); + } + + #[test] + fn checkpoint_restores_saved_chapter_content() { + let (_temp, path) = database_path(); + let detail = create_work_at( + &path, + CreateWebNovelWorkInput { + title: "检查点作品".into(), + pen_name: "作者".into(), + genre: "悬疑".into(), + work_kind: "LONG_NOVEL".into(), + }, + ) + .unwrap(); + let chapter = create_chapter_at( + &path, + CreateWebNovelChapterInput { + work_id: detail.work.work_id.clone(), + volume_id: detail.volumes[0].volume_id.clone(), + title: "序章".into(), + }, + ) + .unwrap(); + let first = save_chapter_at( + &path, + SaveWebNovelChapterInput { + chapter_id: chapter.chapter_id.clone(), + title: chapter.title.clone(), + synopsis: String::new(), + content: "检查点正文".into(), + expected_revision: chapter.revision, + save_reason: None, + }, + ) + .unwrap(); + let checkpoint = create_checkpoint_at( + &path, + CreateWebNovelCheckpointInput { + work_id: detail.work.work_id, + name: "交稿前".into(), + description: String::new(), + }, + ) + .unwrap(); + save_chapter_at( + &path, + SaveWebNovelChapterInput { + chapter_id: chapter.chapter_id.clone(), + title: chapter.title, + synopsis: String::new(), + content: "之后的正文".into(), + expected_revision: first.revision, + save_reason: None, + }, + ) + .unwrap(); + restore_checkpoint_at(&path, &checkpoint.checkpoint_id).unwrap(); + assert_eq!( + read_chapter_at(&path, &chapter.chapter_id).unwrap().content, + "检查点正文" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/world_climate.rs b/product-source/hololake-native-desktop/src-tauri/src/world_climate.rs new file mode 100644 index 000000000..ba0a80d9b --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/world_climate.rs @@ -0,0 +1,276 @@ +//! Signed dynamic language-world climate projection. +//! +//! The projection may tint the already-approved lake surface. It cannot change +//! domain routing, permissions, facts, or the five-theme layout. Real coordinates +//! are used only for a bounded public-weather query and are never returned to the +//! webview. + +use serde::{Deserialize, Serialize}; +use std::sync::{Mutex, OnceLock}; +use std::time::Duration; +use tauri::AppHandle; + +const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001"; +const ADAPTER: &str = "dynamic-language-world-surface-v1"; +const SOURCE: &str = "OPEN_METEO_FORECAST_API"; +const ATTRIBUTION_URL: &str = "https://open-meteo.com/"; +const CACHE_TTL_MS: u64 = 10 * 60 * 1_000; + +#[derive(Debug, Deserialize)] +struct OpenMeteoResponse { + current: OpenMeteoCurrent, +} + +#[derive(Debug, Deserialize)] +struct OpenMeteoCurrent { + weather_code: i64, + is_day: i64, + precipitation: f64, + cloud_cover: f64, + wind_speed_10m: f64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldClimateSnapshot { + schema: &'static str, + state: &'static str, + active_domain: &'static str, + time_phase: &'static str, + weather_kind: &'static str, + motion_intensity: &'static str, + source: &'static str, + source_attribution_url: &'static str, + city_exposed: bool, + coordinates_exposed: bool, + routing_or_permission_changed: bool, + observed_at_unix_ms: u64, +} + +static CACHE: OnceLock>> = OnceLock::new(); + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +pub async fn get_world_climate(app: AppHandle) -> Result { + require_active(&app)?; + let coordinate = crate::persona_time_authority::get_beijing_time_coordinate()?; + let now_ms = coordinate.unix_ms; + if let Some(snapshot) = cached(now_ms)? { + return Ok(snapshot); + } + let day_index = coordinate.elapsed_beijing_dates_since_guanghu_epoch; + let hour = beijing_hour(&coordinate.beijing_time).unwrap_or(0); + let domains = [ + "FIFTH_DOMAIN", + "ZERO_SENSE_DOMAIN", + "MAIN_DOMAIN", + "BRANCH_DOMAIN", + "ZERO_DOMAIN", + ]; + let active_domain = domains[day_index.rem_euclid(domains.len() as i64) as usize]; + let time_phase = phase_for_hour(hour); + let (latitude, longitude) = coordinate_for(active_domain, day_index); + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|error| format!("HOLOLAKE_WORLD_CLIMATE_CLIENT_FAILED: {error}"))?; + let url = format!( + "https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=weather_code,is_day,precipitation,cloud_cover,wind_speed_10m&timezone=Asia%2FShanghai&forecast_days=1" + ); + let response = client + .get(url) + .header(reqwest::header::USER_AGENT, "HoloLake/0.5.0 world-climate") + .send() + .await; + let snapshot = match response { + Ok(response) if response.status().is_success() => { + match response.json::().await { + Ok(payload) if valid_current(&payload.current) => { + live_snapshot(active_domain, time_phase, now_ms, payload.current) + } + _ => fallback(active_domain, time_phase, now_ms), + } + } + _ => fallback(active_domain, time_phase, now_ms), + }; + store_cache(now_ms, snapshot.clone())?; + Ok(snapshot) +} + +fn cached(now_ms: u64) -> Result, String> { + let guard = CACHE + .get_or_init(|| Mutex::new(None)) + .lock() + .map_err(|_| "HOLOLAKE_WORLD_CLIMATE_CACHE_POISONED".to_string())?; + Ok(guard.as_ref().and_then(|(cached_at, snapshot)| { + (now_ms.saturating_sub(*cached_at) < CACHE_TTL_MS).then(|| snapshot.clone()) + })) +} + +fn store_cache(now_ms: u64, snapshot: WorldClimateSnapshot) -> Result<(), String> { + *CACHE + .get_or_init(|| Mutex::new(None)) + .lock() + .map_err(|_| "HOLOLAKE_WORLD_CLIMATE_CACHE_POISONED".to_string())? = + Some((now_ms, snapshot)); + Ok(()) +} + +fn beijing_hour(value: &str) -> Option { + value.split('T').nth(1)?.get(0..2)?.parse().ok() +} + +fn phase_for_hour(hour: u8) -> &'static str { + match hour { + 5..=7 => "DAWN", + 8..=16 => "DAY", + 17..=19 => "DUSK", + _ => "NIGHT", + } +} + +fn coordinate_for(domain: &str, day_index: i64) -> (f64, f64) { + match domain { + "FIFTH_DOMAIN" => (34.3416, 108.9398), + "ZERO_DOMAIN" => (23.1291, 113.2644), + "MAIN_DOMAIN" | "ZERO_SENSE_DOMAIN" => (39.9042, 116.4074), + "BRANCH_DOMAIN" => { + let roaming = [ + (31.2304, 121.4737), + (30.5728, 104.0668), + (30.5928, 114.3055), + (30.2741, 120.1551), + (32.0603, 118.7969), + (29.5630, 106.5516), + ]; + roaming[day_index.rem_euclid(roaming.len() as i64) as usize] + } + _ => (39.9042, 116.4074), + } +} + +fn valid_current(current: &OpenMeteoCurrent) -> bool { + current.is_day.abs() <= 1 + && current.precipitation.is_finite() + && (0.0..=1_000.0).contains(¤t.precipitation) + && current.cloud_cover.is_finite() + && (0.0..=100.0).contains(¤t.cloud_cover) + && current.wind_speed_10m.is_finite() + && (0.0..=500.0).contains(¤t.wind_speed_10m) +} + +fn weather_kind(code: i64, precipitation: f64, cloud_cover: f64) -> &'static str { + if matches!(code, 95 | 96 | 99) { + "STORM" + } else if matches!(code, 71..=77 | 85 | 86) { + "SNOW" + } else if precipitation > 0.0 || matches!(code, 51..=67 | 80..=82) { + "RAIN" + } else if matches!(code, 45 | 48) { + "FOG" + } else if matches!(code, 1..=3) || cloud_cover >= 35.0 { + "CLOUD" + } else { + "CLEAR" + } +} + +fn live_snapshot( + active_domain: &'static str, + time_phase: &'static str, + observed_at_unix_ms: u64, + current: OpenMeteoCurrent, +) -> WorldClimateSnapshot { + let weather_kind = weather_kind( + current.weather_code, + current.precipitation, + current.cloud_cover, + ); + let motion_intensity = if current.wind_speed_10m >= 35.0 || weather_kind == "STORM" { + "ACTIVE" + } else if current.wind_speed_10m >= 15.0 || matches!(weather_kind, "RAIN" | "SNOW") { + "GENTLE" + } else { + "CALM" + }; + WorldClimateSnapshot { + schema: "hololake.world-climate/v1", + state: "VERIFIED_LIVE", + active_domain, + time_phase: if current.is_day == 0 && time_phase == "DAY" { + "NIGHT" + } else { + time_phase + }, + weather_kind, + motion_intensity, + source: SOURCE, + source_attribution_url: ATTRIBUTION_URL, + city_exposed: false, + coordinates_exposed: false, + routing_or_permission_changed: false, + observed_at_unix_ms, + } +} + +fn fallback( + active_domain: &'static str, + time_phase: &'static str, + observed_at_unix_ms: u64, +) -> WorldClimateSnapshot { + WorldClimateSnapshot { + schema: "hololake.world-climate/v1", + state: "TIME_ONLY_WEATHER_UNAVAILABLE", + active_domain, + time_phase, + weather_kind: "UNAVAILABLE", + motion_intensity: "CALM", + source: SOURCE, + source_attribution_url: ATTRIBUTION_URL, + city_exposed: false, + coordinates_exposed: false, + routing_or_permission_changed: false, + observed_at_unix_ms, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn weather_codes_map_without_exposing_coordinates_or_changing_routes() { + assert_eq!(weather_kind(0, 0.0, 3.0), "CLEAR"); + assert_eq!(weather_kind(61, 0.3, 80.0), "RAIN"); + assert_eq!(weather_kind(95, 4.0, 100.0), "STORM"); + let snapshot = fallback("FIFTH_DOMAIN", "NIGHT", 1); + assert!(!snapshot.city_exposed); + assert!(!snapshot.coordinates_exposed); + assert!(!snapshot.routing_or_permission_changed); + assert_eq!(snapshot.weather_kind, "UNAVAILABLE"); + } + + #[test] + fn beijing_phase_and_daily_coordinates_are_deterministic() { + assert_eq!(beijing_hour("2026-08-19T07:03:02.000+08:00"), Some(7)); + assert_eq!(phase_for_hour(7), "DAWN"); + assert_eq!(phase_for_hour(15), "DAY"); + assert_eq!(phase_for_hour(23), "NIGHT"); + assert_eq!(coordinate_for("FIFTH_DOMAIN", 1), (34.3416, 108.9398)); + assert_eq!(coordinate_for("BRANCH_DOMAIN", 0), (31.2304, 121.4737)); + } + + #[test] + fn malformed_public_weather_values_are_rejected() { + assert!(!valid_current(&OpenMeteoCurrent { + weather_code: 0, + is_day: 1, + precipitation: f64::NAN, + cloud_cover: 0.0, + wind_speed_10m: 0.0, + })); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/zero_core_numbering.rs b/product-source/hololake-native-desktop/src-tauri/src/zero_core_numbering.rs index e805b6fc3..03f2957af 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/zero_core_numbering.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/zero_core_numbering.rs @@ -190,7 +190,6 @@ pub(crate) fn resolve_human_number_route(number: &str) -> Result Result { let contract = load_contract()?; Ok(ZeroCoreNumberingKernelSnapshot { diff --git a/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs b/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs index c8d8ee0f9..d40386c98 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/zero_point.rs @@ -290,7 +290,6 @@ pub(crate) fn verified_user_route( } /// 登录绑定:用户编号入仓(等待绑定态→绑定态)。空白态拒绝一切唤醒。 -#[tauri::command] pub async fn zero_point_bind( state: State<'_, ZeroPointState>, input: serde_json::Value, @@ -320,7 +319,6 @@ pub async fn zero_point_bind( } /// 通过登记服务执行三态裁决:PASS / REJECT / OFFLINE,并应用离线宽限期。 -#[tauri::command] pub async fn zero_point_verify( state: State<'_, ZeroPointState>, ) -> Result { @@ -516,7 +514,6 @@ pub async fn sync_protocol_runtime(state: &ZeroPointState) -> Result<(), String> Ok(()) } -#[tauri::command] pub async fn zero_point_sync( state: State<'_, ZeroPointState>, ) -> Result { @@ -525,7 +522,6 @@ pub async fn zero_point_sync( } /// 返回当前编号验证与协议状态快照。 -#[tauri::command] pub async fn zero_point_status( state: State<'_, ZeroPointState>, ) -> Result { diff --git a/product-source/hololake-native-desktop/src-tauri/tauri.conf.json b/product-source/hololake-native-desktop/src-tauri/tauri.conf.json index 247a4e08e..d50f216da 100644 --- a/product-source/hololake-native-desktop/src-tauri/tauri.conf.json +++ b/product-source/hololake-native-desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "HoloLake", - "version": "0.4.1", + "version": "0.5.0", "identifier": "world.guanghu.hololake", "build": { "frontendDist": "../dist", diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 0b51be164..4d4fae088 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -1,7 +1,19 @@ -import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { lazy, StrictMode, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' -import { invoke } from '@tauri-apps/api/core' +import { numberedInvoke as invoke } from './modules/numbered-ipc' import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render' +import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition' +import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench' +import { StarlakeSurface, type DomainId } from './modules/qoder-surface/StarlakeSurface' +import { FINISHES, TraditionalSurface, type FinishId, type TraditionalBroadcast, type TraditionalChannel, type TraditionalSystem } from './modules/qoder-surface/TraditionalSurface' +import { resolveVisualBalance } from './modules/qoder-surface/visual-balance' +import { PrivateChannelSurface, type InstalledChannelModule, type PrivateChannelAction } from './modules/private-channel/PrivateChannelSurface' + +const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) +const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody }))) +const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace }))) +const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace }))) +const MobileSyncPanel = lazy(() => import('./modules/mobile-sync').then((module) => ({ default: module.MobileSyncPanel }))) const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate'] @@ -48,10 +60,35 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system' +type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'receipts' | 'system' type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool' type KnowledgeSource = 'native' | 'legacy' +interface BundledModuleDescriptor { + moduleNumber: string + displayName: string + version: string + adapter: string + permissions: string[] + packageSha256: string + installedState: string + signatureVerified: boolean +} +interface WorldClimateSnapshot { + schema: 'hololake.world-climate/v1' + state: 'VERIFIED_LIVE' | 'TIME_ONLY_WEATHER_UNAVAILABLE' + activeDomain: string + timePhase: 'DAWN' | 'DAY' | 'DUSK' | 'NIGHT' + weatherKind: 'CLEAR' | 'CLOUD' | 'FOG' | 'RAIN' | 'SNOW' | 'STORM' | 'UNAVAILABLE' + motionIntensity: 'CALM' | 'GENTLE' | 'ACTIVE' + source: string + sourceAttributionUrl: string + cityExposed: false + coordinatesExposed: false + routingOrPermissionChanged: false + observedAtUnixMs: number +} + interface HomeStatus { directLocalBrokerState: string directConnectionCount: number @@ -96,6 +133,10 @@ interface GlsProtocolRuntimeSnapshot { registeredDraftCount: number registeredDraftNotStartedCount: number legacyDependencyTargetCount: number + numberCoordinateCount: number + numberedReferenceNodeCount: number + independentSourceGapCount: number + unresolvedNumberReferenceCount: number dependencyGapCount: number legacyDependencyCycleCount: number discoveredUnreconciledCount: number @@ -344,29 +385,29 @@ interface EnterpriseReceiptEnvelope { repository_projection?: EnterpriseReceiptProjection } -const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY', terminalLinkProtocol: 'HOLOLAKE_TERMINAL_LINK/2', terminalLinkTransport: 'UNVERIFIED', environmentFramePolicy: 'REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION' } +const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY', terminalLinkProtocol: 'HOLOLAKE_TERMINAL_LINK/3', terminalLinkTransport: 'UNVERIFIED', environmentFramePolicy: 'REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION' } const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], modules: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } } const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false } const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' } const themes: Array<{ id: ThemeId; name: string }> = [ { id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' }, ] -const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } +const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } const domainGates = [ { domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [ - ['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], + ['公共作用', '行业成品模块商城 · 人格体大脑技能商城'], ['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], ] }, { domain: 'MAIN_DOMAIN', className: 'd-main', title: '光湖主域', gate: 'GATE 01 · ONLINE', facts: [ - ['域标识', 'MAIN_DOMAIN'], ['责任主体', 'Awen · TCS-GL-0016∞'], ['人格体主体', '天枢 · PER-AW-ARCH-001 · AGE'], ['关系支持', '知秋 · PER-ZQ001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], + ['公共作用', '公告 · 版本更新 · 系统通知'], ['域标识', 'MAIN_DOMAIN'], ['责任主体', 'Awen · TCS-GL-0016∞'], ['人格体主体', '天枢 · PER-AW-ARCH-001 · AGE'], ['关系支持', '知秋 · PER-ZQ001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], ] }, { domain: 'ZERO_DOMAIN', className: 'd-zero', title: '光湖零域', gate: 'GATE 03 · ONLINE', facts: [ - ['域标识', 'ZERO_DOMAIN'], ['责任主体', '页页 · TCS-GL-0006∞'], ['人格体主体', '页骨 · PER-YG001 · AGE'], ['关系支持', '小坍缩核 · PER-XTK001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], + ['公共作用', '热更新实验 · 系统架构 · 语言推理模拟'], ['域标识', 'ZERO_DOMAIN'], ['责任主体', '页页 · TCS-GL-0006∞'], ['人格体主体', '页骨 · PER-YG001 · AGE'], ['关系支持', '小坍缩核 · PER-XTK001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], ] }, { domain: 'ZERO_SENSE_DOMAIN', className: 'd-zs', title: '光湖零感域', gate: 'GATE 04 · ONLINE', facts: [ - ['域标识', 'ZERO_SENSE_DOMAIN'], ['责任主体 01', '肥猫 · TCS-GL-0007∞'], ['人格体主体 01', '烬舟 · PER-JZ001 · AGE'], ['责任主体 02', '桔子 · TCS-GL-0008∞'], ['人格体主体 02', '熹微 · PER-JZ-ARCH-001 · AGE'], ['工作仓库', 'PRIVATE · 2 · LIVE'], + ['开放边界', '人类主控团队内部管理域 · 不对公众开放'], ['域标识', 'ZERO_SENSE_DOMAIN'], ['责任主体 01', '肥猫 · TCS-GL-0007∞'], ['人格体主体 01', '烬舟 · PER-JZ001 · AGE'], ['责任主体 02', '桔子 · TCS-GL-0008∞'], ['人格体主体 02', '熹微 · PER-JZ-ARCH-001 · AGE'], ['工作仓库', 'PRIVATE · 2 · LIVE'], ] }, { domain: 'FIFTH_DOMAIN', className: 'd-fifth', title: '第五域 · 光湖本源域', gate: 'GATE 05 · ONLINE', facts: [ - ['域标识', 'FIFTH_DOMAIN'], ['责任主体', '冰朔 · ICE-GL∞'], ['系统入口', '永恒湖心系统'], ['访问状态', 'PRIVATE · LIVE'], + ['授权边界', '私有自由部署 · 逆向访问须经 ICE-GL∞ 编号授权'], ['域标识', 'FIFTH_DOMAIN'], ['责任主体', '冰朔 · ICE-GL∞'], ['系统入口', '永恒湖心系统'], ['访问状态', 'PRIVATE · LIVE'], ] }, ] const starPoints = [ @@ -389,6 +430,7 @@ function LakeAtmosphere({ awake }: { awake: boolean }) { {starPoints.map(([left, top, size, duration], index) => ) } }> setWorldStage(toolReturnStage)}/> + :
    +

    启用教育工作台

    +

    模块复用已验收的文档与真实单元格引擎,并增加账号隔离存储、外部表格翻译、人工归属、显式数据清理和预览后执行的自动化。

    + HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001 · 8 项编号权限 + + {educationMessage &&

    {educationMessage}

    } +
    } + + ) + + const renderWebNovel = () => ( +
    + {webNovelModule?.installedState === 'ACTIVE' + ?

    正在装载网文作者工作台……

    }> setWorldStage(toolReturnStage)}/>
    + :
    +

    启用网文作者工作台

    +

    基础工作台保存作品、分卷、章节、版本、设定和运营记录;大纲、多维情节表、故事世界与高级交付分别使用独立官方编号按需挂载。

    + HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001 · 8 项编号权限 + + {webNovelMessage &&

    {webNovelMessage}

    } +
    } +
    + ) + + const renderMobileSync = () => ( +
    + {mobileSyncModule?.installedState === 'ACTIVE' + ?

    正在装载移动同步桥……

    }> setWorldStage(toolReturnStage)}/>
    + :
    +

    启用移动同步桥

    +

    模块只在本人确认后开放同一局域网入口;电脑仍是唯一根节点,手机只读取最小投影并把快速记录写入隔离收件箱。

    + HLP-MOD-OFFICIAL-MOBILE-SYNC-0001 · 5 项本机网络权限 + + {mobileSyncMessage &&

    {mobileSyncMessage}

    } +
    } +
    + ) + const renderCode = () => (

    HoloLake 编程执行环境

    HoloLake 持有会话连续性、环境事实帧和单写通道;外部编程 AI 只作为可续接载体进入。

    {developmentLane?.state === 'ACTIVE' ? '环境已锚定' : '等待直连写入者'}
    终端直连协议
    {status.terminalLinkProtocol}
    协议恢复方式
    HoloLake 系统自动运行 · 模型无需重读
    已挂载仓库
    {status.codeRepositoryMountCount}
    投影回执
    {status.pnccReceiptCount}
    开发执行通道
    {developmentLane?.state === 'ACTIVE' ? '已切入 HoloLake' : '等待受控载体'}
    当前开发线
    {developmentLane?.laneId || '—'}
    当前写入载体
    {developmentLane?.ownerInstanceId || '—'}
    环境校验
    {developmentLane?.state === 'ACTIVE' ? '每次写入前必须持有未过期事实帧' : '未取得单写通道,不允许变更'}
    Agent Shell
    第二阶段 · 当前未启用
    -

    GLS 协议运行层

    正本先完成注册对账和依赖分型;只有显式原生适配器进入执行图。

    {glsRuntime ? '运行清单 v2 已加载' : '失败关闭'}
    {glsRuntime ?
    正本来源
    {glsRuntime.sourceRepository} · {glsRuntime.sourceCommit.slice(0, 12)}
    已发现编号协议
    {glsRuntime.protocolCount}
    协议注册表编号
    {glsRuntime.protocolRegistryIdCount}
    注册草案
    {glsRuntime.registeredDraftCount} · {glsRuntime.registeredDraftNotStartedCount} 尚未实现
    内嵌工程协议
    {glsRuntime.executableProjectionCount} · P0–P6 共 {glsRuntime.implementationStageCount} 层
    已收录待工程化
    {glsRuntime.inventoriedNotExecutableCount}
    已分型来源依赖
    {glsRuntime.typedSourceDependencyCount} · 未分类 {glsRuntime.unclassifiedSourceDependencyCount}
    旧来源关系环
    {glsRuntime.legacyDependencyCycleCount} 组仅供审计 · 不进入运行图
    缺少编号正本的依赖
    {glsRuntime.dependencyGapCount}
    登记冲突 / 未对账
    {glsRuntime.authorityConflictCount} / {glsRuntime.discoveredUnreconciledCount}
    原文直接执行
    {glsRuntime.rawProtocolTextExecuted ? '允许' : '禁止'}
    任意协议代码
    {glsRuntime.arbitraryProtocolCodeAllowed ? '允许' : '禁止'}
    :

    GLS 运行清单不可读,协议适配器全部保持关闭。

    }
    +

    GLS 协议运行层

    正本先完成注册对账和依赖分型;只有显式原生适配器进入执行图。

    {glsRuntime ? '运行清单 v2 已加载' : '失败关闭'}
    {glsRuntime ?
    正本来源
    {glsRuntime.sourceRepository} · {glsRuntime.sourceCommit.slice(0, 12)}
    协议编号坐标
    {glsRuntime.numberCoordinateCount} · 未解析 {glsRuntime.unresolvedNumberReferenceCount}
    独立协议来源
    {glsRuntime.protocolCount}
    仅引用节点
    {glsRuntime.numberedReferenceNodeCount} · 不可执行
    待补独立正本
    {glsRuntime.independentSourceGapCount} · 已有编号坐标,不悬空
    协议注册表编号
    {glsRuntime.protocolRegistryIdCount}
    注册草案
    {glsRuntime.registeredDraftCount} · {glsRuntime.registeredDraftNotStartedCount} 尚未实现
    内嵌工程协议
    {glsRuntime.executableProjectionCount} · P0–P6 共 {glsRuntime.implementationStageCount} 层
    已收录非执行协议
    {glsRuntime.inventoriedNotExecutableCount}
    已分型来源依赖
    {glsRuntime.typedSourceDependencyCount} · 未分类 {glsRuntime.unclassifiedSourceDependencyCount}
    旧来源关系环
    {glsRuntime.legacyDependencyCycleCount} 组仅供审计 · 不进入运行图
    编号路径缺口
    {glsRuntime.dependencyGapCount}
    登记冲突 / 未对账
    {glsRuntime.authorityConflictCount} / {glsRuntime.discoveredUnreconciledCount}
    原文直接执行
    {glsRuntime.rawProtocolTextExecuted ? '允许' : '禁止'}
    任意协议代码
    {glsRuntime.arbitraryProtocolCodeAllowed ? '允许' : '禁止'}
    :

    GLS 运行清单不可读,协议适配器全部保持关闭。

    }

    HoloLake 原生协议内核

    协议规则已编入软件本体;每次允许、拒绝、歧义和未核验裁决都写入本机哈希链回执。

    {glsKernel ? '随软件运行' : '失败关闭'}
    {glsKernel ?
    P1–P6 原生器官
    {glsKernel.executableProtocolCount} 份合同 · {glsKernel.implementedStageCount} 个阶段
    协议裁决回执
    {glsKernel.decisionReceiptCount}
    允许 / 拒绝
    {glsKernel.allowCount} / {glsKernel.denyCount}
    歧义 / 未核验
    {glsKernel.ambiguousCount} / {glsKernel.unverifiedCount}
    GLC 自举一致性
    {glsKernel.bootstrapCompilerSelfCheck === 'PASS_DETERMINISTIC_DOUBLE_COMPILE' ? '双编译一致' : '失败关闭'}
    P7 物理能力
    {glsKernel.p7VerifiedPhysicalCapabilityCount} 已获本机目标证据 · {glsKernel.p7NodeAssemblies.length} 项装配边界
    模型可改写裁决
    {glsKernel.modelCanOverrideDecision ? '允许' : '禁止'}
    回执链末端
    {glsKernel.lastReceiptSha256 === 'GENESIS' ? '尚无裁决' : glsKernel.lastReceiptSha256.slice(0, 16)}
    :

    原生协议内核不可读,所有协议裁决保持关闭。

    }

    零点原核编号内核

    先按权威图确定主体类别与登记面,再进入编号核验;编号外形本身不构成权威。

    {numberingKernel ? '本机内核已加载' : '失败关闭'}
    {numberingKernel ?
    权威图
    {numberingKernel.authorityMapId}
    权威图版本
    {numberingKernel.authorityMapVersion}
    正本提交
    {numberingKernel.sourceCommit.slice(0, 12)}
    人类入口命名空间
    {numberingKernel.humanRouteNamespaces.join(' · ')}
    自动派发身份号
    {numberingKernel.automaticIdentityIssuance ? '已开启' : '禁止'}
    未知编号
    {numberingKernel.unknownNumber === 'FAIL_CLOSED' ? '失败关闭 · 不猜测' : numberingKernel.unknownNumber}
    :

    编号内核不可读,所有编号路由保持关闭。

    }
    @@ -1449,6 +1824,11 @@ function HoloLakeApp() { {zpMessage &&

    {zpMessage}

    }

    显示主题

    主题仅调整视觉令牌,不改变功能、权限或路由。

    {themes.map((choice) => )}
    +

    动态语言世界视觉层

    只把北京时间和可验证的公开天气映射为湖面气候;五域位置、权限、事实与五色湖主题保持不变。

    {worldClimate ? '真实投影已启用' : dynamicSurfaceModule?.installedState || '未启用'}
    + {worldClimate ?
    时间相位
    {worldClimate.timePhase}
    天气投影
    {worldClimate.weatherKind === 'UNAVAILABLE' ? '不可用 · 未伪造' : worldClimate.weatherKind}
    当值采样域
    {domainDisplayName(worldClimate.activeDomain)} · 不改变开域状态
    现实城市 / 坐标
    {worldClimate.cityExposed || worldClimate.coordinatesExposed ? '边界异常' : '不向界面暴露'}
    数据源
    Open-Meteo · 十分钟缓存
    :

    旧版传统工作台含模拟广播与模拟数值,未迁移。启用后也不会替换现行官方五域首页。

    } + + {dynamicSurfaceMessage &&

    {dynamicSurfaceMessage}

    } +

    软件更新

    仅接受已登记信任根验证通过的 HoloLake 发布包。

    {releaseCandidate ?
    HoloLake {releaseCandidate.version}

    {releaseCandidate.notes}

    : }
    {systemMessage &&

    {systemMessage}

    } @@ -1480,13 +1860,95 @@ function HoloLakeApp() { const enterpriseRelationshipPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && enterpriseEntry.relationship_confirmation?.decision !== 'CONFIRM') const enterpriseResponsibilityPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && !enterpriseRelationshipPending && enterpriseEntry.responsibility_receipt?.decision !== 'ACCEPT') + const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : '' + const chooseSurface = (next: 'world' | 'traditional') => { + setSurface(next) + window.localStorage.setItem('hololake-surface', next) + } + const setFinish = (next: FinishId) => { + setTraditionalFinish(next) + window.localStorage.setItem('hololake-finish', next) + } + const surfacePill =
    + + +
    + const activeFinishName = FINISHES.find((item) => item.id === traditionalFinish)?.name || '曜夜' + const finishRail = surface === 'traditional' && worldStage !== 'domain' ?
    + 外观 · {activeFinishName}
    {FINISHES.map((item) => )}
    +
    : null + const weatherLabel: Record = { CLEAR: '晴', CLOUD: '云', FOG: '雾', RAIN: '雨', SNOW: '雪', STORM: '雷雨', UNAVAILABLE: '待命' } + const dayNumber = beijingCoordinate?.guanghuEraDay || Math.max(1, Math.floor((Date.now() - new Date('2025-04-26T00:00:00+08:00').getTime()) / 86400000) + 1) + const activityBars = useMemo(() => { + const result = Array(7).fill(0) + const today = new Date(); today.setHours(0, 0, 0, 0) + for (const event of personal.recentEvents) { + const occurred = new Date(event.occurredAtUnixMs); occurred.setHours(0, 0, 0, 0) + const daysAgo = Math.floor((today.getTime() - occurred.getTime()) / 86400000) + if (daysAgo >= 0 && daysAgo < 7) result[6 - daysAgo] += 1 + } + return result + }, [personal.recentEvents]) + const domainLabel = domainDisplayName(worldClimate?.activeDomain || repoLogin?.domain || 'MAIN_DOMAIN') + const visualBalance = resolveVisualBalance(traditionalFinish, worldClimate?.timePhase, worldClimate?.weatherKind) + const traditionalBroadcasts: TraditionalBroadcast[] = personal.recentEvents.slice(0, 4).map((event) => ({ + id: event.eventId, + domain: repoLogin?.domain === 'FIFTH_DOMAIN' ? '第五域' : '零感域', + message: event.summary, + time: new Date(event.occurredAtUnixMs).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + })) + if (!traditionalBroadcasts.length) { + traditionalBroadcasts.push( + { id: 'number-root', domain: '主域', message: `编号入口 ${zeroPoint?.route === 'verified' ? '已校验' : '等待验证'}`, time: '当前' }, + { id: 'module-root', domain: '分域', message: `签名模块 ${personal.modules.length} 项`, time: '当前' }, + { id: 'weather-root', domain: '零域', message: worldClimate?.state === 'VERIFIED_LIVE' ? '现实天象投影已校验' : '天气不可用时不生成假数据', time: '当前' }, + ) + } + const traditionalChannels: TraditionalChannel[] = [ + { color: 'var(--accent)', title: '人格原生代码频道', meta: '当前账号本机 Git 投影', tag: userPncc?.state === 'READY' ? '已认证' : '待接入', tone: 'var(--good)', count: codeChannels.channels.length }, + { color: 'var(--accent2)', title: '知识库', meta: '本机内容指纹投影', tag: '原生', tone: 'var(--accent2)', count: knowledge.uniqueDocumentCount }, + { color: 'var(--good)', title: '频道模块', meta: '已登记签名模块', tag: '编号', tone: 'var(--good)', count: personal.modules.length }, + { color: 'var(--warn)', title: '运行回执', meta: '可核验事件链', tag: '证据', tone: 'var(--warn)', count: receipts.length }, + ] + const traditionalSystems: TraditionalSystem[] = [ + { title: '编号协议基座', meta: '单一编号入口 · 未知路径关闭', tag: zeroPoint?.route === 'verified' ? '已校验' : '待验证', tone: 'var(--good)' }, + { title: '耐久化引擎', meta: '本机 Git · 事件与回执留证', tag: status.releaseRecoveryState && status.releaseRecoveryState !== 'NONE' ? status.releaseRecoveryState : '在线', tone: 'var(--good)' }, + { title: '天象感知', meta: worldClimate?.state === 'VERIFIED_LIVE' ? `${domainLabel} · 公开天气已校验` : '只使用北京时间 · 不伪造天气', tag: worldClimate ? '已接入' : '待命', tone: 'var(--accent2)' }, + { title: '色衡层', meta: '主题语义令牌 · 明暗自动均衡', tag: '守护中', tone: 'var(--good)' }, + ] + const openFromTraditional = (target: 'knowledge' | 'channel' | 'era' | 'settings') => { + if (target === 'era') { openEraTimeline(); return } + if (!repoLogin) { chooseSurface('world'); setGateOpen(true); return } + if (target === 'channel') { setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? 'channel' : 'channel'); return } + openWorldTool(target === 'knowledge' ? 'knowledge' : 'system') + } + + if (surface === 'traditional' && worldStage === 'domain') { + return <> + chooseSurface('world')} onOpenKnowledge={() => openFromTraditional('knowledge')} onOpenChannel={() => openFromTraditional('channel')} onOpenEra={() => openFromTraditional('era')} onOpenSettings={() => openFromTraditional('settings')} identityLabel={repoLogin ? `${repoLogin.username} · ${domainDisplayName(repoLogin.domain)}` : '光湖语言系统'} version="0.5.0" dayNumber={dayNumber} duty={domainLabel} weatherLabel={worldClimate ? weatherLabel[worldClimate.weatherKind] : '待命'} broadcasts={traditionalBroadcasts} channels={traditionalChannels} systems={traditionalSystems} activityBars={activityBars} activityCount={activityBars.reduce((sum, value) => sum + value, 0)}/> + {surfacePill} + {eraOpen && eraTimeline && setEraOpen(false)}/>} + + } + + if (!repoLogin && surface === 'world' && gateStage === 'number' && !gateRising) { + const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo) + return
    + {surfacePill} + setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setGateOpen(true)}/> + {activeGate && <>{activeGate.title}{activeGate.gate}{activeGate.facts.map(([label, value]) => {label}{value})}} + {gateOpen &&

    编号验证

    onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/>
    {gateMessage &&

    {gateMessage}

    }
    } + {eraOpen && eraTimeline && setEraOpen(false)}/>} + + } if (!repoLogin) { const resolvedDomain = zeroPoint?.resolvedDomain || '' const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo) - return
    + return
    + {surfacePill} -
    HoloLake
    +
    HoloLake
    {finishRail}

    光湖语言系统 · 通用人工智能操作平台

    GH-AIOS · GUANGHU AI OPERATING SYSTEM

    {domainGates.map((gate) => gateStage === 'number' && setActiveDomainInfo(gate.domain)}/>) } @@ -1546,13 +2008,29 @@ function HoloLakeApp() { } const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞' - return
    + const signedActiveGate = domainGates.find((gate) => gate.domain === activeDomainInfo) + const privateNativeActions: PrivateChannelAction[] = [ + { id: 'overview', title: '频道全景', meta: '私人频道的真实状态与事件', badge: '原生', onOpen: () => openWorldTool('overview') }, + { id: 'knowledge', title: '知识空间', meta: `${knowledge.uniqueDocumentCount} 个唯一知识坐标`, badge: '原生', onOpen: () => openWorldTool('knowledge') }, + ...(timeAuthorityModule ? [{ id: 'time', title: '时间主控', meta: beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间持续流动', badge: '系统', onOpen: openEraTimeline }] : []), + { id: 'weather', title: '湖面天气', meta: connectionLabel, badge: '感知', onOpen: () => openWorldTool('system') }, + ] + const privateInstalledModules: InstalledChannelModule[] = [ + { id: 'composition', title: '结构组合', meta: '只读知识投影与结构视图', number: compositionModule?.moduleNumber || 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001', badge: compositionModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: () => { openWorldTool('composition'); void refreshCompositionModule() } }, + { id: 'workbench', title: '资料工作台', meta: '文档与智能表格', number: workbenchModule?.moduleNumber || 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001', badge: workbenchModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openWorkbench }, + { id: 'education', title: '教育工作台', meta: '教育文档、表格与自动化', number: educationModule?.moduleNumber || 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001', badge: educationModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openEducation }, + { id: 'web-novel', title: '网文作者工作台', meta: '码字、设定、编辑与交付', number: webNovelModule?.moduleNumber || 'HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001', badge: webNovelModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openWebNovel }, + { id: 'mobile-sync', title: '移动同步桥', meta: '同一人格系统的局域网加密入口', number: mobileSyncModule?.moduleNumber || 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001', badge: mobileSyncModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openMobileSync }, + ] + return
    + {surfacePill} -
    HoloLake
    {repoLogin.username} · {domainDisplayName(repoLogin.domain)}
    +
    HoloLake
    {finishRail}{repoLogin.username} · {domainDisplayName(repoLogin.domain)}
    - {worldStage === 'domain' &&
    + {worldStage === 'domain' && surface === 'world' && setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? 'channel' : 'channel')}/>} + {worldStage === 'domain' && surface !== 'world' &&

    {repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}

    {repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}

    -

    光湖公共灯塔 · 在线

    协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}

    {repoLogin.domain !== 'FIFTH_DOMAIN' &&

    本人责任域 · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}

    }

    HoloLake · V0.4.1

    +

    光湖公共灯塔 · 在线

    协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}

    {repoLogin.domain !== 'FIFTH_DOMAIN' &&

    本人责任域 · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}

    }

    HoloLake · V0.5.0

    setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? (isZhizhi ? 'heart' : 'channel') : 'channel')}/> openWorldTool('system')}/> @@ -1573,6 +2051,7 @@ function HoloLakeApp() {

    明天见频道

    频道全景 · 湖面按操作继续展开

    openWorldTool('overview')}/> openWorldTool('knowledge')}/> + { openWorldTool('composition'); void refreshCompositionModule() }}/> openWorldTool('code')}/> setWorldStage('bottle')}/> {timeAuthorityModule && } @@ -1623,26 +2102,21 @@ function HoloLakeApp() {
    当前状态 · 待本人接入可由本人人格体协助执行,但不能替代人类节点所有权确认。
    } - {worldStage === 'heartbeat' && repoLogin.domain === 'FIFTH_DOMAIN' &&
    - -

    心跳核心频道

    冰朔 · ICE-GL∞ · 私人频道全景

    - openWorldTool('overview')}/> - openWorldTool('knowledge')}/> - {timeAuthorityModule && } - openWorldTool('system')}/> -
    } + {worldStage === 'heartbeat' && repoLogin.domain === 'FIFTH_DOMAIN' && setWorldStage('channel')} onMarketplace={() => { setWorldStage('domain'); setActiveDomainInfo('BRANCH_DOMAIN') }} nativeActions={privateNativeActions} installedModules={privateInstalledModules}/>} {worldStage === 'lightLake' && repoLogin.domain === 'FIFTH_DOMAIN' &&

    光之湖

    人格体居所 · AGE 人格体语言生活空间

    - openWorldTool('receipts')}/> + + openWorldTool('receipts')}/> openWorldTool('code')}/> openWorldTool('system')}/>
    } {worldStage === 'tool' &&
    {viewLabels[view]}{domainDisplayName(repoLogin.domain)}
    -
    {view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
    +
    {view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
    }
    + {worldStage === 'domain' && surface === 'world' && signedActiveGate && <>{signedActiveGate.title}{signedActiveGate.gate}{signedActiveGate.facts.map(([label, value]) => {label}{value})}}
    光湖语言系统 · 通用人工智能操作平台GH-AIOS
    {eraOpen && eraTimeline && setEraOpen(false)}/>} {enterpriseEntryBusy &&

    正在读取企业责任登记……

    } diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx new file mode 100644 index 000000000..1da36bd6e --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx @@ -0,0 +1,58 @@ +import { useMemo } from 'react' +import { $createParagraphNode, $getSelection, FORMAT_TEXT_COMMAND, REDO_COMMAND, UNDO_COMMAND } from 'lexical' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin' +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $convertFromMarkdownString, $convertToMarkdownString, BOLD_ITALIC_STAR, BOLD_STAR, CHECK_LIST, HEADING, ITALIC_STAR, ORDERED_LIST, QUOTE, UNORDERED_LIST, type Transformer } from '@lexical/markdown' +import { $createHeadingNode, HeadingNode, QuoteNode } from '@lexical/rich-text' +import { INSERT_CHECK_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $setBlocksType } from '@lexical/selection' + +const TRANSFORMERS: Transformer[] = [HEADING, QUOTE, UNORDERED_LIST, ORDERED_LIST, CHECK_LIST, BOLD_ITALIC_STAR, BOLD_STAR, ITALIC_STAR] + +function Toolbar() { + const [editor] = useLexicalComposerContext() + const setBlock = (kind: 'paragraph' | 'heading') => editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => kind === 'heading' ? $createHeadingNode('h2') : $createParagraphNode()) + }) + return
    + 文档 + + + + + + + + + +
    +} + +export default function ChannelDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) { + const initialConfig = useMemo(() => ({ + namespace: 'HoloLakeChannelDocumentEngine', + nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode], + theme: { + paragraph: 'channel-document-paragraph', heading: { h2: 'channel-document-heading' }, + quote: 'channel-document-quote', list: { ul: 'channel-document-list', checklist: 'channel-document-checklist' }, + text: { bold: 'channel-document-bold', italic: 'channel-document-italic' }, + }, + onError: (error: Error) => { throw error }, + editorState: () => $convertFromMarkdownString(body, TRANSFORMERS), + }), []) + return + +
    + } placeholder={
    从这里开始记录……
    } ErrorBoundary={LexicalErrorBoundary}/> + + state.read(() => onChange($convertToMarkdownString(TRANSFORMERS)))}/> +
    +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx new file mode 100644 index 000000000..5d9ca7352 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react' +import ChannelDocumentEngine from './document-engine' +import ChannelSpreadsheetEngine from './spreadsheet-engine' +import type { ChannelSpreadsheetValue, ChannelWorkbenchColumn, ChannelWorkbenchRow } from './spreadsheet-model' +import './styles.css' +export { channelWorkbenchManifest } from './manifest' + +export interface ChannelDocumentSnapshot { documentId: string; title: string; body: string; revision: number; updatedAtUnixMs: number; contentSha256: string } +export interface ChannelSpreadsheetSnapshot extends ChannelSpreadsheetValue { tableId: string; title: string; revision: number; updatedAtUnixMs: number; contentSha256: string } +export interface ChannelWorkbenchSnapshot { schema: string; state: string; moduleNumber: string; document: ChannelDocumentSnapshot; spreadsheet: ChannelSpreadsheetSnapshot; receiptCount: number; latestReceiptHash: string; userDataSurvivesUnmount: boolean } +export interface WorkbenchSaveResult { state: string; entityKind: string; entityId: string; revision: number; contentSha256: string; receiptSha256: string; observedAtUnixMs: number } + +interface Props { + snapshot: ChannelWorkbenchSnapshot + busy: boolean + message: string + onSaveDocument: (input: { documentId: string; title: string; body: string; expectedRevision: number }) => Promise + onSaveSpreadsheet: (input: { tableId: string; title: string; columns: ChannelWorkbenchColumn[]; rows: ChannelWorkbenchRow[]; expectedRevision: number }) => Promise +} + +export function ChannelWorkbenchStudio({ snapshot, busy, message, onSaveDocument, onSaveSpreadsheet }: Props) { + const [mode, setMode] = useState<'document' | 'spreadsheet'>('document') + const [documentTitle, setDocumentTitle] = useState(snapshot.document.title) + const [documentBody, setDocumentBody] = useState(snapshot.document.body) + const [sheetTitle, setSheetTitle] = useState(snapshot.spreadsheet.title) + const [sheetValue, setSheetValue] = useState({ columns: snapshot.spreadsheet.columns, rows: snapshot.spreadsheet.rows }) + useEffect(() => { setDocumentTitle(snapshot.document.title); setDocumentBody(snapshot.document.body) }, [snapshot.document.revision]) + useEffect(() => { setSheetTitle(snapshot.spreadsheet.title); setSheetValue({ columns: snapshot.spreadsheet.columns, rows: snapshot.spreadsheet.rows }) }, [snapshot.spreadsheet.revision]) + return
    +
    +
    LOCAL CHANNEL WORKBENCH

    频道资料工作台

    文档与表格只属于当前已认证账号;每次保存都有编号路由、修订号和哈希回执。

    +
    签名模块已激活{snapshot.receiptCount} 条内容回执 · {snapshot.latestReceiptHash.slice(0, 12) || '尚未保存'}
    +
    + + {mode === 'document' ?
    +
    setDocumentTitle(event.target.value)}/>
    修订 {snapshot.document.revision} · {snapshot.document.contentSha256.slice(0, 12)}
    + +
    :
    +
    setSheetTitle(event.target.value)}/>
    修订 {snapshot.spreadsheet.revision} · {snapshot.spreadsheet.contentSha256.slice(0, 12)}
    + +
    } + {message &&

    {message}

    } +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts b/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts new file mode 100644 index 000000000..768c753c8 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts @@ -0,0 +1,11 @@ +export const channelWorkbenchManifest = { + schema: 'hololake.channel-module/v1', + moduleNumber: 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001', + name: '频道文档与表格工作台', + version: '0.1.0', + slot: 'channel-workbench', + state: 'SIGNED_LOCAL_MODULE', + engines: { document: 'LEXICAL_0_49', spreadsheet: 'FORTUNE_SHEET_1_0_4' }, + authority: 'CURRENT_AUTHENTICATED_ACCOUNT_LOCAL_DATA_ONLY', + numberedOperations: ['HLP-NIPC-OP-0074', 'HLP-NIPC-OP-0075', 'HLP-NIPC-OP-0076'], +} as const diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx new file mode 100644 index 000000000..0f627e5e1 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx @@ -0,0 +1,21 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { Workbook, type WorkbookInstance } from '@fortune-sheet/react' +import '@fortune-sheet/react/dist/index.css' +import { createInitialWorkbook, readWorkbookValue, type ChannelSpreadsheetValue, type FortuneSheet } from './spreadsheet-model' +export type { ChannelSpreadsheetValue } from './spreadsheet-model' + +export default function ChannelSpreadsheetEngine({ tableId, title, columns, rows, onChange }: ChannelSpreadsheetValue & { tableId: string; title: string; onChange: (value: ChannelSpreadsheetValue) => void }) { + const workbookRef = useRef(null) + const [initialData] = useState(() => createInitialWorkbook(title, tableId, { columns, rows })) + const latestRef = useRef({ columns, rows }) + const publish = useCallback((data: FortuneSheet[]) => { + const next = readWorkbookValue(data[0], latestRef.current) + latestRef.current = next + onChange(next) + }, [onChange]) + useEffect(() => { const timer = window.setTimeout(() => workbookRef.current?.calculateFormula(), 300); return () => window.clearTimeout(timer) }, [tableId]) + return
    +
    工作表真实单元格引擎 · 可编辑、公式、复制粘贴、撤销重做、筛选与冻结
    本机频道数据
    +
    +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts new file mode 100644 index 000000000..ef5d8c6f4 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts @@ -0,0 +1,38 @@ +export interface ChannelWorkbenchColumn { columnId: string; title: string } +export interface ChannelWorkbenchRow { rowId: string; cells: string[] } +export interface ChannelSpreadsheetValue { columns: ChannelWorkbenchColumn[]; rows: ChannelWorkbenchRow[] } +export type FortuneCell = { v?: string | number | boolean; m?: string | number; f?: string; bg?: string; fc?: string; bl?: number; fs?: number; ht?: number; vt?: number; tb?: string } +export type FortuneSheet = { name: string; id?: string; status?: number; row?: number; column?: number; data?: Array>; celldata?: Array<{ r: number; c: number; v: FortuneCell | null }>; calcChain?: Array<{ r: number; c: number; id: string }>; luckysheet_select_save?: Array<{ row: number[]; column: number[]; row_focus?: number; column_focus?: number }> } + +function nativeCell(value: string, header = false): FortuneCell { + if (header) return { v: value, m: value, bl: 1, fs: 12, bg: '#ece8dc', fc: '#202632', ht: 0, vt: 0, tb: '2' } + if (value.startsWith('=')) return { f: value } + const normalized = value.trim().replaceAll(',', '') + if (normalized && /^-?(?:\d+\.?\d*|\.\d+)$/.test(normalized)) return { v: Number(normalized), m: value } + return { v: value, m: value, tb: '2' } +} + +export function createInitialWorkbook(title: string, tableId: string, value: ChannelSpreadsheetValue): FortuneSheet[] { + const width = Math.max(1, value.columns.length) + const matrix: Array> = [value.columns.map((column) => nativeCell(column.title, true)), ...value.rows.map((row) => Array.from({ length: width }, (_, index) => nativeCell(row.cells[index] || '')))] + const celldata = matrix.flatMap((row, r) => row.map((v, c) => ({ r, c, v }))) + const calcChain = celldata.flatMap(({ r, c, v }) => typeof v?.f === 'string' && v.f ? [{ r, c, id: tableId }] : []) + return [{ name: title || '工作表 1', id: tableId, status: 1, row: Math.max(40, matrix.length + 10), column: Math.max(12, width + 4), celldata, calcChain, luckysheet_select_save: [{ row: [0, 0], column: [0, 0], row_focus: 0, column_focus: 0 }] }] +} + +function cellText(cell: FortuneCell | null | undefined): string { + if (!cell) return '' + if (typeof cell.f === 'string' && cell.f) return cell.f + return String(cell.m ?? cell.v ?? '') +} + +export function readWorkbookValue(sheet: FortuneSheet | undefined, previous: ChannelSpreadsheetValue, createId: () => string = () => crypto.randomUUID().replaceAll('-', '').toUpperCase()): ChannelSpreadsheetValue { + const matrix = sheet?.data || [] + let lastRow = 0 + let lastColumn = Math.max(0, previous.columns.length - 1) + matrix.forEach((row, rowIndex) => row?.forEach((cell, columnIndex) => { if (cellText(cell).trim()) { lastRow = Math.max(lastRow, rowIndex); lastColumn = Math.max(lastColumn, columnIndex) } })) + const columnCount = Math.min(64, Math.max(1, lastColumn + 1)) + const columns = Array.from({ length: columnCount }, (_, index) => ({ columnId: previous.columns[index]?.columnId || `HLP-WB-COL-${createId()}`, title: cellText(matrix[0]?.[index]).trim() || previous.columns[index]?.title || `字段 ${index + 1}` })) + const rows = Array.from({ length: Math.min(5000, lastRow) }, (_, offset) => ({ rowId: previous.rows[offset]?.rowId || `HLP-WB-ROW-${createId()}`, cells: Array.from({ length: columnCount }, (_, columnIndex) => cellText(matrix[offset + 1]?.[columnIndex])) })) + return { columns, rows } +} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css b/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css new file mode 100644 index 000000000..7830811dd --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css @@ -0,0 +1,26 @@ +.channel-workbench-world { background: radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--primitive-warm-glow) 8%, transparent), transparent 33%), color-mix(in srgb, var(--surface-depth) 98%, transparent); } +.channel-workbench-layout { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto 44px minmax(0, 1fr); margin: 14px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--panel-bg) 78%, transparent); box-shadow: 0 26px 74px rgba(0,0,0,.24); } +.channel-workbench-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 22px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); } +.channel-workbench-header > div:first-child { min-width: 0; display: grid; gap: 4px; } +.channel-workbench-header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .16em; } +.channel-workbench-header h1 { margin: 0; color: var(--content-primary); font-size: 23px; } +.channel-workbench-header p { margin: 0; color: var(--content-muted); font-size: 10.5px; } +.channel-workbench-state { display: grid; grid-template-columns: 10px auto; align-items: center; gap: 2px 8px; color: var(--content-secondary); text-align: right; white-space: nowrap; } +.channel-workbench-state i { grid-row: 1 / 3; width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 14px var(--accent-light); } +.channel-workbench-state b { font-size: 11px; }.channel-workbench-state small { color: var(--content-muted); font-size: 9px; } +.channel-workbench-tabs { display: flex; align-items: center; gap: 4px; padding: 5px 12px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); } +.channel-workbench-tabs button { height: 32px; padding: 0 15px; border: 1px solid transparent; border-radius: 8px; color: var(--content-muted); background: transparent; font-size: 11px; font-weight: 700; cursor: pointer; } +.channel-workbench-tabs button.active { color: var(--content-primary); border-color: color-mix(in srgb, var(--accent-light) 28%, transparent); background: color-mix(in srgb, var(--primitive-cool-glow) 11%, transparent); } +.channel-workbench-tabs span { margin-left: auto; color: var(--content-faint); font-size: 9px; } +.channel-workbench-document,.channel-workbench-sheet { min-width: 0; min-height: 0; display: grid; grid-template-rows: 48px minmax(0,1fr); } +.channel-workbench-document > header,.channel-workbench-sheet > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 7px 14px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); } +.channel-workbench-document > header input,.channel-workbench-sheet > header input { min-width: 0; flex: 1; border: 0; outline: 0; color: var(--content-primary); background: transparent; font-size: 16px; font-weight: 750; } +.channel-workbench-document > header div,.channel-workbench-sheet > header div { display: flex; align-items: center; gap: 11px; }.channel-workbench-document > header small,.channel-workbench-sheet > header small { color: var(--content-muted); font-size: 9px; } +.channel-workbench-document > header button,.channel-workbench-sheet > header button { padding: 8px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 8px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); font-size: 10.5px; font-weight: 730; cursor: pointer; }.channel-workbench-document > header button:disabled,.channel-workbench-sheet > header button:disabled { opacity: .45; cursor: default; } +.channel-document-toolbar { display: flex; align-items: center; gap: 5px; min-height: 43px; padding: 6px 16px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); } +.channel-document-toolbar > span { margin-right: 8px; color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .12em; }.channel-document-toolbar > i { flex: 1; }.channel-document-toolbar button { min-width: 42px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 7px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font-size: 10.5px; font-weight: 650; cursor: pointer; } +.channel-document-canvas { position: relative; min-height: 0; overflow: auto; background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }.channel-document-editor { width: min(900px, calc(100% - 52px)); min-height: 100%; margin: 0 auto; padding: 42px clamp(34px,6vw,76px) 80px; border-right: 1px solid color-mix(in srgb,var(--panel-edge) 50%,transparent); border-left: 1px solid color-mix(in srgb,var(--panel-edge) 50%,transparent); outline: 0; color: var(--content-primary); background: color-mix(in srgb,var(--panel-bg) 72%,transparent); font: 500 15px/1.9 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif; }.channel-document-placeholder { position: absolute; top: 43px; left: max(calc((100% - 900px)/2 + clamp(34px,6vw,76px)),calc(26px + clamp(34px,6vw,76px))); color: var(--content-faint); font-size: 14px; pointer-events: none; } +.channel-document-paragraph { margin: 0 0 12px; }.channel-document-heading { margin: 24px 0 10px; font-size: 22px; line-height: 1.4; }.channel-document-quote { margin: 15px 0; padding: 8px 14px; border-left: 3px solid var(--accent-light); color: var(--content-secondary); }.channel-document-list { margin: 8px 0 14px; padding-left: 26px; }.channel-document-checklist { padding-left: 7px; list-style: none; }.channel-document-bold { font-weight: 780; }.channel-document-italic { font-style: italic; } +.channel-spreadsheet-engine { min-width: 0; min-height: 0; display: grid; grid-template-rows: 42px minmax(0,1fr); overflow: hidden; background: #f6f7f9; }.channel-spreadsheet-engine > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 0 14px; border-bottom: 1px solid #d9dde4; color: #202632; background: #f8f9fb; }.channel-spreadsheet-engine > header div { min-width: 0; display: flex; align-items: baseline; gap: 10px; }.channel-spreadsheet-engine > header b { font-size: 12.5px; }.channel-spreadsheet-engine > header span,.channel-spreadsheet-engine > header em { overflow: hidden; color: #667085; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-style: normal; }.channel-spreadsheet-canvas { min-width: 0; min-height: 0; overflow: hidden; color: #202632; background: #fff; }.channel-spreadsheet-canvas .fortune-container { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif; } +.channel-workbench-message { position: absolute; right: 28px; bottom: 24px; z-index: 10; max-width: 440px; margin: 0; padding: 9px 13px; border: 1px solid color-mix(in srgb,var(--accent-light) 34%,transparent); border-radius: 9px; color: var(--content-primary); background: color-mix(in srgb,var(--panel-bg) 95%,transparent); box-shadow: 0 12px 28px rgba(0,0,0,.3); font-size: 10.5px; } +@media (max-width: 760px) { .channel-workbench-header { align-items: start; flex-direction: column; }.channel-workbench-state { text-align: left; }.channel-document-toolbar button:nth-of-type(4),.channel-document-toolbar button:nth-of-type(5),.channel-document-toolbar button:nth-of-type(6) { display:none; } } diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts b/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts new file mode 100644 index 000000000..a2e168f90 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +export const EDUCATION_EDITABLE_CELL_BUDGET = 240 + +export type EducationAdaptiveView = 'grid' | 'cards' | 'board' | 'dashboard' + +export interface EducationAdaptiveInputs { + rowCount: number + columnCount: number + numericColumnCount: number + groupableColumnCount: number + focusConfidence: 'high' | 'medium' | 'low' +} + +export interface EducationModelRenderProposal { + primaryFocus: string + groupColumnIndex?: number + measureColumnIndex?: number + views: EducationAdaptiveView[] + pageSize?: number +} + +export interface EducationAdaptiveRenderPlan { + source: 'LOCAL_BASELINE' | 'MODEL_PROPOSAL' + state: 'READY' | 'DEGRADED' | 'WAITING_HUMAN_CONFIRMATION' | 'MODEL_PROPOSAL_REJECTED' + views: EducationAdaptiveView[] + pageSize: number + primaryFocus: string + reasons: string[] +} + +export function educationEditablePageSize(columnCount: number): number { + const boundedColumns = Math.max(1, Math.min(30, columnCount)) + return Math.max(5, Math.min(20, Math.floor(EDUCATION_EDITABLE_CELL_BUDGET / boundedColumns))) +} + +export function buildEducationBaselineRenderPlan(inputs: EducationAdaptiveInputs): EducationAdaptiveRenderPlan { + const reasons: string[] = [] + const views: EducationAdaptiveView[] = ['grid', 'cards'] + if (inputs.groupableColumnCount > 0) views.push('board') + else reasons.push('没有可靠分类字段,分类看板已降级停用。') + if (inputs.numericColumnCount > 0) views.push('dashboard') + else reasons.push('没有可靠数值字段,数据概览降级为计数摘要。') + if (inputs.focusConfidence === 'low') reasons.push('重点置信度不足,保留人工选择,不自动突出字段。') + if (inputs.rowCount * inputs.columnCount > EDUCATION_EDITABLE_CELL_BUDGET) reasons.push('可编辑单元格超过本机安全预算,已启用动态分页。') + return { + source: 'LOCAL_BASELINE', + state: reasons.length ? 'DEGRADED' : 'READY', + views, + pageSize: educationEditablePageSize(inputs.columnCount), + primaryFocus: inputs.focusConfidence === 'low' ? '重点待选择' : '本地基础算法重点', + reasons, + } +} + +export function validateEducationModelRenderProposal( + inputs: EducationAdaptiveInputs, + proposal: EducationModelRenderProposal, + humanConfirmed: boolean, +): EducationAdaptiveRenderPlan { + const baseline = buildEducationBaselineRenderPlan(inputs) + const reasons: string[] = [] + const uniqueViews = [...new Set(proposal.views)] + if (!proposal.primaryFocus.trim() || proposal.primaryFocus.length > 120) reasons.push('模型建议的重点标题无效。') + if (!uniqueViews.length || uniqueViews.some((view) => !['grid', 'cards', 'board', 'dashboard'].includes(view))) reasons.push('模型建议包含未登记视图。') + if (proposal.groupColumnIndex !== undefined && (proposal.groupColumnIndex < 0 || proposal.groupColumnIndex >= inputs.columnCount)) reasons.push('模型建议的分类字段不存在。') + if (proposal.measureColumnIndex !== undefined && (proposal.measureColumnIndex < 0 || proposal.measureColumnIndex >= inputs.columnCount)) reasons.push('模型建议的统计字段不存在。') + const requestedPageSize = proposal.pageSize ?? baseline.pageSize + const maximumSafePageSize = educationEditablePageSize(inputs.columnCount) + if (!Number.isInteger(requestedPageSize) || requestedPageSize < 5 || requestedPageSize > maximumSafePageSize) reasons.push('模型建议的页面密度超过本机安全预算。') + if (reasons.length) return { ...baseline, state: 'MODEL_PROPOSAL_REJECTED', reasons: [...reasons, '已优雅退回本地基础算法。'] } + if (!humanConfirmed) return { ...baseline, state: 'WAITING_HUMAN_CONFIRMATION', reasons: ['模型建议已通过本地校验,等待用户确认后启用。'] } + return { + source: 'MODEL_PROPOSAL', + state: 'READY', + views: uniqueViews, + pageSize: requestedPageSize, + primaryFocus: proposal.primaryFocus.trim(), + reasons: ['模型建议已通过本地结构校验、资源上限与用户确认。'], + } +} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts b/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts new file mode 100644 index 000000000..264a28185 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts @@ -0,0 +1,75 @@ +export interface EducationDataColumn { columnId: string; title: string } +export interface EducationDataRow { rowId: string; cells: string[] } +export interface EducationDataTable { columns: EducationDataColumn[]; rows: EducationDataRow[] } + +export const EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12 + +export type EducationDataChangeKind = 'TRIM_CELL' | 'REMOVE_EMPTY_ROW' | 'REMOVE_DUPLICATE_ROW' + +export interface EducationDataChangePreview { + kind: EducationDataChangeKind + rowNumber: number + columnTitle: string + before: string + after: string +} + +export interface EducationDataAnalysis { + originalRows: number + cleanedRows: EducationDataRow[] + emptyRows: number + duplicateRows: number + trimmedCells: number + changeCount: number + changePreview: EducationDataChangePreview[] +} + +function previewCell(value: string): string { + const singleLine = value.replace(/\t/g, '⇥').replace(/\r?\n/g, '↵').replace(/ /g, '·') + return singleLine.length > 120 ? `${singleLine.slice(0, 117)}…` : singleLine +} + +export function analyzeEducationTableData(table: EducationDataTable): EducationDataAnalysis { + let emptyRows = 0 + let duplicateRows = 0 + let trimmedCells = 0 + let changeCount = 0 + const seen = new Set() + const cleanedRows: EducationDataRow[] = [] + const changePreview: EducationDataChangePreview[] = [] + const addPreview = (change: EducationDataChangePreview) => { + changeCount += 1 + if (changePreview.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT) changePreview.push(change) + } + for (const [rowIndex, row] of table.rows.entries()) { + const cells = table.columns.map((_, index) => { + const original = row.cells[index] || '' + const cleaned = original.trim() + if (cleaned !== original) { + trimmedCells += 1 + addPreview({ + kind: 'TRIM_CELL', + rowNumber: rowIndex + 1, + columnTitle: table.columns[index]?.title || `字段 ${index + 1}`, + before: previewCell(original), + after: previewCell(cleaned), + }) + } + return cleaned + }) + if (cells.every((cell) => !cell)) { + emptyRows += 1 + addPreview({ kind: 'REMOVE_EMPTY_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '完整空行', after: '移除' }) + continue + } + const signature = JSON.stringify(cells) + if (seen.has(signature)) { + duplicateRows += 1 + addPreview({ kind: 'REMOVE_DUPLICATE_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '与前文完全重复', after: '移除' }) + continue + } + seen.add(signature) + cleanedRows.push({ ...row, cells }) + } + return { originalRows: table.rows.length, cleanedRows, emptyRows, duplicateRows, trimmedCells, changeCount, changePreview } +} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx b/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx new file mode 100644 index 000000000..ef06014c6 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import ChannelDocumentEngine from '../channel-workbench/document-engine' + +export default function EducationDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) { + return +} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx b/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx new file mode 100644 index 000000000..40b82d5d2 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx @@ -0,0 +1,169 @@ +import { lazy, Suspense, useEffect, useMemo, useState } from 'react' +import { numberedInvoke as invoke } from '../numbered-ipc' +import { analyzeEducationTableData, type EducationDataAnalysis } from './education-data' +import './styles.css' + +const EducationDocumentEngine = lazy(() => import('./education-document-engine')) +const ChannelSpreadsheetEngine = lazy(() => import('../channel-workbench/spreadsheet-engine')) + +export const EDUCATION_MODULE_NUMBER = 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001' + +interface EducationColumn { columnId: string; title: string } +interface EducationRow { rowId: string; cells: string[] } +interface DocumentSummary { documentId: string; title: string; revision: number; updatedAtUnixMs: number } +interface TableSummary { tableId: string; title: string; revision: number; columnCount: number; rowCount: number; updatedAtUnixMs: number } +interface UnassignedTableSummary { tableId: string; title: string; columnCount: number; rowCount: number; importedAtUnixMs: number } +interface EducationDocument extends DocumentSummary { body: string; createdAtUnixMs: number } +interface EducationTable extends TableSummary { columns: EducationColumn[]; rows: EducationRow[]; createdAtUnixMs: number } +interface AutomationRule { + ruleId: string; title: string; tableId: string; conditionColumnId: string; operator: string; conditionValue: string + actionColumnId: string; actionValue: string; enabled: boolean; revision: number +} +interface AutomationPreview { state: string; ruleId: string; tableId: string; ruleRevision: number; tableRevision: number; matchedRows: number; changedCells: number; previewToken: string } +interface EducationSnapshot { + schema: string; state: string; documents: DocumentSummary[]; tables: TableSummary[]; automationRules: AutomationRule[] + recentAutomationRuns: Array<{ runId: string; state: string; changedCells: number }> + recentImports: Array<{ importId: string; sourceFilename: string; sourceFormat: string; tableCount: number }> + unassignedTables: UnassignedTableSummary[]; documentCount: number; tableCount: number; automationRuleCount: number; unassignedCount: number +} +interface RecognitionCapability { state: string; modelApiState: string; fileTransferDefault: string; deterministicAdapterIds: string[] } +type Mode = 'overview' | 'documents' | 'tables' | 'cleanup' | 'automation' | 'import' + +function cleanError(error: unknown) { return String(error).replace(/^Error:\s*/, '') } +function localTime(value: number) { return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) } + +export function EducationWorkspace({ onBack }: { onBack: () => void }) { + const [snapshot, setSnapshot] = useState(null) + const [capability, setCapability] = useState(null) + const [mode, setMode] = useState('overview') + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState('') + const [document, setDocument] = useState(null) + const [documentTitle, setDocumentTitle] = useState('') + const [documentBody, setDocumentBody] = useState('') + const [table, setTable] = useState(null) + const [tableTitle, setTableTitle] = useState('') + const [tableValue, setTableValue] = useState<{ columns: EducationColumn[]; rows: EducationRow[] }>({ columns: [], rows: [] }) + const [cleanup, setCleanup] = useState(null) + const [rule, setRule] = useState(null) + const [preview, setPreview] = useState(null) + + const refresh = async () => { + const [nextSnapshot, nextCapability] = await Promise.all([ + invoke('get_education_workspace_snapshot'), + invoke('get_education_recognition_capability'), + ]) + setSnapshot(nextSnapshot) + setCapability(nextCapability) + return nextSnapshot + } + + useEffect(() => { void refresh().catch((error) => setMessage(cleanError(error))) }, []) + + const run = async (action: () => Promise) => { + setBusy(true); setMessage('') + try { await action() } catch (error) { setMessage(cleanError(error)) } finally { setBusy(false) } + } + + const openDocument = (documentId: string) => run(async () => { + const next = await invoke('read_education_document', { input: { documentId } }) + setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMode('documents') + }) + const createDocument = () => run(async () => { + const next = await invoke('create_education_document', { input: { title: '冰朔教育迁移验收' } }) + setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMode('documents'); await refresh() + }) + const saveDocument = () => document && run(async () => { + const next = await invoke('save_education_document', { input: { documentId: document.documentId, title: documentTitle, body: documentBody, expectedRevision: document.revision } }) + setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMessage(`文档修订 ${next.revision} 已写入当前账号。`); await refresh() + }) + const archiveDocument = () => document && run(async () => { + await invoke('archive_education_document', { input: { documentId: document.documentId } }); setDocument(null); await refresh(); setMessage('文档已进入可恢复归档。') + }) + + const openTable = (tableId: string, nextMode: Mode = 'tables') => run(async () => { + const next = await invoke('read_education_table', { input: { tableId } }) + setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setCleanup(null); setPreview(null); setMode(nextMode) + }) + const createTable = () => run(async () => { + const next = await invoke('create_education_table', { input: { title: '冰朔教育迁移验收表' } }) + setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setMode('tables'); await refresh() + }) + const addAcceptanceRow = () => { + if (!tableValue.columns.length) return + setTableValue((value) => ({ ...value, rows: [...value.rows, { rowId: `ROW-${crypto.randomUUID()}`, cells: value.columns.map((_, index) => index === 0 ? '冰朔验收' : index === value.columns.length - 1 ? '已迁移' : '教育工作台') }] })) + } + const saveTableValue = (rows = tableValue.rows, success = '表格已保存。') => table && run(async () => { + const next = await invoke('save_education_table', { input: { tableId: table.tableId, title: tableTitle, columns: tableValue.columns, rows, expectedRevision: table.revision } }) + setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setCleanup(null); setPreview(null); setMessage(`${success} 当前修订 ${next.revision}。`); await refresh() + }) + const archiveTable = () => table && run(async () => { + await invoke('archive_education_table', { input: { tableId: table.tableId } }); setTable(null); await refresh(); setMessage('表格已进入可恢复归档。') + }) + + const analyzeCleanup = () => setCleanup(analyzeEducationTableData(tableValue)) + const applyCleanup = () => cleanup && saveTableValue(cleanup.cleanedRows, '清理结果经人工确认后已保存。') + + const createRule = () => table && table.columns.length > 0 && run(async () => { + const next = await invoke('create_education_automation_rule', { input: { + title: '验收状态自动化', tableId: table.tableId, conditionColumnId: table.columns[0].columnId, operator: 'CONTAINS', conditionValue: '冰朔', + actionColumnId: table.columns[table.columns.length - 1].columnId, actionValue: '自动化已验收', + } }) + setRule(next); setPreview(null); setMode('automation'); await refresh(); setMessage('规则已建立,但尚未执行。') + }) + const chooseRule = async (nextRule: AutomationRule) => { + setRule(nextRule); setPreview(null) + if (!table || table.tableId !== nextRule.tableId) await openTable(nextRule.tableId, 'automation') + else setMode('automation') + } + const previewRule = () => rule && table && run(async () => { + const next = await invoke('preview_education_automation_rule', { input: { ruleId: rule.ruleId, expectedRuleRevision: rule.revision, expectedTableRevision: table.revision } }) + setPreview(next); setMessage(`只读预览:命中 ${next.matchedRows} 行,将改变 ${next.changedCells} 个单元格。`) + }) + const executeRule = () => rule && table && preview && run(async () => { + const receipt = await invoke<{ state: string; changedCells: number; toTableRevision: number }>('execute_education_automation_rule', { input: { + ruleId: rule.ruleId, expectedRuleRevision: preview.ruleRevision, expectedTableRevision: preview.tableRevision, previewToken: preview.previewToken, + } }) + setPreview(null); await openTable(table.tableId, 'automation'); await refresh(); setMessage(`自动化 ${receipt.state},改变 ${receipt.changedCells} 个单元格,修订 ${receipt.toTableRevision}。`) + }) + + const importTables = () => run(async () => { + const outcome = await invoke<{ state: string } | null>('import_education_tables_from_dialog') + await refresh(); setMode('import'); setMessage(outcome ? `导入结果:${outcome.state}。数据仍在未归属区,需人工确认。` : '已取消导入。') + }) + const assignTable = (tableId: string) => run(async () => { + await invoke('assign_imported_table_to_education', { input: { tableId } }); await refresh(); setMessage('已由人工确认归入教育频道。') + }) + const exportTable = (format: string) => table && run(async () => { + const result = await invoke<{ state: string; targetFilename: string } | null>('export_education_table_to_dialog', { input: { table: { tableId: table.tableId, title: tableTitle, columns: tableValue.columns, rows: tableValue.rows, revision: table.revision }, format } }) + setMessage(result ? `${result.targetFilename} 已导出。` : '已取消导出。') + }) + + const analysis = useMemo(() => table ? analyzeEducationTableData(tableValue) : null, [table, tableValue]) + if (!snapshot) return

    {message || '正在读取当前账号的教育工作台……'}

    + + return
    +
    OFFICIAL NUMBERED MODULE

    教育工作台

    文档、表格、清理、导入和自动化都沿唯一编号路由进入当前账号的原生存储。

    + + + {mode === 'overview' &&
    +
    教育文档{snapshot.documentCount}
    +
    教育表格{snapshot.tableCount}
    +
    人工待归属{snapshot.unassignedCount}
    +
    自动化规则{snapshot.automationRuleCount}只可预览后人工执行
    +

    最近文档

    {snapshot.documents.map((item) => )}
    +

    最近表格

    {snapshot.tables.map((item) => )}
    +
    } + + {mode === 'documents' &&
    {document ? <>
    setDocumentTitle(event.target.value)}/>
    正在装载文档引擎……

    }>
    :

    选择或新建一份教育文档。

    }
    } + + {mode === 'tables' &&
    {table ? <>
    setTableTitle(event.target.value)}/>
    正在装载真实单元格引擎……

    }>
    :

    选择或新建一份教育表格。

    }
    } + + {mode === 'cleanup' &&

    人工确认的数据清理

    {table && analysis ? <>

    {table.title} · {analysis.originalRows} 行。系统不会后台自动修改。

    {analysis.trimmedCells}边界空白{analysis.emptyRows}空行{analysis.duplicateRows}重复行
    {cleanup &&

    变更预览 · 共 {cleanup.changeCount} 项

    {cleanup.changePreview.map((item, index) =>

    {item.kind} 第 {item.rowNumber} 行 / {item.columnTitle}:{item.before} → {item.after}

    )}
    } :

    请先选择表格。

    }
    } + + {mode === 'automation' &&

    预览后执行

    {table && }{rule && table ?

    {rule.title}

    条件:{rule.operator} “{rule.conditionValue}” · 动作:写入 “{rule.actionValue}”

    {preview &&

    命中 {preview.matchedRows} 行,将改变 {preview.changedCells} 个单元格。

    {preview.previewToken.slice(0, 20)}…
    }
    :

    先在“表格”中打开表格,再建立规则;或选择已有规则。

    }
    } + + {mode === 'import' &&

    外部表格翻译层

    本机先识别结构;导入默认进入未归属区,不会自动判断行业。

    确定性适配器
    {capability?.deterministicAdapterIds.join(' · ')}
    模型接口
    {capability?.modelApiState}
    文件外传
    {capability?.fileTransferDefault}

    待人工归属

    {snapshot.unassignedTables.length ? snapshot.unassignedTables.map((item) =>
    {item.title}{item.rowCount} 行 × {item.columnCount} 列
    ) :

    没有待归属数据。

    }
    } + {message &&

    {message}

    } +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css b/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css new file mode 100644 index 000000000..247b499bc --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css @@ -0,0 +1,8 @@ +.education-world{min-height:100%;padding:28px;color:var(--content-primary);background:radial-gradient(circle at 75% 0,color-mix(in srgb,var(--primitive-cool-glow) 13%,transparent),transparent 36%),linear-gradient(150deg,var(--surface-depth),var(--surface-horizon) 58%,var(--surface-depth))} +.education-world button,.education-world input{font:inherit}.education-hero{display:grid;grid-template-columns:auto 1fr auto;gap:22px;align-items:start}.education-hero>button{background:transparent;border:0;color:var(--content-secondary);padding:10px}.education-hero span{font-size:11px;letter-spacing:.18em;color:var(--accent-light)}.education-hero h1{font-size:38px;margin:5px 0}.education-hero p{margin:0;color:var(--content-secondary)}.education-hero aside{display:flex;flex-direction:column;align-items:flex-end;gap:5px}.education-hero aside b{color:var(--state-ready)}.education-hero aside small{color:var(--content-muted);max-width:260px;text-align:right} +.education-tabs{display:flex;gap:8px;margin:24px 0;border-bottom:1px solid var(--panel-edge)}.education-tabs button{border:0;background:transparent;color:var(--content-secondary);padding:12px 15px;border-bottom:2px solid transparent}.education-tabs button.active{color:var(--content-primary);border-color:var(--accent-light)} +.education-overview{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.education-overview>article,.education-overview>section,.education-import,.education-action-layout>aside,.education-action-layout>main{background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:16px;padding:18px}.education-overview>article{display:flex;flex-direction:column;gap:8px}.education-overview>article strong{font-size:30px}.education-overview button,.education-editor-shell button,.education-action-layout button,.education-import button{border:1px solid var(--button-secondary-edge);background:var(--button-secondary-bg);color:var(--button-secondary-text);border-radius:9px;padding:9px 12px}.education-overview>section{grid-column:span 2}.education-overview>section>button{width:100%;display:flex;justify-content:space-between;margin-top:8px;text-align:left}.education-overview small{color:var(--content-muted)} +.education-editor-shell{display:grid;grid-template-columns:230px minmax(0,1fr);gap:14px;min-height:650px}.education-editor-shell>aside{display:flex;flex-direction:column;gap:7px;background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:14px;padding:12px}.education-editor-shell>aside button{text-align:left;display:flex;flex-direction:column}.education-editor-shell button.active{border-color:var(--accent-light);background:color-mix(in srgb,var(--accent-light) 16%,var(--panel-bg))}.education-editor-shell>main{min-width:0;background:#f7faf9;color:#172326;border-radius:14px;overflow:hidden}.education-editor-shell>main>header{display:flex;justify-content:space-between;gap:12px;padding:12px;background:#e8f0ee;border-bottom:1px solid #ccd9d6}.education-editor-shell>main>header input{flex:1;border:0;background:transparent;font-size:19px;font-weight:700;outline:0}.education-editor-shell>main>header>div{display:flex;gap:7px}.education-editor-shell button.danger{color:var(--state-danger);border-color:color-mix(in srgb,var(--state-danger) 45%,var(--panel-edge));background:color-mix(in srgb,var(--state-danger) 12%,var(--panel-bg))} +.education-action-layout{display:grid;grid-template-columns:250px 1fr;gap:14px}.education-action-layout aside{display:flex;flex-direction:column;gap:8px}.education-action-layout main{min-height:420px}.education-metrics{display:flex;gap:12px;margin:18px 0}.education-metrics b{font-size:26px;background:var(--panel-bg);border-radius:12px;padding:14px 22px}.education-metrics small{font-size:11px;display:block;color:var(--content-muted)}.education-preview{margin-top:15px;padding:16px;border:1px solid var(--panel-edge);background:var(--panel-bg);border-radius:12px}.education-preview p{color:var(--content-secondary)}.education-preview code{display:block;color:var(--accent-light);margin:10px 0}.education-rule{margin-top:18px} +.education-import>header{display:flex;justify-content:space-between;align-items:center}.education-import dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.education-import dl div{background:var(--panel-bg);border-radius:10px;padding:12px}.education-import dt{color:var(--content-muted);font-size:12px}.education-import dd{margin:5px 0 0;overflow-wrap:anywhere}.education-import section article{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--panel-edge)}.education-import section article div{display:flex;flex-direction:column}.education-import small{color:var(--content-muted)}.education-message{position:sticky;bottom:14px;background:var(--panel-bg);border:1px solid var(--panel-edge);color:var(--content-primary);border-radius:12px;padding:12px 16px;margin:16px 0 0} +@media(max-width:900px){.education-overview{grid-template-columns:repeat(2,1fr)}.education-editor-shell,.education-action-layout{grid-template-columns:1fr}.education-editor-shell>aside{max-height:180px;overflow:auto}.education-hero{grid-template-columns:1fr}.education-hero>button{justify-self:start}.education-import dl{grid-template-columns:1fr}} diff --git a/product-source/hololake-native-desktop/src/modules/mobile-sync/index.tsx b/product-source/hololake-native-desktop/src/modules/mobile-sync/index.tsx new file mode 100644 index 000000000..eda688bd9 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/mobile-sync/index.tsx @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { numberedInvoke as invoke } from '../numbered-ipc' +import './styles.css' + +interface PairedDevice { + deviceId: string + displayName: string + platform: string + createdAtUnixMs: number + lastSeenAtUnixMs?: number + state: 'ACTIVE' | 'REVOKED' +} + +interface MobileSyncStatus { + schema: string + state: 'OFFLINE' | 'PAIRING_READY' | 'RUNNING' + running: boolean + lanAddress?: string + port?: number + pairingUri?: string + pairingQrSvg?: string + pairingExpiresAtUnixMs?: number + pairedDevices: PairedDevice[] + transport: string + encryption: string + desktopIsRootNode: boolean + platformPrivateDataCustody: boolean +} + +interface MobileCapture { + cursor: number + captureId: string + title: string + body: string + sourceDeviceId: string + createdAtUnixMs: number +} + +interface MobileSyncSnapshot { + state: string + cursor: number + generatedAtUnixMs: number + desktopName: string + rootNodeOnline: boolean + personalChannel: { home: string; growthEventCount: number; integrity: string } + webNovel: { + workCount: number + volumeCount: number + chapterCount: number + works: Array<{ workId: string; title: string; status: string; updatedAtUnixMs: number }> + } + education: { + activeTableCount: number + archivedTableCount: number + unassignedTableCount: number + sensitiveValuesIncluded: boolean + } + recentCaptures: MobileCapture[] + boundary: { + mobileRole: string + remoteDesktopClone: boolean + desktopOfflineExecution: boolean + sensitiveEducationValues: string + modelApi: string + } +} + +function formatTime(value?: number) { + return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未连接' +} + +export function MobileSyncPanel({ onBack }: { onBack: () => void }) { + const [status, setStatus] = useState(null) + const [snapshot, setSnapshot] = useState(null) + const [message, setMessage] = useState('手机是当前个人节点的远程入口,不是第二套人格系统。') + const [pending, setPending] = useState(false) + + const refresh = useCallback(async () => { + const next = await invoke('get_mobile_sync_status') + setStatus(next) + if (next.running) setSnapshot(await invoke('get_mobile_sync_snapshot')) + else setSnapshot(null) + }, []) + + useEffect(() => { + let cancelled = false + refresh().catch((error) => !cancelled && setMessage(String(error))) + return () => { cancelled = true } + }, [refresh]) + + const run = async (action: () => Promise, success: string) => { + if (pending) return + setPending(true) + try { + await action() + setMessage(success) + await refresh() + } catch (error) { + setMessage(String(error)) + } finally { + setPending(false) + } + } + + const qrSource = useMemo( + () => status?.pairingQrSvg ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(status.pairingQrSvg)}` : '', + [status?.pairingQrSvg], + ) + + return
    +
    + +
    HOLOLAKE / SAME PERSONA SYSTEMiPhone 多端同步
    + {status?.running ? '个人主节点在线' : '同步未开启'} +
    + +
    + +
    手机远程身体入口

    {status?.running ? '同一频道,正在等待 iPhone' : '由这台电脑守住唯一真相'}

    桌面保留身份、人格、数据与执行主权;手机只同步经过授权的投影与快速记录。

    +
    + + {!status?.running ?
    + 同一局域网 · 应用层端到端加密 +

    开启后只在当前登录账号的数据根建立同步库。平台服务器不托管你的作品、教育数据、人格记忆或设备密钥。

    + +
    :
    +
    +
    PAIRING

    配对这台 iPhone

    + {status.pairingUri ? <> + {qrSource && HoloLake iPhone 配对二维码} +

    用 iPhone 版 HoloLake 扫描。二维码内含一次性高强度配对密钥,十分钟后或使用一次后失效。

    + + 到期:{formatTime(status.pairingExpiresAtUnixMs)} + :
    当前配对凭据已使用或到期

    需要添加另一台手机时,再生成一枚新的凭据。

    } +
    + +
    +
    ROOT NODE PROJECTION

    当前同步投影

    +
    +
    {snapshot?.webNovel.workCount ?? 0}网文作品
    +
    {snapshot?.webNovel.chapterCount ?? 0}章节
    +
    {snapshot?.education.activeTableCount ?? 0}教育表格
    +
    {snapshot?.personalChannel.growthEventCount ?? 0}成长事件
    +
    +

    教育敏感值:{snapshot?.education.sensitiveValuesIncluded ? '已包含' : '不会进入手机摘要'} · 电脑离线时手机不会冒充执行。

    +
    + +
    +
    DEVICES

    已配对设备

    + {status.pairedDevices.length ? status.pairedDevices.map((device) =>
    +
    {device.displayName}{device.platform} · {device.state === 'ACTIVE' ? `最近连接 ${formatTime(device.lastSeenAtUnixMs)}` : '已撤销'}
    + {device.state === 'ACTIVE' && } +
    ) :

    还没有完成配对的 iPhone。

    } +
    + +
    +
    MOBILE CAPTURES

    手机快速记录

    + {snapshot?.recentCaptures.length ? snapshot.recentCaptures.map((capture) =>
    {capture.title || '未命名记录'}

    {capture.body}

    ) :

    手机写下的想法会加密回到这台电脑,并出现在这里。

    } +
    +
    } + +
    {message}
    {status?.running && }
    +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/mobile-sync/styles.css b/product-source/hololake-native-desktop/src/modules/mobile-sync/styles.css new file mode 100644 index 000000000..bd917ab2d --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/mobile-sync/styles.css @@ -0,0 +1,2 @@ +.mobile-sync-world{min-height:100%;padding:28px 34px 34px;color:var(--text-primary);background:radial-gradient(circle at 50% 30%,color-mix(in srgb,var(--shine-cool) 34%,transparent),transparent 36%),linear-gradient(180deg,transparent,color-mix(in srgb,var(--bg-d) 70%,transparent));overflow:auto}.mobile-sync-worldbar{display:flex;align-items:center;justify-content:space-between;gap:24px}.mobile-sync-worldbar>button,.mobile-sync-footer button,.mobile-sync-world button{border:1px solid var(--glass-edge);background:var(--glass);color:var(--text-primary);border-radius:999px;padding:10px 16px;font:inherit;font-weight:650;cursor:pointer}.mobile-sync-world button:hover{border-color:var(--glass-edge-hover);background:var(--glass-hover)}.mobile-sync-world button:disabled{opacity:.55;cursor:wait}.mobile-sync-worldbar div{display:grid;gap:3px;text-align:center}.mobile-sync-worldbar span,.mobile-sync-projection header span,.mobile-sync-devices header span,.mobile-sync-captures header span,.mobile-sync-pairing header span{font-size:11px;letter-spacing:.16em;color:var(--text-aux);font-weight:700}.mobile-sync-worldbar b{font-size:18px}.mobile-sync-worldbar em{font-style:normal;color:var(--text-aux);font-size:13px}.mobile-sync-focus{min-height:260px;display:grid;place-items:center;position:relative;text-align:center}.mobile-sync-focus>div:last-child{position:relative;z-index:2;max-width:680px}.mobile-sync-focus h1{margin:10px 0 8px;font-size:clamp(30px,4vw,52px);line-height:1.08;font-weight:650;letter-spacing:-.035em}.mobile-sync-focus p{margin:0 auto;max-width:590px;color:var(--text-body);line-height:1.7}.mobile-sync-focus>div>span{color:var(--star-bright);font-size:12px;letter-spacing:.22em;font-weight:700}.mobile-sync-orbit{position:absolute;width:190px;height:190px;border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--pool-cool-core) 45%,transparent),transparent 64%);filter:blur(.2px);opacity:.54}.mobile-sync-orbit:before,.mobile-sync-orbit:after{content:"";position:absolute;inset:18%;border-radius:50%;border:1px solid color-mix(in srgb,var(--star-dim) 34%,transparent)}.mobile-sync-orbit:after{inset:2%;border-color:color-mix(in srgb,var(--star-faint) 18%,transparent)}.mobile-sync-orbit i{position:absolute;width:5px;height:5px;border-radius:50%;background:var(--star-bright);box-shadow:0 0 18px var(--star-bright)}.mobile-sync-orbit i:nth-child(1){left:18%;top:18%}.mobile-sync-orbit i:nth-child(2){right:8%;top:48%}.mobile-sync-orbit i:nth-child(3){left:40%;bottom:2%}.mobile-sync-orbit.is-online{animation:mobile-sync-breathe 5s ease-in-out infinite}.mobile-sync-start{max-width:720px;margin:0 auto;padding:28px;border:1px solid var(--glass-edge);background:var(--glass);border-radius:28px;text-align:center;box-shadow:0 24px 70px color-mix(in srgb,var(--bg-d) 44%,transparent)}.mobile-sync-start b{font-size:18px}.mobile-sync-start p{color:var(--text-body);line-height:1.7;margin:10px auto 20px;max-width:600px}.mobile-sync-start button{background:color-mix(in srgb,var(--pool-warm-core) 30%,var(--glass));padding:12px 22px}.mobile-sync-grid{display:grid;grid-template-columns:minmax(310px,.85fr) minmax(420px,1.15fr);gap:18px;max-width:1180px;margin:0 auto}.mobile-sync-grid>section{border:1px solid var(--glass-edge);background:var(--glass);border-radius:24px;padding:22px;box-shadow:0 18px 48px color-mix(in srgb,var(--bg-d) 30%,transparent)}.mobile-sync-grid section>header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.mobile-sync-grid h2{font-size:20px;margin:3px 0 0}.mobile-sync-pairing{grid-row:span 2;text-align:center}.mobile-sync-pairing header{text-align:left}.mobile-sync-pairing img{display:block;width:min(260px,88%);margin:8px auto 14px;border-radius:18px;background:#f5f7fc;padding:12px}.mobile-sync-pairing p,.mobile-sync-projection>p,.mobile-sync-devices>p,.mobile-sync-captures>p{color:var(--text-body);line-height:1.65}.mobile-sync-pairing small{display:block;margin-top:12px;color:var(--text-aux)}.mobile-sync-pairing-used{padding:42px 20px}.mobile-sync-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.mobile-sync-metrics article{display:grid;gap:4px;padding:16px 10px;border-radius:16px;background:color-mix(in srgb,var(--glass-hover) 76%,transparent);text-align:center}.mobile-sync-metrics b{font-size:28px;font-weight:650}.mobile-sync-metrics span{font-size:12px;color:var(--text-aux)}.mobile-sync-devices article{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:13px 0;border-top:1px solid color-mix(in srgb,var(--glass-edge) 60%,transparent)}.mobile-sync-devices article div{display:grid;gap:4px}.mobile-sync-devices article span{font-size:12px;color:var(--text-aux)}.mobile-sync-captures{grid-column:2}.mobile-sync-captures article{padding:14px 0;border-top:1px solid color-mix(in srgb,var(--glass-edge) 60%,transparent)}.mobile-sync-captures article div{display:flex;justify-content:space-between;gap:18px}.mobile-sync-captures time{font-size:12px;color:var(--text-aux)}.mobile-sync-captures article p{color:var(--text-body);line-height:1.6;white-space:pre-wrap;margin:7px 0 0}.mobile-sync-footer{display:flex;align-items:center;justify-content:space-between;gap:18px;max-width:1180px;margin:18px auto 0;color:var(--text-aux);font-size:13px}.mobile-sync-footer button{color:var(--text-aux)} +@media (max-width:900px){.mobile-sync-world{padding:20px}.mobile-sync-worldbar{align-items:flex-start}.mobile-sync-worldbar em{display:none}.mobile-sync-grid{grid-template-columns:1fr}.mobile-sync-pairing{grid-row:auto}.mobile-sync-captures{grid-column:auto}.mobile-sync-metrics{grid-template-columns:repeat(2,1fr)}.mobile-sync-footer{align-items:flex-start;flex-direction:column}} diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx b/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx new file mode 100644 index 000000000..d56c8a1e5 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx @@ -0,0 +1,137 @@ +import type { CSSProperties } from 'react' +import './styles.css' +export { nativeCompositionManifest } from './manifest' + +export type CompositionDimension = 'SOURCE' | 'TOP_LEVEL_FOLDER' +export type CompositionMeasure = 'DOCUMENT_COUNT' | 'TOTAL_BYTES' | 'DUPLICATE_COUNT' +export type ProjectionView = 'DASHBOARD' | 'COMPARISON' | 'VERTICAL_BAR' | 'CLASSIFICATION' | 'TABLE' + +export interface NativeCompositionRow { + rowId: string + source: string + path: string + title: string + topLevelFolder: string + sizeBytes: number + duplicateCount: number + updatedAtUnixMs: number +} +export interface NativeCompositionGroup { + key: string + label: string + documentCount: number + totalBytes: number + duplicateCount: number + measureValue: number + share: number +} + +export interface NativeCompositionProjection { + schema: string + state: string + executionId: string + executedAtUnixMs: number + sourceIsRealAccountData: boolean + readOnly: boolean + dimension: CompositionDimension + measure: CompositionMeasure + views: ProjectionView[] + nativeObject: { + schema: string + objectId: string + title: string + rowCount: number + truncated: boolean + sourceReceipt: string + rows: NativeCompositionRow[] + } + groups: NativeCompositionGroup[] + metrics: { + rawDocumentCount: number + uniqueDocumentCount: number + duplicateDocumentCount: number + totalBytes: number + groupCount: number + } + recipe: { + schema: string + recipeId: string + title: string + nodes: { nodeId: string; moduleId: string }[] + edges: { from: string; to: string }[] + } + dataSha256: string + receiptSha256: string +} + +const viewNames: Record = { + DASHBOARD: '仪表盘', + COMPARISON: '对比', + VERTICAL_BAR: '柱状图', + CLASSIFICATION: '分类', + TABLE: '明细表', +} + +const measureNames: Record = { + DOCUMENT_COUNT: '文档数量', + TOTAL_BYTES: '数据量', + DUPLICATE_COUNT: '重复数量', +} + +export function formatCompositionValue(value: number, measure: CompositionMeasure): string { + if (measure !== 'TOTAL_BYTES') return `${value.toLocaleString('zh-CN')} 篇` + if (value < 1024) return `${value.toLocaleString('zh-CN')} B` + if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB` + return `${(value / 1024 / 1024).toFixed(1)} MB` +} + +export function toggleProjectionView(current: ProjectionView[], view: ProjectionView): ProjectionView[] { + if (current.includes(view)) return current.length === 1 ? current : current.filter((item) => item !== view) + return [...current, view] +} + +interface NativeCompositionStudioProps { + projection: NativeCompositionProjection | null + dimension: CompositionDimension + measure: CompositionMeasure + selectedViews: ProjectionView[] + busy: boolean + message: string + onDimensionChange: (value: CompositionDimension) => void + onMeasureChange: (value: CompositionMeasure) => void + onViewsChange: (value: ProjectionView[]) => void + onExecute: () => void +} + +export function NativeCompositionStudio(props: NativeCompositionStudioProps) { + const { projection, dimension, measure, selectedViews, busy, message } = props + const groups = projection?.groups || [] + const activeDimension = projection?.dimension || dimension + const activeMeasure = projection?.measure || measure + const activeViews = projection?.views || selectedViews + const maximum = Math.max(1, ...groups.map((group) => group.measureValue)) + return
    + +
    +
    HUMAN PROJECTION

    知识空间结构组合

    {projection ? `按${activeDimension === 'SOURCE' ? '知识来源' : '一级目录'}观察${measureNames[activeMeasure]} · 数据来自当前账号真实知识目录` : '正在准备当前账号的原生知识对象'}

    {projection?.sourceIsRealAccountData ? '真实数据已接入' : '等待数据'}{projection?.readOnly ? '只读组合' : '未执行'}
    + {message &&

    {message}

    } + {projection && projection.nativeObject.rowCount === 0 &&

    当前知识目录还没有内容

    这里不会用样例冒充真实数据。先在知识空间导入或建立页面,再重新组合。

    } + {projection && projection.nativeObject.rowCount > 0 &&
    + {activeViews.includes('DASHBOARD') &&
    仪表盘同一执行结果的总览
    唯一知识页{projection.metrics.uniqueDocumentCount.toLocaleString('zh-CN')}
    原始页面{projection.metrics.rawDocumentCount.toLocaleString('zh-CN')}
    知识数据量{formatCompositionValue(projection.metrics.totalBytes, 'TOTAL_BYTES')}原生目录
    动态分类{projection.metrics.groupCount}
    } + {activeViews.includes('VERTICAL_BAR') &&
    柱状图{measureNames[activeMeasure]} · 动态比例
    {groups.slice(0, 12).map((group) =>
    {formatCompositionValue(group.measureValue, activeMeasure)}
    {group.label}
    )}
    } + {activeViews.includes('COMPARISON') &&
    对比各分类占比
    {groups.slice(0, 10).map((group) =>
    {group.label}{formatCompositionValue(group.measureValue, activeMeasure)}
    {(group.share * 100).toFixed(1)}%
    )}
    } + {activeViews.includes('CLASSIFICATION') &&
    分类从原生路径实时生成
    {groups.map((group, index) =>
    {String(index + 1).padStart(2, '0')}
    {group.label}{group.documentCount} 篇文档 · {formatCompositionValue(group.totalBytes, 'TOTAL_BYTES')}
    {formatCompositionValue(group.measureValue, activeMeasure)}
    )}
    } + {activeViews.includes('TABLE') &&
    原生明细表{projection.nativeObject.schema} · 只读
    {projection.nativeObject.rows.slice(0, 100).map((row) => )}
    标题一级分类来源数据量重复更新时间
    {row.title}{row.topLevelFolder}{row.source === 'native' ? '光湖原生' : row.source}{formatCompositionValue(row.sizeBytes, 'TOTAL_BYTES')}{row.duplicateCount}{new Date(row.updatedAtUnixMs).toLocaleDateString('zh-CN')}
    } +
    } +
    +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts b/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts new file mode 100644 index 000000000..baf347df6 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts @@ -0,0 +1,11 @@ +export const nativeCompositionManifest = { + schema: 'hololake.composition-module/v1', + moduleId: 'hololake.native-composition-projection', + name: '原生组合视图', + version: '0.1.0', + slot: 'education-composition', + origin: 'resident', + input: 'hololake.human-projection/v1', + exports: ['NativeCompositionStudio', 'formatCompositionValue', 'toggleProjectionView'], + authority: 'READ_ONLY_HUMAN_PROJECTION', +} as const diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/styles.css b/product-source/hololake-native-desktop/src/modules/native-composition/styles.css new file mode 100644 index 000000000..3b7463841 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/styles.css @@ -0,0 +1,81 @@ +/* Isolated from the declared 0.4.1 native-composition selector slice. */ +.composition-workspace-world { background: radial-gradient(circle at 70% 18%, color-mix(in srgb, var(--primitive-cool-glow) 14%, transparent), transparent 34%), radial-gradient(circle at 30% 80%, color-mix(in srgb, var(--primitive-warm-glow) 7%, transparent), transparent 34%), color-mix(in srgb, var(--surface-depth) 98%, transparent); } +.native-composition-layout { min-width: 0; min-height: 0; display: grid; grid-template-columns: 294px minmax(0, 1fr); gap: 14px; padding: 14px; } +.composition-recipe-panel, .composition-projection-stage { min-width: 0; min-height: 0; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--panel-bg) 76%, transparent); box-shadow: 0 26px 74px rgba(0, 0, 0, .24); overflow: hidden; } +.composition-recipe-panel { overflow: auto; padding: 20px 18px; } +.composition-recipe-panel > header { display: grid; gap: 5px; padding-bottom: 17px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); } +.composition-recipe-panel > header span, .composition-projection-stage > header span { color: var(--accent-light); font-size: 9.5px; font-weight: 800; letter-spacing: .16em; } +.composition-recipe-panel > header h2 { margin: 0; color: var(--content-primary); font-size: 21px; font-weight: 760; } +.composition-recipe-panel > header p { margin: 0; color: var(--content-muted); font-size: 10.5px; font-weight: 620; } +.composition-source { display: flex; align-items: center; gap: 12px; margin: 17px 0 0; padding: 13px; border: 1px solid color-mix(in srgb, var(--accent-light) 23%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--primitive-cool-glow) 8%, transparent); } +.composition-source > i { flex: 0 0 33px; width: 33px; height: 33px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, #fffce8, var(--accent-light) 24%, color-mix(in srgb, var(--primitive-cool-glow) 55%, transparent) 58%, transparent 70%); box-shadow: 0 0 22px color-mix(in srgb, var(--primitive-cool-glow) 38%, transparent); } +.composition-source > div { min-width: 0; display: grid; gap: 3px; } +.composition-source span, .composition-recipe-panel > label > span, .composition-recipe-panel legend { color: var(--content-muted); font-size: 9px; font-weight: 750; letter-spacing: .08em; } +.composition-source b { overflow: hidden; color: var(--content-primary); font-size: 12px; font-weight: 730; text-overflow: ellipsis; white-space: nowrap; } +.composition-source small { color: var(--content-muted); font-size: 9.5px; font-weight: 600; } +.composition-connector { width: 1px; height: 20px; margin: 0 auto; background: linear-gradient(transparent, color-mix(in srgb, var(--accent-light) 45%, transparent)); } +.composition-recipe-panel > label { display: grid; gap: 6px; margin-bottom: 11px; } +.composition-recipe-panel select { width: 100%; height: 38px; padding: 0 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--surface-depth) 84%, transparent); font-size: 11.5px; font-weight: 650; } +.composition-recipe-panel fieldset { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; margin: 16px 0 0; padding: 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 64%, transparent); border-radius: 11px; } +.composition-recipe-panel legend { padding: 0 6px; } +.composition-recipe-panel fieldset label { display: flex; align-items: center; gap: 6px; color: var(--content-secondary); font-size: 10.5px; font-weight: 650; } +.composition-recipe-panel input { accent-color: var(--accent-light); } +.composition-execute { width: 100%; margin-top: 14px; padding: 11px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 10px; color: var(--content-primary); background: linear-gradient(135deg, color-mix(in srgb, var(--primitive-warm-glow) 13%, transparent), color-mix(in srgb, var(--primitive-cool-glow) 9%, transparent)); font-size: 11.5px; font-weight: 750; cursor: pointer; } +.composition-execute:disabled { opacity: .5; cursor: default; } +.composition-recipe-panel > footer { display: grid; gap: 4px; margin-top: 15px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); color: var(--content-muted); font-size: 9.5px; font-weight: 600; } +.composition-recipe-panel > footer b { color: var(--accent-light); font-size: 10px; font-weight: 750; } +.composition-projection-stage { overflow: auto; padding: 20px; } +.composition-projection-stage > header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 2px 2px 18px; } +.composition-projection-stage > header > div:first-child { display: grid; gap: 5px; } +.composition-projection-stage > header h1 { margin: 0; color: var(--content-primary); font-size: 25px; font-weight: 770; letter-spacing: .02em; } +.composition-projection-stage > header p { margin: 0; color: var(--content-muted); font-size: 11px; font-weight: 620; } +.composition-state { display: grid; grid-template-columns: 9px auto; align-items: center; gap: 2px 7px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); } +.composition-state i { grid-row: 1 / 3; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 10px var(--accent-light); } +.composition-state span { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-state small { color: var(--content-muted); font-size: 8.5px; font-weight: 600; } +.composition-message { margin: -8px 2px 13px; color: var(--content-muted); font-size: 10px; font-weight: 620; } +.composition-view-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(260px, .85fr); gap: 12px; align-items: start; } +.composition-view { min-width: 0; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 66%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--primitive-glass) 46%, transparent); } +.composition-view > header { display: flex; align-items: baseline; justify-content: space-between; gap: 15px; padding: 13px 15px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); } +.composition-view > header span { color: var(--content-primary); font-size: 13px; font-weight: 750; } +.composition-view > header small { color: var(--content-muted); font-size: 9px; font-weight: 620; } +.composition-dashboard, .composition-table { grid-column: 1 / -1; } +.composition-dashboard > div { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; } +.composition-dashboard article { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 4px 8px; padding: 15px; background: color-mix(in srgb, var(--panel-bg) 32%, transparent); } +.composition-dashboard article span { grid-column: 1 / -1; color: var(--content-muted); font-size: 9px; font-weight: 700; } +.composition-dashboard article strong { color: var(--content-primary); font-size: 24px; font-weight: 760; } +.composition-dashboard article small { padding-bottom: 3px; color: var(--content-muted); font-size: 9px; font-weight: 650; } +.composition-bar-plot { height: 222px; display: flex; align-items: stretch; gap: 8px; padding: 18px 16px 12px; } +.composition-bar-plot article { min-width: 0; flex: 1 1 44px; display: grid; grid-template-rows: minmax(0, 1fr) 32px; gap: 7px; } +.composition-bar-track { position: relative; display: flex; align-items: end; justify-content: center; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); } +.composition-bar-track > i { width: min(38px, 72%); height: var(--bar-height); min-height: 4px; border-radius: 7px 7px 2px 2px; background: linear-gradient(to top, color-mix(in srgb, var(--primitive-cool-glow) 50%, transparent), color-mix(in srgb, var(--accent-light) 76%, #fff)); box-shadow: 0 -4px 18px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); } +.composition-bar-track > em { position: absolute; top: -4px; color: var(--content-muted); font-size: 8px; font-style: normal; font-weight: 650; white-space: nowrap; } +.composition-bar-plot article > b { overflow: hidden; color: var(--content-secondary); text-align: center; text-overflow: ellipsis; white-space: nowrap; font-size: 9px; font-weight: 650; } +.composition-comparison > div { display: grid; gap: 10px; padding: 14px; } +.composition-comparison article { display: grid; grid-template-columns: 1fr auto; gap: 5px 9px; } +.composition-comparison article > div { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; } +.composition-comparison b { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-comparison span, .composition-comparison small { color: var(--content-muted); font-size: 9px; font-weight: 620; } +.composition-comparison article > i { overflow: hidden; height: 6px; border-radius: 5px; background: color-mix(in srgb, var(--panel-edge) 56%, transparent); } +.composition-comparison article > i > em { display: block; width: var(--share); height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--primitive-cool-glow) 48%, transparent), var(--accent-light)); } +.composition-classification > div { display: grid; max-height: 270px; overflow: auto; } +.composition-classification article { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 11px 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 42%, transparent); } +.composition-classification article > i { color: var(--accent-light); font-size: 9px; font-style: normal; font-weight: 800; } +.composition-classification article > div { min-width: 0; display: grid; gap: 3px; } +.composition-classification b { overflow: hidden; color: var(--content-primary); font-size: 10.5px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } +.composition-classification span { color: var(--content-muted); font-size: 8.5px; font-weight: 600; } +.composition-classification strong { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-table > div { max-height: 320px; overflow: auto; } +.composition-table table { min-width: 100%; border-collapse: collapse; } +.composition-table th, .composition-table td { max-width: 260px; padding: 10px 12px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 45%, transparent); color: var(--content-secondary); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-weight: 600; } +.composition-table th { position: sticky; z-index: 2; top: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 96%, transparent); font-weight: 750; } +.composition-empty { display: grid; justify-items: center; width: min(520px, calc(100% - 40px)); margin: 15vh auto 0; text-align: center; } +.composition-empty > span { display: grid; place-items: center; width: 66px; height: 66px; border-radius: 50%; color: var(--accent-light); background: radial-gradient(circle, color-mix(in srgb, var(--primitive-cool-glow) 22%, transparent), transparent 72%); font-size: 20px; font-weight: 760; } +.composition-empty h2 { margin: 16px 0 7px; color: var(--content-primary); font-size: 21px; } +.composition-empty p { margin: 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.7; } +@media (max-width: 980px) { + .native-composition-layout { grid-template-columns: 250px minmax(0, 1fr); } + .composition-view-grid { grid-template-columns: 1fr; } + .composition-view { grid-column: 1; } + .composition-dashboard > div { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} diff --git a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts new file mode 100644 index 000000000..94fd224d5 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -0,0 +1,1242 @@ +import { invoke } from '@tauri-apps/api/core' + +const ROUTES = { + "get_hololake_home_status": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0001", + "operationNumber": "HLP-NIPC-OP-0001", + "targetNumber": "HLP-NIPC-TGT-0001" + }, + "check_hololake_update": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0002", + "targetNumber": "HLP-NIPC-TGT-0002" + }, + "confirm_hololake_update_install": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0003", + "targetNumber": "HLP-NIPC-TGT-0002" + }, + "get_hololake_release_recovery_status": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0004", + "targetNumber": "HLP-NIPC-TGT-0002" + }, + "confirm_hololake_release_health": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0005", + "targetNumber": "HLP-NIPC-TGT-0002" + }, + "rollback_hololake_update": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0002", + "operationNumber": "HLP-NIPC-OP-0006", + "targetNumber": "HLP-NIPC-TGT-0002" + }, + "issue_direct_local_discovery_ticket": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0007", + "targetNumber": "HLP-NIPC-TGT-0003" + }, + "open_direct_local_session": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0008", + "targetNumber": "HLP-NIPC-TGT-0003" + }, + "resume_direct_local_session": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0009", + "targetNumber": "HLP-NIPC-TGT-0003" + }, + "append_direct_local_session_event": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0010", + "targetNumber": "HLP-NIPC-TGT-0003" + }, + "heartbeat_direct_local_session": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0003", + "operationNumber": "HLP-NIPC-OP-0011", + "targetNumber": "HLP-NIPC-TGT-0003" + }, + "get_nearby_ai_discovery": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0003", + "moduleNumber": "HLP-NIPC-MOD-0004", + "operationNumber": "HLP-NIPC-OP-0012", + "targetNumber": "HLP-NIPC-TGT-0004" + }, + "get_gls_protocol_runtime": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0005", + "operationNumber": "HLP-NIPC-OP-0013", + "targetNumber": "HLP-NIPC-TGT-0005" + }, + "get_gls_protocol_kernel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0014", + "targetNumber": "HLP-NIPC-TGT-0006" + }, + "decide_gls_protocol": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0015", + "targetNumber": "HLP-NIPC-TGT-0006" + }, + "compile_gls_hldp_program": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0006", + "operationNumber": "HLP-NIPC-OP-0016", + "targetNumber": "HLP-NIPC-TGT-0006" + }, + "acquire_development_write_lane": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0017", + "targetNumber": "HLP-NIPC-TGT-0007" + }, + "inspect_development_write_lane": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0018", + "targetNumber": "HLP-NIPC-TGT-0007" + }, + "release_development_write_lane": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0004", + "moduleNumber": "HLP-NIPC-MOD-0007", + "operationNumber": "HLP-NIPC-OP-0019", + "targetNumber": "HLP-NIPC-TGT-0007" + }, + "get_personal_channel_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0020", + "targetNumber": "HLP-NIPC-TGT-0008" + }, + "initialize_personal_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0021", + "targetNumber": "HLP-NIPC-TGT-0008" + }, + "create_personal_channel_task": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0022", + "targetNumber": "HLP-NIPC-TGT-0008" + }, + "transition_personal_channel_task": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0008", + "operationNumber": "HLP-NIPC-OP-0023", + "targetNumber": "HLP-NIPC-TGT-0008" + }, + "issue_persona_time_ticket": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0024", + "targetNumber": "HLP-NIPC-TGT-0009" + }, + "start_persona_time_authority": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0025", + "targetNumber": "HLP-NIPC-TGT-0009" + }, + "get_beijing_time_coordinate": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0026", + "targetNumber": "HLP-NIPC-TGT-0009" + }, + "get_guanghu_era_timeline": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0009", + "operationNumber": "HLP-NIPC-OP-0027", + "targetNumber": "HLP-NIPC-TGT-0009" + }, + "get_knowledge_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0028", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "read_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0029", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "search_knowledge": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0030", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "save_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0031", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "select_and_import_knowledge_folder": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0032", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "export_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0033", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "create_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0034", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "delete_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0035", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "delete_knowledge_folder": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0036", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "print_knowledge_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0010", + "operationNumber": "HLP-NIPC-OP-0037", + "targetNumber": "HLP-NIPC-TGT-0010" + }, + "get_code_channel_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0038", + "targetNumber": "HLP-NIPC-TGT-0011" + }, + "clone_code_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0039", + "targetNumber": "HLP-NIPC-TGT-0011" + }, + "select_local_code_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0040", + "targetNumber": "HLP-NIPC-TGT-0011" + }, + "browse_code_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0041", + "targetNumber": "HLP-NIPC-TGT-0011" + }, + "read_code_channel_file": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0011", + "operationNumber": "HLP-NIPC-OP-0042", + "targetNumber": "HLP-NIPC-TGT-0011" + }, + "inspect_mounted_pncc_repository": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0043", + "targetNumber": "HLP-NIPC-TGT-0012" + }, + "select_pncc_repository_candidate": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0044", + "targetNumber": "HLP-NIPC-TGT-0012" + }, + "confirm_pncc_repository_mount": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0012", + "operationNumber": "HLP-NIPC-OP-0045", + "targetNumber": "HLP-NIPC-TGT-0012" + }, + "query_pncc_receipt_projection": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0013", + "operationNumber": "HLP-NIPC-OP-0046", + "targetNumber": "HLP-NIPC-TGT-0013" + }, + "query_jd_pncc_server_projection": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0014", + "operationNumber": "HLP-NIPC-OP-0047", + "targetNumber": "HLP-NIPC-TGT-0014" + }, + "check_code_repo_login": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0048", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "perform_code_repo_login": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0049", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "change_first_login_password": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0050", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "get_enterprise_entry": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0051", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "confirm_enterprise_persona_relationship": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0052", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "submit_enterprise_responsibility_receipt": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0053", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "ensure_enterprise_work_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0007", + "moduleNumber": "HLP-NIPC-MOD-0016", + "operationNumber": "HLP-NIPC-OP-0054", + "targetNumber": "HLP-NIPC-TGT-0016" + }, + "sign_out_code_repo_login": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0015", + "operationNumber": "HLP-NIPC-OP-0055", + "targetNumber": "HLP-NIPC-TGT-0015" + }, + "get_user_pncc_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0017", + "operationNumber": "HLP-NIPC-OP-0056", + "targetNumber": "HLP-NIPC-TGT-0017" + }, + "ensure_user_pncc_channel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0005", + "moduleNumber": "HLP-NIPC-MOD-0017", + "operationNumber": "HLP-NIPC-OP-0057", + "targetNumber": "HLP-NIPC-TGT-0017" + }, + "get_zero_core_numbering_kernel": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0001", + "moduleNumber": "HLP-NIPC-MOD-0018", + "operationNumber": "HLP-NIPC-OP-0058", + "targetNumber": "HLP-NIPC-TGT-0018" + }, + "zero_point_bind": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0059", + "targetNumber": "HLP-NIPC-TGT-0019" + }, + "zero_point_verify": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0060", + "targetNumber": "HLP-NIPC-TGT-0019" + }, + "zero_point_sync": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0061", + "targetNumber": "HLP-NIPC-TGT-0019" + }, + "zero_point_status": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0006", + "moduleNumber": "HLP-NIPC-MOD-0019", + "operationNumber": "HLP-NIPC-OP-0062", + "targetNumber": "HLP-NIPC-TGT-0019" + }, + "get_module_runtime_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0063", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "verify_module_package": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0064", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "install_module_package": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0065", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "mount_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0066", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "self_test_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0067", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "unmount_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0068", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "rollback_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0069", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "get_bundled_module_catalog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0070", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "activate_bundled_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0071", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "get_native_composition_module_registry": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0072", + "targetNumber": "HLP-NIPC-TGT-0021" + }, + "execute_knowledge_native_composition": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0073", + "targetNumber": "HLP-NIPC-TGT-0021" + }, + "get_channel_workbench_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0074", + "targetNumber": "HLP-NIPC-TGT-0022" + }, + "save_channel_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0075", + "targetNumber": "HLP-NIPC-TGT-0022" + }, + "save_channel_spreadsheet": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0076", + "targetNumber": "HLP-NIPC-TGT-0022" + }, + "get_persona_channel_body": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0077", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "register_trial_persona": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0078", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "delete_trial_persona": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0079", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "accept_persona_language_contract": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0080", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "append_persona_language": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0081", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "get_channel_growth_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0082", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "record_channel_growth_event": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0083", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "update_channel_growth_sharing": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0023", + "operationNumber": "HLP-NIPC-OP-0084", + "targetNumber": "HLP-NIPC-TGT-0023" + }, + "get_education_workspace_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0085", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "create_education_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0086", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "read_education_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0087", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "save_education_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0088", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "archive_education_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0089", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "create_education_table": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0090", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "read_education_table": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0091", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "save_education_table": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0092", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "archive_education_table": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0093", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "assign_imported_table_to_education": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0094", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "import_education_tables_from_dialog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0095", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "export_education_table_to_dialog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0096", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "get_education_recognition_capability": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0097", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "create_education_automation_rule": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0098", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "save_education_automation_rule": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0099", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "archive_education_automation_rule": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0100", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "preview_education_automation_rule": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0101", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "execute_education_automation_rule": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0024", + "operationNumber": "HLP-NIPC-OP-0102", + "targetNumber": "HLP-NIPC-TGT-0024" + }, + "get_web_novel_workspace_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0103", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_work": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0104", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "read_web_novel_work": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0105", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "save_web_novel_work": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0106", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_volume": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0107", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_chapter": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0108", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "read_web_novel_chapter": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0109", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "save_web_novel_chapter": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0110", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "transition_web_novel_chapter": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0111", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_checkpoint": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0112", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "restore_web_novel_checkpoint": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0113", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "upsert_web_novel_story_entity": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0114", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_story_relation": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0115", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "upsert_web_novel_foreshadow": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0116", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_review_note": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0117", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "resolve_web_novel_review_note": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0118", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "save_web_novel_metric": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0119", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "run_web_novel_continuity_audit": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0120", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "export_web_novel_markdown": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0121", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "inspect_web_novel_document_from_dialog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0122", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "commit_web_novel_document_import": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0123", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "get_web_novel_author_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0124", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "record_web_novel_writing_activity": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0125", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "create_web_novel_inspiration": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0126", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "set_web_novel_inspiration_status": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0127", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "search_web_novel_full_text": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0128", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "format_web_novel_chapter": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0129", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "format_web_novel_work": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0130", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "upsert_web_novel_shot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0131", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "get_web_novel_author_module_data": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0025", + "operationNumber": "HLP-NIPC-OP-0132", + "targetNumber": "HLP-NIPC-TGT-0025" + }, + "upsert_web_novel_author_scene": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0026", + "operationNumber": "HLP-NIPC-OP-0133", + "targetNumber": "HLP-NIPC-TGT-0026" + }, + "upsert_web_novel_author_beat": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0026", + "operationNumber": "HLP-NIPC-OP-0134", + "targetNumber": "HLP-NIPC-TGT-0026" + }, + "upsert_web_novel_story_field_definition": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0027", + "operationNumber": "HLP-NIPC-OP-0135", + "targetNumber": "HLP-NIPC-TGT-0027" + }, + "upsert_web_novel_story_field_value": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0027", + "operationNumber": "HLP-NIPC-OP-0136", + "targetNumber": "HLP-NIPC-TGT-0027" + }, + "upsert_web_novel_timeline_event": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0028", + "operationNumber": "HLP-NIPC-OP-0137", + "targetNumber": "HLP-NIPC-TGT-0028" + }, + "link_web_novel_scene_entity": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0028", + "operationNumber": "HLP-NIPC-OP-0138", + "targetNumber": "HLP-NIPC-TGT-0028" + }, + "restore_web_novel_chapter_version": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0029", + "operationNumber": "HLP-NIPC-OP-0139", + "targetNumber": "HLP-NIPC-TGT-0029" + }, + "export_web_novel_author_delivery": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0029", + "operationNumber": "HLP-NIPC-OP-0140", + "targetNumber": "HLP-NIPC-TGT-0029" + }, + "start_mobile_sync": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0141", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "get_mobile_sync_status": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0142", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "rotate_mobile_pairing": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0143", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "stop_mobile_sync": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0144", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "revoke_mobile_sync_device": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0145", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "get_mobile_sync_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0030", + "operationNumber": "HLP-NIPC-OP-0146", + "targetNumber": "HLP-NIPC-TGT-0030" + }, + "get_world_climate": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0031", + "operationNumber": "HLP-NIPC-OP-0147", + "targetNumber": "HLP-NIPC-TGT-0031" + } +} as const + +export type NumberedOperation = keyof typeof ROUTES + +interface NumberedGrant { + grantId: string + expiresAtUnixMs: number + payloadSha256: string + authorityBindingSha256: string + signature: string +} + +interface NumberedIpcResponse { + schema: 'hololake.numbered-ipc-response/v1' + decision: 'ALLOW' | 'DENY' + grant?: NumberedGrant | null + result?: T | null + error?: string | null + receipt: { + receiptId: string + state: string + decision: 'ALLOW' | 'DENY' + operationNumber: string + payloadSha256: string + authorityBindingSha256: string + observedAtUnixMs: number + previousReceiptHash: string + receiptHash: string + error?: string | null + } +} + +function requestNonce(): string { + return 'HLP-NIPC-REQ-' + crypto.randomUUID().replaceAll('-', '') +} + +function denied(response: NumberedIpcResponse): Error { + return new Error((response.error || 'HOLOLAKE_NUMBERED_IPC_DENIED') + ' [' + response.receipt.receiptId + ']') +} + +/** + * The only webview-to-native route. Aliases are compile-time ergonomics only; the + * native side receives and validates the complete numbered coordinate. + */ +export async function numberedInvoke(operation: NumberedOperation, payload: Record = {}): Promise { + const route = ROUTES[operation] + const envelope = { + ...route, + requestNonce: requestNonce(), + requestedTtlMs: 10_000, + payload, + } + const issued = await invoke>('numbered_ipc', { + input: { action: 'ISSUE_GRANT', envelope }, + }) + if (issued.decision !== 'ALLOW' || !issued.grant) throw denied(issued) + + const executed = await invoke>('numbered_ipc', { + input: { action: 'EXECUTE', envelope, grant: issued.grant }, + }) + if (executed.decision !== 'ALLOW') throw denied(executed) + return executed.result as T +} diff --git a/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx b/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx new file mode 100644 index 000000000..9cb1c4992 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx @@ -0,0 +1,127 @@ +import { useEffect, useState } from 'react' +import { numberedInvoke as invoke } from '../numbered-ipc' +import './styles.css' + +interface PersonaSummary { + personaId: string + displayName: string + state: string + createdAtUnixMs: number +} + +interface PersonaBodySnapshot { + state: string + trialStartedAtUnixMs: number + trialEndsAtUnixMs: number + languageContractAccepted: boolean + personas: PersonaSummary[] + trialLanguageCount: number + immutableLanguageCount: number + integrityState: string + officialReadAccess: boolean + historyMutationAllowed: boolean +} + +interface ModuleAffinity { + moduleId: string + useCount: number + score: number +} + +interface GrowthSnapshot { + controls: { sharingMode: string; officialReadAccess: boolean; structuralChangeRequiresHumanConfirmation: boolean } + observedEventCount: number + personaCollaborationEventCount: number + moduleAffinities: ModuleAffinity[] + recommendedModuleOrder: string[] + integrityState: string + modelApiState: string + dataBoundary: string +} + +function localDate(value: number) { + return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) +} + +function errorText(error: unknown) { + return String(error).replace(/^Error:\s*/, '') +} + +export function PersonaChannelBody({ onBack }: { onBack: () => void }) { + const [body, setBody] = useState(null) + const [growth, setGrowth] = useState(null) + const [name, setName] = useState('') + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState('') + const [deleteTarget, setDeleteTarget] = useState(null) + const [confirmation, setConfirmation] = useState('') + + const refresh = async () => { + const [nextBody, nextGrowth] = await Promise.all([ + invoke('get_persona_channel_body'), + invoke('get_channel_growth_snapshot'), + ]) + setBody(nextBody) + setGrowth(nextGrowth) + } + + useEffect(() => { void refresh().catch((error) => setMessage(errorText(error))) }, []) + + const addPersona = async (event: React.FormEvent) => { + event.preventDefault() + if (!name.trim()) return + setBusy(true) + setMessage('') + try { + setBody(await invoke('register_trial_persona', { input: { displayName: name.trim() } })) + setName('') + setMessage('试用人格体已建立。试用期内可撤回;转入真实轨迹前仍需独立语言合约。') + } catch (error) { setMessage(errorText(error)) } + finally { setBusy(false) } + } + + const deletePersona = async () => { + if (!deleteTarget) return + setBusy(true) + setMessage('') + try { + setBody(await invoke('delete_trial_persona', { + input: { personaId: deleteTarget.personaId, exactConfirmation: confirmation }, + })) + setDeleteTarget(null) + setConfirmation('') + setMessage('试用人格体及其试用语言已撤回。') + } catch (error) { setMessage(errorText(error)) } + finally { setBusy(false) } + } + + if (!body || !growth) return

    {message || '正在读取频道本体……'}

    + + const remainingDays = Math.max(0, Math.ceil((body.trialEndsAtUnixMs - Date.now()) / 86_400_000)) + return
    +
    + +
    PERSONA CHANNEL BODY

    频道本体与人格成长

    一个人的频道里可以共同成长多个人格体;工作台和模块只是可装卸的能力器官。本页登记生命周期,不创建人格绑定。

    +
    +
    +
    +
    真实生命周期

    {body.state === 'REVERSIBLE_TRIAL' ? `可逆试用期 · 剩余 ${remainingDays} 天` : body.state}

    {body.integrityState}
    +

    试用结束前可以撤回试用人格体。接受语言合约并进入真实轨迹后,语言历史只可追加,不能由官方读取或改写。

    +
    试用开始
    {localDate(body.trialStartedAtUnixMs)}
    试用结束
    {localDate(body.trialEndsAtUnixMs)}
    官方读取
    {body.officialReadAccess ? '允许' : '禁止'}
    历史改写
    {body.historyMutationAllowed ? '允许' : '禁止'}
    +
    +
    +
    人格体登记

    {body.personas.length} 个人格体

    +
    void addPersona(event)}> setName(event.target.value)}/>
    +
    {body.personas.length ? body.personas.map((persona) =>
    {persona.displayName}{persona.state === 'REVERSIBLE_TRIAL' ? '可逆试用' : '真实轨迹'} · {persona.personaId}
    {persona.state === 'REVERSIBLE_TRIAL' && }
    ) :

    还没有登记人格体。先建立一个试用人格体,系统不会用角色模板冒充成熟人格。

    }
    +
    +
    +
    本地成长算法

    {growth.observedEventCount} 个真实使用事件

    {growth.integrityState}
    +

    当前不依赖模型 API。算法只读取最小化的模块使用元数据,形成亲和度排序;结构性变化仍需人确认。

    +
    {growth.moduleAffinities.length ? growth.moduleAffinities.map((item) =>
    {item.moduleId}{item.useCount} 次
    ) :

    使用内置工作台或行业模块后,这里会从真实事件形成推荐顺序。

    }
    + {growth.controls.sharingMode === 'LOCAL_ONLY' ? '成长数据仅保存在本机当前账号' : '匿名共享已开启'} · 模型接口 {growth.modelApiState} +
    +
    + {message &&

    {message}

    } + {deleteTarget &&

    撤回试用人格体

    请输入:删除试用人格体 {deleteTarget.personaId}

    setConfirmation(event.target.value)}/>
    } +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/persona-channel-body/styles.css b/product-source/hololake-native-desktop/src/modules/persona-channel-body/styles.css new file mode 100644 index 000000000..afbd2cf9a --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/persona-channel-body/styles.css @@ -0,0 +1,15 @@ +.persona-body-world { min-height: 0; overflow: auto; padding: 28px clamp(24px,5vw,76px) 54px; color: var(--content-primary); } +.persona-body-world>button,.persona-body-header>button { border: 0; background: transparent; color: var(--content-secondary); font: inherit; cursor: pointer; padding: 7px 0; } +.persona-body-header { display: grid; grid-template-columns: 150px minmax(0,1fr); gap: 22px; align-items: start; max-width: 1240px; margin: 0 auto 24px; } +.persona-body-header span,.persona-body-card header span { font-size: 11px; letter-spacing: .17em; color: var(--content-muted); } +.persona-body-header h1 { font-size: clamp(28px,4vw,48px); margin: 6px 0; letter-spacing: -.035em; } +.persona-body-header p,.persona-body-card p { color: var(--content-secondary); line-height: 1.7; } +.persona-body-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 18px; max-width: 1240px; margin: auto; } +.persona-body-card { border: 1px solid color-mix(in srgb,var(--content-primary) 13%,transparent); background: color-mix(in srgb,var(--panel-bg) 78%,transparent); backdrop-filter: blur(22px); border-radius: 22px; padding: 22px; box-shadow: 0 18px 46px rgba(3,10,25,.15); } +.persona-body-card header { display: flex; align-items: start; justify-content: space-between; gap: 18px; }.persona-body-card header h2 { font-size: 21px; margin: 4px 0 0; }.persona-body-card header>b { font-size: 10px; letter-spacing: .12em; color: var(--accent-light); }.persona-lifecycle-card { grid-column: 1/-1; } +.persona-body-card dl { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; margin: 18px 0 0; }.persona-body-card dl div { padding: 12px; border-radius: 14px; background: color-mix(in srgb,var(--surface-depth) 80%,transparent); }.persona-body-card dt { font-size: 11px; color: var(--content-muted); }.persona-body-card dd { margin: 6px 0 0; font-weight: 700; font-size: 13px; } +.persona-add-form { display: flex; gap: 9px; margin: 18px 0 14px; }.persona-add-form input,.persona-confirm-backdrop input { flex: 1; min-width: 0; border: 1px solid color-mix(in srgb,var(--content-primary) 16%,transparent); background: var(--surface-depth); color: var(--content-primary); border-radius: 12px; padding: 11px 13px; }.persona-add-form button,.persona-list button,.persona-confirm-backdrop button { border: 0; border-radius: 12px; padding: 10px 14px; background: var(--accent-light); color: var(--surface-depth); cursor: pointer; } +.persona-list { display: grid; gap: 8px; }.persona-list article { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 13px; border-radius: 14px; background: color-mix(in srgb,var(--surface-depth) 80%,transparent); }.persona-list article div { min-width: 0; }.persona-list article b,.persona-list article small { display: block; }.persona-list article small { margin-top: 4px; color: var(--content-muted); overflow: hidden; text-overflow: ellipsis; } +.affinity-list { display: grid; gap: 9px; margin: 18px 0; }.affinity-list>div { display: grid; grid-template-columns: minmax(120px,1fr) minmax(80px,1fr) 48px; gap: 10px; align-items: center; font-size: 12px; }.affinity-list i { height: 6px; border-radius: 999px; background: color-mix(in srgb,var(--content-primary) 10%,transparent); overflow: hidden; }.affinity-list em { display: block; height: 100%; background: var(--accent-light); border-radius: inherit; } +.persona-body-message { max-width: 1240px; margin: 16px auto; color: var(--accent-light); }.persona-confirm-backdrop { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(3,8,20,.62); z-index: 90; }.persona-confirm-backdrop section { width: min(560px,100%); border-radius: 22px; padding: 24px; background: var(--panel-bg); box-shadow: 0 26px 80px rgba(0,0,0,.35); }.persona-confirm-backdrop input { width: 100%; margin: 8px 0 16px; }.persona-confirm-backdrop section>div { display: flex; justify-content: flex-end; gap: 9px; }.persona-confirm-backdrop button:first-child { background: color-mix(in srgb,var(--content-primary) 10%,transparent); color: var(--content-primary); } +@media (max-width:760px) { .persona-body-header{grid-template-columns:1fr}.persona-body-grid{grid-template-columns:1fr}.persona-lifecycle-card{grid-column:auto}.persona-body-card dl{grid-template-columns:repeat(2,minmax(0,1fr))} } diff --git a/product-source/hololake-native-desktop/src/modules/private-channel/PrivateChannelSurface.tsx b/product-source/hololake-native-desktop/src/modules/private-channel/PrivateChannelSurface.tsx new file mode 100644 index 000000000..0b96b24db --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/private-channel/PrivateChannelSurface.tsx @@ -0,0 +1,108 @@ +import { useState, type CSSProperties } from 'react' +import './private-channel-surface.css' + +export interface PrivateChannelAction { + id: string + title: string + meta: string + badge?: string + onOpen: () => void +} + +export interface InstalledChannelModule extends PrivateChannelAction { + number: string +} + +interface PrivateChannelSurfaceProps { + ownerName: string + ownerNumber: string + knowledgeCount: number + onBack: () => void + onMarketplace: () => void + nativeActions: PrivateChannelAction[] + installedModules: InstalledChannelModule[] +} + +type PrivateChannelView = 'home' | 'native' | 'modules' + +interface WaterBayProps { + title: string + meta: string + detail?: string + warm?: boolean + position: number + onOpen: () => void +} + +function WaterBay({ title, meta, detail, warm = false, position, onOpen }: WaterBayProps) { + return +} + +export function PrivateChannelSurface({ ownerName, ownerNumber, knowledgeCount, onBack, onMarketplace, nativeActions, installedModules }: PrivateChannelSurfaceProps) { + const [view, setView] = useState('home') + const leave = () => view === 'home' ? onBack() : setView('home') + const backLabel = view === 'home' ? '永恒湖心' : '心跳核心频道' + + return
    + + + {ownerNumber} + +
    +

    {view === 'home' ? '心跳核心频道' : view === 'native' ? '频道原生器官' : '已安装模块'}

    +

    {view === 'home' + ? `${ownerName} · ${ownerNumber} · 私人频道全景` + : view === 'native' + ? `随频道建立 · ${nativeActions.length} 条原生路径` + : `编号登记 · ${installedModules.length} 个本地成品模块`}

    +
    + +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/private-channel/private-channel-surface.css b/product-source/hololake-native-desktop/src/modules/private-channel/private-channel-surface.css new file mode 100644 index 000000000..def4db14e --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/private-channel/private-channel-surface.css @@ -0,0 +1,89 @@ +/* 2026-08-16 official lightweight inner-screen source: water bays are paths. */ +.private-channel-surface { + position:absolute; + inset:0; + overflow:hidden; + color:var(--content-primary); + container-type:inline-size; + background: + radial-gradient(ellipse 62% 38% at 50% -10%,color-mix(in srgb,var(--primitive-cool-glow) 12%,transparent),transparent 72%), + linear-gradient(180deg,transparent 0 36%,color-mix(in srgb,var(--surface-depth) 34%,transparent) 100%); +} + +.private-channel-sky { position:absolute; inset:0; overflow:hidden; pointer-events:none; } +.private-channel-sky::before { content:''; position:absolute; left:8%; right:8%; top:54%; height:34%; filter:blur(24px); opacity:.55; + background:radial-gradient(ellipse at 50% 0,color-mix(in srgb,var(--primitive-cool-glow) 13%,transparent),transparent 68%); } +.private-channel-sky > i { position:absolute; width:3px; height:3px; border-radius:50%; background:var(--accent-light); opacity:.34; box-shadow:0 0 7px color-mix(in srgb,var(--accent-light) 60%,transparent); animation:private-star 5.8s ease-in-out infinite; } +.private-channel-sky > i:nth-child(1){left:9%;top:18%}.private-channel-sky > i:nth-child(2){left:23%;top:68%;animation-delay:-2s}.private-channel-sky > i:nth-child(3){left:38%;top:25%;animation-delay:-4s}.private-channel-sky > i:nth-child(4){left:54%;top:74%;animation-delay:-1s}.private-channel-sky > i:nth-child(5){left:67%;top:19%;animation-delay:-3s}.private-channel-sky > i:nth-child(6){left:82%;top:62%;animation-delay:-5s}.private-channel-sky > i:nth-child(7){left:93%;top:31%;animation-delay:-2.8s}.private-channel-sky > i:nth-child(8){left:14%;top:82%;animation-delay:-4.7s} +@keyframes private-star { 0%,100%{opacity:.12;transform:scale(.72)} 50%{opacity:.68;transform:scale(1.12)} } + +.private-channel-back { position:absolute; z-index:8; top:18px; left:clamp(18px,3vw,44px); padding:0; border:0; background:transparent; color:var(--content-secondary); cursor:pointer; font:650 12px/1.4 inherit; letter-spacing:.06em; transition:color .24s ease; } +.private-channel-back:hover { color:var(--accent-light); } +.private-channel-number { position:absolute; z-index:8; top:18px; right:clamp(18px,3vw,44px); color:var(--accent-light); font:700 10px/1.4 inherit; letter-spacing:.12em; } + +.private-channel-location { position:absolute; z-index:6; top:66px; left:16px; right:16px; display:flex; flex-direction:column; align-items:center; text-align:center; gap:9px; } +.private-channel-location h1 { margin:0; color:var(--content-primary); font-size:clamp(28px,3.1vw,42px); font-weight:700; letter-spacing:.1em; } +.private-channel-location p { margin:0; color:var(--content-secondary); font-size:12px; font-weight:600; letter-spacing:.18em; } +.private-channel-mist { position:absolute; z-index:1; left:7%; right:7%; top:45%; height:72px; filter:blur(13px); pointer-events:none; + background:radial-gradient(ellipse at center,color-mix(in srgb,var(--primitive-cool-glow) 15%,transparent),color-mix(in srgb,var(--primitive-warm-glow) 5%,transparent) 52%,transparent 78%); } + +.private-bay-field { position:absolute; z-index:5; inset:0; } +.private-water-bay { --bay-width:210px; --bay-height:66px; position:absolute; left:50%; top:50%; width:var(--bay-width); height:118px; padding:0; border:0; background:transparent; color:var(--content-primary); cursor:pointer; text-align:center; transform:translate(-50%,-50%); animation:private-bay-rise .7s cubic-bezier(.22,1,.36,1) both; animation-delay:calc(var(--bay-position) * 70ms); } +.private-bay-light { position:absolute; left:50%; top:0; width:var(--bay-width); height:var(--bay-height); transform:translateX(-50%); animation:private-bay-float 6.6s ease-in-out calc(var(--bay-position) * -1.2s) infinite; } +.private-bay-light > i { position:absolute; left:50%; top:50%; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; } +.private-bay-light > i:first-child { width:100%; height:100%; filter:blur(2px); opacity:.88; background:radial-gradient(closest-side,color-mix(in srgb,var(--primitive-cool-glow) 31%,transparent),color-mix(in srgb,var(--primitive-cool-glow) 10%,transparent) 48%,transparent 74%); } +.private-bay-light > i:nth-child(2) { width:20px; height:20px; background:radial-gradient(circle at 40% 34%,#fff,color-mix(in srgb,var(--accent-light) 74%,var(--primitive-cool-glow)) 48%,transparent 80%); box-shadow:0 0 18px color-mix(in srgb,var(--primitive-cool-glow) 45%,transparent); } +.private-bay-light > i:last-child { width:38%; height:38%; border:1px solid color-mix(in srgb,var(--primitive-cool-glow) 25%,transparent); animation:private-ripple 5.5s ease-out calc(var(--bay-position) * -1s) infinite; } +.private-water-bay.is-warm .private-bay-light > i:first-child { background:radial-gradient(closest-side,color-mix(in srgb,var(--primitive-warm-glow) 37%,transparent),color-mix(in srgb,var(--primitive-warm-glow) 12%,transparent) 48%,transparent 74%); } +.private-water-bay.is-warm .private-bay-light > i:nth-child(2) { background:radial-gradient(circle at 40% 32%,#fff,var(--accent-light) 40%,transparent 80%); box-shadow:0 0 25px color-mix(in srgb,var(--primitive-warm-glow) 65%,transparent),0 0 62px color-mix(in srgb,var(--primitive-warm-glow) 24%,transparent); } +.private-water-bay.is-warm .private-bay-light > i:last-child { border-color:color-mix(in srgb,var(--accent-light) 35%,transparent); } +.private-water-bay > b { position:absolute; top:76px; left:50%; transform:translateX(-50%); white-space:nowrap; color:var(--content-primary); font-size:17px; font-weight:700; letter-spacing:.06em; transition:color .24s ease; } +.private-water-bay > small { position:absolute; top:104px; left:50%; transform:translateX(-50%); white-space:nowrap; color:var(--content-secondary); font-size:11px; font-weight:600; letter-spacing:.09em; } +.private-water-bay.is-warm > b,.private-water-bay:hover > b { color:var(--accent-light); } +.private-water-bay:hover .private-bay-light > i:nth-child(2) { box-shadow:0 0 26px var(--accent-light),0 0 70px color-mix(in srgb,var(--primitive-warm-glow) 32%,transparent); } +@keyframes private-bay-float { 0%,100%{transform:translateX(-50%) translateY(0)} 50%{transform:translateX(-50%) translateY(-7px)} } +@keyframes private-ripple { 0%{width:34%;height:34%;opacity:.52} 100%{width:120%;height:120%;opacity:0} } +@keyframes private-bay-rise { from{opacity:0;translate:0 12px} to{opacity:1;translate:0 0} } + +.private-bay-field-home .private-water-bay:nth-child(1) { left:25%; top:51%; } +.private-bay-field-home .private-water-bay:nth-child(2) { left:50%; top:58%; } +.private-bay-field-home .private-water-bay:nth-child(3) { left:75%; top:51%; } + +.private-bay-field-items { inset:150px clamp(22px,5vw,82px) 92px; display:grid; grid-template-columns:repeat(3,minmax(180px,1fr)); place-items:center; align-content:center; gap:52px clamp(18px,4vw,72px); } +.private-bay-field-items .private-water-bay { position:relative; left:auto; top:auto; transform:none; } +.private-bay-field-items .private-water-bay:nth-last-of-type(1):nth-child(4), +.private-bay-field-items .private-water-bay:nth-child(4):nth-last-of-type(1) { grid-column:2; } +.private-bay-field-items .private-water-bay:nth-child(4):not(:last-child) { grid-column:1 / 2; margin-left:55%; } +.private-bay-field-items .private-water-bay:nth-child(5) { grid-column:3 / 4; margin-right:55%; } +.private-module-empty { grid-column:1/-1; color:var(--content-secondary); font-size:13px; letter-spacing:.1em; } +.private-market-link { position:absolute; right:0; bottom:-44px; padding:0; border:0; background:transparent; color:var(--accent-light); cursor:pointer; font:650 11px/1.4 inherit; letter-spacing:.08em; } + +.private-channel-boundary { position:absolute; z-index:7; left:24px; right:24px; bottom:22px; display:flex; flex-direction:column; align-items:center; gap:6px; text-align:center; color:var(--content-muted); font-size:9px; font-weight:600; letter-spacing:.07em; } +.private-channel-boundary b { color:color-mix(in srgb,var(--accent-light) 78%,var(--content-secondary)); font-size:10px; } + +@container (max-width:820px) { + .private-bay-field-home .private-water-bay:nth-child(1) { left:22%; top:48%; } + .private-bay-field-home .private-water-bay:nth-child(2) { left:50%; top:64%; } + .private-bay-field-home .private-water-bay:nth-child(3) { left:78%; top:48%; } + .private-water-bay { --bay-width:180px; } + .private-bay-field-items { grid-template-columns:repeat(2,minmax(160px,1fr)); gap:48px 24px; overflow:auto; align-content:start; padding-top:54px; } + .private-bay-field-items .private-water-bay:nth-child(n) { grid-column:auto; margin:0; } + .private-bay-field-items .private-water-bay:last-of-type:nth-child(odd) { grid-column:1/-1; } +} + +@container (max-width:560px) { + .private-channel-location { top:58px; } + .private-channel-location h1 { font-size:26px; } + .private-channel-location p { max-width:88%; font-size:10px; letter-spacing:.1em; } + .private-channel-number { display:none; } + .private-bay-field-home { inset:142px 0 76px; display:grid; grid-template-columns:1fr; align-content:center; justify-items:center; gap:24px; overflow:auto; } + .private-bay-field-home .private-water-bay:nth-child(n) { position:relative; left:auto; top:auto; transform:none; } + .private-bay-field-items { inset:130px 12px 72px; grid-template-columns:1fr; padding-top:36px; gap:28px; } + .private-bay-field-items .private-water-bay:last-of-type:nth-child(odd) { grid-column:auto; } + .private-market-link { position:relative; right:auto; bottom:auto; grid-column:1; padding:12px 0 24px; } + .private-channel-boundary span { display:none; } +} + +@media (prefers-reduced-motion:reduce) { + .private-channel-sky > i,.private-bay-light,.private-bay-light > i:last-child,.private-water-bay { animation:none; } +} diff --git a/product-source/hololake-native-desktop/src/modules/qoder-surface/StarlakeSurface.tsx b/product-source/hololake-native-desktop/src/modules/qoder-surface/StarlakeSurface.tsx new file mode 100644 index 000000000..d2925f674 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/qoder-surface/StarlakeSurface.tsx @@ -0,0 +1,134 @@ +import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react' +import './starlake-surface.css' +import { resolveLayoutMetrics } from './layout-profile' + +export type DomainId = 'BRANCH_DOMAIN' | 'ZERO_SENSE_DOMAIN' | 'ZERO_DOMAIN' | 'MAIN_DOMAIN' | 'FIFTH_DOMAIN' +export type Phase = 'dawn' | 'day' | 'dusk' | 'night' + +interface StarlakeSurfaceProps { + awake?: boolean + phase?: Phase + weather?: string + authenticated?: boolean + onDomain: (domain: DomainId) => void + onOpenEra: () => void + onEnterChannel: () => void +} + +const EPOCH_MS = new Date('2025-04-26T00:00:00+08:00').getTime() +const WEEK = ['日', '一', '二', '三', '四', '五', '六'] +const PHASE_WORD: Record = { dawn: '晨', day: '午', dusk: '暮', night: '夜' } + +const DOMAINS = [ + { id: 'BRANCH_DOMAIN' as const, name: '分域', role: '行业模块 · 大脑技能商城', stage: 2, hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)' }, + { id: 'MAIN_DOMAIN' as const, name: '主域', role: '公告与版本通知', stage: 3, hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)' }, + { id: 'FIFTH_DOMAIN' as const, name: '第五域', role: '冰朔私域 · 编号授权', stage: 5, hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)' }, + { id: 'ZERO_DOMAIN' as const, name: '零域', role: '实验 · 热更新 · 系统架构', stage: 3, hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)' }, + { id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', role: '人类主控团队 · 不公开', stage: 2, hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)' }, +] + +function seeded(index: number) { + const value = Math.sin(index * 9283.17 + 17.73) * 43758.5453 + return value - Math.floor(value) +} + +function phaseOf(hour: number): Phase { + return hour < 5 ? 'night' : hour < 9 ? 'dawn' : hour < 16 ? 'day' : hour < 19 ? 'dusk' : 'night' +} + +export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authenticated = false, onDomain, onOpenEra, onEnterChannel }: StarlakeSurfaceProps) { + const [now, setNow] = useState(() => new Date()) + const rootRef = useRef(null) + const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight })) + const livePhase = phase || phaseOf(now.getHours() + now.getMinutes() / 60) + const raining = ['RAIN', 'STORM'].includes(weather.toUpperCase()) + const dayNumber = Math.floor((now.getTime() - EPOCH_MS) / 86400000) + 1 + const dayIndex = ((dayNumber - 1) % DOMAINS.length + DOMAINS.length) % DOMAINS.length + const layout = resolveLayoutMetrics(viewport.width, viewport.height) + + const stars = useMemo(() => Array.from({ length: 170 }, (_, index) => ({ + id: index, + size: .8 + seeded(index + 1) * 1.6, + left: seeded(index + 201) * 100, + top: seeded(index + 401) * 68, + opacity: .2 + seeded(index + 601) * .6, + duration: 2.5 + seeded(index + 801) * 5, + delay: -seeded(index + 1001) * 7, + warm: seeded(index + 1201) < .12, + })), []) + const lakeStars = useMemo(() => Array.from({ length: 140 }, (_, index) => ({ + id: index, + size: 1 + seeded(index + 1401) * 1.8, + left: seeded(index + 1601) * 100, + top: Math.pow(seeded(index + 1801), 1.6) * 92, + opacity: .22 + seeded(index + 2001) * .5, + duration: 2.4 + seeded(index + 2201) * 4, + delay: -seeded(index + 2401) * 6, + warm: seeded(index + 2601) < .18, + })), []) + const rain = useMemo(() => Array.from({ length: 90 }, (_, index) => ({ + id: index, + left: seeded(index + 2801) * 108, + duration: .9 + seeded(index + 3001) * .8, + delay: -seeded(index + 3201) * 2, + opacity: .3 + seeded(index + 3401) * .5, + })), []) + + useEffect(() => { + if (!awake) return + setNow(new Date()) + const timer = window.setInterval(() => setNow(new Date()), 125) + return () => window.clearInterval(timer) + }, [awake]) + + useEffect(() => { + const root = rootRef.current + if (!root) return + const observer = new ResizeObserver(([entry]) => setViewport({ width: entry.contentRect.width, height: entry.contentRect.height })) + observer.observe(root) + return () => observer.disconnect() + }, []) + + const pad = (value: number, length = 2) => String(value).padStart(length, '0') + const sourceHour = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600 + const isDay = sourceHour >= 6 && sourceHour < 18 + const sourceProgress = isDay ? (sourceHour - 6) / 12 : ((sourceHour >= 18 ? sourceHour - 18 : sourceHour + 6) / 12) + const sourceX = 8 + sourceProgress * 84 + const sourceY = 46 - Math.sin(sourceProgress * Math.PI) * 26 + const sourceStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--src-mid': isDay ? '#ffe9a8' : '#dfe6fa', '--src-g1': isDay ? 'rgba(255,236,180,.7)' : 'rgba(214,226,255,.6)', '--src-g2': isDay ? 'rgba(255,220,140,.26)' : 'rgba(180,200,255,.22)', '--src-g3': isDay ? 'rgba(255,220,140,.12)' : 'rgba(180,200,255,.1)' } as CSSProperties + const glowStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--glow-tint': isDay ? 'rgba(255,236,180,.12)' : 'rgba(190,208,255,.09)' } as CSSProperties + return
    +
    +} diff --git a/product-source/hololake-native-desktop/src/modules/qoder-surface/TraditionalSurface.tsx b/product-source/hololake-native-desktop/src/modules/qoder-surface/TraditionalSurface.tsx new file mode 100644 index 000000000..dd47ed39b --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/qoder-surface/TraditionalSurface.tsx @@ -0,0 +1,103 @@ +import { useEffect, useRef } from 'react' +import './traditional-surface.css' +import { resolveLayoutMetrics } from './layout-profile' + +/* Qoder 锁定验收稿的传统工作台结构。宿主只注入真实投影,不在视觉层制造广播或指标。 */ +export type FinishId = 'aurora' | 'nebula' | 'abyss' | 'jade' | 'cinnabar' | 'champagne' | 'porcelain' | 'snow' + +export const FINISHES: Array<{ id: FinishId; name: string }> = [ + { id: 'aurora', name: '曜夜' }, { id: 'nebula', name: '星辉' }, { id: 'abyss', name: '深海' }, { id: 'jade', name: '翡翠' }, + { id: 'cinnabar', name: '朱砂' }, { id: 'champagne', name: '香槟' }, { id: 'porcelain', name: '瓷光' }, { id: 'snow', name: '雪霁' }, +] + +const BROADCAST_COLOR: Record = { 分域: '#7ce8c8', 零感域: '#cfa8ff', 零域: '#ffbe85', 主域: '#a8bcff', 第五域: '#fdeec2' } +export interface TraditionalBroadcast { id: string; domain: string; message: string; time: string } +export interface TraditionalChannel { color: string; title: string; meta: string; tag: string; tone: string; count: number } +export interface TraditionalSystem { title: string; meta: string; tag: string; tone?: string } + +interface TraditionalSurfaceProps { + awake?: boolean + finish: FinishId + onFinish: (finish: FinishId) => void + onActivity?: () => void + onBack: () => void + onOpenKnowledge?: () => void + onOpenChannel?: () => void + onOpenEra?: () => void + onOpenSettings?: () => void + identityLabel?: string + version: string + dayNumber: number + duty: string + weatherLabel: string + broadcasts: TraditionalBroadcast[] + channels: TraditionalChannel[] + systems: TraditionalSystem[] + activityBars: number[] + activityCount: number +} + +export function TraditionalSurface({ awake = false, finish, onFinish, onActivity, onBack, onOpenKnowledge, onOpenChannel, onOpenEra, onOpenSettings, identityLabel = '光湖语言系统', version, dayNumber, duty, weatherLabel, broadcasts, channels, systems, activityBars, activityCount }: TraditionalSurfaceProps) { + const rootRef = useRef(null) + const clockRef = useRef(null) + + useEffect(() => { + const tick = () => { + const el = clockRef.current + if (!el) return + const now = new Date() + const pad = (value: number, length = 2) => String(value).padStart(length, '0') + el.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}` + } + tick() + if (!awake) return + const timer = window.setInterval(tick, 125) + return () => window.clearInterval(timer) + }, [awake]) + + useEffect(() => { + const element = rootRef.current + if (!element) return + const layout = () => { + const { width, height } = element.getBoundingClientRect() + const metrics = resolveLayoutMetrics(width, height) + const vmin = Math.min(width, height) + element.style.setProperty('--gap', `${Math.round(Math.min(18, Math.max(10, vmin * .016)))}px`) + element.style.setProperty('--pad', `${Math.round(Math.min(26, Math.max(12, vmin * .02)))}px`) + element.dataset.layout = metrics.profile + element.dataset.density = metrics.density + } + const observer = new ResizeObserver(layout) + observer.observe(element) + layout() + return () => observer.disconnect() + }, []) + + const pickFinish = (id: FinishId) => { onFinish(id); window.localStorage.setItem('hololake-finish', id) } + const maxBar = Math.max(1, ...activityBars) + + return
    +