Host: 256 feature flags, the onboarding mode (mode_get/mode_set, shell-only), and the one-time move of replaced plugin files to feature flags

- settings: MAX_FEATURES 64 -> 256 (an older app's whole-map backup still validates)
- modes: ModeState {mode, onboarded, existing} in modes.json; existing is decided natively on the first start
- plugins::migrate: PluginHost.superseded (file -> flag); enabled and as shipped -> flag on and the file goes; edited -> kept, flag left off; marker written last, every step repeatable; map_backup for imported enabled lists
- a superseded file is never written again by install_bundled
- create-waybetter: the host command list and PluginHost.superseded
This commit is contained in:
CRTkafa 2026-09-30 18:34:13 +07:00
parent f31b00eec7
commit 365f55f604
9 changed files with 477 additions and 6 deletions

View file

@ -96,9 +96,16 @@ pub fn links_script(site: &SiteSpec, settings: &ShellSettings) -> String {
tabs::deliver_script(site.runtime_global, "links", "configure", "__btLinks", &links.to_string())
}
/// Flags a store may hold. It was 64 while every flag was a page tweak; area plugins carry a flag
/// per feature. The map is stored sparsely (only values that differ from the app's defaults), so a
/// backup written by this version normally still fits an older app's limit of 64.
pub const MAX_FEATURES: usize = 256;
/// Characters in a flag key.
pub const MAX_FEATURE_KEY: usize = 64;
/// Feature flags are booleans only, in a small map; the page normalizes them again before use.
pub fn validate_features(features: &Map<String, Value>) -> Result<(), String> {
if features.len() > 64 || features.iter().any(|(key, value)| key.len() > 64 || !value.is_boolean()) {
if features.len() > MAX_FEATURES || features.iter().any(|(key, value)| key.is_empty() || key.len() > MAX_FEATURE_KEY || !value.is_boolean()) {
return Err("VALIDATION: features must be a small map of booleans".into());
}
Ok(())
@ -175,7 +182,13 @@ mod tests {
assert!(validate_features(&map(json!({ "a": "yes" }))).is_err());
let long: Map<String, Value> = [("x".repeat(65), Value::Bool(true))].into_iter().collect();
assert!(validate_features(&long).is_err());
let many: Map<String, Value> = (0..65).map(|n| (format!("f{n}"), Value::Bool(true))).collect();
// What an older app wrote (at most 64 flags, the whole map) still imports; the cap is 256 now.
let old: Map<String, Value> = (0..64).map(|n| (format!("f{n}"), Value::Bool(n % 2 == 0))).collect();
assert!(validate_features(&old).is_ok());
let full: Map<String, Value> = (0..MAX_FEATURES).map(|n| (format!("area{n}.feature"), Value::Bool(true))).collect();
assert!(validate_features(&full).is_ok());
let many: Map<String, Value> = (0..=MAX_FEATURES).map(|n| (format!("f{n}"), Value::Bool(true))).collect();
assert!(validate_features(&many).is_err());
assert!(validate_features(&map(json!({ "": true }))).is_err());
}
}