From 32fa1a4952660ba6aea5e0c683321b34ad511f0b Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 11:26:34 +0200 Subject: [PATCH] feat(workout): include host result contract in mobile kit Record share/haptics and the denied/timeout/unavailable/error host result kinds in Android and iOS release metadata, and make mobile verification require them. req: examples/006 req: host/002 --- docs/recipes/mobile-release.md | 6 +++++- hemx-xtask/src/main.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/recipes/mobile-release.md b/docs/recipes/mobile-release.md index aad882c..de0824a 100644 --- a/docs/recipes/mobile-release.md +++ b/docs/recipes/mobile-release.md @@ -55,6 +55,9 @@ The generated manifest records: - cache policy: release-scoped HTML/CSS/runtime assets only; - offline truth policy: app-owned command/event/projection records, never DOM patches or UI effect payloads; +- host capability policy: Android and iOS shell metadata declare share/haptics and + the same denied, timeout, unavailable, and error result kinds handled by app + code before UI effects; - environment/secrets boundary: public shell config in the kit, signing secrets outside the repo; - rollback: redeploy the previous server binary and rebuild store artifacts from @@ -76,4 +79,5 @@ reviewed without guessing what is still external. req: examples/006 This is not a `hemx-mobile` framework, sync layer, client database, or native UI runtime. The mobile shells load the production Workout web app and route host capabilities such as share/haptics through the typed host boundary before UI -effects are produced. req: host/002 req: local/002 +effects are produced; denied, timeout, unavailable, and error cases share the +same host result shape. req: host/002 req: local/002 diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index 5f93a5d..cfe28dc 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -291,6 +291,10 @@ fn verify_workout_mobile_release( "rollback", "android/twa-release.json", "ios/webview-release.json", + "denied", + "timeout", + "unavailable", + "error", ], &mut failures, ); @@ -301,6 +305,12 @@ fn verify_workout_mobile_release( &config.app_name, origin_host(&config.origin), "external Android keystore", + "share", + "haptics", + "denied", + "timeout", + "unavailable", + "error", ], &mut failures, ); @@ -312,6 +322,10 @@ fn verify_workout_mobile_release( "share", "haptics", "external Apple team", + "denied", + "timeout", + "unavailable", + "error", ], &mut failures, ); @@ -331,7 +345,7 @@ fn check_file_contains(path: &Path, needles: &[&str], failures: &mut Vec fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> String { format!( - "{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"served by hemx_axum::runtime_js_path() from the same release\",\n \"cache_policy\": \"cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth\",\n \"state_policy\": \"app-owned command/event/projection records\",\n \"environment_boundary\": \"public mobile shell config lives here; secrets and signing credentials stay outside the repo\",\n \"rollback\": \"redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs\",\n \"android\": \"android/twa-release.json\",\n \"ios\": \"ios/webview-release.json\",\n \"external_blockers\": [{}]\n}}\n", + "{{\n \"app_id\": \"{}\",\n \"name\": \"{}\",\n \"version\": \"{}\",\n \"origin\": \"{}\",\n \"server_binary\": \"target/release/hemx-workout-example\",\n \"runtime_asset_path\": \"served by hemx_axum::runtime_js_path() from the same release\",\n \"cache_policy\": \"cache only release-scoped HTML/CSS/runtime assets; never store DOM patches or UI effects as truth\",\n \"state_policy\": \"app-owned command/event/projection records\",\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"environment_boundary\": \"public mobile shell config lives here; secrets and signing credentials stay outside the repo\",\n \"rollback\": \"redeploy the previous server binary and matching mobile shell metadata; rebuild store artifacts with the previous version/signing inputs\",\n \"android\": \"android/twa-release.json\",\n \"ios\": \"ios/webview-release.json\",\n \"external_blockers\": [{}]\n}}\n", json_escape(&config.app_id), json_escape(&config.app_name), json_escape(&config.version), @@ -342,7 +356,7 @@ fn workout_mobile_manifest(config: &WorkoutMobileConfig, blockers: &[String]) -> fn android_twa_release_json(config: &WorkoutMobileConfig) -> String { format!( - "{{\n \"package\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"host\": \"{}\",\n \"version\": \"{}\",\n \"signing\": \"external Android keystore; never commit credentials\"\n}}\n", + "{{\n \"package\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"host\": \"{}\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"signing\": \"external Android keystore; never commit credentials\"\n}}\n", json_escape(&config.android_package), json_escape(&config.app_name), json_escape(config.origin.trim_end_matches('/')), @@ -353,7 +367,7 @@ fn android_twa_release_json(config: &WorkoutMobileConfig) -> String { fn ios_webview_release_json(config: &WorkoutMobileConfig) -> String { format!( - "{{\n \"bundle_id\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"signing\": \"external Apple team/provisioning profile; never commit credentials\"\n}}\n", + "{{\n \"bundle_id\": \"{}\",\n \"name\": \"{}\",\n \"start_url\": \"{}/\",\n \"version\": \"{}\",\n \"host_capabilities\": [\"share\", \"haptics\"],\n \"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"],\n \"signing\": \"external Apple team/provisioning profile; never commit credentials\"\n}}\n", json_escape(&config.ios_bundle_id), json_escape(&config.app_name), json_escape(config.origin.trim_end_matches('/')), @@ -750,6 +764,9 @@ mod tests { assert!(manifest.contains("target/release/hemx-workout-example")); assert!(manifest.contains("hemx_axum::runtime_js_path()")); assert!(manifest.contains("app-owned command/event/projection records")); + assert!(manifest.contains( + "\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]" + )); assert!(manifest.contains("secrets and signing credentials stay outside the repo")); assert!(manifest.contains("Android signing key not configured")); } @@ -763,6 +780,10 @@ mod tests { assert_eq!(origin_host(&config.origin), "workout.example.com"); assert!(android.contains("\"start_url\": \"https://workout.example.com/app/\"")); assert!(android.contains("\"host\": \"workout.example.com\"")); + assert!(android.contains("\"host_capabilities\": [\"share\", \"haptics\"]")); + assert!(android.contains( + "\"host_result_kinds\": [\"denied\", \"timeout\", \"unavailable\", \"error\"]" + )); } #[test]