feat: admit signed dynamic world surface module
This commit is contained in:
parent
292889f934
commit
2b1f5d47fd
18 changed files with 584 additions and 14 deletions
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.module-donor-admission-registry/v1",
|
"schema": "hololake.module-donor-admission-registry/v1",
|
||||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
"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": {
|
"root_rule": {
|
||||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||||
"repair_old_application_in_place": false,
|
"repair_old_application_in_place": false,
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
"admission_order": 7,
|
"admission_order": 7,
|
||||||
"candidate_number": "HLP-DONOR-CAND-0007",
|
"candidate_number": "HLP-DONOR-CAND-0007",
|
||||||
"name": "dynamic_language_world_visual_surface",
|
"name": "dynamic_language_world_visual_surface",
|
||||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
|
||||||
"paths": [
|
"paths": [
|
||||||
"contracts/dynamic-language-world-visual-system.json",
|
"contracts/dynamic-language-world-visual-system.json",
|
||||||
"src/StarlakeSurface.tsx",
|
"src/StarlakeSurface.tsx",
|
||||||
|
|
@ -174,7 +174,11 @@
|
||||||
"src/traditional-surface.tsx",
|
"src/traditional-surface.tsx",
|
||||||
"src/traditional-surface.css",
|
"src/traditional-surface.css",
|
||||||
"src-tauri/src/world_climate.rs"
|
"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": [
|
"admission_gate": [
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@
|
||||||
"get_mobile_sync_status",
|
"get_mobile_sync_status",
|
||||||
"rotate_mobile_pairing",
|
"rotate_mobile_pairing",
|
||||||
"stop_mobile_sync",
|
"stop_mobile_sync",
|
||||||
"get_mobile_sync_snapshot"
|
"get_mobile_sync_snapshot",
|
||||||
|
"get_world_climate"
|
||||||
],
|
],
|
||||||
"input_wrapper_aliases": [
|
"input_wrapper_aliases": [
|
||||||
"confirm_hololake_update_install",
|
"confirm_hololake_update_install",
|
||||||
|
|
@ -372,6 +373,11 @@
|
||||||
"module_number": "HLP-NIPC-MOD-0030",
|
"module_number": "HLP-NIPC-MOD-0030",
|
||||||
"target_number": "HLP-NIPC-TGT-0030",
|
"target_number": "HLP-NIPC-TGT-0030",
|
||||||
"internal_name": "mobile_sync"
|
"internal_name": "mobile_sync"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0031",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0031",
|
||||||
|
"internal_name": "world_climate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"operations": [
|
"operations": [
|
||||||
|
|
@ -1980,6 +1986,17 @@
|
||||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
"effect": "READ_OR_STATUS",
|
"effect": "READ_OR_STATUS",
|
||||||
"payload_schema": "hololake.numbered-ipc.payload/get_mobile_sync_snapshot/v1"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTF2cGVLbjVsWWRTdEF2L0FiZHBKUS9hVDdwNjRmRlRPc0xHTGJzbXFyR09tZysrcGNDYWN4eEJ3aGtCNGtURDRjNWg1ZWs5bU5YdDc0SWhBYWNsUEFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDg0NTk2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1EWU5BTUlDLVdPUkxELVNVUkZBQ0UtMDAwMS0wLjEuMC5naG1vZApHVGFucE8vYjliVUlsbSt6STRWTEcrN2tIQzNTSTgrUzlwdVdQUlY5WG8vQytzTEU1VURYMVA5Q3BCNVVwVFBvbnZheWxIbVUrWGQ2K3ZicGgwR3hEdz09Cg==
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||||
"sha256": "40a9d411dc891c8d3333dfea3f9d24c1047c8ae8568c04a83263bb4be5c6a466"
|
"sha256": "f1aa85668a6d33efeb7ce268cd48e7ed517696db990327f22111359aca8c177c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NBROKER-ROOT-001",
|
"recordId": "HLP-NBROKER-ROOT-001",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"everyAcceptedCallHasEvidenceClass": true,
|
"everyAcceptedCallHasEvidenceClass": true,
|
||||||
"mismatchedCoordinate": "FAIL_CLOSED"
|
"mismatchedCoordinate": "FAIL_CLOSED"
|
||||||
},
|
},
|
||||||
"routeCount": 168,
|
"routeCount": 169,
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
||||||
|
|
@ -2031,6 +2031,20 @@
|
||||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
"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"
|
"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",
|
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||||
"protocolVersion": "HLP-NIPC-v1",
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
|
|
||||||
|
|
@ -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, /频道活动 · 七日/)
|
||||||
|
})
|
||||||
|
|
@ -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'))
|
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', () => {
|
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.repair_old_application_in_place, false)
|
||||||
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, 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.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'))
|
const pending = catalog.candidates.filter((candidate) => candidate.state.startsWith('QUARANTINED'))
|
||||||
assert.equal(new Set(coordinates).size, coordinates.length)
|
assert.equal(new Set(coordinates).size, coordinates.length)
|
||||||
assert.ok(admitted.length > 0)
|
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(0, admitted.length), admitted)
|
||||||
assert.deepEqual(catalog.candidates.slice(admitted.length), pending)
|
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.candidate_number_is_runtime_module_number, false)
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
|
||||||
assert.deepEqual(generated, compileUnifiedNumberTree())
|
assert.deepEqual(generated, compileUnifiedNumberTree())
|
||||||
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
||||||
assert.equal(generated.routeCount, 168)
|
assert.equal(generated.routeCount, 169)
|
||||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 168)
|
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 169)
|
||||||
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
||||||
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
||||||
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ mod web_novel_author;
|
||||||
mod web_novel_import;
|
mod web_novel_import;
|
||||||
mod web_novel_modules;
|
mod web_novel_modules;
|
||||||
mod web_novel_workspace;
|
mod web_novel_workspace;
|
||||||
|
mod world_climate;
|
||||||
mod zero_core_numbering;
|
mod zero_core_numbering;
|
||||||
mod zero_point;
|
mod zero_point;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,13 @@ const MOBILE_SYNC_PACKAGE: &[u8] =
|
||||||
const MOBILE_SYNC_SIGNATURE: &str = include_str!(
|
const MOBILE_SYNC_SIGNATURE: &str = include_str!(
|
||||||
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig"
|
"../../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 {
|
struct BundledModuleSource {
|
||||||
module_number: &'static str,
|
module_number: &'static str,
|
||||||
|
|
@ -151,6 +158,11 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
|
||||||
package: MOBILE_SYNC_PACKAGE,
|
package: MOBILE_SYNC_PACKAGE,
|
||||||
signature: MOBILE_SYNC_SIGNATURE,
|
signature: MOBILE_SYNC_SIGNATURE,
|
||||||
},
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: DYNAMIC_WORLD_SURFACE_NUMBER,
|
||||||
|
package: DYNAMIC_WORLD_SURFACE_PACKAGE,
|
||||||
|
signature: DYNAMIC_WORLD_SURFACE_SIGNATURE,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -1599,7 +1611,7 @@ mod tests {
|
||||||
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
|
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
|
||||||
assert_eq!(package.manifest.permissions.len(), 4);
|
assert_eq!(package.manifest.permissions.len(), 4);
|
||||||
assert!(is_sha256(&digest));
|
assert!(is_sha256(&digest));
|
||||||
assert_eq!(BUNDLED_MODULES.len(), 10);
|
assert_eq!(BUNDLED_MODULES.len(), 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1707,6 +1719,32 @@ mod tests {
|
||||||
assert!(is_sha256(&digest));
|
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)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> {
|
||||||
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
||||||
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
||||||
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
||||||
|| tree.route_count != 168
|
|| tree.route_count != 169
|
||||||
|| tree.routes.len() != tree.route_count
|
|| tree.routes.len() != tree.route_count
|
||||||
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
||||||
|| !tree.invariants.path_is_unique_navigation
|
|| !tree.invariants.path_is_unique_navigation
|
||||||
|
|
|
||||||
|
|
@ -934,7 +934,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_is_closed_and_contains_every_migrated_command() {
|
fn registry_is_closed_and_contains_every_migrated_command() {
|
||||||
let registry = load_registry().unwrap();
|
let registry = load_registry().unwrap();
|
||||||
assert_eq!(registry.operations.len(), 146);
|
assert_eq!(registry.operations.len(), 147);
|
||||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ pub(crate) async fn dispatch(
|
||||||
"mobile_sync::get_mobile_sync_snapshot" => {
|
"mobile_sync::get_mobile_sync_snapshot" => {
|
||||||
json(crate::mobile_sync::get_mobile_sync_snapshot(app)?)
|
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" => {
|
"native_composition::get_native_composition_module_registry" => {
|
||||||
json(crate::native_composition::get_native_composition_module_registry())
|
json(crate::native_composition::get_native_composition_module_registry())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Mutex<Option<(u64, WorldClimateSnapshot)>>> = 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<WorldClimateSnapshot, String> {
|
||||||
|
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::<OpenMeteoResponse>().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<Option<WorldClimateSnapshot>, 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<u8> {
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,6 +70,20 @@ interface BundledModuleDescriptor {
|
||||||
installedState: string
|
installedState: string
|
||||||
signatureVerified: boolean
|
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 {
|
interface HomeStatus {
|
||||||
directLocalBrokerState: string
|
directLocalBrokerState: string
|
||||||
|
|
@ -408,6 +422,7 @@ function LakeAtmosphere({ awake }: { awake: boolean }) {
|
||||||
{starPoints.map(([left, top, size, duration], index) => <i className="world-star" key={index} style={{ left: `${left}%`, top: `${top}%`, width: size, height: size, '--star-duration': `${duration}s`, '--star-delay': `${index * .37}s` } as React.CSSProperties}/>) }
|
{starPoints.map(([left, top, size, duration], index) => <i className="world-star" key={index} style={{ left: `${left}%`, top: `${top}%`, width: size, height: size, '--star-duration': `${duration}s`, '--star-delay': `${index * .37}s` } as React.CSSProperties}/>) }
|
||||||
</div>
|
</div>
|
||||||
<div className="world-mist" aria-hidden="true"/>
|
<div className="world-mist" aria-hidden="true"/>
|
||||||
|
<div className="world-climate-veil" aria-hidden="true"/>
|
||||||
{awake && <div className="world-shimmer" aria-hidden="true">
|
{awake && <div className="world-shimmer" aria-hidden="true">
|
||||||
<i className="world-shine cool"/><i className="world-shine warm"/>
|
<i className="world-shine cool"/><i className="world-shine warm"/>
|
||||||
{glintPoints.map((point, index) => <i className={`world-glint ${point.warm ? 'warm' : 'cool'}`} key={index} style={{ left: `${point.left}%`, top: `${point.top}%`, width: point.size, height: point.size, '--glint-duration': `${point.duration}s`, '--glint-delay': `${point.delay}s` } as React.CSSProperties}/>) }
|
{glintPoints.map((point, index) => <i className={`world-glint ${point.warm ? 'warm' : 'cool'}`} key={index} style={{ left: `${point.left}%`, top: `${point.top}%`, width: point.size, height: point.size, '--glint-duration': `${point.duration}s`, '--glint-delay': `${point.delay}s` } as React.CSSProperties}/>) }
|
||||||
|
|
@ -619,6 +634,10 @@ function HoloLakeApp() {
|
||||||
const [mobileSyncModule, setMobileSyncModule] = useState<BundledModuleDescriptor | null>(null)
|
const [mobileSyncModule, setMobileSyncModule] = useState<BundledModuleDescriptor | null>(null)
|
||||||
const [mobileSyncBusy, setMobileSyncBusy] = useState(false)
|
const [mobileSyncBusy, setMobileSyncBusy] = useState(false)
|
||||||
const [mobileSyncMessage, setMobileSyncMessage] = useState('')
|
const [mobileSyncMessage, setMobileSyncMessage] = useState('')
|
||||||
|
const [dynamicSurfaceModule, setDynamicSurfaceModule] = useState<BundledModuleDescriptor | null>(null)
|
||||||
|
const [worldClimate, setWorldClimate] = useState<WorldClimateSnapshot | null>(null)
|
||||||
|
const [dynamicSurfaceBusy, setDynamicSurfaceBusy] = useState(false)
|
||||||
|
const [dynamicSurfaceMessage, setDynamicSurfaceMessage] = useState('')
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
|
@ -1232,6 +1251,40 @@ function HoloLakeApp() {
|
||||||
finally { setMobileSyncBusy(false) }
|
finally { setMobileSyncBusy(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshDynamicSurfaceModule = async () => {
|
||||||
|
try {
|
||||||
|
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
|
||||||
|
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001') || null
|
||||||
|
setDynamicSurfaceModule(module)
|
||||||
|
if (module?.installedState === 'ACTIVE') {
|
||||||
|
const climate = await invoke<WorldClimateSnapshot>('get_world_climate')
|
||||||
|
setWorldClimate(climate)
|
||||||
|
setDynamicSurfaceMessage(climate.state === 'VERIFIED_LIVE' ? '北京时间与公开天气已映射到湖面。' : '天气源暂不可用;湖面只使用北京时间,不生成假天气。')
|
||||||
|
} else {
|
||||||
|
setWorldClimate(null)
|
||||||
|
}
|
||||||
|
return module
|
||||||
|
} catch (error) {
|
||||||
|
setWorldClimate(null)
|
||||||
|
setDynamicSurfaceMessage(humanError(error, 'system'))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const activateDynamicSurfaceModule = async () => {
|
||||||
|
setDynamicSurfaceBusy(true)
|
||||||
|
setDynamicSurfaceMessage('正在验证官方签名、登记视觉编号并确认现实时间与公开天气权限……')
|
||||||
|
try {
|
||||||
|
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001', humanConfirmedPermissionExpansion: true } })
|
||||||
|
const module = await refreshDynamicSurfaceModule()
|
||||||
|
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
|
||||||
|
} catch (error) { setDynamicSurfaceMessage(humanError(error, 'system')) }
|
||||||
|
finally { setDynamicSurfaceBusy(false) }
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (repoLogin) void refreshDynamicSurfaceModule()
|
||||||
|
else { setDynamicSurfaceModule(null); setWorldClimate(null); setDynamicSurfaceMessage('') }
|
||||||
|
}, [repoLogin?.username, repoLogin?.domain])
|
||||||
|
|
||||||
const cloneCodeChannel = async (event: React.FormEvent) => {
|
const cloneCodeChannel = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!cloneUrl.trim()) return
|
if (!cloneUrl.trim()) return
|
||||||
|
|
@ -1761,6 +1814,11 @@ function HoloLakeApp() {
|
||||||
{zpMessage && <p className="global-message">{zpMessage}</p>}
|
{zpMessage && <p className="global-message">{zpMessage}</p>}
|
||||||
</section>
|
</section>
|
||||||
<section className="plain-panel"><header><div><h2>显示主题</h2><p>主题仅调整视觉令牌,不改变功能、权限或路由。</p></div></header><div className="theme-options">{themes.map((choice) => <button className={theme === choice.id ? 'active' : ''} key={choice.id} type="button" onClick={() => setTheme(choice.id)}><i className={choice.id}/><span>{choice.name}</span></button>)}</div></section>
|
<section className="plain-panel"><header><div><h2>显示主题</h2><p>主题仅调整视觉令牌,不改变功能、权限或路由。</p></div></header><div className="theme-options">{themes.map((choice) => <button className={theme === choice.id ? 'active' : ''} key={choice.id} type="button" onClick={() => setTheme(choice.id)}><i className={choice.id}/><span>{choice.name}</span></button>)}</div></section>
|
||||||
|
<section className="plain-panel climate-panel"><header><div><h2>动态语言世界视觉层</h2><p>只把北京时间和可验证的公开天气映射为湖面气候;五域位置、权限、事实与五色湖主题保持不变。</p></div><span className={worldClimate ? 'status-chip online' : 'status-chip'}>{worldClimate ? '真实投影已启用' : dynamicSurfaceModule?.installedState || '未启用'}</span></header>
|
||||||
|
{worldClimate ? <dl className="evidence-list"><div><dt>时间相位</dt><dd>{worldClimate.timePhase}</dd></div><div><dt>天气投影</dt><dd>{worldClimate.weatherKind === 'UNAVAILABLE' ? '不可用 · 未伪造' : worldClimate.weatherKind}</dd></div><div><dt>当值采样域</dt><dd>{domainDisplayName(worldClimate.activeDomain)} · 不改变开域状态</dd></div><div><dt>现实城市 / 坐标</dt><dd>{worldClimate.cityExposed || worldClimate.coordinatesExposed ? '边界异常' : '不向界面暴露'}</dd></div><div><dt>数据源</dt><dd><a href={worldClimate.sourceAttributionUrl} target="_blank" rel="noreferrer">Open-Meteo</a> · 十分钟缓存</dd></div></dl> : <p className="boundary-note">旧版传统工作台含模拟广播与模拟数值,未迁移。启用后也不会替换现行官方五域首页。</p>}
|
||||||
|
<button className="secondary-button" type="button" disabled={dynamicSurfaceBusy} onClick={() => void (dynamicSurfaceModule?.installedState === 'ACTIVE' ? refreshDynamicSurfaceModule() : activateDynamicSurfaceModule())}>{dynamicSurfaceBusy ? '正在验收…' : dynamicSurfaceModule?.installedState === 'ACTIVE' ? '刷新现实气候' : '确认两项权限并启用'}</button>
|
||||||
|
{dynamicSurfaceMessage && <p className="global-message">{dynamicSurfaceMessage}</p>}
|
||||||
|
</section>
|
||||||
<section className="plain-panel"><header><div><h2>软件更新</h2><p>仅接受已登记信任根验证通过的 HoloLake 发布包。</p></div></header>{releaseCandidate ? <div className="release-summary"><b>HoloLake {releaseCandidate.version}</b><p>{releaseCandidate.notes}</p><button className="primary-button" onClick={() => void installUpdate()}>验证并安装</button></div> : <button className="secondary-button" disabled={systemBusy} onClick={() => void checkUpdate()}>检查签名更新</button>}</section>
|
<section className="plain-panel"><header><div><h2>软件更新</h2><p>仅接受已登记信任根验证通过的 HoloLake 发布包。</p></div></header>{releaseCandidate ? <div className="release-summary"><b>HoloLake {releaseCandidate.version}</b><p>{releaseCandidate.notes}</p><button className="primary-button" onClick={() => void installUpdate()}>验证并安装</button></div> : <button className="secondary-button" disabled={systemBusy} onClick={() => void checkUpdate()}>检查签名更新</button>}</section>
|
||||||
</div>
|
</div>
|
||||||
{systemMessage && <p className="global-message">{systemMessage}</p>}
|
{systemMessage && <p className="global-message">{systemMessage}</p>}
|
||||||
|
|
@ -1792,6 +1850,7 @@ function HoloLakeApp() {
|
||||||
|
|
||||||
const enterpriseRelationshipPending = Boolean(repoLogin && repoLogin.domain !== 'FIFTH_DOMAIN' && enterpriseEntry && enterpriseEntry.relationship_confirmation?.decision !== 'CONFIRM')
|
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 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()}` : ''
|
||||||
|
|
||||||
if (!repoLogin) {
|
if (!repoLogin) {
|
||||||
const resolvedDomain = zeroPoint?.resolvedDomain || ''
|
const resolvedDomain = zeroPoint?.resolvedDomain || ''
|
||||||
|
|
@ -1858,7 +1917,7 @@ function HoloLakeApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞'
|
const isZhizhi = repoLogin.domain === 'FIFTH_DOMAIN' && zeroPoint?.userNumber === 'ICE-GL-ZHI∞'
|
||||||
return <div className={`official-world signed-in-world${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
return <div className={`official-world signed-in-world${motionAwake ? ' motion-awake' : ''}${climateClasses}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||||
<LakeAtmosphere awake={motionAwake}/>
|
<LakeAtmosphere awake={motionAwake}/>
|
||||||
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions"><span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
||||||
<main className="world-scene signed-in-scene">
|
<main className="world-scene signed-in-scene">
|
||||||
|
|
|
||||||
|
|
@ -1168,6 +1168,14 @@ const ROUTES = {
|
||||||
"moduleNumber": "HLP-NIPC-MOD-0030",
|
"moduleNumber": "HLP-NIPC-MOD-0030",
|
||||||
"operationNumber": "HLP-NIPC-OP-0146",
|
"operationNumber": "HLP-NIPC-OP-0146",
|
||||||
"targetNumber": "HLP-NIPC-TGT-0030"
|
"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
|
} as const
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -563,6 +563,17 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
||||||
.world-star { position: absolute; border-radius: 50%; background: var(--primitive-star-core); animation: world-star-breathe var(--star-duration) ease-in-out var(--star-delay) infinite; }
|
.world-star { position: absolute; border-radius: 50%; background: var(--primitive-star-core); animation: world-star-breathe var(--star-duration) ease-in-out var(--star-delay) infinite; }
|
||||||
@keyframes world-star-breathe { 0%, 100% { opacity: .14; transform: scale(.8); } 50% { opacity: .86; transform: scale(1.15); } }
|
@keyframes world-star-breathe { 0%, 100% { opacity: .14; transform: scale(.8); } 50% { opacity: .86; transform: scale(1.15); } }
|
||||||
.world-mist { position: absolute; z-index: 1; left: 6%; right: 6%; top: 38%; height: 70px; filter: blur(12px); pointer-events: none; background: radial-gradient(60% 100% at 50% 50%, var(--primitive-mist-a), var(--primitive-mist-b) 55%, transparent 78%); }
|
.world-mist { position: absolute; z-index: 1; left: 6%; right: 6%; top: 38%; height: 70px; filter: blur(12px); pointer-events: none; background: radial-gradient(60% 100% at 50% 50%, var(--primitive-mist-a), var(--primitive-mist-b) 55%, transparent 78%); }
|
||||||
|
.world-climate-veil { position: absolute; z-index: 1; inset: 44px 0 45px; pointer-events: none; opacity: 0; transition: opacity 1.4s ease, background 1.4s ease; }
|
||||||
|
.official-world.phase-dawn .world-climate-veil { opacity: .36; background: radial-gradient(90% 58% at 28% 12%, rgba(255, 185, 136, .24), transparent 68%); }
|
||||||
|
.official-world.phase-day .world-climate-veil { opacity: .24; background: radial-gradient(100% 62% at 50% 0, rgba(214, 232, 255, .26), transparent 70%); }
|
||||||
|
.official-world.phase-dusk .world-climate-veil { opacity: .34; background: radial-gradient(90% 58% at 72% 16%, rgba(255, 146, 112, .22), transparent 68%); }
|
||||||
|
.official-world.phase-night .world-climate-veil { opacity: .2; background: radial-gradient(90% 58% at 52% 8%, rgba(80, 98, 176, .2), transparent 72%); }
|
||||||
|
.official-world.climate-cloud .world-climate-veil,
|
||||||
|
.official-world.climate-fog .world-climate-veil { opacity: .5; background: radial-gradient(82% 24% at 52% 42%, color-mix(in srgb, var(--primitive-mist-a) 78%, transparent), transparent 76%), radial-gradient(70% 22% at 28% 61%, color-mix(in srgb, var(--primitive-mist-b) 65%, transparent), transparent 78%); }
|
||||||
|
.official-world.climate-rain .world-climate-veil,
|
||||||
|
.official-world.climate-storm .world-climate-veil { opacity: .58; background: radial-gradient(95% 48% at 50% 12%, rgba(40, 54, 86, .34), transparent 74%), radial-gradient(70% 22% at 50% 64%, color-mix(in srgb, var(--primitive-cool-glow) 20%, transparent), transparent 75%); }
|
||||||
|
.official-world.climate-snow .world-climate-veil { opacity: .55; background: radial-gradient(circle at 18% 22%, rgba(255,255,255,.3) 0 1px, transparent 2px), radial-gradient(circle at 68% 34%, rgba(255,255,255,.24) 0 1px, transparent 2px), radial-gradient(90% 42% at 50% 24%, rgba(226,238,255,.18), transparent 74%); background-size: 82px 74px, 113px 96px, auto; }
|
||||||
|
.climate-panel a { color: var(--accent-light); font-weight: 650; }
|
||||||
.world-shimmer { position: absolute; z-index: 2; inset: 39% 0 42px; overflow: hidden; pointer-events: none; }
|
.world-shimmer { position: absolute; z-index: 2; inset: 39% 0 42px; overflow: hidden; pointer-events: none; }
|
||||||
.world-shine { position: absolute; width: 38%; height: 130px; border-radius: 50%; filter: blur(34px); animation: world-shine-glide 28s ease-in-out infinite alternate; }
|
.world-shine { position: absolute; width: 38%; height: 130px; border-radius: 50%; filter: blur(34px); animation: world-shine-glide 28s ease-in-out infinite alternate; }
|
||||||
.world-shine.cool { left: 6%; top: 12%; background: color-mix(in srgb, var(--primitive-cool-glow) 18%, transparent); }
|
.world-shine.cool { left: 6%; top: 12%; background: color-mix(in srgb, var(--primitive-cool-glow) 18%, transparent); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue