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..c05c217ce --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/dynamic-language-world-visual-system.json @@ -0,0 +1,62 @@ +{ + "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": "EXISTING_OFFICIAL_FIVE_LAKE_SHELL_UNCHANGED", + "themes": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"], + "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": "BEHAVIOR_MERGED_INTO_EXISTING_OFFICIAL_SHELL_NOT_COPIED", + "traditional_surface": "REJECTED_DUPLICATE_SHELL_EIGHT_THEMES_AND_SIMULATED_STATUS", + "traditional_surface_css": "REJECTED_CONFLICTS_WITH_LOCKED_FIVE_LAKE_TOKENS", + "fake_broadcasts_and_fake_metrics": "REJECTED" + }, + "current_acceptance": { + "state": "PASS_SIGNED_NUMBERED_REAL_UI_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": "5612519f56ea83cfd167cd099cc2cb27de9da057b262716bb71ff38714c76c8f", + "signed_app_cdhash": "5cc0836a1f12f42f9f48196ce691d6f4cde2f801", + "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_AND_REAL_CLIMATE_RESTORED", + "official_shell_preserved": "PASS_FIVE_LAKE_LAYOUT_AND_FIVE_THEMES_UNCHANGED", + "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 index c0bb08290..cddd95ef7 100644 --- a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -1,7 +1,7 @@ { "schema": "hololake.module-donor-admission-registry/v1", "record_id": "HLP-MODULE-DONOR-ADMISSION-001", - "state": "SIX_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED", + "state": "SEVEN_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY", "root_rule": { "official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT", "repair_old_application_in_place": false, @@ -166,7 +166,7 @@ "admission_order": 7, "candidate_number": "HLP-DONOR-CAND-0007", "name": "dynamic_language_world_visual_surface", - "state": "QUARANTINED_PENDING_ADMISSION", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", "paths": [ "contracts/dynamic-language-world-visual-system.json", "src/StarlakeSurface.tsx", @@ -174,7 +174,11 @@ "src/traditional-surface.tsx", "src/traditional-surface.css", "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": "REALITY_TIME_AND_PUBLIC_WEATHER_VISUAL_PROJECTION_ONLY; OFFICIAL_FIVE_LAKE_SHELL_AND_ROUTING_UNCHANGED; NO_FAKE_WEATHER_OR_SIMULATED_STATUS" } ], "admission_gate": [ diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json index 1d384179a..90233331f 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -76,7 +76,8 @@ "get_mobile_sync_status", "rotate_mobile_pairing", "stop_mobile_sync", - "get_mobile_sync_snapshot" + "get_mobile_sync_snapshot", + "get_world_climate" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -372,6 +373,11 @@ "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": [ @@ -1980,6 +1986,17 @@ "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/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/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json index 3680f30c5..84d5dcf85 100644 --- a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -54,7 +54,7 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "40a9d411dc891c8d3333dfea3f9d24c1047c8ae8568c04a83263bb4be5c6a466" + "sha256": "f1aa85668a6d33efeb7ce268cd48e7ed517696db990327f22111359aca8c177c" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -69,7 +69,7 @@ "everyAcceptedCallHasEvidenceClass": true, "mismatchedCoordinate": "FAIL_CLOSED" }, - "routeCount": 168, + "routeCount": 169, "routes": [ { "transport": "DIRECT_LOCAL_NUMBERED_BROKER", @@ -2031,6 +2031,20 @@ "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", 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..ea137e895 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/dynamic-world-surface-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/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') + +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('official shell and five lake themes remain the only admitted layout', () => { + assert.equal(contract.visual_lock.layout, 'EXISTING_OFFICIAL_FIVE_LAKE_SHELL_UNCHANGED') + assert.equal(contract.visual_lock.themes.length, 5) + assert.equal(contract.donor_disposition.traditional_surface, 'REJECTED_DUPLICATE_SHELL_EIGHT_THEMES_AND_SIMULATED_STATUS') + assert.match(frontend, /夜湖星光/) + assert.match(frontend, /天气源暂不可用;湖面只使用北京时间,不生成假天气。/) + assert.match(styles, /\.world-climate-veil/) + assert.doesNotMatch(frontend, /语料库新增 217 条/) + assert.doesNotMatch(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 index 11c66db52..c265c9808 100644 --- a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -6,7 +6,7 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis 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.match(catalog.state, /^[A-Z]+_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED$/) + assert.equal(catalog.state, 'SEVEN_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) @@ -19,7 +19,7 @@ test('candidate coordinates are unique but are not permanent runtime module numb const pending = catalog.candidates.filter((candidate) => candidate.state.startsWith('QUARANTINED')) assert.equal(new Set(coordinates).size, coordinates.length) assert.ok(admitted.length > 0) - assert.ok(pending.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) 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 index 835a93147..3580fdb02 100644 --- 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 @@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8')) assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.routeCount, 168) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 168) + assert.equal(generated.routeCount, 169) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 169) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.ok(generated.routes.every((route) => route.admission && route.evidence)) 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 391e49510..563af5f67 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -40,6 +40,7 @@ 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; 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 index 0c3b6faf7..7f671fdb4 100644 --- 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 @@ -93,6 +93,13 @@ const MOBILE_SYNC_PACKAGE: &[u8] = 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, @@ -151,6 +158,11 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[ 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)] @@ -1599,7 +1611,7 @@ mod tests { assert_eq!(package.manifest.adapter, "channel-workbench-v1"); assert_eq!(package.manifest.permissions.len(), 4); assert!(is_sha256(&digest)); - assert_eq!(BUNDLED_MODULES.len(), 10); + assert_eq!(BUNDLED_MODULES.len(), 11); } #[test] @@ -1707,6 +1719,32 @@ mod tests { 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() { 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 index c9b92d506..6a9d65987 100644 --- 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 @@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> { || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" - || tree.route_count != 168 + || tree.route_count != 169 || tree.routes.len() != tree.route_count || !tree.invariants.number_is_stable_coordinate_not_authority || !tree.invariants.path_is_unique_navigation 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 index 5e5973ed5..c35cea065 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -934,7 +934,7 @@ mod tests { #[test] fn registry_is_closed_and_contains_every_migrated_command() { let registry = load_registry().unwrap(); - assert_eq!(registry.operations.len(), 146); + assert_eq!(registry.operations.len(), 147); assert!(!registry.runtime.legacy_direct_commands_allowed); } 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 index ceba2fa0f..5bee9d24b 100644 --- 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 @@ -140,6 +140,9 @@ pub(crate) async fn dispatch( "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()) } 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/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index bb1675dad..058596048 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -70,6 +70,20 @@ interface BundledModuleDescriptor { 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 @@ -408,6 +422,7 @@ function LakeAtmosphere({ awake }: { awake: boolean }) { {starPoints.map(([left, top, size, duration], index) => ) }